@hilum/ui 0.1.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.js +3 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.d.ts +992 -0
  7. package/dist/index.js +4433 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tokens.css +265 -0
  10. package/dist/tokens.d.ts +189 -0
  11. package/dist/tokens.js +193 -0
  12. package/dist/tokens.js.map +1 -0
  13. package/package.json +80 -0
  14. package/src/components/__tests__/data.test.tsx +180 -0
  15. package/src/components/__tests__/disclosure.test.tsx +198 -0
  16. package/src/components/__tests__/display.test.tsx +262 -0
  17. package/src/components/__tests__/feedback.test.tsx +138 -0
  18. package/src/components/__tests__/form.test.tsx +297 -0
  19. package/src/components/__tests__/layout.test.tsx +388 -0
  20. package/src/components/__tests__/overlay.test.tsx +90 -0
  21. package/src/components/__tests__/primitives.test.tsx +248 -0
  22. package/src/components/accordion.tsx +68 -0
  23. package/src/components/action-panel.tsx +112 -0
  24. package/src/components/activity-feed.tsx +69 -0
  25. package/src/components/alert-dialog.tsx +132 -0
  26. package/src/components/alert.tsx +44 -0
  27. package/src/components/aspect-ratio.tsx +19 -0
  28. package/src/components/avatar-stack.tsx +70 -0
  29. package/src/components/avatar.tsx +103 -0
  30. package/src/components/badge.tsx +33 -0
  31. package/src/components/breadcrumb.tsx +76 -0
  32. package/src/components/button-group.tsx +45 -0
  33. package/src/components/button.tsx +60 -0
  34. package/src/components/calendar.tsx +61 -0
  35. package/src/components/card-heading.tsx +60 -0
  36. package/src/components/card.tsx +120 -0
  37. package/src/components/carousel.tsx +222 -0
  38. package/src/components/chart.tsx +77 -0
  39. package/src/components/checkbox.tsx +35 -0
  40. package/src/components/collapsible.tsx +9 -0
  41. package/src/components/color-input.tsx +90 -0
  42. package/src/components/color-picker.tsx +111 -0
  43. package/src/components/combobox.tsx +207 -0
  44. package/src/components/command-palette.tsx +137 -0
  45. package/src/components/command.tsx +248 -0
  46. package/src/components/context-menu.tsx +201 -0
  47. package/src/components/data-table.tsx +151 -0
  48. package/src/components/date-picker.tsx +56 -0
  49. package/src/components/description-list.tsx +70 -0
  50. package/src/components/dialog.tsx +97 -0
  51. package/src/components/drawer.tsx +90 -0
  52. package/src/components/dropdown-menu.tsx +202 -0
  53. package/src/components/empty-state.tsx +43 -0
  54. package/src/components/field.tsx +45 -0
  55. package/src/components/grid-list.tsx +159 -0
  56. package/src/components/hover-card.tsx +34 -0
  57. package/src/components/input-group.tsx +134 -0
  58. package/src/components/input-number.tsx +142 -0
  59. package/src/components/input-otp.tsx +68 -0
  60. package/src/components/input.tsx +23 -0
  61. package/src/components/kbd.tsx +18 -0
  62. package/src/components/label.tsx +23 -0
  63. package/src/components/media-object.tsx +49 -0
  64. package/src/components/menubar.tsx +235 -0
  65. package/src/components/native-select.tsx +44 -0
  66. package/src/components/navigation-menu.tsx +158 -0
  67. package/src/components/notification.tsx +109 -0
  68. package/src/components/page-heading.tsx +125 -0
  69. package/src/components/pagination.tsx +89 -0
  70. package/src/components/popover.tsx +35 -0
  71. package/src/components/progress.tsx +23 -0
  72. package/src/components/property-row.tsx +56 -0
  73. package/src/components/radio-card.tsx +105 -0
  74. package/src/components/radio-group.tsx +38 -0
  75. package/src/components/resizable.tsx +74 -0
  76. package/src/components/scroll-area.tsx +42 -0
  77. package/src/components/section-heading.tsx +64 -0
  78. package/src/components/select.tsx +130 -0
  79. package/src/components/separator.tsx +25 -0
  80. package/src/components/sheet.tsx +117 -0
  81. package/src/components/sidebar.tsx +636 -0
  82. package/src/components/skeleton.tsx +14 -0
  83. package/src/components/slider.tsx +24 -0
  84. package/src/components/sonner.tsx +34 -0
  85. package/src/components/spinner.tsx +37 -0
  86. package/src/components/stacked-list.tsx +64 -0
  87. package/src/components/stat-card.tsx +54 -0
  88. package/src/components/steps.tsx +179 -0
  89. package/src/components/switch.tsx +33 -0
  90. package/src/components/table.tsx +101 -0
  91. package/src/components/tabs.tsx +55 -0
  92. package/src/components/textarea.tsx +21 -0
  93. package/src/components/toggle-group.tsx +49 -0
  94. package/src/components/toggle.tsx +66 -0
  95. package/src/components/tooltip.tsx +43 -0
  96. package/src/icons.ts +158 -0
  97. package/src/index.ts +84 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/tokens/tokens.ts +194 -0
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ interface InputNumberProps extends Omit<
7
+ React.ComponentProps<"input">,
8
+ "type" | "value" | "onChange"
9
+ > {
10
+ value: number;
11
+ onChange: (next: number) => void;
12
+ min?: number;
13
+ max?: number;
14
+ step?: number;
15
+ /** Suffix shown after the number (e.g. "px", "mm", "°", "%"). */
16
+ unit?: string;
17
+ /** Number of decimals to display. Default: 0. */
18
+ precision?: number;
19
+ /** Hides the up/down stepper buttons. */
20
+ hideSteppers?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Numeric input with up/down steppers, unit suffix, and arrow-key step (Shift = 10×).
25
+ * Designed for designer property panels — see PHASE_0_AUDIT.md §P0.2.
26
+ */
27
+ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
28
+ (
29
+ {
30
+ value,
31
+ onChange,
32
+ min = -Infinity,
33
+ max = Infinity,
34
+ step = 1,
35
+ unit,
36
+ precision = 0,
37
+ hideSteppers = false,
38
+ className,
39
+ disabled,
40
+ ...rest
41
+ },
42
+ ref,
43
+ ) => {
44
+ const [text, setText] = React.useState<string>(value.toFixed(precision));
45
+
46
+ React.useEffect(() => {
47
+ setText(value.toFixed(precision));
48
+ }, [value, precision]);
49
+
50
+ const clamp = React.useCallback((n: number) => Math.min(max, Math.max(min, n)), [min, max]);
51
+
52
+ const commit = (raw: string) => {
53
+ const n = parseFloat(raw);
54
+ if (Number.isFinite(n)) {
55
+ const next = clamp(n);
56
+ onChange(next);
57
+ setText(next.toFixed(precision));
58
+ } else {
59
+ setText(value.toFixed(precision));
60
+ }
61
+ };
62
+
63
+ const bump = (delta: number) => {
64
+ const next = clamp(value + delta);
65
+ onChange(next);
66
+ setText(next.toFixed(precision));
67
+ };
68
+
69
+ const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
70
+ if (e.key === "ArrowUp") {
71
+ e.preventDefault();
72
+ bump(step * (e.shiftKey ? 10 : 1));
73
+ } else if (e.key === "ArrowDown") {
74
+ e.preventDefault();
75
+ bump(-step * (e.shiftKey ? 10 : 1));
76
+ } else if (e.key === "Enter") {
77
+ commit((e.target as HTMLInputElement).value);
78
+ }
79
+ };
80
+
81
+ return (
82
+ <div
83
+ className={cn(
84
+ "inline-flex h-8 items-stretch rounded-md border border-taupe-200 bg-white overflow-hidden",
85
+ "focus-within:ring-2 focus-within:ring-brand-primary/20 focus-within:border-brand-primary",
86
+ disabled && "opacity-50 pointer-events-none",
87
+ className,
88
+ )}
89
+ >
90
+ <input
91
+ ref={ref}
92
+ type="text"
93
+ inputMode="decimal"
94
+ spellCheck={false}
95
+ value={text}
96
+ onChange={(e) => setText(e.target.value)}
97
+ onBlur={(e) => commit(e.target.value)}
98
+ onKeyDown={onKeyDown}
99
+ disabled={disabled}
100
+ className="w-full caption text-taupe-900 px-2 bg-transparent focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none text-right"
101
+ {...rest}
102
+ />
103
+ {unit && (
104
+ <span aria-hidden className="caption-xs text-taupe-400 self-center pr-1.5 select-none">
105
+ {unit}
106
+ </span>
107
+ )}
108
+ {!hideSteppers && (
109
+ <div className="flex flex-col border-l border-taupe-100">
110
+ <button
111
+ type="button"
112
+ tabIndex={-1}
113
+ onClick={() => bump(step)}
114
+ aria-label="Increment"
115
+ className="flex-1 px-1 hover:bg-taupe-50 active:bg-taupe-100 transition-colors text-taupe-500"
116
+ >
117
+ <svg viewBox="0 0 8 5" width="8" height="5" fill="currentColor">
118
+ <path d="M0 5 L4 0 L8 5 Z" />
119
+ </svg>
120
+ </button>
121
+ <div className="border-t border-taupe-100" />
122
+ <button
123
+ type="button"
124
+ tabIndex={-1}
125
+ onClick={() => bump(-step)}
126
+ aria-label="Decrement"
127
+ className="flex-1 px-1 hover:bg-taupe-50 active:bg-taupe-100 transition-colors text-taupe-500"
128
+ >
129
+ <svg viewBox="0 0 8 5" width="8" height="5" fill="currentColor">
130
+ <path d="M0 0 L4 5 L8 0 Z" />
131
+ </svg>
132
+ </button>
133
+ </div>
134
+ )}
135
+ </div>
136
+ );
137
+ },
138
+ );
139
+ InputNumber.displayName = "InputNumber";
140
+
141
+ export { InputNumber };
142
+ export type { InputNumberProps };
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { OTPInput, OTPInputContext } from "input-otp";
5
+ import { Minus } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const InputOTP = React.forwardRef<
9
+ React.ComponentRef<typeof OTPInput>,
10
+ React.ComponentPropsWithoutRef<typeof OTPInput>
11
+ >(({ className, containerClassName, ...props }, ref) => (
12
+ <OTPInput
13
+ ref={ref}
14
+ containerClassName={cn("flex items-center gap-2 has-disabled:opacity-50", containerClassName)}
15
+ className={cn("disabled:cursor-not-allowed", className)}
16
+ {...props}
17
+ />
18
+ ));
19
+ InputOTP.displayName = "InputOTP";
20
+
21
+ const InputOTPGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
22
+ ({ className, ...props }, ref) => (
23
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
24
+ ),
25
+ );
26
+ InputOTPGroup.displayName = "InputOTPGroup";
27
+
28
+ const InputOTPSlot = React.forwardRef<
29
+ HTMLDivElement,
30
+ React.HTMLAttributes<HTMLDivElement> & { index: number }
31
+ >(({ index, className, ...props }, ref) => {
32
+ const inputOTPContext = React.useContext(OTPInputContext);
33
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
34
+
35
+ return (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "relative flex h-12 w-11 items-center justify-center",
40
+ "border-y border-r border-taupe-200 body font-medium text-taupe-900",
41
+ "first:rounded-l-xl first:border-l last:rounded-r-xl",
42
+ "transition-all",
43
+ isActive && "z-10 ring-2 ring-brand-primary/40 border-brand-primary",
44
+ className,
45
+ )}
46
+ {...props}
47
+ >
48
+ {char}
49
+ {hasFakeCaret && (
50
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51
+ <div className="h-5 w-px animate-caret-blink bg-taupe-900 duration-1000" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ );
56
+ });
57
+ InputOTPSlot.displayName = "InputOTPSlot";
58
+
59
+ const InputOTPSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
60
+ ({ ...props }, ref) => (
61
+ <div ref={ref} role="separator" {...props}>
62
+ <Minus size={14} className="text-taupe-300" />
63
+ </div>
64
+ ),
65
+ );
66
+ InputOTPSeparator.displayName = "InputOTPSeparator";
67
+
68
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
5
+ return (
6
+ <input
7
+ type={type}
8
+ data-slot="input"
9
+ className={cn(
10
+ "flex h-9 w-full rounded-md border border-taupe-200 bg-white px-3 py-1",
11
+ "body text-taupe-900 placeholder:text-taupe-400",
12
+ "transition-colors duration-150",
13
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/20 focus-visible:border-brand-primary",
14
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-taupe-50",
15
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-taupe-900",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export { Input };
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const Kbd = React.forwardRef<HTMLElement, React.ComponentProps<"kbd">>(
5
+ ({ className, ...props }, ref) => (
6
+ <kbd
7
+ ref={ref}
8
+ className={cn(
9
+ "inline-flex h-5 min-w-5 items-center justify-center rounded border border-taupe-200 bg-taupe-50 px-1.5 font-mono caption-xs font-medium text-taupe-500",
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ ),
15
+ );
16
+ Kbd.displayName = "Kbd";
17
+
18
+ export { Kbd };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Label as LabelPrimitive } from "radix-ui";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const Label = React.forwardRef<
8
+ React.ElementRef<typeof LabelPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <LabelPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "body font-medium leading-none text-taupe-700",
15
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ Label.displayName = "Label";
22
+
23
+ export { Label };
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ interface MediaObjectProps {
5
+ media: React.ReactNode;
6
+ children: React.ReactNode;
7
+ mediaPosition?: "left" | "right";
8
+ align?: "top" | "center" | "bottom";
9
+ gap?: "sm" | "md" | "lg";
10
+ className?: string;
11
+ }
12
+
13
+ const alignMap = {
14
+ top: "items-start",
15
+ center: "items-center",
16
+ bottom: "items-end",
17
+ };
18
+
19
+ const gapMap = {
20
+ sm: "gap-3",
21
+ md: "gap-4",
22
+ lg: "gap-6",
23
+ };
24
+
25
+ function MediaObject({
26
+ media,
27
+ children,
28
+ mediaPosition = "left",
29
+ align = "top",
30
+ gap = "md",
31
+ className,
32
+ }: MediaObjectProps) {
33
+ return (
34
+ <div
35
+ className={cn(
36
+ "flex",
37
+ mediaPosition === "right" && "flex-row-reverse",
38
+ alignMap[align],
39
+ gapMap[gap],
40
+ className,
41
+ )}
42
+ >
43
+ <div className="shrink-0">{media}</div>
44
+ <div className="min-w-0 flex-1">{children}</div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export { MediaObject };
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Menubar } from "radix-ui";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const MenubarRoot = React.forwardRef<
9
+ React.ComponentRef<typeof Menubar.Root>,
10
+ React.ComponentPropsWithoutRef<typeof Menubar.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <Menubar.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "flex h-9 items-center gap-0.5 rounded-lg border border-taupe-100 bg-white px-1",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ MenubarRoot.displayName = "Menubar";
22
+
23
+ const MenubarMenu = Menubar.Menu;
24
+
25
+ const MenubarGroup = Menubar.Group;
26
+
27
+ const MenubarSub = Menubar.Sub;
28
+
29
+ const MenubarRadioGroup = Menubar.RadioGroup;
30
+
31
+ const MenubarTrigger = React.forwardRef<
32
+ React.ComponentRef<typeof Menubar.Trigger>,
33
+ React.ComponentPropsWithoutRef<typeof Menubar.Trigger>
34
+ >(({ className, ...props }, ref) => (
35
+ <Menubar.Trigger
36
+ ref={ref}
37
+ className={cn(
38
+ "flex cursor-default items-center rounded-md px-3 py-1 body font-medium text-taupe-700 select-none outline-none",
39
+ "hover:bg-taupe-50",
40
+ "data-[state=open]:bg-taupe-100 data-[state=open]:text-taupe-900",
41
+ "focus:bg-taupe-50",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ ));
47
+ MenubarTrigger.displayName = "MenubarTrigger";
48
+
49
+ const MenubarContent = React.forwardRef<
50
+ React.ComponentRef<typeof Menubar.Content>,
51
+ React.ComponentPropsWithoutRef<typeof Menubar.Content>
52
+ >(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
53
+ <Menubar.Portal>
54
+ <Menubar.Content
55
+ ref={ref}
56
+ align={align}
57
+ alignOffset={alignOffset}
58
+ sideOffset={sideOffset}
59
+ className={cn(
60
+ "z-50 min-w-[12rem] bg-white rounded-xl shadow-natural p-1",
61
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
62
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
63
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
64
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
65
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
66
+ className,
67
+ )}
68
+ {...props}
69
+ />
70
+ </Menubar.Portal>
71
+ ));
72
+ MenubarContent.displayName = "MenubarContent";
73
+
74
+ const MenubarItem = React.forwardRef<
75
+ React.ComponentRef<typeof Menubar.Item>,
76
+ React.ComponentPropsWithoutRef<typeof Menubar.Item> & {
77
+ inset?: boolean;
78
+ }
79
+ >(({ className, inset, ...props }, ref) => (
80
+ <Menubar.Item
81
+ ref={ref}
82
+ className={cn(
83
+ "relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-1.5",
84
+ "body text-taupe-700 outline-none transition-colors",
85
+ "focus:bg-taupe-50 focus:text-taupe-900",
86
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
87
+ inset && "pl-8",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ ));
93
+ MenubarItem.displayName = "MenubarItem";
94
+
95
+ const MenubarCheckboxItem = React.forwardRef<
96
+ React.ComponentRef<typeof Menubar.CheckboxItem>,
97
+ React.ComponentPropsWithoutRef<typeof Menubar.CheckboxItem>
98
+ >(({ className, children, checked, ...props }, ref) => (
99
+ <Menubar.CheckboxItem
100
+ ref={ref}
101
+ className={cn(
102
+ "relative flex cursor-default select-none items-center gap-2 rounded-md pl-8 pr-2.5 py-1.5",
103
+ "body text-taupe-700 outline-none transition-colors",
104
+ "focus:bg-taupe-50 focus:text-taupe-900",
105
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
106
+ className,
107
+ )}
108
+ checked={checked}
109
+ {...props}
110
+ >
111
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
112
+ <Menubar.ItemIndicator>
113
+ <Check size={13} className="text-taupe-900" />
114
+ </Menubar.ItemIndicator>
115
+ </span>
116
+ {children}
117
+ </Menubar.CheckboxItem>
118
+ ));
119
+ MenubarCheckboxItem.displayName = "MenubarCheckboxItem";
120
+
121
+ const MenubarRadioItem = React.forwardRef<
122
+ React.ComponentRef<typeof Menubar.RadioItem>,
123
+ React.ComponentPropsWithoutRef<typeof Menubar.RadioItem>
124
+ >(({ className, children, ...props }, ref) => (
125
+ <Menubar.RadioItem
126
+ ref={ref}
127
+ className={cn(
128
+ "relative flex cursor-default select-none items-center gap-2 rounded-md pl-8 pr-2.5 py-1.5",
129
+ "body text-taupe-700 outline-none transition-colors",
130
+ "focus:bg-taupe-50 focus:text-taupe-900",
131
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
132
+ className,
133
+ )}
134
+ {...props}
135
+ >
136
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
137
+ <Menubar.ItemIndicator>
138
+ <Circle size={8} className="fill-taupe-900 text-taupe-900" />
139
+ </Menubar.ItemIndicator>
140
+ </span>
141
+ {children}
142
+ </Menubar.RadioItem>
143
+ ));
144
+ MenubarRadioItem.displayName = "MenubarRadioItem";
145
+
146
+ const MenubarLabel = React.forwardRef<
147
+ React.ComponentRef<typeof Menubar.Label>,
148
+ React.ComponentPropsWithoutRef<typeof Menubar.Label> & {
149
+ inset?: boolean;
150
+ }
151
+ >(({ className, inset, ...props }, ref) => (
152
+ <Menubar.Label
153
+ ref={ref}
154
+ className={cn("px-2.5 py-1 label text-taupe-400", inset && "pl-8", className)}
155
+ {...props}
156
+ />
157
+ ));
158
+ MenubarLabel.displayName = "MenubarLabel";
159
+
160
+ const MenubarSeparator = React.forwardRef<
161
+ React.ComponentRef<typeof Menubar.Separator>,
162
+ React.ComponentPropsWithoutRef<typeof Menubar.Separator>
163
+ >(({ className, ...props }, ref) => (
164
+ <Menubar.Separator
165
+ ref={ref}
166
+ className={cn("mx-1 my-1 h-px bg-taupe-100", className)}
167
+ {...props}
168
+ />
169
+ ));
170
+ MenubarSeparator.displayName = "MenubarSeparator";
171
+
172
+ const MenubarSubTrigger = React.forwardRef<
173
+ React.ComponentRef<typeof Menubar.SubTrigger>,
174
+ React.ComponentPropsWithoutRef<typeof Menubar.SubTrigger> & {
175
+ inset?: boolean;
176
+ }
177
+ >(({ className, inset, children, ...props }, ref) => (
178
+ <Menubar.SubTrigger
179
+ ref={ref}
180
+ className={cn(
181
+ "relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-1.5",
182
+ "body text-taupe-700 outline-none transition-colors",
183
+ "focus:bg-taupe-50 focus:text-taupe-900 data-[state=open]:bg-taupe-50",
184
+ inset && "pl-8",
185
+ className,
186
+ )}
187
+ {...props}
188
+ >
189
+ {children}
190
+ <ChevronRight size={14} className="ml-auto text-taupe-400" />
191
+ </Menubar.SubTrigger>
192
+ ));
193
+ MenubarSubTrigger.displayName = "MenubarSubTrigger";
194
+
195
+ const MenubarSubContent = React.forwardRef<
196
+ React.ComponentRef<typeof Menubar.SubContent>,
197
+ React.ComponentPropsWithoutRef<typeof Menubar.SubContent>
198
+ >(({ className, ...props }, ref) => (
199
+ <Menubar.SubContent
200
+ ref={ref}
201
+ className={cn(
202
+ "z-50 min-w-[8rem] bg-white rounded-xl border border-taupe-100 shadow-natural p-1",
203
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
204
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
205
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
206
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
207
+ className,
208
+ )}
209
+ {...props}
210
+ />
211
+ ));
212
+ MenubarSubContent.displayName = "MenubarSubContent";
213
+
214
+ function MenubarShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
215
+ return <span className={cn("ml-auto caption text-taupe-400", className)} {...props} />;
216
+ }
217
+ MenubarShortcut.displayName = "MenubarShortcut";
218
+
219
+ export {
220
+ MenubarRoot as Menubar,
221
+ MenubarMenu,
222
+ MenubarTrigger,
223
+ MenubarContent,
224
+ MenubarItem,
225
+ MenubarCheckboxItem,
226
+ MenubarRadioGroup,
227
+ MenubarRadioItem,
228
+ MenubarLabel,
229
+ MenubarSeparator,
230
+ MenubarSub,
231
+ MenubarSubTrigger,
232
+ MenubarSubContent,
233
+ MenubarShortcut,
234
+ MenubarGroup,
235
+ };
@@ -0,0 +1,44 @@
1
+ import * as React from "react";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const NativeSelect = React.forwardRef<
6
+ HTMLSelectElement,
7
+ React.SelectHTMLAttributes<HTMLSelectElement>
8
+ >(({ className, children, ...props }, ref) => (
9
+ <div className="relative">
10
+ <select
11
+ ref={ref}
12
+ className={cn(
13
+ "peer h-9 w-full appearance-none rounded-lg border border-taupe-200 bg-white pl-3 pr-8",
14
+ "body text-taupe-900",
15
+ "focus:outline-none focus:ring-2 focus:ring-taupe-400/30 focus:border-taupe-400",
16
+ "disabled:cursor-not-allowed disabled:opacity-50",
17
+ "aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:ring-red-500/20",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ {children}
23
+ </select>
24
+ <ChevronDown
25
+ size={14}
26
+ className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-taupe-400 peer-disabled:opacity-50"
27
+ />
28
+ </div>
29
+ ));
30
+ NativeSelect.displayName = "NativeSelect";
31
+
32
+ const NativeSelectOption = React.forwardRef<
33
+ HTMLOptionElement,
34
+ React.OptionHTMLAttributes<HTMLOptionElement>
35
+ >((props, ref) => <option ref={ref} {...props} />);
36
+ NativeSelectOption.displayName = "NativeSelectOption";
37
+
38
+ const NativeSelectOptGroup = React.forwardRef<
39
+ HTMLOptGroupElement,
40
+ React.OptgroupHTMLAttributes<HTMLOptGroupElement>
41
+ >((props, ref) => <optgroup ref={ref} {...props} />);
42
+ NativeSelectOptGroup.displayName = "NativeSelectOptGroup";
43
+
44
+ export { NativeSelect, NativeSelectOption, NativeSelectOptGroup };