@moontra/moonui 2.0.8 → 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.
- package/package.json +9 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/ui/__tests__/accordion.test.tsx +571 -0
- package/src/components/ui/__tests__/alert.test.tsx +484 -0
- package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
- package/src/components/ui/__tests__/avatar.test.tsx +382 -0
- package/src/components/ui/__tests__/badge.test.tsx +364 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
- package/src/components/ui/__tests__/button.test.tsx +91 -0
- package/src/components/ui/__tests__/card.test.tsx +104 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
- package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
- package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
- package/src/components/ui/__tests__/command.test.tsx +677 -0
- package/src/components/ui/__tests__/data-table.test.tsx +256 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
- package/src/components/ui/__tests__/dialog.test.tsx +764 -0
- package/src/components/ui/__tests__/input.test.tsx +318 -0
- package/src/components/ui/__tests__/label.test.tsx +103 -0
- package/src/components/ui/__tests__/popover.test.tsx +637 -0
- package/src/components/ui/__tests__/progress.test.tsx +382 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
- package/src/components/ui/__tests__/select.test.tsx +705 -0
- package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
- package/src/components/ui/__tests__/slider.test.tsx +493 -0
- package/src/components/ui/__tests__/switch.test.tsx +372 -0
- package/src/components/ui/__tests__/table.test.tsx +824 -0
- package/src/components/ui/__tests__/tabs.test.tsx +887 -0
- package/src/components/ui/__tests__/toast.test.tsx +458 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/alert.tsx +130 -0
- package/src/components/ui/aspect-ratio.tsx +72 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/breadcrumb.tsx +207 -0
- package/src/components/ui/button.stories.tsx +162 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/card.stories.tsx +208 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/collapsible.tsx +129 -0
- package/src/components/ui/color-picker.tsx +664 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/data-table.stories.tsx +509 -0
- package/src/components/ui/data-table.tsx +623 -0
- package/src/components/ui/date-picker.stories.tsx +321 -0
- package/src/components/ui/date-picker.tsx +603 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +400 -0
- package/src/components/ui/github-stars.tsx +201 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/input.stories.tsx +255 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/locked-component.tsx +215 -0
- package/src/components/ui/moon-logo.tsx +97 -0
- package/src/components/ui/pagination.tsx +116 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/progress.tsx +182 -0
- package/src/components/ui/radio-group.tsx +243 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/simple-editor.tsx +652 -0
- package/src/components/ui/skeleton.tsx +351 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +322 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +46 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/docs/theme-system.md +1194 -0
- package/src/index.tsx +39 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/theme.tsx +398 -0
- package/src/lib/utils.ts +69 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- 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";
|