@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
@@ -0,0 +1,77 @@
1
+ import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
2
+
3
+ import { cn } from "../lib/utils"
4
+
5
+ type ProgressProps = {
6
+ /** `null` is indeterminate: a slow moving segment, still under `prefers-reduced-motion`. */
7
+ value: number | null
8
+ max?: number
9
+ /** The accessible name; shown above the bar when `showLabel`. */
10
+ label?: string
11
+ showLabel?: boolean
12
+ /** Right-aligned text the caller builds ("12.4 / 44.7 MB", "63%"). The kit does not format it. */
13
+ detail?: string
14
+ className?: string
15
+ }
16
+
17
+ // A 6 px rounded bar that fills its container: `--primary` on `--muted`. No per-state colour:
18
+ // a failure is the caller's message, not a red bar.
19
+ function Progress({
20
+ value,
21
+ max = 100,
22
+ label,
23
+ showLabel = false,
24
+ detail,
25
+ className,
26
+ }: ProgressProps) {
27
+ const labelShown = showLabel && label !== undefined
28
+ return (
29
+ <ProgressPrimitive.Root
30
+ data-slot="progress"
31
+ value={value}
32
+ max={max}
33
+ aria-label={labelShown ? undefined : label}
34
+ className={cn("flex w-full flex-col gap-1.5", className)}
35
+ >
36
+ {(labelShown || detail !== undefined) && (
37
+ <div className="flex items-baseline justify-between gap-3 text-xs">
38
+ {labelShown ? (
39
+ <ProgressPrimitive.Label data-slot="progress-label" className="font-medium">
40
+ {label}
41
+ </ProgressPrimitive.Label>
42
+ ) : (
43
+ <span />
44
+ )}
45
+ {detail !== undefined && (
46
+ <span data-slot="progress-detail" className="text-muted-foreground tabular-nums">
47
+ {detail}
48
+ </span>
49
+ )}
50
+ </div>
51
+ )}
52
+ <ProgressPrimitive.Track
53
+ data-slot="progress-track"
54
+ className="relative h-1.5 w-full overflow-hidden rounded-full bg-muted"
55
+ >
56
+ <ProgressPrimitive.Indicator
57
+ data-slot="progress-indicator"
58
+ className="h-full rounded-full bg-primary transition-[width] data-indeterminate:w-2/5 data-indeterminate:animate-[progress-slide_2s_ease-in-out_infinite] data-indeterminate:transition-none motion-reduce:data-indeterminate:animate-none"
59
+ />
60
+ </ProgressPrimitive.Track>
61
+ </ProgressPrimitive.Root>
62
+ )
63
+ }
64
+
65
+ // Decimal sizes, one place: bytes under a kilobyte, then kilobytes, megabytes and gigabytes.
66
+ // 46_900_000 is "46.9 MB", 1_200_000_000 is "1.2 GB", 2_048 is "2.0 KB", 0 is "0 B".
67
+ function formatBytes(n: number): string {
68
+ if (!Number.isFinite(n) || n <= 0) return "0 B"
69
+ if (n < 1_000) return `${Math.round(n)} B`
70
+ // Each threshold sits where one place rounds up to 1000.0 of the smaller unit.
71
+ if (n < 999_950) return `${(n / 1_000).toFixed(1)} KB`
72
+ if (n < 999_950_000) return `${(n / 1_000_000).toFixed(1)} MB`
73
+ return `${(n / 1_000_000_000).toFixed(1)} GB`
74
+ }
75
+
76
+ export type { ProgressProps }
77
+ export { formatBytes, Progress }
@@ -1,10 +1,9 @@
1
1
  "use client"
2
2
 
3
- import * as React from "react"
4
3
  import { Select as SelectPrimitive } from "@base-ui/react/select"
5
-
4
+ import { CaretDownIcon, CaretUpIcon, CheckIcon } from "@phosphor-icons/react"
5
+ import type * as React from "react"
6
6
  import { cn } from "../lib/utils"
7
- import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
8
7
 
9
8
  const Select = SelectPrimitive.Root
10
9
 
@@ -42,15 +41,13 @@ function SelectTrigger({
42
41
  data-size={size}
43
42
  className={cn(
44
43
  "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
44
+ className,
46
45
  )}
47
46
  {...props}
48
47
  >
49
48
  {children}
50
49
  <SelectPrimitive.Icon
51
- render={
52
- <CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />
53
- }
50
+ render={<CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />}
54
51
  />
55
52
  </SelectPrimitive.Trigger>
56
53
  )
@@ -83,7 +80,10 @@ function SelectContent({
83
80
  <SelectPrimitive.Popup
84
81
  data-slot="select-content"
85
82
  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 )}
83
+ className={cn(
84
+ "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",
85
+ className,
86
+ )}
87
87
  {...props}
88
88
  >
89
89
  <SelectScrollUpButton />
@@ -95,10 +95,7 @@ function SelectContent({
95
95
  )
96
96
  }
97
97
 
98
- function SelectLabel({
99
- className,
100
- ...props
101
- }: SelectPrimitive.GroupLabel.Props) {
98
+ function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
102
99
  return (
103
100
  <SelectPrimitive.GroupLabel
104
101
  data-slot="select-label"
@@ -108,17 +105,13 @@ function SelectLabel({
108
105
  )
109
106
  }
110
107
 
111
- function SelectItem({
112
- className,
113
- children,
114
- ...props
115
- }: SelectPrimitive.Item.Props) {
108
+ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
116
109
  return (
117
110
  <SelectPrimitive.Item
118
111
  data-slot="select-item"
119
112
  className={cn(
120
113
  "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
114
+ className,
122
115
  )}
123
116
  {...props}
124
117
  >
@@ -136,10 +129,7 @@ function SelectItem({
136
129
  )
137
130
  }
138
131
 
139
- function SelectSeparator({
140
- className,
141
- ...props
142
- }: SelectPrimitive.Separator.Props) {
132
+ function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
143
133
  return (
144
134
  <SelectPrimitive.Separator
145
135
  data-slot="select-separator"
@@ -158,12 +148,11 @@ function SelectScrollUpButton({
158
148
  data-slot="select-scroll-up-button"
159
149
  className={cn(
160
150
  "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
151
+ className,
162
152
  )}
163
153
  {...props}
164
154
  >
165
- <CaretUpIcon
166
- />
155
+ <CaretUpIcon />
167
156
  </SelectPrimitive.ScrollUpArrow>
168
157
  )
169
158
  }
@@ -177,12 +166,11 @@ function SelectScrollDownButton({
177
166
  data-slot="select-scroll-down-button"
178
167
  className={cn(
179
168
  "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
169
+ className,
181
170
  )}
182
171
  {...props}
183
172
  >
184
- <CaretDownIcon
185
- />
173
+ <CaretDownIcon />
186
174
  </SelectPrimitive.ScrollDownArrow>
187
175
  )
188
176
  }
@@ -4,18 +4,14 @@ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
4
4
 
5
5
  import { cn } from "../lib/utils"
6
6
 
7
- function Separator({
8
- className,
9
- orientation = "horizontal",
10
- ...props
11
- }: SeparatorPrimitive.Props) {
7
+ function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
12
8
  return (
13
9
  <SeparatorPrimitive
14
10
  data-slot="separator"
15
11
  orientation={orientation}
16
12
  className={cn(
17
13
  "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
18
- className
14
+ className,
19
15
  )}
20
16
  {...props}
21
17
  />
@@ -1,6 +1,6 @@
1
- import * as React from "react"
2
1
  import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
3
2
  import { XIcon } from "@phosphor-icons/react"
3
+ import type * as React from "react"
4
4
 
5
5
  import { cn } from "../lib/utils"
6
6
  import { Button } from "./button"
@@ -28,7 +28,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
28
28
  data-slot="sheet-overlay"
29
29
  className={cn(
30
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
31
+ className,
32
32
  )}
33
33
  {...props}
34
34
  />
@@ -61,7 +61,7 @@ function SheetContent({
61
61
  "inset-x-0 top-0 h-auto border-b data-closed:slide-out-to-top data-open:slide-in-from-top",
62
62
  side === "bottom" &&
63
63
  "inset-x-0 bottom-0 h-auto border-t data-closed:slide-out-to-bottom data-open:slide-in-from-bottom",
64
- className
64
+ className,
65
65
  )}
66
66
  {...props}
67
67
  >
@@ -82,13 +82,21 @@ function SheetContent({
82
82
 
83
83
  function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
84
84
  return (
85
- <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />
85
+ <div
86
+ data-slot="sheet-header"
87
+ className={cn("flex flex-col gap-1.5 p-4", className)}
88
+ {...props}
89
+ />
86
90
  )
87
91
  }
88
92
 
89
93
  function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
90
94
  return (
91
- <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
95
+ <div
96
+ data-slot="sheet-footer"
97
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
98
+ {...props}
99
+ />
92
100
  )
93
101
  }
94
102
 
@@ -114,11 +122,11 @@ function SheetDescription({ className, ...props }: SheetPrimitive.Description.Pr
114
122
 
115
123
  export {
116
124
  Sheet,
117
- SheetTrigger,
118
125
  SheetClose,
119
126
  SheetContent,
120
- SheetHeader,
127
+ SheetDescription,
121
128
  SheetFooter,
129
+ SheetHeader,
122
130
  SheetTitle,
123
- SheetDescription,
131
+ SheetTrigger,
124
132
  }
@@ -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
  import { buttonVariants } from "./button"
@@ -58,7 +58,12 @@ function SidePanel({
58
58
  data-slot="side-panel"
59
59
  data-state="closed"
60
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)}
61
+ className={cn(
62
+ "flex w-10 shrink-0 flex-col items-center bg-sidebar py-2 text-sidebar-foreground",
63
+ border,
64
+ "border-sidebar-border",
65
+ className,
66
+ )}
62
67
  {...props}
63
68
  >
64
69
  <Tooltip>
@@ -77,7 +82,10 @@ function SidePanel({
77
82
  <TooltipContent side={side === "left" ? "right" : "left"}>
78
83
  <span>Show {title}</span>
79
84
  {shortcut ? (
80
- <kbd data-slot="kbd" className="rounded-sm bg-background/20 px-1 font-sans text-[11px]">
85
+ <kbd
86
+ data-slot="kbd"
87
+ className="rounded-sm bg-background/20 px-1 font-sans text-[11px]"
88
+ >
81
89
  {shortcut}
82
90
  </kbd>
83
91
  ) : null}
@@ -121,10 +129,16 @@ function SidePanel({
121
129
  data-state="open"
122
130
  aria-label={title}
123
131
  style={{ width: w }}
124
- className={cn("relative flex shrink-0 flex-col overflow-hidden bg-sidebar text-sidebar-foreground", border, "border-sidebar-border", className)}
132
+ className={cn(
133
+ "relative flex shrink-0 flex-col overflow-hidden bg-sidebar text-sidebar-foreground",
134
+ border,
135
+ "border-sidebar-border",
136
+ className,
137
+ )}
125
138
  {...props}
126
139
  >
127
140
  {children}
141
+ {/* biome-ignore lint/a11y/useSemanticElements: a focusable, keyboard-operable splitter (WAI-ARIA window splitter); <hr> is not focusable */}
128
142
  <div
129
143
  role="separator"
130
144
  aria-orientation="vertical"
@@ -137,7 +151,7 @@ function SidePanel({
137
151
  onKeyDown={onKey}
138
152
  className={cn(
139
153
  "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"
154
+ side === "left" ? "right-0" : "left-0",
141
155
  )}
142
156
  />
143
157
  </aside>
@@ -149,7 +163,10 @@ function SidePanelHeader({ className, children, ...props }: React.ComponentProps
149
163
  return (
150
164
  <div
151
165
  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)}
166
+ className={cn(
167
+ "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",
168
+ className,
169
+ )}
153
170
  {...props}
154
171
  >
155
172
  {children}
@@ -159,8 +176,14 @@ function SidePanelHeader({ className, children, ...props }: React.ComponentProps
159
176
 
160
177
  /** The scrolling body under the header. */
161
178
  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} />
179
+ return (
180
+ <div
181
+ data-slot="side-panel-content"
182
+ className={cn("min-h-0 flex-1 overflow-auto", className)}
183
+ {...props}
184
+ />
185
+ )
163
186
  }
164
187
 
165
- export { SidePanel, SidePanelContent, SidePanelHeader, clampWidth }
166
188
  export type { SidePanelProps }
189
+ export { clampWidth, SidePanel, SidePanelContent, SidePanelHeader }
@@ -1,29 +1,22 @@
1
1
  "use client"
2
2
 
3
- import * as React from "react"
4
3
  import { useRender } from "@base-ui/react/use-render"
5
- import { cva, type VariantProps } from "class-variance-authority"
6
4
  import { SidebarSimpleIcon } from "@phosphor-icons/react"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import * as React from "react"
7
7
 
8
8
  import { useIsMobile } from "../lib/use-mobile"
9
9
  import { cn } from "../lib/utils"
10
10
  import { Button } from "./button"
11
11
  import { Input } from "./input"
12
12
  import { Separator } from "./separator"
13
- import {
14
- Sheet,
15
- SheetContent,
16
- SheetDescription,
17
- SheetHeader,
18
- SheetTitle,
19
- } from "./sheet"
13
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet"
20
14
  import { Skeleton } from "./skeleton"
21
- import {
22
- Tooltip,
23
- TooltipContent,
24
- TooltipProvider,
25
- TooltipTrigger,
26
- } from "./tooltip"
15
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
16
+
17
+ // Sidebar: shadcn's sidebar with `asChild` replaced by `render`, and `useIsMobile` kept in
18
+ // the kit. Only the `collapsible="none"` form is exercised in the playground; the offcanvas
19
+ // and icon forms are untested here.
27
20
 
28
21
  // Base UI's replacement for Radix's asChild: pass `render={<a href="..." />}`
29
22
  // to swap the element, props and ref are merged onto it.
@@ -93,6 +86,7 @@ function SidebarProvider({
93
86
  }
94
87
 
95
88
  // This sets the cookie to keep the sidebar state.
89
+ // biome-ignore lint/suspicious/noDocumentCookie: the Cookie Store API is not in every webview, and the cookie is written for hosts that read it server-side
96
90
  document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
97
91
  },
98
92
  [setOpenProp, open],
@@ -101,15 +95,12 @@ function SidebarProvider({
101
95
  // Helper to toggle the sidebar.
102
96
  const toggleSidebar = React.useCallback(() => {
103
97
  return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
104
- }, [isMobile, setOpen, setOpenMobile])
98
+ }, [isMobile, setOpen])
105
99
 
106
100
  // Adds a keyboard shortcut to toggle the sidebar.
107
101
  React.useEffect(() => {
108
102
  const handleKeyDown = (event: KeyboardEvent) => {
109
- if (
110
- event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
111
- (event.metaKey || event.ctrlKey)
112
- ) {
103
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
113
104
  event.preventDefault()
114
105
  toggleSidebar()
115
106
  }
@@ -133,7 +124,7 @@ function SidebarProvider({
133
124
  setOpenMobile,
134
125
  toggleSidebar,
135
126
  }),
136
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
127
+ [state, open, setOpen, isMobile, openMobile, toggleSidebar],
137
128
  )
138
129
 
139
130
  return (
@@ -263,11 +254,7 @@ function Sidebar({
263
254
  )
264
255
  }
265
256
 
266
- function SidebarTrigger({
267
- className,
268
- onClick,
269
- ...props
270
- }: React.ComponentProps<typeof Button>) {
257
+ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
271
258
  const { toggleSidebar } = useSidebar()
272
259
 
273
260
  return (
@@ -328,10 +315,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
328
315
  )
329
316
  }
330
317
 
331
- function SidebarInput({
332
- className,
333
- ...props
334
- }: React.ComponentProps<typeof Input>) {
318
+ function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
335
319
  return (
336
320
  <Input
337
321
  data-slot="sidebar-input"
@@ -364,10 +348,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
364
348
  )
365
349
  }
366
350
 
367
- function SidebarSeparator({
368
- className,
369
- ...props
370
- }: React.ComponentProps<typeof Separator>) {
351
+ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
371
352
  return (
372
353
  <Separator
373
354
  data-slot="sidebar-separator"
@@ -447,10 +428,7 @@ function SidebarGroupAction({
447
428
  )
448
429
  }
449
430
 
450
- function SidebarGroupContent({
451
- className,
452
- ...props
453
- }: React.ComponentProps<"div">) {
431
+ function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
454
432
  return (
455
433
  <div
456
434
  data-slot="sidebar-group-content"
@@ -588,10 +566,7 @@ function SidebarMenuAction({
588
566
  )
589
567
  }
590
568
 
591
- function SidebarMenuBadge({
592
- className,
593
- ...props
594
- }: React.ComponentProps<"div">) {
569
+ function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
595
570
  return (
596
571
  <div
597
572
  data-slot="sidebar-menu-badge"
@@ -627,12 +602,7 @@ function SidebarMenuSkeleton({
627
602
  className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
628
603
  {...props}
629
604
  >
630
- {showIcon && (
631
- <Skeleton
632
- className="size-4 rounded-md"
633
- data-sidebar="menu-skeleton-icon"
634
- />
635
- )}
605
+ {showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
636
606
  <Skeleton
637
607
  className="h-4 max-w-(--skeleton-width) flex-1"
638
608
  data-sidebar="menu-skeleton-text"
@@ -661,10 +631,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
661
631
  )
662
632
  }
663
633
 
664
- function SidebarMenuSubItem({
665
- className,
666
- ...props
667
- }: React.ComponentProps<"li">) {
634
+ function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
668
635
  return (
669
636
  <li
670
637
  data-slot="sidebar-menu-sub-item"
@@ -1,4 +1,4 @@
1
- import * as React from "react"
1
+ import type * as React from "react"
2
2
  import { cn } from "../lib/utils"
3
3
 
4
4
  function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
@@ -1,5 +1,5 @@
1
- import * as React from "react"
2
1
  import { Slider as SliderPrimitive } from "@base-ui/react/slider"
2
+ import * as React from "react"
3
3
 
4
4
  import { cn } from "../lib/utils"
5
5
 
@@ -21,13 +21,8 @@ function Slider({
21
21
  const [live, setLive] = React.useState<typeof valueProp | null>(null)
22
22
  const value = live ?? valueProp
23
23
  const _values = React.useMemo(
24
- () =>
25
- Array.isArray(value)
26
- ? value
27
- : Array.isArray(defaultValue)
28
- ? defaultValue
29
- : [min],
30
- [value, defaultValue, min]
24
+ () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min]),
25
+ [value, defaultValue, min],
31
26
  )
32
27
 
33
28
  return (
@@ -62,6 +57,7 @@ function Slider({
62
57
  {Array.from({ length: _values.length }, (_, index) => (
63
58
  <SliderPrimitive.Thumb
64
59
  data-slot="slider-thumb"
60
+ // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional; the count is fixed by the value array and they have no identity
65
61
  key={index}
66
62
  index={index}
67
63
  className="block h-4 w-2 shrink-0 rounded-[1px] border border-muted-foreground bg-background ring-ring/50 transition-[color,box-shadow] select-none hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden has-[:focus-visible]:ring-4 data-disabled:pointer-events-none data-disabled:opacity-50"
@@ -1,11 +1,12 @@
1
- import * as React from "react"
2
1
  import { Toast } from "@base-ui/react/toast"
3
2
  import { CheckCircleIcon, WarningIcon, XCircleIcon, XIcon } from "@phosphor-icons/react"
3
+ import type * as React from "react"
4
4
 
5
5
  import { cn } from "../lib/utils"
6
6
 
7
- // The kit's one toast: Strata's snackbar, on Base UI Toast.
8
- // One manager for the module, so `notify()` works outside React. Mount one provider per window.
7
+ // The kit's one toast: Strata's snackbar, on Base UI Toast. Kinds: plain, success, warning, error.
8
+ // One manager for the module, so `notify()` works outside React. Mount one `SnackbarProvider` per
9
+ // window, with a `SnackbarViewport` inside it.
9
10
  const manager = Toast.createToastManager()
10
11
 
11
12
  export function SnackbarProvider({ children }: { children: React.ReactNode }) {
@@ -17,7 +18,7 @@ export function SnackbarViewport({ className }: { className?: string }) {
17
18
  <Toast.Viewport
18
19
  className={cn(
19
20
  "pointer-events-none absolute bottom-3 left-3 z-50 flex w-72 flex-col gap-2 outline-none",
20
- className
21
+ className,
21
22
  )}
22
23
  >
23
24
  <SnackbarList />
@@ -52,40 +53,40 @@ function SnackbarList() {
52
53
  {toasts.map((t) => {
53
54
  const kind = KIND[(t.type as SnackbarKind | undefined) ?? "plain"] ?? KIND.plain
54
55
  return (
55
- <Toast.Root
56
- key={t.id}
57
- toast={t}
58
- className={cn(
59
- "pointer-events-auto flex items-start gap-2 rounded-sm border border-border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md outline-none",
60
- "transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
61
- kind.root
62
- )}
63
- >
64
- {kind.icon}
65
- <div className="min-w-0 flex-1">
66
- <Toast.Title className="font-medium">{t.title}</Toast.Title>
67
- {t.description && (
68
- <Toast.Description className="mt-0.5 opacity-80">{t.description}</Toast.Description>
69
- )}
70
- </div>
71
- <Toast.Action
72
- className={cn(
73
- "inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium",
74
- "text-current/80 hover:bg-foreground/10 hover:text-current",
75
- "focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
76
- )}
77
- />
78
- <Toast.Close
79
- aria-label="Dismiss"
56
+ <Toast.Root
57
+ key={t.id}
58
+ toast={t}
80
59
  className={cn(
81
- "inline-flex size-4 items-center justify-center rounded-sm",
82
- "opacity-60 hover:bg-foreground/10 hover:opacity-100",
83
- "focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
60
+ "pointer-events-auto flex items-start gap-2 rounded-sm border border-border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md outline-none",
61
+ "transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
62
+ kind.root,
84
63
  )}
85
64
  >
86
- <XIcon weight="bold" className="size-3" />
87
- </Toast.Close>
88
- </Toast.Root>
65
+ {kind.icon}
66
+ <div className="min-w-0 flex-1">
67
+ <Toast.Title className="font-medium">{t.title}</Toast.Title>
68
+ {t.description && (
69
+ <Toast.Description className="mt-0.5 opacity-80">{t.description}</Toast.Description>
70
+ )}
71
+ </div>
72
+ <Toast.Action
73
+ className={cn(
74
+ "inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium",
75
+ "text-current/80 hover:bg-foreground/10 hover:text-current",
76
+ "focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
77
+ )}
78
+ />
79
+ <Toast.Close
80
+ aria-label="Dismiss"
81
+ className={cn(
82
+ "inline-flex size-4 items-center justify-center rounded-sm",
83
+ "opacity-60 hover:bg-foreground/10 hover:opacity-100",
84
+ "focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
85
+ )}
86
+ >
87
+ <XIcon weight="bold" className="size-3" />
88
+ </Toast.Close>
89
+ </Toast.Root>
89
90
  )
90
91
  })}
91
92
  </>
@@ -113,7 +114,9 @@ const show = (m: Manager, opts: SnackbarOptions) =>
113
114
  description: opts.description,
114
115
  type: opts.kind ?? "plain",
115
116
  timeout: opts.persistent ? 0 : (opts.timeout ?? 5000),
116
- actionProps: opts.action ? { children: opts.action.label, onClick: opts.action.onClick } : undefined,
117
+ actionProps: opts.action
118
+ ? { children: opts.action.label, onClick: opts.action.onClick }
119
+ : undefined,
117
120
  })
118
121
 
119
122
  // Must be called under a SnackbarProvider.