♻️ 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:
parent
6ccf9b8bdc
commit
59faeab34a
@ -34,8 +34,8 @@ import { z } from 'zod';
|
||||
|
||||
const deviceSchema = z.object({
|
||||
id: z.number(),
|
||||
method: z.string().default('device'),
|
||||
enabled: z.boolean().default(false),
|
||||
method: z.string(),
|
||||
enabled: z.boolean(),
|
||||
config: z
|
||||
.object({
|
||||
show_ads: z.boolean().optional(),
|
||||
|
||||
@ -40,23 +40,23 @@ import { z } from 'zod';
|
||||
|
||||
const emailSettingsSchema = z.object({
|
||||
id: z.number(),
|
||||
method: z.string().default('email'),
|
||||
enabled: z.boolean().default(false),
|
||||
method: z.string(),
|
||||
enabled: z.boolean(),
|
||||
config: z
|
||||
.object({
|
||||
enable_verify: z.boolean().default(false),
|
||||
enable_domain_suffix: z.boolean().default(false),
|
||||
enable_verify: z.boolean(),
|
||||
enable_domain_suffix: z.boolean(),
|
||||
domain_suffix_list: z.string().optional(),
|
||||
verify_email_template: z.string().optional(),
|
||||
expiration_email_template: z.string().optional(),
|
||||
maintenance_email_template: z.string().optional(),
|
||||
traffic_exceed_email_template: z.string().optional(),
|
||||
platform: z.string().default('smtp'),
|
||||
platform: z.string(),
|
||||
platform_config: z
|
||||
.object({
|
||||
host: z.string().optional(),
|
||||
port: z.coerce.number().optional(),
|
||||
ssl: z.boolean().default(false),
|
||||
port: z.number().optional(),
|
||||
ssl: z.boolean(),
|
||||
user: z.string().optional(),
|
||||
pass: z.string().optional(),
|
||||
from: z.string().optional(),
|
||||
|
||||
@ -33,8 +33,8 @@ import { z } from 'zod';
|
||||
|
||||
const googleSchema = z.object({
|
||||
id: z.number(),
|
||||
method: z.string().default('google'),
|
||||
enabled: z.boolean().default(false),
|
||||
method: z.string().default('google').optional(),
|
||||
enabled: z.boolean().default(false).optional(),
|
||||
config: z
|
||||
.object({
|
||||
client_id: z.string().optional(),
|
||||
|
||||
@ -49,8 +49,8 @@ import { z } from 'zod';
|
||||
|
||||
const phoneSettingsSchema = z.object({
|
||||
id: z.number(),
|
||||
method: z.string().default('mobile'),
|
||||
enabled: z.boolean().default(false),
|
||||
method: z.string(),
|
||||
enabled: z.boolean(),
|
||||
config: z
|
||||
.object({
|
||||
enable_whitelist: z.boolean().optional(),
|
||||
|
||||
@ -267,7 +267,7 @@ export default function CouponForm<T extends Record<string, any>>({
|
||||
<DatePicker
|
||||
placeholder={t('form.enterValue')}
|
||||
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) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { UserDetail } from '@/app/dashboard/user/user-detail';
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import { filterTrafficLogDetails } from '@/services/admin/log';
|
||||
import { formatBytes, formatDate } from '@workspace/ui/utils';
|
||||
@ -22,7 +23,11 @@ export default function TrafficDetailsPage() {
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{ 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: 'upload',
|
||||
|
||||
@ -47,7 +47,7 @@ export default function EmailBroadcastForm() {
|
||||
const emailBroadcastSchema = z.object({
|
||||
subject: z.string().min(1, t('subject') + ' ' + 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_end_time: z.string().optional(),
|
||||
additional: z
|
||||
|
||||
@ -42,39 +42,31 @@ export type ProtocolName =
|
||||
|
||||
type ServerRow = API.Server;
|
||||
|
||||
export type NodeFormValues = {
|
||||
name: string;
|
||||
server_id?: number;
|
||||
protocol: ProtocolName | '';
|
||||
address: string;
|
||||
port: number;
|
||||
tags: string[];
|
||||
};
|
||||
const buildSchema = (t: ReturnType<typeof useTranslations>) =>
|
||||
z.object({
|
||||
name: z.string().trim().min(1, t('errors.nameRequired')),
|
||||
server_id: z
|
||||
.number({ message: t('errors.serverRequired') })
|
||||
.int()
|
||||
.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[]> {
|
||||
const { data } = await filterServerList({ page: 1, size: 1000 });
|
||||
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: {
|
||||
trigger: string;
|
||||
title: string;
|
||||
|
||||
@ -75,9 +75,9 @@ export default function PaymentForm<T>({
|
||||
icon: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
config: z.any(),
|
||||
fee_mode: z.coerce.number().min(0).max(2),
|
||||
fee_percent: z.coerce.number().optional(),
|
||||
fee_amount: z.coerce.number().optional(),
|
||||
fee_mode: z.number().min(0).max(2),
|
||||
fee_percent: z.number().optional(),
|
||||
fee_amount: z.number().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
@ -319,7 +319,7 @@ export default function PaymentForm<T>({
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error - disabled prop type mismatch with SelectTrigger component
|
||||
disabled={isEdit && Boolean(initialValues?.platform)}
|
||||
>
|
||||
<FormControl>
|
||||
|
||||
@ -87,7 +87,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
unit_price: z.number(),
|
||||
unit_time: z.string().default('Month'),
|
||||
unit_time: z.string(),
|
||||
replacement: z.number().optional(),
|
||||
discount: z
|
||||
.array(
|
||||
@ -97,22 +97,22 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
inventory: z.number().optional().default(-1),
|
||||
speed_limit: z.number().optional().default(0),
|
||||
device_limit: z.number().optional().default(0),
|
||||
traffic: z.number().optional().default(0),
|
||||
quota: z.number().optional().default(0),
|
||||
inventory: z.number().optional(),
|
||||
speed_limit: z.number().optional(),
|
||||
device_limit: z.number().optional(),
|
||||
traffic: z.number().optional(),
|
||||
quota: z.number().optional(),
|
||||
group_id: z.number().optional().nullish(),
|
||||
// Use tags as group identifiers; accept string (tag) or number (legacy id)
|
||||
node_tags: z.array(z.string()).optional().default([]),
|
||||
nodes: z.array(z.number()).optional().default([]),
|
||||
deduction_ratio: z.number().optional().default(0),
|
||||
allow_deduction: z.boolean().optional().default(false),
|
||||
reset_cycle: z.number().optional().default(0),
|
||||
renewal_reset: z.boolean().optional().default(false),
|
||||
node_tags: z.array(z.string()).optional(),
|
||||
nodes: z.array(z.number()).optional(),
|
||||
deduction_ratio: z.number().optional(),
|
||||
allow_deduction: z.boolean().optional(),
|
||||
reset_cycle: z.number().optional(),
|
||||
renewal_reset: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: assign(
|
||||
defaultValues,
|
||||
@ -204,7 +204,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
form.setValue(fieldName, calculatedValues, { shouldDirty: true });
|
||||
form.setValue(fieldName as any, calculatedValues, { shouldDirty: true });
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
@ -321,6 +321,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
<Combobox<number, false>
|
||||
placeholder={t('form.selectSubscribeGroup')}
|
||||
{...field}
|
||||
value={field.value ?? undefined}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value || 0);
|
||||
}}
|
||||
@ -665,7 +666,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
formatInput: (value) => unitConversion('centsToDollars', value),
|
||||
formatOutput: (value) => unitConversion('dollarsToCents', value),
|
||||
formatOutput: (value) =>
|
||||
unitConversion('dollarsToCents', value).toString(),
|
||||
},
|
||||
]}
|
||||
value={field.value}
|
||||
@ -911,7 +913,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
const keys = Object.keys(errors);
|
||||
for (const key of keys) {
|
||||
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;
|
||||
}
|
||||
})}
|
||||
|
||||
@ -96,8 +96,8 @@ function getTimeRangeData(slots: API.TimePeriod[]) {
|
||||
|
||||
const nodeConfigSchema = z.object({
|
||||
node_secret: z.string().optional(),
|
||||
node_pull_interval: z.number().or(z.string().pipe(z.coerce.number())).optional(),
|
||||
node_push_interval: z.number().or(z.string().pipe(z.coerce.number())).optional(),
|
||||
node_pull_interval: z.number().optional(),
|
||||
node_push_interval: z.number().optional(),
|
||||
});
|
||||
type NodeConfigFormData = z.infer<typeof nodeConfigSchema>;
|
||||
|
||||
|
||||
@ -64,9 +64,9 @@ const createClientFormSchema = (t: any) =>
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
user_agent: z.string().min(1, `User-Agent ${t('form.validation.userAgentRequiredSuffix')}`),
|
||||
scheme: z.string().default(''),
|
||||
template: z.string().default(''),
|
||||
output_format: z.string().default(''),
|
||||
scheme: z.string(),
|
||||
template: z.string(),
|
||||
output_format: z.string(),
|
||||
download_link: z.object({
|
||||
windows: z.string().optional(),
|
||||
mac: z.string().optional(),
|
||||
|
||||
@ -142,7 +142,7 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
placeholder={t('areaCodePlaceholder')}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value.phone);
|
||||
form.setValue(field.name, value.phone as string);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -205,7 +205,7 @@ export function SubscriptionForm({ trigger, title, loading, initialData, onSubmi
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
placeholder={t('permanent')}
|
||||
value={field.value}
|
||||
value={field.value ?? undefined}
|
||||
onChange={(value) => {
|
||||
if (value === field.value) {
|
||||
form.setValue(field.name, 0);
|
||||
|
||||
@ -13,10 +13,10 @@ import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const FormSchema = z.object({
|
||||
enable_balance_notify: z.boolean().default(false),
|
||||
enable_login_notify: z.boolean().default(false),
|
||||
enable_subscribe_notify: z.boolean().default(false),
|
||||
enable_trade_notify: z.boolean().default(false),
|
||||
enable_balance_notify: z.boolean(),
|
||||
enable_login_notify: z.boolean(),
|
||||
enable_subscribe_notify: z.boolean(),
|
||||
enable_trade_notify: z.boolean(),
|
||||
});
|
||||
|
||||
export default function NotifySettings() {
|
||||
|
||||
@ -6,12 +6,6 @@ import useGlobalStore from '@/config/use-global';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
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 { isBrowser } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
@ -54,25 +48,18 @@ export function SidebarRight({ ...props }: React.ComponentProps<typeof Sidebar>)
|
||||
<Card>
|
||||
<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>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CopyToClipboard
|
||||
text={`${isBrowser() && location?.origin}/auth?invite=${user?.refer_code}`}
|
||||
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>
|
||||
</CopyToClipboard>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('copyInviteLink')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<CopyToClipboard
|
||||
text={`${isBrowser() && location?.origin}/auth?invite=${user?.refer_code}`}
|
||||
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>
|
||||
</CopyToClipboard>
|
||||
</CardHeader>
|
||||
<CardContent className='truncate p-3 font-bold'>{user?.refer_code}</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
"./utils/*": "./src/utils/*.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --max-warnings 0"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@iconify-json/flagpack": "^1.2.2",
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/mdi": "^1.2.2",
|
||||
@ -28,78 +28,78 @@
|
||||
"@iconify-json/uil": "^1.2.3",
|
||||
"@iconify/react": "^5.2.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-menubar": "^1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^11.18.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.473.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"mathjs": "^14.0.1",
|
||||
"monaco-themes": "^0.4.6",
|
||||
"motion": "^11.18.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-day-picker": "^9.9.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-resizable-panels": "^3.0.5",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"recharts": "^2.15.0",
|
||||
"recharts": "2.15.4",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"rtl-detect": "^1.1.2",
|
||||
"sonner": "^1.7.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbo/gen": "^2.3.3",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^19.0.4",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@turbo/gen": "^2.5.6",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/rtl-detect": "^1.0.3",
|
||||
"@workspace/eslint-config": "workspace:*",
|
||||
"@workspace/typescript-config": "workspace:*",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,69 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-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';
|
||||
|
||||
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 (
|
||||
<DayPicker
|
||||
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={{
|
||||
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
||||
month: 'space-y-4',
|
||||
caption: 'flex justify-center pt-1 relative items-center',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'space-x-1 flex items-center',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
root: cn('w-fit', defaultClassNames.root),
|
||||
months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
|
||||
month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
|
||||
nav: cn(
|
||||
'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
|
||||
defaultClassNames.nav,
|
||||
),
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
table: 'w-full border-collapse space-y-1',
|
||||
head_row: 'flex',
|
||||
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: cn(
|
||||
'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',
|
||||
props.mode === 'range'
|
||||
? '[&: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',
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
'h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50',
|
||||
defaultClassNames.button_previous,
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
'h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50',
|
||||
defaultClassNames.button_next,
|
||||
),
|
||||
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(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-8 w-8 p-0 font-normal aria-selected:opacity-100',
|
||||
'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',
|
||||
defaultClassNames.day,
|
||||
),
|
||||
day_range_start: 'day-range-start',
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||
day_today: 'bg-accent text-accent-foreground',
|
||||
day_outside:
|
||||
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
|
||||
day_disabled: 'text-muted-foreground opacity-50',
|
||||
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
day_hidden: 'invisible',
|
||||
range_start: cn('bg-accent rounded-l-md', defaultClassNames.range_start),
|
||||
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
||||
range_end: cn('bg-accent rounded-r-md', defaultClassNames.range_end),
|
||||
today: cn(
|
||||
'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
|
||||
defaultClassNames.today,
|
||||
),
|
||||
outside: cn(
|
||||
'text-muted-foreground aria-selected:text-muted-foreground',
|
||||
defaultClassNames.outside,
|
||||
),
|
||||
disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
|
||||
hidden: cn('invisible', defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn('h-4 w-4', className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn('h-4 w-4', className)} {...props} />
|
||||
),
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return <div data-slot='calendar' ref={rootRef} className={cn(className)} {...props} />;
|
||||
},
|
||||
Chevron: ({ className, orientation, ...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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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 };
|
||||
|
||||
@ -39,7 +39,6 @@ const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
|
||||
<Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
<CommandPrimitive.Input
|
||||
|
||||
@ -46,7 +46,7 @@ const ContextMenuSubContent = React.forwardRef<
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
@ -62,7 +62,7 @@ const ContextMenuContent = React.forwardRef<
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -13,13 +13,13 @@ const Drawer = ({
|
||||
);
|
||||
Drawer.displayName = 'Drawer';
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
const DrawerTrigger: typeof DrawerPrimitive.Trigger = DrawerPrimitive.Trigger;
|
||||
|
||||
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.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
@ -31,7 +31,7 @@ const DrawerOverlay = React.forwardRef<
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
const DrawerContent: typeof DrawerPrimitive.Content = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
@ -62,7 +62,7 @@ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
|
||||
);
|
||||
DrawerFooter.displayName = 'DrawerFooter';
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
const DrawerTitle: typeof DrawerPrimitive.Title = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
@ -74,7 +74,7 @@ const DrawerTitle = React.forwardRef<
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
const DrawerDescription: typeof DrawerPrimitive.Description = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
|
||||
@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
@ -63,8 +63,8 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-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',
|
||||
'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 origin-[--radix-dropdown-menu-content-transform-origin]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -5,11 +5,11 @@ import { Slot } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from 'react-hook-form';
|
||||
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
@ -138,7 +138,7 @@ const FormMessage = React.forwardRef<
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
const body = error ? String(error?.message ?? '') : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
|
||||
@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -6,15 +6,25 @@ import * as React from 'react';
|
||||
|
||||
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<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
@ -74,7 +84,7 @@ const MenubarSubContent = React.forwardRef<
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
@ -93,7 +103,7 @@ const MenubarContent = React.forwardRef<
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -35,7 +35,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
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<
|
||||
|
||||
@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
@ -68,7 +68,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
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' &&
|
||||
'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,
|
||||
|
||||
@ -8,7 +8,13 @@ import * as React from 'react';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
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 {
|
||||
Tooltip,
|
||||
@ -19,14 +25,14 @@ import {
|
||||
import { useIsMobile } from '@workspace/ui/hooks/use-mobile';
|
||||
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_WIDTH = '16rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
||||
type SidebarContext = {
|
||||
type SidebarContextProps = {
|
||||
state: 'expanded' | 'collapsed';
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
@ -36,7 +42,7 @@ type SidebarContext = {
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContext | null>(null);
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
@ -111,7 +117,7 @@ const SidebarProvider = React.forwardRef<
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? 'expanded' : 'collapsed';
|
||||
|
||||
const contextValue = React.useMemo<SidebarContext>(
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
@ -201,6 +207,10 @@ const Sidebar = React.forwardRef<
|
||||
}
|
||||
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>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
@ -219,7 +229,7 @@ const Sidebar = React.forwardRef<
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
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-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
@ -314,8 +324,8 @@ const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background relative flex min-h-svh 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',
|
||||
'bg-background relative flex w-full flex-1 flex-col',
|
||||
'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,
|
||||
)}
|
||||
{...props}
|
||||
@ -428,7 +438,7 @@ const SidebarGroupLabel = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar='group-label'
|
||||
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',
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -14,7 +14,7 @@ export const Timeline = ({ data }: { data: TimelineEntry[] }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const rect = ref.current?.getBoundingClientRect?.();
|
||||
setHeight(rect.height);
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -2,19 +2,20 @@ import { Input } from '@workspace/ui/components/input';
|
||||
import { cn } from '@workspace/ui/lib/utils';
|
||||
import { ChangeEvent, ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
export interface EnhancedInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||
export interface EnhancedInputProps<T = string>
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix' | 'value' | 'onChange'> {
|
||||
prefix?: string | ReactNode;
|
||||
suffix?: string | ReactNode;
|
||||
formatInput?: (value: string | number) => string | number;
|
||||
formatOutput?: (value: string | number) => string | number;
|
||||
onValueChange?: (value: string | number) => void;
|
||||
onValueBlur?: (value: string | number) => void;
|
||||
value?: T;
|
||||
formatInput?: (value: T) => string | number;
|
||||
formatOutput?: (value: string | number) => T;
|
||||
onValueChange?: (value: T) => void;
|
||||
onValueBlur?: (value: T) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export function EnhancedInput({
|
||||
export function EnhancedInput<T = string>({
|
||||
suffix,
|
||||
prefix,
|
||||
formatInput,
|
||||
@ -24,38 +25,36 @@ export function EnhancedInput({
|
||||
onValueChange,
|
||||
onValueBlur,
|
||||
...props
|
||||
}: EnhancedInputProps) {
|
||||
}: EnhancedInputProps<T>) {
|
||||
const getProcessedValue = (inputValue: unknown) => {
|
||||
if (inputValue === '' || inputValue === 0 || inputValue === '0') return '';
|
||||
const newValue = String(inputValue ?? '');
|
||||
return formatInput ? formatInput(newValue) : newValue;
|
||||
return formatInput ? formatInput(inputValue as T) : newValue;
|
||||
};
|
||||
|
||||
const [value, setValue] = useState<string | number>(() => getProcessedValue(initialValue));
|
||||
// @ts-expect-error - This is a controlled component
|
||||
const [internalValue, setInternalValue] = useState<string | number>(initialValue ?? '');
|
||||
const [internalValue, setInternalValue] = useState<T | string | number>(initialValue ?? '');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue !== internalValue) {
|
||||
const newValue = getProcessedValue(initialValue);
|
||||
if (value !== newValue) {
|
||||
setValue(newValue);
|
||||
// @ts-expect-error - This is a controlled component
|
||||
setInternalValue(initialValue ?? '');
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialValue, formatInput]);
|
||||
|
||||
const processValue = (inputValue: string | number) => {
|
||||
const processValue = (inputValue: string | number): T => {
|
||||
let processedValue: number | string = inputValue?.toString().trim();
|
||||
|
||||
if (processedValue === '0' && props.type === 'number') {
|
||||
return 0;
|
||||
return (formatOutput ? formatOutput(0) : 0) as T;
|
||||
}
|
||||
|
||||
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>) => {
|
||||
@ -65,7 +64,7 @@ export function EnhancedInput({
|
||||
if (inputValue === '0') {
|
||||
setValue('');
|
||||
setInternalValue(0);
|
||||
onValueChange?.(0);
|
||||
onValueChange?.(processValue(0));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -96,14 +95,14 @@ export function EnhancedInput({
|
||||
if (value === '-' || value === '.') {
|
||||
setValue('');
|
||||
setInternalValue('');
|
||||
onValueBlur?.('');
|
||||
onValueBlur?.('' as T);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保0值显示为空
|
||||
if (value === '0') {
|
||||
setValue('');
|
||||
onValueBlur?.(0);
|
||||
onValueBlur?.(processValue(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
export interface ICountry {
|
||||
name: string;
|
||||
alpha2: string;
|
||||
alpha3: string | null;
|
||||
numeric: string | null;
|
||||
phone: string | null;
|
||||
lang: string | null;
|
||||
alpha3?: string | null;
|
||||
numeric?: string | null;
|
||||
phone?: string | null;
|
||||
lang?: string | null;
|
||||
langs: string[];
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user