@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,108 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
|
7
|
+
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
|
11
|
+
return (
|
|
12
|
+
<ol
|
|
13
|
+
data-slot="breadcrumb-list"
|
|
14
|
+
className={cn(
|
|
15
|
+
'flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5',
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
|
24
|
+
return (
|
|
25
|
+
<li
|
|
26
|
+
data-slot="breadcrumb-item"
|
|
27
|
+
className={cn('inline-flex items-center gap-1.5', className)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function BreadcrumbLink({
|
|
34
|
+
asChild,
|
|
35
|
+
className,
|
|
36
|
+
...props
|
|
37
|
+
}: React.ComponentProps<'a'> & {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
}) {
|
|
40
|
+
const Comp = asChild ? Slot : 'a';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Comp
|
|
44
|
+
data-slot="breadcrumb-link"
|
|
45
|
+
className={cn('transition-colors hover:text-foreground', className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
|
52
|
+
return (
|
|
53
|
+
<span
|
|
54
|
+
data-slot="breadcrumb-page"
|
|
55
|
+
role="link"
|
|
56
|
+
aria-disabled="true"
|
|
57
|
+
aria-current="page"
|
|
58
|
+
className={cn('font-normal text-foreground', className)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function BreadcrumbSeparator({
|
|
65
|
+
children,
|
|
66
|
+
className,
|
|
67
|
+
...props
|
|
68
|
+
}: React.ComponentProps<'li'>) {
|
|
69
|
+
return (
|
|
70
|
+
<li
|
|
71
|
+
data-slot="breadcrumb-separator"
|
|
72
|
+
role="presentation"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
className={cn('[&>svg]:size-3.5', className)}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{children ?? <ChevronRight />}
|
|
78
|
+
</li>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function BreadcrumbEllipsis({
|
|
83
|
+
className,
|
|
84
|
+
...props
|
|
85
|
+
}: React.ComponentProps<'span'>) {
|
|
86
|
+
return (
|
|
87
|
+
<span
|
|
88
|
+
data-slot="breadcrumb-ellipsis"
|
|
89
|
+
role="presentation"
|
|
90
|
+
aria-hidden="true"
|
|
91
|
+
className={cn('flex size-9 items-center justify-center', className)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<MoreHorizontal className="size-4" />
|
|
95
|
+
<span className="sr-only">More</span>
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
Breadcrumb,
|
|
102
|
+
BreadcrumbEllipsis,
|
|
103
|
+
BreadcrumbItem,
|
|
104
|
+
BreadcrumbLink,
|
|
105
|
+
BreadcrumbList,
|
|
106
|
+
BreadcrumbPage,
|
|
107
|
+
BreadcrumbSeparator,
|
|
108
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
|
|
13
|
+
destructive:
|
|
14
|
+
'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
|
|
15
|
+
outline:
|
|
16
|
+
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
|
17
|
+
secondary:
|
|
18
|
+
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
|
19
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
20
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
24
|
+
xs: 'h-8 rounded-md has-[>svg]:px-2',
|
|
25
|
+
sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5',
|
|
26
|
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
27
|
+
icon: 'size-9',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: 'default',
|
|
32
|
+
size: 'default',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function Button({
|
|
38
|
+
className,
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
asChild = false,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<'button'> &
|
|
44
|
+
VariantProps<typeof buttonVariants> & {
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot : 'button';
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
data-slot="button"
|
|
52
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type ButtonProps = React.ComponentProps<typeof Button>;
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants };
|
|
61
|
+
export type { ButtonProps };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { buttonVariants } from './button';
|
|
4
|
+
import { DateInput } from './custom/date-input';
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from './select';
|
|
12
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
13
|
+
import { format } from 'date-fns';
|
|
14
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { DayPicker } from 'react-day-picker';
|
|
17
|
+
|
|
18
|
+
export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
|
|
19
|
+
// eslint-disable-next-line no-unused-vars
|
|
20
|
+
onSubmit?: (date: Date) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function Calendar({
|
|
24
|
+
className,
|
|
25
|
+
classNames,
|
|
26
|
+
showOutsideDays = true,
|
|
27
|
+
onSubmit,
|
|
28
|
+
...props
|
|
29
|
+
}: CalendarProps) {
|
|
30
|
+
const defaultMonth = props.defaultMonth || new Date();
|
|
31
|
+
const [month, setMonth] = React.useState<Date>(defaultMonth);
|
|
32
|
+
|
|
33
|
+
const years = Array.from({ length: 200 }, (_, i) => {
|
|
34
|
+
const year = new Date().getFullYear() - 100 + i;
|
|
35
|
+
return { value: year.toString(), label: year.toString() };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const months = Array.from({ length: 12 }, (_, i) => {
|
|
39
|
+
const month = new Date(2024, i, 1);
|
|
40
|
+
return {
|
|
41
|
+
value: i.toString(),
|
|
42
|
+
label: format(month, 'MMMM'),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const currentYear = new Date().getFullYear();
|
|
47
|
+
const isCurrentYear = month.getFullYear() === currentYear;
|
|
48
|
+
const isCurrentMonth =
|
|
49
|
+
month.getMonth() === new Date().getMonth() && isCurrentYear;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-4">
|
|
53
|
+
{props.mode === 'single' && (
|
|
54
|
+
<div className="flex items-center justify-center border-b p-2">
|
|
55
|
+
<DateInput
|
|
56
|
+
value={props.selected as Date}
|
|
57
|
+
onChange={props.onSelect as any}
|
|
58
|
+
onSubmit={onSubmit}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<div className="flex items-center justify-between gap-2 border-b px-2 pb-4">
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => {
|
|
67
|
+
const prev = new Date(month);
|
|
68
|
+
prev.setMonth(prev.getMonth() - 1);
|
|
69
|
+
setMonth(prev);
|
|
70
|
+
}}
|
|
71
|
+
className={cn(
|
|
72
|
+
buttonVariants({ variant: 'ghost', size: 'icon' }),
|
|
73
|
+
'h-7 w-7 transition-colors hover:bg-accent/50'
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<ChevronLeft className="h-4 w-4" />
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<Select
|
|
81
|
+
value={month.getFullYear().toString()}
|
|
82
|
+
onValueChange={(year) => {
|
|
83
|
+
const newDate = new Date(month);
|
|
84
|
+
newDate.setFullYear(parseInt(year));
|
|
85
|
+
setMonth(newDate);
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<SelectTrigger
|
|
89
|
+
className={cn(
|
|
90
|
+
'h-8 w-[90px] transition-colors',
|
|
91
|
+
isCurrentYear && 'font-medium text-primary'
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<SelectValue placeholder="Year" />
|
|
95
|
+
</SelectTrigger>
|
|
96
|
+
<SelectContent
|
|
97
|
+
position="popper"
|
|
98
|
+
className="h-[300px] overflow-y-auto"
|
|
99
|
+
>
|
|
100
|
+
<div className="sticky top-0 -mx-1 flex items-center justify-center border-b bg-background py-1">
|
|
101
|
+
<div className="px-2 text-sm font-medium text-muted-foreground">
|
|
102
|
+
{currentYear}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
{years.map((year) => (
|
|
106
|
+
<SelectItem
|
|
107
|
+
key={year.value}
|
|
108
|
+
value={year.value}
|
|
109
|
+
className={cn(
|
|
110
|
+
'transition-colors',
|
|
111
|
+
parseInt(year.value) === currentYear &&
|
|
112
|
+
'font-medium text-primary'
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{year.label}
|
|
116
|
+
</SelectItem>
|
|
117
|
+
))}
|
|
118
|
+
</SelectContent>
|
|
119
|
+
</Select>
|
|
120
|
+
|
|
121
|
+
<Select
|
|
122
|
+
value={month.getMonth().toString()}
|
|
123
|
+
onValueChange={(monthValue) => {
|
|
124
|
+
const newDate = new Date(month);
|
|
125
|
+
newDate.setMonth(parseInt(monthValue));
|
|
126
|
+
setMonth(newDate);
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<SelectTrigger
|
|
130
|
+
className={cn(
|
|
131
|
+
'h-8 w-[130px] transition-colors',
|
|
132
|
+
isCurrentMonth && 'font-medium text-primary'
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<SelectValue placeholder="Month" />
|
|
136
|
+
</SelectTrigger>
|
|
137
|
+
<SelectContent position="popper">
|
|
138
|
+
{months.map((month) => (
|
|
139
|
+
<SelectItem
|
|
140
|
+
key={month.value}
|
|
141
|
+
value={month.value}
|
|
142
|
+
className="capitalize"
|
|
143
|
+
>
|
|
144
|
+
{month.label}
|
|
145
|
+
</SelectItem>
|
|
146
|
+
))}
|
|
147
|
+
</SelectContent>
|
|
148
|
+
</Select>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<button
|
|
152
|
+
onClick={() => {
|
|
153
|
+
const next = new Date(month);
|
|
154
|
+
next.setMonth(next.getMonth() + 1);
|
|
155
|
+
setMonth(next);
|
|
156
|
+
}}
|
|
157
|
+
className={cn(
|
|
158
|
+
buttonVariants({ variant: 'ghost', size: 'icon' }),
|
|
159
|
+
'h-7 w-7 transition-colors hover:bg-accent/50'
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<ChevronRight className="h-4 w-4" />
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<DayPicker
|
|
167
|
+
{...props}
|
|
168
|
+
month={month}
|
|
169
|
+
onMonthChange={setMonth}
|
|
170
|
+
defaultMonth={defaultMonth}
|
|
171
|
+
showOutsideDays={showOutsideDays}
|
|
172
|
+
className={cn('', className)}
|
|
173
|
+
classNames={{
|
|
174
|
+
root: 'bg-transparent',
|
|
175
|
+
months: 'flex flex-col',
|
|
176
|
+
month:
|
|
177
|
+
'space-y-4 min-w-[276px] text-center p-2 font-semibold shrink-0',
|
|
178
|
+
caption: 'hidden',
|
|
179
|
+
nav: 'hidden',
|
|
180
|
+
nav_button: 'hidden',
|
|
181
|
+
table: 'w-full border-collapse',
|
|
182
|
+
head_row: 'grid grid-cols-7 gap-1',
|
|
183
|
+
weekday:
|
|
184
|
+
'text-muted-foreground rounded-md font-normal text-[0.8rem] text-center',
|
|
185
|
+
row: 'grid grid-cols-7 gap-1 mt-2',
|
|
186
|
+
day: 'text-center text-sm p-0 relative w-9',
|
|
187
|
+
day_button: cn(
|
|
188
|
+
buttonVariants({ variant: 'ghost' }),
|
|
189
|
+
'h-9 w-full rounded-md p-0 font-normal transition-colors duration-300',
|
|
190
|
+
'aria-selected:bg-foreground aria-selected:text-background',
|
|
191
|
+
'hover:bg-accent/50 hover:text-accent-foreground',
|
|
192
|
+
'hover:aria-selected:bg-foreground hover:aria-selected:text-background'
|
|
193
|
+
),
|
|
194
|
+
selected: '!bg-foreground !text-background rounded-md',
|
|
195
|
+
today: 'bg-accent text-accent-foreground rounded-md font-medium',
|
|
196
|
+
outside: 'text-muted-foreground opacity-50',
|
|
197
|
+
disabled: 'text-muted-foreground opacity-50',
|
|
198
|
+
range_start: '!bg-foreground !text-background rounded-l-md',
|
|
199
|
+
range_end: '!bg-foreground !text-background rounded-r-md',
|
|
200
|
+
range_middle: 'aria-selected:bg-foreground/20',
|
|
201
|
+
hidden: 'invisible',
|
|
202
|
+
month_grid: 'w-full',
|
|
203
|
+
...classNames,
|
|
204
|
+
}}
|
|
205
|
+
fixedWeeks
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { Calendar };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot="card"
|
|
8
|
+
className={cn(
|
|
9
|
+
'rounded-xl border bg-card text-card-foreground shadow-sm',
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
data-slot="card-header"
|
|
21
|
+
className={cn('flex flex-col gap-1.5 p-6', className)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
data-slot="card-title"
|
|
31
|
+
className={cn('leading-none font-semibold tracking-tight', className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="card-description"
|
|
41
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="card-content"
|
|
51
|
+
className={cn('p-6 pt-0', className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
data-slot="card-footer"
|
|
61
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
Card,
|
|
69
|
+
CardContent,
|
|
70
|
+
CardDescription,
|
|
71
|
+
CardFooter,
|
|
72
|
+
CardHeader,
|
|
73
|
+
CardTitle,
|
|
74
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from './button';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import useEmblaCarousel, {
|
|
6
|
+
type UseEmblaCarouselType,
|
|
7
|
+
} from 'embla-carousel-react';
|
|
8
|
+
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
|
|
11
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
12
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
13
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
14
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
15
|
+
|
|
16
|
+
type CarouselProps = {
|
|
17
|
+
opts?: CarouselOptions;
|
|
18
|
+
plugins?: CarouselPlugin;
|
|
19
|
+
orientation?: 'horizontal' | 'vertical';
|
|
20
|
+
setApi?: (api: CarouselApi) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CarouselContextProps = {
|
|
24
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
25
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
26
|
+
scrollPrev: () => void;
|
|
27
|
+
scrollNext: () => void;
|
|
28
|
+
canScrollPrev: boolean;
|
|
29
|
+
canScrollNext: boolean;
|
|
30
|
+
} & CarouselProps;
|
|
31
|
+
|
|
32
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
33
|
+
|
|
34
|
+
function useCarousel() {
|
|
35
|
+
const context = React.useContext(CarouselContext);
|
|
36
|
+
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error('useCarousel must be used within a <Carousel />');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Carousel({
|
|
45
|
+
orientation = 'horizontal',
|
|
46
|
+
opts,
|
|
47
|
+
setApi,
|
|
48
|
+
plugins,
|
|
49
|
+
className,
|
|
50
|
+
children,
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<'div'> & CarouselProps) {
|
|
53
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
54
|
+
{
|
|
55
|
+
...opts,
|
|
56
|
+
axis: orientation === 'horizontal' ? 'x' : 'y',
|
|
57
|
+
},
|
|
58
|
+
plugins
|
|
59
|
+
);
|
|
60
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
61
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
62
|
+
|
|
63
|
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
64
|
+
if (!api) return;
|
|
65
|
+
setCanScrollPrev(api.canScrollPrev());
|
|
66
|
+
setCanScrollNext(api.canScrollNext());
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const scrollPrev = React.useCallback(() => {
|
|
70
|
+
api?.scrollPrev();
|
|
71
|
+
}, [api]);
|
|
72
|
+
|
|
73
|
+
const scrollNext = React.useCallback(() => {
|
|
74
|
+
api?.scrollNext();
|
|
75
|
+
}, [api]);
|
|
76
|
+
|
|
77
|
+
const handleKeyDown = React.useCallback(
|
|
78
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
79
|
+
if (event.key === 'ArrowLeft') {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
scrollPrev();
|
|
82
|
+
} else if (event.key === 'ArrowRight') {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
scrollNext();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[scrollPrev, scrollNext]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
if (!api || !setApi) return;
|
|
92
|
+
setApi(api);
|
|
93
|
+
}, [api, setApi]);
|
|
94
|
+
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
if (!api) return;
|
|
97
|
+
onSelect(api);
|
|
98
|
+
api.on('reInit', onSelect);
|
|
99
|
+
api.on('select', onSelect);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
api?.off('select', onSelect);
|
|
103
|
+
};
|
|
104
|
+
}, [api, onSelect]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<CarouselContext.Provider
|
|
108
|
+
value={{
|
|
109
|
+
carouselRef,
|
|
110
|
+
api: api,
|
|
111
|
+
opts,
|
|
112
|
+
orientation:
|
|
113
|
+
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
|
114
|
+
scrollPrev,
|
|
115
|
+
scrollNext,
|
|
116
|
+
canScrollPrev,
|
|
117
|
+
canScrollNext,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
onKeyDownCapture={handleKeyDown}
|
|
122
|
+
className={cn('relative', className)}
|
|
123
|
+
role="region"
|
|
124
|
+
aria-roledescription="carousel"
|
|
125
|
+
data-slot="carousel"
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
</div>
|
|
130
|
+
</CarouselContext.Provider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
135
|
+
const { carouselRef, orientation } = useCarousel();
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
ref={carouselRef}
|
|
140
|
+
className="overflow-hidden"
|
|
141
|
+
data-slot="carousel-content"
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
className={cn(
|
|
145
|
+
'flex',
|
|
146
|
+
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
|
|
156
|
+
const { orientation } = useCarousel();
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
role="group"
|
|
161
|
+
aria-roledescription="slide"
|
|
162
|
+
data-slot="carousel-item"
|
|
163
|
+
className={cn(
|
|
164
|
+
'min-w-0 shrink-0 grow-0 basis-full',
|
|
165
|
+
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
|
166
|
+
className
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function CarouselPrevious({
|
|
174
|
+
className,
|
|
175
|
+
variant = 'outline',
|
|
176
|
+
size = 'icon',
|
|
177
|
+
...props
|
|
178
|
+
}: React.ComponentProps<typeof Button>) {
|
|
179
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Button
|
|
183
|
+
data-slot="carousel-previous"
|
|
184
|
+
variant={variant}
|
|
185
|
+
size={size}
|
|
186
|
+
className={cn(
|
|
187
|
+
'absolute size-8 rounded-full',
|
|
188
|
+
orientation === 'horizontal'
|
|
189
|
+
? 'top-1/2 -left-12 -translate-y-1/2'
|
|
190
|
+
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
|
191
|
+
className
|
|
192
|
+
)}
|
|
193
|
+
disabled={!canScrollPrev}
|
|
194
|
+
onClick={scrollPrev}
|
|
195
|
+
{...props}
|
|
196
|
+
>
|
|
197
|
+
<ArrowLeft />
|
|
198
|
+
<span className="sr-only">Previous slide</span>
|
|
199
|
+
</Button>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function CarouselNext({
|
|
204
|
+
className,
|
|
205
|
+
variant = 'outline',
|
|
206
|
+
size = 'icon',
|
|
207
|
+
...props
|
|
208
|
+
}: React.ComponentProps<typeof Button>) {
|
|
209
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<Button
|
|
213
|
+
data-slot="carousel-next"
|
|
214
|
+
variant={variant}
|
|
215
|
+
size={size}
|
|
216
|
+
className={cn(
|
|
217
|
+
'absolute size-8 rounded-full',
|
|
218
|
+
orientation === 'horizontal'
|
|
219
|
+
? 'top-1/2 -right-12 -translate-y-1/2'
|
|
220
|
+
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
|
221
|
+
className
|
|
222
|
+
)}
|
|
223
|
+
disabled={!canScrollNext}
|
|
224
|
+
onClick={scrollNext}
|
|
225
|
+
{...props}
|
|
226
|
+
>
|
|
227
|
+
<ArrowRight />
|
|
228
|
+
<span className="sr-only">Next slide</span>
|
|
229
|
+
</Button>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
Carousel,
|
|
235
|
+
CarouselContent,
|
|
236
|
+
CarouselItem,
|
|
237
|
+
CarouselNext,
|
|
238
|
+
CarouselPrevious,
|
|
239
|
+
type CarouselApi,
|
|
240
|
+
};
|