@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.
Files changed (39) hide show
  1. package/dist/index.d.ts +364 -1
  2. package/dist/index.js +2209 -493
  3. package/package.json +6 -2
  4. package/src/components/alert.tsx +1 -1
  5. package/src/components/app-bar.tsx +2 -2
  6. package/src/components/color-picker.tsx +352 -0
  7. package/src/components/data-grid.tsx +304 -0
  8. package/src/components/dialog.tsx +3 -2
  9. package/src/components/diff-viewer.tsx +155 -0
  10. package/src/components/direction-provider.tsx +27 -0
  11. package/src/components/drawer.tsx +1 -1
  12. package/src/components/dropdown-menu.tsx +9 -9
  13. package/src/components/input-group.tsx +2 -2
  14. package/src/components/json-viewer.tsx +168 -0
  15. package/src/components/kanban-board.tsx +247 -0
  16. package/src/components/markdown-editor.tsx +288 -0
  17. package/src/components/message-bubble.tsx +83 -0
  18. package/src/components/multi-select.tsx +134 -0
  19. package/src/components/native-select.tsx +2 -2
  20. package/src/components/notification-center.tsx +119 -0
  21. package/src/components/select.tsx +2 -2
  22. package/src/components/sheet.tsx +5 -2
  23. package/src/components/tour.tsx +156 -0
  24. package/src/components/tree-view.tsx +273 -0
  25. package/src/components/virtual-list.tsx +92 -0
  26. package/src/index.ts +24 -0
  27. package/src/stories/ColorPicker.stories.tsx +28 -0
  28. package/src/stories/DataGrid.stories.tsx +54 -0
  29. package/src/stories/DiffViewer.stories.tsx +26 -0
  30. package/src/stories/JsonViewer.stories.tsx +31 -0
  31. package/src/stories/KanbanBoard.stories.tsx +44 -0
  32. package/src/stories/MarkdownEditor.stories.tsx +25 -0
  33. package/src/stories/MessageBubble.stories.tsx +32 -0
  34. package/src/stories/MultiSelect.stories.tsx +35 -0
  35. package/src/stories/NotificationCenter.stories.tsx +41 -0
  36. package/src/stories/Tour.stories.tsx +70 -0
  37. package/src/stories/TreeView.stories.tsx +38 -0
  38. package/src/stories/VirtualList.stories.tsx +36 -0
  39. package/tailwind.config.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/ui",
3
- "version": "0.11.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",
@@ -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]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
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
- "ml-auto inline-flex size-9 items-center justify-center rounded-md text-foreground outline-none transition-colors md:hidden",
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("ml-auto flex shrink-0 items-center gap-2", className)} {...props} />
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 };