@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,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';