@moontra/moonui 2.0.7 → 2.0.9

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 (94) hide show
  1. package/package.json +26 -19
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,664 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../../lib/utils";
5
+ import { Button } from "./button";
6
+ import { Input } from "./input";
7
+ import { Label } from "./label";
8
+ import {
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ } from "./popover";
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
14
+ import { Slider } from "./slider";
15
+ import { cva, type VariantProps } from "class-variance-authority";
16
+ import { motion, AnimatePresence } from "framer-motion";
17
+ import { Copy, Check, Palette, Pipette } from "lucide-react";
18
+
19
+ /**
20
+ * ColorPicker Component
21
+ *
22
+ * A comprehensive color picker with multiple input methods
23
+ */
24
+
25
+ const colorPickerVariants = cva(
26
+ "relative inline-flex items-center justify-center",
27
+ {
28
+ variants: {
29
+ size: {
30
+ default: "h-10 w-10",
31
+ sm: "h-8 w-8",
32
+ lg: "h-12 w-12",
33
+ xl: "h-16 w-16",
34
+ },
35
+ shape: {
36
+ square: "rounded-md",
37
+ circle: "rounded-full",
38
+ rounded: "rounded-lg",
39
+ },
40
+ },
41
+ defaultVariants: {
42
+ size: "default",
43
+ shape: "square",
44
+ },
45
+ }
46
+ );
47
+
48
+ // Color utilities
49
+ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
50
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
51
+ return result
52
+ ? {
53
+ r: parseInt(result[1], 16),
54
+ g: parseInt(result[2], 16),
55
+ b: parseInt(result[3], 16),
56
+ }
57
+ : null;
58
+ }
59
+
60
+ function rgbToHex(r: number, g: number, b: number): string {
61
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
62
+ }
63
+
64
+ function hexToHsl(hex: string): { h: number; s: number; l: number } | null {
65
+ const rgb = hexToRgb(hex);
66
+ if (!rgb) return null;
67
+
68
+ const r = rgb.r / 255;
69
+ const g = rgb.g / 255;
70
+ const b = rgb.b / 255;
71
+
72
+ const max = Math.max(r, g, b);
73
+ const min = Math.min(r, g, b);
74
+ let h = 0;
75
+ let s = 0;
76
+ const l = (max + min) / 2;
77
+
78
+ if (max !== min) {
79
+ const d = max - min;
80
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
81
+ switch (max) {
82
+ case r:
83
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
84
+ break;
85
+ case g:
86
+ h = ((b - r) / d + 2) / 6;
87
+ break;
88
+ case b:
89
+ h = ((r - g) / d + 4) / 6;
90
+ break;
91
+ }
92
+ }
93
+
94
+ return {
95
+ h: Math.round(h * 360),
96
+ s: Math.round(s * 100),
97
+ l: Math.round(l * 100),
98
+ };
99
+ }
100
+
101
+ function hslToHex(h: number, s: number, l: number): string {
102
+ h = h / 360;
103
+ s = s / 100;
104
+ l = l / 100;
105
+
106
+ let r, g, b;
107
+
108
+ if (s === 0) {
109
+ r = g = b = l;
110
+ } else {
111
+ const hue2rgb = (p: number, q: number, t: number) => {
112
+ if (t < 0) t += 1;
113
+ if (t > 1) t -= 1;
114
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
115
+ if (t < 1 / 2) return q;
116
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
117
+ return p;
118
+ };
119
+
120
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
121
+ const p = 2 * l - q;
122
+ r = hue2rgb(p, q, h + 1 / 3);
123
+ g = hue2rgb(p, q, h);
124
+ b = hue2rgb(p, q, h - 1 / 3);
125
+ }
126
+
127
+ return rgbToHex(
128
+ Math.round(r * 255),
129
+ Math.round(g * 255),
130
+ Math.round(b * 255)
131
+ );
132
+ }
133
+
134
+ export interface ColorPickerProps
135
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "value">,
136
+ VariantProps<typeof colorPickerVariants> {
137
+ /**
138
+ * Current color value
139
+ */
140
+ value?: string;
141
+ /**
142
+ * Callback when color changes
143
+ */
144
+ onChange?: (color: string) => void;
145
+ /**
146
+ * Show color input
147
+ */
148
+ showInput?: boolean;
149
+ /**
150
+ * Show preset colors
151
+ */
152
+ showPresets?: boolean;
153
+ /**
154
+ * Preset colors
155
+ */
156
+ presets?: string[];
157
+ /**
158
+ * Allow opacity
159
+ */
160
+ allowOpacity?: boolean;
161
+ /**
162
+ * Color format
163
+ */
164
+ format?: "hex" | "rgb" | "hsl";
165
+ /**
166
+ * Disable copy button
167
+ */
168
+ disableCopy?: boolean;
169
+ /**
170
+ * Trigger mode
171
+ */
172
+ triggerMode?: "click" | "hover";
173
+ }
174
+
175
+ // Default preset colors
176
+ const defaultPresets = [
177
+ "#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff",
178
+ "#ffff00", "#ff00ff", "#00ffff", "#ff8800", "#8800ff",
179
+ "#88ff00", "#0088ff", "#ff0088", "#00ff88", "#888888",
180
+ "#f87171", "#fb923c", "#fbbf24", "#facc15", "#a3e635",
181
+ "#4ade80", "#34d399", "#2dd4bf", "#22d3ee", "#38bdf8",
182
+ "#60a5fa", "#818cf8", "#a78bfa", "#c084fc", "#e879f9",
183
+ "#f472b6", "#fb7185", "#f43f5e", "#ef4444", "#dc2626",
184
+ ];
185
+
186
+ export function ColorPicker({
187
+ className,
188
+ size,
189
+ shape,
190
+ value = "#000000",
191
+ onChange,
192
+ showInput = true,
193
+ showPresets = true,
194
+ presets = defaultPresets,
195
+ allowOpacity = false,
196
+ format = "hex",
197
+ disableCopy = false,
198
+ triggerMode = "click",
199
+ disabled,
200
+ ...props
201
+ }: ColorPickerProps) {
202
+ const [open, setOpen] = React.useState(false);
203
+ const [color, setColor] = React.useState(value);
204
+ const [opacity, setOpacity] = React.useState(100);
205
+ const [copied, setCopied] = React.useState(false);
206
+ const [inputValue, setInputValue] = React.useState(value);
207
+
208
+ // Color components
209
+ const hsl = React.useMemo(() => hexToHsl(color) || { h: 0, s: 0, l: 0 }, [color]);
210
+ const rgb = React.useMemo(() => hexToRgb(color) || { r: 0, g: 0, b: 0 }, [color]);
211
+
212
+ React.useEffect(() => {
213
+ setColor(value);
214
+ setInputValue(value);
215
+ }, [value]);
216
+
217
+ const handleColorChange = (newColor: string) => {
218
+ setColor(newColor);
219
+ setInputValue(newColor);
220
+ onChange?.(newColor);
221
+ };
222
+
223
+ const handleHslChange = (component: "h" | "s" | "l", value: number) => {
224
+ const newHsl = { ...hsl, [component]: value };
225
+ const newColor = hslToHex(newHsl.h, newHsl.s, newHsl.l);
226
+ handleColorChange(newColor);
227
+ };
228
+
229
+ const handleRgbChange = (component: "r" | "g" | "b", value: number) => {
230
+ const newRgb = { ...rgb, [component]: value };
231
+ const newColor = rgbToHex(newRgb.r, newRgb.g, newRgb.b);
232
+ handleColorChange(newColor);
233
+ };
234
+
235
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
236
+ const newValue = e.target.value;
237
+ setInputValue(newValue);
238
+ if (/^#[0-9A-F]{6}$/i.test(newValue)) {
239
+ handleColorChange(newValue);
240
+ }
241
+ };
242
+
243
+ const copyToClipboard = async () => {
244
+ try {
245
+ let textToCopy = color;
246
+ if (format === "rgb") {
247
+ textToCopy = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
248
+ } else if (format === "hsl") {
249
+ textToCopy = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
250
+ }
251
+ await navigator.clipboard.writeText(textToCopy);
252
+ setCopied(true);
253
+ setTimeout(() => setCopied(false), 2000);
254
+ } catch (err) {
255
+ console.error("Failed to copy:", err);
256
+ }
257
+ };
258
+
259
+ const formatDisplay = () => {
260
+ if (format === "rgb") return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
261
+ if (format === "hsl") return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
262
+ return color;
263
+ };
264
+
265
+ return (
266
+ <Popover open={open} onOpenChange={setOpen}>
267
+ <PopoverTrigger asChild>
268
+ <Button
269
+ variant="outline"
270
+ className={cn(
271
+ colorPickerVariants({ size, shape }),
272
+ "p-0 border-2",
273
+ className
274
+ )}
275
+ style={{ backgroundColor: color }}
276
+ disabled={disabled}
277
+ {...props}
278
+ >
279
+ <span className="sr-only">Pick a color</span>
280
+ </Button>
281
+ </PopoverTrigger>
282
+ <PopoverContent className="w-80" align="start" sideOffset={8}>
283
+ <Tabs defaultValue="picker" className="w-full">
284
+ <TabsList className="grid w-full grid-cols-3">
285
+ <TabsTrigger value="picker">Picker</TabsTrigger>
286
+ <TabsTrigger value="sliders">Sliders</TabsTrigger>
287
+ <TabsTrigger value="input">Input</TabsTrigger>
288
+ </TabsList>
289
+
290
+ <TabsContent value="picker" className="space-y-4">
291
+ {/* Color gradient picker */}
292
+ <div className="relative h-48 rounded-md overflow-hidden">
293
+ <div
294
+ className="absolute inset-0"
295
+ style={{
296
+ background: `linear-gradient(to right, #fff, hsl(${hsl.h}, 100%, 50%))`,
297
+ }}
298
+ />
299
+ <div
300
+ className="absolute inset-0"
301
+ style={{
302
+ background: "linear-gradient(to bottom, transparent, #000)",
303
+ }}
304
+ />
305
+ <motion.div
306
+ className="absolute w-4 h-4 border-2 border-white rounded-full shadow-md cursor-pointer"
307
+ drag
308
+ dragConstraints={{
309
+ left: -8,
310
+ right: 280,
311
+ top: -8,
312
+ bottom: 184,
313
+ }}
314
+ dragElastic={0}
315
+ style={{
316
+ backgroundColor: color,
317
+ left: `${(hsl.s / 100) * 280 - 8}px`,
318
+ top: `${(1 - hsl.l / 100) * 192 - 8}px`,
319
+ }}
320
+ />
321
+ </div>
322
+
323
+ {/* Hue slider */}
324
+ <div className="space-y-2">
325
+ <Label>Hue</Label>
326
+ <div
327
+ className="relative h-3 rounded-full"
328
+ style={{
329
+ background:
330
+ "linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
331
+ }}
332
+ >
333
+ <Slider
334
+ value={[hsl.h]}
335
+ onValueChange={([value]) => handleHslChange("h", value)}
336
+ max={360}
337
+ step={1}
338
+ className="absolute inset-0"
339
+ />
340
+ </div>
341
+ </div>
342
+
343
+ {/* Opacity slider */}
344
+ {allowOpacity && (
345
+ <div className="space-y-2">
346
+ <Label>Opacity</Label>
347
+ <div className="flex items-center gap-3">
348
+ <Slider
349
+ value={[opacity]}
350
+ onValueChange={([value]) => setOpacity(value)}
351
+ max={100}
352
+ step={1}
353
+ className="flex-1"
354
+ />
355
+ <span className="text-sm w-10 text-right">{opacity}%</span>
356
+ </div>
357
+ </div>
358
+ )}
359
+ </TabsContent>
360
+
361
+ <TabsContent value="sliders" className="space-y-4">
362
+ {/* RGB Sliders */}
363
+ <div className="space-y-3">
364
+ <div className="space-y-2">
365
+ <div className="flex items-center justify-between">
366
+ <Label>Red</Label>
367
+ <span className="text-sm text-muted-foreground">{rgb.r}</span>
368
+ </div>
369
+ <Slider
370
+ value={[rgb.r]}
371
+ onValueChange={([value]) => handleRgbChange("r", value)}
372
+ max={255}
373
+ step={1}
374
+ className="[&_[role=slider]]:bg-red-500"
375
+ />
376
+ </div>
377
+ <div className="space-y-2">
378
+ <div className="flex items-center justify-between">
379
+ <Label>Green</Label>
380
+ <span className="text-sm text-muted-foreground">{rgb.g}</span>
381
+ </div>
382
+ <Slider
383
+ value={[rgb.g]}
384
+ onValueChange={([value]) => handleRgbChange("g", value)}
385
+ max={255}
386
+ step={1}
387
+ className="[&_[role=slider]]:bg-green-500"
388
+ />
389
+ </div>
390
+ <div className="space-y-2">
391
+ <div className="flex items-center justify-between">
392
+ <Label>Blue</Label>
393
+ <span className="text-sm text-muted-foreground">{rgb.b}</span>
394
+ </div>
395
+ <Slider
396
+ value={[rgb.b]}
397
+ onValueChange={([value]) => handleRgbChange("b", value)}
398
+ max={255}
399
+ step={1}
400
+ className="[&_[role=slider]]:bg-blue-500"
401
+ />
402
+ </div>
403
+ </div>
404
+
405
+ {/* HSL Info */}
406
+ <div className="pt-2 border-t">
407
+ <div className="grid grid-cols-3 gap-2 text-sm">
408
+ <div>
409
+ <p className="text-muted-foreground">H</p>
410
+ <p className="font-medium">{hsl.h}°</p>
411
+ </div>
412
+ <div>
413
+ <p className="text-muted-foreground">S</p>
414
+ <p className="font-medium">{hsl.s}%</p>
415
+ </div>
416
+ <div>
417
+ <p className="text-muted-foreground">L</p>
418
+ <p className="font-medium">{hsl.l}%</p>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </TabsContent>
423
+
424
+ <TabsContent value="input" className="space-y-4">
425
+ <div className="space-y-2">
426
+ <Label>Hex Color</Label>
427
+ <div className="flex gap-2">
428
+ <Input
429
+ value={inputValue}
430
+ onChange={handleInputChange}
431
+ placeholder="#000000"
432
+ className="font-mono"
433
+ />
434
+ <Button
435
+ size="icon"
436
+ variant="outline"
437
+ onClick={() => handleColorChange(inputValue)}
438
+ >
439
+ <Check className="h-4 w-4" />
440
+ </Button>
441
+ </div>
442
+ </div>
443
+
444
+ <div className="space-y-2">
445
+ <Label>RGB Values</Label>
446
+ <div className="grid grid-cols-3 gap-2">
447
+ <Input
448
+ type="number"
449
+ value={rgb.r}
450
+ onChange={(e) => handleRgbChange("r", parseInt(e.target.value) || 0)}
451
+ min={0}
452
+ max={255}
453
+ className="text-center"
454
+ />
455
+ <Input
456
+ type="number"
457
+ value={rgb.g}
458
+ onChange={(e) => handleRgbChange("g", parseInt(e.target.value) || 0)}
459
+ min={0}
460
+ max={255}
461
+ className="text-center"
462
+ />
463
+ <Input
464
+ type="number"
465
+ value={rgb.b}
466
+ onChange={(e) => handleRgbChange("b", parseInt(e.target.value) || 0)}
467
+ min={0}
468
+ max={255}
469
+ className="text-center"
470
+ />
471
+ </div>
472
+ </div>
473
+ </TabsContent>
474
+ </Tabs>
475
+
476
+ {/* Color preview and copy */}
477
+ <div className="mt-4 flex items-center justify-between p-3 border rounded-lg">
478
+ <div className="flex items-center gap-3">
479
+ <div
480
+ className={cn(
481
+ "h-10 w-10 rounded border-2",
482
+ colorPickerVariants({ shape })
483
+ )}
484
+ style={{ backgroundColor: color }}
485
+ />
486
+ <div>
487
+ <p className="text-sm font-medium">{formatDisplay()}</p>
488
+ <p className="text-xs text-muted-foreground">Current color</p>
489
+ </div>
490
+ </div>
491
+ {!disableCopy && (
492
+ <Button
493
+ size="icon"
494
+ variant="ghost"
495
+ onClick={copyToClipboard}
496
+ className="h-8 w-8"
497
+ >
498
+ <AnimatePresence mode="wait">
499
+ {copied ? (
500
+ <motion.div
501
+ key="check"
502
+ initial={{ scale: 0 }}
503
+ animate={{ scale: 1 }}
504
+ exit={{ scale: 0 }}
505
+ >
506
+ <Check className="h-4 w-4 text-green-600" />
507
+ </motion.div>
508
+ ) : (
509
+ <motion.div
510
+ key="copy"
511
+ initial={{ scale: 0 }}
512
+ animate={{ scale: 1 }}
513
+ exit={{ scale: 0 }}
514
+ >
515
+ <Copy className="h-4 w-4" />
516
+ </motion.div>
517
+ )}
518
+ </AnimatePresence>
519
+ </Button>
520
+ )}
521
+ </div>
522
+
523
+ {/* Preset colors */}
524
+ {showPresets && presets.length > 0 && (
525
+ <div className="mt-4 space-y-2">
526
+ <Label>Preset Colors</Label>
527
+ <div className="grid grid-cols-10 gap-1">
528
+ {presets.map((presetColor) => (
529
+ <button
530
+ key={presetColor}
531
+ className={cn(
532
+ "h-7 w-7 rounded border-2 transition-all hover:scale-110",
533
+ color === presetColor
534
+ ? "border-primary"
535
+ : "border-transparent"
536
+ )}
537
+ style={{ backgroundColor: presetColor }}
538
+ onClick={() => handleColorChange(presetColor)}
539
+ />
540
+ ))}
541
+ </div>
542
+ </div>
543
+ )}
544
+ </PopoverContent>
545
+ </Popover>
546
+ );
547
+ }
548
+
549
+ /**
550
+ * SimpleColorPicker Component
551
+ *
552
+ * A simplified color picker with preset colors only
553
+ */
554
+
555
+ export interface SimpleColorPickerProps {
556
+ value?: string;
557
+ onChange?: (color: string) => void;
558
+ colors?: string[];
559
+ className?: string;
560
+ size?: "sm" | "default" | "lg";
561
+ }
562
+
563
+ export function SimpleColorPicker({
564
+ value = "#000000",
565
+ onChange,
566
+ colors = defaultPresets.slice(0, 10),
567
+ className,
568
+ size = "default",
569
+ }: SimpleColorPickerProps) {
570
+ const sizeClasses = {
571
+ sm: "h-6 w-6",
572
+ default: "h-8 w-8",
573
+ lg: "h-10 w-10",
574
+ };
575
+
576
+ return (
577
+ <div className={cn("flex flex-wrap gap-2", className)}>
578
+ {colors.map((color) => (
579
+ <button
580
+ key={color}
581
+ className={cn(
582
+ "rounded-full border-2 transition-all hover:scale-110",
583
+ sizeClasses[size],
584
+ value === color ? "border-primary ring-2 ring-primary/20" : "border-transparent"
585
+ )}
586
+ style={{ backgroundColor: color }}
587
+ onClick={() => onChange?.(color)}
588
+ />
589
+ ))}
590
+ </div>
591
+ );
592
+ }
593
+
594
+ /**
595
+ * GradientPicker Component
596
+ *
597
+ * Pick gradient colors
598
+ */
599
+
600
+ export interface GradientPickerProps {
601
+ value?: { start: string; end: string; angle?: number };
602
+ onChange?: (gradient: { start: string; end: string; angle: number }) => void;
603
+ className?: string;
604
+ }
605
+
606
+ export function GradientPicker({
607
+ value = { start: "#ff0000", end: "#0000ff", angle: 90 },
608
+ onChange,
609
+ className,
610
+ }: GradientPickerProps) {
611
+ const [gradient, setGradient] = React.useState(value);
612
+
613
+ const handleChange = (field: "start" | "end" | "angle", newValue: string | number) => {
614
+ const newGradient = { ...gradient, [field]: newValue };
615
+ setGradient(newGradient);
616
+ onChange?.(newGradient as { start: string; end: string; angle: number });
617
+ };
618
+
619
+ return (
620
+ <div className={cn("space-y-4", className)}>
621
+ <div
622
+ className="h-20 rounded-lg border-2"
623
+ style={{
624
+ background: `linear-gradient(${gradient.angle}deg, ${gradient.start}, ${gradient.end})`,
625
+ }}
626
+ />
627
+
628
+ <div className="grid grid-cols-2 gap-4">
629
+ <div className="space-y-2">
630
+ <Label>Start Color</Label>
631
+ <ColorPicker
632
+ value={gradient.start}
633
+ onChange={(color) => handleChange("start", color)}
634
+ size="sm"
635
+ />
636
+ </div>
637
+ <div className="space-y-2">
638
+ <Label>End Color</Label>
639
+ <ColorPicker
640
+ value={gradient.end}
641
+ onChange={(color) => handleChange("end", color)}
642
+ size="sm"
643
+ />
644
+ </div>
645
+ </div>
646
+
647
+ <div className="space-y-2">
648
+ <div className="flex items-center justify-between">
649
+ <Label>Angle</Label>
650
+ <span className="text-sm text-muted-foreground">{gradient.angle}°</span>
651
+ </div>
652
+ <Slider
653
+ value={[gradient.angle || 90]}
654
+ onValueChange={([value]) => handleChange("angle", value)}
655
+ max={360}
656
+ step={1}
657
+ />
658
+ </div>
659
+ </div>
660
+ );
661
+ }
662
+
663
+ // Re-export icons used
664
+ export { Palette, Pipette } from "lucide-react";