@momentumworld/ui-react 0.17.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 (138) hide show
  1. package/dist/chunk-2HQPCV2L.js +895 -0
  2. package/dist/chunk-2HQPCV2L.js.map +1 -0
  3. package/dist/form.d.ts +175 -0
  4. package/dist/form.js +356 -0
  5. package/dist/form.js.map +1 -0
  6. package/dist/index.d.ts +2162 -0
  7. package/dist/index.js +11640 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/input-JCZs61MO.d.ts +24 -0
  10. package/package.json +90 -0
  11. package/src/components/form/checkbox-group-field.tsx +101 -0
  12. package/src/components/form/date-field.tsx +79 -0
  13. package/src/components/form/date-range-field.tsx +106 -0
  14. package/src/components/form/form-context.ts +10 -0
  15. package/src/components/form/form.tsx +54 -0
  16. package/src/components/form/number-field.tsx +69 -0
  17. package/src/components/form/select-field.tsx +76 -0
  18. package/src/components/form/submit-button.tsx +28 -0
  19. package/src/components/form/text-field.tsx +107 -0
  20. package/src/components/layout/dashboard-header.tsx +54 -0
  21. package/src/components/layout/dashboard-panel.tsx +34 -0
  22. package/src/components/layout/dashboard-topbar.tsx +221 -0
  23. package/src/components/theme-provider.tsx +403 -0
  24. package/src/components/ui/accordion.tsx +69 -0
  25. package/src/components/ui/alert-dialog.tsx +169 -0
  26. package/src/components/ui/alert.tsx +80 -0
  27. package/src/components/ui/animated-counter.tsx +109 -0
  28. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  29. package/src/components/ui/app-store-buttons.tsx +182 -0
  30. package/src/components/ui/aspect-ratio.tsx +23 -0
  31. package/src/components/ui/aurora-orb.tsx +163 -0
  32. package/src/components/ui/aurora-text.tsx +76 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +150 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/chat-divider.tsx +67 -0
  45. package/src/components/ui/chat.tsx +494 -0
  46. package/src/components/ui/checkbox-group.tsx +16 -0
  47. package/src/components/ui/checkbox.tsx +82 -0
  48. package/src/components/ui/collapsible.tsx +45 -0
  49. package/src/components/ui/color-picker.tsx +389 -0
  50. package/src/components/ui/combobox.tsx +411 -0
  51. package/src/components/ui/command.tsx +264 -0
  52. package/src/components/ui/confetti.tsx +196 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/context-meter.tsx +134 -0
  55. package/src/components/ui/credit-card.tsx +214 -0
  56. package/src/components/ui/data-table-paged.tsx +163 -0
  57. package/src/components/ui/date-picker.tsx +154 -0
  58. package/src/components/ui/dialog.tsx +196 -0
  59. package/src/components/ui/drawer.tsx +135 -0
  60. package/src/components/ui/empty.tsx +127 -0
  61. package/src/components/ui/fab.tsx +45 -0
  62. package/src/components/ui/featured-icon.tsx +149 -0
  63. package/src/components/ui/field.tsx +88 -0
  64. package/src/components/ui/fieldset.tsx +29 -0
  65. package/src/components/ui/form.tsx +17 -0
  66. package/src/components/ui/frame.tsx +82 -0
  67. package/src/components/ui/generic-empty.tsx +142 -0
  68. package/src/components/ui/group.tsx +97 -0
  69. package/src/components/ui/help-tip.tsx +94 -0
  70. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  71. package/src/components/ui/input-group.tsx +102 -0
  72. package/src/components/ui/input-otp.tsx +96 -0
  73. package/src/components/ui/input.tsx +66 -0
  74. package/src/components/ui/item.tsx +198 -0
  75. package/src/components/ui/kbd.tsx +30 -0
  76. package/src/components/ui/label.tsx +28 -0
  77. package/src/components/ui/markdown.tsx +188 -0
  78. package/src/components/ui/menu.tsx +312 -0
  79. package/src/components/ui/menubar.tsx +93 -0
  80. package/src/components/ui/meter.tsx +67 -0
  81. package/src/components/ui/multi-select.tsx +308 -0
  82. package/src/components/ui/navigation-menu.tsx +143 -0
  83. package/src/components/ui/number-field.tsx +160 -0
  84. package/src/components/ui/pagination-controls.tsx +74 -0
  85. package/src/components/ui/pagination.tsx +149 -0
  86. package/src/components/ui/pipeline.tsx +102 -0
  87. package/src/components/ui/popover.tsx +119 -0
  88. package/src/components/ui/preview-card.tsx +55 -0
  89. package/src/components/ui/progress.tsx +289 -0
  90. package/src/components/ui/qr-code.tsx +150 -0
  91. package/src/components/ui/radio-group.tsx +103 -0
  92. package/src/components/ui/resizable.tsx +56 -0
  93. package/src/components/ui/scroll-area.tsx +90 -0
  94. package/src/components/ui/scroller.tsx +38 -0
  95. package/src/components/ui/section-header.tsx +118 -0
  96. package/src/components/ui/segmented-control.tsx +83 -0
  97. package/src/components/ui/select.tsx +181 -0
  98. package/src/components/ui/separator.tsx +23 -0
  99. package/src/components/ui/sheet.tsx +224 -0
  100. package/src/components/ui/sidebar.tsx +774 -0
  101. package/src/components/ui/skeleton.tsx +16 -0
  102. package/src/components/ui/slider.tsx +108 -0
  103. package/src/components/ui/smooth-scroll.tsx +206 -0
  104. package/src/components/ui/social-button.tsx +247 -0
  105. package/src/components/ui/spinner-on-demand.tsx +32 -0
  106. package/src/components/ui/spinner.tsx +18 -0
  107. package/src/components/ui/stat.tsx +187 -0
  108. package/src/components/ui/stepper.tsx +167 -0
  109. package/src/components/ui/switch.tsx +56 -0
  110. package/src/components/ui/table.tsx +126 -0
  111. package/src/components/ui/tabs.tsx +90 -0
  112. package/src/components/ui/tag.tsx +242 -0
  113. package/src/components/ui/target-countdown.tsx +46 -0
  114. package/src/components/ui/text-editor.tsx +313 -0
  115. package/src/components/ui/textarea.tsx +51 -0
  116. package/src/components/ui/thinking-orb.tsx +6 -0
  117. package/src/components/ui/time-picker.tsx +308 -0
  118. package/src/components/ui/timeline.tsx +116 -0
  119. package/src/components/ui/toast.tsx +268 -0
  120. package/src/components/ui/toggle-group.tsx +101 -0
  121. package/src/components/ui/toggle.tsx +45 -0
  122. package/src/components/ui/toolbar.tsx +89 -0
  123. package/src/components/ui/tooltip.tsx +102 -0
  124. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  125. package/src/components/ui/video-player.tsx +275 -0
  126. package/src/components/upload/avatar-upload-base.tsx +131 -0
  127. package/src/components/upload/image-upload-base.tsx +112 -0
  128. package/src/form.ts +17 -0
  129. package/src/index.ts +143 -0
  130. package/src/lib/hooks/use-callback-ref.ts +15 -0
  131. package/src/lib/hooks/use-first-render.ts +11 -0
  132. package/src/lib/hooks/use-hover.ts +53 -0
  133. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  134. package/src/lib/hooks/use-media-query.ts +164 -0
  135. package/src/lib/utils/css.ts +6 -0
  136. package/src/styles.css +329 -0
  137. package/src/types/helpers.ts +24 -0
  138. package/src/types/react.d.ts +7 -0
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { Meter as MeterPrimitive } from "@base-ui/react/meter";
4
+
5
+ import { cn } from "../../lib/utils/css";
6
+
7
+ function Meter({ className, children, ...props }: MeterPrimitive.Root.Props) {
8
+ return (
9
+ <MeterPrimitive.Root
10
+ className={cn("flex w-full flex-col gap-2", className)}
11
+ {...props}
12
+ >
13
+ {children ? (
14
+ children
15
+ ) : (
16
+ <MeterTrack>
17
+ <MeterIndicator />
18
+ </MeterTrack>
19
+ )}
20
+ </MeterPrimitive.Root>
21
+ );
22
+ }
23
+
24
+ function MeterLabel({ className, ...props }: MeterPrimitive.Label.Props) {
25
+ return (
26
+ <MeterPrimitive.Label
27
+ className={cn("font-medium text-foreground text-sm", className)}
28
+ data-slot="meter-label"
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function MeterTrack({ className, ...props }: MeterPrimitive.Track.Props) {
35
+ return (
36
+ <MeterPrimitive.Track
37
+ className={cn("block h-2 w-full overflow-hidden bg-input", className)}
38
+ data-slot="meter-track"
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function MeterIndicator({
45
+ className,
46
+ ...props
47
+ }: MeterPrimitive.Indicator.Props) {
48
+ return (
49
+ <MeterPrimitive.Indicator
50
+ className={cn("bg-primary transition-all duration-500", className)}
51
+ data-slot="meter-indicator"
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function MeterValue({ className, ...props }: MeterPrimitive.Value.Props) {
58
+ return (
59
+ <MeterPrimitive.Value
60
+ className={cn("text-foreground text-sm tabular-nums", className)}
61
+ data-slot="meter-value"
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ export { Meter, MeterLabel, MeterTrack, MeterIndicator, MeterValue };
@@ -0,0 +1,308 @@
1
+ "use client";
2
+
3
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
4
+ import { ChevronsUpDownIcon, SearchIcon } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "../../lib/utils/css";
8
+ import { Button } from "./button";
9
+
10
+ interface MultiSelectItem {
11
+ label: string;
12
+ value: string;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ interface MultiSelectProps {
17
+ items: MultiSelectItem[];
18
+ /** Controlled selected values. */
19
+ value?: string[];
20
+ /** Uncontrolled initial selected values. */
21
+ defaultValue?: string[];
22
+ onValueChange?: (value: string[]) => void;
23
+ size?: "sm" | "default" | "lg";
24
+ placeholder?: React.ReactNode;
25
+ /** Extra context rendered next to the selected count, e.g. "of 12 users". */
26
+ supportingText?: React.ReactNode;
27
+ /** Formats the trigger text from the selected count. Defaults to `${count} selected`. */
28
+ selectedCountFormatter?: (count: number) => React.ReactNode;
29
+ showSearch?: boolean;
30
+ searchPlaceholder?: string;
31
+ showFooter?: boolean;
32
+ selectAllLabel?: React.ReactNode;
33
+ resetLabel?: React.ReactNode;
34
+ emptyStateTitle?: React.ReactNode;
35
+ emptyStateDescription?: React.ReactNode;
36
+ disabled?: boolean;
37
+ name?: string;
38
+ required?: boolean;
39
+ id?: string;
40
+ /** Trigger class name. */
41
+ className?: string;
42
+ popupClassName?: string;
43
+ sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
44
+ }
45
+
46
+ function MultiSelect({
47
+ items,
48
+ value: valueProp,
49
+ defaultValue,
50
+ onValueChange,
51
+ size = "default",
52
+ placeholder = "Select options",
53
+ supportingText,
54
+ selectedCountFormatter = (count) => `${count} selected`,
55
+ showSearch = true,
56
+ searchPlaceholder = "Search",
57
+ showFooter = true,
58
+ selectAllLabel = "Select all",
59
+ resetLabel = "Reset",
60
+ emptyStateTitle = "No results found",
61
+ emptyStateDescription,
62
+ disabled,
63
+ name,
64
+ required,
65
+ id,
66
+ className,
67
+ popupClassName,
68
+ sideOffset = 4,
69
+ }: MultiSelectProps) {
70
+ const [uncontrolledValue, setUncontrolledValue] = React.useState<string[]>(
71
+ defaultValue ?? [],
72
+ );
73
+ const [search, setSearch] = React.useState("");
74
+
75
+ const isControlled = valueProp !== undefined;
76
+ const value = isControlled ? valueProp : uncontrolledValue;
77
+
78
+ const setValue = (next: string[]) => {
79
+ if (!isControlled) {
80
+ setUncontrolledValue(next);
81
+ }
82
+ onValueChange?.(next);
83
+ };
84
+
85
+ const query = search.trim().toLowerCase();
86
+ const filteredItems = query
87
+ ? items.filter((item) => item.label.toLowerCase().includes(query))
88
+ : items;
89
+
90
+ const count = value.length;
91
+
92
+ return (
93
+ <SelectPrimitive.Root
94
+ disabled={disabled}
95
+ multiple
96
+ name={name}
97
+ onOpenChange={(open) => {
98
+ if (!open) {
99
+ setSearch("");
100
+ }
101
+ }}
102
+ onValueChange={(next) => setValue(next as string[])}
103
+ required={required}
104
+ value={value}
105
+ >
106
+ <SelectPrimitive.Trigger
107
+ className={cn(
108
+ "relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-center gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
109
+ size === "sm" &&
110
+ "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
111
+ size === "lg" && "min-h-10 sm:min-h-9",
112
+ className,
113
+ )}
114
+ data-slot="multi-select-trigger"
115
+ id={id}
116
+ >
117
+ <span className="flex flex-1 items-baseline gap-1.5 truncate">
118
+ {count > 0 ? (
119
+ <>
120
+ <span className="truncate">{selectedCountFormatter(count)}</span>
121
+ {supportingText ? (
122
+ <span className="truncate font-normal text-muted-foreground">
123
+ {supportingText}
124
+ </span>
125
+ ) : null}
126
+ </>
127
+ ) : (
128
+ <span className="truncate text-muted-foreground">
129
+ {placeholder}
130
+ </span>
131
+ )}
132
+ </span>
133
+ <SelectPrimitive.Icon data-slot="multi-select-icon">
134
+ <ChevronsUpDownIcon className="-me-1 size-4.5 opacity-80 sm:size-4" />
135
+ </SelectPrimitive.Icon>
136
+ </SelectPrimitive.Trigger>
137
+ <SelectPrimitive.Portal>
138
+ <SelectPrimitive.Positioner
139
+ alignItemWithTrigger={false}
140
+ className="z-50 select-none"
141
+ data-slot="multi-select-positioner"
142
+ sideOffset={sideOffset}
143
+ >
144
+ <SelectPrimitive.Popup
145
+ className={cn(
146
+ "relative flex max-h-[min(var(--available-height),20rem)] w-(--anchor-width) min-w-48 max-w-(--available-width) origin-(--transform-origin) flex-col overflow-hidden rounded-lg border bg-popover not-dark:bg-clip-padding text-foreground shadow-lg/5 transition-[transform,scale,opacity] duration-150 ease-out before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
147
+ popupClassName,
148
+ )}
149
+ data-slot="multi-select-popup"
150
+ >
151
+ {showSearch ? (
152
+ <MultiSelectSearch
153
+ onChange={setSearch}
154
+ placeholder={searchPlaceholder}
155
+ value={search}
156
+ />
157
+ ) : null}
158
+ <SelectPrimitive.List
159
+ className="min-h-0 flex-1 overflow-y-auto p-1"
160
+ data-slot="multi-select-list"
161
+ >
162
+ {filteredItems.map((item) => (
163
+ <MultiSelectOption
164
+ disabled={item.disabled}
165
+ key={item.value}
166
+ label={item.label}
167
+ value={item.value}
168
+ />
169
+ ))}
170
+ {filteredItems.length === 0 ? (
171
+ <div
172
+ className="flex flex-col items-center justify-center gap-0.5 px-4 py-6 text-center"
173
+ data-slot="multi-select-empty"
174
+ >
175
+ <p className="font-medium text-foreground text-sm">
176
+ {emptyStateTitle}
177
+ </p>
178
+ {emptyStateDescription ? (
179
+ <p className="text-muted-foreground text-sm">
180
+ {emptyStateDescription}
181
+ </p>
182
+ ) : null}
183
+ </div>
184
+ ) : null}
185
+ </SelectPrimitive.List>
186
+ {showFooter ? (
187
+ <div
188
+ className="flex items-center justify-between gap-1 border-t p-1"
189
+ data-slot="multi-select-footer"
190
+ >
191
+ <Button
192
+ onClick={() =>
193
+ setValue(
194
+ items
195
+ .filter((item) => !item.disabled)
196
+ .map((item) => item.value),
197
+ )
198
+ }
199
+ size="xs"
200
+ variant="ghost"
201
+ >
202
+ {selectAllLabel}
203
+ </Button>
204
+ <Button
205
+ className="text-muted-foreground"
206
+ onClick={() => setValue([])}
207
+ size="xs"
208
+ variant="ghost"
209
+ >
210
+ {resetLabel}
211
+ </Button>
212
+ </div>
213
+ ) : null}
214
+ </SelectPrimitive.Popup>
215
+ </SelectPrimitive.Positioner>
216
+ </SelectPrimitive.Portal>
217
+ </SelectPrimitive.Root>
218
+ );
219
+ }
220
+
221
+ function MultiSelectSearch({
222
+ value,
223
+ onChange,
224
+ placeholder,
225
+ }: {
226
+ value: string;
227
+ onChange: (value: string) => void;
228
+ placeholder?: string;
229
+ }) {
230
+ const inputRef = React.useRef<HTMLInputElement>(null);
231
+
232
+ // Focus after Base UI's own initial focus management has run.
233
+ React.useEffect(() => {
234
+ const frame = requestAnimationFrame(() => inputRef.current?.focus());
235
+ return () => cancelAnimationFrame(frame);
236
+ }, []);
237
+
238
+ return (
239
+ <div
240
+ className="flex shrink-0 items-center gap-2 border-b px-2.5"
241
+ data-slot="multi-select-search"
242
+ >
243
+ <SearchIcon className="size-4.5 shrink-0 opacity-64 sm:size-4" />
244
+ <input
245
+ className="h-9 min-w-0 flex-1 bg-transparent text-base text-foreground outline-none placeholder:text-muted-foreground sm:h-8 sm:text-sm"
246
+ onChange={(event) => onChange(event.target.value)}
247
+ onKeyDown={(event) => {
248
+ // Let list navigation keys bubble to Base UI; keep everything else
249
+ // (printable characters, Space, Backspace…) from triggering the
250
+ // select's typeahead while typing in the search box.
251
+ if (
252
+ event.key !== "ArrowDown" &&
253
+ event.key !== "ArrowUp" &&
254
+ event.key !== "Enter" &&
255
+ event.key !== "Escape"
256
+ ) {
257
+ event.stopPropagation();
258
+ }
259
+ }}
260
+ placeholder={placeholder}
261
+ ref={inputRef}
262
+ value={value}
263
+ />
264
+ </div>
265
+ );
266
+ }
267
+
268
+ function MultiSelectOption({
269
+ label,
270
+ value,
271
+ disabled,
272
+ }: {
273
+ label: string;
274
+ value: string;
275
+ disabled?: boolean;
276
+ }) {
277
+ return (
278
+ <SelectPrimitive.Item
279
+ className="grid min-h-8 cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
280
+ data-slot="multi-select-item"
281
+ disabled={disabled}
282
+ value={value}
283
+ >
284
+ <SelectPrimitive.ItemIndicator className="col-start-1">
285
+ {/** biome-ignore lint/a11y/noSvgWithoutTitle: Imported from library */}
286
+ <svg
287
+ fill="none"
288
+ height="24"
289
+ stroke="currentColor"
290
+ strokeLinecap="round"
291
+ strokeLinejoin="round"
292
+ strokeWidth="2"
293
+ viewBox="0 0 24 24"
294
+ width="24"
295
+ xmlns="http://www.w3.org/1500/svg"
296
+ >
297
+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
298
+ </svg>
299
+ </SelectPrimitive.ItemIndicator>
300
+ <SelectPrimitive.ItemText className="col-start-2 min-w-0 truncate">
301
+ {label}
302
+ </SelectPrimitive.ItemText>
303
+ </SelectPrimitive.Item>
304
+ );
305
+ }
306
+
307
+ export { MultiSelect };
308
+ export type { MultiSelectItem, MultiSelectProps };
@@ -0,0 +1,143 @@
1
+ "use client";
2
+
3
+ import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu";
4
+ import { cva } from "class-variance-authority";
5
+ import { ChevronDownIcon } from "lucide-react";
6
+ import type * as React from "react";
7
+
8
+ import { cn } from "../../lib/utils/css";
9
+
10
+ function NavigationMenu({
11
+ className,
12
+ children,
13
+ ...props
14
+ }: NavigationMenuPrimitive.Root.Props) {
15
+ return (
16
+ <NavigationMenuPrimitive.Root
17
+ className={cn(
18
+ "relative flex max-w-max flex-1 items-center justify-center",
19
+ className,
20
+ )}
21
+ data-slot="navigation-menu"
22
+ {...props}
23
+ >
24
+ {children}
25
+ <NavigationMenuPrimitive.Portal>
26
+ <NavigationMenuPrimitive.Positioner
27
+ className="z-50 box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] before:absolute before:content-['']"
28
+ collisionAvoidance={{ side: "none" }}
29
+ collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
30
+ data-slot="navigation-menu-positioner"
31
+ sideOffset={8}
32
+ >
33
+ <NavigationMenuPrimitive.Popup
34
+ className="relative h-(--popup-height) w-full min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 outline-none transition-[opacity,transform,width,height,scale,translate] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 sm:w-(--popup-width) dark:before:shadow-[0_-1px_--theme(--color-white/6%)]"
35
+ data-slot="navigation-menu-popup"
36
+ >
37
+ <NavigationMenuPrimitive.Viewport
38
+ className="relative h-full w-full overflow-hidden"
39
+ data-slot="navigation-menu-viewport"
40
+ />
41
+ </NavigationMenuPrimitive.Popup>
42
+ </NavigationMenuPrimitive.Positioner>
43
+ </NavigationMenuPrimitive.Portal>
44
+ </NavigationMenuPrimitive.Root>
45
+ );
46
+ }
47
+
48
+ function NavigationMenuList({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
52
+ return (
53
+ <NavigationMenuPrimitive.List
54
+ className={cn(
55
+ "group flex flex-1 list-none items-center justify-center gap-1",
56
+ className,
57
+ )}
58
+ data-slot="navigation-menu-list"
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function NavigationMenuItem({
65
+ className,
66
+ ...props
67
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
68
+ return (
69
+ <NavigationMenuPrimitive.Item
70
+ className={cn("relative", className)}
71
+ data-slot="navigation-menu-item"
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ const navigationMenuTriggerStyle = cva(
78
+ "group inline-flex h-9 w-max cursor-pointer select-none items-center justify-center gap-1 rounded-lg bg-transparent px-3 py-2 font-medium text-base outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-popup-open:bg-accent/50 data-popup-open:text-accent-foreground disabled:pointer-events-none disabled:opacity-64 sm:h-8 sm:text-sm",
79
+ );
80
+
81
+ function NavigationMenuTrigger({
82
+ className,
83
+ children,
84
+ ...props
85
+ }: NavigationMenuPrimitive.Trigger.Props) {
86
+ return (
87
+ <NavigationMenuPrimitive.Trigger
88
+ className={cn(navigationMenuTriggerStyle(), className)}
89
+ data-slot="navigation-menu-trigger"
90
+ {...props}
91
+ >
92
+ {children}
93
+ <NavigationMenuPrimitive.Icon
94
+ className="relative top-px transition-transform duration-300 ease-in-out group-data-popup-open:rotate-180"
95
+ data-slot="navigation-menu-icon"
96
+ >
97
+ <ChevronDownIcon className="size-3.5 opacity-80" />
98
+ </NavigationMenuPrimitive.Icon>
99
+ </NavigationMenuPrimitive.Trigger>
100
+ );
101
+ }
102
+
103
+ function NavigationMenuContent({
104
+ className,
105
+ ...props
106
+ }: NavigationMenuPrimitive.Content.Props) {
107
+ return (
108
+ <NavigationMenuPrimitive.Content
109
+ className={cn(
110
+ "h-full w-max p-2 transition-[opacity,translate,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:opacity-0 data-starting-style:opacity-0 data-[activation-direction=left]:data-ending-style:translate-x-[50%] data-[activation-direction=left]:data-starting-style:translate-x-[-50%] data-[activation-direction=right]:data-ending-style:translate-x-[-50%] data-[activation-direction=right]:data-starting-style:translate-x-[50%]",
111
+ className,
112
+ )}
113
+ data-slot="navigation-menu-content"
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ function NavigationMenuLink({
120
+ className,
121
+ ...props
122
+ }: NavigationMenuPrimitive.Link.Props) {
123
+ return (
124
+ <NavigationMenuPrimitive.Link
125
+ className={cn(
126
+ "flex cursor-pointer flex-col gap-1 rounded-md p-2 text-base no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background sm:text-sm [&_svg:not([class*='size-'])]:size-4",
127
+ className,
128
+ )}
129
+ data-slot="navigation-menu-link"
130
+ {...props}
131
+ />
132
+ );
133
+ }
134
+
135
+ export {
136
+ NavigationMenu,
137
+ NavigationMenuList,
138
+ NavigationMenuItem,
139
+ NavigationMenuTrigger,
140
+ NavigationMenuContent,
141
+ NavigationMenuLink,
142
+ navigationMenuTriggerStyle,
143
+ };
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field";
4
+ import { MinusIcon, PlusIcon } from "lucide-react";
5
+ import * as React from "react";
6
+ import { Label } from "./label";
7
+ import { cn } from "../../lib/utils/css";
8
+
9
+ const NumberFieldContext = React.createContext<{
10
+ fieldId: string;
11
+ } | null>(null);
12
+
13
+ function NumberField({
14
+ id,
15
+ className,
16
+ size = "default",
17
+ ...props
18
+ }: NumberFieldPrimitive.Root.Props & {
19
+ size?: "sm" | "default" | "lg";
20
+ }) {
21
+ const generatedId = React.useId();
22
+ const fieldId = id ?? generatedId;
23
+
24
+ return (
25
+ <NumberFieldContext.Provider value={{ fieldId }}>
26
+ <NumberFieldPrimitive.Root
27
+ className={cn("flex w-full flex-col items-start gap-2", className)}
28
+ data-size={size}
29
+ data-slot="number-field"
30
+ id={fieldId}
31
+ {...props}
32
+ />
33
+ </NumberFieldContext.Provider>
34
+ );
35
+ }
36
+
37
+ function NumberFieldGroup({
38
+ className,
39
+ ...props
40
+ }: NumberFieldPrimitive.Group.Props) {
41
+ return (
42
+ <NumberFieldPrimitive.Group
43
+ className={cn(
44
+ "relative flex w-full justify-between rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/6%)] focus-within:border-ring focus-within:ring-[3px] has-aria-invalid:border-destructive/36 focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/48 data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-within,[aria-invalid]]:shadow-none",
45
+ className,
46
+ )}
47
+ data-slot="number-field-group"
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ function NumberFieldDecrement({
54
+ className,
55
+ ...props
56
+ }: NumberFieldPrimitive.Decrement.Props) {
57
+ return (
58
+ <NumberFieldPrimitive.Decrement
59
+ className={cn(
60
+ "relative flex shrink-0 cursor-pointer items-center justify-center rounded-s-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent",
61
+ className,
62
+ )}
63
+ data-slot="number-field-decrement"
64
+ {...props}
65
+ >
66
+ <MinusIcon />
67
+ </NumberFieldPrimitive.Decrement>
68
+ );
69
+ }
70
+
71
+ function NumberFieldIncrement({
72
+ className,
73
+ ...props
74
+ }: NumberFieldPrimitive.Increment.Props) {
75
+ return (
76
+ <NumberFieldPrimitive.Increment
77
+ className={cn(
78
+ "relative flex shrink-0 cursor-pointer items-center justify-center rounded-e-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent",
79
+ className,
80
+ )}
81
+ data-slot="number-field-increment"
82
+ {...props}
83
+ >
84
+ <PlusIcon />
85
+ </NumberFieldPrimitive.Increment>
86
+ );
87
+ }
88
+
89
+ function NumberFieldInput({
90
+ className,
91
+ ...props
92
+ }: NumberFieldPrimitive.Input.Props) {
93
+ return (
94
+ <NumberFieldPrimitive.Input
95
+ className={cn(
96
+ "h-8.5 in-data-[size=lg]:h-9.5 in-data-[size=sm]:h-7.5 w-full min-w-0 grow bg-transparent in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] text-center tabular-nums in-data-[size=lg]:leading-9.5 in-data-[size=sm]:leading-7.5 leading-8.5 outline-none sm:h-7.5 sm:in-data-[size=lg]:h-8.5 sm:in-data-[size=sm]:h-6.5 sm:in-data-[size=lg]:leading-8.5 sm:in-data-[size=sm]:leading-8.5 sm:leading-7.5",
97
+ className,
98
+ )}
99
+ data-slot="number-field-input"
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ function NumberFieldScrubArea({
106
+ className,
107
+ label,
108
+ ...props
109
+ }: NumberFieldPrimitive.ScrubArea.Props & {
110
+ label: string;
111
+ }) {
112
+ const context = React.useContext(NumberFieldContext);
113
+
114
+ if (!context) {
115
+ throw new Error(
116
+ "NumberFieldScrubArea must be used within a NumberField component for accessibility.",
117
+ );
118
+ }
119
+
120
+ return (
121
+ <NumberFieldPrimitive.ScrubArea
122
+ className={cn("flex cursor-ew-resize", className)}
123
+ data-slot="number-field-scrub-area"
124
+ {...props}
125
+ >
126
+ <Label className="cursor-ew-resize" htmlFor={context.fieldId}>
127
+ {label}
128
+ </Label>
129
+ <NumberFieldPrimitive.ScrubAreaCursor className="drop-shadow-[0_1px_1px_#0008] filter">
130
+ <CursorGrowIcon />
131
+ </NumberFieldPrimitive.ScrubAreaCursor>
132
+ </NumberFieldPrimitive.ScrubArea>
133
+ );
134
+ }
135
+
136
+ function CursorGrowIcon(props: React.ComponentProps<"svg">) {
137
+ return (
138
+ // biome-ignore lint/a11y/noSvgWithoutTitle: Imported from library
139
+ <svg
140
+ fill="black"
141
+ height="14"
142
+ stroke="white"
143
+ viewBox="0 0 24 14"
144
+ width="26"
145
+ xmlns="http://www.w3.org/2000/svg"
146
+ {...props}
147
+ >
148
+ <path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" />
149
+ </svg>
150
+ );
151
+ }
152
+
153
+ export {
154
+ NumberField,
155
+ NumberFieldScrubArea,
156
+ NumberFieldDecrement,
157
+ NumberFieldIncrement,
158
+ NumberFieldGroup,
159
+ NumberFieldInput,
160
+ };