@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,69 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ /**
7
+ * List — a composable list of rows: leading icon/avatar, title, optional
8
+ * description, trailing content. Distinct from `Table` (tabular data): List
9
+ * is single-column, built for mobile menus, settings screens and search
10
+ * results.
11
+ */
12
+ const List = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
13
+ ({ className, ...props }, ref) => (
14
+ <div ref={ref} role="list" className={cn("flex flex-col divide-y divide-border", className)} {...props} />
15
+ ),
16
+ );
17
+ List.displayName = "List";
18
+
19
+ export interface ListItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
20
+ leading?: React.ReactNode;
21
+ title: React.ReactNode;
22
+ description?: React.ReactNode;
23
+ trailing?: React.ReactNode;
24
+ disabled?: boolean;
25
+ /** render as a pressable row */
26
+ onSelect?: () => void;
27
+ }
28
+
29
+ const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
30
+ ({ className, leading, title, description, trailing, disabled, onSelect, ...props }, ref) => {
31
+ const interactive = !!onSelect && !disabled;
32
+ return (
33
+ <div
34
+ ref={ref}
35
+ role={interactive ? "button" : "listitem"}
36
+ tabIndex={interactive ? 0 : undefined}
37
+ aria-disabled={disabled}
38
+ onClick={disabled ? undefined : onSelect}
39
+ onKeyDown={
40
+ interactive
41
+ ? (e) => {
42
+ if (e.key === "Enter" || e.key === " ") {
43
+ e.preventDefault();
44
+ onSelect?.();
45
+ }
46
+ }
47
+ : undefined
48
+ }
49
+ className={cn(
50
+ "flex items-center gap-3 px-3 py-3 font-sans outline-none",
51
+ interactive && "cursor-pointer hover:bg-accent focus-visible:bg-accent",
52
+ disabled && "pointer-events-none opacity-50",
53
+ className,
54
+ )}
55
+ {...props}
56
+ >
57
+ {leading && <div className="flex shrink-0 items-center justify-center">{leading}</div>}
58
+ <div className="min-w-0 flex-1">
59
+ <div className="truncate text-body-md text-foreground">{title}</div>
60
+ {description && <div className="truncate text-body-sm text-muted-foreground">{description}</div>}
61
+ </div>
62
+ {trailing && <div className="flex shrink-0 items-center gap-2">{trailing}</div>}
63
+ </div>
64
+ );
65
+ },
66
+ );
67
+ ListItem.displayName = "ListItem";
68
+
69
+ export { List, ListItem };
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const MenubarMenu = (props: React.ComponentProps<typeof MenubarPrimitive.Menu>) => (
9
+ <MenubarPrimitive.Menu {...props} />
10
+ );
11
+ const MenubarGroup = (props: React.ComponentProps<typeof MenubarPrimitive.Group>) => (
12
+ <MenubarPrimitive.Group {...props} />
13
+ );
14
+ const MenubarPortal = (props: React.ComponentProps<typeof MenubarPrimitive.Portal>) => (
15
+ <MenubarPrimitive.Portal {...props} />
16
+ );
17
+ const MenubarSub = (props: React.ComponentProps<typeof MenubarPrimitive.Sub>) => (
18
+ <MenubarPrimitive.Sub {...props} />
19
+ );
20
+ const MenubarRadioGroup = (props: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) => (
21
+ <MenubarPrimitive.RadioGroup {...props} />
22
+ );
23
+
24
+ const itemCls =
25
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
26
+ const contentCls =
27
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95";
28
+
29
+ const Menubar = React.forwardRef<
30
+ React.ElementRef<typeof MenubarPrimitive.Root>,
31
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
32
+ >(({ className, ...props }, ref) => (
33
+ <MenubarPrimitive.Root
34
+ ref={ref}
35
+ className={cn("flex h-9 items-center gap-1 rounded-md border bg-background p-1 shadow-sm font-sans", className)}
36
+ {...props}
37
+ />
38
+ ));
39
+ Menubar.displayName = MenubarPrimitive.Root.displayName;
40
+
41
+ const MenubarTrigger = React.forwardRef<
42
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
43
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
44
+ >(({ className, ...props }, ref) => (
45
+ <MenubarPrimitive.Trigger
46
+ ref={ref}
47
+ className={cn(
48
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ ));
54
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
55
+
56
+ const MenubarSubTrigger = React.forwardRef<
57
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
58
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { inset?: boolean }
59
+ >(({ className, inset, children, ...props }, ref) => (
60
+ <MenubarPrimitive.SubTrigger ref={ref} className={cn(itemCls, "data-[state=open]:bg-accent", inset && "pl-8", className)} {...props}>
61
+ {children}
62
+ <ChevronRight className="ml-auto size-4" />
63
+ </MenubarPrimitive.SubTrigger>
64
+ ));
65
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
66
+
67
+ const MenubarSubContent = React.forwardRef<
68
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
69
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
70
+ >(({ className, ...props }, ref) => (
71
+ <MenubarPrimitive.SubContent ref={ref} className={cn(contentCls, className)} {...props} />
72
+ ));
73
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
74
+
75
+ const MenubarContent = React.forwardRef<
76
+ React.ElementRef<typeof MenubarPrimitive.Content>,
77
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
78
+ >(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
79
+ <MenubarPrimitive.Portal>
80
+ <MenubarPrimitive.Content
81
+ ref={ref}
82
+ align={align}
83
+ alignOffset={alignOffset}
84
+ sideOffset={sideOffset}
85
+ className={cn(contentCls, className)}
86
+ {...props}
87
+ />
88
+ </MenubarPrimitive.Portal>
89
+ ));
90
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName;
91
+
92
+ const MenubarItem = React.forwardRef<
93
+ React.ElementRef<typeof MenubarPrimitive.Item>,
94
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { inset?: boolean }
95
+ >(({ className, inset, ...props }, ref) => (
96
+ <MenubarPrimitive.Item ref={ref} className={cn(itemCls, inset && "pl-8", className)} {...props} />
97
+ ));
98
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName;
99
+
100
+ const MenubarCheckboxItem = React.forwardRef<
101
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
102
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
103
+ >(({ className, children, checked, ...props }, ref) => (
104
+ <MenubarPrimitive.CheckboxItem ref={ref} className={cn(itemCls, "pl-8", className)} checked={checked} {...props}>
105
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
106
+ <MenubarPrimitive.ItemIndicator>
107
+ <Check className="size-4" />
108
+ </MenubarPrimitive.ItemIndicator>
109
+ </span>
110
+ {children}
111
+ </MenubarPrimitive.CheckboxItem>
112
+ ));
113
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
114
+
115
+ const MenubarRadioItem = React.forwardRef<
116
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
117
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
118
+ >(({ className, children, ...props }, ref) => (
119
+ <MenubarPrimitive.RadioItem ref={ref} className={cn(itemCls, "pl-8", className)} {...props}>
120
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
121
+ <MenubarPrimitive.ItemIndicator>
122
+ <Circle className="size-2 fill-current" />
123
+ </MenubarPrimitive.ItemIndicator>
124
+ </span>
125
+ {children}
126
+ </MenubarPrimitive.RadioItem>
127
+ ));
128
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
129
+
130
+ const MenubarLabel = React.forwardRef<
131
+ React.ElementRef<typeof MenubarPrimitive.Label>,
132
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { inset?: boolean }
133
+ >(({ className, inset, ...props }, ref) => (
134
+ <MenubarPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} />
135
+ ));
136
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
137
+
138
+ const MenubarSeparator = React.forwardRef<
139
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
140
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
141
+ >(({ className, ...props }, ref) => (
142
+ <MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
143
+ ));
144
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
145
+
146
+ const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
147
+ <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
148
+ );
149
+ MenubarShortcut.displayName = "MenubarShortcut";
150
+
151
+ export {
152
+ Menubar,
153
+ MenubarMenu,
154
+ MenubarTrigger,
155
+ MenubarContent,
156
+ MenubarItem,
157
+ MenubarSeparator,
158
+ MenubarLabel,
159
+ MenubarCheckboxItem,
160
+ MenubarRadioGroup,
161
+ MenubarRadioItem,
162
+ MenubarPortal,
163
+ MenubarSubContent,
164
+ MenubarSubTrigger,
165
+ MenubarGroup,
166
+ MenubarSub,
167
+ MenubarShortcut,
168
+ };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ArrowDown, ArrowUp } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Metric — a stat / KPI display: label, value, an optional trend indicator
9
+ * and an optional chart or icon slot (bring your own chart, e.g. compose
10
+ * with `ChartContainer`).
11
+ */
12
+ export interface MetricProps extends React.HTMLAttributes<HTMLDivElement> {
13
+ label: React.ReactNode;
14
+ value: React.ReactNode;
15
+ trend?: "up" | "down" | "neutral";
16
+ change?: React.ReactNode;
17
+ icon?: React.ReactNode;
18
+ chart?: React.ReactNode;
19
+ }
20
+
21
+ const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
22
+ ({ className, label, value, trend, change, icon, chart, ...props }, ref) => (
23
+ <div ref={ref} className={cn("flex flex-col gap-2 rounded-md border border-input p-4 font-sans", className)} {...props}>
24
+ <div className="flex items-center justify-between">
25
+ <p className="text-body-sm text-muted-foreground">{label}</p>
26
+ {icon && <span className="text-muted-foreground [&_svg]:size-5">{icon}</span>}
27
+ </div>
28
+ <div className="flex items-end justify-between gap-2">
29
+ <p className="text-headline-sm font-medium text-foreground">{value}</p>
30
+ {trend && (
31
+ <span
32
+ className={cn(
33
+ "inline-flex items-center gap-0.5 text-label-md font-medium",
34
+ trend === "up" && "text-success",
35
+ trend === "down" && "text-destructive",
36
+ trend === "neutral" && "text-muted-foreground",
37
+ )}
38
+ >
39
+ {trend === "up" && <ArrowUp className="size-3.5" />}
40
+ {trend === "down" && <ArrowDown className="size-3.5" />}
41
+ {change}
42
+ </span>
43
+ )}
44
+ </div>
45
+ {chart && <div className="mt-1">{chart}</div>}
46
+ </div>
47
+ ),
48
+ );
49
+ Metric.displayName = "Metric";
50
+
51
+ export { Metric };
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+ import { Button } from "./button";
8
+ import { Dialog, DialogOverlay, DialogPortal, DialogTrigger } from "./dialog";
9
+
10
+ /**
11
+ * Modal — reconciled 1:1 with the KinetixUI design source, node 54857:1322.
12
+ * A structured dialog: header (title + close) · divider · body (description) ·
13
+ * divider · right-aligned footer. `--radius-xl` corner, soft popup shadow.
14
+ *
15
+ * type:
16
+ * Info → one primary action ("Got it")
17
+ * Confirmation → Cancel + primary ("Confirm")
18
+ * Warning → Cancel + primary ("Proceed")
19
+ * Destructive → Cancel + destructive ("Delete")
20
+ */
21
+ export type ModalType = "Info" | "Confirmation" | "Warning" | "Destructive";
22
+
23
+ const DEFAULT_ACTION: Record<ModalType, string> = {
24
+ Info: "Got it",
25
+ Confirmation: "Confirm",
26
+ Warning: "Proceed",
27
+ Destructive: "Delete",
28
+ };
29
+
30
+ export interface ModalProps {
31
+ type?: ModalType;
32
+ title: React.ReactNode;
33
+ description?: React.ReactNode;
34
+ /** overrides the type's default action label */
35
+ actionLabel?: string;
36
+ cancelLabel?: string;
37
+ onAction?: () => void;
38
+ onCancel?: () => void;
39
+ open?: boolean;
40
+ defaultOpen?: boolean;
41
+ onOpenChange?: (open: boolean) => void;
42
+ trigger?: React.ReactNode;
43
+ children?: React.ReactNode;
44
+ className?: string;
45
+ }
46
+
47
+ function Modal({
48
+ type = "Info",
49
+ title,
50
+ description,
51
+ actionLabel,
52
+ cancelLabel = "Cancel",
53
+ onAction,
54
+ onCancel,
55
+ open,
56
+ defaultOpen,
57
+ onOpenChange,
58
+ trigger,
59
+ children,
60
+ className,
61
+ }: ModalProps) {
62
+ const showCancel = type !== "Info";
63
+
64
+ return (
65
+ <Dialog open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
66
+ {trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
67
+ <DialogPortal>
68
+ <DialogOverlay />
69
+ <DialogPrimitive.Content
70
+ className={cn(
71
+ "fixed left-1/2 top-1/2 z-50 flex w-full max-w-[480px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl bg-background font-sans",
72
+ "shadow-[0_4px_24px_rgba(0,0,0,0.12)]",
73
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
74
+ className,
75
+ )}
76
+ >
77
+ <header className="flex items-center justify-between px-6 pb-3 pt-5">
78
+ <DialogPrimitive.Title className="min-w-0 flex-1 text-[18px] font-semibold leading-6 text-foreground">
79
+ {title}
80
+ </DialogPrimitive.Title>
81
+ <DialogPrimitive.Close
82
+ className="rounded-sm opacity-70 outline-none transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring"
83
+ aria-label="Close"
84
+ >
85
+ <X className="size-5" />
86
+ </DialogPrimitive.Close>
87
+ </header>
88
+
89
+ <div className="h-px w-full bg-border" />
90
+
91
+ <div className="px-6 py-5">
92
+ {description && (
93
+ <DialogPrimitive.Description className="text-[14px] leading-5 tracking-[0.25px] text-muted-foreground">
94
+ {description}
95
+ </DialogPrimitive.Description>
96
+ )}
97
+ {children}
98
+ </div>
99
+
100
+ <div className="h-px w-full bg-border" />
101
+
102
+ <footer className="flex items-center justify-end gap-3 px-6 py-4">
103
+ {showCancel && (
104
+ <DialogPrimitive.Close asChild>
105
+ <Button variant="Outline" size="lg" onClick={onCancel}>
106
+ {cancelLabel}
107
+ </Button>
108
+ </DialogPrimitive.Close>
109
+ )}
110
+ <DialogPrimitive.Close asChild>
111
+ <Button
112
+ variant={type === "Destructive" ? "Destructive" : "Primary"}
113
+ size="lg"
114
+ onClick={onAction}
115
+ >
116
+ {actionLabel ?? DEFAULT_ACTION[type]}
117
+ </Button>
118
+ </DialogPrimitive.Close>
119
+ </footer>
120
+ </DialogPrimitive.Content>
121
+ </DialogPortal>
122
+ </Dialog>
123
+ );
124
+ }
125
+
126
+ export { Modal };
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronLeft } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * NavigationBar — a mobile top app bar. Leading back button (or a custom
9
+ * `leading` slot), title with optional info text, trailing actions.
10
+ */
11
+ export interface NavigationBarProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
12
+ title: React.ReactNode;
13
+ infoText?: React.ReactNode;
14
+ onBack?: () => void;
15
+ leading?: React.ReactNode;
16
+ actions?: React.ReactNode;
17
+ }
18
+
19
+ const NavigationBar = React.forwardRef<HTMLElement, NavigationBarProps>(
20
+ ({ className, title, infoText, onBack, leading, actions, ...props }, ref) => (
21
+ <header
22
+ ref={ref}
23
+ className={cn("flex h-14 w-full items-center gap-2 border-b border-border bg-background px-2 font-sans", className)}
24
+ {...props}
25
+ >
26
+ <div className="flex min-w-9 shrink-0 items-center">
27
+ {leading ??
28
+ (onBack && (
29
+ <button
30
+ type="button"
31
+ onClick={onBack}
32
+ aria-label="Back"
33
+ className="flex size-9 items-center justify-center rounded-full text-foreground outline-none transition-colors hover:bg-accent focus-visible:bg-accent"
34
+ >
35
+ <ChevronLeft className="size-5" />
36
+ </button>
37
+ ))}
38
+ </div>
39
+ <div className="min-w-0 flex-1">
40
+ <p className="truncate text-body-lg font-medium text-foreground">{title}</p>
41
+ {infoText && <p className="truncate text-body-sm text-muted-foreground">{infoText}</p>}
42
+ </div>
43
+ {actions && <div className="flex shrink-0 items-center gap-1">{actions}</div>}
44
+ </header>
45
+ ),
46
+ );
47
+ NavigationBar.displayName = "NavigationBar";
48
+
49
+ export { NavigationBar };
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
5
+ import { cva } from "class-variance-authority";
6
+ import { ChevronDown } from "lucide-react";
7
+ import { cn } from "../lib/utils";
8
+
9
+ const NavigationMenu = React.forwardRef<
10
+ React.ElementRef<typeof NavigationMenuPrimitive.Root>,
11
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
12
+ >(({ className, children, ...props }, ref) => (
13
+ <NavigationMenuPrimitive.Root
14
+ ref={ref}
15
+ className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center font-sans", className)}
16
+ {...props}
17
+ >
18
+ {children}
19
+ <NavigationMenuViewport />
20
+ </NavigationMenuPrimitive.Root>
21
+ ));
22
+ NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
23
+
24
+ const NavigationMenuList = React.forwardRef<
25
+ React.ElementRef<typeof NavigationMenuPrimitive.List>,
26
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
27
+ >(({ className, ...props }, ref) => (
28
+ <NavigationMenuPrimitive.List
29
+ ref={ref}
30
+ className={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
31
+ {...props}
32
+ />
33
+ ));
34
+ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
35
+
36
+ const NavigationMenuItem = NavigationMenuPrimitive.Item;
37
+
38
+ const navigationMenuTriggerStyle = cva(
39
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50",
40
+ );
41
+
42
+ const NavigationMenuTrigger = React.forwardRef<
43
+ React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
44
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
45
+ >(({ className, children, ...props }, ref) => (
46
+ <NavigationMenuPrimitive.Trigger ref={ref} className={cn(navigationMenuTriggerStyle(), "group", className)} {...props}>
47
+ {children}
48
+ <ChevronDown
49
+ className="relative top-px ml-1 size-3 transition-transform duration-200 group-data-[state=open]:rotate-180"
50
+ aria-hidden
51
+ />
52
+ </NavigationMenuPrimitive.Trigger>
53
+ ));
54
+ NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
55
+
56
+ const NavigationMenuContent = React.forwardRef<
57
+ React.ElementRef<typeof NavigationMenuPrimitive.Content>,
58
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
59
+ >(({ className, ...props }, ref) => (
60
+ <NavigationMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out md:absolute md:w-auto",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ ));
69
+ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
70
+
71
+ const NavigationMenuLink = NavigationMenuPrimitive.Link;
72
+
73
+ const NavigationMenuViewport = React.forwardRef<
74
+ React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
75
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
76
+ >(({ className, ...props }, ref) => (
77
+ <div className="absolute left-0 top-full flex justify-center">
78
+ <NavigationMenuPrimitive.Viewport
79
+ ref={ref}
80
+ className={cn(
81
+ "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
82
+ className,
83
+ )}
84
+ {...props}
85
+ />
86
+ </div>
87
+ ));
88
+ NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
89
+
90
+ const NavigationMenuIndicator = React.forwardRef<
91
+ React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
92
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
93
+ >(({ className, ...props }, ref) => (
94
+ <NavigationMenuPrimitive.Indicator
95
+ ref={ref}
96
+ className={cn(
97
+ "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
98
+ className,
99
+ )}
100
+ {...props}
101
+ >
102
+ <div className="relative top-[60%] size-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
103
+ </NavigationMenuPrimitive.Indicator>
104
+ ));
105
+ NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
106
+
107
+ export {
108
+ navigationMenuTriggerStyle,
109
+ NavigationMenu,
110
+ NavigationMenuList,
111
+ NavigationMenuItem,
112
+ NavigationMenuContent,
113
+ NavigationMenuTrigger,
114
+ NavigationMenuLink,
115
+ NavigationMenuIndicator,
116
+ NavigationMenuViewport,
117
+ };
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Minus, Plus } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * NumberInput — a numeric field with increment / decrement controls.
9
+ */
10
+ export interface NumberInputProps
11
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange"> {
12
+ value?: number;
13
+ defaultValue?: number;
14
+ min?: number;
15
+ max?: number;
16
+ step?: number;
17
+ onChange?: (value: number) => void;
18
+ }
19
+
20
+ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
21
+ ({ className, value, defaultValue = 0, min, max, step = 1, onChange, disabled, ...props }, ref) => {
22
+ const [internal, setInternal] = React.useState(defaultValue);
23
+ const current = value ?? internal;
24
+
25
+ const clamp = (n: number) => {
26
+ let v = n;
27
+ if (min != null) v = Math.max(min, v);
28
+ if (max != null) v = Math.min(max, v);
29
+ return v;
30
+ };
31
+
32
+ const set = (n: number) => {
33
+ const v = clamp(n);
34
+ setInternal(v);
35
+ onChange?.(v);
36
+ };
37
+
38
+ return (
39
+ <div
40
+ className={cn(
41
+ "flex h-10 items-stretch overflow-hidden rounded-md border border-input font-sans focus-within:shadow-focus",
42
+ disabled && "pointer-events-none opacity-50",
43
+ className,
44
+ )}
45
+ >
46
+ <button
47
+ type="button"
48
+ aria-label="Decrease"
49
+ disabled={disabled || (min != null && current <= min)}
50
+ onClick={() => set(current - step)}
51
+ className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
52
+ >
53
+ <Minus className="size-4" />
54
+ </button>
55
+ <input
56
+ ref={ref}
57
+ type="number"
58
+ inputMode="numeric"
59
+ value={current}
60
+ min={min}
61
+ max={max}
62
+ step={step}
63
+ disabled={disabled}
64
+ onChange={(e) => {
65
+ const n = e.target.valueAsNumber;
66
+ if (!Number.isNaN(n)) set(n);
67
+ }}
68
+ className="w-full min-w-0 flex-1 border-x border-input bg-transparent px-2 text-center text-body-md text-foreground outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
69
+ {...props}
70
+ />
71
+ <button
72
+ type="button"
73
+ aria-label="Increase"
74
+ disabled={disabled || (max != null && current >= max)}
75
+ onClick={() => set(current + step)}
76
+ className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
77
+ >
78
+ <Plus className="size-4" />
79
+ </button>
80
+ </div>
81
+ );
82
+ },
83
+ );
84
+ NumberInput.displayName = "NumberInput";
85
+
86
+ export { NumberInput };