@preset.nz/ux-kit 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +117 -2
- package/THIRD_PARTY_NOTICES.md +253 -0
- package/package.json +50 -3
- 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/avatar.tsx +35 -0
- package/src/components/badge.tsx +40 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/button.tsx +56 -0
- package/src/components/card.tsx +75 -0
- package/src/components/checkbox.tsx +25 -0
- package/src/components/color-field.tsx +261 -0
- package/src/components/color-swatch.tsx +52 -0
- package/src/components/context-menu.tsx +249 -0
- package/src/components/dialog.tsx +133 -0
- package/src/components/dropdown-menu.tsx +249 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/input.tsx +20 -0
- package/src/components/label.tsx +19 -0
- package/src/components/navigation-menu.tsx +150 -0
- package/src/components/number-field.tsx +432 -0
- package/src/components/popover.tsx +75 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +132 -0
- package/src/components/side-panel.tsx +189 -0
- package/src/components/sidebar.tsx +698 -0
- package/src/components/skeleton.tsx +14 -0
- package/src/components/slider.tsx +71 -0
- package/src/components/snackbar.tsx +143 -0
- package/src/components/status-bar.tsx +45 -0
- package/src/components/tabs.tsx +75 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +86 -0
- package/src/components/toggle.tsx +43 -0
- package/src/components/toolbar.tsx +304 -0
- package/src/components/tooltip.tsx +54 -0
- package/src/graphics/adsr.ts +78 -0
- package/src/graphics/curve-editor.tsx +567 -0
- package/src/graphics/curve-edits.ts +146 -0
- package/src/graphics/index.ts +16 -0
- package/src/graphics/scale.ts +25 -0
- package/src/graphics/use-canvas.ts +56 -0
- package/src/graphics/use-tokens.ts +35 -0
- package/src/icons.ts +3 -0
- package/src/index.css +199 -0
- package/src/index.ts +35 -0
- package/src/lib/use-mobile.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/shadcn-tailwind.css +665 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
|
4
|
+
import { CaretDownIcon, CaretUpIcon, CheckIcon } from "@phosphor-icons/react"
|
|
5
|
+
import type * as React from "react"
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
const Select = SelectPrimitive.Root
|
|
9
|
+
|
|
10
|
+
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
|
11
|
+
return (
|
|
12
|
+
<SelectPrimitive.Group
|
|
13
|
+
data-slot="select-group"
|
|
14
|
+
className={cn("scroll-my-1", className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
|
21
|
+
return (
|
|
22
|
+
<SelectPrimitive.Value
|
|
23
|
+
data-slot="select-value"
|
|
24
|
+
className={cn("flex flex-1 text-left", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function SelectTrigger({
|
|
31
|
+
className,
|
|
32
|
+
size = "default",
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: SelectPrimitive.Trigger.Props & {
|
|
36
|
+
size?: "sm" | "default"
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<SelectPrimitive.Trigger
|
|
40
|
+
data-slot="select-trigger"
|
|
41
|
+
data-size={size}
|
|
42
|
+
className={cn(
|
|
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",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
<SelectPrimitive.Icon
|
|
50
|
+
render={<CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />}
|
|
51
|
+
/>
|
|
52
|
+
</SelectPrimitive.Trigger>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function SelectContent({
|
|
57
|
+
className,
|
|
58
|
+
children,
|
|
59
|
+
side = "bottom",
|
|
60
|
+
sideOffset = 4,
|
|
61
|
+
align = "center",
|
|
62
|
+
alignOffset = 0,
|
|
63
|
+
alignItemWithTrigger = true,
|
|
64
|
+
...props
|
|
65
|
+
}: SelectPrimitive.Popup.Props &
|
|
66
|
+
Pick<
|
|
67
|
+
SelectPrimitive.Positioner.Props,
|
|
68
|
+
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
|
69
|
+
>) {
|
|
70
|
+
return (
|
|
71
|
+
<SelectPrimitive.Portal>
|
|
72
|
+
<SelectPrimitive.Positioner
|
|
73
|
+
side={side}
|
|
74
|
+
sideOffset={sideOffset}
|
|
75
|
+
align={align}
|
|
76
|
+
alignOffset={alignOffset}
|
|
77
|
+
alignItemWithTrigger={alignItemWithTrigger}
|
|
78
|
+
className="isolate z-50"
|
|
79
|
+
>
|
|
80
|
+
<SelectPrimitive.Popup
|
|
81
|
+
data-slot="select-content"
|
|
82
|
+
data-align-trigger={alignItemWithTrigger}
|
|
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
|
+
{...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({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
|
|
99
|
+
return (
|
|
100
|
+
<SelectPrimitive.GroupLabel
|
|
101
|
+
data-slot="select-label"
|
|
102
|
+
className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
|
|
109
|
+
return (
|
|
110
|
+
<SelectPrimitive.Item
|
|
111
|
+
data-slot="select-item"
|
|
112
|
+
className={cn(
|
|
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",
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
|
119
|
+
{children}
|
|
120
|
+
</SelectPrimitive.ItemText>
|
|
121
|
+
<SelectPrimitive.ItemIndicator
|
|
122
|
+
render={
|
|
123
|
+
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
<CheckIcon className="pointer-events-none" />
|
|
127
|
+
</SelectPrimitive.ItemIndicator>
|
|
128
|
+
</SelectPrimitive.Item>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
|
|
133
|
+
return (
|
|
134
|
+
<SelectPrimitive.Separator
|
|
135
|
+
data-slot="select-separator"
|
|
136
|
+
className={cn("pointer-events-none -mx-1 h-px bg-border", className)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function SelectScrollUpButton({
|
|
143
|
+
className,
|
|
144
|
+
...props
|
|
145
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
|
146
|
+
return (
|
|
147
|
+
<SelectPrimitive.ScrollUpArrow
|
|
148
|
+
data-slot="select-scroll-up-button"
|
|
149
|
+
className={cn(
|
|
150
|
+
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
151
|
+
className,
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
<CaretUpIcon />
|
|
156
|
+
</SelectPrimitive.ScrollUpArrow>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function SelectScrollDownButton({
|
|
161
|
+
className,
|
|
162
|
+
...props
|
|
163
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
|
164
|
+
return (
|
|
165
|
+
<SelectPrimitive.ScrollDownArrow
|
|
166
|
+
data-slot="select-scroll-down-button"
|
|
167
|
+
className={cn(
|
|
168
|
+
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
169
|
+
className,
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<CaretDownIcon />
|
|
174
|
+
</SelectPrimitive.ScrollDownArrow>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
Select,
|
|
180
|
+
SelectContent,
|
|
181
|
+
SelectGroup,
|
|
182
|
+
SelectItem,
|
|
183
|
+
SelectLabel,
|
|
184
|
+
SelectScrollDownButton,
|
|
185
|
+
SelectScrollUpButton,
|
|
186
|
+
SelectSeparator,
|
|
187
|
+
SelectTrigger,
|
|
188
|
+
SelectValue,
|
|
189
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
|
|
8
|
+
return (
|
|
9
|
+
<SeparatorPrimitive
|
|
10
|
+
data-slot="separator"
|
|
11
|
+
orientation={orientation}
|
|
12
|
+
className={cn(
|
|
13
|
+
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Separator }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
|
2
|
+
import { XIcon } from "@phosphor-icons/react"
|
|
3
|
+
import type * as React from "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
|
|
86
|
+
data-slot="sheet-header"
|
|
87
|
+
className={cn("flex flex-col gap-1.5 p-4", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
data-slot="sheet-footer"
|
|
97
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
|
104
|
+
return (
|
|
105
|
+
<SheetPrimitive.Title
|
|
106
|
+
data-slot="sheet-title"
|
|
107
|
+
className={cn("font-semibold text-foreground", className)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
|
|
114
|
+
return (
|
|
115
|
+
<SheetPrimitive.Description
|
|
116
|
+
data-slot="sheet-description"
|
|
117
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
Sheet,
|
|
125
|
+
SheetClose,
|
|
126
|
+
SheetContent,
|
|
127
|
+
SheetDescription,
|
|
128
|
+
SheetFooter,
|
|
129
|
+
SheetHeader,
|
|
130
|
+
SheetTitle,
|
|
131
|
+
SheetTrigger,
|
|
132
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type * 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(
|
|
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
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
<Tooltip>
|
|
70
|
+
<TooltipTrigger
|
|
71
|
+
render={
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
aria-label={`Show ${title}`}
|
|
75
|
+
onClick={() => onOpenChange(true)}
|
|
76
|
+
className={buttonVariants({ variant: "ghost", size: "icon" })}
|
|
77
|
+
/>
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
{icon}
|
|
81
|
+
</TooltipTrigger>
|
|
82
|
+
<TooltipContent side={side === "left" ? "right" : "left"}>
|
|
83
|
+
<span>Show {title}</span>
|
|
84
|
+
{shortcut ? (
|
|
85
|
+
<kbd
|
|
86
|
+
data-slot="kbd"
|
|
87
|
+
className="rounded-sm bg-background/20 px-1 font-sans text-[11px]"
|
|
88
|
+
>
|
|
89
|
+
{shortcut}
|
|
90
|
+
</kbd>
|
|
91
|
+
) : null}
|
|
92
|
+
</TooltipContent>
|
|
93
|
+
</Tooltip>
|
|
94
|
+
</aside>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const startDrag = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
const el = e.currentTarget
|
|
101
|
+
el.setPointerCapture(e.pointerId)
|
|
102
|
+
const startX = e.clientX
|
|
103
|
+
const startWidth = width
|
|
104
|
+
const sign = side === "left" ? 1 : -1
|
|
105
|
+
const move = (ev: PointerEvent) =>
|
|
106
|
+
onWidthChange(clampWidth(startWidth + sign * (ev.clientX - startX), minWidth, maxWidth))
|
|
107
|
+
const end = () => {
|
|
108
|
+
el.removeEventListener("pointermove", move)
|
|
109
|
+
el.removeEventListener("pointerup", end)
|
|
110
|
+
el.removeEventListener("pointercancel", end)
|
|
111
|
+
}
|
|
112
|
+
el.addEventListener("pointermove", move)
|
|
113
|
+
el.addEventListener("pointerup", end)
|
|
114
|
+
el.addEventListener("pointercancel", end)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const onKey = (e: React.KeyboardEvent) => {
|
|
118
|
+
const grow = side === "left" ? "ArrowRight" : "ArrowLeft"
|
|
119
|
+
const shrink = side === "left" ? "ArrowLeft" : "ArrowRight"
|
|
120
|
+
if (e.key !== grow && e.key !== shrink) return
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
onWidthChange(clampWidth(width + (e.key === grow ? 16 : -16), minWidth, maxWidth))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const w = clampWidth(width, minWidth, maxWidth)
|
|
126
|
+
return (
|
|
127
|
+
<aside
|
|
128
|
+
data-slot="side-panel"
|
|
129
|
+
data-state="open"
|
|
130
|
+
aria-label={title}
|
|
131
|
+
style={{ width: w }}
|
|
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
|
+
)}
|
|
138
|
+
{...props}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
{/* biome-ignore lint/a11y/useSemanticElements: a focusable, keyboard-operable splitter (WAI-ARIA window splitter); <hr> is not focusable */}
|
|
142
|
+
<div
|
|
143
|
+
role="separator"
|
|
144
|
+
aria-orientation="vertical"
|
|
145
|
+
aria-label={`Resize ${title}`}
|
|
146
|
+
aria-valuenow={w}
|
|
147
|
+
aria-valuemin={minWidth}
|
|
148
|
+
aria-valuemax={maxWidth}
|
|
149
|
+
tabIndex={0}
|
|
150
|
+
onPointerDown={startDrag}
|
|
151
|
+
onKeyDown={onKey}
|
|
152
|
+
className={cn(
|
|
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",
|
|
154
|
+
side === "left" ? "right-0" : "left-0",
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
</aside>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A panel's title row; put the panel's own actions in `children`. */
|
|
162
|
+
function SidePanelHeader({ className, children, ...props }: React.ComponentProps<"div">) {
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
data-slot="side-panel-header"
|
|
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
|
+
)}
|
|
170
|
+
{...props}
|
|
171
|
+
>
|
|
172
|
+
{children}
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The scrolling body under the header. */
|
|
178
|
+
function SidePanelContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
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
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type { SidePanelProps }
|
|
189
|
+
export { clampWidth, SidePanel, SidePanelContent, SidePanelHeader }
|