@kinetixui/ui 0.1.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +1176 -0
  3. package/dist/index.js +3939 -0
  4. package/package.json +106 -0
  5. package/src/components/accordion.tsx +52 -0
  6. package/src/components/alert-dialog.tsx +115 -0
  7. package/src/components/alert.tsx +47 -0
  8. package/src/components/aspect-ratio.tsx +7 -0
  9. package/src/components/audio-player.tsx +150 -0
  10. package/src/components/avatar.tsx +76 -0
  11. package/src/components/badge.tsx +40 -0
  12. package/src/components/breadcrumb.tsx +77 -0
  13. package/src/components/button.tsx +119 -0
  14. package/src/components/calendar.tsx +60 -0
  15. package/src/components/card.tsx +50 -0
  16. package/src/components/carousel.tsx +181 -0
  17. package/src/components/chart.tsx +177 -0
  18. package/src/components/checkbox.tsx +38 -0
  19. package/src/components/circular-progress.tsx +67 -0
  20. package/src/components/code-block.tsx +96 -0
  21. package/src/components/collapsible.tsx +9 -0
  22. package/src/components/command.tsx +123 -0
  23. package/src/components/context-menu.tsx +132 -0
  24. package/src/components/data-table.tsx +92 -0
  25. package/src/components/date-picker.tsx +68 -0
  26. package/src/components/dialog.tsx +101 -0
  27. package/src/components/drawer.tsx +82 -0
  28. package/src/components/dropdown-menu.tsx +132 -0
  29. package/src/components/fab.tsx +58 -0
  30. package/src/components/field.tsx +128 -0
  31. package/src/components/file-upload.tsx +183 -0
  32. package/src/components/footer.tsx +62 -0
  33. package/src/components/form.tsx +130 -0
  34. package/src/components/hover-card.tsx +28 -0
  35. package/src/components/image.tsx +87 -0
  36. package/src/components/inform.tsx +82 -0
  37. package/src/components/input-group.tsx +96 -0
  38. package/src/components/input-otp.tsx +63 -0
  39. package/src/components/input.tsx +72 -0
  40. package/src/components/label.tsx +20 -0
  41. package/src/components/list.tsx +69 -0
  42. package/src/components/menubar.tsx +168 -0
  43. package/src/components/metric.tsx +51 -0
  44. package/src/components/modal.tsx +126 -0
  45. package/src/components/navigation-bar.tsx +49 -0
  46. package/src/components/navigation-menu.tsx +117 -0
  47. package/src/components/number-input.tsx +86 -0
  48. package/src/components/pagination.tsx +77 -0
  49. package/src/components/password-input.tsx +36 -0
  50. package/src/components/popover.tsx +32 -0
  51. package/src/components/progress.tsx +24 -0
  52. package/src/components/quote.tsx +34 -0
  53. package/src/components/radio-group.tsx +44 -0
  54. package/src/components/rating.tsx +88 -0
  55. package/src/components/resizable.tsx +40 -0
  56. package/src/components/scroll-area.tsx +39 -0
  57. package/src/components/select.tsx +124 -0
  58. package/src/components/separator.tsx +25 -0
  59. package/src/components/sheet.tsx +104 -0
  60. package/src/components/sidebar.tsx +390 -0
  61. package/src/components/skeleton.tsx +9 -0
  62. package/src/components/slider.tsx +24 -0
  63. package/src/components/sonner.tsx +30 -0
  64. package/src/components/spinner.tsx +43 -0
  65. package/src/components/stepper.tsx +75 -0
  66. package/src/components/switch.tsx +37 -0
  67. package/src/components/tab-bar.tsx +58 -0
  68. package/src/components/table-of-contents.tsx +46 -0
  69. package/src/components/table.tsx +70 -0
  70. package/src/components/tabs.tsx +54 -0
  71. package/src/components/tag.tsx +56 -0
  72. package/src/components/textarea.tsx +56 -0
  73. package/src/components/toggle-group.tsx +41 -0
  74. package/src/components/toggle.tsx +34 -0
  75. package/src/components/tooltip.tsx +31 -0
  76. package/src/index.ts +305 -0
  77. package/src/lib/utils.ts +7 -0
  78. package/src/stories/Button.stories.tsx +155 -0
  79. package/src/stories/Input.stories.tsx +80 -0
  80. package/src/stories/Textarea.stories.tsx +69 -0
  81. package/tailwind.config.ts +107 -0
@@ -0,0 +1,390 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import { PanelLeft } from "lucide-react";
7
+ import { cn } from "../lib/utils";
8
+ import { Button } from "./button";
9
+ import { Input } from "./input";
10
+ import { Separator } from "./separator";
11
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet";
12
+ import { Skeleton } from "./skeleton";
13
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
14
+
15
+ const SIDEBAR_COOKIE_NAME = "sidebar:state";
16
+ const SIDEBAR_WIDTH = "16rem";
17
+ const SIDEBAR_WIDTH_MOBILE = "18rem";
18
+ const SIDEBAR_WIDTH_ICON = "3rem";
19
+ const SIDEBAR_KEYBOARD_SHORTCUT = "b";
20
+
21
+ type SidebarContextValue = {
22
+ state: "expanded" | "collapsed";
23
+ open: boolean;
24
+ setOpen: (open: boolean) => void;
25
+ openMobile: boolean;
26
+ setOpenMobile: (open: boolean) => void;
27
+ isMobile: boolean;
28
+ toggleSidebar: () => void;
29
+ };
30
+
31
+ const SidebarContext = React.createContext<SidebarContextValue | null>(null);
32
+
33
+ function useSidebar() {
34
+ const context = React.useContext(SidebarContext);
35
+ if (!context) throw new Error("useSidebar must be used within a SidebarProvider.");
36
+ return context;
37
+ }
38
+
39
+ function useIsMobile() {
40
+ const [isMobile, setIsMobile] = React.useState(false);
41
+ React.useEffect(() => {
42
+ const mql = window.matchMedia("(max-width: 767px)");
43
+ const onChange = () => setIsMobile(window.innerWidth < 768);
44
+ mql.addEventListener("change", onChange);
45
+ onChange();
46
+ return () => mql.removeEventListener("change", onChange);
47
+ }, []);
48
+ return isMobile;
49
+ }
50
+
51
+ const SidebarProvider = React.forwardRef<
52
+ HTMLDivElement,
53
+ React.ComponentProps<"div"> & {
54
+ defaultOpen?: boolean;
55
+ open?: boolean;
56
+ onOpenChange?: (open: boolean) => void;
57
+ }
58
+ >(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
59
+ const isMobile = useIsMobile();
60
+ const [openMobile, setOpenMobile] = React.useState(false);
61
+ const [_open, _setOpen] = React.useState(defaultOpen);
62
+ const open = openProp ?? _open;
63
+
64
+ const setOpen = React.useCallback(
65
+ (value: boolean | ((v: boolean) => boolean)) => {
66
+ const openState = typeof value === "function" ? value(open) : value;
67
+ if (setOpenProp) setOpenProp(openState);
68
+ else _setOpen(openState);
69
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${60 * 60 * 24 * 7}`;
70
+ },
71
+ [setOpenProp, open],
72
+ );
73
+
74
+ const toggleSidebar = React.useCallback(() => {
75
+ return isMobile ? setOpenMobile((o) => !o) : setOpen((o) => !o);
76
+ }, [isMobile, setOpen]);
77
+
78
+ React.useEffect(() => {
79
+ const handleKeyDown = (event: KeyboardEvent) => {
80
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
81
+ event.preventDefault();
82
+ toggleSidebar();
83
+ }
84
+ };
85
+ window.addEventListener("keydown", handleKeyDown);
86
+ return () => window.removeEventListener("keydown", handleKeyDown);
87
+ }, [toggleSidebar]);
88
+
89
+ const state = open ? "expanded" : "collapsed";
90
+
91
+ const contextValue = React.useMemo<SidebarContextValue>(
92
+ () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar }),
93
+ [state, open, setOpen, isMobile, openMobile, toggleSidebar],
94
+ );
95
+
96
+ return (
97
+ <SidebarContext.Provider value={contextValue}>
98
+ <TooltipProvider delayDuration={0}>
99
+ <div
100
+ ref={ref}
101
+ style={{ "--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, ...style } as React.CSSProperties}
102
+ className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar font-sans", className)}
103
+ {...props}
104
+ >
105
+ {children}
106
+ </div>
107
+ </TooltipProvider>
108
+ </SidebarContext.Provider>
109
+ );
110
+ });
111
+ SidebarProvider.displayName = "SidebarProvider";
112
+
113
+ const Sidebar = React.forwardRef<
114
+ HTMLDivElement,
115
+ React.ComponentProps<"div"> & {
116
+ side?: "left" | "right";
117
+ variant?: "sidebar" | "floating" | "inset";
118
+ collapsible?: "offcanvas" | "icon" | "none";
119
+ }
120
+ >(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
121
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
122
+
123
+ if (collapsible === "none") {
124
+ return (
125
+ <div ref={ref} className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)} {...props}>
126
+ {children}
127
+ </div>
128
+ );
129
+ }
130
+
131
+ if (isMobile) {
132
+ return (
133
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
134
+ <SheetContent
135
+ data-sidebar="sidebar"
136
+ data-mobile="true"
137
+ className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
138
+ style={{ "--sidebar-width": SIDEBAR_WIDTH_MOBILE } as React.CSSProperties}
139
+ side={side}
140
+ >
141
+ <SheetHeader className="sr-only">
142
+ <SheetTitle>Sidebar</SheetTitle>
143
+ <SheetDescription>Displays the mobile sidebar.</SheetDescription>
144
+ </SheetHeader>
145
+ <div className="flex h-full w-full flex-col">{children}</div>
146
+ </SheetContent>
147
+ </Sheet>
148
+ );
149
+ }
150
+
151
+ return (
152
+ <div
153
+ ref={ref}
154
+ className="group peer hidden text-sidebar-foreground md:block"
155
+ data-state={state}
156
+ data-collapsible={state === "collapsed" ? collapsible : ""}
157
+ data-variant={variant}
158
+ data-side={side}
159
+ >
160
+ <div
161
+ className={cn(
162
+ "relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
163
+ "group-data-[collapsible=offcanvas]:w-0",
164
+ variant === "floating" || variant === "inset"
165
+ ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+theme(spacing.4))]"
166
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
167
+ )}
168
+ />
169
+ <div
170
+ className={cn(
171
+ "fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
172
+ side === "left"
173
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
174
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
175
+ variant === "floating" || variant === "inset"
176
+ ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+theme(spacing.4)+2px)]"
177
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
178
+ className,
179
+ )}
180
+ {...props}
181
+ >
182
+ <div
183
+ data-sidebar="sidebar"
184
+ className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
185
+ >
186
+ {children}
187
+ </div>
188
+ </div>
189
+ </div>
190
+ );
191
+ });
192
+ Sidebar.displayName = "Sidebar";
193
+
194
+ const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
195
+ ({ className, onClick, ...props }, ref) => {
196
+ const { toggleSidebar } = useSidebar();
197
+ return (
198
+ <Button
199
+ ref={ref}
200
+ variant="Ghost"
201
+ size="icon"
202
+ className={cn("size-7", className)}
203
+ onClick={(e) => {
204
+ onClick?.(e);
205
+ toggleSidebar();
206
+ }}
207
+ {...props}
208
+ >
209
+ <PanelLeft />
210
+ <span className="sr-only">Toggle Sidebar</span>
211
+ </Button>
212
+ );
213
+ },
214
+ );
215
+ SidebarTrigger.displayName = "SidebarTrigger";
216
+
217
+ const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => (
218
+ <main
219
+ ref={ref}
220
+ className={cn(
221
+ "relative flex min-h-svh flex-1 flex-col bg-background",
222
+ "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
223
+ className,
224
+ )}
225
+ {...props}
226
+ />
227
+ ));
228
+ SidebarInset.displayName = "SidebarInset";
229
+
230
+ const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => (
231
+ <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />
232
+ ));
233
+ SidebarHeader.displayName = "SidebarHeader";
234
+
235
+ const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => (
236
+ <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />
237
+ ));
238
+ SidebarFooter.displayName = "SidebarFooter";
239
+
240
+ const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
241
+ ({ className, ...props }, ref) => (
242
+ <Separator ref={ref} data-sidebar="separator" className={cn("mx-2 w-auto bg-sidebar-border", className)} {...props} />
243
+ ),
244
+ );
245
+ SidebarSeparator.displayName = "SidebarSeparator";
246
+
247
+ const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => (
248
+ <div
249
+ ref={ref}
250
+ data-sidebar="content"
251
+ className={cn("flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", className)}
252
+ {...props}
253
+ />
254
+ ));
255
+ SidebarContent.displayName = "SidebarContent";
256
+
257
+ const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => (
258
+ <div ref={ref} data-sidebar="group" className={cn("relative flex w-full min-w-0 flex-col p-2", className)} {...props} />
259
+ ));
260
+ SidebarGroup.displayName = "SidebarGroup";
261
+
262
+ const SidebarGroupLabel = React.forwardRef<
263
+ HTMLDivElement,
264
+ React.ComponentProps<"div"> & { asChild?: boolean }
265
+ >(({ className, asChild = false, ...props }, ref) => {
266
+ const Comp = asChild ? Slot : "div";
267
+ return (
268
+ <Comp
269
+ ref={ref}
270
+ data-sidebar="group-label"
271
+ className={cn(
272
+ "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
273
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
274
+ className,
275
+ )}
276
+ {...props}
277
+ />
278
+ );
279
+ });
280
+ SidebarGroupLabel.displayName = "SidebarGroupLabel";
281
+
282
+ const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
283
+ ({ className, ...props }, ref) => (
284
+ <div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
285
+ ),
286
+ );
287
+ SidebarGroupContent.displayName = "SidebarGroupContent";
288
+
289
+ const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
290
+ <ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
291
+ ));
292
+ SidebarMenu.displayName = "SidebarMenu";
293
+
294
+ const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
295
+ <li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
296
+ ));
297
+ SidebarMenuItem.displayName = "SidebarMenuItem";
298
+
299
+ const sidebarMenuButtonVariants = cva(
300
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
301
+ {
302
+ variants: {
303
+ variant: {
304
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
305
+ outline: "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
306
+ },
307
+ size: {
308
+ default: "h-8 text-sm",
309
+ sm: "h-7 text-xs",
310
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
311
+ },
312
+ },
313
+ defaultVariants: { variant: "default", size: "default" },
314
+ },
315
+ );
316
+
317
+ const SidebarMenuButton = React.forwardRef<
318
+ HTMLButtonElement,
319
+ React.ComponentProps<"button"> & {
320
+ asChild?: boolean;
321
+ isActive?: boolean;
322
+ tooltip?: string | React.ComponentProps<typeof TooltipContent>;
323
+ } & VariantProps<typeof sidebarMenuButtonVariants>
324
+ >(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
325
+ const Comp = asChild ? Slot : "button";
326
+ const { isMobile, state } = useSidebar();
327
+
328
+ const button = (
329
+ <Comp
330
+ ref={ref}
331
+ data-sidebar="menu-button"
332
+ data-size={size}
333
+ data-active={isActive}
334
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
335
+ {...props}
336
+ />
337
+ );
338
+
339
+ if (!tooltip) return button;
340
+ const tooltipProps = typeof tooltip === "string" ? { children: tooltip } : tooltip;
341
+
342
+ return (
343
+ <Tooltip>
344
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
345
+ <TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltipProps} />
346
+ </Tooltip>
347
+ );
348
+ });
349
+ SidebarMenuButton.displayName = "SidebarMenuButton";
350
+
351
+ const SidebarMenuSkeleton = React.forwardRef<
352
+ HTMLDivElement,
353
+ React.ComponentProps<"div"> & { showIcon?: boolean }
354
+ >(({ className, showIcon = false, ...props }, ref) => {
355
+ const width = React.useMemo(() => `${Math.floor(Math.random() * 40) + 50}%`, []);
356
+ return (
357
+ <div ref={ref} data-sidebar="menu-skeleton" className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} {...props}>
358
+ {showIcon && <Skeleton className="size-4 rounded-md" />}
359
+ <Skeleton className="h-4 max-w-[--skeleton-width] flex-1" style={{ "--skeleton-width": width } as React.CSSProperties} />
360
+ </div>
361
+ );
362
+ });
363
+ SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
364
+
365
+ const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
366
+ ({ className, ...props }, ref) => (
367
+ <Input ref={ref} data-sidebar="input" className={cn("h-8 w-full bg-background shadow-none", className)} {...props} />
368
+ ),
369
+ );
370
+ SidebarInput.displayName = "SidebarInput";
371
+
372
+ export {
373
+ Sidebar,
374
+ SidebarContent,
375
+ SidebarFooter,
376
+ SidebarGroup,
377
+ SidebarGroupContent,
378
+ SidebarGroupLabel,
379
+ SidebarHeader,
380
+ SidebarInput,
381
+ SidebarInset,
382
+ SidebarMenu,
383
+ SidebarMenuButton,
384
+ SidebarMenuItem,
385
+ SidebarMenuSkeleton,
386
+ SidebarProvider,
387
+ SidebarSeparator,
388
+ SidebarTrigger,
389
+ useSidebar,
390
+ };
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import { cn } from "../lib/utils";
4
+
5
+ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
7
+ }
8
+
9
+ export { Skeleton };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SliderPrimitive from "@radix-ui/react-slider";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const Slider = React.forwardRef<
8
+ React.ElementRef<typeof SliderPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <SliderPrimitive.Root
12
+ ref={ref}
13
+ className={cn("relative flex w-full touch-none select-none items-center", className)}
14
+ {...props}
15
+ >
16
+ <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
17
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
18
+ </SliderPrimitive.Track>
19
+ <SliderPrimitive.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
20
+ </SliderPrimitive.Root>
21
+ ));
22
+ Slider.displayName = SliderPrimitive.Root.displayName;
23
+
24
+ export { Slider };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { Toaster as Sonner, toast } from "sonner";
4
+
5
+ type ToasterProps = React.ComponentProps<typeof Sonner>;
6
+
7
+ /**
8
+ * Toaster — wraps `sonner`, themed to the KinetixUI token contract.
9
+ * Pass `theme` from your framework's theme hook (e.g. next-themes `useTheme`).
10
+ */
11
+ function Toaster({ theme = "system", ...props }: ToasterProps) {
12
+ return (
13
+ <Sonner
14
+ theme={theme}
15
+ className="toaster group font-sans"
16
+ toastOptions={{
17
+ classNames: {
18
+ toast:
19
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
20
+ description: "group-[.toast]:text-muted-foreground",
21
+ actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
22
+ cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
23
+ },
24
+ }}
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ export { Toaster, toast };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Spinner — a lightweight loading indicator. Border-based ring, no icon
9
+ * dependency; `size` sets the diameter, `variant` swaps the indicator
10
+ * colour for on-color surfaces.
11
+ */
12
+ const spinnerVariants = cva("inline-block animate-spin rounded-full border-2 border-current border-t-transparent", {
13
+ variants: {
14
+ size: {
15
+ sm: "size-4",
16
+ md: "size-6",
17
+ lg: "size-8",
18
+ },
19
+ variant: {
20
+ default: "text-primary",
21
+ muted: "text-muted-foreground",
22
+ onColor: "text-primary-foreground",
23
+ },
24
+ },
25
+ defaultVariants: { size: "md", variant: "default" },
26
+ });
27
+
28
+ export interface SpinnerProps
29
+ extends React.HTMLAttributes<HTMLSpanElement>,
30
+ VariantProps<typeof spinnerVariants> {
31
+ label?: string;
32
+ }
33
+
34
+ const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
35
+ ({ className, size, variant, label = "Loading…", ...props }, ref) => (
36
+ <span ref={ref} role="status" aria-label={label} className={cn(spinnerVariants({ size, variant }), className)} {...props}>
37
+ <span className="sr-only">{label}</span>
38
+ </span>
39
+ ),
40
+ );
41
+ Spinner.displayName = "Spinner";
42
+
43
+ export { Spinner, spinnerVariants };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Stepper — a numbered multi-step progress indicator. Horizontal or
9
+ * vertical; each step is complete / current / upcoming, derived from
10
+ * `current` against its index.
11
+ */
12
+ export interface StepperStep {
13
+ label: React.ReactNode;
14
+ description?: React.ReactNode;
15
+ }
16
+
17
+ export interface StepperProps extends React.HTMLAttributes<HTMLOListElement> {
18
+ steps: StepperStep[];
19
+ /** zero-based index of the active step */
20
+ current: number;
21
+ orientation?: "horizontal" | "vertical";
22
+ }
23
+
24
+ const Stepper = React.forwardRef<HTMLOListElement, StepperProps>(
25
+ ({ className, steps, current, orientation = "horizontal", ...props }, ref) => {
26
+ const vertical = orientation === "vertical";
27
+ return (
28
+ <ol ref={ref} className={cn("flex font-sans", vertical ? "flex-col" : "w-full items-start", className)} {...props}>
29
+ {steps.map((step, i) => {
30
+ const status = i < current ? "complete" : i === current ? "current" : "upcoming";
31
+ const isLast = i === steps.length - 1;
32
+ return (
33
+ <li
34
+ key={i}
35
+ className={cn("flex", vertical ? "flex-row gap-3" : "flex-1 flex-col items-center gap-2 text-center")}
36
+ aria-current={status === "current" ? "step" : undefined}
37
+ >
38
+ <div className={cn("flex items-center", vertical ? "flex-col" : "w-full")}>
39
+ <span
40
+ className={cn(
41
+ "flex size-7 shrink-0 items-center justify-center rounded-full text-label-md font-medium",
42
+ status === "complete" && "bg-primary text-primary-foreground",
43
+ status === "current" && "border-2 border-primary text-primary",
44
+ status === "upcoming" && "border border-input text-muted-foreground",
45
+ )}
46
+ >
47
+ {status === "complete" ? <Check className="size-4" /> : i + 1}
48
+ </span>
49
+ {!isLast && (
50
+ <span
51
+ aria-hidden
52
+ className={cn(
53
+ "bg-border",
54
+ vertical ? "my-1 min-h-6 w-px flex-1 self-stretch" : "mt-3.5 h-px flex-1",
55
+ status === "complete" && "bg-primary",
56
+ )}
57
+ />
58
+ )}
59
+ </div>
60
+ <div className={cn(vertical && "pb-6")}>
61
+ <p className={cn("text-label-md font-medium", status === "upcoming" ? "text-muted-foreground" : "text-foreground")}>
62
+ {step.label}
63
+ </p>
64
+ {step.description && <p className="text-body-sm text-muted-foreground">{step.description}</p>}
65
+ </div>
66
+ </li>
67
+ );
68
+ })}
69
+ </ol>
70
+ );
71
+ },
72
+ );
73
+ Stepper.displayName = "Stepper";
74
+
75
+ export { Stepper };
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Switch — reconciled 1:1 with the KinetixUI design source, node 54855:13984.
9
+ * 48×24 pill track. Off = `--tertiary` (#b0b0b0), on = `--primary`. 20px
10
+ * `--background` thumb, travels 24px. Focus = 2px `--ring` offset.
11
+ */
12
+ const Switch = React.forwardRef<
13
+ React.ElementRef<typeof SwitchPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
15
+ >(({ className, ...props }, ref) => (
16
+ <SwitchPrimitive.Root
17
+ ref={ref}
18
+ className={cn(
19
+ "peer inline-flex h-6 w-12 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
20
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
21
+ "disabled:cursor-not-allowed disabled:opacity-50",
22
+ "data-[state=checked]:bg-primary data-[state=unchecked]:bg-tertiary",
23
+ className,
24
+ )}
25
+ {...props}
26
+ >
27
+ <SwitchPrimitive.Thumb
28
+ className={cn(
29
+ "pointer-events-none block size-5 rounded-full bg-background shadow-lg ring-0 transition-transform",
30
+ "data-[state=checked]:translate-x-6 data-[state=unchecked]:translate-x-0",
31
+ )}
32
+ />
33
+ </SwitchPrimitive.Root>
34
+ ));
35
+ Switch.displayName = SwitchPrimitive.Root.displayName;
36
+
37
+ export { Switch };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ /**
7
+ * TabBar — a mobile bottom navigation bar. A fixed row of icon + label
8
+ * destinations; the active item is driven by `active` on `TabBarItem`.
9
+ */
10
+ const TabBar = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
11
+ ({ className, children, ...props }, ref) => (
12
+ <nav
13
+ ref={ref}
14
+ className={cn("flex w-full items-stretch border-t border-border bg-background font-sans", className)}
15
+ {...props}
16
+ >
17
+ {children}
18
+ </nav>
19
+ ),
20
+ );
21
+ TabBar.displayName = "TabBar";
22
+
23
+ export interface TabBarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
24
+ icon: React.ReactNode;
25
+ label: React.ReactNode;
26
+ active?: boolean;
27
+ badge?: React.ReactNode;
28
+ }
29
+
30
+ const TabBarItem = React.forwardRef<HTMLButtonElement, TabBarItemProps>(
31
+ ({ className, icon, label, active, badge, ...props }, ref) => (
32
+ <button
33
+ ref={ref}
34
+ type="button"
35
+ aria-current={active ? "page" : undefined}
36
+ className={cn(
37
+ "relative flex flex-1 flex-col items-center justify-center gap-1 py-2 text-label-sm outline-none transition-colors",
38
+ active ? "text-primary" : "text-muted-foreground hover:text-foreground",
39
+ "focus-visible:bg-accent",
40
+ className,
41
+ )}
42
+ {...props}
43
+ >
44
+ <span className="relative [&_svg]:size-6">
45
+ {icon}
46
+ {badge && (
47
+ <span className="absolute -right-1.5 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-medium text-destructive-foreground">
48
+ {badge}
49
+ </span>
50
+ )}
51
+ </span>
52
+ <span className="font-medium">{label}</span>
53
+ </button>
54
+ ),
55
+ );
56
+ TabBarItem.displayName = "TabBarItem";
57
+
58
+ export { TabBar, TabBarItem };