@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,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
 
@@ -11,7 +11,10 @@ function StatusBar({ className, ...props }: React.ComponentProps<"div">) {
11
11
  <div
12
12
  data-slot="status-bar"
13
13
  role="status"
14
- className={cn("flex h-6 shrink-0 items-center gap-3 border-t border-border bg-background px-3 text-xs text-muted-foreground", className)}
14
+ className={cn(
15
+ "flex h-6 shrink-0 items-center gap-3 border-t border-border bg-background px-3 text-xs text-muted-foreground",
16
+ className,
17
+ )}
15
18
  {...props}
16
19
  />
17
20
  )
@@ -24,7 +27,11 @@ interface StatusItemProps extends React.ComponentProps<"span"> {
24
27
 
25
28
  function StatusItem({ label, className, children, ...props }: StatusItemProps) {
26
29
  return (
27
- <span data-slot="status-item" className={cn("flex items-center gap-1 tabular-nums", className)} {...props}>
30
+ <span
31
+ data-slot="status-item"
32
+ className={cn("flex items-center gap-1 tabular-nums", className)}
33
+ {...props}
34
+ >
28
35
  {label ? <span className="opacity-70">{label}</span> : null}
29
36
  <span className="text-foreground">{children}</span>
30
37
  </span>
@@ -8,10 +8,7 @@ function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive
8
8
  <TabsPrimitive.Root
9
9
  data-slot="tabs"
10
10
  orientation={orientation}
11
- className={cn(
12
- "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
13
- className
14
- )}
11
+ className={cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className)}
15
12
  {...props}
16
13
  />
17
14
  )
@@ -29,7 +26,7 @@ const tabsListVariants = cva(
29
26
  defaultVariants: {
30
27
  variant: "default",
31
28
  },
32
- }
29
+ },
33
30
  )
34
31
 
35
32
  function TabsList({
@@ -47,6 +44,8 @@ function TabsList({
47
44
  )
48
45
  }
49
46
 
47
+ // Orientation styles key on `data-[orientation=...]`.
48
+
50
49
  // Base UI calls the trigger `Tab` and the content `Panel`; the kit keeps the
51
50
  // shadcn names (TabsTrigger, TabsContent) so apps read the same.
52
51
  function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
@@ -58,7 +57,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
58
57
  "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
59
58
  "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
60
59
  "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
61
- className
60
+ className,
62
61
  )}
63
62
  {...props}
64
63
  />
@@ -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
 
@@ -8,7 +8,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
8
8
  data-slot="textarea"
9
9
  className={cn(
10
10
  "flex min-h-20 w-full rounded-none border border-input bg-transparent px-2.5 py-2 text-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
- className
11
+ className,
12
12
  )}
13
13
  {...props}
14
14
  />
@@ -1,7 +1,7 @@
1
- import * as React from "react"
2
1
  import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
3
2
  import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
4
- import { type VariantProps } from "class-variance-authority"
3
+ import type { VariantProps } from "class-variance-authority"
4
+ import * as React from "react"
5
5
 
6
6
  import { cn } from "../lib/utils"
7
7
  import { toggleVariants } from "./toggle"
@@ -42,7 +42,7 @@ function ToggleGroup({
42
42
  style={{ "--gap": spacing } as React.CSSProperties}
43
43
  className={cn(
44
44
  "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
45
- className
45
+ className,
46
46
  )}
47
47
  {...props}
48
48
  >
@@ -74,7 +74,7 @@ function ToggleGroupItem({
74
74
  variant: context.variant || variant,
75
75
  size: context.size || size,
76
76
  }),
77
- className
77
+ className,
78
78
  )}
79
79
  {...props}
80
80
  >
@@ -3,6 +3,8 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
 
4
4
  import { cn } from "../lib/utils"
5
5
 
6
+ // Toggle: the pressed state is `data-pressed`, not Radix's `data-[state=on]`. Style with that.
7
+
6
8
  const toggleVariants = cva(
7
9
  "group/toggle inline-flex items-center justify-center gap-1 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
10
  {
@@ -22,7 +24,7 @@ const toggleVariants = cva(
22
24
  variant: "default",
23
25
  size: "default",
24
26
  },
25
- }
27
+ },
26
28
  )
27
29
 
28
30
  function Toggle({
@@ -1,5 +1,5 @@
1
- import * as React from "react"
2
1
  import { Toolbar as ToolbarPrimitive } from "@base-ui/react/toolbar"
2
+ import * as React from "react"
3
3
 
4
4
  import { cn } from "../lib/utils"
5
5
  import { buttonVariants } from "./button"
@@ -52,16 +52,13 @@ interface ToolbarItemSpec {
52
52
  menu?: ToolbarMenuEntry[]
53
53
  }
54
54
 
55
- function Toolbar({
56
- className,
57
- ...props
58
- }: React.ComponentProps<typeof ToolbarPrimitive.Root>) {
55
+ function Toolbar({ className, ...props }: React.ComponentProps<typeof ToolbarPrimitive.Root>) {
59
56
  return (
60
57
  <ToolbarPrimitive.Root
61
58
  data-slot="toolbar"
62
59
  className={cn(
63
60
  "flex items-center gap-1 border-b border-border bg-background px-2 py-1 data-[orientation=vertical]:w-fit data-[orientation=vertical]:flex-col data-[orientation=vertical]:border-r data-[orientation=vertical]:border-b-0",
64
- className
61
+ className,
65
62
  )}
66
63
  {...props}
67
64
  />
@@ -90,7 +87,7 @@ function ToolbarSeparator({
90
87
  data-slot="toolbar-separator"
91
88
  className={cn(
92
89
  "mx-1 h-5 w-px shrink-0 bg-border data-[orientation=horizontal]:mx-0 data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-5",
93
- className
90
+ className,
94
91
  )}
95
92
  {...props}
96
93
  />
@@ -125,7 +122,7 @@ const buttonClass = (pressed?: boolean) =>
125
122
  cn(
126
123
  buttonVariants({ variant: "ghost", size: "icon" }),
127
124
  "data-disabled:cursor-default data-disabled:opacity-40 data-disabled:hover:bg-transparent",
128
- pressed && "bg-muted text-foreground"
125
+ pressed && "bg-muted text-foreground",
129
126
  )
130
127
 
131
128
  interface ToolbarButtonProps
@@ -242,8 +239,7 @@ function ToolbarItem({
242
239
  return <ToolbarButton {...item} onClick={() => onCommand?.(item.id)} />
243
240
  }
244
241
 
245
- interface ToolbarItemsProps
246
- extends Omit<React.ComponentProps<typeof Toolbar>, "children"> {
242
+ interface ToolbarItemsProps extends Omit<React.ComponentProps<typeof Toolbar>, "children"> {
247
243
  /** Groups of items; a separator is drawn between groups. */
248
244
  groups: ToolbarItemSpec[][]
249
245
  /** Called with the item's (or menu entry's) id. The app runs the same command the menu does. */
@@ -270,6 +266,7 @@ function ToolbarItems({
270
266
  return (
271
267
  <Toolbar {...props}>
272
268
  {all.map((group, i) => (
269
+ // biome-ignore lint/suspicious/noArrayIndexKey: groups are anonymous lists rebuilt each render and never reordered
273
270
  <React.Fragment key={i}>
274
271
  {i > 0 && <ToolbarSeparator />}
275
272
  <ToolbarGroup>
@@ -294,6 +291,7 @@ function ToolbarItems({
294
291
  )
295
292
  }
296
293
 
294
+ export type { ToolbarItemSpec, ToolbarMenuEntry }
297
295
  export {
298
296
  Toolbar,
299
297
  ToolbarButton,
@@ -304,4 +302,3 @@ export {
304
302
  ToolbarSeparator,
305
303
  ToolbarSpacer,
306
304
  }
307
- export type { ToolbarItemSpec, ToolbarMenuEntry }
@@ -39,7 +39,7 @@ function TooltipContent({
39
39
  data-slot="tooltip-content"
40
40
  className={cn(
41
41
  "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm 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",
42
- className
42
+ className,
43
43
  )}
44
44
  {...props}
45
45
  >
@@ -1,4 +1,4 @@
1
- import { clamp, type Curve } from "@preset.nz/math"
1
+ import { type Curve, clamp } from "@preset.nz/math"
2
2
 
3
3
  import type { CurveConstraint } from "./curve-edits"
4
4
 
@@ -47,24 +47,32 @@ export function adsrOf(curve: Curve): Adsr | null {
47
47
  export function constrainAdsr(maxX = Infinity): CurveConstraint {
48
48
  return (next, prev) => {
49
49
  if (next.points.length !== 4 || prev.points.length !== 4 || next.sustain !== 2) return null
50
- const was = durations(prev)
51
- const now = durations(next)
50
+ const [n0, n1, n2, n3] = next.points
51
+ const [p0, p1, p2, p3] = prev.points
52
+ if (!(n0 && n1 && n2 && n3 && p0 && p1 && p2 && p3)) return null
53
+ const [wa, wd, wr] = durations(p0, p1, p2, p3)
54
+ const [na, nd, nr] = durations(n0, n1, n2, n3)
52
55
  // The stage whose end moved takes the new length; the others keep theirs.
53
- const moved = [1, 2, 3].find((i) => next.points[i]!.x !== prev.points[i]!.x)
54
- const len = was.map((d, k) => (moved === k + 1 ? Math.max(0, now[k]!) : d))
55
- const over = len[0]! + len[1]! + len[2]! - maxX
56
- if (over > 0 && moved !== undefined) len[moved - 1] = Math.max(0, len[moved - 1]! - over)
57
- const ys = [0, 1, clamp(next.points[2]!.y, 0, 1), 0]
56
+ const moved = [n1.x !== p1.x, n2.x !== p2.x, n3.x !== p3.x].indexOf(true) + 1
57
+ const len = [
58
+ moved === 1 ? Math.max(0, na) : wa,
59
+ moved === 2 ? Math.max(0, nd) : wd,
60
+ moved === 3 ? Math.max(0, nr) : wr,
61
+ ] as [number, number, number]
62
+ const over = len[0] + len[1] + len[2] - maxX
63
+ if (over > 0 && moved > 0) len[moved - 1] = Math.max(0, (len[moved - 1] ?? 0) - over)
64
+ const ys = [0, 1, clamp(n2.y, 0, 1), 0]
58
65
  let x = 0
59
66
  const points = next.points.map((p, i) => {
60
- if (i > 0) x += len[i - 1]!
61
- return { ...p, x, y: ys[i]! }
67
+ if (i > 0) x += len[i - 1] ?? 0
68
+ return { ...p, x, y: ys[i] ?? p.y }
62
69
  })
63
70
  return { points, sustain: 2 }
64
71
  }
65
72
  }
66
73
 
67
- function durations(curve: Curve): number[] {
68
- const p = curve.points
69
- return [p[1]!.x - p[0]!.x, p[2]!.x - p[1]!.x, p[3]!.x - p[2]!.x]
74
+ type Point = Curve["points"][number]
75
+
76
+ function durations(p0: Point, p1: Point, p2: Point, p3: Point): [number, number, number] {
77
+ return [p1.x - p0.x, p2.x - p1.x, p3.x - p2.x]
70
78
  }
@@ -1,5 +1,5 @@
1
+ import { type Basis, type Curve, evaluate } from "@preset.nz/math"
1
2
  import * as React from "react"
2
- import { evaluate, type Basis, type Curve } from "@preset.nz/math"
3
3
 
4
4
  import {
5
5
  ContextMenu,
@@ -17,14 +17,14 @@ import {
17
17
  import { cn } from "../lib/utils"
18
18
  import {
19
19
  addPoint,
20
+ type CurveConstraint,
21
+ type Domain,
20
22
  movePoint,
21
23
  removePoint,
22
24
  setBasis,
23
25
  setTension,
24
26
  tensionForMidpoint,
25
27
  toggleSustain,
26
- type CurveConstraint,
27
- type Domain,
28
28
  } from "./curve-edits"
29
29
  import { linearScale } from "./scale"
30
30
  import { useCanvasDraw, useSize } from "./use-canvas"
@@ -101,8 +101,14 @@ const defaultFormat = (v: number) => v.toFixed(3)
101
101
  function sameCurve(a: Curve, b: Curve): boolean {
102
102
  if (a.points.length !== b.points.length || (a.sustain ?? -1) !== (b.sustain ?? -1)) return false
103
103
  return a.points.every((p, i) => {
104
- const q = b.points[i]!
105
- return p.x === q.x && p.y === q.y && p.basis === q.basis && (p.tension ?? 0) === (q.tension ?? 0)
104
+ const q = b.points[i]
105
+ return (
106
+ q !== undefined &&
107
+ p.x === q.x &&
108
+ p.y === q.y &&
109
+ p.basis === q.basis &&
110
+ (p.tension ?? 0) === (q.tension ?? 0)
111
+ )
106
112
  })
107
113
  }
108
114
 
@@ -152,8 +158,14 @@ export function CurveEditor({
152
158
  const [x0, x1] = xDomain
153
159
  const [y0, y1] = yDomain
154
160
  const domain: Domain = React.useMemo(() => ({ x: [x0, x1], y: [y0, y1] }), [x0, x1, y0, y1])
155
- const sx = React.useMemo(() => linearScale([x0, x1], [PAD, Math.max(PAD, size.width - PAD)]), [x0, x1, size.width])
156
- const sy = React.useMemo(() => linearScale([y0, y1], [Math.max(PAD, size.height - PAD), PAD]), [y0, y1, size.height])
161
+ const sx = React.useMemo(
162
+ () => linearScale([x0, x1], [PAD, Math.max(PAD, size.width - PAD)]),
163
+ [x0, x1, size.width],
164
+ )
165
+ const sy = React.useMemo(
166
+ () => linearScale([y0, y1], [Math.max(PAD, size.height - PAD), PAD]),
167
+ [y0, y1, size.height],
168
+ )
157
169
 
158
170
  const sel = selected !== null && selected < curve.points.length ? selected : null
159
171
 
@@ -217,7 +229,8 @@ export function CurveEditor({
217
229
  )
218
230
 
219
231
  const toData = (e: { clientX: number; clientY: number }) => {
220
- const r = svg.current!.getBoundingClientRect()
232
+ const r = svg.current?.getBoundingClientRect()
233
+ if (!r) return { x: 0, y: 0 }
221
234
  return { x: sx.invert(e.clientX - r.left), y: sy.invert(e.clientY - r.top) }
222
235
  }
223
236
 
@@ -336,6 +349,7 @@ export function CurveEditor({
336
349
  </div>
337
350
  )}
338
351
  <canvas ref={canvas} className="absolute inset-0 size-full" />
352
+ {/* biome-ignore lint/a11y/useSemanticElements: an SVG canvas has no semantic element; role="group" with a label names it */}
339
353
  <svg
340
354
  ref={svg}
341
355
  role="group"
@@ -345,6 +359,7 @@ export function CurveEditor({
345
359
  onPointerUp={() => finishDrag(true)}
346
360
  onLostPointerCapture={() => finishDrag(true)}
347
361
  >
362
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: the backing rect only takes pointer input to deselect and add points; the points are the keyboard targets */}
348
363
  <rect
349
364
  width="100%"
350
365
  height="100%"
@@ -358,10 +373,21 @@ export function CurveEditor({
358
373
  if (!next || p.basis !== "linear" || next.x === p.x) return null
359
374
  const mx = (p.x + next.x) / 2
360
375
  return (
376
+ // biome-ignore lint/a11y/noStaticElementInteractions: pointer-only tension handle; the keyboard route is the point menu and arrow keys on the points
361
377
  <g
378
+ // biome-ignore lint/suspicious/noArrayIndexKey: curve points have no id; the index is their identity
362
379
  key={`t${i}`}
363
380
  className="cursor-ns-resize"
364
- onPointerDown={(e) => beginDrag(e, { kind: "tension", index: i, start: curve, pointer: e.pointerId, from: { x: e.clientX, y: e.clientY }, started: false })}
381
+ onPointerDown={(e) =>
382
+ beginDrag(e, {
383
+ kind: "tension",
384
+ index: i,
385
+ start: curve,
386
+ pointer: e.pointerId,
387
+ from: { x: e.clientX, y: e.clientY },
388
+ started: false,
389
+ })
390
+ }
365
391
  onDoubleClick={(e) => {
366
392
  e.stopPropagation()
367
393
  commit(setTension(curve, i, 0, constrain), "edit")
@@ -386,7 +412,9 @@ export function CurveEditor({
386
412
  const isSel = sel === i
387
413
  const isSustain = mode === "envelope" && curve.sustain === i
388
414
  return (
415
+ // biome-ignore lint/a11y/useSemanticElements: an SVG <g> cannot be a <button>; role="button" with tabIndex and key handling
389
416
  <g
417
+ // biome-ignore lint/suspicious/noArrayIndexKey: curve points have no id; the index is their identity
390
418
  key={`p${i}`}
391
419
  tabIndex={0}
392
420
  role="button"
@@ -398,7 +426,14 @@ export function CurveEditor({
398
426
  onPointerDown={(e) => {
399
427
  setSelected(i)
400
428
  ;(e.currentTarget as SVGGElement).focus()
401
- beginDrag(e, { kind: "point", index: i, start: curve, pointer: e.pointerId, from: { x: e.clientX, y: e.clientY }, started: false })
429
+ beginDrag(e, {
430
+ kind: "point",
431
+ index: i,
432
+ start: curve,
433
+ pointer: e.pointerId,
434
+ from: { x: e.clientX, y: e.clientY },
435
+ started: false,
436
+ })
402
437
  }}
403
438
  onContextMenu={() => {
404
439
  setSelected(i)
@@ -419,7 +454,9 @@ export function CurveEditor({
419
454
  cy={sy(p.y)}
420
455
  r={POINT_R}
421
456
  strokeWidth={1.5}
422
- className={cn(isSel ? "fill-primary stroke-primary" : "fill-background stroke-primary")}
457
+ className={cn(
458
+ isSel ? "fill-primary stroke-primary" : "fill-background stroke-primary",
459
+ )}
423
460
  />
424
461
  {isSustain && (
425
462
  <text
@@ -453,7 +490,9 @@ export function CurveEditor({
453
490
  <ContextMenuLabel>Interpolation to the next point</ContextMenuLabel>
454
491
  <ContextMenuRadioGroup
455
492
  value={menuPoint.basis}
456
- onValueChange={(b) => commit(setBasis(curve, target.index, b as Basis, constrain), "edit")}
493
+ onValueChange={(b) =>
494
+ commit(setBasis(curve, target.index, b as Basis, constrain), "edit")
495
+ }
457
496
  >
458
497
  {BASES.map((b) => (
459
498
  <ContextMenuRadioItem
@@ -461,7 +500,8 @@ export function CurveEditor({
461
500
  value={b.value}
462
501
  disabled={
463
502
  target.index === curve.points.length - 1 ||
464
- (b.value !== menuPoint.basis && !setBasis(curve, target.index, b.value, constrain))
503
+ (b.value !== menuPoint.basis &&
504
+ !setBasis(curve, target.index, b.value, constrain))
465
505
  }
466
506
  >
467
507
  {b.label}
@@ -475,7 +515,9 @@ export function CurveEditor({
475
515
  <ContextMenuCheckboxItem
476
516
  checked={curve.sustain === target.index}
477
517
  disabled={!toggleSustain(curve, target.index, constrain)}
478
- onCheckedChange={() => commit(toggleSustain(curve, target.index, constrain), "edit")}
518
+ onCheckedChange={() =>
519
+ commit(toggleSustain(curve, target.index, constrain), "edit")
520
+ }
479
521
  >
480
522
  Sustain here
481
523
  </ContextMenuCheckboxItem>
@@ -1,4 +1,11 @@
1
- import { clamp, invlerp, TENSION_STRENGTH, type Basis, type Curve, type CurvePoint } from "@preset.nz/math"
1
+ import {
2
+ type Basis,
3
+ type Curve,
4
+ type CurvePoint,
5
+ clamp,
6
+ invlerp,
7
+ TENSION_STRENGTH,
8
+ } from "@preset.nz/math"
2
9
 
3
10
  /**
4
11
  * Limits on a curve, applied to every proposed edit: a drag, a nudge, an added or removed
@@ -87,7 +94,12 @@ export function removePoint(curve: Curve, i: number, constrain?: CurveConstraint
87
94
  }
88
95
 
89
96
  /** Point `i`'s basis. Tension only means something on a linear segment, so others drop it. */
90
- export function setBasis(curve: Curve, i: number, basis: Basis, constrain?: CurveConstraint): Curve | null {
97
+ export function setBasis(
98
+ curve: Curve,
99
+ i: number,
100
+ basis: Basis,
101
+ constrain?: CurveConstraint,
102
+ ): Curve | null {
91
103
  const points = curve.points.map((p, j) => {
92
104
  if (j !== i) return p
93
105
  const q: CurvePoint = { ...p, basis }
@@ -105,7 +117,12 @@ export function toggleSustain(curve: Curve, i: number, constrain?: CurveConstrai
105
117
  }
106
118
 
107
119
  /** The tension of the segment from point `i`, -1..1. Zero removes it. */
108
- export function setTension(curve: Curve, i: number, tension: number, constrain?: CurveConstraint): Curve | null {
120
+ export function setTension(
121
+ curve: Curve,
122
+ i: number,
123
+ tension: number,
124
+ constrain?: CurveConstraint,
125
+ ): Curve | null {
109
126
  const t = clamp(tension, -1, 1)
110
127
  const points = curve.points.map((p, j) => {
111
128
  if (j !== i) return p
@@ -1,16 +1,16 @@
1
- export { CurveEditor, type CurveEditKind, type CurveEditorProps } from "./curve-editor"
1
+ export { type Adsr, adsrCurve, adsrOf, constrainAdsr } from "./adsr"
2
+ export { type CurveEditKind, CurveEditor, type CurveEditorProps } from "./curve-editor"
2
3
  export {
3
4
  addPoint,
5
+ type CurveConstraint,
6
+ type Domain,
4
7
  movePoint,
5
8
  removePoint,
6
9
  setBasis,
7
10
  setTension,
8
11
  tensionForMidpoint,
9
12
  toggleSustain,
10
- type CurveConstraint,
11
- type Domain,
12
13
  } from "./curve-edits"
13
- export { adsrCurve, adsrOf, constrainAdsr, type Adsr } from "./adsr"
14
- export { linearScale, type LinearScale } from "./scale"
15
- export { useCanvasDraw, useSize, type Size } from "./use-canvas"
14
+ export { type LinearScale, linearScale } from "./scale"
15
+ export { type Size, useCanvasDraw, useSize } from "./use-canvas"
16
16
  export { useTokenColours } from "./use-tokens"
@@ -11,7 +11,10 @@ export interface LinearScale {
11
11
  range: readonly [number, number]
12
12
  }
13
13
 
14
- export function linearScale(domain: readonly [number, number], range: readonly [number, number]): LinearScale {
14
+ export function linearScale(
15
+ domain: readonly [number, number],
16
+ range: readonly [number, number],
17
+ ): LinearScale {
15
18
  const [d0, d1] = domain
16
19
  const [r0, r1] = range
17
20
  const scale = ((v: number) => fitUnclamped(v, d0, d1, r0, r1)) as LinearScale
@@ -1,4 +1,4 @@
1
- import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from "react"
1
+ import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from "react"
2
2
 
3
3
  export interface Size {
4
4
  width: number
@@ -13,7 +13,10 @@ export function useTokenColours<K extends string>(names: readonly K[]): Record<K
13
13
  const update = () => setColours(read(list))
14
14
  update()
15
15
  const observer = new MutationObserver(update)
16
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style", "data-theme"] })
16
+ observer.observe(document.documentElement, {
17
+ attributes: true,
18
+ attributeFilter: ["class", "style", "data-theme"],
19
+ })
17
20
  const media = window.matchMedia("(prefers-color-scheme: dark)")
18
21
  media.addEventListener("change", update)
19
22
  return () => {
package/src/index.css CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Seeded from Strata's index.css. Dark mode is a .dark class on <html>. */
9
9
  @import "tw-animate-css";
10
- @import "shadcn/tailwind.css";
10
+ @import "./shadcn-tailwind.css";
11
11
 
12
12
  /* Two families only (Georg 2026-10-03): Geist for the interface, JetBrains Mono for values and code. */
13
13
  @import "@fontsource-variable/geist";
@@ -15,6 +15,14 @@
15
15
 
16
16
  @custom-variant dark (&:is(.dark *));
17
17
 
18
+ /* Progress, indeterminate: the 40%-wide segment crosses the track left to right. */
19
+ @theme inline {
20
+ @keyframes progress-slide {
21
+ from { transform: translateX(-100%); }
22
+ to { transform: translateX(250%); }
23
+ }
24
+ }
25
+
18
26
  @theme inline {
19
27
  /* Headings use the interface face; hierarchy comes from size and weight. */
20
28
  --font-heading: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
package/src/index.ts CHANGED
@@ -1,6 +1,3 @@
1
- export { cn } from "./lib/utils"
2
- export { useIsMobile } from "./lib/use-mobile"
3
-
4
1
  export * from "./components/avatar"
5
2
  export * from "./components/badge"
6
3
  export * from "./components/breadcrumb"
@@ -9,19 +6,23 @@ export * from "./components/card"
9
6
  export * from "./components/checkbox"
10
7
  export * from "./components/color-field"
11
8
  export * from "./components/color-swatch"
9
+ export * from "./components/confirm-dialog"
12
10
  export * from "./components/context-menu"
13
11
  export * from "./components/dialog"
12
+ export * from "./components/download-prompt"
14
13
  export * from "./components/dropdown-menu"
15
14
  export * from "./components/empty-state"
16
15
  export * from "./components/input"
17
16
  export * from "./components/label"
17
+ export * from "./components/list"
18
18
  export * from "./components/navigation-menu"
19
19
  export * from "./components/number-field"
20
20
  export * from "./components/popover"
21
+ export * from "./components/progress"
21
22
  export * from "./components/select"
22
- export * from "./components/side-panel"
23
23
  export * from "./components/separator"
24
24
  export * from "./components/sheet"
25
+ export * from "./components/side-panel"
25
26
  export * from "./components/sidebar"
26
27
  export * from "./components/skeleton"
27
28
  export * from "./components/slider"
@@ -33,5 +34,6 @@ export * from "./components/toggle"
33
34
  export * from "./components/toggle-group"
34
35
  export * from "./components/toolbar"
35
36
  export * from "./components/tooltip"
36
-
37
37
  export * as Icons from "./icons"
38
+ export { useIsMobile } from "./lib/use-mobile"
39
+ export { cn } from "./lib/utils"
@@ -13,6 +13,6 @@ export function useIsMobile() {
13
13
  return React.useSyncExternalStore(
14
14
  subscribe,
15
15
  () => window.matchMedia(QUERY).matches,
16
- () => false
16
+ () => false,
17
17
  )
18
18
  }
package/src/lib/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { clsx, type ClassValue } from "clsx"
1
+ import { type ClassValue, clsx } from "clsx"
2
2
  import { twMerge } from "tailwind-merge"
3
3
 
4
4
  export function cn(...inputs: ClassValue[]) {