@tuturuuu/ui 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.checksum +1 -0
- package/README.md +46 -0
- package/components.json +20 -0
- package/eslint.config.mjs +20 -0
- package/jsr.json +10 -0
- package/package.json +120 -0
- package/postcss.config.mjs +8 -0
- package/rollup.config.js +40 -0
- package/src/components/ui/accordion.tsx +70 -0
- package/src/components/ui/alert-dialog.tsx +156 -0
- package/src/components/ui/alert.tsx +58 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +52 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +212 -0
- package/src/components/ui/card.tsx +74 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +365 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/codeblock.tsx +161 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/color-picker.tsx +143 -0
- package/src/components/ui/command.tsx +176 -0
- package/src/components/ui/context-menu.tsx +251 -0
- package/src/components/ui/custom/autosize-textarea.tsx +111 -0
- package/src/components/ui/custom/calendar/core.tsx +61 -0
- package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
- package/src/components/ui/custom/calendar/month-header.tsx +59 -0
- package/src/components/ui/custom/calendar/month-view.tsx +110 -0
- package/src/components/ui/custom/calendar/utils.ts +76 -0
- package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
- package/src/components/ui/custom/calendar/year-view.tsx +58 -0
- package/src/components/ui/custom/combobox.tsx +197 -0
- package/src/components/ui/custom/common-footer.tsx +215 -0
- package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
- package/src/components/ui/custom/date-input.tsx +279 -0
- package/src/components/ui/custom/empty-card.tsx +39 -0
- package/src/components/ui/custom/feature-summary.tsx +135 -0
- package/src/components/ui/custom/file-uploader.tsx +349 -0
- package/src/components/ui/custom/input-field.tsx +29 -0
- package/src/components/ui/custom/loading-indicator.tsx +28 -0
- package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
- package/src/components/ui/custom/month-picker.tsx +157 -0
- package/src/components/ui/custom/report-preview.tsx +175 -0
- package/src/components/ui/custom/search-bar.tsx +56 -0
- package/src/components/ui/custom/select-field.tsx +78 -0
- package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
- package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
- package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
- package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
- package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
- package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
- package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
- package/src/components/ui/custom/tables/data-table.tsx +228 -0
- package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +131 -0
- package/src/components/ui/dropdown-menu.tsx +256 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +41 -0
- package/src/components/ui/icons.tsx +506 -0
- package/src/components/ui/input-otp.tsx +78 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/markdown.tsx +7 -0
- package/src/components/ui/menubar.tsx +275 -0
- package/src/components/ui/navigation-menu.tsx +169 -0
- package/src/components/ui/pagination.tsx +126 -0
- package/src/components/ui/popover.tsx +47 -0
- package/src/components/ui/progress.tsx +30 -0
- package/src/components/ui/radio-group.tsx +44 -0
- package/src/components/ui/resizable.tsx +55 -0
- package/src/components/ui/scroll-area.tsx +57 -0
- package/src/components/ui/select.tsx +180 -0
- package/src/components/ui/separator.tsx +27 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/sonner.tsx +29 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +112 -0
- package/src/components/ui/tabs.tsx +68 -0
- package/src/components/ui/tag-input.tsx +141 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/time-picker-input.tsx +117 -0
- package/src/components/ui/time-picker-utils.tsx +146 -0
- package/src/components/ui/toast.tsx +128 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/toggle-group.tsx +72 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +60 -0
- package/src/globals.css +252 -0
- package/src/hooks/use-callback-ref.ts +28 -0
- package/src/hooks/use-controllable-state.ts +68 -0
- package/src/hooks/use-copy-to-clipboard.ts +46 -0
- package/src/hooks/use-form.ts +23 -0
- package/src/hooks/use-forwarded-ref.ts +17 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +191 -0
- package/src/resolvers.ts +3 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn('animate-pulse rounded-md bg-primary/10', className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as SliderPrimitive from '@radix-ui/react-slider';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
function Slider({
|
|
8
|
+
className,
|
|
9
|
+
defaultValue,
|
|
10
|
+
value,
|
|
11
|
+
min = 0,
|
|
12
|
+
max = 100,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
|
15
|
+
const _values = React.useMemo(
|
|
16
|
+
() =>
|
|
17
|
+
Array.isArray(value)
|
|
18
|
+
? value
|
|
19
|
+
: Array.isArray(defaultValue)
|
|
20
|
+
? defaultValue
|
|
21
|
+
: [min, max],
|
|
22
|
+
[value, defaultValue, min, max]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<SliderPrimitive.Root
|
|
27
|
+
data-slot="slider"
|
|
28
|
+
defaultValue={defaultValue}
|
|
29
|
+
value={value}
|
|
30
|
+
min={min}
|
|
31
|
+
max={max}
|
|
32
|
+
className={cn(
|
|
33
|
+
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
<SliderPrimitive.Track
|
|
39
|
+
data-slot="slider-track"
|
|
40
|
+
className={cn(
|
|
41
|
+
'relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5'
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<SliderPrimitive.Range
|
|
45
|
+
data-slot="slider-range"
|
|
46
|
+
className={cn(
|
|
47
|
+
'absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full'
|
|
48
|
+
)}
|
|
49
|
+
/>
|
|
50
|
+
</SliderPrimitive.Track>
|
|
51
|
+
{Array.from({ length: _values.length }, (_, index) => (
|
|
52
|
+
<SliderPrimitive.Thumb
|
|
53
|
+
data-slot="slider-thumb"
|
|
54
|
+
key={index}
|
|
55
|
+
className="block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/20 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</SliderPrimitive.Root>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Slider };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from 'next-themes';
|
|
4
|
+
import { Toaster as Sonner, ToasterProps, toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
7
|
+
const { theme = 'system' } = useTheme();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Sonner
|
|
11
|
+
theme={theme as ToasterProps['theme']}
|
|
12
|
+
className="toaster group"
|
|
13
|
+
toastOptions={{
|
|
14
|
+
classNames: {
|
|
15
|
+
toast:
|
|
16
|
+
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
|
17
|
+
description: 'group-[.toast]:text-muted-foreground',
|
|
18
|
+
actionButton:
|
|
19
|
+
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
|
20
|
+
cancelButton:
|
|
21
|
+
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
|
22
|
+
},
|
|
23
|
+
}}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { toast, Toaster };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
function Switch({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<SwitchPrimitive.Root
|
|
13
|
+
data-slot="switch"
|
|
14
|
+
className={cn(
|
|
15
|
+
'peer inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent shadow-xs ring-ring/10 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-hidden focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-0 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:ring-ring/20 dark:outline-ring/40',
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<SwitchPrimitive.Thumb
|
|
21
|
+
data-slot="switch-thumb"
|
|
22
|
+
className={cn(
|
|
23
|
+
'pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
|
24
|
+
)}
|
|
25
|
+
/>
|
|
26
|
+
</SwitchPrimitive.Root>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Switch };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="relative w-full overflow-auto">
|
|
9
|
+
<table
|
|
10
|
+
data-slot="table"
|
|
11
|
+
className={cn('w-full caption-bottom text-sm', className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
|
19
|
+
return (
|
|
20
|
+
<thead
|
|
21
|
+
data-slot="table-header"
|
|
22
|
+
className={cn('[&_tr]:border-b', className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
|
29
|
+
return (
|
|
30
|
+
<tbody
|
|
31
|
+
data-slot="table-body"
|
|
32
|
+
className={cn('[&_tr:last-child]:border-0', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
|
39
|
+
return (
|
|
40
|
+
<tfoot
|
|
41
|
+
data-slot="table-footer"
|
|
42
|
+
className={cn(
|
|
43
|
+
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
|
52
|
+
return (
|
|
53
|
+
<tr
|
|
54
|
+
data-slot="table-row"
|
|
55
|
+
className={cn(
|
|
56
|
+
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
|
65
|
+
return (
|
|
66
|
+
<th
|
|
67
|
+
data-slot="table-head"
|
|
68
|
+
className={cn(
|
|
69
|
+
'h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
|
78
|
+
return (
|
|
79
|
+
<td
|
|
80
|
+
data-slot="table-cell"
|
|
81
|
+
className={cn(
|
|
82
|
+
'p-2 pl-3 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function TableCaption({
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
}: React.ComponentProps<'caption'>) {
|
|
94
|
+
return (
|
|
95
|
+
<caption
|
|
96
|
+
data-slot="table-caption"
|
|
97
|
+
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
Table,
|
|
105
|
+
TableBody,
|
|
106
|
+
TableCaption,
|
|
107
|
+
TableCell,
|
|
108
|
+
TableFooter,
|
|
109
|
+
TableHead,
|
|
110
|
+
TableHeader,
|
|
111
|
+
TableRow,
|
|
112
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
function Tabs({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<TabsPrimitive.Root
|
|
13
|
+
data-slot="tabs"
|
|
14
|
+
className={cn('flex flex-col gap-2', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TabsList({
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
24
|
+
return (
|
|
25
|
+
<TabsPrimitive.List
|
|
26
|
+
data-slot="tabs-list"
|
|
27
|
+
className={cn(
|
|
28
|
+
'inline-flex h-9 w-fit items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function TabsTrigger({
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
40
|
+
return (
|
|
41
|
+
<TabsPrimitive.Trigger
|
|
42
|
+
data-slot="tabs-trigger"
|
|
43
|
+
className={cn(
|
|
44
|
+
"inline-flex items-center justify-center gap-2 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap ring-ring/10 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-invalid:focus-visible:ring-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm dark:ring-ring/20 dark:outline-ring/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function TabsContent({
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
55
|
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
56
|
+
return (
|
|
57
|
+
<TabsPrimitive.Content
|
|
58
|
+
data-slot="tabs-content"
|
|
59
|
+
className={cn(
|
|
60
|
+
'flex-1 ring-ring/10 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0 dark:ring-ring/20 dark:outline-ring/40',
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from './badge';
|
|
4
|
+
import { Command, CommandGroup, CommandItem } from './command';
|
|
5
|
+
import { Command as CommandPrimitive } from 'cmdk';
|
|
6
|
+
import { X } from 'lucide-react';
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
|
|
9
|
+
interface TagInputProps {
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
// eslint-disable-next-line no-unused-vars
|
|
13
|
+
setTags: (tags: string[]) => void;
|
|
14
|
+
suggestions?: string[];
|
|
15
|
+
maxTags?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
|
18
|
+
onTagAdd?: (tag: string) => void;
|
|
19
|
+
// eslint-disable-next-line no-unused-vars
|
|
20
|
+
onTagRemove?: (tag: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TagInput({
|
|
24
|
+
placeholder = 'Add tag...',
|
|
25
|
+
tags,
|
|
26
|
+
setTags,
|
|
27
|
+
suggestions = [],
|
|
28
|
+
maxTags,
|
|
29
|
+
disabled = false,
|
|
30
|
+
onTagAdd,
|
|
31
|
+
onTagRemove,
|
|
32
|
+
}: TagInputProps) {
|
|
33
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
34
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
35
|
+
const [open, setOpen] = React.useState(false);
|
|
36
|
+
|
|
37
|
+
const handleAddTag = (value: string) => {
|
|
38
|
+
const trimmedValue = value.trim();
|
|
39
|
+
if (
|
|
40
|
+
trimmedValue !== '' &&
|
|
41
|
+
!tags.includes(trimmedValue) &&
|
|
42
|
+
(!maxTags || tags.length < maxTags)
|
|
43
|
+
) {
|
|
44
|
+
const newTags = [...tags, trimmedValue];
|
|
45
|
+
setTags(newTags);
|
|
46
|
+
onTagAdd?.(trimmedValue);
|
|
47
|
+
}
|
|
48
|
+
setInputValue('');
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleRemoveTag = (tagToRemove: string) => {
|
|
52
|
+
const newTags = tags.filter((tag) => tag !== tagToRemove);
|
|
53
|
+
setTags(newTags);
|
|
54
|
+
onTagRemove?.(tagToRemove);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const filteredSuggestions = suggestions.filter(
|
|
58
|
+
(suggestion) =>
|
|
59
|
+
!tags.includes(suggestion) &&
|
|
60
|
+
suggestion.toLowerCase().includes(inputValue.toLowerCase())
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Command className="overflow-visible bg-transparent">
|
|
65
|
+
<div className="group rounded-md border border-input px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
66
|
+
<div className="flex flex-wrap gap-1">
|
|
67
|
+
{tags.map((tag) => (
|
|
68
|
+
<Badge key={tag} variant="secondary">
|
|
69
|
+
{tag}
|
|
70
|
+
{!disabled && (
|
|
71
|
+
<button
|
|
72
|
+
className="ml-1 rounded-full ring-offset-background outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
73
|
+
onKeyDown={(e) => {
|
|
74
|
+
if (e.key === 'Enter') {
|
|
75
|
+
handleRemoveTag(tag);
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
onMouseDown={(e) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
}}
|
|
82
|
+
onClick={() => handleRemoveTag(tag)}
|
|
83
|
+
>
|
|
84
|
+
<X className="h-3 w-3" />
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
87
|
+
</Badge>
|
|
88
|
+
))}
|
|
89
|
+
<CommandPrimitive.Input
|
|
90
|
+
ref={inputRef}
|
|
91
|
+
value={inputValue}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
onValueChange={setInputValue}
|
|
94
|
+
onKeyDown={(e) => {
|
|
95
|
+
if (e.key === 'Enter' && inputValue) {
|
|
96
|
+
handleAddTag(inputValue);
|
|
97
|
+
} else if (
|
|
98
|
+
e.key === 'Backspace' &&
|
|
99
|
+
!inputValue &&
|
|
100
|
+
tags.length > 0
|
|
101
|
+
) {
|
|
102
|
+
const lastTag = tags[tags.length - 1];
|
|
103
|
+
if (lastTag) {
|
|
104
|
+
handleRemoveTag(lastTag);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
onBlur={() => {
|
|
109
|
+
setOpen(false);
|
|
110
|
+
if (inputValue) {
|
|
111
|
+
handleAddTag(inputValue);
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
onFocus={() => setOpen(true)}
|
|
115
|
+
placeholder={placeholder}
|
|
116
|
+
className="flex-1 bg-transparent outline-hidden placeholder:text-muted-foreground"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="relative mt-2">
|
|
121
|
+
{open && filteredSuggestions.length > 0 && (
|
|
122
|
+
<div className="absolute top-0 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-hidden animate-in">
|
|
123
|
+
<CommandGroup className="h-full overflow-auto">
|
|
124
|
+
{filteredSuggestions.map((suggestion) => (
|
|
125
|
+
<CommandItem
|
|
126
|
+
key={suggestion}
|
|
127
|
+
onSelect={() => {
|
|
128
|
+
handleAddTag(suggestion);
|
|
129
|
+
setOpen(false);
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{suggestion}
|
|
133
|
+
</CommandItem>
|
|
134
|
+
))}
|
|
135
|
+
</CommandGroup>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</Command>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
5
|
+
return (
|
|
6
|
+
<textarea
|
|
7
|
+
data-slot="textarea"
|
|
8
|
+
className={cn(
|
|
9
|
+
'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs ring-ring/10 outline-ring/50 transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive/60 aria-invalid:ring-destructive/20 aria-invalid:outline-destructive/60 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:ring-ring/20 dark:outline-ring/40 dark:aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40 dark:aria-invalid:ring-destructive/50 dark:aria-invalid:outline-destructive dark:aria-invalid:focus-visible:ring-4',
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { Textarea };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Input } from './input';
|
|
4
|
+
import {
|
|
5
|
+
TimePickerType,
|
|
6
|
+
getArrowByType,
|
|
7
|
+
getDateByType,
|
|
8
|
+
setDateByType,
|
|
9
|
+
} from './time-picker-utils';
|
|
10
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
|
|
13
|
+
export interface TimePickerInputProps
|
|
14
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
15
|
+
picker: TimePickerType;
|
|
16
|
+
date: Date | undefined;
|
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
|
18
|
+
setDate: (date: Date | undefined) => void;
|
|
19
|
+
onRightFocus?: () => void;
|
|
20
|
+
onLeftFocus?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TimePickerInput = React.forwardRef<
|
|
24
|
+
HTMLInputElement,
|
|
25
|
+
TimePickerInputProps
|
|
26
|
+
>(
|
|
27
|
+
(
|
|
28
|
+
{
|
|
29
|
+
className,
|
|
30
|
+
type = 'tel',
|
|
31
|
+
value,
|
|
32
|
+
id,
|
|
33
|
+
name,
|
|
34
|
+
date = new Date(new Date().setHours(0, 0, 0, 0)),
|
|
35
|
+
setDate,
|
|
36
|
+
onChange,
|
|
37
|
+
onKeyDown,
|
|
38
|
+
picker,
|
|
39
|
+
onLeftFocus,
|
|
40
|
+
onRightFocus,
|
|
41
|
+
...props
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
const [flag, setFlag] = React.useState<boolean>(false);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* allow the user to enter the second digit within 2 seconds
|
|
49
|
+
* otherwise start again with entering first digit
|
|
50
|
+
*/
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (flag) {
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
setFlag(false);
|
|
55
|
+
}, 2000);
|
|
56
|
+
|
|
57
|
+
return () => clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}, [flag]);
|
|
60
|
+
|
|
61
|
+
const calculatedValue = React.useMemo(
|
|
62
|
+
() => getDateByType(date, picker),
|
|
63
|
+
[date, picker]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
67
|
+
if (e.key === 'Tab') return;
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
if (e.key === 'ArrowRight') onRightFocus?.();
|
|
70
|
+
if (e.key === 'ArrowLeft') onLeftFocus?.();
|
|
71
|
+
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
|
|
72
|
+
const step = e.key === 'ArrowUp' ? 1 : -1;
|
|
73
|
+
const newValue = getArrowByType(calculatedValue, step, picker);
|
|
74
|
+
if (flag) setFlag(false);
|
|
75
|
+
const tempDate = new Date(date);
|
|
76
|
+
setDate(setDateByType(tempDate, newValue, picker));
|
|
77
|
+
}
|
|
78
|
+
if (e.key >= '0' && e.key <= '9') {
|
|
79
|
+
const newValue = !flag
|
|
80
|
+
? '0' + e.key
|
|
81
|
+
: calculatedValue.slice(1, 2) + e.key;
|
|
82
|
+
if (flag) onRightFocus?.();
|
|
83
|
+
setFlag((prev) => !prev);
|
|
84
|
+
const tempDate = new Date(date);
|
|
85
|
+
setDate(setDateByType(tempDate, newValue, picker));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Input
|
|
91
|
+
ref={ref}
|
|
92
|
+
id={id || picker}
|
|
93
|
+
name={name || picker}
|
|
94
|
+
className={cn(
|
|
95
|
+
'w-[48px] text-center font-mono text-base tabular-nums caret-transparent focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
98
|
+
value={value || calculatedValue}
|
|
99
|
+
onChange={(e) => {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
onChange?.(e);
|
|
102
|
+
}}
|
|
103
|
+
type={type}
|
|
104
|
+
inputMode="decimal"
|
|
105
|
+
onKeyDown={(e) => {
|
|
106
|
+
onKeyDown?.(e);
|
|
107
|
+
handleKeyDown(e);
|
|
108
|
+
}}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
TimePickerInput.displayName = 'TimePickerInput';
|
|
116
|
+
|
|
117
|
+
export { TimePickerInput };
|