@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,7 +1,7 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
1
|
import { mergeProps } from "@base-ui/react/merge-props"
|
|
3
2
|
import { useRender } from "@base-ui/react/use-render"
|
|
4
3
|
import { CaretRightIcon, DotsThreeIcon } from "@phosphor-icons/react"
|
|
4
|
+
import type * as React from "react"
|
|
5
5
|
|
|
6
6
|
import { cn } from "../lib/utils"
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
|
15
15
|
data-slot="breadcrumb-list"
|
|
16
16
|
className={cn(
|
|
17
17
|
"flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
|
|
18
|
-
className
|
|
18
|
+
className,
|
|
19
19
|
)}
|
|
20
20
|
{...props}
|
|
21
21
|
/>
|
|
@@ -33,16 +33,12 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Plain anchor by default. Pass `render={<RouterLink to="..." />}` for a router link.
|
|
36
|
-
function BreadcrumbLink({
|
|
37
|
-
className,
|
|
38
|
-
render,
|
|
39
|
-
...props
|
|
40
|
-
}: useRender.ComponentProps<"a">) {
|
|
36
|
+
function BreadcrumbLink({ className, render, ...props }: useRender.ComponentProps<"a">) {
|
|
41
37
|
return useRender({
|
|
42
38
|
defaultTagName: "a",
|
|
43
39
|
props: mergeProps<"a">(
|
|
44
40
|
{ className: cn("transition-colors hover:text-foreground", className) },
|
|
45
|
-
props
|
|
41
|
+
props,
|
|
46
42
|
),
|
|
47
43
|
render,
|
|
48
44
|
state: { slot: "breadcrumb-link" },
|
|
@@ -51,6 +47,8 @@ function BreadcrumbLink({
|
|
|
51
47
|
|
|
52
48
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
53
49
|
return (
|
|
50
|
+
// biome-ignore lint/a11y/useFocusableInteractive: the current page is a disabled link (WAI-ARIA breadcrumb pattern), so it is not focusable
|
|
51
|
+
// biome-ignore lint/a11y/useSemanticElements: a disabled link has no native element; role="link" with aria-disabled is the breadcrumb pattern
|
|
54
52
|
<span
|
|
55
53
|
data-slot="breadcrumb-page"
|
|
56
54
|
role="link"
|
|
@@ -93,10 +91,10 @@ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span"
|
|
|
93
91
|
|
|
94
92
|
export {
|
|
95
93
|
Breadcrumb,
|
|
96
|
-
|
|
94
|
+
BreadcrumbEllipsis,
|
|
97
95
|
BreadcrumbItem,
|
|
98
96
|
BreadcrumbLink,
|
|
97
|
+
BreadcrumbList,
|
|
99
98
|
BreadcrumbPage,
|
|
100
99
|
BreadcrumbSeparator,
|
|
101
|
-
BreadcrumbEllipsis,
|
|
102
100
|
}
|
package/src/components/card.tsx
CHANGED
|
@@ -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
|
|
|
@@ -52,23 +52,14 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
52
52
|
return (
|
|
53
53
|
<div
|
|
54
54
|
data-slot="card-action"
|
|
55
|
-
className={cn(
|
|
56
|
-
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
-
className,
|
|
58
|
-
)}
|
|
55
|
+
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
|
59
56
|
{...props}
|
|
60
57
|
/>
|
|
61
58
|
)
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
-
return (
|
|
66
|
-
<div
|
|
67
|
-
data-slot="card-content"
|
|
68
|
-
className={cn("px-4", className)}
|
|
69
|
-
{...props}
|
|
70
|
-
/>
|
|
71
|
-
)
|
|
62
|
+
return <div data-slot="card-content" className={cn("px-4", className)} {...props} />
|
|
72
63
|
}
|
|
73
64
|
|
|
74
65
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
@@ -81,12 +72,4 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
81
72
|
)
|
|
82
73
|
}
|
|
83
74
|
|
|
84
|
-
export {
|
|
85
|
-
Card,
|
|
86
|
-
CardHeader,
|
|
87
|
-
CardFooter,
|
|
88
|
-
CardTitle,
|
|
89
|
-
CardAction,
|
|
90
|
-
CardDescription,
|
|
91
|
-
CardContent,
|
|
92
|
-
}
|
|
75
|
+
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
|
2
|
-
|
|
3
|
-
import { cn } from "../lib/utils"
|
|
4
2
|
import { CheckIcon } from "@phosphor-icons/react"
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
5
4
|
|
|
6
5
|
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
7
6
|
return (
|
|
@@ -9,7 +8,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
|
9
8
|
data-slot="checkbox"
|
|
10
9
|
className={cn(
|
|
11
10
|
"peer relative flex size-4 shrink-0 items-center justify-center rounded-none border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 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 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
|
12
|
-
className
|
|
11
|
+
className,
|
|
13
12
|
)}
|
|
14
13
|
{...props}
|
|
15
14
|
>
|
|
@@ -17,8 +16,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
|
17
16
|
data-slot="checkbox-indicator"
|
|
18
17
|
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
|
19
18
|
>
|
|
20
|
-
<CheckIcon
|
|
21
|
-
/>
|
|
19
|
+
<CheckIcon />
|
|
22
20
|
</CheckboxPrimitive.Indicator>
|
|
23
21
|
</CheckboxPrimitive.Root>
|
|
24
22
|
)
|
|
@@ -35,8 +35,7 @@ function toHex({ r, g, b, a }: Rgba, withAlpha: boolean): string {
|
|
|
35
35
|
return `#${p(r)}${p(g)}${p(b)}${withAlpha ? p(a * 255) : ""}`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
interface ColorFieldProps
|
|
39
|
-
extends Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> {
|
|
38
|
+
interface ColorFieldProps extends Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> {
|
|
40
39
|
/** #rrggbb or #rrggbbaa. `null` shows the empty chip and a dash. */
|
|
41
40
|
value: string | null
|
|
42
41
|
/** Called with a normalised lowercase hex, or null from the clear button. */
|
|
@@ -109,7 +108,9 @@ function ColorField({
|
|
|
109
108
|
const pct = Number(alphaDraft)
|
|
110
109
|
setAlphaDraft(null)
|
|
111
110
|
if (!Number.isFinite(pct)) return
|
|
112
|
-
|
|
111
|
+
const base = rgba ?? parseHex(fallback)
|
|
112
|
+
if (!base) return
|
|
113
|
+
emit({ ...base, a: Math.min(100, Math.max(0, pct)) / 100 })
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
const pick = (hex: string, session = false) => {
|
|
@@ -126,7 +127,10 @@ function ColorField({
|
|
|
126
127
|
return (
|
|
127
128
|
<div data-slot="color-field" className={cn("flex flex-col gap-1", className)} {...props}>
|
|
128
129
|
{label ? (
|
|
129
|
-
<label
|
|
130
|
+
<label
|
|
131
|
+
htmlFor={inputId}
|
|
132
|
+
className="text-[11px] font-medium tracking-wide text-muted-foreground"
|
|
133
|
+
>
|
|
130
134
|
{label}
|
|
131
135
|
</label>
|
|
132
136
|
) : null}
|
|
@@ -143,7 +147,11 @@ function ColorField({
|
|
|
143
147
|
<input
|
|
144
148
|
type="color"
|
|
145
149
|
aria-label={`${swatchLabel} picker`}
|
|
146
|
-
value={
|
|
150
|
+
value={
|
|
151
|
+
rgba
|
|
152
|
+
? toHex(rgba, false)
|
|
153
|
+
: toHex(parseHex(fallback) ?? { r: 0, g: 0, b: 0, a: 1 }, false)
|
|
154
|
+
}
|
|
147
155
|
onChange={(e) => pick(e.target.value, true)}
|
|
148
156
|
onFocus={onPickStart}
|
|
149
157
|
onBlur={() => {
|
|
@@ -163,6 +171,7 @@ function ColorField({
|
|
|
163
171
|
</PopoverTrigger>
|
|
164
172
|
<PopoverContent align="start" className="w-auto gap-2 p-2">
|
|
165
173
|
{presets?.length || clearable ? (
|
|
174
|
+
// biome-ignore lint/a11y/useSemanticElements: a div keeps the swatch grid layout; role="group" labels it
|
|
166
175
|
<div className="grid grid-cols-7 gap-1" role="group" aria-label="Presets">
|
|
167
176
|
{presets?.map((c) => (
|
|
168
177
|
<button
|
|
@@ -191,7 +200,11 @@ function ColorField({
|
|
|
191
200
|
<input
|
|
192
201
|
type="color"
|
|
193
202
|
aria-label={`${swatchLabel} picker`}
|
|
194
|
-
value={
|
|
203
|
+
value={
|
|
204
|
+
rgba
|
|
205
|
+
? toHex(rgba, false)
|
|
206
|
+
: toHex(parseHex(fallback) ?? { r: 0, g: 0, b: 0, a: 1 }, false)
|
|
207
|
+
}
|
|
195
208
|
onChange={(e) => pick(e.target.value, true)}
|
|
196
209
|
onFocus={onPickStart}
|
|
197
210
|
onBlur={() => {
|
|
@@ -244,5 +257,5 @@ function ColorField({
|
|
|
244
257
|
)
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
export { ColorField, parseHex, toHex }
|
|
248
260
|
export type { ColorFieldProps }
|
|
261
|
+
export { ColorField, parseHex, toHex }
|
|
@@ -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
|
|
|
@@ -38,7 +38,7 @@ function ColorSwatch({
|
|
|
38
38
|
size === "lg" && "size-6",
|
|
39
39
|
shape === "round" ? "rounded-full" : "rounded-xs",
|
|
40
40
|
!color && "border-dashed border-muted-foreground/60",
|
|
41
|
-
className
|
|
41
|
+
className,
|
|
42
42
|
)}
|
|
43
43
|
style={color ? { background: CHECKER, ...style } : style}
|
|
44
44
|
{...props}
|
|
@@ -48,5 +48,5 @@ function ColorSwatch({
|
|
|
48
48
|
)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export { ColorSwatch }
|
|
52
51
|
export type { ColorSwatchProps }
|
|
52
|
+
export { ColorSwatch }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { AlertShell } from "./alert-shell"
|
|
3
|
+
import { Button } from "./button"
|
|
4
|
+
|
|
5
|
+
// ConfirmDialog: ask before something that can't be taken back. Controlled
|
|
6
|
+
// only; the caller owns `open`. It is answered, not dismissed: no close "x",
|
|
7
|
+
// and a click outside does nothing, so Cancel and Escape are the ways out.
|
|
8
|
+
|
|
9
|
+
interface ConfirmDialogProps {
|
|
10
|
+
open: boolean
|
|
11
|
+
onOpenChange: (open: boolean) => void
|
|
12
|
+
title: string
|
|
13
|
+
/** The body: text, or a node (a list of apps, a size). */
|
|
14
|
+
children?: React.ReactNode
|
|
15
|
+
/** What the action is called: "Remove", never "OK". */
|
|
16
|
+
confirmLabel: string
|
|
17
|
+
/** Defaults to "Cancel". */
|
|
18
|
+
cancelLabel?: string
|
|
19
|
+
/** Confirm uses the destructive Button variant and initial focus is Cancel. */
|
|
20
|
+
destructive?: boolean
|
|
21
|
+
/** While a returned promise is pending the dialog stays open; a rejection keeps it open. */
|
|
22
|
+
onConfirm: () => void | Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ConfirmDialog({
|
|
26
|
+
open,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
title,
|
|
29
|
+
children,
|
|
30
|
+
confirmLabel,
|
|
31
|
+
cancelLabel = "Cancel",
|
|
32
|
+
destructive = false,
|
|
33
|
+
onConfirm,
|
|
34
|
+
}: ConfirmDialogProps) {
|
|
35
|
+
const [pending, setPending] = React.useState(false)
|
|
36
|
+
const cancelRef = React.useRef<HTMLButtonElement>(null)
|
|
37
|
+
const confirmRef = React.useRef<HTMLButtonElement>(null)
|
|
38
|
+
|
|
39
|
+
// Nothing closes the dialog while the action runs.
|
|
40
|
+
const handleOpenChange = (next: boolean) => {
|
|
41
|
+
if (pending && !next) return
|
|
42
|
+
onOpenChange(next)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handleConfirm = async () => {
|
|
46
|
+
if (pending) return
|
|
47
|
+
setPending(true)
|
|
48
|
+
try {
|
|
49
|
+
await onConfirm()
|
|
50
|
+
} catch {
|
|
51
|
+
// The caller shows its own failure; the dialog stays open.
|
|
52
|
+
setPending(false)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
setPending(false)
|
|
56
|
+
onOpenChange(false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<AlertShell
|
|
61
|
+
open={open}
|
|
62
|
+
onDismiss={() => handleOpenChange(false)}
|
|
63
|
+
title={title}
|
|
64
|
+
initialFocus={destructive ? cancelRef : confirmRef}
|
|
65
|
+
footer={
|
|
66
|
+
<>
|
|
67
|
+
<Button
|
|
68
|
+
ref={cancelRef}
|
|
69
|
+
variant="outline"
|
|
70
|
+
disabled={pending}
|
|
71
|
+
onClick={() => handleOpenChange(false)}
|
|
72
|
+
>
|
|
73
|
+
{cancelLabel}
|
|
74
|
+
</Button>
|
|
75
|
+
<Button
|
|
76
|
+
ref={confirmRef}
|
|
77
|
+
variant={destructive ? "destructive" : "default"}
|
|
78
|
+
disabled={pending}
|
|
79
|
+
focusableWhenDisabled
|
|
80
|
+
onClick={handleConfirm}
|
|
81
|
+
>
|
|
82
|
+
{confirmLabel}
|
|
83
|
+
</Button>
|
|
84
|
+
</>
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</AlertShell>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { ConfirmDialogProps }
|
|
93
|
+
export { ConfirmDialog }
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
1
|
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
|
|
3
2
|
import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react"
|
|
3
|
+
import type * as React from "react"
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/utils"
|
|
6
6
|
|
|
7
|
+
// Base UI: `ContextMenuLabel` is Base UI's `GroupLabel`, so it must sit inside a `ContextMenuGroup`.
|
|
8
|
+
// Same shape as DropdownMenu.
|
|
9
|
+
|
|
7
10
|
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
|
|
8
11
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
|
9
12
|
}
|
|
@@ -44,7 +47,7 @@ function ContextMenuContent({
|
|
|
44
47
|
data-slot="context-menu-content"
|
|
45
48
|
className={cn(
|
|
46
49
|
"z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none 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-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",
|
|
47
|
-
className
|
|
50
|
+
className,
|
|
48
51
|
)}
|
|
49
52
|
{...props}
|
|
50
53
|
/>
|
|
@@ -68,7 +71,7 @@ function ContextMenuLabel({
|
|
|
68
71
|
data-inset={inset}
|
|
69
72
|
className={cn(
|
|
70
73
|
"px-2 py-1.5 text-xs font-medium text-muted-foreground data-inset:pl-8",
|
|
71
|
-
className
|
|
74
|
+
className,
|
|
72
75
|
)}
|
|
73
76
|
{...props}
|
|
74
77
|
/>
|
|
@@ -91,7 +94,7 @@ function ContextMenuItem({
|
|
|
91
94
|
data-variant={variant}
|
|
92
95
|
className={cn(
|
|
93
96
|
"group/context-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
|
94
|
-
className
|
|
97
|
+
className,
|
|
95
98
|
)}
|
|
96
99
|
{...props}
|
|
97
100
|
/>
|
|
@@ -114,7 +117,7 @@ function ContextMenuSubTrigger({
|
|
|
114
117
|
data-inset={inset}
|
|
115
118
|
className={cn(
|
|
116
119
|
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
117
|
-
className
|
|
120
|
+
className,
|
|
118
121
|
)}
|
|
119
122
|
{...props}
|
|
120
123
|
>
|
|
@@ -157,7 +160,7 @@ function ContextMenuCheckboxItem({
|
|
|
157
160
|
data-inset={inset}
|
|
158
161
|
className={cn(
|
|
159
162
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
160
|
-
className
|
|
163
|
+
className,
|
|
161
164
|
)}
|
|
162
165
|
{...props}
|
|
163
166
|
>
|
|
@@ -190,7 +193,7 @@ function ContextMenuRadioItem({
|
|
|
190
193
|
data-inset={inset}
|
|
191
194
|
className={cn(
|
|
192
195
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
193
|
-
className
|
|
196
|
+
className,
|
|
194
197
|
)}
|
|
195
198
|
{...props}
|
|
196
199
|
>
|
|
@@ -223,7 +226,7 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span
|
|
|
223
226
|
data-slot="context-menu-shortcut"
|
|
224
227
|
className={cn(
|
|
225
228
|
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
|
|
226
|
-
className
|
|
229
|
+
className,
|
|
227
230
|
)}
|
|
228
231
|
{...props}
|
|
229
232
|
/>
|
|
@@ -232,18 +235,18 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span
|
|
|
232
235
|
|
|
233
236
|
export {
|
|
234
237
|
ContextMenu,
|
|
235
|
-
|
|
236
|
-
ContextMenuTrigger,
|
|
238
|
+
ContextMenuCheckboxItem,
|
|
237
239
|
ContextMenuContent,
|
|
238
240
|
ContextMenuGroup,
|
|
239
|
-
ContextMenuLabel,
|
|
240
241
|
ContextMenuItem,
|
|
241
|
-
|
|
242
|
+
ContextMenuLabel,
|
|
243
|
+
ContextMenuPortal,
|
|
242
244
|
ContextMenuRadioGroup,
|
|
243
245
|
ContextMenuRadioItem,
|
|
244
246
|
ContextMenuSeparator,
|
|
245
247
|
ContextMenuShortcut,
|
|
246
248
|
ContextMenuSub,
|
|
247
|
-
ContextMenuSubTrigger,
|
|
248
249
|
ContextMenuSubContent,
|
|
250
|
+
ContextMenuSubTrigger,
|
|
251
|
+
ContextMenuTrigger,
|
|
249
252
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
1
|
import { Dialog as DialogPrimitive } 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"
|
|
7
7
|
|
|
8
|
+
// Dialog: Base UI Dialog in the kit's theme. Triggers and closers take
|
|
9
|
+
// `render={<Button />}` where Radix used `asChild`.
|
|
10
|
+
|
|
8
11
|
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
9
12
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
10
13
|
}
|
|
@@ -27,7 +30,7 @@ function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props)
|
|
|
27
30
|
data-slot="dialog-overlay"
|
|
28
31
|
className={cn(
|
|
29
32
|
"fixed inset-0 isolate z-50 bg-foreground/15 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
30
|
-
className
|
|
33
|
+
className,
|
|
31
34
|
)}
|
|
32
35
|
{...props}
|
|
33
36
|
/>
|
|
@@ -49,7 +52,7 @@ function DialogContent({
|
|
|
49
52
|
data-slot="dialog-content"
|
|
50
53
|
className={cn(
|
|
51
54
|
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md 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",
|
|
52
|
-
className
|
|
55
|
+
className,
|
|
53
56
|
)}
|
|
54
57
|
{...props}
|
|
55
58
|
>
|
|
@@ -70,11 +73,7 @@ function DialogContent({
|
|
|
70
73
|
|
|
71
74
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
72
75
|
return (
|
|
73
|
-
<div
|
|
74
|
-
data-slot="dialog-header"
|
|
75
|
-
className={cn("flex flex-col gap-2", className)}
|
|
76
|
-
{...props}
|
|
77
|
-
/>
|
|
76
|
+
<div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />
|
|
78
77
|
)
|
|
79
78
|
}
|
|
80
79
|
|
|
@@ -116,7 +115,7 @@ function DialogDescription({ className, ...props }: DialogPrimitive.Description.
|
|
|
116
115
|
data-slot="dialog-description"
|
|
117
116
|
className={cn(
|
|
118
117
|
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
|
119
|
-
className
|
|
118
|
+
className,
|
|
120
119
|
)}
|
|
121
120
|
{...props}
|
|
122
121
|
/>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { AlertShell } from "./alert-shell"
|
|
3
|
+
import { Button } from "./button"
|
|
4
|
+
import { formatBytes, Progress } from "./progress"
|
|
5
|
+
|
|
6
|
+
// DownloadPrompt: one dialog for a download, in three states the caller drives:
|
|
7
|
+
// ask first, show progress, offer Retry. It owns no timer and no network; the
|
|
8
|
+
// caller sets `state` to null when the download is done. Escape always means
|
|
9
|
+
// "stop": Not now, Cancel, Close.
|
|
10
|
+
|
|
11
|
+
type DownloadPromptProps = {
|
|
12
|
+
/** `null` is closed. */
|
|
13
|
+
state: "ask" | "downloading" | "failed" | null
|
|
14
|
+
/** "Smart Select needs a model". */
|
|
15
|
+
title: string
|
|
16
|
+
/** What is being downloaded: "MobileSAM". */
|
|
17
|
+
name: string
|
|
18
|
+
/** Formatted by the caller; an omitted fact is left out of the line. */
|
|
19
|
+
facts: { size?: string; licence?: string; host?: string }
|
|
20
|
+
/** Bytes, while downloading. Without it the bar is indeterminate. */
|
|
21
|
+
progress?: { done: number; total: number }
|
|
22
|
+
/** One line, when failed. */
|
|
23
|
+
reason?: string
|
|
24
|
+
/** ask: Download. */
|
|
25
|
+
onDownload: () => void
|
|
26
|
+
/** ask: Not now. failed: Close. */
|
|
27
|
+
onDismiss: () => void
|
|
28
|
+
/** downloading: Cancel. */
|
|
29
|
+
onCancel: () => void
|
|
30
|
+
/** failed: Retry. */
|
|
31
|
+
onRetry: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function DownloadPrompt(props: DownloadPromptProps) {
|
|
35
|
+
// Keep the last open props so the closing animation shows the old content, not a blank dialog.
|
|
36
|
+
const last = React.useRef<DownloadPromptProps & { state: "ask" | "downloading" | "failed" }>(null)
|
|
37
|
+
if (props.state !== null) last.current = { ...props, state: props.state }
|
|
38
|
+
const shown = last.current
|
|
39
|
+
const focusRef = React.useRef<HTMLButtonElement>(null)
|
|
40
|
+
|
|
41
|
+
// initialFocus only acts on open. A state change swaps the buttons out from under the focus,
|
|
42
|
+
// so move it to the new state's default button.
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (props.state !== null) focusRef.current?.focus()
|
|
45
|
+
}, [props.state])
|
|
46
|
+
|
|
47
|
+
if (shown === null) return null
|
|
48
|
+
const { state, title, name, facts, progress, reason } = shown
|
|
49
|
+
// Callbacks come from the live props: the stored copy may be a render old.
|
|
50
|
+
const live = props
|
|
51
|
+
const onEscape = state === "downloading" ? live.onCancel : live.onDismiss
|
|
52
|
+
|
|
53
|
+
const factLine = [facts.size, facts.licence, facts.host ? `from ${facts.host}` : undefined]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(" · ")
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<AlertShell
|
|
59
|
+
open={props.state !== null}
|
|
60
|
+
onDismiss={onEscape}
|
|
61
|
+
title={title}
|
|
62
|
+
initialFocus={focusRef}
|
|
63
|
+
footer={
|
|
64
|
+
state === "ask" ? (
|
|
65
|
+
<>
|
|
66
|
+
<Button variant="outline" onClick={live.onDismiss}>
|
|
67
|
+
Not now
|
|
68
|
+
</Button>
|
|
69
|
+
<Button ref={focusRef} onClick={live.onDownload}>
|
|
70
|
+
Download
|
|
71
|
+
</Button>
|
|
72
|
+
</>
|
|
73
|
+
) : state === "downloading" ? (
|
|
74
|
+
<Button ref={focusRef} variant="outline" onClick={live.onCancel}>
|
|
75
|
+
Cancel
|
|
76
|
+
</Button>
|
|
77
|
+
) : (
|
|
78
|
+
<>
|
|
79
|
+
<Button variant="outline" onClick={live.onDismiss}>
|
|
80
|
+
Close
|
|
81
|
+
</Button>
|
|
82
|
+
<Button ref={focusRef} onClick={live.onRetry}>
|
|
83
|
+
Retry
|
|
84
|
+
</Button>
|
|
85
|
+
</>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
<p className="font-medium text-foreground">{name}</p>
|
|
90
|
+
{state === "ask" && factLine ? <p className="mt-1">{factLine}</p> : null}
|
|
91
|
+
{state === "downloading" ? (
|
|
92
|
+
<Progress
|
|
93
|
+
className="mt-3"
|
|
94
|
+
label={name}
|
|
95
|
+
value={progress ? progress.done : null}
|
|
96
|
+
max={progress?.total}
|
|
97
|
+
detail={
|
|
98
|
+
progress ? `${formatBytes(progress.done)} / ${formatBytes(progress.total)}` : undefined
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
) : null}
|
|
102
|
+
{state === "failed" && reason ? <p className="mt-1">{reason}</p> : null}
|
|
103
|
+
</AlertShell>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type { DownloadPromptProps }
|
|
108
|
+
export { DownloadPrompt }
|