@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
@@ -30,6 +30,8 @@ const sheetVariants = cva(
30
30
  "fixed z-overlay gap-4 bg-background p-6 shadow-lg transition ease-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-base data-[state=open]:duration-slow font-sans",
31
31
  {
32
32
  variants: {
33
+ // rtl-ok-start: `side` picks a physical screen edge on purpose (a caller-chosen
34
+ // slide-in direction, not text-direction-relative) — left/right stay physical.
33
35
  side: {
34
36
  top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
35
37
  bottom:
@@ -38,6 +40,7 @@ const sheetVariants = cva(
38
40
  right:
39
41
  "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
40
42
  },
43
+ // rtl-ok-end
41
44
  },
42
45
  defaultVariants: { side: "right" },
43
46
  },
@@ -55,7 +58,7 @@ const SheetContent = React.forwardRef<
55
58
  <SheetOverlay />
56
59
  <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
57
60
  {children}
58
- <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-muted transition-opacity hover:opacity-visible focus:outline-none focus:ring-2 focus:ring-ring">
61
+ <SheetPrimitive.Close className="absolute end-4 top-4 rounded-sm opacity-muted transition-opacity hover:opacity-visible focus:outline-none focus:ring-2 focus:ring-ring">
59
62
  <X className="size-4" />
60
63
  <span className="sr-only">Close</span>
61
64
  </SheetPrimitive.Close>
@@ -65,7 +68,7 @@ const SheetContent = React.forwardRef<
65
68
  SheetContent.displayName = SheetPrimitive.Content.displayName;
66
69
 
67
70
  const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
68
- <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
71
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-start", className)} {...props} />
69
72
  );
70
73
  SheetHeader.displayName = "SheetHeader";
71
74
 
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Button } from "./button";
6
+
7
+ /**
8
+ * Tour — sequenced spotlight popovers over real elements (onboarding
9
+ * walkthroughs), with dismiss/skip/next. `target` is a CSS selector
10
+ * resolved against the live DOM on every step change (and on resize/
11
+ * scroll, so the spotlight tracks a target that moves or resizes) rather
12
+ * than a ref, since steps are authored as plain data ahead of the
13
+ * elements existing. The spotlight itself is a single positioned `div`
14
+ * with a `box-shadow: 0 0 0 9999px` — a well-known CSS-only cutout
15
+ * technique, no SVG mask or second overlay layer needed. No focus trap:
16
+ * a tour narrates the page rather than blocking interaction with it, so
17
+ * the underlying page stays operable while a step is shown. Gap-fill
18
+ * addition (not in the original Figma source).
19
+ */
20
+ export interface TourStep {
21
+ /** CSS selector for the element this step spotlights */
22
+ target: string;
23
+ title: React.ReactNode;
24
+ content: React.ReactNode;
25
+ }
26
+
27
+ export interface TourProps {
28
+ steps: TourStep[];
29
+ open: boolean;
30
+ stepIndex: number;
31
+ onStepIndexChange: (index: number) => void;
32
+ onOpenChange: (open: boolean) => void;
33
+ nextLabel?: React.ReactNode;
34
+ backLabel?: React.ReactNode;
35
+ skipLabel?: React.ReactNode;
36
+ doneLabel?: React.ReactNode;
37
+ }
38
+
39
+ const SPOTLIGHT_PADDING = 8;
40
+
41
+ const Tour: React.FC<TourProps> = ({
42
+ steps,
43
+ open,
44
+ stepIndex,
45
+ onStepIndexChange,
46
+ onOpenChange,
47
+ nextLabel = "Next",
48
+ backLabel = "Back",
49
+ skipLabel = "Skip",
50
+ doneLabel = "Done",
51
+ }) => {
52
+ const [mounted, setMounted] = React.useState(false);
53
+ const [rect, setRect] = React.useState<DOMRect | null>(null);
54
+ const step = steps[stepIndex];
55
+
56
+ React.useEffect(() => setMounted(true), []);
57
+
58
+ React.useEffect(() => {
59
+ if (!open || !step) return;
60
+ const update = () => {
61
+ const el = document.querySelector(step.target);
62
+ if (el) {
63
+ setRect(el.getBoundingClientRect());
64
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
65
+ } else {
66
+ setRect(null);
67
+ }
68
+ };
69
+ update();
70
+ window.addEventListener("resize", update);
71
+ window.addEventListener("scroll", update, true);
72
+ return () => {
73
+ window.removeEventListener("resize", update);
74
+ window.removeEventListener("scroll", update, true);
75
+ };
76
+ }, [open, step]);
77
+
78
+ React.useEffect(() => {
79
+ if (!open) return;
80
+ const onKeyDown = (e: KeyboardEvent) => {
81
+ if (e.key === "Escape") onOpenChange(false);
82
+ };
83
+ document.addEventListener("keydown", onKeyDown);
84
+ return () => document.removeEventListener("keydown", onKeyDown);
85
+ }, [open, onOpenChange]);
86
+
87
+ if (!mounted || !open || !step) return null;
88
+
89
+ const isFirst = stepIndex === 0;
90
+ const isLast = stepIndex === steps.length - 1;
91
+
92
+ const spotlightStyle: React.CSSProperties = rect
93
+ ? {
94
+ top: rect.top - SPOTLIGHT_PADDING,
95
+ left: rect.left - SPOTLIGHT_PADDING,
96
+ width: rect.width + SPOTLIGHT_PADDING * 2,
97
+ height: rect.height + SPOTLIGHT_PADDING * 2,
98
+ boxShadow: "0 0 0 9999px rgb(0 0 0 / 0.6)",
99
+ }
100
+ : { inset: 0, background: "rgb(0 0 0 / 0.6)" };
101
+
102
+ const cardStyle: React.CSSProperties = rect
103
+ ? {
104
+ top: Math.min(rect.bottom + SPOTLIGHT_PADDING + 8, window.innerHeight - 220),
105
+ left: Math.min(Math.max(rect.left, 16), window.innerWidth - 336),
106
+ }
107
+ : { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
108
+
109
+ return createPortal(
110
+ <>
111
+ <div
112
+ aria-hidden
113
+ className="pointer-events-none fixed z-overlay rounded-lg transition-all duration-fast"
114
+ style={spotlightStyle}
115
+ />
116
+ <div
117
+ role="dialog"
118
+ aria-modal="true"
119
+ className="fixed z-overlay w-80 rounded-lg border border-border bg-popover p-4 font-sans text-popover-foreground shadow-lg transition-all duration-fast"
120
+ style={cardStyle}
121
+ >
122
+ <p className="text-title-sm font-medium text-foreground">{step.title}</p>
123
+ <div className="mt-1.5 text-body-sm text-muted-foreground">{step.content}</div>
124
+ <div className="mt-4 flex items-center justify-between">
125
+ <span className="text-label-sm text-muted-foreground">
126
+ {stepIndex + 1} / {steps.length}
127
+ </span>
128
+ <div className="flex items-center gap-2">
129
+ {!isFirst && (
130
+ <Button variant="Ghost" size="sm" onClick={() => onStepIndexChange(stepIndex - 1)}>
131
+ {backLabel}
132
+ </Button>
133
+ )}
134
+ {!isLast ? (
135
+ <>
136
+ <Button variant="Link" size="sm" onClick={() => onOpenChange(false)}>
137
+ {skipLabel}
138
+ </Button>
139
+ <Button size="sm" onClick={() => onStepIndexChange(stepIndex + 1)}>
140
+ {nextLabel}
141
+ </Button>
142
+ </>
143
+ ) : (
144
+ <Button size="sm" onClick={() => onOpenChange(false)}>
145
+ {doneLabel}
146
+ </Button>
147
+ )}
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </>,
152
+ document.body,
153
+ );
154
+ };
155
+
156
+ export { Tour };
@@ -0,0 +1,273 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronRight } from "lucide-react";
5
+ import { Checkbox } from "./checkbox";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * TreeView / TreeItem — nested expand/collapse rows with keyboard roving
10
+ * tabindex and optional checkboxes. Sidebar/file/nav trees have no
11
+ * primitive today (`Sidebar` is flat); Radix has no tree component to
12
+ * build on, so this is hand-rolled: `role="treeitem"` sits on each
13
+ * item's own container (not a separate "row" element) so a nested
14
+ * item's `closest('[role="treeitem"]')` correctly walks up to its real
15
+ * ancestor rather than a sibling row, and visible items are queried live
16
+ * from the DOM on every arrow-key press — collapsed subtrees simply
17
+ * don't render, so that query is always exactly the visible, in-order
18
+ * item list with no separate registration bookkeeping to keep in sync.
19
+ * `checkable` checkboxes are independent per item — no automatic
20
+ * parent-selects-all-children / indeterminate propagation. A real,
21
+ * documented simplification, not a silent one. Gap-fill addition (not in
22
+ * the original Figma source).
23
+ */
24
+ interface TreeViewContextValue {
25
+ isExpanded: (value: string) => boolean;
26
+ toggleExpanded: (value: string) => void;
27
+ selected?: string;
28
+ select: (value: string) => void;
29
+ checkable: boolean;
30
+ isChecked: (value: string) => boolean;
31
+ toggleChecked: (value: string) => void;
32
+ tabbableValue: string | null;
33
+ setTabbableValue: (value: string) => void;
34
+ firstValueRef: React.MutableRefObject<string | null>;
35
+ }
36
+
37
+ const TreeViewContext = React.createContext<TreeViewContextValue | null>(null);
38
+
39
+ function useTreeViewContext() {
40
+ const ctx = React.useContext(TreeViewContext);
41
+ if (!ctx) throw new Error("TreeItem must be rendered inside a TreeView");
42
+ return ctx;
43
+ }
44
+
45
+ const TreeLevelContext = React.createContext(0);
46
+
47
+ export interface TreeViewProps extends React.HTMLAttributes<HTMLDivElement> {
48
+ /** show a checkbox on every item */
49
+ checkable?: boolean;
50
+ expanded?: string[];
51
+ defaultExpanded?: string[];
52
+ onExpandedChange?: (expanded: string[]) => void;
53
+ selected?: string;
54
+ onSelectedChange?: (value: string) => void;
55
+ checkedValues?: string[];
56
+ defaultCheckedValues?: string[];
57
+ onCheckedChange?: (checkedValues: string[]) => void;
58
+ }
59
+
60
+ const TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(
61
+ (
62
+ {
63
+ className,
64
+ checkable = false,
65
+ expanded,
66
+ defaultExpanded = [],
67
+ onExpandedChange,
68
+ selected,
69
+ onSelectedChange,
70
+ checkedValues,
71
+ defaultCheckedValues = [],
72
+ onCheckedChange,
73
+ children,
74
+ ...props
75
+ },
76
+ ref,
77
+ ) => {
78
+ const [internalExpanded, setInternalExpanded] = React.useState(defaultExpanded);
79
+ const currentExpanded = expanded ?? internalExpanded;
80
+ const [internalChecked, setInternalChecked] = React.useState(defaultCheckedValues);
81
+ const currentChecked = checkedValues ?? internalChecked;
82
+ const [tabbableValue, setTabbableValue] = React.useState<string | null>(null);
83
+ const firstValueRef = React.useRef<string | null>(null);
84
+
85
+ const toggleExpanded = React.useCallback(
86
+ (value: string) => {
87
+ const next = currentExpanded.includes(value)
88
+ ? currentExpanded.filter((v) => v !== value)
89
+ : [...currentExpanded, value];
90
+ setInternalExpanded(next);
91
+ onExpandedChange?.(next);
92
+ },
93
+ [currentExpanded, onExpandedChange],
94
+ );
95
+
96
+ const toggleChecked = React.useCallback(
97
+ (value: string) => {
98
+ const next = currentChecked.includes(value)
99
+ ? currentChecked.filter((v) => v !== value)
100
+ : [...currentChecked, value];
101
+ setInternalChecked(next);
102
+ onCheckedChange?.(next);
103
+ },
104
+ [currentChecked, onCheckedChange],
105
+ );
106
+
107
+ const contextValue: TreeViewContextValue = {
108
+ isExpanded: (value) => currentExpanded.includes(value),
109
+ toggleExpanded,
110
+ selected,
111
+ select: (value) => onSelectedChange?.(value),
112
+ checkable,
113
+ isChecked: (value) => currentChecked.includes(value),
114
+ toggleChecked,
115
+ tabbableValue,
116
+ setTabbableValue,
117
+ firstValueRef,
118
+ };
119
+
120
+ return (
121
+ <div
122
+ ref={ref}
123
+ role="tree"
124
+ aria-multiselectable={checkable || undefined}
125
+ className={cn("flex flex-col font-sans", className)}
126
+ {...props}
127
+ >
128
+ <TreeViewContext.Provider value={contextValue}>
129
+ <TreeLevelContext.Provider value={0}>{children}</TreeLevelContext.Provider>
130
+ </TreeViewContext.Provider>
131
+ </div>
132
+ );
133
+ },
134
+ );
135
+ TreeView.displayName = "TreeView";
136
+
137
+ export interface TreeItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
138
+ value: string;
139
+ label: React.ReactNode;
140
+ icon?: React.ReactNode;
141
+ disabled?: boolean;
142
+ children?: React.ReactNode;
143
+ }
144
+
145
+ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
146
+ ({ className, value, label, icon, disabled, children, onClick, onKeyDown, ...props }, ref) => {
147
+ const ctx = useTreeViewContext();
148
+ const level = React.useContext(TreeLevelContext);
149
+ const hasChildren = React.Children.count(children) > 0;
150
+ const expanded = ctx.isExpanded(value);
151
+ const isChecked = ctx.isChecked(value);
152
+
153
+ if (ctx.firstValueRef.current === null) ctx.firstValueRef.current = value;
154
+ const isTabbable = (ctx.tabbableValue ?? ctx.firstValueRef.current) === value;
155
+
156
+ const localRef = React.useRef<HTMLDivElement>(null);
157
+ const setRefs = (node: HTMLDivElement | null) => {
158
+ localRef.current = node;
159
+ if (typeof ref === "function") ref(node);
160
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
161
+ };
162
+
163
+ const activate = () => {
164
+ ctx.select(value);
165
+ if (ctx.checkable) ctx.toggleChecked(value);
166
+ };
167
+
168
+ const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
169
+ onKeyDown?.(e);
170
+ if (e.defaultPrevented || disabled) return;
171
+ const root = localRef.current?.closest('[role="tree"]');
172
+ const items = root ? Array.from(root.querySelectorAll<HTMLElement>('[role="treeitem"]')) : [];
173
+ const idx = items.indexOf(e.currentTarget);
174
+
175
+ switch (e.key) {
176
+ case "ArrowDown":
177
+ e.preventDefault();
178
+ items[idx + 1]?.focus();
179
+ break;
180
+ case "ArrowUp":
181
+ e.preventDefault();
182
+ items[idx - 1]?.focus();
183
+ break;
184
+ case "Home":
185
+ e.preventDefault();
186
+ items[0]?.focus();
187
+ break;
188
+ case "End":
189
+ e.preventDefault();
190
+ items[items.length - 1]?.focus();
191
+ break;
192
+ case "ArrowRight":
193
+ e.preventDefault();
194
+ if (hasChildren && !expanded) ctx.toggleExpanded(value);
195
+ else if (hasChildren) items[idx + 1]?.focus();
196
+ break;
197
+ case "ArrowLeft":
198
+ e.preventDefault();
199
+ if (hasChildren && expanded) ctx.toggleExpanded(value);
200
+ else {
201
+ const parent = localRef.current?.parentElement?.closest<HTMLElement>('[role="treeitem"]');
202
+ parent?.focus();
203
+ }
204
+ break;
205
+ case "Enter":
206
+ case " ":
207
+ e.preventDefault();
208
+ activate();
209
+ break;
210
+ }
211
+ };
212
+
213
+ return (
214
+ <div
215
+ ref={setRefs}
216
+ role="treeitem"
217
+ aria-expanded={hasChildren ? expanded : undefined}
218
+ aria-selected={ctx.selected === value}
219
+ aria-disabled={disabled || undefined}
220
+ tabIndex={disabled ? undefined : isTabbable ? 0 : -1}
221
+ onFocus={() => ctx.setTabbableValue(value)}
222
+ onKeyDown={handleKeyDown}
223
+ onClick={(e) => {
224
+ onClick?.(e);
225
+ if (e.defaultPrevented || disabled) return;
226
+ activate();
227
+ }}
228
+ className={cn("outline-none", disabled && "pointer-events-none opacity-disabled", className)}
229
+ {...props}
230
+ >
231
+ <div
232
+ style={{ paddingLeft: level * 20 + 8 }}
233
+ className={cn(
234
+ "flex items-center gap-1.5 rounded-sm py-1.5 pr-2 text-body-sm text-foreground",
235
+ "hover:bg-accent",
236
+ ctx.selected === value && "bg-accent",
237
+ "focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
238
+ )}
239
+ >
240
+ {hasChildren ? (
241
+ <ChevronRight
242
+ className={cn("size-4 shrink-0 text-muted-foreground transition-transform", expanded && "rotate-90")}
243
+ onClick={(e) => {
244
+ e.stopPropagation();
245
+ ctx.toggleExpanded(value);
246
+ }}
247
+ />
248
+ ) : (
249
+ <span className="size-4 shrink-0" />
250
+ )}
251
+ {ctx.checkable && (
252
+ <Checkbox
253
+ checked={isChecked}
254
+ onCheckedChange={() => ctx.toggleChecked(value)}
255
+ onClick={(e) => e.stopPropagation()}
256
+ tabIndex={-1}
257
+ />
258
+ )}
259
+ {icon}
260
+ <span className="truncate">{label}</span>
261
+ </div>
262
+ {hasChildren && expanded && (
263
+ <div role="group">
264
+ <TreeLevelContext.Provider value={level + 1}>{children}</TreeLevelContext.Provider>
265
+ </div>
266
+ )}
267
+ </div>
268
+ );
269
+ },
270
+ );
271
+ TreeItem.displayName = "TreeItem";
272
+
273
+ export { TreeView, TreeItem };
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ /**
7
+ * VirtualList — a windowed-rendering primitive: only the rows visible in
8
+ * the scroll viewport (plus `overscan`) actually mount, so a list of
9
+ * thousands of items costs the same as rendering a couple dozen. Fixed
10
+ * row height only — variable-height virtualization needs a measurement
11
+ * pass per row (a real library's job, e.g. `@tanstack/react-virtual`),
12
+ * which is out of scope for this primitive. A prerequisite for `DataGrid`
13
+ * and any `Command`/`Combobox`/`Select` with a large option list. Gap-fill
14
+ * addition (not in the original Figma source).
15
+ */
16
+ export interface VirtualListProps<T>
17
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
18
+ items: T[];
19
+ /** row height in px — every row is this tall */
20
+ itemHeight: number;
21
+ /** viewport height — a number (px) or any CSS height value */
22
+ height: number | string;
23
+ renderItem: (item: T, index: number) => React.ReactNode;
24
+ /** extra rows rendered above/below the viewport, to reduce blank flashes on fast scroll */
25
+ overscan?: number;
26
+ getItemKey?: (item: T, index: number) => React.Key;
27
+ }
28
+
29
+ function VirtualList<T>({
30
+ items,
31
+ itemHeight,
32
+ height,
33
+ renderItem,
34
+ overscan = 4,
35
+ getItemKey,
36
+ className,
37
+ style,
38
+ onScroll,
39
+ ...props
40
+ }: VirtualListProps<T>) {
41
+ const containerRef = React.useRef<HTMLDivElement>(null);
42
+ const [scrollTop, setScrollTop] = React.useState(0);
43
+ const [viewportHeight, setViewportHeight] = React.useState(typeof height === "number" ? height : 0);
44
+
45
+ React.useEffect(() => {
46
+ const el = containerRef.current;
47
+ if (!el) return;
48
+ const observer = new ResizeObserver((entries) => {
49
+ const entry = entries[0];
50
+ if (entry) setViewportHeight(entry.contentRect.height);
51
+ });
52
+ observer.observe(el);
53
+ return () => observer.disconnect();
54
+ }, []);
55
+
56
+ const totalHeight = items.length * itemHeight;
57
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
58
+ const visibleCount = Math.ceil(viewportHeight / itemHeight) + overscan * 2;
59
+ const endIndex = Math.min(items.length, startIndex + Math.max(visibleCount, 0));
60
+ const visibleItems = items.slice(startIndex, endIndex);
61
+
62
+ return (
63
+ <div
64
+ ref={containerRef}
65
+ role="list"
66
+ className={cn("overflow-y-auto font-sans", className)}
67
+ style={{ height, ...style }}
68
+ onScroll={(e) => {
69
+ onScroll?.(e);
70
+ setScrollTop(e.currentTarget.scrollTop);
71
+ }}
72
+ {...props}
73
+ >
74
+ <div style={{ height: totalHeight, position: "relative" }}>
75
+ {visibleItems.map((item, i) => {
76
+ const index = startIndex + i;
77
+ return (
78
+ <div
79
+ key={getItemKey ? getItemKey(item, index) : index}
80
+ role="listitem"
81
+ style={{ position: "absolute", top: index * itemHeight, left: 0, right: 0, height: itemHeight }}
82
+ >
83
+ {renderItem(item, index)}
84
+ </div>
85
+ );
86
+ })}
87
+ </div>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ export { VirtualList };
package/src/index.ts CHANGED
@@ -24,6 +24,10 @@ export {
24
24
  AlertDialogCancel,
25
25
  } from "./components/alert-dialog";
26
26
  export { AspectRatio } from "./components/aspect-ratio";
27
+ export {
28
+ KinetixDirectionProvider,
29
+ type KinetixDirectionProviderProps,
30
+ } from "./components/direction-provider";
27
31
  export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, type AvatarGroupProps } from "./components/avatar";
28
32
  export { Badge, badgeVariants, type BadgeProps } from "./components/badge";
29
33
  export {
@@ -354,5 +358,25 @@ export { Timeline, type TimelineItem, type TimelineProps } from "./components/ti
354
358
  export { Marquee, type MarqueeProps } from "./components/marquee";
355
359
  export { ComparisonSlider, type ComparisonSliderProps } from "./components/comparison-slider";
356
360
  export { PageHeader, type PageHeaderProps } from "./components/page-header";
361
+ export {
362
+ NotificationCenter,
363
+ NotificationCenterTrigger,
364
+ NotificationCenterContent,
365
+ NotificationItem,
366
+ type NotificationCenterTriggerProps,
367
+ type NotificationCenterContentProps,
368
+ type NotificationItemProps,
369
+ } from "./components/notification-center";
370
+ export { TreeView, TreeItem, type TreeViewProps, type TreeItemProps } from "./components/tree-view";
371
+ export { MultiSelect, type MultiSelectOption, type MultiSelectProps } from "./components/multi-select";
372
+ export { MessageBubble, TypingIndicator, type MessageBubbleProps } from "./components/message-bubble";
373
+ export { Tour, type TourStep, type TourProps } from "./components/tour";
374
+ export { VirtualList, type VirtualListProps } from "./components/virtual-list";
375
+ export { DataGrid, type DataGridColumn, type DataGridProps } from "./components/data-grid";
376
+ export { JsonViewer, type JsonViewerProps } from "./components/json-viewer";
377
+ export { DiffViewer, type DiffViewerProps } from "./components/diff-viewer";
378
+ export { ColorPicker, type ColorPickerProps } from "./components/color-picker";
379
+ export { KanbanBoard, type KanbanBoardProps, type KanbanColumn, type KanbanCard } from "./components/kanban-board";
380
+ export { MarkdownEditor, type MarkdownEditorProps } from "./components/markdown-editor";
357
381
 
358
382
  export { cn } from "./lib/utils";
@@ -0,0 +1,28 @@
1
+ /* AUTO-GENERATED by scripts/gen-stories.mjs — do not edit.
2
+ Source of truth: apps/web/src/registry/demos.tsx */
3
+ import * as React from "react";
4
+ import type { Meta, StoryObj } from "@storybook/react-vite";
5
+ import { ColorPicker } from "@kinetixui/ui";
6
+
7
+ const Demo = () => {
8
+ const [color, setColor] = React.useState("#3b82f6");
9
+ return (
10
+ <ColorPicker
11
+ value={color}
12
+ onChange={setColor}
13
+ swatches={["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899"]}
14
+ />
15
+ );
16
+ };
17
+
18
+ const meta = {
19
+ title: "Form Inputs/ColorPicker",
20
+ parameters: {
21
+ layout: "centered",
22
+ docs: { source: { code: "<ColorPicker\\n value={color}\\n onChange={setColor}\\n swatches={[\"#ef4444\", \"#f97316\", \"#eab308\", \"#22c55e\", \"#3b82f6\"]}\\n/>", language: "tsx" } },
23
+ },
24
+ } satisfies Meta;
25
+
26
+ export default meta;
27
+
28
+ export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
@@ -0,0 +1,54 @@
1
+ /* AUTO-GENERATED by scripts/gen-stories.mjs — do not edit.
2
+ Source of truth: apps/web/src/registry/demos.tsx */
3
+ import * as React from "react";
4
+ import type { Meta, StoryObj } from "@storybook/react-vite";
5
+ import { DataGrid } from "@kinetixui/ui";
6
+
7
+ const Demo = () => {
8
+ const rows = React.useMemo(
9
+ () =>
10
+ Array.from({ length: 2000 }, (_, i) => ({
11
+ id: i,
12
+ name: `Item ${i + 1}`,
13
+ category: ["Widgets", "Gadgets", "Gizmos"][i % 3],
14
+ qty: (i * 7) % 500,
15
+ })),
16
+ [],
17
+ );
18
+ const [data, setData] = React.useState(rows);
19
+ return (
20
+ <DataGrid
21
+ className="w-full"
22
+ height={280}
23
+ data={data}
24
+ getRowId={(row) => row.id}
25
+ columns={[
26
+ { id: "id", header: "ID", cell: (row) => row.id, value: (row) => row.id, sortable: true, width: 72, pinned: "left" },
27
+ {
28
+ id: "name",
29
+ header: "Name",
30
+ cell: (row) => row.name,
31
+ value: (row) => row.name,
32
+ sortable: true,
33
+ editable: true,
34
+ onCellEdit: (row, rowIndex, value) =>
35
+ setData((prev) => prev.map((r, i) => (i === rowIndex ? { ...r, name: value } : r))),
36
+ },
37
+ { id: "category", header: "Category", cell: (row) => row.category, value: (row) => row.category, sortable: true },
38
+ { id: "qty", header: "Qty", cell: (row) => row.qty, value: (row) => row.qty, sortable: true, width: 100 },
39
+ ]}
40
+ />
41
+ );
42
+ };
43
+
44
+ const meta = {
45
+ title: "Data Display/DataGrid",
46
+ parameters: {
47
+ layout: "padded",
48
+ docs: { source: { code: "<DataGrid\\n height={280}\\n data={data}\\n getRowId={(row) => row.id}\\n columns={[\\n { id: \"id\", header: \"ID\", cell: (row) => row.id, value: (row) => row.id, sortable: true, pinned: \"left\" },\\n { id: \"name\", header: \"Name\", cell: (row) => row.name, value: (row) => row.name, editable: true, onCellEdit: updateName },\\n { id: \"qty\", header: \"Qty\", cell: (row) => row.qty, value: (row) => row.qty, sortable: true },\\n ]}\\n/>", language: "tsx" } },
49
+ },
50
+ } satisfies Meta;
51
+
52
+ export default meta;
53
+
54
+ export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };