@preset.nz/ux-kit 0.1.0 → 0.1.2

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 (60) hide show
  1. package/README.md +31 -42
  2. package/THIRD_PARTY_NOTICES.md +253 -0
  3. package/package.json +13 -13
  4. package/src/chat/index.ts +9 -0
  5. package/src/chat/message.tsx +80 -0
  6. package/src/chat/prompt-input.tsx +121 -0
  7. package/src/chat/reasoning.tsx +182 -0
  8. package/src/chat/scroller.tsx +147 -0
  9. package/src/chat/suggestion.tsx +65 -0
  10. package/src/chat/tool.tsx +143 -0
  11. package/src/chat/types.ts +8 -0
  12. package/src/components/alert-shell.tsx +66 -0
  13. package/src/components/avatar.tsx +1 -1
  14. package/src/components/badge.tsx +6 -6
  15. package/src/components/breadcrumb.tsx +8 -10
  16. package/src/components/button.tsx +1 -1
  17. package/src/components/card.tsx +4 -21
  18. package/src/components/checkbox.tsx +3 -5
  19. package/src/components/color-field.tsx +20 -7
  20. package/src/components/color-swatch.tsx +3 -3
  21. package/src/components/confirm-dialog.tsx +93 -0
  22. package/src/components/context-menu.tsx +16 -13
  23. package/src/components/dialog.tsx +8 -9
  24. package/src/components/download-prompt.tsx +108 -0
  25. package/src/components/dropdown-menu.tsx +16 -13
  26. package/src/components/empty-state.tsx +6 -3
  27. package/src/components/input.tsx +2 -2
  28. package/src/components/label.tsx +3 -2
  29. package/src/components/list.tsx +205 -0
  30. package/src/components/navigation-menu.tsx +17 -10
  31. package/src/components/number-field.tsx +30 -26
  32. package/src/components/popover.tsx +6 -10
  33. package/src/components/progress.tsx +77 -0
  34. package/src/components/select.tsx +16 -28
  35. package/src/components/separator.tsx +2 -6
  36. package/src/components/sheet.tsx +16 -8
  37. package/src/components/side-panel.tsx +31 -8
  38. package/src/components/sidebar.tsx +19 -52
  39. package/src/components/skeleton.tsx +1 -1
  40. package/src/components/slider.tsx +4 -8
  41. package/src/components/snackbar.tsx +39 -36
  42. package/src/components/status-bar.tsx +10 -3
  43. package/src/components/tabs.tsx +5 -6
  44. package/src/components/textarea.tsx +2 -2
  45. package/src/components/toggle-group.tsx +4 -4
  46. package/src/components/toggle.tsx +3 -1
  47. package/src/components/toolbar.tsx +8 -11
  48. package/src/components/tooltip.tsx +1 -1
  49. package/src/graphics/adsr.ts +21 -13
  50. package/src/graphics/curve-editor.tsx +56 -14
  51. package/src/graphics/curve-edits.ts +20 -3
  52. package/src/graphics/index.ts +6 -6
  53. package/src/graphics/scale.ts +4 -1
  54. package/src/graphics/use-canvas.ts +1 -1
  55. package/src/graphics/use-tokens.ts +4 -1
  56. package/src/index.css +9 -1
  57. package/src/index.ts +7 -5
  58. package/src/lib/use-mobile.ts +1 -1
  59. package/src/lib/utils.ts +1 -1
  60. package/src/shadcn-tailwind.css +665 -0
@@ -1,9 +1,12 @@
1
- import * as React from "react"
2
1
  import { Menu as MenuPrimitive } from "@base-ui/react/menu"
3
2
  import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react"
3
+ import type * as React from "react"
4
4
 
5
5
  import { cn } from "../lib/utils"
6
6
 
7
+ // Base UI: `DropdownMenuLabel` is Base UI's `GroupLabel`, so it must sit inside a `DropdownMenuGroup`.
8
+ // Same shape as ContextMenu.
9
+
7
10
  function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
8
11
  return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
9
12
  }
@@ -44,7 +47,7 @@ function DropdownMenuContent({
44
47
  data-slot="dropdown-menu-content"
45
48
  className={cn(
46
49
  "z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
47
- className
50
+ className,
48
51
  )}
49
52
  {...props}
50
53
  />
@@ -68,7 +71,7 @@ function DropdownMenuLabel({
68
71
  data-inset={inset}
69
72
  className={cn(
70
73
  "px-2 py-1.5 text-xs font-medium text-muted-foreground data-inset:pl-8",
71
- className
74
+ className,
72
75
  )}
73
76
  {...props}
74
77
  />
@@ -91,7 +94,7 @@ function DropdownMenuItem({
91
94
  data-variant={variant}
92
95
  className={cn(
93
96
  "group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
94
- className
97
+ className,
95
98
  )}
96
99
  {...props}
97
100
  />
@@ -114,7 +117,7 @@ function DropdownMenuSubTrigger({
114
117
  data-inset={inset}
115
118
  className={cn(
116
119
  "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
117
- className
120
+ className,
118
121
  )}
119
122
  {...props}
120
123
  >
@@ -157,7 +160,7 @@ function DropdownMenuCheckboxItem({
157
160
  data-inset={inset}
158
161
  className={cn(
159
162
  "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
160
- className
163
+ className,
161
164
  )}
162
165
  {...props}
163
166
  >
@@ -190,7 +193,7 @@ function DropdownMenuRadioItem({
190
193
  data-inset={inset}
191
194
  className={cn(
192
195
  "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
193
- className
196
+ className,
194
197
  )}
195
198
  {...props}
196
199
  >
@@ -223,7 +226,7 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"spa
223
226
  data-slot="dropdown-menu-shortcut"
224
227
  className={cn(
225
228
  "ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
226
- className
229
+ className,
227
230
  )}
228
231
  {...props}
229
232
  />
@@ -232,18 +235,18 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"spa
232
235
 
233
236
  export {
234
237
  DropdownMenu,
235
- DropdownMenuPortal,
236
- DropdownMenuTrigger,
238
+ DropdownMenuCheckboxItem,
237
239
  DropdownMenuContent,
238
240
  DropdownMenuGroup,
239
- DropdownMenuLabel,
240
241
  DropdownMenuItem,
241
- DropdownMenuCheckboxItem,
242
+ DropdownMenuLabel,
243
+ DropdownMenuPortal,
242
244
  DropdownMenuRadioGroup,
243
245
  DropdownMenuRadioItem,
244
246
  DropdownMenuSeparator,
245
247
  DropdownMenuShortcut,
246
248
  DropdownMenuSub,
247
- DropdownMenuSubTrigger,
248
249
  DropdownMenuSubContent,
250
+ DropdownMenuSubTrigger,
251
+ DropdownMenuTrigger,
249
252
  }
@@ -1,4 +1,4 @@
1
- import * as React from "react"
1
+ import type * as React from "react"
2
2
 
3
3
  import { cn } from "../lib/utils"
4
4
 
@@ -17,7 +17,10 @@ function EmptyState({ icon, title, description, action, className, ...props }: E
17
17
  return (
18
18
  <div
19
19
  data-slot="empty-state"
20
- className={cn("flex h-full flex-col items-center justify-center gap-2 p-8 text-center", className)}
20
+ className={cn(
21
+ "flex h-full flex-col items-center justify-center gap-2 p-8 text-center",
22
+ className,
23
+ )}
21
24
  {...props}
22
25
  >
23
26
  {icon ? <div className="text-muted-foreground [&_svg]:size-8">{icon}</div> : null}
@@ -28,5 +31,5 @@ function EmptyState({ icon, title, description, action, className, ...props }: E
28
31
  )
29
32
  }
30
33
 
31
- export { EmptyState }
32
34
  export type { EmptyStateProps }
35
+ export { EmptyState }
@@ -1,5 +1,5 @@
1
- import * as React from "react"
2
1
  import { Input as InputPrimitive } from "@base-ui/react/input"
2
+ import type * as React from "react"
3
3
 
4
4
  import { cn } from "../lib/utils"
5
5
 
@@ -10,7 +10,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
10
10
  data-slot="input"
11
11
  className={cn(
12
12
  "h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
13
- className
13
+ className,
14
14
  )}
15
15
  {...props}
16
16
  />
@@ -1,14 +1,15 @@
1
- import * as React from "react"
1
+ import type * as React from "react"
2
2
 
3
3
  import { cn } from "../lib/utils"
4
4
 
5
5
  function Label({ className, ...props }: React.ComponentProps<"label">) {
6
6
  return (
7
+ // biome-ignore lint/a11y/noLabelWithoutControl: a primitive: the consumer supplies htmlFor or wraps the control
7
8
  <label
8
9
  data-slot="label"
9
10
  className={cn(
10
11
  "flex items-center gap-2 text-xs leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
11
- className
12
+ className,
12
13
  )}
13
14
  {...props}
14
15
  />
@@ -0,0 +1,205 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../lib/utils"
4
+
5
+ // List and ListRow: a set of things (models, files, recent items) built from
6
+ // slots. Without `selection` it is a plain `role="list"` and the rows'
7
+ // `trailing` actions are the only interactive parts (Tab walks them). With
8
+ // `selection` it is a `role="listbox"` with roving tabindex: Up, Down, Home
9
+ // and End move focus, Enter or Space selects. Rows must be direct children of
10
+ // the List (the List reads their ids). A selectable list takes no `trailing`
11
+ // controls: a button inside an option is invalid ARIA, and its click would
12
+ // select the row too. No virtualisation, reorder or sections.
13
+ //
14
+ // `className` and other props go on the List's outer element; the accessible
15
+ // name (`aria-label`, `aria-labelledby`) goes on the element with the role.
16
+
17
+ interface ListSelection {
18
+ selectedId: string | null
19
+ onSelect: (id: string) => void
20
+ }
21
+
22
+ interface ListContextValue {
23
+ selection: ListSelection | undefined
24
+ tabStopId: string | undefined
25
+ setFocusId: (id: string) => void
26
+ }
27
+
28
+ const ListContext = React.createContext<ListContextValue>({
29
+ selection: undefined,
30
+ tabStopId: undefined,
31
+ setFocusId: () => {},
32
+ })
33
+
34
+ interface ListProps extends Omit<React.ComponentProps<"div">, "role" | "children"> {
35
+ /** Makes the list a selectable listbox. Leave undefined for a plain list. */
36
+ selection?: ListSelection
37
+ /** Shown in place of the rows when there are no children. Usually an EmptyState. */
38
+ empty?: React.ReactNode
39
+ /** After the rows. */
40
+ footer?: React.ReactNode
41
+ children?: React.ReactNode
42
+ }
43
+
44
+ function List({
45
+ selection,
46
+ empty,
47
+ footer,
48
+ className,
49
+ children,
50
+ onKeyDown,
51
+ "aria-label": ariaLabel,
52
+ "aria-labelledby": ariaLabelledBy,
53
+ ...props
54
+ }: ListProps) {
55
+ const rows = React.Children.toArray(children)
56
+ const ids = rows.flatMap((row) =>
57
+ React.isValidElement<{ id?: string }>(row) && typeof row.props.id === "string"
58
+ ? [row.props.id]
59
+ : [],
60
+ )
61
+ const [focusId, setFocusId] = React.useState<string | undefined>(undefined)
62
+ const rowsRef = React.useRef<HTMLDivElement>(null)
63
+
64
+ // The one tab stop: the last focused row, else the selected one, else the first.
65
+ const tabStopId =
66
+ [focusId, selection?.selectedId ?? undefined].find(
67
+ (id): id is string => id !== undefined && ids.includes(id),
68
+ ) ?? ids[0]
69
+
70
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
71
+ onKeyDown?.(event)
72
+ if (!selection || event.defaultPrevented) return
73
+ const options = Array.from(
74
+ rowsRef.current?.querySelectorAll<HTMLElement>('[role="option"]') ?? [],
75
+ )
76
+ const current = options.indexOf(event.target as HTMLElement)
77
+ if (current === -1) return
78
+ let next: number
79
+ switch (event.key) {
80
+ case "ArrowDown":
81
+ next = Math.min(current + 1, options.length - 1)
82
+ break
83
+ case "ArrowUp":
84
+ next = Math.max(current - 1, 0)
85
+ break
86
+ case "Home":
87
+ next = 0
88
+ break
89
+ case "End":
90
+ next = options.length - 1
91
+ break
92
+ default:
93
+ return
94
+ }
95
+ event.preventDefault()
96
+ options[next]?.focus()
97
+ }
98
+
99
+ const context = React.useMemo<ListContextValue>(
100
+ () => ({ selection, tabStopId, setFocusId }),
101
+ [selection, tabStopId],
102
+ )
103
+
104
+ return (
105
+ <div data-slot="list" className={cn("flex flex-col", className)} {...props}>
106
+ {rows.length === 0 ? (
107
+ empty
108
+ ) : (
109
+ <ListContext.Provider value={context}>
110
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: arrow keys on the listbox are the roving-tabindex pattern, the options hold the focus */}
111
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: the role is always list or listbox, and both take a name; Biome can't read the ternary */}
112
+ <div
113
+ ref={rowsRef}
114
+ role={selection ? "listbox" : "list"}
115
+ aria-label={ariaLabel}
116
+ aria-labelledby={ariaLabelledBy}
117
+ data-slot="list-rows"
118
+ className="flex flex-col divide-y divide-border"
119
+ onKeyDown={handleKeyDown}
120
+ >
121
+ {rows}
122
+ </div>
123
+ </ListContext.Provider>
124
+ )}
125
+ {footer && rows.length > 0 ? (
126
+ <div
127
+ data-slot="list-footer"
128
+ className="border-t border-border px-3 py-2 text-sm text-muted-foreground"
129
+ >
130
+ {footer}
131
+ </div>
132
+ ) : null}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ interface ListRowProps
138
+ extends Omit<
139
+ React.ComponentProps<"div">,
140
+ "id" | "title" | "role" | "children" | "onClick" | "onSelect"
141
+ > {
142
+ /** Identifies the row to `selection`. Not set as the DOM id. */
143
+ id: string
144
+ /** Before the text: an icon or thumbnail. */
145
+ leading?: React.ReactNode
146
+ title: React.ReactNode
147
+ /** One muted line under the title. */
148
+ detail?: React.ReactNode
149
+ /** After the text: a Button, a Badge. Clickable things go here, not on the row, and only in a plain list. */
150
+ trailing?: React.ReactNode
151
+ }
152
+
153
+ function ListRow({ id, leading, title, detail, trailing, className, ...props }: ListRowProps) {
154
+ const { selection, tabStopId, setFocusId } = React.useContext(ListContext)
155
+ const selectable = selection !== undefined
156
+ const selected = selectable && selection.selectedId === id
157
+
158
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
159
+ props.onKeyDown?.(event)
160
+ // Keys from a trailing control stay with that control.
161
+ if (!selectable || event.defaultPrevented || event.target !== event.currentTarget) return
162
+ if (event.key === "Enter" || event.key === " ") {
163
+ event.preventDefault()
164
+ selection.onSelect(id)
165
+ }
166
+ }
167
+
168
+ return (
169
+ // biome-ignore lint/a11y/noStaticElementInteractions: the role is option when selectable; a plain row has no handlers that act
170
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-selected is set only when the role is option
171
+ <div
172
+ data-slot="list-row"
173
+ data-row-id={id}
174
+ data-selected={selected ? "" : undefined}
175
+ role={selectable ? "option" : "listitem"}
176
+ aria-selected={selectable ? selected : undefined}
177
+ tabIndex={selectable ? (tabStopId === id ? 0 : -1) : undefined}
178
+ className={cn(
179
+ "flex min-h-11 items-center gap-3 px-3 py-1.5 outline-none",
180
+ selectable &&
181
+ "cursor-default hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 data-selected:bg-accent",
182
+ className,
183
+ )}
184
+ {...props}
185
+ onKeyDown={handleKeyDown}
186
+ onFocus={(event) => {
187
+ props.onFocus?.(event)
188
+ if (selectable && event.target === event.currentTarget) setFocusId(id)
189
+ }}
190
+ onClick={selectable ? () => selection.onSelect(id) : undefined}
191
+ >
192
+ {leading ? (
193
+ <div className="shrink-0 text-muted-foreground [&_svg]:size-5">{leading}</div>
194
+ ) : null}
195
+ <div className="min-w-0 flex-1">
196
+ <p className="truncate text-sm font-medium">{title}</p>
197
+ {detail ? <p className="truncate text-xs text-muted-foreground">{detail}</p> : null}
198
+ </div>
199
+ {trailing ? <div className="flex shrink-0 items-center gap-2">{trailing}</div> : null}
200
+ </div>
201
+ )
202
+ }
203
+
204
+ export type { ListProps, ListRowProps, ListSelection }
205
+ export { List, ListRow }
@@ -1,12 +1,13 @@
1
1
  import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
2
- import { cva } from "class-variance-authority"
3
2
  import { CaretDownIcon } from "@phosphor-icons/react"
3
+ import { cva } from "class-variance-authority"
4
4
 
5
5
  import { cn } from "../lib/utils"
6
6
 
7
7
  // Base UI renders content through a shared Portal > Positioner > Popup >
8
8
  // Viewport. NavigationMenu bundles that, so `NavigationMenuContent` items
9
9
  // morph inside one popup like the shadcn viewport variant.
10
+ // Always the shared-viewport form: the Radix indicator and viewport={false} mode are gone.
10
11
  function NavigationMenu({
11
12
  className,
12
13
  children,
@@ -16,7 +17,10 @@ function NavigationMenu({
16
17
  return (
17
18
  <NavigationMenuPrimitive.Root
18
19
  data-slot="navigation-menu"
19
- className={cn("group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", className)}
20
+ className={cn(
21
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
22
+ className,
23
+ )}
20
24
  {...props}
21
25
  >
22
26
  {children}
@@ -46,7 +50,7 @@ function NavigationMenuItem({ className, ...props }: NavigationMenuPrimitive.Ite
46
50
  }
47
51
 
48
52
  const navigationMenuTriggerStyle = cva(
49
- "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium outline-none transition-[color,box-shadow] hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-accent/50 data-popup-open:text-accent-foreground data-popup-open:hover:bg-accent data-popup-open:focus:bg-accent"
53
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium outline-none transition-[color,box-shadow] hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-accent/50 data-popup-open:text-accent-foreground data-popup-open:hover:bg-accent data-popup-open:focus:bg-accent",
50
54
  )
51
55
 
52
56
  function NavigationMenuTrigger({
@@ -79,7 +83,7 @@ function NavigationMenuContent({ className, ...props }: NavigationMenuPrimitive.
79
83
  data-slot="navigation-menu-content"
80
84
  className={cn(
81
85
  "w-full p-2 pr-2.5 transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 md:w-auto **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
82
- className
86
+ className,
83
87
  )}
84
88
  {...props}
85
89
  />
@@ -103,7 +107,7 @@ function NavigationMenuPositioner({
103
107
  alignOffset={alignOffset}
104
108
  className={cn(
105
109
  "isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-200",
106
- className
110
+ className,
107
111
  )}
108
112
  {...props}
109
113
  >
@@ -111,7 +115,10 @@ function NavigationMenuPositioner({
111
115
  data-slot="navigation-menu-popup"
112
116
  className="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) overflow-hidden rounded-md border bg-popover text-popover-foreground shadow transition-[width,height,opacity,scale] duration-200 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
113
117
  >
114
- <NavigationMenuPrimitive.Viewport data-slot="navigation-menu-viewport" className="relative size-full overflow-hidden" />
118
+ <NavigationMenuPrimitive.Viewport
119
+ data-slot="navigation-menu-viewport"
120
+ className="relative size-full overflow-hidden"
121
+ />
115
122
  </NavigationMenuPrimitive.Popup>
116
123
  </NavigationMenuPrimitive.Positioner>
117
124
  </NavigationMenuPrimitive.Portal>
@@ -125,7 +132,7 @@ function NavigationMenuLink({ className, ...props }: NavigationMenuPrimitive.Lin
125
132
  data-slot="navigation-menu-link"
126
133
  className={cn(
127
134
  "flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 data-active:bg-accent/50 data-active:text-accent-foreground data-active:hover:bg-accent data-active:focus:bg-accent [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
128
- className
135
+ className,
129
136
  )}
130
137
  {...props}
131
138
  />
@@ -134,11 +141,11 @@ function NavigationMenuLink({ className, ...props }: NavigationMenuPrimitive.Lin
134
141
 
135
142
  export {
136
143
  NavigationMenu,
137
- NavigationMenuList,
138
- NavigationMenuItem,
139
144
  NavigationMenuContent,
140
- NavigationMenuTrigger,
145
+ NavigationMenuItem,
141
146
  NavigationMenuLink,
147
+ NavigationMenuList,
142
148
  NavigationMenuPositioner,
149
+ NavigationMenuTrigger,
143
150
  navigationMenuTriggerStyle,
144
151
  }
@@ -1,8 +1,7 @@
1
- import * as React from "react"
2
1
  import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field"
3
-
4
- import { cn } from "../lib/utils"
2
+ import * as React from "react"
5
3
  import { ArrowsHorizontalIcon } from "../icons"
4
+ import { cn } from "../lib/utils"
6
5
 
7
6
  // NumberField: a number you can drag. The label is the scrub handle (Base UI's ScrubArea);
8
7
  // click it, or tab in, to type. Behaviour is Oblique's InputNumber on Base UI's NumberField:
@@ -29,10 +28,7 @@ interface NumberChange {
29
28
  }
30
29
 
31
30
  interface NumberFieldProps
32
- extends Omit<
33
- React.ComponentProps<"div">,
34
- "onChange" | "defaultValue" | "children" | "value"
35
- > {
31
+ extends Omit<React.ComponentProps<"div">, "onChange" | "defaultValue" | "children" | "value"> {
36
32
  value: number
37
33
  onValueChange?: (value: number, details: NumberChange) => void
38
34
  min?: number
@@ -112,7 +108,7 @@ function NumberField({
112
108
  const digits = integer ? 0 : (precision ?? placesOf(small))
113
109
  const options = React.useMemo<Intl.NumberFormatOptions>(
114
110
  () => ({ maximumFractionDigits: digits, useGrouping: false, ...format }),
115
- [digits, format]
111
+ [digits, format],
116
112
  )
117
113
 
118
114
  // The field's own value, so a scrub follows the pointer without waiting for the app's round
@@ -165,6 +161,7 @@ function NumberField({
165
161
  })
166
162
  }
167
163
 
164
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the effect follows `value` alone; `show` and `clearSettle` are per-render closures over refs and state setters
168
165
  React.useEffect(() => {
169
166
  valueRef.current = value
170
167
  if (scrub.current.started) return
@@ -174,15 +171,17 @@ function NumberField({
174
171
  show(value)
175
172
  }, [value])
176
173
 
174
+ // biome-ignore lint/correctness/useExhaustiveDependencies: unmount cleanup only; `clearSettle` is a per-render closure over refs
177
175
  React.useEffect(
178
176
  () => () => {
179
177
  clearSettle()
180
178
  cancelAnimationFrame(pending.current.raf)
181
179
  },
182
- []
180
+ [],
183
181
  )
184
182
 
185
183
  // Escape while scrubbing. Captured so the app's own Escape (clear selection) doesn't also run.
184
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the listener lives while a scrub does; `show` and `dropPending` are per-render closures over refs
186
185
  React.useEffect(() => {
187
186
  if (!scrubbing || !onScrubCancel) return
188
187
  const onKey = (e: KeyboardEvent) => {
@@ -297,7 +296,10 @@ function NumberField({
297
296
  />
298
297
  )
299
298
  const unit = suffix != null && (
300
- <span data-slot="number-field-suffix" className="flex shrink-0 items-center pr-2 text-muted-foreground select-none">
299
+ <span
300
+ data-slot="number-field-suffix"
301
+ className="flex shrink-0 items-center pr-2 text-muted-foreground select-none"
302
+ >
301
303
  {suffix}
302
304
  </span>
303
305
  )
@@ -322,9 +324,10 @@ function NumberField({
322
324
  className={cn(
323
325
  "min-w-0",
324
326
  !outside && `group/number-field relative ${boxClass}`,
325
- labelPlacement === "column" && "col-span-2 grid grid-cols-subgrid items-center data-disabled:opacity-50",
327
+ labelPlacement === "column" &&
328
+ "col-span-2 grid grid-cols-subgrid items-center data-disabled:opacity-50",
326
329
  labelPlacement === "above" && "flex flex-col gap-1 data-disabled:opacity-50",
327
- className
330
+ className,
328
331
  )}
329
332
  {...props}
330
333
  >
@@ -340,8 +343,8 @@ function NumberField({
340
343
  ? "cursor-default"
341
344
  : cn(
342
345
  "cursor-ew-resize hover:text-foreground data-scrubbing:text-foreground",
343
- !outside && "hover:bg-muted data-scrubbing:bg-selection/20"
344
- )
346
+ !outside && "hover:bg-muted data-scrubbing:bg-selection/20",
347
+ ),
345
348
  )}
346
349
  >
347
350
  {label}
@@ -367,10 +370,7 @@ function NumberField({
367
370
  const AXES = ["X", "Y", "Z", "W"]
368
371
 
369
372
  interface VectorFieldProps
370
- extends Omit<
371
- NumberFieldProps,
372
- "value" | "onValueChange" | "label" | "className"
373
- > {
373
+ extends Omit<NumberFieldProps, "value" | "onValueChange" | "label" | "className"> {
374
374
  value: number[]
375
375
  /** The whole vector with the changed component replaced. */
376
376
  onValueChange?: (value: number[], details: NumberChange) => void
@@ -393,20 +393,20 @@ function VectorField({
393
393
  return (
394
394
  <div
395
395
  data-slot="vector-field"
396
- className={cn(
397
- "flex min-w-0 gap-1",
398
- orientation === "column" && "flex-col",
399
- className
400
- )}
396
+ className={cn("flex min-w-0 gap-1", orientation === "column" && "flex-col", className)}
401
397
  >
402
398
  {value.map((x, i) => (
403
399
  <NumberField
400
+ // biome-ignore lint/suspicious/noArrayIndexKey: one field per axis; the index is the axis
404
401
  key={i}
405
402
  {...props}
406
403
  label={labels?.[i] ?? (value.length <= AXES.length ? AXES[i] : String(i + 1))}
407
404
  value={x}
408
405
  onValueChange={(n, d) =>
409
- onValueChange?.(value.map((v, j) => (j === i ? n : v)), d)
406
+ onValueChange?.(
407
+ value.map((v, j) => (j === i ? n : v)),
408
+ d,
409
+ )
410
410
  }
411
411
  />
412
412
  ))}
@@ -414,15 +414,19 @@ function VectorField({
414
414
  )
415
415
  }
416
416
 
417
- export { NumberField, VectorField }
418
417
  export type { NumberChange, NumberFieldProps, VectorFieldProps }
418
+ export { NumberField, VectorField }
419
419
 
420
420
  /**
421
421
  * Pixels of drag per step, so a full-range scrub is about 300px whatever the step: a 0–8 field
422
422
  * with 0.25 steps moves one step per ~9px instead of per pixel, which read as jumping.
423
423
  * Unbounded ranges keep one step per pixel.
424
424
  */
425
- function pixelsPerStep(min: number | undefined, max: number | undefined, step: number | undefined): number {
425
+ function pixelsPerStep(
426
+ min: number | undefined,
427
+ max: number | undefined,
428
+ step: number | undefined,
429
+ ): number {
426
430
  if (min == null || max == null || !step || max <= min) return 1
427
431
  return Math.min(12, Math.max(1, Math.round((300 * step) / (max - min))))
428
432
  }
@@ -1,8 +1,11 @@
1
- import * as React from "react"
2
1
  import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
2
+ import type * as React from "react"
3
3
 
4
4
  import { cn } from "../lib/utils"
5
5
 
6
+ // Popover: Base UI Popover. There is no `PopoverAnchor`: Base UI anchors through the
7
+ // Positioner's `anchor` prop.
8
+
6
9
  function Popover({ ...props }: PopoverPrimitive.Root.Props) {
7
10
  return <PopoverPrimitive.Root data-slot="popover" {...props} />
8
11
  }
@@ -33,7 +36,7 @@ function PopoverContent({
33
36
  data-slot="popover-content"
34
37
  className={cn(
35
38
  "z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-md bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
36
- className
39
+ className,
37
40
  )}
38
41
  {...props}
39
42
  />
@@ -72,11 +75,4 @@ function PopoverDescription({ className, ...props }: PopoverPrimitive.Descriptio
72
75
  )
73
76
  }
74
77
 
75
- export {
76
- Popover,
77
- PopoverContent,
78
- PopoverDescription,
79
- PopoverHeader,
80
- PopoverTitle,
81
- PopoverTrigger,
82
- }
78
+ export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }