@preset.nz/ux-kit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -2
  3. package/package.json +51 -3
  4. package/src/components/avatar.tsx +35 -0
  5. package/src/components/badge.tsx +42 -0
  6. package/src/components/breadcrumb.tsx +102 -0
  7. package/src/components/button.tsx +56 -0
  8. package/src/components/card.tsx +92 -0
  9. package/src/components/checkbox.tsx +27 -0
  10. package/src/components/color-field.tsx +248 -0
  11. package/src/components/color-swatch.tsx +52 -0
  12. package/src/components/context-menu.tsx +249 -0
  13. package/src/components/dialog.tsx +137 -0
  14. package/src/components/dropdown-menu.tsx +249 -0
  15. package/src/components/empty-state.tsx +32 -0
  16. package/src/components/input.tsx +20 -0
  17. package/src/components/label.tsx +18 -0
  18. package/src/components/navigation-menu.tsx +144 -0
  19. package/src/components/number-field.tsx +428 -0
  20. package/src/components/popover.tsx +82 -0
  21. package/src/components/select.tsx +201 -0
  22. package/src/components/separator.tsx +25 -0
  23. package/src/components/sheet.tsx +124 -0
  24. package/src/components/side-panel.tsx +166 -0
  25. package/src/components/sidebar.tsx +735 -0
  26. package/src/components/skeleton.tsx +14 -0
  27. package/src/components/slider.tsx +75 -0
  28. package/src/components/snackbar.tsx +141 -0
  29. package/src/components/status-bar.tsx +38 -0
  30. package/src/components/tabs.tsx +78 -0
  31. package/src/components/textarea.tsx +18 -0
  32. package/src/components/toggle-group.tsx +86 -0
  33. package/src/components/toggle.tsx +43 -0
  34. package/src/components/toolbar.tsx +307 -0
  35. package/src/components/tooltip.tsx +54 -0
  36. package/src/graphics/adsr.ts +70 -0
  37. package/src/graphics/curve-editor.tsx +525 -0
  38. package/src/graphics/curve-edits.ts +129 -0
  39. package/src/graphics/index.ts +16 -0
  40. package/src/graphics/scale.ts +22 -0
  41. package/src/graphics/use-canvas.ts +56 -0
  42. package/src/graphics/use-tokens.ts +32 -0
  43. package/src/icons.ts +3 -0
  44. package/src/index.css +199 -0
  45. package/src/index.ts +37 -0
  46. package/src/lib/use-mobile.ts +18 -0
  47. package/src/lib/utils.ts +6 -0
@@ -0,0 +1,201 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Select as SelectPrimitive } from "@base-ui/react/select"
5
+
6
+ import { cn } from "../lib/utils"
7
+ import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
8
+
9
+ const Select = SelectPrimitive.Root
10
+
11
+ function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
12
+ return (
13
+ <SelectPrimitive.Group
14
+ data-slot="select-group"
15
+ className={cn("scroll-my-1", className)}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
22
+ return (
23
+ <SelectPrimitive.Value
24
+ data-slot="select-value"
25
+ className={cn("flex flex-1 text-left", className)}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function SelectTrigger({
32
+ className,
33
+ size = "default",
34
+ children,
35
+ ...props
36
+ }: SelectPrimitive.Trigger.Props & {
37
+ size?: "sm" | "default"
38
+ }) {
39
+ return (
40
+ <SelectPrimitive.Trigger
41
+ data-slot="select-trigger"
42
+ data-size={size}
43
+ className={cn(
44
+ "flex w-fit items-center justify-between gap-1.5 rounded-none border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs whitespace-nowrap transition-colors outline-none select-none 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 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
45
+ className
46
+ )}
47
+ {...props}
48
+ >
49
+ {children}
50
+ <SelectPrimitive.Icon
51
+ render={
52
+ <CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />
53
+ }
54
+ />
55
+ </SelectPrimitive.Trigger>
56
+ )
57
+ }
58
+
59
+ function SelectContent({
60
+ className,
61
+ children,
62
+ side = "bottom",
63
+ sideOffset = 4,
64
+ align = "center",
65
+ alignOffset = 0,
66
+ alignItemWithTrigger = true,
67
+ ...props
68
+ }: SelectPrimitive.Popup.Props &
69
+ Pick<
70
+ SelectPrimitive.Positioner.Props,
71
+ "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
72
+ >) {
73
+ return (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Positioner
76
+ side={side}
77
+ sideOffset={sideOffset}
78
+ align={align}
79
+ alignOffset={alignOffset}
80
+ alignItemWithTrigger={alignItemWithTrigger}
81
+ className="isolate z-50"
82
+ >
83
+ <SelectPrimitive.Popup
84
+ data-slot="select-content"
85
+ data-align-trigger={alignItemWithTrigger}
86
+ className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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", className )}
87
+ {...props}
88
+ >
89
+ <SelectScrollUpButton />
90
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
91
+ <SelectScrollDownButton />
92
+ </SelectPrimitive.Popup>
93
+ </SelectPrimitive.Positioner>
94
+ </SelectPrimitive.Portal>
95
+ )
96
+ }
97
+
98
+ function SelectLabel({
99
+ className,
100
+ ...props
101
+ }: SelectPrimitive.GroupLabel.Props) {
102
+ return (
103
+ <SelectPrimitive.GroupLabel
104
+ data-slot="select-label"
105
+ className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ )
109
+ }
110
+
111
+ function SelectItem({
112
+ className,
113
+ children,
114
+ ...props
115
+ }: SelectPrimitive.Item.Props) {
116
+ return (
117
+ <SelectPrimitive.Item
118
+ data-slot="select-item"
119
+ className={cn(
120
+ "relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
121
+ className
122
+ )}
123
+ {...props}
124
+ >
125
+ <SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
126
+ {children}
127
+ </SelectPrimitive.ItemText>
128
+ <SelectPrimitive.ItemIndicator
129
+ render={
130
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
131
+ }
132
+ >
133
+ <CheckIcon className="pointer-events-none" />
134
+ </SelectPrimitive.ItemIndicator>
135
+ </SelectPrimitive.Item>
136
+ )
137
+ }
138
+
139
+ function SelectSeparator({
140
+ className,
141
+ ...props
142
+ }: SelectPrimitive.Separator.Props) {
143
+ return (
144
+ <SelectPrimitive.Separator
145
+ data-slot="select-separator"
146
+ className={cn("pointer-events-none -mx-1 h-px bg-border", className)}
147
+ {...props}
148
+ />
149
+ )
150
+ }
151
+
152
+ function SelectScrollUpButton({
153
+ className,
154
+ ...props
155
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
156
+ return (
157
+ <SelectPrimitive.ScrollUpArrow
158
+ data-slot="select-scroll-up-button"
159
+ className={cn(
160
+ "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
161
+ className
162
+ )}
163
+ {...props}
164
+ >
165
+ <CaretUpIcon
166
+ />
167
+ </SelectPrimitive.ScrollUpArrow>
168
+ )
169
+ }
170
+
171
+ function SelectScrollDownButton({
172
+ className,
173
+ ...props
174
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
175
+ return (
176
+ <SelectPrimitive.ScrollDownArrow
177
+ data-slot="select-scroll-down-button"
178
+ className={cn(
179
+ "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
180
+ className
181
+ )}
182
+ {...props}
183
+ >
184
+ <CaretDownIcon
185
+ />
186
+ </SelectPrimitive.ScrollDownArrow>
187
+ )
188
+ }
189
+
190
+ export {
191
+ Select,
192
+ SelectContent,
193
+ SelectGroup,
194
+ SelectItem,
195
+ SelectLabel,
196
+ SelectScrollDownButton,
197
+ SelectScrollUpButton,
198
+ SelectSeparator,
199
+ SelectTrigger,
200
+ SelectValue,
201
+ }
@@ -0,0 +1,25 @@
1
+ "use client"
2
+
3
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
4
+
5
+ import { cn } from "../lib/utils"
6
+
7
+ function Separator({
8
+ className,
9
+ orientation = "horizontal",
10
+ ...props
11
+ }: SeparatorPrimitive.Props) {
12
+ return (
13
+ <SeparatorPrimitive
14
+ data-slot="separator"
15
+ orientation={orientation}
16
+ className={cn(
17
+ "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ export { Separator }
@@ -0,0 +1,124 @@
1
+ import * as React from "react"
2
+ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
3
+ import { XIcon } from "@phosphor-icons/react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { Button } from "./button"
7
+
8
+ // A sheet is a dialog docked to one edge. Built on Base UI's Dialog.
9
+ function Sheet({ ...props }: SheetPrimitive.Root.Props) {
10
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />
11
+ }
12
+
13
+ function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
14
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
15
+ }
16
+
17
+ function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
18
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
19
+ }
20
+
21
+ function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
22
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
23
+ }
24
+
25
+ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
26
+ return (
27
+ <SheetPrimitive.Backdrop
28
+ data-slot="sheet-overlay"
29
+ className={cn(
30
+ "fixed inset-0 z-50 bg-foreground/40 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+
38
+ function SheetContent({
39
+ className,
40
+ children,
41
+ side = "right",
42
+ showCloseButton = true,
43
+ ...props
44
+ }: SheetPrimitive.Popup.Props & {
45
+ side?: "top" | "right" | "bottom" | "left"
46
+ showCloseButton?: boolean
47
+ }) {
48
+ return (
49
+ <SheetPortal>
50
+ <SheetOverlay />
51
+ <SheetPrimitive.Popup
52
+ data-slot="sheet-content"
53
+ data-side={side}
54
+ className={cn(
55
+ "fixed z-50 flex flex-col gap-4 bg-background shadow-lg outline-none transition ease-in-out data-open:animate-in data-closed:animate-out data-closed:duration-300 data-open:duration-500",
56
+ side === "right" &&
57
+ "inset-y-0 right-0 h-full w-3/4 border-l data-closed:slide-out-to-right data-open:slide-in-from-right sm:max-w-sm",
58
+ side === "left" &&
59
+ "inset-y-0 left-0 h-full w-3/4 border-r data-closed:slide-out-to-left data-open:slide-in-from-left sm:max-w-sm",
60
+ side === "top" &&
61
+ "inset-x-0 top-0 h-auto border-b data-closed:slide-out-to-top data-open:slide-in-from-top",
62
+ side === "bottom" &&
63
+ "inset-x-0 bottom-0 h-auto border-t data-closed:slide-out-to-bottom data-open:slide-in-from-bottom",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {showCloseButton && (
70
+ <SheetPrimitive.Close
71
+ data-slot="sheet-close"
72
+ render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}
73
+ >
74
+ <XIcon />
75
+ <span className="sr-only">Close</span>
76
+ </SheetPrimitive.Close>
77
+ )}
78
+ </SheetPrimitive.Popup>
79
+ </SheetPortal>
80
+ )
81
+ }
82
+
83
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />
86
+ )
87
+ }
88
+
89
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
90
+ return (
91
+ <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
92
+ )
93
+ }
94
+
95
+ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
96
+ return (
97
+ <SheetPrimitive.Title
98
+ data-slot="sheet-title"
99
+ className={cn("font-semibold text-foreground", className)}
100
+ {...props}
101
+ />
102
+ )
103
+ }
104
+
105
+ function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
106
+ return (
107
+ <SheetPrimitive.Description
108
+ data-slot="sheet-description"
109
+ className={cn("text-sm text-muted-foreground", className)}
110
+ {...props}
111
+ />
112
+ )
113
+ }
114
+
115
+ export {
116
+ Sheet,
117
+ SheetTrigger,
118
+ SheetClose,
119
+ SheetContent,
120
+ SheetHeader,
121
+ SheetFooter,
122
+ SheetTitle,
123
+ SheetDescription,
124
+ }
@@ -0,0 +1,166 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../lib/utils"
4
+ import { buttonVariants } from "./button"
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
6
+
7
+ // SidePanel: a docked panel with a collapsed icon rail and a drag-to-resize
8
+ // edge (ux-patterns.md, "Side panels"). Props only: the app owns `open` and
9
+ // `width`, so it can persist them and toggle from the menu, the toolbar or a
10
+ // shortcut. Collapsed, the panel leaves a narrow rail with one icon that
11
+ // reopens it. The drag uses pointer capture, so the edge never drifts from
12
+ // the cursor; arrow keys on the focused edge resize too.
13
+
14
+ /** Keeps a width inside [min, max]; a corrupt stored value falls back to `min`. */
15
+ function clampWidth(width: number, min: number, max: number): number {
16
+ if (!Number.isFinite(width)) return min
17
+ return Math.min(max, Math.max(min, width))
18
+ }
19
+
20
+ interface SidePanelProps extends Omit<React.ComponentProps<"aside">, "title"> {
21
+ /** Which window edge the panel docks to. The resize handle sits on the inner edge. */
22
+ side: "left" | "right"
23
+ open: boolean
24
+ onOpenChange: (open: boolean) => void
25
+ /** Width in px while open. */
26
+ width: number
27
+ onWidthChange: (width: number) => void
28
+ minWidth?: number
29
+ maxWidth?: number
30
+ /** Names the panel in the rail tooltip and for assistive tech. */
31
+ title: string
32
+ /** The rail's icon. */
33
+ icon: React.ReactNode
34
+ /** Shown in the rail tooltip, in platform notation ("⌥⌘S"). */
35
+ shortcut?: string
36
+ }
37
+
38
+ function SidePanel({
39
+ side,
40
+ open,
41
+ onOpenChange,
42
+ width,
43
+ onWidthChange,
44
+ minWidth = 180,
45
+ maxWidth = 480,
46
+ title,
47
+ icon,
48
+ shortcut,
49
+ className,
50
+ children,
51
+ ...props
52
+ }: SidePanelProps) {
53
+ const border = side === "left" ? "border-r" : "border-l"
54
+
55
+ if (!open) {
56
+ return (
57
+ <aside
58
+ data-slot="side-panel"
59
+ data-state="closed"
60
+ aria-label={title}
61
+ className={cn("flex w-10 shrink-0 flex-col items-center bg-sidebar py-2 text-sidebar-foreground", border, "border-sidebar-border", className)}
62
+ {...props}
63
+ >
64
+ <Tooltip>
65
+ <TooltipTrigger
66
+ render={
67
+ <button
68
+ type="button"
69
+ aria-label={`Show ${title}`}
70
+ onClick={() => onOpenChange(true)}
71
+ className={buttonVariants({ variant: "ghost", size: "icon" })}
72
+ />
73
+ }
74
+ >
75
+ {icon}
76
+ </TooltipTrigger>
77
+ <TooltipContent side={side === "left" ? "right" : "left"}>
78
+ <span>Show {title}</span>
79
+ {shortcut ? (
80
+ <kbd data-slot="kbd" className="rounded-sm bg-background/20 px-1 font-sans text-[11px]">
81
+ {shortcut}
82
+ </kbd>
83
+ ) : null}
84
+ </TooltipContent>
85
+ </Tooltip>
86
+ </aside>
87
+ )
88
+ }
89
+
90
+ const startDrag = (e: React.PointerEvent<HTMLDivElement>) => {
91
+ e.preventDefault()
92
+ const el = e.currentTarget
93
+ el.setPointerCapture(e.pointerId)
94
+ const startX = e.clientX
95
+ const startWidth = width
96
+ const sign = side === "left" ? 1 : -1
97
+ const move = (ev: PointerEvent) =>
98
+ onWidthChange(clampWidth(startWidth + sign * (ev.clientX - startX), minWidth, maxWidth))
99
+ const end = () => {
100
+ el.removeEventListener("pointermove", move)
101
+ el.removeEventListener("pointerup", end)
102
+ el.removeEventListener("pointercancel", end)
103
+ }
104
+ el.addEventListener("pointermove", move)
105
+ el.addEventListener("pointerup", end)
106
+ el.addEventListener("pointercancel", end)
107
+ }
108
+
109
+ const onKey = (e: React.KeyboardEvent) => {
110
+ const grow = side === "left" ? "ArrowRight" : "ArrowLeft"
111
+ const shrink = side === "left" ? "ArrowLeft" : "ArrowRight"
112
+ if (e.key !== grow && e.key !== shrink) return
113
+ e.preventDefault()
114
+ onWidthChange(clampWidth(width + (e.key === grow ? 16 : -16), minWidth, maxWidth))
115
+ }
116
+
117
+ const w = clampWidth(width, minWidth, maxWidth)
118
+ return (
119
+ <aside
120
+ data-slot="side-panel"
121
+ data-state="open"
122
+ aria-label={title}
123
+ style={{ width: w }}
124
+ className={cn("relative flex shrink-0 flex-col overflow-hidden bg-sidebar text-sidebar-foreground", border, "border-sidebar-border", className)}
125
+ {...props}
126
+ >
127
+ {children}
128
+ <div
129
+ role="separator"
130
+ aria-orientation="vertical"
131
+ aria-label={`Resize ${title}`}
132
+ aria-valuenow={w}
133
+ aria-valuemin={minWidth}
134
+ aria-valuemax={maxWidth}
135
+ tabIndex={0}
136
+ onPointerDown={startDrag}
137
+ onKeyDown={onKey}
138
+ className={cn(
139
+ "absolute inset-y-0 z-10 w-1 cursor-col-resize touch-none hover:bg-primary/30 focus-visible:bg-primary/40 focus-visible:outline-none",
140
+ side === "left" ? "right-0" : "left-0"
141
+ )}
142
+ />
143
+ </aside>
144
+ )
145
+ }
146
+
147
+ /** A panel's title row; put the panel's own actions in `children`. */
148
+ function SidePanelHeader({ className, children, ...props }: React.ComponentProps<"div">) {
149
+ return (
150
+ <div
151
+ data-slot="side-panel-header"
152
+ className={cn("flex h-9 shrink-0 items-center gap-2 border-b border-sidebar-border px-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", className)}
153
+ {...props}
154
+ >
155
+ {children}
156
+ </div>
157
+ )
158
+ }
159
+
160
+ /** The scrolling body under the header. */
161
+ function SidePanelContent({ className, ...props }: React.ComponentProps<"div">) {
162
+ return <div data-slot="side-panel-content" className={cn("min-h-0 flex-1 overflow-auto", className)} {...props} />
163
+ }
164
+
165
+ export { SidePanel, SidePanelContent, SidePanelHeader, clampWidth }
166
+ export type { SidePanelProps }