@preset.nz/ux-kit 0.1.1 → 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 +4 -42
- package/package.json +5 -4
- package/src/components/alert-shell.tsx +66 -0
- package/src/components/badge.tsx +2 -0
- package/src/components/confirm-dialog.tsx +93 -0
- package/src/components/context-menu.tsx +3 -0
- package/src/components/dialog.tsx +3 -0
- package/src/components/download-prompt.tsx +108 -0
- package/src/components/dropdown-menu.tsx +3 -0
- package/src/components/list.tsx +205 -0
- package/src/components/navigation-menu.tsx +1 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/progress.tsx +77 -0
- package/src/components/sidebar.tsx +4 -0
- package/src/components/snackbar.tsx +3 -2
- package/src/components/tabs.tsx +2 -0
- package/src/components/toggle.tsx +2 -0
- package/src/index.css +8 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -15,44 +15,7 @@ Like [facets](https://github.com/preset-nz/facets), the kit ships unbuilt TypeSc
|
|
|
15
15
|
|
|
16
16
|
## Components
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| Component | In kit | Came from | Notes |
|
|
21
|
-
|---|---|---|---|
|
|
22
|
-
| button | yes | Strata, Shard, M&T | Base UI Button |
|
|
23
|
-
| checkbox | yes | Strata, Oblique, Shard | |
|
|
24
|
-
| input | yes | all four | |
|
|
25
|
-
| label | yes | Strata, Oblique, Shard | |
|
|
26
|
-
| select | yes | Strata, Oblique, Shard | |
|
|
27
|
-
| separator | yes | all four | |
|
|
28
|
-
| dialog | yes | Oblique, Shard, M&T | Shard's themed variant. `render={<Button />}` replaces `asChild` |
|
|
29
|
-
| sheet | yes | M&T | Base UI Dialog docked to an edge (`side`) |
|
|
30
|
-
| popover | yes | Oblique | `PopoverAnchor` dropped: Base UI anchors through the Positioner's `anchor` prop |
|
|
31
|
-
| tooltip | yes | Oblique, M&T | `TooltipProvider delay` replaces `delayDuration` |
|
|
32
|
-
| tabs | yes | Oblique | `TabsTrigger` / `TabsContent` wrap Base UI `Tab` / `Panel`. Orientation uses `data-[orientation=...]` |
|
|
33
|
-
| toggle | yes | Oblique | `data-pressed` replaces `data-[state=on]` |
|
|
34
|
-
| toggle-group | yes | Oblique | Base UI `value` is always an array and `multiple` replaces `type="single" \| "multiple"` |
|
|
35
|
-
| slider | yes | Oblique, Shard, M&T | Takes a number or an array; one thumb per value |
|
|
36
|
-
| dropdown-menu | yes | Shard, M&T | M&T's fuller variant. `DropdownMenuLabel` is Base UI's `GroupLabel`, so it must sit inside a `DropdownMenuGroup` |
|
|
37
|
-
| context-menu | yes | Shard | Same shape as dropdown-menu, same `Label` caveat |
|
|
38
|
-
| navigation-menu | yes | M&T | Always the shared-viewport form. Radix's `NavigationMenuIndicator` and the `viewport={false}` mode are gone |
|
|
39
|
-
| snackbar | yes | Strata, Oblique | The kit's one toast, on Base UI Toast. `useSnackbar().show({ message, kind, description, action, persistent, timeout })` returns an id and `dismiss(id?)` closes one or all; kinds are `plain`, `success`, `warning`, `error`. Mount `SnackbarProvider` once and a `SnackbarViewport` inside it. `notify(opts)` and `dismissNotification(id?)` do the same from outside React |
|
|
40
|
-
| avatar | yes | M&T | Base UI Avatar |
|
|
41
|
-
| badge | yes | M&T | Plain, `render` prop replaces `asChild` |
|
|
42
|
-
| breadcrumb | yes | M&T | Plain `<a>`, no `next/link`. Use `render` for a router link |
|
|
43
|
-
| card | yes | M&T | Plain |
|
|
44
|
-
| skeleton | yes | M&T | Plain |
|
|
45
|
-
| textarea | yes | M&T | Plain, restyled to match Input |
|
|
46
|
-
| sidebar | yes | M&T | Plain, 700 lines. `asChild` became `render`. `useIsMobile` moved into the kit. Untested beyond the `collapsible="none"` form in the playground |
|
|
47
|
-
| toolbar | yes | Oblique, M&T | Base UI Toolbar (roving focus). `ToolbarItems` takes `groups` of `ToolbarItemSpec` (id, label, icon, shortcut, enabled, disabledReason, pressed, menu) and `onCommand(id)`; Oblique's `ToolButtonSpec` table with M&T's disabled reason. Disabled items stay focusable so the tooltip can say why. `ToolbarButton`, `ToolbarGroup`, `ToolbarSeparator`, `ToolbarSpacer` compose by hand. Shard's header buttons are inline JSX and Fault's are not a toolbar, so neither contributed |
|
|
48
|
-
| color-field | yes | M&T, Oblique, Strata | Swatch, hex text (commits on blur or Enter, bad input reverts), optional alpha (`#rrggbbaa`), preset grid in a Popover, native picker with `onPickStart`/`onPickEnd` for undo transactions (Oblique), `clearable` and null value (Strata, Oblique's optional colour), read-only, `trailing` slot for an eyedropper |
|
|
49
|
-
| number-field | yes | Base UI, Oblique | Base UI NumberField with the label as the ScrubArea (drag it; click it to type). Shift is coarse (x10), Alt/Option fine (x0.1), as Oblique's InputNumber, and `integer` keeps Alt on whole steps (integerStepFix). `onValueChange(value, { reason })` is `scrub`, `step` or `typed` (typed commits on blur or Enter, never per keystroke; Escape reverts). `onScrubStart`/`onScrubEnd` bracket one drag as the undo transaction, `onScrubCancel` is Escape mid-drag. `VectorField` is N of them with X/Y/Z/W labels. Scrubbing only on the label: no gutter scrub |
|
|
50
|
-
| color-swatch | yes | Strata, M&T, Oblique | Chip over a checkerboard; `null` is the dashed empty chip |
|
|
51
|
-
| side-panel | yes | Strata, M&T | Docked panel, props only (`open`, `width` owned by the app). Collapsed it leaves an icon rail that reopens it; the inner edge resizes by pointer drag (pointer capture) or arrow keys, clamped by `clampWidth`. `SidePanelHeader`, `SidePanelContent`. Strata's `SidePanel` ideas; persistence stays app-side |
|
|
52
|
-
| status-bar | yes | Oblique, Strata, M&T | `StatusBar`, `StatusItem` (optional label), `StatusSpacer`. Contents stay app-side |
|
|
53
|
-
| empty-state | yes | new | Icon, title, description, action slot |
|
|
54
|
-
|
|
55
|
-
M&T's `src/palettes/` (presets and derivation) was considered for the colour field and left out: it is map domain data and stays in the app, which passes its colours in through `presets`.
|
|
18
|
+
Every component is exported from `src/index.ts`, which is the list. Each file's top comment says how it differs from shadcn and Radix. M&T's `src/palettes/` (presets and derivation) stays in the app, as map domain data; it passes its colours to the colour field through `presets`.
|
|
56
19
|
|
|
57
20
|
## Using it
|
|
58
21
|
|
|
@@ -107,12 +70,11 @@ The components are adapted from Vercel AI Elements (Apache-2.0) and shadcn/ui's
|
|
|
107
70
|
## Working on it
|
|
108
71
|
|
|
109
72
|
```sh
|
|
110
|
-
just install # pnpm install
|
|
73
|
+
just install # pnpm install, and the playground's crates
|
|
111
74
|
just dev # the playground as a desktop app
|
|
112
|
-
just check #
|
|
113
|
-
just check-all # the kit, the playground and cargo check
|
|
75
|
+
just check # the kit, the playground and cargo check
|
|
114
76
|
```
|
|
115
77
|
|
|
116
|
-
The playground links the
|
|
78
|
+
Run them in `ux-kit/` of [preset-nz/packages](https://github.com/preset-nz/packages). The playground links the other packages in that repo (rhizome, app-kit, preferences, facets, math), so a change to one of them that breaks it fails `just check`.
|
|
117
79
|
|
|
118
80
|
MIT.
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preset.nz/ux-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "One look and one way of working for the preset.nz desktop apps: tokens, palettes, fonts and Base UI primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/preset-nz/
|
|
8
|
+
"url": "git+https://github.com/preset-nz/packages.git",
|
|
9
|
+
"directory": "ux-kit"
|
|
9
10
|
},
|
|
10
11
|
"type": "module",
|
|
11
12
|
"sideEffects": [
|
|
@@ -30,13 +31,13 @@
|
|
|
30
31
|
"@fontsource-variable/geist": "^5.2.9",
|
|
31
32
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
32
33
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
|
-
"@preset.nz/math": "^0.1.0",
|
|
34
34
|
"@shadcn/react": "^0.3.1",
|
|
35
35
|
"class-variance-authority": "^0.7.1",
|
|
36
36
|
"clsx": "^2.1.1",
|
|
37
37
|
"streamdown": "^2.7.0",
|
|
38
38
|
"tailwind-merge": "^3.6.0",
|
|
39
|
-
"tw-animate-css": "^1.4.0"
|
|
39
|
+
"tw-animate-css": "^1.4.0",
|
|
40
|
+
"@preset.nz/math": "^0.1.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
|
|
2
|
+
import type * as React from "react"
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
import { DialogFooter, DialogHeader, DialogOverlay } from "./dialog"
|
|
5
|
+
|
|
6
|
+
// AlertShell: the markup ConfirmDialog and DownloadPrompt share. Internal, not
|
|
7
|
+
// exported from the package. It is answered, not dismissed: no close "x", and a
|
|
8
|
+
// click outside does nothing, so the footer buttons and Escape are the ways out.
|
|
9
|
+
|
|
10
|
+
interface AlertShellProps {
|
|
11
|
+
open: boolean
|
|
12
|
+
/** Escape. The caller decides what that means (and may ignore it). */
|
|
13
|
+
onDismiss: () => void
|
|
14
|
+
title: string
|
|
15
|
+
/** The body, below the title. */
|
|
16
|
+
children?: React.ReactNode
|
|
17
|
+
/** The buttons, secondary first and primary last. */
|
|
18
|
+
footer: React.ReactNode
|
|
19
|
+
/** The element that takes focus when the dialog opens. */
|
|
20
|
+
initialFocus?: React.RefObject<HTMLElement | null>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function AlertShell({ open, onDismiss, title, children, footer, initialFocus }: AlertShellProps) {
|
|
24
|
+
return (
|
|
25
|
+
<AlertDialogPrimitive.Root
|
|
26
|
+
data-slot="confirm-dialog"
|
|
27
|
+
open={open}
|
|
28
|
+
onOpenChange={(next) => {
|
|
29
|
+
if (!next) onDismiss()
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<AlertDialogPrimitive.Portal data-slot="confirm-dialog-portal">
|
|
33
|
+
<DialogOverlay />
|
|
34
|
+
<AlertDialogPrimitive.Popup
|
|
35
|
+
data-slot="confirm-dialog-content"
|
|
36
|
+
initialFocus={initialFocus}
|
|
37
|
+
className={cn(
|
|
38
|
+
"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",
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<DialogHeader>
|
|
42
|
+
<AlertDialogPrimitive.Title
|
|
43
|
+
data-slot="dialog-title"
|
|
44
|
+
className="font-heading leading-none font-medium"
|
|
45
|
+
>
|
|
46
|
+
{title}
|
|
47
|
+
</AlertDialogPrimitive.Title>
|
|
48
|
+
{children ? (
|
|
49
|
+
<AlertDialogPrimitive.Description
|
|
50
|
+
render={<div />}
|
|
51
|
+
data-slot="dialog-description"
|
|
52
|
+
className="text-sm text-muted-foreground"
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</AlertDialogPrimitive.Description>
|
|
56
|
+
) : null}
|
|
57
|
+
</DialogHeader>
|
|
58
|
+
<DialogFooter>{footer}</DialogFooter>
|
|
59
|
+
</AlertDialogPrimitive.Popup>
|
|
60
|
+
</AlertDialogPrimitive.Portal>
|
|
61
|
+
</AlertDialogPrimitive.Root>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type { AlertShellProps }
|
|
66
|
+
export { AlertShell }
|
package/src/components/badge.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/utils"
|
|
6
6
|
|
|
7
|
+
// Badge: a plain span. The `render` prop replaces Radix's `asChild`.
|
|
8
|
+
|
|
7
9
|
const badgeVariants = cva(
|
|
8
10
|
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
9
11
|
{
|
|
@@ -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 }
|
|
@@ -4,6 +4,9 @@ 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
|
}
|
|
@@ -5,6 +5,9 @@ import type * as React from "react"
|
|
|
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
|
}
|
|
@@ -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 }
|
|
@@ -4,6 +4,9 @@ import type * as React from "react"
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/utils"
|
|
6
6
|
|
|
7
|
+
// Base UI: `DropdownMenuLabel` is Base UI's `GroupLabel`, so it must sit inside a `DropdownMenuGroup`.
|
|
8
|
+
// Same shape as ContextMenu.
|
|
9
|
+
|
|
7
10
|
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
|
|
8
11
|
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
9
12
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
// List and ListRow: a set of things (models, files, recent items) built from
|
|
6
|
+
// slots. Without `selection` it is a plain `role="list"` and the rows'
|
|
7
|
+
// `trailing` actions are the only interactive parts (Tab walks them). With
|
|
8
|
+
// `selection` it is a `role="listbox"` with roving tabindex: Up, Down, Home
|
|
9
|
+
// and End move focus, Enter or Space selects. Rows must be direct children of
|
|
10
|
+
// the List (the List reads their ids). A selectable list takes no `trailing`
|
|
11
|
+
// controls: a button inside an option is invalid ARIA, and its click would
|
|
12
|
+
// select the row too. No virtualisation, reorder or sections.
|
|
13
|
+
//
|
|
14
|
+
// `className` and other props go on the List's outer element; the accessible
|
|
15
|
+
// name (`aria-label`, `aria-labelledby`) goes on the element with the role.
|
|
16
|
+
|
|
17
|
+
interface ListSelection {
|
|
18
|
+
selectedId: string | null
|
|
19
|
+
onSelect: (id: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ListContextValue {
|
|
23
|
+
selection: ListSelection | undefined
|
|
24
|
+
tabStopId: string | undefined
|
|
25
|
+
setFocusId: (id: string) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ListContext = React.createContext<ListContextValue>({
|
|
29
|
+
selection: undefined,
|
|
30
|
+
tabStopId: undefined,
|
|
31
|
+
setFocusId: () => {},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
interface ListProps extends Omit<React.ComponentProps<"div">, "role" | "children"> {
|
|
35
|
+
/** Makes the list a selectable listbox. Leave undefined for a plain list. */
|
|
36
|
+
selection?: ListSelection
|
|
37
|
+
/** Shown in place of the rows when there are no children. Usually an EmptyState. */
|
|
38
|
+
empty?: React.ReactNode
|
|
39
|
+
/** After the rows. */
|
|
40
|
+
footer?: React.ReactNode
|
|
41
|
+
children?: React.ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function List({
|
|
45
|
+
selection,
|
|
46
|
+
empty,
|
|
47
|
+
footer,
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
onKeyDown,
|
|
51
|
+
"aria-label": ariaLabel,
|
|
52
|
+
"aria-labelledby": ariaLabelledBy,
|
|
53
|
+
...props
|
|
54
|
+
}: ListProps) {
|
|
55
|
+
const rows = React.Children.toArray(children)
|
|
56
|
+
const ids = rows.flatMap((row) =>
|
|
57
|
+
React.isValidElement<{ id?: string }>(row) && typeof row.props.id === "string"
|
|
58
|
+
? [row.props.id]
|
|
59
|
+
: [],
|
|
60
|
+
)
|
|
61
|
+
const [focusId, setFocusId] = React.useState<string | undefined>(undefined)
|
|
62
|
+
const rowsRef = React.useRef<HTMLDivElement>(null)
|
|
63
|
+
|
|
64
|
+
// The one tab stop: the last focused row, else the selected one, else the first.
|
|
65
|
+
const tabStopId =
|
|
66
|
+
[focusId, selection?.selectedId ?? undefined].find(
|
|
67
|
+
(id): id is string => id !== undefined && ids.includes(id),
|
|
68
|
+
) ?? ids[0]
|
|
69
|
+
|
|
70
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
71
|
+
onKeyDown?.(event)
|
|
72
|
+
if (!selection || event.defaultPrevented) return
|
|
73
|
+
const options = Array.from(
|
|
74
|
+
rowsRef.current?.querySelectorAll<HTMLElement>('[role="option"]') ?? [],
|
|
75
|
+
)
|
|
76
|
+
const current = options.indexOf(event.target as HTMLElement)
|
|
77
|
+
if (current === -1) return
|
|
78
|
+
let next: number
|
|
79
|
+
switch (event.key) {
|
|
80
|
+
case "ArrowDown":
|
|
81
|
+
next = Math.min(current + 1, options.length - 1)
|
|
82
|
+
break
|
|
83
|
+
case "ArrowUp":
|
|
84
|
+
next = Math.max(current - 1, 0)
|
|
85
|
+
break
|
|
86
|
+
case "Home":
|
|
87
|
+
next = 0
|
|
88
|
+
break
|
|
89
|
+
case "End":
|
|
90
|
+
next = options.length - 1
|
|
91
|
+
break
|
|
92
|
+
default:
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
options[next]?.focus()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const context = React.useMemo<ListContextValue>(
|
|
100
|
+
() => ({ selection, tabStopId, setFocusId }),
|
|
101
|
+
[selection, tabStopId],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div data-slot="list" className={cn("flex flex-col", className)} {...props}>
|
|
106
|
+
{rows.length === 0 ? (
|
|
107
|
+
empty
|
|
108
|
+
) : (
|
|
109
|
+
<ListContext.Provider value={context}>
|
|
110
|
+
{/* biome-ignore lint/a11y/noStaticElementInteractions: arrow keys on the listbox are the roving-tabindex pattern, the options hold the focus */}
|
|
111
|
+
{/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: the role is always list or listbox, and both take a name; Biome can't read the ternary */}
|
|
112
|
+
<div
|
|
113
|
+
ref={rowsRef}
|
|
114
|
+
role={selection ? "listbox" : "list"}
|
|
115
|
+
aria-label={ariaLabel}
|
|
116
|
+
aria-labelledby={ariaLabelledBy}
|
|
117
|
+
data-slot="list-rows"
|
|
118
|
+
className="flex flex-col divide-y divide-border"
|
|
119
|
+
onKeyDown={handleKeyDown}
|
|
120
|
+
>
|
|
121
|
+
{rows}
|
|
122
|
+
</div>
|
|
123
|
+
</ListContext.Provider>
|
|
124
|
+
)}
|
|
125
|
+
{footer && rows.length > 0 ? (
|
|
126
|
+
<div
|
|
127
|
+
data-slot="list-footer"
|
|
128
|
+
className="border-t border-border px-3 py-2 text-sm text-muted-foreground"
|
|
129
|
+
>
|
|
130
|
+
{footer}
|
|
131
|
+
</div>
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ListRowProps
|
|
138
|
+
extends Omit<
|
|
139
|
+
React.ComponentProps<"div">,
|
|
140
|
+
"id" | "title" | "role" | "children" | "onClick" | "onSelect"
|
|
141
|
+
> {
|
|
142
|
+
/** Identifies the row to `selection`. Not set as the DOM id. */
|
|
143
|
+
id: string
|
|
144
|
+
/** Before the text: an icon or thumbnail. */
|
|
145
|
+
leading?: React.ReactNode
|
|
146
|
+
title: React.ReactNode
|
|
147
|
+
/** One muted line under the title. */
|
|
148
|
+
detail?: React.ReactNode
|
|
149
|
+
/** After the text: a Button, a Badge. Clickable things go here, not on the row, and only in a plain list. */
|
|
150
|
+
trailing?: React.ReactNode
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ListRow({ id, leading, title, detail, trailing, className, ...props }: ListRowProps) {
|
|
154
|
+
const { selection, tabStopId, setFocusId } = React.useContext(ListContext)
|
|
155
|
+
const selectable = selection !== undefined
|
|
156
|
+
const selected = selectable && selection.selectedId === id
|
|
157
|
+
|
|
158
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
159
|
+
props.onKeyDown?.(event)
|
|
160
|
+
// Keys from a trailing control stay with that control.
|
|
161
|
+
if (!selectable || event.defaultPrevented || event.target !== event.currentTarget) return
|
|
162
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
selection.onSelect(id)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: the role is option when selectable; a plain row has no handlers that act
|
|
170
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-selected is set only when the role is option
|
|
171
|
+
<div
|
|
172
|
+
data-slot="list-row"
|
|
173
|
+
data-row-id={id}
|
|
174
|
+
data-selected={selected ? "" : undefined}
|
|
175
|
+
role={selectable ? "option" : "listitem"}
|
|
176
|
+
aria-selected={selectable ? selected : undefined}
|
|
177
|
+
tabIndex={selectable ? (tabStopId === id ? 0 : -1) : undefined}
|
|
178
|
+
className={cn(
|
|
179
|
+
"flex min-h-11 items-center gap-3 px-3 py-1.5 outline-none",
|
|
180
|
+
selectable &&
|
|
181
|
+
"cursor-default hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 data-selected:bg-accent",
|
|
182
|
+
className,
|
|
183
|
+
)}
|
|
184
|
+
{...props}
|
|
185
|
+
onKeyDown={handleKeyDown}
|
|
186
|
+
onFocus={(event) => {
|
|
187
|
+
props.onFocus?.(event)
|
|
188
|
+
if (selectable && event.target === event.currentTarget) setFocusId(id)
|
|
189
|
+
}}
|
|
190
|
+
onClick={selectable ? () => selection.onSelect(id) : undefined}
|
|
191
|
+
>
|
|
192
|
+
{leading ? (
|
|
193
|
+
<div className="shrink-0 text-muted-foreground [&_svg]:size-5">{leading}</div>
|
|
194
|
+
) : null}
|
|
195
|
+
<div className="min-w-0 flex-1">
|
|
196
|
+
<p className="truncate text-sm font-medium">{title}</p>
|
|
197
|
+
{detail ? <p className="truncate text-xs text-muted-foreground">{detail}</p> : null}
|
|
198
|
+
</div>
|
|
199
|
+
{trailing ? <div className="flex shrink-0 items-center gap-2">{trailing}</div> : null}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type { ListProps, ListRowProps, ListSelection }
|
|
205
|
+
export { List, ListRow }
|
|
@@ -7,6 +7,7 @@ import { cn } from "../lib/utils"
|
|
|
7
7
|
// Base UI renders content through a shared Portal > Positioner > Popup >
|
|
8
8
|
// Viewport. NavigationMenu bundles that, so `NavigationMenuContent` items
|
|
9
9
|
// morph inside one popup like the shadcn viewport variant.
|
|
10
|
+
// Always the shared-viewport form: the Radix indicator and viewport={false} mode are gone.
|
|
10
11
|
function NavigationMenu({
|
|
11
12
|
className,
|
|
12
13
|
children,
|
|
@@ -3,6 +3,9 @@ import type * as React from "react"
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/utils"
|
|
5
5
|
|
|
6
|
+
// Popover: Base UI Popover. There is no `PopoverAnchor`: Base UI anchors through the
|
|
7
|
+
// Positioner's `anchor` prop.
|
|
8
|
+
|
|
6
9
|
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
|
7
10
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
8
11
|
}
|
|
@@ -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 }
|
|
@@ -14,6 +14,10 @@ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "
|
|
|
14
14
|
import { Skeleton } from "./skeleton"
|
|
15
15
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
|
|
16
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.
|
|
20
|
+
|
|
17
21
|
// Base UI's replacement for Radix's asChild: pass `render={<a href="..." />}`
|
|
18
22
|
// to swap the element, props and ref are merged onto it.
|
|
19
23
|
function Slotted({
|
|
@@ -4,8 +4,9 @@ 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
|
|
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 }) {
|
package/src/components/tabs.tsx
CHANGED
|
@@ -44,6 +44,8 @@ function TabsList({
|
|
|
44
44
|
)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Orientation styles key on `data-[orientation=...]`.
|
|
48
|
+
|
|
47
49
|
// Base UI calls the trigger `Tab` and the content `Panel`; the kit keeps the
|
|
48
50
|
// shadcn names (TabsTrigger, TabsContent) so apps read the same.
|
|
49
51
|
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
@@ -3,6 +3,8 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/utils"
|
|
5
5
|
|
|
6
|
+
// Toggle: the pressed state is `data-pressed`, not Radix's `data-[state=on]`. Style with that.
|
|
7
|
+
|
|
6
8
|
const toggleVariants = cva(
|
|
7
9
|
"group/toggle inline-flex items-center justify-center gap-1 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
10
|
{
|
package/src/index.css
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
@custom-variant dark (&:is(.dark *));
|
|
17
17
|
|
|
18
|
+
/* Progress, indeterminate: the 40%-wide segment crosses the track left to right. */
|
|
19
|
+
@theme inline {
|
|
20
|
+
@keyframes progress-slide {
|
|
21
|
+
from { transform: translateX(-100%); }
|
|
22
|
+
to { transform: translateX(250%); }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
@theme inline {
|
|
19
27
|
/* Headings use the interface face; hierarchy comes from size and weight. */
|
|
20
28
|
--font-heading: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
|
package/src/index.ts
CHANGED
|
@@ -6,15 +6,19 @@ export * from "./components/card"
|
|
|
6
6
|
export * from "./components/checkbox"
|
|
7
7
|
export * from "./components/color-field"
|
|
8
8
|
export * from "./components/color-swatch"
|
|
9
|
+
export * from "./components/confirm-dialog"
|
|
9
10
|
export * from "./components/context-menu"
|
|
10
11
|
export * from "./components/dialog"
|
|
12
|
+
export * from "./components/download-prompt"
|
|
11
13
|
export * from "./components/dropdown-menu"
|
|
12
14
|
export * from "./components/empty-state"
|
|
13
15
|
export * from "./components/input"
|
|
14
16
|
export * from "./components/label"
|
|
17
|
+
export * from "./components/list"
|
|
15
18
|
export * from "./components/navigation-menu"
|
|
16
19
|
export * from "./components/number-field"
|
|
17
20
|
export * from "./components/popover"
|
|
21
|
+
export * from "./components/progress"
|
|
18
22
|
export * from "./components/select"
|
|
19
23
|
export * from "./components/separator"
|
|
20
24
|
export * from "./components/sheet"
|