@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.
- package/README.md +31 -42
- package/THIRD_PARTY_NOTICES.md +253 -0
- package/package.json +13 -13
- package/src/chat/index.ts +9 -0
- package/src/chat/message.tsx +80 -0
- package/src/chat/prompt-input.tsx +121 -0
- package/src/chat/reasoning.tsx +182 -0
- package/src/chat/scroller.tsx +147 -0
- package/src/chat/suggestion.tsx +65 -0
- package/src/chat/tool.tsx +143 -0
- package/src/chat/types.ts +8 -0
- package/src/components/alert-shell.tsx +66 -0
- package/src/components/avatar.tsx +1 -1
- package/src/components/badge.tsx +6 -6
- package/src/components/breadcrumb.tsx +8 -10
- package/src/components/button.tsx +1 -1
- package/src/components/card.tsx +4 -21
- package/src/components/checkbox.tsx +3 -5
- package/src/components/color-field.tsx +20 -7
- package/src/components/color-swatch.tsx +3 -3
- package/src/components/confirm-dialog.tsx +93 -0
- package/src/components/context-menu.tsx +16 -13
- package/src/components/dialog.tsx +8 -9
- package/src/components/download-prompt.tsx +108 -0
- package/src/components/dropdown-menu.tsx +16 -13
- package/src/components/empty-state.tsx +6 -3
- package/src/components/input.tsx +2 -2
- package/src/components/label.tsx +3 -2
- package/src/components/list.tsx +205 -0
- package/src/components/navigation-menu.tsx +17 -10
- package/src/components/number-field.tsx +30 -26
- package/src/components/popover.tsx +6 -10
- package/src/components/progress.tsx +77 -0
- package/src/components/select.tsx +16 -28
- package/src/components/separator.tsx +2 -6
- package/src/components/sheet.tsx +16 -8
- package/src/components/side-panel.tsx +31 -8
- package/src/components/sidebar.tsx +19 -52
- package/src/components/skeleton.tsx +1 -1
- package/src/components/slider.tsx +4 -8
- package/src/components/snackbar.tsx +39 -36
- package/src/components/status-bar.tsx +10 -3
- package/src/components/tabs.tsx +5 -6
- package/src/components/textarea.tsx +2 -2
- package/src/components/toggle-group.tsx +4 -4
- package/src/components/toggle.tsx +3 -1
- package/src/components/toolbar.tsx +8 -11
- package/src/components/tooltip.tsx +1 -1
- package/src/graphics/adsr.ts +21 -13
- package/src/graphics/curve-editor.tsx +56 -14
- package/src/graphics/curve-edits.ts +20 -3
- package/src/graphics/index.ts +6 -6
- package/src/graphics/scale.ts +4 -1
- package/src/graphics/use-canvas.ts +1 -1
- package/src/graphics/use-tokens.ts +4 -1
- package/src/index.css +9 -1
- package/src/index.ts +7 -5
- package/src/lib/use-mobile.ts +1 -1
- package/src/lib/utils.ts +1 -1
- 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
|
-
|
|
236
|
-
DropdownMenuTrigger,
|
|
238
|
+
DropdownMenuCheckboxItem,
|
|
237
239
|
DropdownMenuContent,
|
|
238
240
|
DropdownMenuGroup,
|
|
239
|
-
DropdownMenuLabel,
|
|
240
241
|
DropdownMenuItem,
|
|
241
|
-
|
|
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(
|
|
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 }
|
package/src/components/input.tsx
CHANGED
|
@@ -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
|
/>
|
package/src/components/label.tsx
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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" &&
|
|
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?.(
|
|
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(
|
|
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 }
|