@hilum/ui 0.1.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.js +3 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.d.ts +992 -0
  7. package/dist/index.js +4433 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tokens.css +265 -0
  10. package/dist/tokens.d.ts +189 -0
  11. package/dist/tokens.js +193 -0
  12. package/dist/tokens.js.map +1 -0
  13. package/package.json +80 -0
  14. package/src/components/__tests__/data.test.tsx +180 -0
  15. package/src/components/__tests__/disclosure.test.tsx +198 -0
  16. package/src/components/__tests__/display.test.tsx +262 -0
  17. package/src/components/__tests__/feedback.test.tsx +138 -0
  18. package/src/components/__tests__/form.test.tsx +297 -0
  19. package/src/components/__tests__/layout.test.tsx +388 -0
  20. package/src/components/__tests__/overlay.test.tsx +90 -0
  21. package/src/components/__tests__/primitives.test.tsx +248 -0
  22. package/src/components/accordion.tsx +68 -0
  23. package/src/components/action-panel.tsx +112 -0
  24. package/src/components/activity-feed.tsx +69 -0
  25. package/src/components/alert-dialog.tsx +132 -0
  26. package/src/components/alert.tsx +44 -0
  27. package/src/components/aspect-ratio.tsx +19 -0
  28. package/src/components/avatar-stack.tsx +70 -0
  29. package/src/components/avatar.tsx +103 -0
  30. package/src/components/badge.tsx +33 -0
  31. package/src/components/breadcrumb.tsx +76 -0
  32. package/src/components/button-group.tsx +45 -0
  33. package/src/components/button.tsx +60 -0
  34. package/src/components/calendar.tsx +61 -0
  35. package/src/components/card-heading.tsx +60 -0
  36. package/src/components/card.tsx +120 -0
  37. package/src/components/carousel.tsx +222 -0
  38. package/src/components/chart.tsx +77 -0
  39. package/src/components/checkbox.tsx +35 -0
  40. package/src/components/collapsible.tsx +9 -0
  41. package/src/components/color-input.tsx +90 -0
  42. package/src/components/color-picker.tsx +111 -0
  43. package/src/components/combobox.tsx +207 -0
  44. package/src/components/command-palette.tsx +137 -0
  45. package/src/components/command.tsx +248 -0
  46. package/src/components/context-menu.tsx +201 -0
  47. package/src/components/data-table.tsx +151 -0
  48. package/src/components/date-picker.tsx +56 -0
  49. package/src/components/description-list.tsx +70 -0
  50. package/src/components/dialog.tsx +97 -0
  51. package/src/components/drawer.tsx +90 -0
  52. package/src/components/dropdown-menu.tsx +202 -0
  53. package/src/components/empty-state.tsx +43 -0
  54. package/src/components/field.tsx +45 -0
  55. package/src/components/grid-list.tsx +159 -0
  56. package/src/components/hover-card.tsx +34 -0
  57. package/src/components/input-group.tsx +134 -0
  58. package/src/components/input-number.tsx +142 -0
  59. package/src/components/input-otp.tsx +68 -0
  60. package/src/components/input.tsx +23 -0
  61. package/src/components/kbd.tsx +18 -0
  62. package/src/components/label.tsx +23 -0
  63. package/src/components/media-object.tsx +49 -0
  64. package/src/components/menubar.tsx +235 -0
  65. package/src/components/native-select.tsx +44 -0
  66. package/src/components/navigation-menu.tsx +158 -0
  67. package/src/components/notification.tsx +109 -0
  68. package/src/components/page-heading.tsx +125 -0
  69. package/src/components/pagination.tsx +89 -0
  70. package/src/components/popover.tsx +35 -0
  71. package/src/components/progress.tsx +23 -0
  72. package/src/components/property-row.tsx +56 -0
  73. package/src/components/radio-card.tsx +105 -0
  74. package/src/components/radio-group.tsx +38 -0
  75. package/src/components/resizable.tsx +74 -0
  76. package/src/components/scroll-area.tsx +42 -0
  77. package/src/components/section-heading.tsx +64 -0
  78. package/src/components/select.tsx +130 -0
  79. package/src/components/separator.tsx +25 -0
  80. package/src/components/sheet.tsx +117 -0
  81. package/src/components/sidebar.tsx +636 -0
  82. package/src/components/skeleton.tsx +14 -0
  83. package/src/components/slider.tsx +24 -0
  84. package/src/components/sonner.tsx +34 -0
  85. package/src/components/spinner.tsx +37 -0
  86. package/src/components/stacked-list.tsx +64 -0
  87. package/src/components/stat-card.tsx +54 -0
  88. package/src/components/steps.tsx +179 -0
  89. package/src/components/switch.tsx +33 -0
  90. package/src/components/table.tsx +101 -0
  91. package/src/components/tabs.tsx +55 -0
  92. package/src/components/textarea.tsx +21 -0
  93. package/src/components/toggle-group.tsx +49 -0
  94. package/src/components/toggle.tsx +66 -0
  95. package/src/components/tooltip.tsx +43 -0
  96. package/src/icons.ts +158 -0
  97. package/src/index.ts +84 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/tokens/tokens.ts +194 -0
@@ -0,0 +1,222 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
5
+ import { ArrowLeft, ArrowRight } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+ import { Button } from "./button";
8
+
9
+ type CarouselApi = UseEmblaCarouselType[1];
10
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
11
+ type CarouselOptions = UseCarouselParameters[0];
12
+ type CarouselPlugin = UseCarouselParameters[1];
13
+
14
+ interface CarouselProps {
15
+ opts?: CarouselOptions;
16
+ plugins?: CarouselPlugin;
17
+ orientation?: "horizontal" | "vertical";
18
+ setApi?: (api: CarouselApi) => void;
19
+ }
20
+
21
+ type CarouselContextProps = {
22
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
23
+ api: ReturnType<typeof useEmblaCarousel>[1];
24
+ scrollPrev: () => void;
25
+ scrollNext: () => void;
26
+ canScrollPrev: boolean;
27
+ canScrollNext: boolean;
28
+ } & CarouselProps;
29
+
30
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
31
+
32
+ function useCarousel() {
33
+ const context = React.useContext(CarouselContext);
34
+ if (!context) throw new Error("useCarousel must be used within a <Carousel />");
35
+ return context;
36
+ }
37
+
38
+ function Carousel({
39
+ opts,
40
+ plugins,
41
+ orientation = "horizontal",
42
+ setApi,
43
+ className,
44
+ children,
45
+ ...props
46
+ }: CarouselProps & React.ComponentProps<"div">) {
47
+ const [carouselRef, emblaApi] = useEmblaCarousel(
48
+ {
49
+ ...opts,
50
+ axis: orientation === "vertical" ? "y" : "x",
51
+ },
52
+ plugins,
53
+ );
54
+
55
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
56
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
57
+
58
+ const onSelect = React.useCallback((api: CarouselApi) => {
59
+ if (!api) return;
60
+ setCanScrollPrev(api.canScrollPrev());
61
+ setCanScrollNext(api.canScrollNext());
62
+ }, []);
63
+
64
+ const scrollPrev = React.useCallback(() => {
65
+ emblaApi?.scrollPrev();
66
+ }, [emblaApi]);
67
+
68
+ const scrollNext = React.useCallback(() => {
69
+ emblaApi?.scrollNext();
70
+ }, [emblaApi]);
71
+
72
+ const handleKeyDown = React.useCallback(
73
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
74
+ if (event.key === "ArrowLeft") {
75
+ event.preventDefault();
76
+ scrollPrev();
77
+ } else if (event.key === "ArrowRight") {
78
+ event.preventDefault();
79
+ scrollNext();
80
+ }
81
+ },
82
+ [scrollPrev, scrollNext],
83
+ );
84
+
85
+ React.useEffect(() => {
86
+ if (!emblaApi) return;
87
+ onSelect(emblaApi);
88
+ emblaApi.on("reInit", onSelect);
89
+ emblaApi.on("select", onSelect);
90
+ return () => {
91
+ emblaApi.off("reInit", onSelect);
92
+ emblaApi.off("select", onSelect);
93
+ };
94
+ }, [emblaApi, onSelect]);
95
+
96
+ React.useEffect(() => {
97
+ if (!emblaApi || !setApi) return;
98
+ setApi(emblaApi);
99
+ }, [emblaApi, setApi]);
100
+
101
+ return (
102
+ <CarouselContext.Provider
103
+ value={{
104
+ carouselRef,
105
+ api: emblaApi,
106
+ opts,
107
+ orientation,
108
+ scrollPrev,
109
+ scrollNext,
110
+ canScrollPrev,
111
+ canScrollNext,
112
+ }}
113
+ >
114
+ <div
115
+ onKeyDownCapture={handleKeyDown}
116
+ className={cn("relative", className)}
117
+ role="region"
118
+ aria-roledescription="carousel"
119
+ {...props}
120
+ >
121
+ {children}
122
+ </div>
123
+ </CarouselContext.Provider>
124
+ );
125
+ }
126
+
127
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
128
+ const { carouselRef, orientation } = useCarousel();
129
+
130
+ return (
131
+ <div ref={carouselRef} className="overflow-hidden">
132
+ <div
133
+ className={cn("flex", orientation === "vertical" ? "-mt-4 flex-col" : "-ml-4", className)}
134
+ {...props}
135
+ />
136
+ </div>
137
+ );
138
+ }
139
+
140
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
141
+ const { orientation } = useCarousel();
142
+
143
+ return (
144
+ <div
145
+ role="group"
146
+ aria-roledescription="slide"
147
+ className={cn(
148
+ "min-w-0 shrink-0 grow-0 basis-full",
149
+ orientation === "vertical" ? "pt-4" : "pl-4",
150
+ className,
151
+ )}
152
+ {...props}
153
+ />
154
+ );
155
+ }
156
+
157
+ function CarouselPrevious({
158
+ className,
159
+ variant = "outline",
160
+ size = "icon",
161
+ ...props
162
+ }: React.ComponentProps<typeof Button>) {
163
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
164
+
165
+ return (
166
+ <Button
167
+ variant={variant}
168
+ size={size}
169
+ className={cn(
170
+ "absolute h-9 w-9 rounded-full",
171
+ orientation === "vertical"
172
+ ? "-top-12 left-1/2 -translate-x-1/2 rotate-90"
173
+ : "-left-12 top-1/2 -translate-y-1/2",
174
+ className,
175
+ )}
176
+ disabled={!canScrollPrev}
177
+ onClick={scrollPrev}
178
+ {...props}
179
+ >
180
+ <ArrowLeft className="size-4" />
181
+ <span className="sr-only">Previous slide</span>
182
+ </Button>
183
+ );
184
+ }
185
+
186
+ function CarouselNext({
187
+ className,
188
+ variant = "outline",
189
+ size = "icon",
190
+ ...props
191
+ }: React.ComponentProps<typeof Button>) {
192
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
193
+
194
+ return (
195
+ <Button
196
+ variant={variant}
197
+ size={size}
198
+ className={cn(
199
+ "absolute h-9 w-9 rounded-full",
200
+ orientation === "vertical"
201
+ ? "-bottom-12 left-1/2 -translate-x-1/2 rotate-90"
202
+ : "-right-12 top-1/2 -translate-y-1/2",
203
+ className,
204
+ )}
205
+ disabled={!canScrollNext}
206
+ onClick={scrollNext}
207
+ {...props}
208
+ >
209
+ <ArrowRight className="size-4" />
210
+ <span className="sr-only">Next slide</span>
211
+ </Button>
212
+ );
213
+ }
214
+
215
+ export {
216
+ type CarouselApi,
217
+ Carousel,
218
+ CarouselContent,
219
+ CarouselItem,
220
+ CarouselPrevious,
221
+ CarouselNext,
222
+ };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ResponsiveContainer, type TooltipContentProps } from "recharts";
5
+ import { cn } from "../lib/utils";
6
+
7
+ // Color palette for charts
8
+ export const CHART_COLORS = {
9
+ primary: "#FF4D01", // brand-primary
10
+ secondary: "#CDEA19", // brand-secondary
11
+ tertiary: "#FDE086", // brand-secondary
12
+ muted: "#a8978a", // taupe-400
13
+ subtle: "#e3dcd4", // taupe-200
14
+ dark: "#26181a", // taupe-900
15
+ };
16
+
17
+ interface ChartContainerProps {
18
+ children: React.ReactNode;
19
+ className?: string;
20
+ height?: number;
21
+ }
22
+
23
+ function ChartContainer({ children, className, height = 300 }: ChartContainerProps) {
24
+ return (
25
+ <div className={cn("w-full", className)}>
26
+ <ResponsiveContainer width="100%" height={height}>
27
+ {children as React.ReactElement}
28
+ </ResponsiveContainer>
29
+ </div>
30
+ );
31
+ }
32
+ ChartContainer.displayName = "ChartContainer";
33
+
34
+ // Custom tooltip component styled to match the design system
35
+ interface ChartTooltipProps extends Partial<TooltipContentProps<number, string>> {
36
+ labelClassName?: string;
37
+ }
38
+
39
+ function ChartTooltip({ active, payload, label, labelClassName }: ChartTooltipProps) {
40
+ if (!active || !payload?.length) return null;
41
+ return (
42
+ <div className="rounded-xl border border-taupe-100 bg-white px-3 py-2 shadow-elevated">
43
+ {label && <p className={cn("mb-1 label text-taupe-400", labelClassName)}>{label}</p>}
44
+ {payload.map((entry, i) => (
45
+ <div key={i} className="flex items-center gap-2">
46
+ <span
47
+ className="inline-block h-2 w-2 rounded-full"
48
+ style={{ backgroundColor: entry.color }}
49
+ />
50
+ <span className="caption text-taupe-500">{entry.name}:</span>
51
+ <span className="caption font-medium text-taupe-900">{entry.value}</span>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ );
56
+ }
57
+ ChartTooltip.displayName = "ChartTooltip";
58
+
59
+ export { ChartContainer, ChartTooltip };
60
+
61
+ export {
62
+ BarChart,
63
+ Bar,
64
+ LineChart,
65
+ Line,
66
+ AreaChart,
67
+ Area,
68
+ PieChart,
69
+ Pie,
70
+ Cell,
71
+ CartesianGrid,
72
+ XAxis,
73
+ YAxis,
74
+ Tooltip as RechartsTooltip,
75
+ Legend,
76
+ type TooltipProps as RechartsTooltipProps,
77
+ } from "recharts";
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Checkbox } from "radix-ui";
5
+ import { Check, Minus } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const CheckboxRoot = React.forwardRef<
9
+ React.ComponentRef<typeof Checkbox.Root>,
10
+ React.ComponentPropsWithoutRef<typeof Checkbox.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <Checkbox.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "peer size-4 shrink-0 rounded border border-taupe-300 bg-white transition-colors",
16
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30",
17
+ "disabled:cursor-not-allowed disabled:opacity-50",
18
+ "data-[state=checked]:bg-brand-primary data-[state=checked]:border-brand-primary",
19
+ "data-[state=indeterminate]:bg-brand-primary data-[state=indeterminate]:border-brand-primary",
20
+ className,
21
+ )}
22
+ {...props}
23
+ >
24
+ <Checkbox.Indicator className="flex items-center justify-center text-white">
25
+ {props.checked === "indeterminate" ? (
26
+ <Minus size={11} strokeWidth={3} />
27
+ ) : (
28
+ <Check size={11} strokeWidth={3} />
29
+ )}
30
+ </Checkbox.Indicator>
31
+ </Checkbox.Root>
32
+ ));
33
+ CheckboxRoot.displayName = "Checkbox";
34
+
35
+ export { CheckboxRoot as Checkbox };
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import { Collapsible } from "radix-ui";
4
+
5
+ const CollapsibleRoot = Collapsible.Root;
6
+ const CollapsibleTrigger = Collapsible.Trigger;
7
+ const CollapsibleContent = Collapsible.Content;
8
+
9
+ export { CollapsibleRoot as Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+ import { ColorPicker } from "./color-picker";
6
+
7
+ interface ColorInputProps {
8
+ value: string;
9
+ onChange: (next: string) => void;
10
+ /** Show an opacity slider (0–100). Pass `opacity` and `onOpacityChange`. */
11
+ opacity?: number;
12
+ onOpacityChange?: (next: number) => void;
13
+ className?: string;
14
+ disabled?: boolean;
15
+ presets?: string[];
16
+ }
17
+
18
+ /**
19
+ * Compact, inline color input designed for designer property panels.
20
+ * Combines a swatch trigger (ColorPicker), a hex input, and an optional opacity field.
21
+ */
22
+ function ColorInput({
23
+ value,
24
+ onChange,
25
+ opacity,
26
+ onOpacityChange,
27
+ className,
28
+ disabled,
29
+ presets,
30
+ }: ColorInputProps) {
31
+ const [hex, setHex] = React.useState(value);
32
+
33
+ React.useEffect(() => {
34
+ setHex(value);
35
+ }, [value]);
36
+
37
+ const commitHex = (next: string) => {
38
+ if (/^#([0-9a-fA-F]{3}){1,2}$/.test(next)) onChange(next);
39
+ else setHex(value);
40
+ };
41
+
42
+ return (
43
+ <div
44
+ className={cn(
45
+ "inline-flex h-8 items-stretch gap-0 rounded-md border border-taupe-200 bg-white overflow-hidden",
46
+ "focus-within:ring-2 focus-within:ring-brand-primary/20 focus-within:border-brand-primary",
47
+ disabled && "opacity-50 pointer-events-none",
48
+ className,
49
+ )}
50
+ >
51
+ <ColorPicker
52
+ value={value}
53
+ onChange={onChange}
54
+ presets={presets}
55
+ disabled={disabled}
56
+ className="h-full w-8 border-0 rounded-none focus-visible:ring-0"
57
+ />
58
+ <input
59
+ type="text"
60
+ value={hex.replace(/^#/, "")}
61
+ onChange={(e) => setHex("#" + e.target.value.replace(/^#/, ""))}
62
+ onBlur={(e) => commitHex("#" + e.target.value.replace(/^#/, ""))}
63
+ onKeyDown={(e) => {
64
+ if (e.key === "Enter")
65
+ commitHex("#" + (e.target as HTMLInputElement).value.replace(/^#/, ""));
66
+ }}
67
+ spellCheck={false}
68
+ className="w-[5.5rem] caption text-taupe-900 px-2 bg-transparent border-l border-taupe-100 focus:outline-none uppercase"
69
+ />
70
+ {typeof opacity === "number" && onOpacityChange && (
71
+ <div className="relative flex items-center border-l border-taupe-100">
72
+ <input
73
+ type="number"
74
+ min={0}
75
+ max={100}
76
+ value={Math.round(opacity)}
77
+ onChange={(e) => onOpacityChange(Number(e.target.value))}
78
+ className="w-12 caption text-taupe-900 px-2 bg-transparent focus:outline-none text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
79
+ />
80
+ <span aria-hidden className="caption-xs text-taupe-400 pr-2">
81
+ %
82
+ </span>
83
+ </div>
84
+ )}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export { ColorInput };
90
+ export type { ColorInputProps };
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+ import { Popover } from "radix-ui";
6
+
7
+ interface ColorPickerProps {
8
+ value: string;
9
+ onChange: (next: string) => void;
10
+ className?: string;
11
+ disabled?: boolean;
12
+ /** Optional preset palette shown above the picker. */
13
+ presets?: string[];
14
+ /** Accessible label for the trigger. */
15
+ ariaLabel?: string;
16
+ }
17
+
18
+ function isValidHex(s: string): boolean {
19
+ return /^#([0-9a-fA-F]{3}){1,2}$/.test(s);
20
+ }
21
+
22
+ function ColorPicker({
23
+ value,
24
+ onChange,
25
+ className,
26
+ disabled,
27
+ presets,
28
+ ariaLabel = "Color",
29
+ }: ColorPickerProps) {
30
+ const [hex, setHex] = React.useState(value);
31
+
32
+ React.useEffect(() => {
33
+ setHex(value);
34
+ }, [value]);
35
+
36
+ const commit = (next: string) => {
37
+ if (isValidHex(next)) onChange(next);
38
+ };
39
+
40
+ return (
41
+ <Popover.Root>
42
+ <Popover.Trigger asChild>
43
+ <button
44
+ type="button"
45
+ disabled={disabled}
46
+ aria-label={ariaLabel}
47
+ className={cn(
48
+ "inline-flex h-8 w-8 items-center justify-center rounded-md border border-taupe-200 bg-white",
49
+ "transition-colors hover:border-taupe-300",
50
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/20 focus-visible:border-brand-primary",
51
+ "disabled:cursor-not-allowed disabled:opacity-50",
52
+ className,
53
+ )}
54
+ >
55
+ <span
56
+ aria-hidden
57
+ className="h-5 w-5 rounded-sm border border-black/5"
58
+ style={{ backgroundColor: value }}
59
+ />
60
+ </button>
61
+ </Popover.Trigger>
62
+ <Popover.Portal>
63
+ <Popover.Content
64
+ align="start"
65
+ sideOffset={6}
66
+ className="z-50 w-56 rounded-xl bg-white p-3 shadow-natural outline-none"
67
+ >
68
+ <input
69
+ type="color"
70
+ value={isValidHex(hex) ? hex : "#000000"}
71
+ onChange={(e) => {
72
+ setHex(e.target.value);
73
+ onChange(e.target.value);
74
+ }}
75
+ className="h-10 w-full cursor-pointer rounded-md border border-taupe-200 bg-white p-0.5"
76
+ />
77
+ <div className="mt-2 flex items-center gap-2">
78
+ <span className="caption text-taupe-500">Hex</span>
79
+ <input
80
+ type="text"
81
+ value={hex}
82
+ onChange={(e) => setHex(e.target.value)}
83
+ onBlur={(e) => commit(e.target.value)}
84
+ onKeyDown={(e) => {
85
+ if (e.key === "Enter") commit((e.target as HTMLInputElement).value);
86
+ }}
87
+ className="flex-1 h-7 rounded-md border border-taupe-200 bg-white px-2 caption text-taupe-900 focus:outline-none focus:border-brand-primary"
88
+ />
89
+ </div>
90
+ {presets && presets.length > 0 && (
91
+ <div className="mt-3 grid grid-cols-8 gap-1.5">
92
+ {presets.map((p) => (
93
+ <button
94
+ key={p}
95
+ type="button"
96
+ onClick={() => onChange(p)}
97
+ aria-label={p}
98
+ className="h-5 w-5 rounded-sm border border-black/10 transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30"
99
+ style={{ backgroundColor: p }}
100
+ />
101
+ ))}
102
+ </div>
103
+ )}
104
+ </Popover.Content>
105
+ </Popover.Portal>
106
+ </Popover.Root>
107
+ );
108
+ }
109
+
110
+ export { ColorPicker };
111
+ export type { ColorPickerProps };