@sableui/react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +157 -0
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +221 -0
- package/DESIGN.md +548 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/THIRD_PARTY_NOTICES.md +44 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +1223 -0
- package/dist/_contract-CYScOdf5.d.ts +150 -0
- package/dist/chunk-3IWZYBGH.js +398 -0
- package/dist/chunk-5Z5KLOAU.js +167 -0
- package/dist/chunk-7MG4SHYX.js +15 -0
- package/dist/chunk-ADLLDWLW.js +15 -0
- package/dist/chunk-EP55KSYA.js +14 -0
- package/dist/chunk-J23ELQQZ.js +9396 -0
- package/dist/chunk-JI7WJP2L.js +286 -0
- package/dist/chunk-K7RUHUGC.js +14 -0
- package/dist/chunk-KO3NAEBF.js +15 -0
- package/dist/chunk-L4IEIQQA.js +14 -0
- package/dist/chunk-L6Y46C62.js +270 -0
- package/dist/chunk-NEQNXI2G.js +25 -0
- package/dist/chunk-PAOZBZME.js +40 -0
- package/dist/chunk-RWP4XBSK.js +139 -0
- package/dist/chunk-SRQWQUTW.js +63 -0
- package/dist/chunk-YXBOYWBN.js +15 -0
- package/dist/foundations/index.d.ts +88 -0
- package/dist/foundations/index.js +792 -0
- package/dist/icon/index.d.ts +60 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon-sets/brands.d.ts +44 -0
- package/dist/icon-sets/brands.js +147 -0
- package/dist/icon-sets/reicon.d.ts +8 -0
- package/dist/icon-sets/reicon.js +1 -0
- package/dist/index.d.ts +7013 -0
- package/dist/index.js +15462 -0
- package/dist/ios/chrome.css +83 -0
- package/dist/ios/frame.css +5 -0
- package/dist/ios/index.d.ts +70 -0
- package/dist/ios/index.js +313 -0
- package/dist/ios/preview.d.ts +64 -0
- package/dist/ios/preview.js +133 -0
- package/dist/ios/styles.css +77 -0
- package/dist/layout/index.d.ts +280 -0
- package/dist/layout/index.js +2 -0
- package/dist/meta-6dUzgC6M.d.ts +205 -0
- package/dist/patterns/index.d.ts +36 -0
- package/dist/patterns/index.js +934 -0
- package/dist/presets/index.d.ts +37 -0
- package/dist/presets/index.js +117 -0
- package/dist/registry-CyL28OzJ.d.ts +28 -0
- package/dist/styles.css +1893 -0
- package/dist/theme-provider-BWcizhAt.d.ts +105 -0
- package/dist/themes/atelier/atelier.css +213 -0
- package/dist/themes/atelier/index.d.ts +9 -0
- package/dist/themes/atelier/index.js +1 -0
- package/dist/themes/base/base.css +443 -0
- package/dist/themes/base/index.d.ts +9 -0
- package/dist/themes/base/index.js +1 -0
- package/dist/themes/ember/ember.css +270 -0
- package/dist/themes/ember/index.d.ts +9 -0
- package/dist/themes/ember/index.js +1 -0
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +8 -0
- package/dist/themes/meridian/index.d.ts +9 -0
- package/dist/themes/meridian/index.js +1 -0
- package/dist/themes/meridian/meridian.css +255 -0
- package/dist/themes/poppy/index.d.ts +6 -0
- package/dist/themes/poppy/index.js +1 -0
- package/dist/themes/poppy/poppy.css +174 -0
- package/dist/themes/prism/index.d.ts +6 -0
- package/dist/themes/prism/index.js +1 -0
- package/dist/themes/prism/prism.css +183 -0
- package/dist/themes/verdant/index.d.ts +9 -0
- package/dist/themes/verdant/index.js +1 -0
- package/dist/themes/verdant/verdant.css +210 -0
- package/dist/themes.css +30 -0
- package/llms/components/accordion.md +93 -0
- package/llms/components/app-header.md +70 -0
- package/llms/components/app-shell.md +117 -0
- package/llms/components/attachment.md +105 -0
- package/llms/components/avatar.md +141 -0
- package/llms/components/badge.md +117 -0
- package/llms/components/banner.md +119 -0
- package/llms/components/bottom-nav.md +80 -0
- package/llms/components/breadcrumbs.md +75 -0
- package/llms/components/browser.md +67 -0
- package/llms/components/bubble.md +119 -0
- package/llms/components/button.md +199 -0
- package/llms/components/calendar.md +75 -0
- package/llms/components/card.md +135 -0
- package/llms/components/carousel.md +87 -0
- package/llms/components/chat-composer.md +126 -0
- package/llms/components/checkbox-card.md +98 -0
- package/llms/components/checkbox.md +90 -0
- package/llms/components/code-block.md +81 -0
- package/llms/components/code.md +62 -0
- package/llms/components/collapsible.md +83 -0
- package/llms/components/color-field.md +100 -0
- package/llms/components/combobox.md +119 -0
- package/llms/components/command-menu.md +158 -0
- package/llms/components/context-card.md +73 -0
- package/llms/components/context-menu.md +79 -0
- package/llms/components/data-table.md +99 -0
- package/llms/components/date-picker.md +97 -0
- package/llms/components/description.md +96 -0
- package/llms/components/destructive-action-modal.md +76 -0
- package/llms/components/dialog.md +127 -0
- package/llms/components/dots-menu.md +64 -0
- package/llms/components/empty-state.md +113 -0
- package/llms/components/entity.md +67 -0
- package/llms/components/feedback.md +63 -0
- package/llms/components/fieldset.md +106 -0
- package/llms/components/file-dropzone.md +103 -0
- package/llms/components/form.md +111 -0
- package/llms/components/gliding-highlight.md +84 -0
- package/llms/components/inline-status.md +62 -0
- package/llms/components/input.md +127 -0
- package/llms/components/kbd.md +57 -0
- package/llms/components/label.md +73 -0
- package/llms/components/marker.md +99 -0
- package/llms/components/marquee.md +66 -0
- package/llms/components/menu.md +122 -0
- package/llms/components/message-scroller.md +110 -0
- package/llms/components/message.md +114 -0
- package/llms/components/meter.md +124 -0
- package/llms/components/middle-truncate.md +62 -0
- package/llms/components/note.md +81 -0
- package/llms/components/notification-badge.md +67 -0
- package/llms/components/number-field.md +70 -0
- package/llms/components/otp-field.md +101 -0
- package/llms/components/page-header.md +76 -0
- package/llms/components/page-loader.md +67 -0
- package/llms/components/pagination.md +92 -0
- package/llms/components/panel-group.md +120 -0
- package/llms/components/panel-section.md +122 -0
- package/llms/components/phone.md +70 -0
- package/llms/components/pill.md +73 -0
- package/llms/components/popover.md +79 -0
- package/llms/components/progress.md +98 -0
- package/llms/components/radio-card.md +90 -0
- package/llms/components/radio.md +86 -0
- package/llms/components/relative-time.md +60 -0
- package/llms/components/scroller.md +88 -0
- package/llms/components/scrub-field.md +97 -0
- package/llms/components/search-input.md +84 -0
- package/llms/components/select.md +106 -0
- package/llms/components/separator.md +70 -0
- package/llms/components/settings-menu.md +139 -0
- package/llms/components/sheet.md +97 -0
- package/llms/components/show-more.md +59 -0
- package/llms/components/side-nav.md +106 -0
- package/llms/components/sidebar.md +87 -0
- package/llms/components/skeleton.md +62 -0
- package/llms/components/slider.md +114 -0
- package/llms/components/spinner.md +87 -0
- package/llms/components/status-bar.md +91 -0
- package/llms/components/status-dot.md +70 -0
- package/llms/components/stepper.md +125 -0
- package/llms/components/streaming-text.md +86 -0
- package/llms/components/switch.md +87 -0
- package/llms/components/table.md +112 -0
- package/llms/components/tabs.md +84 -0
- package/llms/components/textarea.md +88 -0
- package/llms/components/theme-selector.md +95 -0
- package/llms/components/theme-switcher.md +71 -0
- package/llms/components/thinking-orb.md +91 -0
- package/llms/components/toast.md +130 -0
- package/llms/components/toggle.md +95 -0
- package/llms/components/toolbar.md +168 -0
- package/llms/components/tooltip.md +93 -0
- package/llms/components/tree.md +227 -0
- package/llms/components/video.md +66 -0
- package/llms/components/workspace.md +138 -0
- package/llms/foundations/accessibility.md +60 -0
- package/llms/foundations/motion.md +96 -0
- package/llms/foundations/scrolling.md +41 -0
- package/llms/foundations/shape.md +73 -0
- package/llms/foundations/spacing.md +82 -0
- package/llms/foundations/surfaces.md +69 -0
- package/llms/foundations/typography.md +120 -0
- package/llms/layout/aspect-box.md +37 -0
- package/llms/layout/bleed.md +31 -0
- package/llms/layout/center.md +37 -0
- package/llms/layout/columns.md +51 -0
- package/llms/layout/grid-overlay.md +40 -0
- package/llms/layout/grid.md +47 -0
- package/llms/layout/page.md +39 -0
- package/llms/layout/region.md +49 -0
- package/llms/layout/row.md +40 -0
- package/llms/layout/safe-area.md +37 -0
- package/llms/layout/spacer.md +39 -0
- package/llms/layout/split-pane.md +41 -0
- package/llms/layout/stack.md +39 -0
- package/llms/patterns/auth-page.md +68 -0
- package/llms/patterns/brand-landing-page.md +423 -0
- package/llms/patterns/chat-panel.md +210 -0
- package/llms/patterns/dashboard-page.md +170 -0
- package/llms/patterns/feed-page.md +103 -0
- package/llms/patterns/landing-page.md +122 -0
- package/llms/patterns/list-detail-page.md +100 -0
- package/llms/patterns/settings-page.md +121 -0
- package/llms/reference/token-intent.md +324 -0
- package/llms/reference/utilities.md +129 -0
- package/llms/tasks/adding-a-component.md +113 -0
- package/llms/tasks/auditing-an-app.md +216 -0
- package/llms/tasks/building-a-page.md +109 -0
- package/llms/tasks/building-a-tool.md +141 -0
- package/llms/tasks/building-an-ios-prototype.md +105 -0
- package/llms/tasks/building-with-sable.md +73 -0
- package/llms/tasks/choosing-a-component.md +109 -0
- package/llms/tasks/converting-an-app.md +139 -0
- package/llms/tasks/creating-a-preset.md +43 -0
- package/llms/tasks/creating-design-assets.md +96 -0
- package/llms/tasks/designing-a-surface.md +115 -0
- package/llms/tasks/developing-taste.md +157 -0
- package/llms/tasks/getting-started.md +63 -0
- package/llms/tasks/learning-from-builds.md +74 -0
- package/llms/tasks/reviewing-a-design.md +114 -0
- package/llms/tasks/reviewing-a-journey.md +84 -0
- package/llms/tasks/reviewing-motion.md +92 -0
- package/llms/tasks/using-presets.md +87 -0
- package/llms/taste/asset-plan.template.json +53 -0
- package/llms/taste/composition-follows-content.md +20 -0
- package/llms/taste/hierarchy-is-a-relationship.md +22 -0
- package/llms/taste/lessons.json +57 -0
- package/llms/taste/motion-needs-a-reason.md +21 -0
- package/llms/taste/surface-states-travel-together.md +20 -0
- package/llms-full.txt +14906 -0
- package/llms.txt +405 -0
- package/package.json +185 -3
- package/registry.json +18812 -0
- package/scripts/agent-setup.mjs +54 -0
- package/scripts/design-atlas.mjs +221 -0
- package/scripts/design-directions.mjs +123 -0
- package/scripts/design-memory.mjs +166 -0
- package/scripts/design-preferences.mjs +227 -0
- package/scripts/design-presets.mjs +48 -0
- package/scripts/grade.mjs +509 -0
- package/scripts/lint-rules.mjs +1065 -0
- package/scripts/review-evidence.mjs +120 -0
- package/scripts/skeleton.mjs +248 -0
- package/skills/entry/SKILL.md +34 -0
- package/skills/sable/SKILL.md +187 -0
- package/skills/sable/agents/openai.yaml +6 -0
- package/targets/ios.json +982 -0
- package/third-party/reicon-LICENSE.txt +20 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# PanelGroup
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A row of docked chrome panels around a flexible centre, with draggable and keyboard-resizable edges and optional persisted widths.
|
|
6
|
+
|
|
7
|
+
**Keywords:** panel, panels, resize, resizable, splitter, docked, workspace, tool, chrome, inspector, layout
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left and/or right, a flexible Panel in the middle for the canvas, and a PanelResizer between them. Panels scroll internally and the group never scrolls. This is the workspace-register counterpart to SplitPane, which only switches between side-by-side and stacked at a breakpoint and cannot be resized.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Give every docked Panel a fixed width and leave the centre Panel width-less so it absorbs the remaining space.
|
|
16
|
+
- Do: Drive the width with usePanelWidth and pass a storageKey, so a user's layout survives a reload. Spread its bounds onto the PanelResizer rather than repeating min and max by hand.
|
|
17
|
+
- Do: Label every Panel — they are landmarks, and 'Layers' or 'Properties' is what a screen-reader user navigates by.
|
|
18
|
+
- Don't: Let the group scroll. Only panels and the canvas scroll; a scrolling group means the shell lost its height constraint.
|
|
19
|
+
- Don't: Use it for page content columns — that's Grid or Columns. PanelGroup is chrome, and it sits outside the content grid.
|
|
20
|
+
- Don't: Omit PanelResizer to save effort and then let users live with a fixed panel. If a panel holds a tree or a property grid, its width is a preference.
|
|
21
|
+
|
|
22
|
+
### Anatomy
|
|
23
|
+
|
|
24
|
+
| Part | Required | Description |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| PanelGroup | yes | The flex row. Sets min-h-0/min-w-0 so children can actually shrink and scroll. |
|
|
27
|
+
| Panel | yes | One column. Fixed-width when `width` is set, flexible otherwise. Scrolls internally. |
|
|
28
|
+
| PanelResizer | no | The draggable edge between two panels: a 1px hairline with a wider hit area, operable by pointer and keyboard. |
|
|
29
|
+
|
|
30
|
+
## Subcomponents
|
|
31
|
+
|
|
32
|
+
| Name | Type | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Panel | { label: string; width?: number; scroll?: boolean } | A docked or flexible column. `label` names the landmark; omit `width` for the flexible centre. `scroll` defaults to true — set it false for a header + scrolling body + pinned footer, so the footer doesn't scroll away with the list. When you own the layout, give the header and footer shrink-0 and the middle min-h-0 flex-1 overflow-y-auto. overflow-y-auto only activates if every flex ancestor can shrink: a single missing min-h-0 anywhere up the chain makes the panel grow instead of scroll, and nothing reports it. |
|
|
35
|
+
| PanelResizer | { side: 'left' \| 'right'; width: number; onWidthChange: (n: number) => void; label: string; min?: number; max?: number; step?: number } | The draggable edge. `side` says which side the resized panel is on, so the drag delta's sign is right. |
|
|
36
|
+
| usePanelWidth | (defaultWidth: number, options?: { storageKey?: string; min?: number; max?: number }) => readonly [number, (n: number) => void, { min: number; max: number }] | Controlled width for a Panel, clamped and optionally persisted to localStorage (only when `storageKey` is passed). Read through useSyncExternalStore, so there is no server/client hydration mismatch and no setState in an effect; do not hand-roll a localStorage read during render or a write from an effect. Without `storageKey` the width is kept in memory per hook instance. The third element is the `{ min, max }` it clamps to: spread it onto the paired PanelResizer so the separator's aria-valuemin/max and Home/End match the real clamp. |
|
|
37
|
+
|
|
38
|
+
## Tokens used
|
|
39
|
+
|
|
40
|
+
`primary`, `panel`, `hairline`, `hairline-strong`
|
|
41
|
+
|
|
42
|
+
## Accessibility
|
|
43
|
+
|
|
44
|
+
- PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable — a pointer-only resizer is a keyboard trap.
|
|
45
|
+
- Arrow keys resize by `step`; Home and End jump to the minimum and maximum.
|
|
46
|
+
- Each Panel renders an <aside> with an accessible name from `label`, so panels are navigable landmarks.
|
|
47
|
+
|
|
48
|
+
## Related
|
|
49
|
+
|
|
50
|
+
- Components: PanelSection, ScrubField, SplitPane, Sidebar, AppShell
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
### Tool body with a resizable left panel
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Panel, PanelGroup, PanelResizer, PanelSection, PropertyRow, Switch, usePanelWidth } from "@sableui/react"
|
|
58
|
+
|
|
59
|
+
function Body() {
|
|
60
|
+
const [leftWidth, setLeftWidth, leftBounds] = usePanelWidth(240, { storageKey: "app:panel:left", min: 180, max: 360 })
|
|
61
|
+
return (
|
|
62
|
+
<PanelGroup>
|
|
63
|
+
<Panel label="Layers" width={leftWidth}>
|
|
64
|
+
<PanelSection title="Layers">
|
|
65
|
+
<PropertyRow label="Background">
|
|
66
|
+
<Switch checked />
|
|
67
|
+
</PropertyRow>
|
|
68
|
+
</PanelSection>
|
|
69
|
+
</Panel>
|
|
70
|
+
<PanelResizer side="left" width={leftWidth} onWidthChange={setLeftWidth} label="Resize layers panel" {...leftBounds} />
|
|
71
|
+
<Panel label="Canvas">
|
|
72
|
+
<div className="grid h-full place-items-center bg-canvas text-ui-body text-muted-foreground">Canvas</div>
|
|
73
|
+
</Panel>
|
|
74
|
+
</PanelGroup>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Both edges resizable
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { Panel, PanelGroup, PanelResizer, PanelSection, PropertyPair, ScrubField, usePanelWidth } from "@sableui/react"
|
|
83
|
+
|
|
84
|
+
function Body() {
|
|
85
|
+
const [left, setLeft, leftBounds] = usePanelWidth(240, { storageKey: "app:left" })
|
|
86
|
+
const [right, setRight, rightBounds] = usePanelWidth(256, { storageKey: "app:right" })
|
|
87
|
+
return (
|
|
88
|
+
<PanelGroup>
|
|
89
|
+
<Panel label="Layers" width={left} />
|
|
90
|
+
<PanelResizer side="left" width={left} onWidthChange={setLeft} label="Resize layers panel" {...leftBounds} />
|
|
91
|
+
<Panel label="Canvas" />
|
|
92
|
+
<PanelResizer side="right" width={right} onWidthChange={setRight} label="Resize properties panel" {...rightBounds} />
|
|
93
|
+
<Panel label="Properties" width={right}>
|
|
94
|
+
<PanelSection title="Blending">
|
|
95
|
+
<PropertyPair>
|
|
96
|
+
<ScrubField label="Opacity" icon="grid" suffix="%" value={100} min={0} max={100} />
|
|
97
|
+
<ScrubField label="Blur" value={0} min={0} max={64} suffix="px" />
|
|
98
|
+
</PropertyPair>
|
|
99
|
+
</PanelSection>
|
|
100
|
+
</Panel>
|
|
101
|
+
</PanelGroup>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Fixed panel, no resizer
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { Button, Icon, Panel, PanelGroup } from "@sableui/react"
|
|
110
|
+
|
|
111
|
+
<PanelGroup>
|
|
112
|
+
<Panel label="Tools" width={56}>
|
|
113
|
+
<div className="flex flex-col gap-1 p-1">
|
|
114
|
+
<Button size="icon-xs" variant="ghost" aria-label="Toggle grid"><Icon name="grid" /></Button>
|
|
115
|
+
<Button size="icon-xs" variant="ghost" aria-label="Hide"><Icon name="eye" /></Button>
|
|
116
|
+
</div>
|
|
117
|
+
</Panel>
|
|
118
|
+
<Panel label="Canvas" />
|
|
119
|
+
</PanelGroup>
|
|
120
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# PanelSection
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A flush group of property rows inside a docked panel — eyebrow header, hairline separator, rows beneath. The workspace register's answer to Card, which does not belong in a panel.
|
|
6
|
+
|
|
7
|
+
**Keywords:** panel, section, inspector, properties, property grid, workspace, tool, chrome, sidebar
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use PanelSection to group properties inside an inspector or docked panel, with PropertyRow for one label-plus-control line and PropertyPair for two controls side by side. This is the workspace register's grouping primitive: it separates with a hairline and nothing else, because a Card inside a 240px panel spends its border and inset on nothing and pushes every row inward.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Group related properties under a short functional title — Blending, Layout, Fill — the way a tool inspector names its sections.
|
|
16
|
+
- Do: Reach for PropertyPair before giving each property its own full-width row; pairing halves the vertical cost in a narrow panel.
|
|
17
|
+
- Do: Put a section-scoped control (a visibility toggle, an add action) in the action slot rather than inventing a second header row.
|
|
18
|
+
- Don't: Wrap panel content in Card. Cards are page-register furniture; in a panel they read as a page pasted into chrome.
|
|
19
|
+
- Don't: Repeat the panel's own title as the section title. If the header above already names the selection, title the section by what it controls (Parameters, Blending).
|
|
20
|
+
- Don't: Hand-roll a property line (`<div className="flex justify-between"><span>Opacity</span><Slider /></div>`). Use PropertyRow for every label-plus-control line: when each property type invents its own arrangement, a panel of fifteen properties ends up with fifteen layouts.
|
|
21
|
+
- Don't: Use PropertyRow for page forms — use Field, which carries label/description/error semantics. PropertyRow is a dense chrome row, not a form control.
|
|
22
|
+
|
|
23
|
+
### Anatomy
|
|
24
|
+
|
|
25
|
+
| Part | Required | Description |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Header | yes | The section title in the UI section tier, with an optional trailing action. |
|
|
28
|
+
| PropertyRow | no | One property: label left, control right. `stacked` moves the control to its own line for full-width controls. |
|
|
29
|
+
| PropertyPair | no | Two controls side by side, with optional captions above each. |
|
|
30
|
+
|
|
31
|
+
## Subcomponents
|
|
32
|
+
|
|
33
|
+
| Name | Type | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| PropertyRow | { label: ReactNode; htmlFor?: string; hint?: string; stacked?: boolean; scrub?: { value, onValueChange, step?, min?, max?, onGestureStart?, onGestureEnd?, disabled? } } | One property line. `hint` becomes the label's hover explanation; `stacked` puts the control on its own line. `scrub` makes the property NAME a drag handle for its value — the Figma/Framer/Paper convention, and the answer for a numeric row with no honest glyph to put in a ScrubField (a schema-generated `levels` or `seed`, where a `#` would be noise pretending to be meaning). Opt-in and additive: a row without it is unchanged. The label stays a real `<label htmlFor>` — a 2px jitter threshold keeps a click a click, and the click synthesised after a real drag is swallowed so a scrub does not also select the field's text. |
|
|
36
|
+
| PropertyPair | { labels?: [ReactNode, ReactNode] } | Two controls side by side in a 2-up grid, with optional captions above each. |
|
|
37
|
+
|
|
38
|
+
## Props
|
|
39
|
+
|
|
40
|
+
| Name | Type | Default | Description |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| title | ReactNode | — | Section name, rendered as the header in the UI section tier. |
|
|
43
|
+
| action | ReactNode | — | Optional trailing control in the header — a visibility toggle, an add action. |
|
|
44
|
+
|
|
45
|
+
## Tokens used
|
|
46
|
+
|
|
47
|
+
`foreground`, `muted-foreground`, `hairline`
|
|
48
|
+
|
|
49
|
+
## Accessibility
|
|
50
|
+
|
|
51
|
+
- PropertyRow renders a real <label>; pass htmlFor with the control's id so clicking the label focuses it.
|
|
52
|
+
- The section title is an h3, so panels contribute a real heading outline rather than styled divs.
|
|
53
|
+
- `hint` is exposed as the label's title attribute — keep the essential meaning in the label itself, since title text is not reliably announced.
|
|
54
|
+
- A `scrub` label does not replace keyboard editing and must not be the only way to set the value: the drag is a pointer affordance, so the control it names still has to accept typing and arrow keys. It also keeps its `htmlFor` behaviour, which is the reason the gesture arms on a 2px threshold rather than on pointerdown.
|
|
55
|
+
|
|
56
|
+
## Related
|
|
57
|
+
|
|
58
|
+
- Components: Workspace, PanelGroup, ScrubField, StatusBar, Field, Card
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
### Blending section, 2-up
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { PanelSection, PropertyPair, ScrubField, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
|
|
66
|
+
|
|
67
|
+
<PanelSection title="Blending">
|
|
68
|
+
<PropertyPair>
|
|
69
|
+
<ScrubField label="Opacity" value={100} min={0} max={100} suffix="%" />
|
|
70
|
+
<Select value="normal">
|
|
71
|
+
<SelectTrigger size="xs" />
|
|
72
|
+
<SelectContent>
|
|
73
|
+
<SelectItem value="normal">Normal</SelectItem>
|
|
74
|
+
<SelectItem value="multiply">Multiply</SelectItem>
|
|
75
|
+
</SelectContent>
|
|
76
|
+
</Select>
|
|
77
|
+
</PropertyPair>
|
|
78
|
+
</PanelSection>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Rows with a stacked slider
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { PanelSection, PropertyRow, Select, SelectContent, SelectItem, SelectTrigger, Slider, Switch } from "@sableui/react"
|
|
85
|
+
|
|
86
|
+
<PanelSection title="Parameters">
|
|
87
|
+
<PropertyRow label="Block size" hint="Block edge length in reference pixels." stacked>
|
|
88
|
+
<Slider value={32} min={4} max={256} />
|
|
89
|
+
</PropertyRow>
|
|
90
|
+
<PropertyRow label="Direction">
|
|
91
|
+
<Select value="both">
|
|
92
|
+
<SelectTrigger size="xs" />
|
|
93
|
+
<SelectContent>
|
|
94
|
+
<SelectItem value="both">Both</SelectItem>
|
|
95
|
+
<SelectItem value="horizontal">Horizontal</SelectItem>
|
|
96
|
+
</SelectContent>
|
|
97
|
+
</Select>
|
|
98
|
+
</PropertyRow>
|
|
99
|
+
<PropertyRow label="Reverse">
|
|
100
|
+
<Switch checked={false} />
|
|
101
|
+
</PropertyRow>
|
|
102
|
+
</PanelSection>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Section with a header action
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { Button, Icon, PanelSection, PropertyRow, ScrubField } from "@sableui/react"
|
|
109
|
+
|
|
110
|
+
<PanelSection
|
|
111
|
+
title="Blending"
|
|
112
|
+
action={
|
|
113
|
+
<Button size="icon-xs" variant="ghost" aria-label="Hide layer">
|
|
114
|
+
<Icon name="eye" />
|
|
115
|
+
</Button>
|
|
116
|
+
}
|
|
117
|
+
>
|
|
118
|
+
<PropertyRow label="Opacity">
|
|
119
|
+
<ScrubField label="Opacity" value={60} min={0} max={100} suffix="%" />
|
|
120
|
+
</PropertyRow>
|
|
121
|
+
</PanelSection>
|
|
122
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Phone
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A current smartphone frame at iPhone proportions: a dark body with a titanium band, thin bezels, an island, an optional status bar and a scrollable screen.
|
|
6
|
+
|
|
7
|
+
**Keywords:** phone, device mockup, mobile preview, frame
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Phone to frame a mobile product preview when the device context is useful.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for mobile UI demos, screenshots, and responsive previews.
|
|
16
|
+
- Do: Keep the framed content legible and avoid stuffing full desktop layouts into the phone frame.
|
|
17
|
+
- Don't: Do not use Phone as a generic panel, card, or production app shell.
|
|
18
|
+
- Don't: Do not use it to test responsive behaviour. The frame is a fixed aspect box, not a viewport, so media queries inside key off the real window.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Name | Type | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| children | ReactNode | — | Content rendered inside the scrollable screen region. |
|
|
25
|
+
| statusBar | boolean | true | Draw a status bar (time, signal, battery) across the top of the screen. Turn it off to frame a screenshot that already has one. |
|
|
26
|
+
| systemChrome | boolean | true | Draw the camera pill and home indicator. Turn it off when the content draws its own system chrome, such as an iOS prototype framed by PhoneFrame from @sableui/react/ios/preview. |
|
|
27
|
+
| className | string | — | Additional classes merged onto the outer wrapper (set the width here; the screen keeps its proportions). |
|
|
28
|
+
|
|
29
|
+
## Tokens used
|
|
30
|
+
|
|
31
|
+
`background`, `foreground`
|
|
32
|
+
|
|
33
|
+
## Accessibility
|
|
34
|
+
|
|
35
|
+
- Purely presentational frame; the screen region scrolls when content overflows. Add headings/landmarks in the content as needed.
|
|
36
|
+
- The frame's chrome (status bar, camera, buttons) is aria-hidden, so the content inside carries the entire accessible meaning. A screenshot with no alt text makes the whole mockup announce as nothing at all.
|
|
37
|
+
|
|
38
|
+
## Related
|
|
39
|
+
|
|
40
|
+
- Components: Browser
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Framing a mobile layout
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { Phone } from "@sableui/react"
|
|
48
|
+
|
|
49
|
+
<Phone><div className="p-4"><h2 className="text-title-lg">Activity</h2><p className="text-body-sm text-muted-foreground">3 new collects today.</p></div></Phone>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Mockup, not a responsive preview
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { Phone } from "@sableui/react"
|
|
56
|
+
|
|
57
|
+
// Phone is DECORATIVE framing for a mobile screenshot on a marketing surface.
|
|
58
|
+
|
|
59
|
+
<Phone>
|
|
60
|
+
<img src="/screenshots/mobile.png" alt="The mobile app" />
|
|
61
|
+
</Phone>
|
|
62
|
+
|
|
63
|
+
// ✗ Using it to test responsive layout. The frame is a fixed aspect box, not a
|
|
64
|
+
// viewport — media queries inside it key off the real window, so what you see
|
|
65
|
+
// is not what a phone renders. Resize the browser, or use device emulation.
|
|
66
|
+
// ✗ Wrapping live, interactive app UI (forms, buttons that act). Same as Browser: the frame says "picture of
|
|
67
|
+
// software", so real controls inside it read as a screenshot that moves. Static composed content is fine.
|
|
68
|
+
// The exception is a clickable iOS prototype: PhoneFrame (@sableui/react/ios/preview)
|
|
69
|
+
// frames a whole phone-sized document in an iframe, where the frame is the device.
|
|
70
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Pill
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance="solid" (filters; selected lifts onto the primary surface) or "outline" (quiet chips that annotate content, like reactions); size md or sm. A static label is a Badge.
|
|
6
|
+
|
|
7
|
+
**Keywords:** pill, chip, filter, tag, toggle chip, category
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Pill for a row of filters or choices a person clicks to narrow or toggle what they see. For a label that only describes something, use Badge.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use short text that names the filter or choice.
|
|
16
|
+
- Do: Reflect the current choice with selected, so the active filter reads at a glance.
|
|
17
|
+
- Don't: Do not use Pill as a static label. A pressable-looking chip that does nothing is the most common cause of dead clicks in a filter row — that is a Badge.
|
|
18
|
+
- Don't: Do not use Pill for primary actions or important system status.
|
|
19
|
+
- Don't: Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance="outline" (and size="sm" for a small one); an override is a look that exists nowhere else in the system.
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Name | Type | Default | Description |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| selected | boolean | false | Marks the pill as chosen. Solid lifts onto the primary surface; outline takes a primary tint and soft primary edge. Sets aria-pressed. |
|
|
26
|
+
| appearance | "solid" \| "outline" | "solid" | solid for a filter row; outline for quiet chips that annotate other content (a reaction under a message, a tag on a card), where a filled chip would compete with what it annotates. |
|
|
27
|
+
| size | "md" \| "sm" | "md" | sm sits on the density ladder's xs control height, for chips under or inside other content. |
|
|
28
|
+
| icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. |
|
|
29
|
+
|
|
30
|
+
## Variants
|
|
31
|
+
|
|
32
|
+
- **appearance**: solid, outline
|
|
33
|
+
- **size**: md, sm
|
|
34
|
+
- **selected**: true, false
|
|
35
|
+
|
|
36
|
+
## Tokens used
|
|
37
|
+
|
|
38
|
+
`secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `primary`, `primary-foreground`, `card`, `border`, `muted-foreground`, `foreground`
|
|
39
|
+
|
|
40
|
+
## Accessibility
|
|
41
|
+
|
|
42
|
+
- Renders a native <button type="button"> for focus and keyboard activation.
|
|
43
|
+
- Selected state is exposed via aria-pressed.
|
|
44
|
+
|
|
45
|
+
## Related
|
|
46
|
+
|
|
47
|
+
- Components: Badge, ToggleGroup
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
### Filter row
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { Pill, Row } from "@sableui/react"
|
|
55
|
+
|
|
56
|
+
<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Editions</Pill><Pill onClick={() => {}}>Collectibles</Pill></Row>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Outline, small (reactions)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { Pill, Row } from "@sableui/react"
|
|
63
|
+
|
|
64
|
+
<Row gap={1}><Pill appearance="outline" size="sm" selected onClick={() => {}}>🎉 3</Pill><Pill appearance="outline" size="sm" onClick={() => {}}>👀</Pill></Row>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### With icon
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Icon, Pill } from "@sableui/react"
|
|
71
|
+
|
|
72
|
+
<Pill icon={<Icon name="lock" />} onClick={() => {}}>Locked</Pill>
|
|
73
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Click-triggered overlay over Base UI Popover (focus management, collision-aware positioning, Esc/outside dismissal). Holds interactive content; 18rem-wide surface.
|
|
6
|
+
|
|
7
|
+
**Keywords:** popover, floating panel, anchored, disclosure
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Popover for anchored secondary content that needs more room than a tooltip but less commitment than a dialog.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for small pickers, summaries, or contextual controls tied to a trigger.
|
|
16
|
+
- Do: Keep focus behavior and dismissal predictable.
|
|
17
|
+
- Don't: Do not use Popover for blocking confirmations, long forms, or critical information.
|
|
18
|
+
- Don't: Do not put a form that can fail validation inside one. A popover dismisses on outside click, which discards what the user typed with no warning — that is a Dialog.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| PopoverTrigger | yes | The element that anchors and opens the popover. |
|
|
25
|
+
| PopoverContent | yes | The floating content surface. |
|
|
26
|
+
| PopoverTitle | no | An optional heading for the content. |
|
|
27
|
+
| PopoverDescription | no | Optional supporting copy. |
|
|
28
|
+
| PopoverClose | no | An explicit close control when helpful. |
|
|
29
|
+
| PopoverArrow | no | An optional arrow pointing to the trigger. |
|
|
30
|
+
|
|
31
|
+
## Subcomponents
|
|
32
|
+
|
|
33
|
+
| Name | Type | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| PopoverTrigger | Base UI Trigger props + render | The anchor element. Use render={<Button/>} to compose with another component. |
|
|
36
|
+
| PopoverContent | Base UI Popup props + side: "top" \| "bottom" \| "left" \| "right" \| "inline-*" + align: "start" \| "center" \| "end" + sideOffset: number + anchor: Element \| Ref \| (() => Element) \| VirtualElement + className | Portals + positions + paints the popup. side picks which side of the trigger to render against (auto-flips on collision; defaults to "bottom"); align sets alignment along that side (defaults to "center"); sideOffset is the gap in px (defaults to 8). anchor positions against a different element/ref/getter than the trigger — Base UI 1.5 has no separate Anchor part, so pass it here to decouple the open-location from the trigger. |
|
|
37
|
+
| PopoverTitle | Base UI Title props + className | Accessible heading; Base UI wires aria-labelledby to the popup. |
|
|
38
|
+
| PopoverDescription | Base UI Description props + className | Supplementary copy; Base UI wires aria-describedby to the popup. |
|
|
39
|
+
| PopoverClose | Base UI Close props + render | A button that closes the popover. Use render={<Button/>} to compose. |
|
|
40
|
+
| PopoverArrow | Base UI Arrow props + className | Optional pointer connecting the popup to its trigger. Place inside PopoverContent. |
|
|
41
|
+
|
|
42
|
+
## Props
|
|
43
|
+
|
|
44
|
+
| Name | Type | Default | Description |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| Popover (Root) | Base UI Popover.Root props (open, defaultOpen, onOpenChange, modal) | — | Groups the trigger and content. Uncontrolled by default. |
|
|
47
|
+
|
|
48
|
+
## Tokens used
|
|
49
|
+
|
|
50
|
+
`popover`, `popover-foreground`, `border-floating`, `muted-foreground`
|
|
51
|
+
|
|
52
|
+
## Accessibility
|
|
53
|
+
|
|
54
|
+
- Base UI manages focus into and out of the popup, traps focus when modal, and restores focus to the trigger on close.
|
|
55
|
+
- Opens on trigger press (click/Enter/Space); dismisses on Esc and outside press.
|
|
56
|
+
- PopoverTitle/PopoverDescription wire aria-labelledby/aria-describedby on the popup.
|
|
57
|
+
- Enters and exits on the motion-pop recipe (Base UI starting/ending-style attributes), so it follows the motion policy.
|
|
58
|
+
|
|
59
|
+
## Related
|
|
60
|
+
|
|
61
|
+
- Components: Tooltip, ContextCard, Dialog
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
### Basic
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Button, Popover, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "@sableui/react"
|
|
69
|
+
|
|
70
|
+
<Popover><PopoverTrigger render={<Button variant="secondary">Details</Button>} /><PopoverContent><PopoverTitle>Edition details</PopoverTitle><PopoverDescription>A limited run of 100, stored permanently on Arweave.</PopoverDescription></PopoverContent></Popover>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### With close
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { Button, Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "@sableui/react"
|
|
77
|
+
|
|
78
|
+
<Popover><PopoverTrigger render={<Button variant="secondary">Open</Button>} /><PopoverContent><PopoverTitle>Heads up</PopoverTitle><PopoverDescription>Collecting mints a compressed NFT to your wallet.</PopoverDescription><PopoverClose render={<Button size="sm">Got it</Button>} /></PopoverContent></Popover>
|
|
79
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Linear progress bar over Base UI Progress — value 0–100 drives the fill; omit value (or pass null) for an indeterminate, pulsing bar.
|
|
6
|
+
|
|
7
|
+
**Keywords:** progress, task progress, meter, loading
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Progress to communicate determinate or indeterminate completion for a task or process.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use determinate values when progress can be measured.
|
|
16
|
+
- Do: Pair the visual indicator with text when users need context or exact values.
|
|
17
|
+
- Don't: Do not use Progress as a decorative divider or status color bar.
|
|
18
|
+
- Don't: Do not use a determinate Progress for an unknown duration. A bar that sits at 90% is worse than a Spinner, because it makes a promise about time that you cannot keep.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| ProgressTrack | yes | The background track that defines the range. |
|
|
25
|
+
| ProgressIndicator | yes | The filled or animated progress indicator. |
|
|
26
|
+
| ProgressLabel | no | A text label for the progress task. |
|
|
27
|
+
| ProgressValue | no | A readable value such as 45 percent. |
|
|
28
|
+
|
|
29
|
+
## Subcomponents
|
|
30
|
+
|
|
31
|
+
| Name | Type | Description |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| ProgressTrack | Base UI Track props + className | The rail. Rendered automatically when Progress has no children; renders a ProgressIndicator child by default. Takes `size` (inherited from Progress) for its thickness — use that rather than a className height. |
|
|
34
|
+
| ProgressIndicator | Base UI Indicator props + className | The fill. Width is set by Base UI from the value; spans full width and pulses while indeterminate. |
|
|
35
|
+
| ProgressLabel | Base UI Label props + className | Accessible label; wires aria-labelledby on the Root. |
|
|
36
|
+
| ProgressValue | Base UI Value props + className | Formatted value readout. Accepts a render-function child for custom formatting. |
|
|
37
|
+
|
|
38
|
+
## Props
|
|
39
|
+
|
|
40
|
+
| Name | Type | Default | Description |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| value | number \| null | null | Current value between min and max. null (or omitted) renders the indeterminate state. |
|
|
43
|
+
| min | number | 0 | The minimum value of the range. |
|
|
44
|
+
| max | number | 100 | The maximum value of the range. |
|
|
45
|
+
| format | Intl.NumberFormatOptions | — | Formatting options for ProgressValue (e.g. style: 'percent', or a currency). |
|
|
46
|
+
| getAriaValueText | (formatted, value) => string | — | Returns a human-readable text alternative for aria-valuenow. |
|
|
47
|
+
| size | "sm" \| "md" \| "lg" | "md" | Bar thickness, on the same three rails as Meter so the two match on one page. ProgressTrack inherits it. |
|
|
48
|
+
|
|
49
|
+
## Variants
|
|
50
|
+
|
|
51
|
+
- **size**: sm, md, lg
|
|
52
|
+
|
|
53
|
+
## Tokens used
|
|
54
|
+
|
|
55
|
+
`secondary`, `primary`, `foreground`, `muted-foreground`
|
|
56
|
+
|
|
57
|
+
## Motion
|
|
58
|
+
|
|
59
|
+
- **arrival** · Root · _no motion_ — A progress bar appears because work started. Staging its entrance delays the one piece of information the reader is waiting for.
|
|
60
|
+
- **response** · Root · _no motion_ — It reports; it does not accept input. A bar that reacted to the pointer would read as a Slider, which is a control the reader can drag.
|
|
61
|
+
- **transition** · ProgressIndicator · _custom (outside the recipe layer)_ — Fill width eases between values on the motion tokens (no recipe animates width), so the bar reads as filling rather than as a series of unrelated widths. It matters most where progress arrives in coarse jumps — four chunked upload callbacks look like a glitching bar without it.
|
|
62
|
+
- **activity** · ProgressIndicator · `motion-pulse` — Indeterminate (value null) has no width to animate, so the pulse carries the whole signal that work is still happening. Without it the indeterminate state is a static bar — indistinguishable from a stalled determinate one.
|
|
63
|
+
|
|
64
|
+
## Accessibility
|
|
65
|
+
|
|
66
|
+
- Base UI sets role=progressbar with aria-valuenow / aria-valuemin / aria-valuemax and reports status to screen readers.
|
|
67
|
+
- Indeterminate state (value null) omits aria-valuenow per the ARIA spec.
|
|
68
|
+
- Pair ProgressLabel with the bar for a visible, associated name; ProgressValue exposes the formatted value.
|
|
69
|
+
|
|
70
|
+
## Related
|
|
71
|
+
|
|
72
|
+
- Components: Spinner, Skeleton
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
### Determinate
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { Progress } from "@sableui/react"
|
|
80
|
+
|
|
81
|
+
<Progress value={64} aria-label="Upload" />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Indeterminate
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { Progress } from "@sableui/react"
|
|
88
|
+
|
|
89
|
+
<Progress aria-label="Preparing export" />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### With label and value
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { Progress, ProgressLabel, ProgressTrack, ProgressValue } from "@sableui/react"
|
|
96
|
+
|
|
97
|
+
<Progress value={40}><div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1"><ProgressLabel>Uploading</ProgressLabel><ProgressValue /></div><ProgressTrack /></Progress>
|
|
98
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# RadioCard
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A selectable card option (radio-card) over Base UI RadioGroup + Radio. RadioGroup shares selection state; each RadioCard is a card with a radio indicator, title, and description. Add an icon to switch to the richer two-dimensional card, and a tone to give the selected state a semantic colour identity.
|
|
6
|
+
|
|
7
|
+
**Keywords:** radio-card, choice card, selection, option card
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use RadioCard for visual choice cards when users compare a small set of named options.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it when labels, descriptions, or icons make options easier to understand.
|
|
16
|
+
- Do: Make the selected state explicit and preserve keyboard access through the group.
|
|
17
|
+
- Don't: Do not use RadioCard for dense menus, long lists, or simple yes/no settings.
|
|
18
|
+
- Don't: Do not use a single RadioCard on its own. It is a radio, so one option cannot be deselected — a lone one is a switch, and should be a Switch.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| RadioCard | yes | One selectable option card. |
|
|
25
|
+
| RadioGroup | no | A grouped set of related choice cards. |
|
|
26
|
+
|
|
27
|
+
## Subcomponents
|
|
28
|
+
|
|
29
|
+
| Name | Type | Description |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| RadioGroup | Base UI RadioGroup props (value, defaultValue, onValueChange, name, disabled, readOnly, required) + className | The group container that wraps the RadioCard cards and shares their selection state. Controlled via value + onValueChange; uncontrolled via defaultValue. Lays its cards out in a single column by default; override with className (e.g. 'grid sm:grid-cols-3') for a grid — the group is a flex column, so a grid-cols-* class alone does nothing. |
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Name | Type | Default | Description |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| value | string | — | The unique value that pairs this card with the group's selected value. |
|
|
38
|
+
| title | ReactNode | — | The card's primary label (rendered as text-title-sm). |
|
|
39
|
+
| description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
|
|
40
|
+
| icon | ReactNode | — | Leading icon (e.g. a solid <Icon>). Its presence switches to the two-dimensional card — icon top-left, indicator top-right, copy below — and the glyph is rendered as a solid-fill icon in the tone colour. |
|
|
41
|
+
| tone | "neutral" \| "standard" \| "collectible" \| "edition" \| "info" \| "warning" | "neutral" | Semantic colour the selected state carries across the border, radio, and icon glyph. Neutral uses the primary ring with a muted wash. |
|
|
42
|
+
| disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
|
|
43
|
+
| titleClassName | string | — | Merged onto the title span (default text-title-sm) — e.g. to tighten copy in a two-column layout. |
|
|
44
|
+
| descriptionClassName | string | — | Merged onto the description span (default text-body-sm text-muted-foreground). |
|
|
45
|
+
|
|
46
|
+
## Variants
|
|
47
|
+
|
|
48
|
+
- **tone**: neutral, standard, collectible, edition, info, warning
|
|
49
|
+
|
|
50
|
+
## Tokens used
|
|
51
|
+
|
|
52
|
+
`input`, `card`, `foreground`, `primary`, `muted`, `ring`, `muted-foreground`, `tone-standard`, `tone-collectible`, `tone-edition`, `tone-info`, `tone-warning`
|
|
53
|
+
|
|
54
|
+
## Accessibility
|
|
55
|
+
|
|
56
|
+
- Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.
|
|
57
|
+
- Arrow keys move and select between cards; Space selects the focused card; Tab enters/leaves the group.
|
|
58
|
+
- Checked state styling reads from the data-[checked] attribute; disabled cards from data-disabled.
|
|
59
|
+
- focus-within draws a 2px ring on the focused card; the whole card is the hit target.
|
|
60
|
+
- The icon is decorative — the title labels the card — so selection meaning never relies on colour alone (the border ring and the filled radio both shift; the neutral tone adds a muted fill).
|
|
61
|
+
|
|
62
|
+
## Related
|
|
63
|
+
|
|
64
|
+
- Components: Radio, CheckboxCard, Select
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
### Post type (tone + icon)
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Icon, RadioCard, RadioGroup } from "@sableui/react"
|
|
72
|
+
|
|
73
|
+
<RadioGroup defaultValue="post" className="grid sm:grid-cols-3"><RadioCard value="post" tone="standard" icon={<Icon name="plus" />} title="Standard" description="Share without minting" /><RadioCard value="collectible" tone="collectible" icon={<Icon name="gem" />} title="Collectible" description="Free to collect as NFT" /><RadioCard value="edition" tone="edition" icon={<Icon name="image-stack" />} title="Edition" description="Sell as NFT editions" /></RadioGroup>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Plan picker (plain)
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { RadioCard, RadioGroup } from "@sableui/react"
|
|
80
|
+
|
|
81
|
+
<RadioGroup defaultValue="pro"><RadioCard value="free" title="Free" description="For trying things out." /><RadioCard value="pro" title="Pro" description="For growing teams." /><RadioCard value="enterprise" title="Enterprise" description="Custom limits and support." /></RadioGroup>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### With a disabled option
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { RadioCard, RadioGroup } from "@sableui/react"
|
|
88
|
+
|
|
89
|
+
<RadioGroup defaultValue="free"><RadioCard value="free" title="Free" description="For trying things out." /><RadioCard value="enterprise" title="Enterprise" description="Contact sales to enable." disabled /></RadioGroup>
|
|
90
|
+
```
|