@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,279 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ interface DateInputProps {
4
+ value?: Date;
5
+ // eslint-disable-next-line no-unused-vars
6
+ onChange?: (date: Date) => void;
7
+ // eslint-disable-next-line no-unused-vars
8
+ onSubmit?: (date: Date) => void;
9
+ }
10
+
11
+ interface DateParts {
12
+ day: number;
13
+ month: number;
14
+ year: number;
15
+ }
16
+
17
+ const DateInput: React.FC<DateInputProps> = ({ value, onChange, onSubmit }) => {
18
+ const [date, setDate] = React.useState<DateParts>(() => {
19
+ const d = value ? new Date(value) : new Date();
20
+ return {
21
+ day: d.getDate(),
22
+ month: d.getMonth() + 1, // JavaScript months are 0-indexed
23
+ year: d.getFullYear(),
24
+ };
25
+ });
26
+
27
+ const monthRef = useRef<HTMLInputElement | null>(null);
28
+ const dayRef = useRef<HTMLInputElement | null>(null);
29
+ const yearRef = useRef<HTMLInputElement | null>(null);
30
+
31
+ useEffect(() => {
32
+ const d = value ? new Date(value) : new Date();
33
+ setDate({
34
+ day: d.getDate(),
35
+ month: d.getMonth() + 1,
36
+ year: d.getFullYear(),
37
+ });
38
+ }, [value]);
39
+
40
+ const validateDate = (field: keyof DateParts, value: number): boolean => {
41
+ if (
42
+ (field === 'day' && (value < 1 || value > 31)) ||
43
+ (field === 'month' && (value < 1 || value > 12)) ||
44
+ (field === 'year' && (value < 1000 || value > 9999))
45
+ ) {
46
+ return false;
47
+ }
48
+
49
+ // Validate the day of the month
50
+ const newDate = { ...date, [field]: value };
51
+ const d = new Date(newDate.year, newDate.month - 1, newDate.day);
52
+ return (
53
+ d.getFullYear() === newDate.year &&
54
+ d.getMonth() + 1 === newDate.month &&
55
+ d.getDate() === newDate.day
56
+ );
57
+ };
58
+
59
+ const handleInputChange =
60
+ (field: keyof DateParts) => (e: React.ChangeEvent<HTMLInputElement>) => {
61
+ const newValue = e.target.value.trim()
62
+ ? Number(e.target.value.trim())
63
+ : '';
64
+
65
+ const isValid =
66
+ typeof newValue === 'number' && validateDate(field, newValue);
67
+
68
+ const newDate = { ...date, [field]: newValue };
69
+ setDate(newDate);
70
+
71
+ // Only call onChange when the input is valid and complete
72
+ if (isValid && e.target.value.trim().length > 0) {
73
+ const isComplete =
74
+ (field === 'day' && e.target.value.trim().length === 2) ||
75
+ (field === 'month' && e.target.value.trim().length === 2) ||
76
+ (field === 'year' && e.target.value.trim().length === 4);
77
+
78
+ if (isComplete) {
79
+ onChange?.(new Date(newDate.year, newDate.month - 1, newDate.day));
80
+ }
81
+ }
82
+ };
83
+
84
+ const initialDate = useRef<DateParts>(date);
85
+
86
+ const handleBlur =
87
+ (field: keyof DateParts) =>
88
+ (e: React.FocusEvent<HTMLInputElement>): void => {
89
+ if (!e.target.value) {
90
+ setDate(initialDate.current);
91
+ return;
92
+ }
93
+
94
+ const newValue = Number(e.target.value);
95
+ const isValid = validateDate(field, newValue);
96
+
97
+ if (!isValid) {
98
+ setDate(initialDate.current);
99
+ } else {
100
+ // If the new value is valid, update the initial value
101
+ initialDate.current = { ...date, [field]: newValue };
102
+ }
103
+
104
+ // Focus logic
105
+ if (field === 'day' && e.target.value.trim().length === 2) {
106
+ monthRef.current?.focus();
107
+ monthRef.current?.select();
108
+ } else if (field === 'month' && e.target.value.trim().length === 2) {
109
+ yearRef.current?.focus();
110
+ yearRef.current?.select();
111
+ }
112
+ };
113
+
114
+ const handleKeyDown =
115
+ (field: keyof DateParts) => (e: React.KeyboardEvent<HTMLInputElement>) => {
116
+ // Allow command (or control) combinations
117
+ if (e.metaKey || e.ctrlKey) {
118
+ return;
119
+ }
120
+
121
+ // Prevent non-numeric characters, excluding allowed keys
122
+ if (
123
+ !/^[0-9]$/.test(e.key) &&
124
+ ![
125
+ 'ArrowUp',
126
+ 'ArrowDown',
127
+ 'ArrowLeft',
128
+ 'ArrowRight',
129
+ 'Delete',
130
+ 'Tab',
131
+ 'Backspace',
132
+ 'Enter',
133
+ ].includes(e.key)
134
+ ) {
135
+ e.preventDefault();
136
+ return;
137
+ }
138
+
139
+ if (e.key === 'ArrowUp') {
140
+ e.preventDefault();
141
+ let newDate = { ...date };
142
+
143
+ if (field === 'day') {
144
+ if (date[field] === new Date(date.year, date.month, 0).getDate()) {
145
+ newDate = { ...newDate, day: 1, month: (date.month % 12) + 1 };
146
+ if (newDate.month === 1) newDate.year += 1;
147
+ } else {
148
+ newDate.day += 1;
149
+ }
150
+ }
151
+
152
+ if (field === 'month') {
153
+ if (date[field] === 12) {
154
+ newDate = { ...newDate, month: 1, year: date.year + 1 };
155
+ } else {
156
+ newDate.month += 1;
157
+ }
158
+ }
159
+
160
+ if (field === 'year') {
161
+ newDate.year += 1;
162
+ }
163
+
164
+ setDate(newDate);
165
+ onChange?.(new Date(newDate.year, newDate.month - 1, newDate.day));
166
+ } else if (e.key === 'ArrowDown') {
167
+ e.preventDefault();
168
+ let newDate = { ...date };
169
+
170
+ if (field === 'day') {
171
+ if (date[field] === 1) {
172
+ newDate.month -= 1;
173
+ if (newDate.month === 0) {
174
+ newDate.month = 12;
175
+ newDate.year -= 1;
176
+ }
177
+ newDate.day = new Date(newDate.year, newDate.month, 0).getDate();
178
+ } else {
179
+ newDate.day -= 1;
180
+ }
181
+ }
182
+
183
+ if (field === 'month') {
184
+ if (date[field] === 1) {
185
+ newDate = { ...newDate, month: 12, year: date.year - 1 };
186
+ } else {
187
+ newDate.month -= 1;
188
+ }
189
+ }
190
+
191
+ if (field === 'year') {
192
+ newDate.year -= 1;
193
+ }
194
+
195
+ setDate(newDate);
196
+ onChange?.(new Date(newDate.year, newDate.month - 1, newDate.day));
197
+ }
198
+
199
+ if (e.key === 'ArrowRight') {
200
+ if (
201
+ e.currentTarget.selectionStart === e.currentTarget.value.length ||
202
+ (e.currentTarget.selectionStart === 0 &&
203
+ e.currentTarget.selectionEnd === e.currentTarget.value.length)
204
+ ) {
205
+ e.preventDefault();
206
+ if (field === 'month') yearRef.current?.focus();
207
+ if (field === 'day') monthRef.current?.focus();
208
+ }
209
+ } else if (e.key === 'ArrowLeft') {
210
+ if (
211
+ e.currentTarget.selectionStart === 0 ||
212
+ (e.currentTarget.selectionStart === 0 &&
213
+ e.currentTarget.selectionEnd === e.currentTarget.value.length)
214
+ ) {
215
+ e.preventDefault();
216
+ if (field === 'month') dayRef.current?.focus();
217
+ if (field === 'year') monthRef.current?.focus();
218
+ }
219
+ }
220
+
221
+ if (e.key === 'Enter') {
222
+ e.preventDefault();
223
+ dayRef.current?.blur();
224
+ monthRef.current?.blur();
225
+ yearRef.current?.blur();
226
+ onSubmit?.(new Date(date.year, date.month - 1, date.day));
227
+ }
228
+ };
229
+
230
+ return (
231
+ <div className="flex items-center rounded-md border px-1 text-sm">
232
+ <input
233
+ type="text"
234
+ ref={dayRef}
235
+ max={31}
236
+ maxLength={2}
237
+ value={date.day.toString()}
238
+ onChange={handleInputChange('day')}
239
+ onKeyDown={handleKeyDown('day')}
240
+ onBlur={handleBlur('day')}
241
+ className="w-7 border-none bg-transparent p-0 text-center outline-hidden"
242
+ placeholder="D"
243
+ autoFocus={false}
244
+ />
245
+ <span className="-mx-px opacity-20">/</span>
246
+ <input
247
+ type="text"
248
+ ref={monthRef}
249
+ max={12}
250
+ maxLength={2}
251
+ value={date.month.toString()}
252
+ onChange={handleInputChange('month')}
253
+ onKeyDown={handleKeyDown('month')}
254
+ onBlur={handleBlur('month')}
255
+ className="w-6 border-none bg-transparent p-0 text-center outline-hidden"
256
+ placeholder="M"
257
+ autoFocus={false}
258
+ />
259
+ <span className="-mx-px opacity-20">/</span>
260
+ <input
261
+ type="text"
262
+ ref={yearRef}
263
+ max={9999}
264
+ maxLength={4}
265
+ value={date.year.toString()}
266
+ onChange={handleInputChange('year')}
267
+ onKeyDown={handleKeyDown('year')}
268
+ onBlur={handleBlur('year')}
269
+ className="w-12 border-none bg-transparent p-0 text-center outline-hidden"
270
+ placeholder="YYYY"
271
+ autoFocus={false}
272
+ />
273
+ </div>
274
+ );
275
+ };
276
+
277
+ DateInput.displayName = 'DateInput';
278
+
279
+ export { DateInput };
@@ -0,0 +1,39 @@
1
+ import { Card, CardDescription, CardTitle } from '@tuturuuu/ui/card';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import { Image } from 'lucide-react';
4
+ import React from 'react';
5
+
6
+ interface EmptyCardProps extends React.ComponentPropsWithoutRef<typeof Card> {
7
+ title: string;
8
+ description?: string;
9
+ action?: React.ReactNode;
10
+ icon?: React.ComponentType<{ className?: string }>;
11
+ }
12
+
13
+ export function EmptyCard({
14
+ title,
15
+ description,
16
+ icon: Icon = Image,
17
+ action,
18
+ className,
19
+ ...props
20
+ }: EmptyCardProps) {
21
+ return (
22
+ <Card
23
+ className={cn(
24
+ 'flex w-full flex-col items-center justify-center space-y-6 bg-transparent p-16',
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ <div className="mr-4 shrink-0 rounded-full border border-dashed p-4">
30
+ <Icon className="size-8 text-muted-foreground" aria-hidden="true" />
31
+ </div>
32
+ <div className="flex flex-col items-center gap-1.5 text-center">
33
+ <CardTitle>{title}</CardTitle>
34
+ {description ? <CardDescription>{description}</CardDescription> : null}
35
+ </div>
36
+ {action ? action : null}
37
+ </Card>
38
+ );
39
+ }
@@ -0,0 +1,135 @@
1
+ import { Button } from '../button';
2
+ import ModifiableDialogTrigger from './modifiable-dialog-trigger';
3
+ import { cn } from '@tuturuuu/utils/format';
4
+ import { Cog, Plus } from 'lucide-react';
5
+ import { type ReactElement, ReactNode } from 'react';
6
+
7
+ interface FormProps<T> {
8
+ data?: T;
9
+ forceDefault?: boolean;
10
+ onFinish?: () => void;
11
+ form?: ReactElement<FormProps<T>>;
12
+ }
13
+
14
+ interface Props<T> {
15
+ data?: T & { id?: string };
16
+ defaultData?: T & { id?: string };
17
+ trigger?: ReactNode;
18
+ form?: ReactElement<FormProps<T>>;
19
+ href?: string;
20
+ title?: ReactNode;
21
+ pluralTitle?: string;
22
+ singularTitle?: string;
23
+ description?: ReactNode;
24
+ action?: ReactNode;
25
+ createTitle?: string;
26
+ createDescription?: string;
27
+ secondaryTriggerTitle?: string;
28
+ secondaryTitle?: string;
29
+ secondaryDescription?: string;
30
+ requireExpansion?: boolean;
31
+ primaryTrigger?: ReactNode;
32
+ secondaryTrigger?: ReactNode;
33
+ secondaryTriggerIcon?: ReactNode;
34
+ showSecondaryTrigger?: boolean;
35
+ disableSecondaryTrigger?: boolean;
36
+ showCustomSecondaryTrigger?: boolean;
37
+ showDefaultFormAsSecondary?: boolean;
38
+ open?: boolean;
39
+ // eslint-disable-next-line no-unused-vars
40
+ setOpen?: (open: boolean) => void;
41
+ onSecondaryTriggerClick?: () => void;
42
+ }
43
+
44
+ export default function FeatureSummary<T>({
45
+ data,
46
+ defaultData,
47
+ form,
48
+ href,
49
+ title,
50
+ pluralTitle,
51
+ singularTitle,
52
+ description,
53
+ action,
54
+ open,
55
+ createTitle: primaryTriggerTitle,
56
+ createDescription,
57
+ requireExpansion,
58
+ secondaryTriggerTitle,
59
+ secondaryTitle,
60
+ secondaryDescription,
61
+ primaryTrigger = form || href ? (
62
+ <Button size="xs" className="w-full md:w-fit" disabled={!form && !href}>
63
+ <Plus className={cn('h-5 w-5', primaryTriggerTitle ? 'mr-1' : '')} />
64
+ {primaryTriggerTitle}
65
+ </Button>
66
+ ) : undefined,
67
+ secondaryTriggerIcon,
68
+ disableSecondaryTrigger,
69
+ secondaryTrigger = (
70
+ <Button
71
+ size="xs"
72
+ variant="ghost"
73
+ className="w-full md:w-fit"
74
+ disabled={(!form && !href && !defaultData) || disableSecondaryTrigger}
75
+ >
76
+ {secondaryTriggerIcon || (
77
+ <Cog className={cn('h-5 w-5', secondaryTriggerTitle ? 'mr-1' : '')} />
78
+ )}
79
+ {secondaryTriggerTitle}
80
+ </Button>
81
+ ),
82
+ showSecondaryTrigger,
83
+ showCustomSecondaryTrigger,
84
+ showDefaultFormAsSecondary,
85
+ setOpen,
86
+ }: Props<T>) {
87
+ return (
88
+ <div className="flex flex-col justify-between gap-4 rounded-lg border border-border bg-foreground/5 p-4 md:flex-row md:items-start">
89
+ <div className="w-full">
90
+ {title || <h1 className="w-full text-2xl font-bold">{pluralTitle}</h1>}
91
+ {description && (
92
+ <div className="whitespace-pre-wrap text-foreground/80">
93
+ {description}
94
+ </div>
95
+ )}
96
+ </div>
97
+ {(form ||
98
+ action ||
99
+ showDefaultFormAsSecondary ||
100
+ (showSecondaryTrigger && !showCustomSecondaryTrigger) ||
101
+ (showSecondaryTrigger && secondaryTrigger)) && (
102
+ <div className="flex flex-col items-center justify-center gap-2 md:flex-row">
103
+ {showDefaultFormAsSecondary ||
104
+ (showSecondaryTrigger && !showCustomSecondaryTrigger) ? (
105
+ <ModifiableDialogTrigger
106
+ data={defaultData}
107
+ trigger={secondaryTrigger}
108
+ form={form}
109
+ open={open}
110
+ setOpen={setOpen}
111
+ editDescription={secondaryDescription}
112
+ requireExpansion={requireExpansion}
113
+ title={secondaryTitle}
114
+ forceDefault
115
+ />
116
+ ) : (
117
+ showSecondaryTrigger && secondaryTrigger
118
+ )}
119
+ {action || (
120
+ <ModifiableDialogTrigger
121
+ data={data}
122
+ trigger={primaryTrigger}
123
+ form={form}
124
+ open={open}
125
+ setOpen={setOpen}
126
+ createDescription={createDescription}
127
+ requireExpansion={requireExpansion}
128
+ title={singularTitle}
129
+ />
130
+ )}
131
+ </div>
132
+ )}
133
+ </div>
134
+ );
135
+ }