@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,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(
|
|
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
|
|
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>
|
package/src/components/tabs.tsx
CHANGED
|
@@ -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 {
|
|
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
|
>
|
package/src/graphics/adsr.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
51
|
-
const
|
|
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 = [
|
|
54
|
-
const len =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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(
|
|
156
|
-
|
|
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
|
|
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) =>
|
|
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, {
|
|
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(
|
|
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) =>
|
|
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 &&
|
|
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={() =>
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
package/src/graphics/index.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
export {
|
|
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 {
|
|
14
|
-
export {
|
|
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"
|
package/src/graphics/scale.ts
CHANGED
|
@@ -11,7 +11,10 @@ export interface LinearScale {
|
|
|
11
11
|
range: readonly [number, number]
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function 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
|
|
@@ -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, {
|
|
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
|
|
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"
|
package/src/lib/use-mobile.ts
CHANGED
package/src/lib/utils.ts
CHANGED