♻️ refactor: Update component imports and improve code consistency

- Refactored sidebar component to include additional sheet elements and improved accessibility with SheetHeader, SheetTitle, and SheetDescription.
- Updated skeleton component for better readability and consistency in class names.
- Refined slider component by standardizing import statements and enhancing class name formatting.
- Enhanced sonner component with consistent import statements and improved class name formatting.
- Standardized switch component imports and class names for better readability.
- Improved table component structure and class name consistency across various elements.
- Refactored tabs component for better readability and consistent class name formatting.
- Updated textarea component for improved readability and consistency in class names.
- Fixed timeline component to safely access getBoundingClientRect.
- Refactored toggle group and toggle components for improved readability and consistency in class names.
- Updated tooltip component for better readability and consistent class name formatting.
- Enhanced enhanced-input component to support generic types for better type safety.
- Refactored use-mobile hook for improved readability and consistency.
- Updated countries utility to make certain properties optional for better flexibility.
- Refined tailwind configuration for improved readability and consistency in theme settings.
This commit is contained in:
web 2025-09-02 06:00:28 -07:00
parent 6ccf9b8bdc
commit 59faeab34a
34 changed files with 366 additions and 252 deletions

View File

@ -34,8 +34,8 @@ import { z } from 'zod';
const deviceSchema = z.object({ const deviceSchema = z.object({
id: z.number(), id: z.number(),
method: z.string().default('device'), method: z.string(),
enabled: z.boolean().default(false), enabled: z.boolean(),
config: z config: z
.object({ .object({
show_ads: z.boolean().optional(), show_ads: z.boolean().optional(),

View File

@ -40,23 +40,23 @@ import { z } from 'zod';
const emailSettingsSchema = z.object({ const emailSettingsSchema = z.object({
id: z.number(), id: z.number(),
method: z.string().default('email'), method: z.string(),
enabled: z.boolean().default(false), enabled: z.boolean(),
config: z config: z
.object({ .object({
enable_verify: z.boolean().default(false), enable_verify: z.boolean(),
enable_domain_suffix: z.boolean().default(false), enable_domain_suffix: z.boolean(),
domain_suffix_list: z.string().optional(), domain_suffix_list: z.string().optional(),
verify_email_template: z.string().optional(), verify_email_template: z.string().optional(),
expiration_email_template: z.string().optional(), expiration_email_template: z.string().optional(),
maintenance_email_template: z.string().optional(), maintenance_email_template: z.string().optional(),
traffic_exceed_email_template: z.string().optional(), traffic_exceed_email_template: z.string().optional(),
platform: z.string().default('smtp'), platform: z.string(),
platform_config: z platform_config: z
.object({ .object({
host: z.string().optional(), host: z.string().optional(),
port: z.coerce.number().optional(), port: z.number().optional(),
ssl: z.boolean().default(false), ssl: z.boolean(),
user: z.string().optional(), user: z.string().optional(),
pass: z.string().optional(), pass: z.string().optional(),
from: z.string().optional(), from: z.string().optional(),

View File

@ -33,8 +33,8 @@ import { z } from 'zod';
const googleSchema = z.object({ const googleSchema = z.object({
id: z.number(), id: z.number(),
method: z.string().default('google'), method: z.string().default('google').optional(),
enabled: z.boolean().default(false), enabled: z.boolean().default(false).optional(),
config: z config: z
.object({ .object({
client_id: z.string().optional(), client_id: z.string().optional(),

View File

@ -49,8 +49,8 @@ import { z } from 'zod';
const phoneSettingsSchema = z.object({ const phoneSettingsSchema = z.object({
id: z.number(), id: z.number(),
method: z.string().default('mobile'), method: z.string(),
enabled: z.boolean().default(false), enabled: z.boolean(),
config: z config: z
.object({ .object({
enable_whitelist: z.boolean().optional(), enable_whitelist: z.boolean().optional(),

View File

@ -267,7 +267,7 @@ export default function CouponForm<T extends Record<string, any>>({
<DatePicker <DatePicker
placeholder={t('form.enterValue')} placeholder={t('form.enterValue')}
value={field.value} value={field.value}
disabled={(date) => date < new Date(Date.now() - 24 * 60 * 60 * 1000)} disabled={(date: Date) => date < new Date(Date.now() - 24 * 60 * 60 * 1000)}
onChange={(value) => { onChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
}} }}

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table'; import { ProTable } from '@/components/pro-table';
import { filterTrafficLogDetails } from '@/services/admin/log'; import { filterTrafficLogDetails } from '@/services/admin/log';
import { formatBytes, formatDate } from '@workspace/ui/utils'; import { formatBytes, formatDate } from '@workspace/ui/utils';
@ -22,7 +23,11 @@ export default function TrafficDetailsPage() {
initialFilters={initialFilters} initialFilters={initialFilters}
columns={[ columns={[
{ accessorKey: 'server_id', header: t('column.serverId') }, { accessorKey: 'server_id', header: t('column.serverId') },
{ accessorKey: 'user_id', header: t('column.userId') }, {
accessorKey: 'user_id',
header: t('column.userId'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'subscribe_id', header: t('column.subscribeId') }, { accessorKey: 'subscribe_id', header: t('column.subscribeId') },
{ {
accessorKey: 'upload', accessorKey: 'upload',

View File

@ -47,7 +47,7 @@ export default function EmailBroadcastForm() {
const emailBroadcastSchema = z.object({ const emailBroadcastSchema = z.object({
subject: z.string().min(1, t('subject') + ' ' + t('cannotBeEmpty')), subject: z.string().min(1, t('subject') + ' ' + t('cannotBeEmpty')),
content: z.string().min(1, t('content') + ' ' + t('cannotBeEmpty')), content: z.string().min(1, t('content') + ' ' + t('cannotBeEmpty')),
scope: z.string().default('all'), scope: z.string(),
register_start_time: z.string().optional(), register_start_time: z.string().optional(),
register_end_time: z.string().optional(), register_end_time: z.string().optional(),
additional: z additional: z

View File

@ -42,39 +42,31 @@ export type ProtocolName =
type ServerRow = API.Server; type ServerRow = API.Server;
export type NodeFormValues = { const buildSchema = (t: ReturnType<typeof useTranslations>) =>
name: string; z.object({
server_id?: number; name: z.string().trim().min(1, t('errors.nameRequired')),
protocol: ProtocolName | ''; server_id: z
address: string; .number({ message: t('errors.serverRequired') })
port: number; .int()
tags: string[]; .gt(0, t('errors.serverRequired'))
}; .optional(),
protocol: z.string().min(1, t('errors.protocolRequired')),
address: z.string().trim().min(1, t('errors.serverAddrRequired')),
port: z
.number({ message: t('errors.portRange') })
.int()
.min(1, t('errors.portRange'))
.max(65535, t('errors.portRange')),
tags: z.array(z.string()),
});
export type NodeFormValues = z.infer<ReturnType<typeof buildSchema>>;
async function getServers(): Promise<ServerRow[]> { async function getServers(): Promise<ServerRow[]> {
const { data } = await filterServerList({ page: 1, size: 1000 }); const { data } = await filterServerList({ page: 1, size: 1000 });
return (data?.data?.list || []) as ServerRow[]; return (data?.data?.list || []) as ServerRow[];
} }
const buildSchema = (t: ReturnType<typeof useTranslations>) =>
z.object({
name: z.string().trim().min(1, t('errors.nameRequired')),
server_id: z.coerce
.number({ invalid_type_error: t('errors.serverRequired') })
.int()
.gt(0, t('errors.serverRequired')),
protocol: z.custom<ProtocolName>((v) => typeof v === 'string' && v.length > 0, {
message: t('errors.protocolRequired'),
}),
address: z.string().trim().min(1, t('errors.serverAddrRequired')),
port: z.coerce
.number({ invalid_type_error: t('errors.portRange') })
.int()
.min(1, t('errors.portRange'))
.max(65535, t('errors.portRange')),
tags: z.array(z.string()).default([]),
});
export default function NodeForm(props: { export default function NodeForm(props: {
trigger: string; trigger: string;
title: string; title: string;

View File

@ -75,9 +75,9 @@ export default function PaymentForm<T>({
icon: z.string().optional(), icon: z.string().optional(),
domain: z.string().optional(), domain: z.string().optional(),
config: z.any(), config: z.any(),
fee_mode: z.coerce.number().min(0).max(2), fee_mode: z.number().min(0).max(2),
fee_percent: z.coerce.number().optional(), fee_percent: z.number().optional(),
fee_amount: z.coerce.number().optional(), fee_amount: z.number().optional(),
description: z.string().optional(), description: z.string().optional(),
}); });
@ -319,7 +319,7 @@ export default function PaymentForm<T>({
}} }}
defaultValue={field.value} defaultValue={field.value}
value={field.value} value={field.value}
// @ts-expect-error // @ts-expect-error - disabled prop type mismatch with SelectTrigger component
disabled={isEdit && Boolean(initialValues?.platform)} disabled={isEdit && Boolean(initialValues?.platform)}
> >
<FormControl> <FormControl>

View File

@ -87,7 +87,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
name: z.string(), name: z.string(),
description: z.string().optional(), description: z.string().optional(),
unit_price: z.number(), unit_price: z.number(),
unit_time: z.string().default('Month'), unit_time: z.string(),
replacement: z.number().optional(), replacement: z.number().optional(),
discount: z discount: z
.array( .array(
@ -97,22 +97,22 @@ export default function SubscribeForm<T extends Record<string, any>>({
}), }),
) )
.optional(), .optional(),
inventory: z.number().optional().default(-1), inventory: z.number().optional(),
speed_limit: z.number().optional().default(0), speed_limit: z.number().optional(),
device_limit: z.number().optional().default(0), device_limit: z.number().optional(),
traffic: z.number().optional().default(0), traffic: z.number().optional(),
quota: z.number().optional().default(0), quota: z.number().optional(),
group_id: z.number().optional().nullish(), group_id: z.number().optional().nullish(),
// Use tags as group identifiers; accept string (tag) or number (legacy id) // Use tags as group identifiers; accept string (tag) or number (legacy id)
node_tags: z.array(z.string()).optional().default([]), node_tags: z.array(z.string()).optional(),
nodes: z.array(z.number()).optional().default([]), nodes: z.array(z.number()).optional(),
deduction_ratio: z.number().optional().default(0), deduction_ratio: z.number().optional(),
allow_deduction: z.boolean().optional().default(false), allow_deduction: z.boolean().optional(),
reset_cycle: z.number().optional().default(0), reset_cycle: z.number().optional(),
renewal_reset: z.boolean().optional().default(false), renewal_reset: z.boolean().optional(),
}); });
const form = useForm({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: assign( defaultValues: assign(
defaultValues, defaultValues,
@ -204,7 +204,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
}); });
if (hasChanges) { if (hasChanges) {
form.setValue(fieldName, calculatedValues, { shouldDirty: true }); form.setValue(fieldName as any, calculatedValues, { shouldDirty: true });
} }
}, 300); }, 300);
}, },
@ -321,6 +321,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
<Combobox<number, false> <Combobox<number, false>
placeholder={t('form.selectSubscribeGroup')} placeholder={t('form.selectSubscribeGroup')}
{...field} {...field}
value={field.value ?? undefined}
onChange={(value) => { onChange={(value) => {
form.setValue(field.name, value || 0); form.setValue(field.name, value || 0);
}} }}
@ -665,7 +666,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
min: 0, min: 0,
step: 0.01, step: 0.01,
formatInput: (value) => unitConversion('centsToDollars', value), formatInput: (value) => unitConversion('centsToDollars', value),
formatOutput: (value) => unitConversion('dollarsToCents', value), formatOutput: (value) =>
unitConversion('dollarsToCents', value).toString(),
}, },
]} ]}
value={field.value} value={field.value}
@ -911,7 +913,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
const keys = Object.keys(errors); const keys = Object.keys(errors);
for (const key of keys) { for (const key of keys) {
const formattedKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); const formattedKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
toast.error(`${t(`form.${formattedKey}`)} is ${errors[key]?.message}`); const error = (errors as any)[key];
toast.error(`${t(`form.${formattedKey}`)} is ${error?.message}`);
return false; return false;
} }
})} })}

View File

@ -96,8 +96,8 @@ function getTimeRangeData(slots: API.TimePeriod[]) {
const nodeConfigSchema = z.object({ const nodeConfigSchema = z.object({
node_secret: z.string().optional(), node_secret: z.string().optional(),
node_pull_interval: z.number().or(z.string().pipe(z.coerce.number())).optional(), node_pull_interval: z.number().optional(),
node_push_interval: z.number().or(z.string().pipe(z.coerce.number())).optional(), node_push_interval: z.number().optional(),
}); });
type NodeConfigFormData = z.infer<typeof nodeConfigSchema>; type NodeConfigFormData = z.infer<typeof nodeConfigSchema>;

View File

@ -64,9 +64,9 @@ const createClientFormSchema = (t: any) =>
description: z.string().optional(), description: z.string().optional(),
icon: z.string().optional(), icon: z.string().optional(),
user_agent: z.string().min(1, `User-Agent ${t('form.validation.userAgentRequiredSuffix')}`), user_agent: z.string().min(1, `User-Agent ${t('form.validation.userAgentRequiredSuffix')}`),
scheme: z.string().default(''), scheme: z.string(),
template: z.string().default(''), template: z.string(),
output_format: z.string().default(''), output_format: z.string(),
download_link: z.object({ download_link: z.object({
windows: z.string().optional(), windows: z.string().optional(),
mac: z.string().optional(), mac: z.string().optional(),

View File

@ -142,7 +142,7 @@ export default function UserForm<T extends Record<string, any>>({
placeholder={t('areaCodePlaceholder')} placeholder={t('areaCodePlaceholder')}
value={field.value} value={field.value}
onChange={(value) => { onChange={(value) => {
form.setValue(field.name, value.phone); form.setValue(field.name, value.phone as string);
}} }}
/> />
</FormControl> </FormControl>

View File

@ -205,7 +205,7 @@ export function SubscriptionForm({ trigger, title, loading, initialData, onSubmi
<FormControl> <FormControl>
<DatePicker <DatePicker
placeholder={t('permanent')} placeholder={t('permanent')}
value={field.value} value={field.value ?? undefined}
onChange={(value) => { onChange={(value) => {
if (value === field.value) { if (value === field.value) {
form.setValue(field.name, 0); form.setValue(field.name, 0);

View File

@ -13,10 +13,10 @@ import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
const FormSchema = z.object({ const FormSchema = z.object({
enable_balance_notify: z.boolean().default(false), enable_balance_notify: z.boolean(),
enable_login_notify: z.boolean().default(false), enable_login_notify: z.boolean(),
enable_subscribe_notify: z.boolean().default(false), enable_subscribe_notify: z.boolean(),
enable_trade_notify: z.boolean().default(false), enable_trade_notify: z.boolean(),
}); });
export default function NotifySettings() { export default function NotifySettings() {

View File

@ -6,12 +6,6 @@ import useGlobalStore from '@/config/use-global';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Sidebar, SidebarContent } from '@workspace/ui/components/sidebar'; import { Sidebar, SidebarContent } from '@workspace/ui/components/sidebar';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@workspace/ui/components/tooltip';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { isBrowser } from '@workspace/ui/utils'; import { isBrowser } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
@ -54,25 +48,18 @@ export function SidebarRight({ ...props }: React.ComponentProps<typeof Sidebar>)
<Card> <Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 p-3 pb-2'> <CardHeader className='flex flex-row items-center justify-between space-y-0 p-3 pb-2'>
<CardTitle className='text-sm font-medium'>{t('inviteCode')}</CardTitle> <CardTitle className='text-sm font-medium'>{t('inviteCode')}</CardTitle>
<TooltipProvider> <CopyToClipboard
<Tooltip> text={`${isBrowser() && location?.origin}/auth?invite=${user?.refer_code}`}
<TooltipTrigger asChild> onCopy={(text, result) => {
<CopyToClipboard if (result) {
text={`${isBrowser() && location?.origin}/auth?invite=${user?.refer_code}`} toast.success(t('copySuccess'));
onCopy={(text, result) => { }
if (result) { }}
toast.success(t('copySuccess')); >
} <Button variant='ghost' className='size-5 p-0'>
}} <Icon icon='mdi:content-copy' className='text-primary text-2xl' />
> </Button>
<Button variant='ghost' className='size-5 p-0'> </CopyToClipboard>
<Icon icon='mdi:content-copy' className='text-primary text-2xl' />
</Button>
</CopyToClipboard>
</TooltipTrigger>
<TooltipContent>{t('copyInviteLink')}</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader> </CardHeader>
<CardContent className='truncate p-3 font-bold'>{user?.refer_code}</CardContent> <CardContent className='truncate p-3 font-bold'>{user?.refer_code}</CardContent>
</Card> </Card>

BIN
bun.lockb

Binary file not shown.

View File

@ -15,12 +15,12 @@
"./utils/*": "./src/utils/*.ts" "./utils/*": "./src/utils/*.ts"
}, },
"scripts": { "scripts": {
"lint": "eslint . --max-warnings 0" "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^5.2.1",
"@iconify-json/flagpack": "^1.2.2", "@iconify-json/flagpack": "^1.2.2",
"@iconify-json/logos": "^1.2.4", "@iconify-json/logos": "^1.2.4",
"@iconify-json/mdi": "^1.2.2", "@iconify-json/mdi": "^1.2.2",
@ -28,78 +28,78 @@
"@iconify-json/uil": "^1.2.3", "@iconify-json/uil": "^1.2.3",
"@iconify/react": "^5.2.0", "@iconify/react": "^5.2.0",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.4", "@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.5.2", "embla-carousel-react": "^8.6.0",
"framer-motion": "^11.18.1", "framer-motion": "^11.18.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.473.0", "lucide-react": "^0.542.0",
"mathjs": "^14.0.1", "mathjs": "^14.0.1",
"monaco-themes": "^0.4.6", "monaco-themes": "^0.4.6",
"motion": "^11.18.1", "motion": "^11.18.1",
"next-themes": "^0.4.4", "next-themes": "^0.4.6",
"react-day-picker": "8.10.1", "react-day-picker": "^9.9.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.62.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^3.0.5",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"recharts": "^2.15.0", "recharts": "2.15.4",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"rtl-detect": "^1.1.2", "rtl-detect": "^1.1.2",
"sonner": "^1.7.2", "sonner": "^2.0.7",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.1" "zod": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@turbo/gen": "^2.3.3", "@turbo/gen": "^2.5.6",
"@types/node": "^22.10.5", "@types/node": "^24.3.0",
"@types/react": "^19.0.4", "@types/react": "^19.1.12",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.1.9",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/rtl-detect": "^1.0.3", "@types/rtl-detect": "^1.0.3",
"@workspace/eslint-config": "workspace:*", "@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*", "@workspace/typescript-config": "workspace:*",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.21",
"postcss": "^8.4.49", "postcss": "^8.5.6",
"react": "^19.0.0", "react": "^19.1.1",
"react-dom": "^19.0.0", "react-dom": "^19.1.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3" "typescript": "^5.9.2"
} }
} }

View File

@ -1,69 +1,178 @@
'use client'; 'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react'; import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { DayPicker } from 'react-day-picker'; import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker';
import { buttonVariants } from '@workspace/ui/components/button'; import { Button, buttonVariants } from '@workspace/ui/components/button';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/utils';
export type CalendarProps = React.ComponentProps<typeof DayPicker>; export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = 'label',
buttonVariant = 'ghost',
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>['variant'];
}) {
const defaultClassNames = getDefaultClassNames();
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn('p-3', className)} className={cn(
'bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className,
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
...formatters,
}}
classNames={{ classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', root: cn('w-fit', defaultClassNames.root),
month: 'space-y-4', months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
caption: 'flex justify-center pt-1 relative items-center', month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
caption_label: 'text-sm font-medium', nav: cn(
nav: 'space-x-1 flex items-center', 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
nav_button: cn( defaultClassNames.nav,
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
), ),
nav_button_previous: 'absolute left-1', button_previous: cn(
nav_button_next: 'absolute right-1', buttonVariants({ variant: buttonVariant }),
table: 'w-full border-collapse space-y-1', 'h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50',
head_row: 'flex', defaultClassNames.button_previous,
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]', ),
row: 'flex w-full mt-2', button_next: cn(
cell: cn( buttonVariants({ variant: buttonVariant }),
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md', 'h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50',
props.mode === 'range' defaultClassNames.button_next,
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md' ),
: '[&:has([aria-selected])]:rounded-md', month_caption: cn(
'flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]',
defaultClassNames.month_caption,
),
dropdowns: cn(
'flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium',
defaultClassNames.dropdowns,
),
dropdown_root: cn(
'has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border',
defaultClassNames.dropdown_root,
),
dropdown: cn('bg-popover absolute inset-0 opacity-0', defaultClassNames.dropdown),
caption_label: cn(
'select-none font-medium',
captionLayout === 'label'
? 'text-sm'
: '[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5',
defaultClassNames.caption_label,
),
table: 'w-full border-collapse',
weekdays: cn('flex', defaultClassNames.weekdays),
weekday: cn(
'text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal',
defaultClassNames.weekday,
),
week: cn('mt-2 flex w-full', defaultClassNames.week),
week_number_header: cn('w-[--cell-size] select-none', defaultClassNames.week_number_header),
week_number: cn(
'text-muted-foreground select-none text-[0.8rem]',
defaultClassNames.week_number,
), ),
day: cn( day: cn(
buttonVariants({ variant: 'ghost' }), 'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md',
'h-8 w-8 p-0 font-normal aria-selected:opacity-100', defaultClassNames.day,
), ),
day_range_start: 'day-range-start', range_start: cn('bg-accent rounded-l-md', defaultClassNames.range_start),
day_range_end: 'day-range-end', range_middle: cn('rounded-none', defaultClassNames.range_middle),
day_selected: range_end: cn('bg-accent rounded-r-md', defaultClassNames.range_end),
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', today: cn(
day_today: 'bg-accent text-accent-foreground', 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
day_outside: defaultClassNames.today,
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground', ),
day_disabled: 'text-muted-foreground opacity-50', outside: cn(
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground', 'text-muted-foreground aria-selected:text-muted-foreground',
day_hidden: 'invisible', defaultClassNames.outside,
),
disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
hidden: cn('invisible', defaultClassNames.hidden),
...classNames, ...classNames,
}} }}
components={{ components={{
IconLeft: ({ className, ...props }) => ( Root: ({ className, rootRef, ...props }) => {
<ChevronLeft className={cn('h-4 w-4', className)} {...props} /> return <div data-slot='calendar' ref={rootRef} className={cn(className)} {...props} />;
), },
IconRight: ({ className, ...props }) => ( Chevron: ({ className, orientation, ...props }) => {
<ChevronRight className={cn('h-4 w-4', className)} {...props} /> if (orientation === 'left') {
), return <ChevronLeftIcon className={cn('size-4', className)} {...props} />;
}
if (orientation === 'right') {
return <ChevronRightIcon className={cn('size-4', className)} {...props} />;
}
return <ChevronDownIcon className={cn('size-4', className)} {...props} />;
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className='flex size-[--cell-size] items-center justify-center text-center'>
{children}
</div>
</td>
);
},
...components,
}} }}
{...props} {...props}
/> />
); );
} }
Calendar.displayName = 'Calendar';
export { Calendar }; function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
variant='ghost'
size='icon'
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70',
defaultClassNames.day,
className,
)}
{...props}
/>
);
}
export { Calendar, CalendarDayButton };

View File

@ -39,7 +39,6 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
// eslint-disable-next-line react/no-unknown-property
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''> <div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
<Search className='mr-2 h-4 w-4 shrink-0 opacity-50' /> <Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
<CommandPrimitive.Input <CommandPrimitive.Input

View File

@ -46,7 +46,7 @@ const ContextMenuSubContent = React.forwardRef<
<ContextMenuPrimitive.SubContent <ContextMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-hidden rounded-md border p-1 shadow-lg',
className, className,
)} )}
{...props} {...props}
@ -62,7 +62,7 @@ const ContextMenuContent = React.forwardRef<
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
className, className,
)} )}
{...props} {...props}

View File

@ -13,13 +13,13 @@ const Drawer = ({
); );
Drawer.displayName = 'Drawer'; Drawer.displayName = 'Drawer';
const DrawerTrigger = DrawerPrimitive.Trigger; const DrawerTrigger: typeof DrawerPrimitive.Trigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal; const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close; const DrawerClose: typeof DrawerPrimitive.Close = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef< const DrawerOverlay: typeof DrawerPrimitive.Overlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
@ -31,7 +31,7 @@ const DrawerOverlay = React.forwardRef<
)); ));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef< const DrawerContent: typeof DrawerPrimitive.Content = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
@ -62,7 +62,7 @@ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
); );
DrawerFooter.displayName = 'DrawerFooter'; DrawerFooter.displayName = 'DrawerFooter';
const DrawerTitle = React.forwardRef< const DrawerTitle: typeof DrawerPrimitive.Title = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
@ -74,7 +74,7 @@ const DrawerTitle = React.forwardRef<
)); ));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName; DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef< const DrawerDescription: typeof DrawerPrimitive.Description = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (

View File

@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden rounded-md border p-1 shadow-lg',
className, className,
)} )}
{...props} {...props}
@ -63,8 +63,8 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md', 'bg-popover text-popover-foreground z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
className, className,
)} )}
{...props} {...props}

View File

@ -5,11 +5,11 @@ import { Slot } from '@radix-ui/react-slot';
import * as React from 'react'; import * as React from 'react';
import { import {
Controller, Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider, FormProvider,
useFormContext, useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
} from 'react-hook-form'; } from 'react-hook-form';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
@ -138,7 +138,7 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField(); const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children; const body = error ? String(error?.message ?? '') : children;
if (!body) { if (!body) {
return null; return null;

View File

@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef<
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-none', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-[--radix-hover-card-content-transform-origin] rounded-md border p-4 shadow-md outline-none',
className, className,
)} )}
{...props} {...props}

View File

@ -6,15 +6,25 @@ import * as React from 'react';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/utils';
const MenubarMenu = MenubarPrimitive.Menu; function MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu {...props} />;
}
const MenubarGroup = MenubarPrimitive.Group; function MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group {...props} />;
}
const MenubarPortal = MenubarPrimitive.Portal; function MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal {...props} />;
}
const MenubarSub = MenubarPrimitive.Sub; function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return <MenubarPrimitive.RadioGroup {...props} />;
}
const MenubarRadioGroup = MenubarPrimitive.RadioGroup; function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot='menubar-sub' {...props} />;
}
const Menubar = React.forwardRef< const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>, React.ElementRef<typeof MenubarPrimitive.Root>,
@ -74,7 +84,7 @@ const MenubarSubContent = React.forwardRef<
<MenubarPrimitive.SubContent <MenubarPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-menubar-content-transform-origin] overflow-hidden rounded-md border p-1 shadow-lg',
className, className,
)} )}
{...props} {...props}
@ -93,7 +103,7 @@ const MenubarContent = React.forwardRef<
alignOffset={alignOffset} alignOffset={alignOffset}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] overflow-hidden rounded-md border p-1 shadow-md', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-[--radix-menubar-content-transform-origin] overflow-hidden rounded-md border p-1 shadow-md',
className, className,
)} )}
{...props} {...props}

View File

@ -35,7 +35,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item; const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50', 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent',
); );
const NavigationMenuTrigger = React.forwardRef< const NavigationMenuTrigger = React.forwardRef<

View File

@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef<
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-[--radix-popover-content-transform-origin] rounded-md border p-4 shadow-md outline-none',
className, className,
)} )}
{...props} {...props}

View File

@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', 'border-input ring-offset-background data-[placeholder]:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className, className,
)} )}
{...props} {...props}
@ -68,7 +68,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] origin-[--radix-select-content-transform-origin] overflow-y-auto overflow-x-hidden rounded-md border shadow-md',
position === 'popper' && position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className, className,

View File

@ -8,7 +8,13 @@ import * as React from 'react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Input } from '@workspace/ui/components/input'; import { Input } from '@workspace/ui/components/input';
import { Separator } from '@workspace/ui/components/separator'; import { Separator } from '@workspace/ui/components/separator';
import { Sheet, SheetContent } from '@workspace/ui/components/sheet'; import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@workspace/ui/components/sheet';
import { Skeleton } from '@workspace/ui/components/skeleton'; import { Skeleton } from '@workspace/ui/components/skeleton';
import { import {
Tooltip, Tooltip,
@ -19,14 +25,14 @@ import {
import { useIsMobile } from '@workspace/ui/hooks/use-mobile'; import { useIsMobile } from '@workspace/ui/hooks/use-mobile';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/utils';
const SIDEBAR_COOKIE_NAME = 'sidebar:state'; const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem'; const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem'; const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem'; const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = { type SidebarContextProps = {
state: 'expanded' | 'collapsed'; state: 'expanded' | 'collapsed';
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
@ -36,7 +42,7 @@ type SidebarContext = {
toggleSidebar: () => void; toggleSidebar: () => void;
}; };
const SidebarContext = React.createContext<SidebarContext | null>(null); const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() { function useSidebar() {
const context = React.useContext(SidebarContext); const context = React.useContext(SidebarContext);
@ -111,7 +117,7 @@ const SidebarProvider = React.forwardRef<
// This makes it easier to style the sidebar with Tailwind classes. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed'; const state = open ? 'expanded' : 'collapsed';
const contextValue = React.useMemo<SidebarContext>( const contextValue = React.useMemo<SidebarContextProps>(
() => ({ () => ({
state, state,
open, open,
@ -201,6 +207,10 @@ const Sidebar = React.forwardRef<
} }
side={side} side={side}
> >
<SheetHeader className='sr-only'>
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className='flex h-full w-full flex-col'>{children}</div> <div className='flex h-full w-full flex-col'>{children}</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
@ -219,7 +229,7 @@ const Sidebar = React.forwardRef<
{/* This is what handles the sidebar gap on desktop */} {/* This is what handles the sidebar gap on desktop */}
<div <div
className={cn( className={cn(
'relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear', 'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0', 'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180', 'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset' variant === 'floating' || variant === 'inset'
@ -314,8 +324,8 @@ const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main
<main <main
ref={ref} ref={ref}
className={cn( className={cn(
'bg-background relative flex min-h-svh flex-1 flex-col', 'bg-background relative flex w-full flex-1 flex-col',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow', 'md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
className, className,
)} )}
{...props} {...props}
@ -428,7 +438,7 @@ const SidebarGroupLabel = React.forwardRef<
ref={ref} ref={ref}
data-sidebar='group-label' data-sidebar='group-label'
className={cn( className={cn(
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className, className,
)} )}

View File

@ -14,7 +14,7 @@ export const Timeline = ({ data }: { data: TimelineEntry[] }) => {
useEffect(() => { useEffect(() => {
if (ref.current) { if (ref.current) {
const rect = ref.current.getBoundingClientRect(); const rect = ref.current?.getBoundingClientRect?.();
setHeight(rect.height); setHeight(rect.height);
} }
}, [ref]); }, [ref]);

View File

@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs', 'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md px-3 py-1.5 text-xs',
className, className,
)} )}
{...props} {...props}

View File

@ -2,19 +2,20 @@ import { Input } from '@workspace/ui/components/input';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/utils';
import { ChangeEvent, ReactNode, useEffect, useState } from 'react'; import { ChangeEvent, ReactNode, useEffect, useState } from 'react';
export interface EnhancedInputProps export interface EnhancedInputProps<T = string>
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> { extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix' | 'value' | 'onChange'> {
prefix?: string | ReactNode; prefix?: string | ReactNode;
suffix?: string | ReactNode; suffix?: string | ReactNode;
formatInput?: (value: string | number) => string | number; value?: T;
formatOutput?: (value: string | number) => string | number; formatInput?: (value: T) => string | number;
onValueChange?: (value: string | number) => void; formatOutput?: (value: string | number) => T;
onValueBlur?: (value: string | number) => void; onValueChange?: (value: T) => void;
onValueBlur?: (value: T) => void;
min?: number; min?: number;
max?: number; max?: number;
} }
export function EnhancedInput({ export function EnhancedInput<T = string>({
suffix, suffix,
prefix, prefix,
formatInput, formatInput,
@ -24,38 +25,36 @@ export function EnhancedInput({
onValueChange, onValueChange,
onValueBlur, onValueBlur,
...props ...props
}: EnhancedInputProps) { }: EnhancedInputProps<T>) {
const getProcessedValue = (inputValue: unknown) => { const getProcessedValue = (inputValue: unknown) => {
if (inputValue === '' || inputValue === 0 || inputValue === '0') return ''; if (inputValue === '' || inputValue === 0 || inputValue === '0') return '';
const newValue = String(inputValue ?? ''); const newValue = String(inputValue ?? '');
return formatInput ? formatInput(newValue) : newValue; return formatInput ? formatInput(inputValue as T) : newValue;
}; };
const [value, setValue] = useState<string | number>(() => getProcessedValue(initialValue)); const [value, setValue] = useState<string | number>(() => getProcessedValue(initialValue));
// @ts-expect-error - This is a controlled component const [internalValue, setInternalValue] = useState<T | string | number>(initialValue ?? '');
const [internalValue, setInternalValue] = useState<string | number>(initialValue ?? '');
useEffect(() => { useEffect(() => {
if (initialValue !== internalValue) { if (initialValue !== internalValue) {
const newValue = getProcessedValue(initialValue); const newValue = getProcessedValue(initialValue);
if (value !== newValue) { if (value !== newValue) {
setValue(newValue); setValue(newValue);
// @ts-expect-error - This is a controlled component
setInternalValue(initialValue ?? ''); setInternalValue(initialValue ?? '');
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValue, formatInput]); }, [initialValue, formatInput]);
const processValue = (inputValue: string | number) => { const processValue = (inputValue: string | number): T => {
let processedValue: number | string = inputValue?.toString().trim(); let processedValue: number | string = inputValue?.toString().trim();
if (processedValue === '0' && props.type === 'number') { if (processedValue === '0' && props.type === 'number') {
return 0; return (formatOutput ? formatOutput(0) : 0) as T;
} }
if (processedValue && props.type === 'number') processedValue = Number(processedValue); if (processedValue && props.type === 'number') processedValue = Number(processedValue);
return formatOutput ? formatOutput(processedValue) : processedValue; return formatOutput ? formatOutput(processedValue) : (processedValue as T);
}; };
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
@ -65,7 +64,7 @@ export function EnhancedInput({
if (inputValue === '0') { if (inputValue === '0') {
setValue(''); setValue('');
setInternalValue(0); setInternalValue(0);
onValueChange?.(0); onValueChange?.(processValue(0));
return; return;
} }
@ -96,14 +95,14 @@ export function EnhancedInput({
if (value === '-' || value === '.') { if (value === '-' || value === '.') {
setValue(''); setValue('');
setInternalValue(''); setInternalValue('');
onValueBlur?.(''); onValueBlur?.('' as T);
return; return;
} }
// 确保0值显示为空 // 确保0值显示为空
if (value === '0') { if (value === '0') {
setValue(''); setValue('');
onValueBlur?.(0); onValueBlur?.(processValue(0));
return; return;
} }
} }

View File

@ -1,10 +1,10 @@
export interface ICountry { export interface ICountry {
name: string; name: string;
alpha2: string; alpha2: string;
alpha3: string | null; alpha3?: string | null;
numeric: string | null; numeric?: string | null;
phone: string | null; phone?: string | null;
lang: string | null; lang?: string | null;
langs: string[]; langs: string[];
} }