@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +1176 -0
- package/dist/index.js +3939 -0
- package/package.json +106 -0
- package/src/components/accordion.tsx +52 -0
- package/src/components/alert-dialog.tsx +115 -0
- package/src/components/alert.tsx +47 -0
- package/src/components/aspect-ratio.tsx +7 -0
- package/src/components/audio-player.tsx +150 -0
- package/src/components/avatar.tsx +76 -0
- package/src/components/badge.tsx +40 -0
- package/src/components/breadcrumb.tsx +77 -0
- package/src/components/button.tsx +119 -0
- package/src/components/calendar.tsx +60 -0
- package/src/components/card.tsx +50 -0
- package/src/components/carousel.tsx +181 -0
- package/src/components/chart.tsx +177 -0
- package/src/components/checkbox.tsx +38 -0
- package/src/components/circular-progress.tsx +67 -0
- package/src/components/code-block.tsx +96 -0
- package/src/components/collapsible.tsx +9 -0
- package/src/components/command.tsx +123 -0
- package/src/components/context-menu.tsx +132 -0
- package/src/components/data-table.tsx +92 -0
- package/src/components/date-picker.tsx +68 -0
- package/src/components/dialog.tsx +101 -0
- package/src/components/drawer.tsx +82 -0
- package/src/components/dropdown-menu.tsx +132 -0
- package/src/components/fab.tsx +58 -0
- package/src/components/field.tsx +128 -0
- package/src/components/file-upload.tsx +183 -0
- package/src/components/footer.tsx +62 -0
- package/src/components/form.tsx +130 -0
- package/src/components/hover-card.tsx +28 -0
- package/src/components/image.tsx +87 -0
- package/src/components/inform.tsx +82 -0
- package/src/components/input-group.tsx +96 -0
- package/src/components/input-otp.tsx +63 -0
- package/src/components/input.tsx +72 -0
- package/src/components/label.tsx +20 -0
- package/src/components/list.tsx +69 -0
- package/src/components/menubar.tsx +168 -0
- package/src/components/metric.tsx +51 -0
- package/src/components/modal.tsx +126 -0
- package/src/components/navigation-bar.tsx +49 -0
- package/src/components/navigation-menu.tsx +117 -0
- package/src/components/number-input.tsx +86 -0
- package/src/components/pagination.tsx +77 -0
- package/src/components/password-input.tsx +36 -0
- package/src/components/popover.tsx +32 -0
- package/src/components/progress.tsx +24 -0
- package/src/components/quote.tsx +34 -0
- package/src/components/radio-group.tsx +44 -0
- package/src/components/rating.tsx +88 -0
- package/src/components/resizable.tsx +40 -0
- package/src/components/scroll-area.tsx +39 -0
- package/src/components/select.tsx +124 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/sheet.tsx +104 -0
- package/src/components/sidebar.tsx +390 -0
- package/src/components/skeleton.tsx +9 -0
- package/src/components/slider.tsx +24 -0
- package/src/components/sonner.tsx +30 -0
- package/src/components/spinner.tsx +43 -0
- package/src/components/stepper.tsx +75 -0
- package/src/components/switch.tsx +37 -0
- package/src/components/tab-bar.tsx +58 -0
- package/src/components/table-of-contents.tsx +46 -0
- package/src/components/table.tsx +70 -0
- package/src/components/tabs.tsx +54 -0
- package/src/components/tag.tsx +56 -0
- package/src/components/textarea.tsx +56 -0
- package/src/components/toggle-group.tsx +41 -0
- package/src/components/toggle.tsx +34 -0
- package/src/components/tooltip.tsx +31 -0
- package/src/index.ts +305 -0
- package/src/lib/utils.ts +7 -0
- package/src/stories/Button.stories.tsx +155 -0
- package/src/stories/Input.stories.tsx +80 -0
- package/src/stories/Textarea.stories.tsx +69 -0
- 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,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 };
|