@kinetixui/ui 0.11.0 → 0.13.0
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/dist/index.d.ts +364 -1
- package/dist/index.js +2209 -493
- package/package.json +6 -2
- package/src/components/alert.tsx +1 -1
- package/src/components/app-bar.tsx +2 -2
- package/src/components/color-picker.tsx +352 -0
- package/src/components/data-grid.tsx +304 -0
- package/src/components/dialog.tsx +3 -2
- package/src/components/diff-viewer.tsx +155 -0
- package/src/components/direction-provider.tsx +27 -0
- package/src/components/drawer.tsx +1 -1
- package/src/components/dropdown-menu.tsx +9 -9
- package/src/components/input-group.tsx +2 -2
- package/src/components/json-viewer.tsx +168 -0
- package/src/components/kanban-board.tsx +247 -0
- package/src/components/markdown-editor.tsx +288 -0
- package/src/components/message-bubble.tsx +83 -0
- package/src/components/multi-select.tsx +134 -0
- package/src/components/native-select.tsx +2 -2
- package/src/components/notification-center.tsx +119 -0
- package/src/components/select.tsx +2 -2
- package/src/components/sheet.tsx +5 -2
- package/src/components/tour.tsx +156 -0
- package/src/components/tree-view.tsx +273 -0
- package/src/components/virtual-list.tsx +92 -0
- package/src/index.ts +24 -0
- package/src/stories/ColorPicker.stories.tsx +28 -0
- package/src/stories/DataGrid.stories.tsx +54 -0
- package/src/stories/DiffViewer.stories.tsx +26 -0
- package/src/stories/JsonViewer.stories.tsx +31 -0
- package/src/stories/KanbanBoard.stories.tsx +44 -0
- package/src/stories/MarkdownEditor.stories.tsx +25 -0
- package/src/stories/MessageBubble.stories.tsx +32 -0
- package/src/stories/MultiSelect.stories.tsx +35 -0
- package/src/stories/NotificationCenter.stories.tsx +41 -0
- package/src/stories/Tour.stories.tsx +70 -0
- package/src/stories/TreeView.stories.tsx +38 -0
- package/src/stories/VirtualList.stories.tsx +36 -0
- package/tailwind.config.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinetixui/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
|
|
6
6
|
"keywords": [
|
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
"tailwind.config.ts"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@dnd-kit/core": "^6.3.1",
|
|
44
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
45
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
43
46
|
"@hookform/resolvers": "^5.9.1",
|
|
47
|
+
"@kinetixui/tokens": "0.13.0",
|
|
44
48
|
"@radix-ui/react-accordion": "^1.2.20",
|
|
45
49
|
"@radix-ui/react-alert-dialog": "^1.1.23",
|
|
46
50
|
"@radix-ui/react-aspect-ratio": "^1.1.15",
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"@radix-ui/react-collapsible": "^1.1.20",
|
|
50
54
|
"@radix-ui/react-context-menu": "^2.3.7",
|
|
51
55
|
"@radix-ui/react-dialog": "^1.1.23",
|
|
56
|
+
"@radix-ui/react-direction": "^1.1.4",
|
|
52
57
|
"@radix-ui/react-dropdown-menu": "^2.1.24",
|
|
53
58
|
"@radix-ui/react-hover-card": "^1.1.23",
|
|
54
59
|
"@radix-ui/react-label": "^2.1.15",
|
|
@@ -90,7 +95,6 @@
|
|
|
90
95
|
"react-dom": ">=18"
|
|
91
96
|
},
|
|
92
97
|
"devDependencies": {
|
|
93
|
-
"@kinetixui/tokens": "0.11.0",
|
|
94
98
|
"@testing-library/jest-dom": "^7.0.1",
|
|
95
99
|
"@testing-library/react": "^16.3.3",
|
|
96
100
|
"@testing-library/user-event": "^14.6.7",
|
package/src/components/alert.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
|
|
7
7
|
const alertVariants = cva(
|
|
8
|
-
"relative w-full rounded-lg border px-4 py-3 text-sm font-sans [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:
|
|
8
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm font-sans [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:start-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:ps-7",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -63,7 +63,7 @@ const AppBar = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
|
|
63
63
|
aria-expanded={open}
|
|
64
64
|
onClick={() => setOpen((o) => !o)}
|
|
65
65
|
className={cn(
|
|
66
|
-
"
|
|
66
|
+
"ms-auto inline-flex size-9 items-center justify-center rounded-md text-foreground outline-none transition-colors md:hidden",
|
|
67
67
|
"hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring",
|
|
68
68
|
)}
|
|
69
69
|
>
|
|
@@ -131,7 +131,7 @@ AppBarLink.displayName = "AppBarLink";
|
|
|
131
131
|
|
|
132
132
|
const AppBarActions = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
133
133
|
({ className, ...props }, ref) => (
|
|
134
|
-
<div ref={ref} className={cn("
|
|
134
|
+
<div ref={ref} className={cn("ms-auto flex shrink-0 items-center gap-2", className)} {...props} />
|
|
135
135
|
),
|
|
136
136
|
);
|
|
137
137
|
AppBarActions.displayName = "AppBarActions";
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
5
|
+
import { Pipette } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Input } from "./input";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));
|
|
17
|
+
|
|
18
|
+
function hexToRgba(hex: string): [number, number, number, number] {
|
|
19
|
+
const h = hex.replace("#", "");
|
|
20
|
+
if (h.length === 8) {
|
|
21
|
+
const n = parseInt(h, 16);
|
|
22
|
+
return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, (n & 255) / 255];
|
|
23
|
+
}
|
|
24
|
+
const n = parseInt(h.length === 3 ? h.replace(/(.)/g, "$1$1") : h, 16);
|
|
25
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rgbaToHex(r: number, g: number, b: number, a: number, includeAlpha: boolean): string {
|
|
29
|
+
const toHex = (v: number) => clamp(Math.round(v), 0, 255).toString(16).padStart(2, "0");
|
|
30
|
+
const base = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
31
|
+
return includeAlpha ? `${base}${toHex(a * 255)}` : base;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
|
35
|
+
s /= 100;
|
|
36
|
+
v /= 100;
|
|
37
|
+
const c = v * s;
|
|
38
|
+
const hh = h / 60;
|
|
39
|
+
const x = c * (1 - Math.abs((hh % 2) - 1));
|
|
40
|
+
let r = 0;
|
|
41
|
+
let g = 0;
|
|
42
|
+
let b = 0;
|
|
43
|
+
if (hh < 1) [r, g, b] = [c, x, 0];
|
|
44
|
+
else if (hh < 2) [r, g, b] = [x, c, 0];
|
|
45
|
+
else if (hh < 3) [r, g, b] = [0, c, x];
|
|
46
|
+
else if (hh < 4) [r, g, b] = [0, x, c];
|
|
47
|
+
else if (hh < 5) [r, g, b] = [x, 0, c];
|
|
48
|
+
else [r, g, b] = [c, 0, x];
|
|
49
|
+
const m = v - c;
|
|
50
|
+
return [(r + m) * 255, (g + m) * 255, (b + m) * 255];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
|
54
|
+
r /= 255;
|
|
55
|
+
g /= 255;
|
|
56
|
+
b /= 255;
|
|
57
|
+
const max = Math.max(r, g, b);
|
|
58
|
+
const min = Math.min(r, g, b);
|
|
59
|
+
const d = max - min;
|
|
60
|
+
let h = 0;
|
|
61
|
+
if (d !== 0) {
|
|
62
|
+
switch (max) {
|
|
63
|
+
case r:
|
|
64
|
+
h = ((g - b) / d) % 6;
|
|
65
|
+
break;
|
|
66
|
+
case g:
|
|
67
|
+
h = (b - r) / d + 2;
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
h = (r - g) / d + 4;
|
|
71
|
+
}
|
|
72
|
+
h *= 60;
|
|
73
|
+
if (h < 0) h += 360;
|
|
74
|
+
}
|
|
75
|
+
const s = max === 0 ? 0 : d / max;
|
|
76
|
+
return [h, s * 100, max * 100];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const HEX_PATTERN_ALPHA = /^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/;
|
|
80
|
+
const HEX_PATTERN = /^[0-9a-fA-F]{6}$/;
|
|
81
|
+
|
|
82
|
+
export interface ColorPickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onChange"> {
|
|
83
|
+
/** hex color, e.g. `#3b82f6` (or `#3b82f6ff` when `alpha` is on) */
|
|
84
|
+
value: string;
|
|
85
|
+
onChange: (hex: string) => void;
|
|
86
|
+
/** show an alpha slider and read/emit 8-digit hex */
|
|
87
|
+
alpha?: boolean;
|
|
88
|
+
/** preset swatches row */
|
|
89
|
+
swatches?: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* ColorPicker — a saturation/value square, hue and (optional) alpha
|
|
94
|
+
* sliders, a hex field, and swatches. Deliberately stateful rather than
|
|
95
|
+
* deriving hue/saturation/value from `value` on every render: HSV is a
|
|
96
|
+
* degenerate representation at the edges — saturation 0 (grayscale) or
|
|
97
|
+
* value 0 (black) both erase hue information, since every hue produces
|
|
98
|
+
* the same RGB there. Re-deriving HSV from the emitted hex on every
|
|
99
|
+
* change would make the hue thumb jump to red the moment a drag crosses
|
|
100
|
+
* either edge. Internal HSV state persists across those edges instead,
|
|
101
|
+
* reconciled with the `value` prop only when it changes from something
|
|
102
|
+
* other than this component's own last emission (tracked via a ref) — the
|
|
103
|
+
* standard fix for this class of controlled color-picker bug.
|
|
104
|
+
*
|
|
105
|
+
* The 2D square is hand-rolled (no Radix primitive for a 2D slider); the
|
|
106
|
+
* hue and alpha rails reuse `@radix-ui/react-slider` directly (not the
|
|
107
|
+
* package's own `Slider` wrapper, whose track styling is fixed) for their
|
|
108
|
+
* built-in keyboard and ARIA handling. The eyedropper button only renders
|
|
109
|
+
* when `window.EyeDropper` exists (Chromium-only, behind a feature flag
|
|
110
|
+
* on some browsers) — no polyfill, it just disappears elsewhere.
|
|
111
|
+
*/
|
|
112
|
+
const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
|
|
113
|
+
({ value, onChange, alpha = false, swatches, className, ...props }, ref) => {
|
|
114
|
+
const [hsv, setHsv] = React.useState<[number, number, number]>(() => {
|
|
115
|
+
const [r, g, b] = hexToRgba(value);
|
|
116
|
+
return rgbToHsv(r, g, b);
|
|
117
|
+
});
|
|
118
|
+
const [a, setA] = React.useState(() => hexToRgba(value)[3]);
|
|
119
|
+
const lastEmitted = React.useRef(value);
|
|
120
|
+
const [h, s, v] = hsv;
|
|
121
|
+
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
if (value === lastEmitted.current) return;
|
|
124
|
+
const [r, g, b, na] = hexToRgba(value);
|
|
125
|
+
setHsv(rgbToHsv(r, g, b));
|
|
126
|
+
setA(na);
|
|
127
|
+
lastEmitted.current = value;
|
|
128
|
+
}, [value]);
|
|
129
|
+
|
|
130
|
+
const commit = React.useCallback(
|
|
131
|
+
(nh: number, ns: number, nv: number, na: number) => {
|
|
132
|
+
setHsv([nh, ns, nv]);
|
|
133
|
+
setA(na);
|
|
134
|
+
const [r, g, b] = hsvToRgb(nh, ns, nv);
|
|
135
|
+
const hex = rgbaToHex(r, g, b, na, alpha);
|
|
136
|
+
lastEmitted.current = hex;
|
|
137
|
+
onChange(hex);
|
|
138
|
+
},
|
|
139
|
+
[alpha, onChange],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const squareRef = React.useRef<HTMLDivElement>(null);
|
|
143
|
+
const draggingRef = React.useRef(false);
|
|
144
|
+
|
|
145
|
+
const updateFromPoint = React.useCallback(
|
|
146
|
+
(clientX: number, clientY: number) => {
|
|
147
|
+
const el = squareRef.current;
|
|
148
|
+
if (!el) return;
|
|
149
|
+
const rect = el.getBoundingClientRect();
|
|
150
|
+
const ns = clamp(((clientX - rect.left) / rect.width) * 100, 0, 100);
|
|
151
|
+
const nv = clamp(100 - ((clientY - rect.top) / rect.height) * 100, 0, 100);
|
|
152
|
+
commit(h, ns, nv, a);
|
|
153
|
+
},
|
|
154
|
+
[commit, h, a],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const handleSquarePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
158
|
+
draggingRef.current = true;
|
|
159
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
160
|
+
updateFromPoint(e.clientX, e.clientY);
|
|
161
|
+
};
|
|
162
|
+
const handleSquarePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
163
|
+
if (!draggingRef.current) return;
|
|
164
|
+
updateFromPoint(e.clientX, e.clientY);
|
|
165
|
+
};
|
|
166
|
+
const handleSquarePointerUp = () => {
|
|
167
|
+
draggingRef.current = false;
|
|
168
|
+
};
|
|
169
|
+
const handleSquareKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
170
|
+
const step = e.shiftKey ? 10 : 1;
|
|
171
|
+
if (e.key === "ArrowLeft") commit(h, clamp(s - step, 0, 100), v, a);
|
|
172
|
+
else if (e.key === "ArrowRight") commit(h, clamp(s + step, 0, 100), v, a);
|
|
173
|
+
else if (e.key === "ArrowUp") commit(h, s, clamp(v + step, 0, 100), a);
|
|
174
|
+
else if (e.key === "ArrowDown") commit(h, s, clamp(v - step, 0, 100), a);
|
|
175
|
+
else return;
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const [supportsEyeDropper, setSupportsEyeDropper] = React.useState(false);
|
|
180
|
+
React.useEffect(() => setSupportsEyeDropper(typeof window !== "undefined" && !!window.EyeDropper), []);
|
|
181
|
+
|
|
182
|
+
const pickFromScreen = async () => {
|
|
183
|
+
if (!window.EyeDropper) return;
|
|
184
|
+
try {
|
|
185
|
+
const result = await new window.EyeDropper().open();
|
|
186
|
+
const [r, g, b] = hexToRgba(result.sRGBHex);
|
|
187
|
+
commit(...rgbToHsv(r, g, b), a);
|
|
188
|
+
} catch {
|
|
189
|
+
// cancelled — no-op
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div ref={ref} className={cn("flex w-64 flex-col gap-3 font-sans", className)} {...props}>
|
|
195
|
+
<div
|
|
196
|
+
ref={squareRef}
|
|
197
|
+
role="slider"
|
|
198
|
+
tabIndex={0}
|
|
199
|
+
aria-label="Saturation and value"
|
|
200
|
+
aria-valuetext={`saturation ${Math.round(s)}%, value ${Math.round(v)}%`}
|
|
201
|
+
onPointerDown={handleSquarePointerDown}
|
|
202
|
+
onPointerMove={handleSquarePointerMove}
|
|
203
|
+
onPointerUp={handleSquarePointerUp}
|
|
204
|
+
onKeyDown={handleSquareKeyDown}
|
|
205
|
+
className="relative h-40 w-full touch-none rounded-md outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
206
|
+
style={{
|
|
207
|
+
backgroundColor: `hsl(${h} 100% 50%)`,
|
|
208
|
+
backgroundImage: "linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent)",
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<div
|
|
212
|
+
className="absolute size-3.5 -translate-x-1/2 translate-y-1/2 rounded-full border-2 border-white shadow"
|
|
213
|
+
style={{ left: `${s}%`, bottom: `${v}%`, backgroundColor: `hsl(${h} ${s}% ${v}%)` }}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<SliderPrimitive.Root
|
|
218
|
+
value={[h]}
|
|
219
|
+
min={0}
|
|
220
|
+
max={360}
|
|
221
|
+
step={1}
|
|
222
|
+
onValueChange={([nh]) => commit(nh ?? h, s, v, a)}
|
|
223
|
+
aria-label="Hue"
|
|
224
|
+
className="relative flex h-4 w-full touch-none select-none items-center"
|
|
225
|
+
>
|
|
226
|
+
<SliderPrimitive.Track
|
|
227
|
+
className="relative h-3 w-full grow rounded-full"
|
|
228
|
+
style={{
|
|
229
|
+
backgroundImage:
|
|
230
|
+
"linear-gradient(to right, hsl(0 100% 50%), hsl(60 100% 50%), hsl(120 100% 50%), hsl(180 100% 50%), hsl(240 100% 50%), hsl(300 100% 50%), hsl(360 100% 50%))",
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
<SliderPrimitive.Thumb className="block size-4 rounded-full border-2 border-white bg-background shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
234
|
+
</SliderPrimitive.Root>
|
|
235
|
+
|
|
236
|
+
{alpha && (
|
|
237
|
+
<SliderPrimitive.Root
|
|
238
|
+
value={[Math.round(a * 100)]}
|
|
239
|
+
min={0}
|
|
240
|
+
max={100}
|
|
241
|
+
step={1}
|
|
242
|
+
onValueChange={([na]) => commit(h, s, v, (na ?? a * 100) / 100)}
|
|
243
|
+
aria-label="Alpha"
|
|
244
|
+
className="relative flex h-4 w-full touch-none select-none items-center"
|
|
245
|
+
>
|
|
246
|
+
<SliderPrimitive.Track
|
|
247
|
+
className="relative h-3 w-full grow overflow-hidden rounded-full"
|
|
248
|
+
style={{
|
|
249
|
+
backgroundImage:
|
|
250
|
+
"conic-gradient(#0000001a 90deg, transparent 90deg 180deg, #0000001a 180deg 270deg, transparent 270deg)",
|
|
251
|
+
backgroundSize: "8px 8px",
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<div
|
|
255
|
+
className="absolute inset-0"
|
|
256
|
+
style={{ backgroundImage: `linear-gradient(to right, transparent, hsl(${h} ${s}% ${v}%))` }}
|
|
257
|
+
/>
|
|
258
|
+
</SliderPrimitive.Track>
|
|
259
|
+
<SliderPrimitive.Thumb className="block size-4 rounded-full border-2 border-white bg-background shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
260
|
+
</SliderPrimitive.Root>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
<div className="flex items-center gap-2">
|
|
264
|
+
<div
|
|
265
|
+
className="size-8 shrink-0 rounded-md border"
|
|
266
|
+
style={{
|
|
267
|
+
backgroundImage:
|
|
268
|
+
"conic-gradient(#0000001a 90deg, transparent 90deg 180deg, #0000001a 180deg 270deg, transparent 270deg), linear-gradient(hsl(0 0% 0% / 0), hsl(0 0% 0% / 0))",
|
|
269
|
+
backgroundSize: "8px 8px",
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<div className="size-full rounded-md" style={{ backgroundColor: value }} />
|
|
273
|
+
</div>
|
|
274
|
+
<div className="flex flex-1 items-center gap-1">
|
|
275
|
+
<span className="text-muted-foreground">#</span>
|
|
276
|
+
<HexField value={value} alpha={alpha} onCommit={onChange} />
|
|
277
|
+
</div>
|
|
278
|
+
{supportsEyeDropper && (
|
|
279
|
+
<Button
|
|
280
|
+
type="button"
|
|
281
|
+
variant="Ghost"
|
|
282
|
+
size="icon"
|
|
283
|
+
aria-label="Pick color from screen"
|
|
284
|
+
onClick={pickFromScreen}
|
|
285
|
+
>
|
|
286
|
+
<Pipette className="size-4" />
|
|
287
|
+
</Button>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{swatches && swatches.length > 0 && (
|
|
292
|
+
<div className="flex flex-wrap gap-1.5">
|
|
293
|
+
{swatches.map((sw) => (
|
|
294
|
+
<button
|
|
295
|
+
key={sw}
|
|
296
|
+
type="button"
|
|
297
|
+
aria-label={sw}
|
|
298
|
+
onClick={() => {
|
|
299
|
+
const [r, g, b] = hexToRgba(sw);
|
|
300
|
+
commit(...rgbToHsv(r, g, b), a);
|
|
301
|
+
}}
|
|
302
|
+
className="size-6 rounded-sm border transition-shadow hover:ring-1 hover:ring-ring hover:ring-offset-1"
|
|
303
|
+
style={{ backgroundColor: sw }}
|
|
304
|
+
/>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
ColorPicker.displayName = "ColorPicker";
|
|
313
|
+
|
|
314
|
+
function HexField({
|
|
315
|
+
value,
|
|
316
|
+
alpha,
|
|
317
|
+
onCommit,
|
|
318
|
+
}: {
|
|
319
|
+
value: string;
|
|
320
|
+
alpha: boolean;
|
|
321
|
+
onCommit: (hex: string) => void;
|
|
322
|
+
}) {
|
|
323
|
+
const [draft, setDraft] = React.useState(value.replace("#", "").toUpperCase());
|
|
324
|
+
|
|
325
|
+
React.useEffect(() => setDraft(value.replace("#", "").toUpperCase()), [value]);
|
|
326
|
+
|
|
327
|
+
const commit = () => {
|
|
328
|
+
const pattern = alpha ? HEX_PATTERN_ALPHA : HEX_PATTERN;
|
|
329
|
+
if (pattern.test(draft)) {
|
|
330
|
+
onCommit(`#${draft.toLowerCase()}`);
|
|
331
|
+
} else {
|
|
332
|
+
setDraft(value.replace("#", "").toUpperCase());
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<Input
|
|
338
|
+
value={draft}
|
|
339
|
+
onChange={(e) => setDraft(e.target.value.replace(/[^0-9a-fA-F]/g, "").slice(0, alpha ? 8 : 6))}
|
|
340
|
+
onBlur={commit}
|
|
341
|
+
onKeyDown={(e) => {
|
|
342
|
+
if (e.key === "Enter") {
|
|
343
|
+
commit();
|
|
344
|
+
e.currentTarget.blur();
|
|
345
|
+
}
|
|
346
|
+
}}
|
|
347
|
+
className="h-8 flex-1 px-2 py-1 font-mono text-xs uppercase"
|
|
348
|
+
/>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export { ColorPicker };
|