@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,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useForwardedRef } from '../../hooks/use-forwarded-ref';
|
|
4
|
+
import type { ButtonProps } from './button';
|
|
5
|
+
import { Button } from './button';
|
|
6
|
+
import { Input } from './input';
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
|
8
|
+
import { useTheme } from 'next-themes';
|
|
9
|
+
import { forwardRef, useMemo, useState } from 'react';
|
|
10
|
+
import { HexColorPicker } from 'react-colorful';
|
|
11
|
+
|
|
12
|
+
interface ColorPickerProps {
|
|
13
|
+
text?: string;
|
|
14
|
+
value: string;
|
|
15
|
+
// eslint-disable-next-line no-unused-vars
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
onBlur?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ColorPicker = forwardRef<
|
|
21
|
+
HTMLInputElement,
|
|
22
|
+
Omit<ButtonProps, 'value' | 'onChange' | 'onBlur'> & ColorPickerProps
|
|
23
|
+
>(
|
|
24
|
+
(
|
|
25
|
+
{ disabled, value, onChange, onBlur, text, name, className, ...props },
|
|
26
|
+
forwardedRef
|
|
27
|
+
) => {
|
|
28
|
+
const { resolvedTheme } = useTheme();
|
|
29
|
+
|
|
30
|
+
const ref = useForwardedRef(forwardedRef);
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
|
|
33
|
+
const parsedValue = useMemo(() => {
|
|
34
|
+
return value || '#FFFFFF';
|
|
35
|
+
}, [value]);
|
|
36
|
+
|
|
37
|
+
const color = useMemo(() => {
|
|
38
|
+
return ensureVisibleColor(parsedValue, resolvedTheme as 'light' | 'dark');
|
|
39
|
+
}, [parsedValue, resolvedTheme]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Popover onOpenChange={onChange ? setOpen : () => {}} open={open}>
|
|
43
|
+
<PopoverTrigger asChild disabled={disabled} onBlur={onBlur}>
|
|
44
|
+
<Button
|
|
45
|
+
{...props}
|
|
46
|
+
className={className}
|
|
47
|
+
name={name}
|
|
48
|
+
onClick={
|
|
49
|
+
onChange
|
|
50
|
+
? () => {
|
|
51
|
+
setOpen(true);
|
|
52
|
+
}
|
|
53
|
+
: undefined
|
|
54
|
+
}
|
|
55
|
+
size="icon"
|
|
56
|
+
style={{
|
|
57
|
+
padding: '0.5rem',
|
|
58
|
+
color: color,
|
|
59
|
+
backgroundColor: `${color}1A`, // 10% opacity in hex is 1A
|
|
60
|
+
borderColor: color,
|
|
61
|
+
borderWidth: '1px',
|
|
62
|
+
borderStyle: 'solid',
|
|
63
|
+
}}
|
|
64
|
+
variant="outline"
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
>
|
|
67
|
+
{text || parsedValue}
|
|
68
|
+
</Button>
|
|
69
|
+
</PopoverTrigger>
|
|
70
|
+
<PopoverContent className="flex w-full flex-col items-center justify-center gap-4">
|
|
71
|
+
<HexColorPicker color={parsedValue} onChange={onChange} />
|
|
72
|
+
<Input
|
|
73
|
+
maxLength={7}
|
|
74
|
+
onChange={(e) => {
|
|
75
|
+
if (onChange) onChange(e?.currentTarget?.value);
|
|
76
|
+
}}
|
|
77
|
+
ref={ref}
|
|
78
|
+
value={parsedValue}
|
|
79
|
+
/>
|
|
80
|
+
</PopoverContent>
|
|
81
|
+
</Popover>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
ColorPicker.displayName = 'ColorPicker';
|
|
86
|
+
|
|
87
|
+
export { ColorPicker };
|
|
88
|
+
|
|
89
|
+
export function ensureVisibleColor(
|
|
90
|
+
color: string,
|
|
91
|
+
theme: 'light' | 'dark'
|
|
92
|
+
): string {
|
|
93
|
+
const getLuminance = (r: number, g: number, b: number) => {
|
|
94
|
+
const a = [r, g, b].map((v) => {
|
|
95
|
+
v /= 255;
|
|
96
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
97
|
+
});
|
|
98
|
+
return 0.2126 * a[0]! + 0.7152 * a[1]! + 0.0722 * a[2]!;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getContrastRatio = (l1: number, l2: number) => {
|
|
102
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const parseColor = (color: string) => {
|
|
106
|
+
const num = parseInt(color.slice(1), 16);
|
|
107
|
+
const r = (num >> 16) & 255;
|
|
108
|
+
const g = (num >> 8) & 255;
|
|
109
|
+
const b = num & 255;
|
|
110
|
+
return [r, g, b];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const adjustColor = (r: number, g: number, b: number, factor: number) => {
|
|
114
|
+
r = Math.min(255, Math.max(0, r + factor));
|
|
115
|
+
g = Math.min(255, Math.max(0, g + factor));
|
|
116
|
+
b = Math.min(255, Math.max(0, b + factor));
|
|
117
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const [r, g, b] = parseColor(color);
|
|
121
|
+
const luminance = getLuminance(r!, g!, b!);
|
|
122
|
+
const backgroundLuminance = theme === 'light' ? 1 : 0;
|
|
123
|
+
const contrastRatio = getContrastRatio(luminance, backgroundLuminance);
|
|
124
|
+
|
|
125
|
+
// Adjust color to meet the minimum contrast ratio of 4.5:1
|
|
126
|
+
const minimumContrastRatio = 4.5;
|
|
127
|
+
let factor = 0;
|
|
128
|
+
while (contrastRatio < minimumContrastRatio) {
|
|
129
|
+
factor += theme === 'light' ? -10 : 10;
|
|
130
|
+
const adjustedColor = adjustColor(r!, g!, b!, factor);
|
|
131
|
+
const [newR, newG, newB] = parseColor(adjustedColor);
|
|
132
|
+
const newLuminance = getLuminance(newR!, newG!, newB!);
|
|
133
|
+
const newContrastRatio = getContrastRatio(
|
|
134
|
+
newLuminance,
|
|
135
|
+
backgroundLuminance
|
|
136
|
+
);
|
|
137
|
+
if (newContrastRatio >= minimumContrastRatio) {
|
|
138
|
+
return adjustedColor;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return color;
|
|
143
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from './dialog';
|
|
10
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
11
|
+
import { Command as CommandPrimitive } from 'cmdk';
|
|
12
|
+
import { SearchIcon } from 'lucide-react';
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
|
|
15
|
+
function Command({
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: React.ComponentProps<typeof CommandPrimitive>) {
|
|
19
|
+
return (
|
|
20
|
+
<CommandPrimitive
|
|
21
|
+
data-slot="command"
|
|
22
|
+
className={cn(
|
|
23
|
+
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CommandDialog({
|
|
32
|
+
title = 'Command Palette',
|
|
33
|
+
description = 'Search for a command to run...',
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof Dialog> & {
|
|
37
|
+
title?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<Dialog {...props}>
|
|
42
|
+
<DialogHeader className="sr-only">
|
|
43
|
+
<DialogTitle>{title}</DialogTitle>
|
|
44
|
+
<DialogDescription>{description}</DialogDescription>
|
|
45
|
+
</DialogHeader>
|
|
46
|
+
<DialogContent className="overflow-hidden p-0">
|
|
47
|
+
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
48
|
+
{children}
|
|
49
|
+
</Command>
|
|
50
|
+
</DialogContent>
|
|
51
|
+
</Dialog>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function CommandInput({
|
|
56
|
+
className,
|
|
57
|
+
...props
|
|
58
|
+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="command-input-wrapper"
|
|
62
|
+
className="flex h-9 items-center gap-2 border-b px-3"
|
|
63
|
+
>
|
|
64
|
+
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
|
65
|
+
<CommandPrimitive.Input
|
|
66
|
+
data-slot="command-input"
|
|
67
|
+
className={cn(
|
|
68
|
+
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function CommandList({
|
|
78
|
+
className,
|
|
79
|
+
...props
|
|
80
|
+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
81
|
+
return (
|
|
82
|
+
<CommandPrimitive.List
|
|
83
|
+
data-slot="command-list"
|
|
84
|
+
className={cn(
|
|
85
|
+
'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function CommandEmpty({
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
96
|
+
return (
|
|
97
|
+
<CommandPrimitive.Empty
|
|
98
|
+
data-slot="command-empty"
|
|
99
|
+
className="py-6 text-center text-sm"
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function CommandGroup({
|
|
106
|
+
className,
|
|
107
|
+
...props
|
|
108
|
+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
109
|
+
return (
|
|
110
|
+
<CommandPrimitive.Group
|
|
111
|
+
data-slot="command-group"
|
|
112
|
+
className={cn(
|
|
113
|
+
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
|
114
|
+
className
|
|
115
|
+
)}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function CommandSeparator({
|
|
122
|
+
className,
|
|
123
|
+
...props
|
|
124
|
+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
125
|
+
return (
|
|
126
|
+
<CommandPrimitive.Separator
|
|
127
|
+
data-slot="command-separator"
|
|
128
|
+
className={cn('-mx-1 h-px bg-border', className)}
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function CommandItem({
|
|
135
|
+
className,
|
|
136
|
+
...props
|
|
137
|
+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
138
|
+
return (
|
|
139
|
+
<CommandPrimitive.Item
|
|
140
|
+
data-slot="command-item"
|
|
141
|
+
className={cn(
|
|
142
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
143
|
+
className
|
|
144
|
+
)}
|
|
145
|
+
{...props}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function CommandShortcut({
|
|
151
|
+
className,
|
|
152
|
+
...props
|
|
153
|
+
}: React.ComponentProps<'span'>) {
|
|
154
|
+
return (
|
|
155
|
+
<span
|
|
156
|
+
data-slot="command-shortcut"
|
|
157
|
+
className={cn(
|
|
158
|
+
'ml-auto text-xs tracking-widest text-muted-foreground',
|
|
159
|
+
className
|
|
160
|
+
)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
Command,
|
|
168
|
+
CommandDialog,
|
|
169
|
+
CommandEmpty,
|
|
170
|
+
CommandGroup,
|
|
171
|
+
CommandInput,
|
|
172
|
+
CommandItem,
|
|
173
|
+
CommandList,
|
|
174
|
+
CommandSeparator,
|
|
175
|
+
CommandShortcut,
|
|
176
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
|
|
8
|
+
function ContextMenu({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
|
11
|
+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ContextMenuTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
|
17
|
+
return (
|
|
18
|
+
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ContextMenuGroup({
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
|
25
|
+
return (
|
|
26
|
+
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ContextMenuPortal({
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
|
33
|
+
return (
|
|
34
|
+
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ContextMenuSub({
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
|
41
|
+
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ContextMenuRadioGroup({
|
|
45
|
+
...props
|
|
46
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
|
47
|
+
return (
|
|
48
|
+
<ContextMenuPrimitive.RadioGroup
|
|
49
|
+
data-slot="context-menu-radio-group"
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ContextMenuSubTrigger({
|
|
56
|
+
className,
|
|
57
|
+
inset,
|
|
58
|
+
children,
|
|
59
|
+
...props
|
|
60
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
61
|
+
inset?: boolean;
|
|
62
|
+
}) {
|
|
63
|
+
return (
|
|
64
|
+
<ContextMenuPrimitive.SubTrigger
|
|
65
|
+
data-slot="context-menu-sub-trigger"
|
|
66
|
+
data-inset={inset}
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
<ChevronRightIcon className="ml-auto" />
|
|
75
|
+
</ContextMenuPrimitive.SubTrigger>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ContextMenuSubContent({
|
|
80
|
+
className,
|
|
81
|
+
...props
|
|
82
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
|
83
|
+
return (
|
|
84
|
+
<ContextMenuPrimitive.SubContent
|
|
85
|
+
data-slot="context-menu-sub-content"
|
|
86
|
+
className={cn(
|
|
87
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ContextMenuContent({
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
|
99
|
+
return (
|
|
100
|
+
<ContextMenuPrimitive.Portal>
|
|
101
|
+
<ContextMenuPrimitive.Content
|
|
102
|
+
data-slot="context-menu-content"
|
|
103
|
+
className={cn(
|
|
104
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
105
|
+
className
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
</ContextMenuPrimitive.Portal>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ContextMenuItem({
|
|
114
|
+
className,
|
|
115
|
+
inset,
|
|
116
|
+
variant = 'default',
|
|
117
|
+
...props
|
|
118
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
|
119
|
+
inset?: boolean;
|
|
120
|
+
variant?: 'default' | 'destructive';
|
|
121
|
+
}) {
|
|
122
|
+
return (
|
|
123
|
+
<ContextMenuPrimitive.Item
|
|
124
|
+
data-slot="context-menu-item"
|
|
125
|
+
data-inset={inset}
|
|
126
|
+
data-variant={variant}
|
|
127
|
+
className={cn(
|
|
128
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:!text-destructive",
|
|
129
|
+
className
|
|
130
|
+
)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ContextMenuCheckboxItem({
|
|
137
|
+
className,
|
|
138
|
+
children,
|
|
139
|
+
checked,
|
|
140
|
+
...props
|
|
141
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
|
142
|
+
return (
|
|
143
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
144
|
+
data-slot="context-menu-checkbox-item"
|
|
145
|
+
className={cn(
|
|
146
|
+
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
checked={checked}
|
|
150
|
+
{...props}
|
|
151
|
+
>
|
|
152
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
153
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
154
|
+
<CheckIcon className="size-4" />
|
|
155
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
156
|
+
</span>
|
|
157
|
+
{children}
|
|
158
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ContextMenuRadioItem({
|
|
163
|
+
className,
|
|
164
|
+
children,
|
|
165
|
+
...props
|
|
166
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
|
167
|
+
return (
|
|
168
|
+
<ContextMenuPrimitive.RadioItem
|
|
169
|
+
data-slot="context-menu-radio-item"
|
|
170
|
+
className={cn(
|
|
171
|
+
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
172
|
+
className
|
|
173
|
+
)}
|
|
174
|
+
{...props}
|
|
175
|
+
>
|
|
176
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
177
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
178
|
+
<CircleIcon className="size-2 fill-current" />
|
|
179
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
180
|
+
</span>
|
|
181
|
+
{children}
|
|
182
|
+
</ContextMenuPrimitive.RadioItem>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function ContextMenuLabel({
|
|
187
|
+
className,
|
|
188
|
+
inset,
|
|
189
|
+
...props
|
|
190
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
|
191
|
+
inset?: boolean;
|
|
192
|
+
}) {
|
|
193
|
+
return (
|
|
194
|
+
<ContextMenuPrimitive.Label
|
|
195
|
+
data-slot="context-menu-label"
|
|
196
|
+
data-inset={inset}
|
|
197
|
+
className={cn(
|
|
198
|
+
'px-2 py-1.5 text-sm font-semibold text-foreground data-[inset]:pl-8',
|
|
199
|
+
className
|
|
200
|
+
)}
|
|
201
|
+
{...props}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ContextMenuSeparator({
|
|
207
|
+
className,
|
|
208
|
+
...props
|
|
209
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
|
210
|
+
return (
|
|
211
|
+
<ContextMenuPrimitive.Separator
|
|
212
|
+
data-slot="context-menu-separator"
|
|
213
|
+
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
|
214
|
+
{...props}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ContextMenuShortcut({
|
|
220
|
+
className,
|
|
221
|
+
...props
|
|
222
|
+
}: React.ComponentProps<'span'>) {
|
|
223
|
+
return (
|
|
224
|
+
<span
|
|
225
|
+
data-slot="context-menu-shortcut"
|
|
226
|
+
className={cn(
|
|
227
|
+
'ml-auto text-xs tracking-widest text-muted-foreground',
|
|
228
|
+
className
|
|
229
|
+
)}
|
|
230
|
+
{...props}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
ContextMenu,
|
|
237
|
+
ContextMenuCheckboxItem,
|
|
238
|
+
ContextMenuContent,
|
|
239
|
+
ContextMenuGroup,
|
|
240
|
+
ContextMenuItem,
|
|
241
|
+
ContextMenuLabel,
|
|
242
|
+
ContextMenuPortal,
|
|
243
|
+
ContextMenuRadioGroup,
|
|
244
|
+
ContextMenuRadioItem,
|
|
245
|
+
ContextMenuSeparator,
|
|
246
|
+
ContextMenuShortcut,
|
|
247
|
+
ContextMenuSub,
|
|
248
|
+
ContextMenuSubContent,
|
|
249
|
+
ContextMenuSubTrigger,
|
|
250
|
+
ContextMenuTrigger,
|
|
251
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { useImperativeHandle } from 'react';
|
|
6
|
+
|
|
7
|
+
interface UseAutosizeTextAreaProps {
|
|
8
|
+
textAreaRef: HTMLTextAreaElement | null;
|
|
9
|
+
minHeight?: number;
|
|
10
|
+
maxHeight?: number;
|
|
11
|
+
triggerAutoSize: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useAutosizeTextArea = ({
|
|
15
|
+
textAreaRef,
|
|
16
|
+
triggerAutoSize,
|
|
17
|
+
maxHeight = Number.MAX_SAFE_INTEGER,
|
|
18
|
+
minHeight = 0,
|
|
19
|
+
}: UseAutosizeTextAreaProps) => {
|
|
20
|
+
const [init, setInit] = React.useState(true);
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
|
23
|
+
const offsetBorder = 2;
|
|
24
|
+
if (textAreaRef) {
|
|
25
|
+
if (init) {
|
|
26
|
+
textAreaRef.style.minHeight = `${minHeight + offsetBorder}px`;
|
|
27
|
+
if (maxHeight > minHeight) {
|
|
28
|
+
textAreaRef.style.maxHeight = `${maxHeight}px`;
|
|
29
|
+
}
|
|
30
|
+
setInit(false);
|
|
31
|
+
}
|
|
32
|
+
textAreaRef.style.height = `${minHeight + offsetBorder}px`;
|
|
33
|
+
const scrollHeight = textAreaRef.scrollHeight;
|
|
34
|
+
// We then set the height directly, outside of the render loop
|
|
35
|
+
// Trying to set this with state or a ref will product an incorrect value.
|
|
36
|
+
if (scrollHeight > maxHeight) {
|
|
37
|
+
textAreaRef.style.height = `${maxHeight}px`;
|
|
38
|
+
} else {
|
|
39
|
+
textAreaRef.style.height = `${scrollHeight + offsetBorder}px`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, [init, minHeight, maxHeight, textAreaRef, triggerAutoSize]);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type AutosizeTextAreaRef = {
|
|
46
|
+
textArea: HTMLTextAreaElement;
|
|
47
|
+
maxHeight: number;
|
|
48
|
+
minHeight: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type AutosizeTextAreaProps = {
|
|
52
|
+
maxHeight?: number;
|
|
53
|
+
minHeight?: number;
|
|
54
|
+
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
55
|
+
|
|
56
|
+
export const AutosizeTextarea = React.forwardRef<
|
|
57
|
+
AutosizeTextAreaRef,
|
|
58
|
+
AutosizeTextAreaProps
|
|
59
|
+
>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
maxHeight = Number.MAX_SAFE_INTEGER,
|
|
63
|
+
minHeight = 52,
|
|
64
|
+
className,
|
|
65
|
+
onChange,
|
|
66
|
+
value,
|
|
67
|
+
...props
|
|
68
|
+
}: AutosizeTextAreaProps,
|
|
69
|
+
ref: React.Ref<AutosizeTextAreaRef>
|
|
70
|
+
) => {
|
|
71
|
+
const textAreaRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
72
|
+
const [triggerAutoSize, setTriggerAutoSize] = React.useState('');
|
|
73
|
+
|
|
74
|
+
useAutosizeTextArea({
|
|
75
|
+
textAreaRef: textAreaRef.current,
|
|
76
|
+
triggerAutoSize: triggerAutoSize,
|
|
77
|
+
maxHeight,
|
|
78
|
+
minHeight,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useImperativeHandle(ref, () => ({
|
|
82
|
+
textArea: textAreaRef.current as HTMLTextAreaElement,
|
|
83
|
+
focus: () => textAreaRef.current?.focus(),
|
|
84
|
+
maxHeight,
|
|
85
|
+
minHeight,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
if (value || props?.defaultValue) {
|
|
90
|
+
setTriggerAutoSize(value as string);
|
|
91
|
+
}
|
|
92
|
+
}, [value, props?.defaultValue]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<textarea
|
|
96
|
+
{...props}
|
|
97
|
+
value={value}
|
|
98
|
+
ref={textAreaRef}
|
|
99
|
+
className={cn(
|
|
100
|
+
'scrollbar-none flex w-full overflow-auto rounded-md border border-input bg-background px-3 py-2 text-sm ring-0 ring-transparent outline-hidden placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
onChange={(e) => {
|
|
104
|
+
setTriggerAutoSize(e.target.value);
|
|
105
|
+
onChange?.(e);
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
AutosizeTextarea.displayName = 'AutosizeTextarea';
|