@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.
Files changed (104) hide show
  1. package/.checksum +1 -0
  2. package/README.md +46 -0
  3. package/components.json +20 -0
  4. package/eslint.config.mjs +20 -0
  5. package/jsr.json +10 -0
  6. package/package.json +120 -0
  7. package/postcss.config.mjs +8 -0
  8. package/rollup.config.js +40 -0
  9. package/src/components/ui/accordion.tsx +70 -0
  10. package/src/components/ui/alert-dialog.tsx +156 -0
  11. package/src/components/ui/alert.tsx +58 -0
  12. package/src/components/ui/aspect-ratio.tsx +11 -0
  13. package/src/components/ui/avatar.tsx +52 -0
  14. package/src/components/ui/badge.tsx +49 -0
  15. package/src/components/ui/breadcrumb.tsx +108 -0
  16. package/src/components/ui/button.tsx +61 -0
  17. package/src/components/ui/calendar.tsx +212 -0
  18. package/src/components/ui/card.tsx +74 -0
  19. package/src/components/ui/carousel.tsx +240 -0
  20. package/src/components/ui/chart.tsx +365 -0
  21. package/src/components/ui/checkbox.tsx +31 -0
  22. package/src/components/ui/codeblock.tsx +161 -0
  23. package/src/components/ui/collapsible.tsx +33 -0
  24. package/src/components/ui/color-picker.tsx +143 -0
  25. package/src/components/ui/command.tsx +176 -0
  26. package/src/components/ui/context-menu.tsx +251 -0
  27. package/src/components/ui/custom/autosize-textarea.tsx +111 -0
  28. package/src/components/ui/custom/calendar/core.tsx +61 -0
  29. package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
  30. package/src/components/ui/custom/calendar/month-header.tsx +59 -0
  31. package/src/components/ui/custom/calendar/month-view.tsx +110 -0
  32. package/src/components/ui/custom/calendar/utils.ts +76 -0
  33. package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
  34. package/src/components/ui/custom/calendar/year-view.tsx +58 -0
  35. package/src/components/ui/custom/combobox.tsx +197 -0
  36. package/src/components/ui/custom/common-footer.tsx +215 -0
  37. package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
  38. package/src/components/ui/custom/date-input.tsx +279 -0
  39. package/src/components/ui/custom/empty-card.tsx +39 -0
  40. package/src/components/ui/custom/feature-summary.tsx +135 -0
  41. package/src/components/ui/custom/file-uploader.tsx +349 -0
  42. package/src/components/ui/custom/input-field.tsx +29 -0
  43. package/src/components/ui/custom/loading-indicator.tsx +28 -0
  44. package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
  45. package/src/components/ui/custom/month-picker.tsx +157 -0
  46. package/src/components/ui/custom/report-preview.tsx +175 -0
  47. package/src/components/ui/custom/search-bar.tsx +56 -0
  48. package/src/components/ui/custom/select-field.tsx +78 -0
  49. package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
  50. package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
  51. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
  52. package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
  53. package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
  54. package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
  55. package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
  56. package/src/components/ui/custom/tables/data-table.tsx +228 -0
  57. package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +137 -0
  59. package/src/components/ui/drawer.tsx +131 -0
  60. package/src/components/ui/dropdown-menu.tsx +256 -0
  61. package/src/components/ui/form.tsx +167 -0
  62. package/src/components/ui/hover-card.tsx +41 -0
  63. package/src/components/ui/icons.tsx +506 -0
  64. package/src/components/ui/input-otp.tsx +78 -0
  65. package/src/components/ui/input.tsx +18 -0
  66. package/src/components/ui/label.tsx +23 -0
  67. package/src/components/ui/markdown.tsx +7 -0
  68. package/src/components/ui/menubar.tsx +275 -0
  69. package/src/components/ui/navigation-menu.tsx +169 -0
  70. package/src/components/ui/pagination.tsx +126 -0
  71. package/src/components/ui/popover.tsx +47 -0
  72. package/src/components/ui/progress.tsx +30 -0
  73. package/src/components/ui/radio-group.tsx +44 -0
  74. package/src/components/ui/resizable.tsx +55 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +180 -0
  77. package/src/components/ui/separator.tsx +27 -0
  78. package/src/components/ui/sheet.tsx +138 -0
  79. package/src/components/ui/sidebar.tsx +734 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/slider.tsx +62 -0
  82. package/src/components/ui/sonner.tsx +29 -0
  83. package/src/components/ui/switch.tsx +30 -0
  84. package/src/components/ui/table.tsx +112 -0
  85. package/src/components/ui/tabs.tsx +68 -0
  86. package/src/components/ui/tag-input.tsx +141 -0
  87. package/src/components/ui/textarea.tsx +17 -0
  88. package/src/components/ui/time-picker-input.tsx +117 -0
  89. package/src/components/ui/time-picker-utils.tsx +146 -0
  90. package/src/components/ui/toast.tsx +128 -0
  91. package/src/components/ui/toaster.tsx +35 -0
  92. package/src/components/ui/toggle-group.tsx +72 -0
  93. package/src/components/ui/toggle.tsx +46 -0
  94. package/src/components/ui/tooltip.tsx +60 -0
  95. package/src/globals.css +252 -0
  96. package/src/hooks/use-callback-ref.ts +28 -0
  97. package/src/hooks/use-controllable-state.ts +68 -0
  98. package/src/hooks/use-copy-to-clipboard.ts +46 -0
  99. package/src/hooks/use-form.ts +23 -0
  100. package/src/hooks/use-forwarded-ref.ts +17 -0
  101. package/src/hooks/use-mobile.tsx +21 -0
  102. package/src/hooks/use-toast.ts +191 -0
  103. package/src/resolvers.ts +3 -0
  104. 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 };