@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,139 @@
|
|
|
1
|
+
# SettingsMenu
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A compact trigger that states the current settings, over a menu of rows that change them. Each row shows its own value and opens a submenu.
|
|
6
|
+
|
|
7
|
+
**Keywords:** settings, model picker, model switcher, options menu, preferences, submenu, quality settings, ai chat
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
For a cluster of related settings that a reader checks often and changes occasionally — the model and effort on an AI composer, quality and format on an export control, density and sort on a table. It encodes one shape: summarise the state on the trigger, then open to a list of name → current value → submenu. It knows nothing about any particular domain; the AI-model case is one instance of it.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Put the current values in `summary`, not a noun. "5.6 Sol High" tells the reader where they are; a trigger reading "Settings" makes them open it to find out, which is the wrong trade for a control glanced at twenty times a session.
|
|
16
|
+
- Do: Give every SettingsRow a `value`. One glance at the open menu should answer every setting at once — a menu of bare names is a menu you have to explore.
|
|
17
|
+
- Do: Use `MenuRadioGroup` + `MenuRadioItem` inside a row for single-choice settings, so the current option is checked rather than merely remembered.
|
|
18
|
+
- Do: Reach for `variant="subtle"` when the trigger sits on a surface where a ghost control would float unanchored — inside a ChatComposer, for instance.
|
|
19
|
+
- Don't: Do not use it for a single setting. One choice is a `Select`; the nesting only earns its cost when there are several related settings to summarise.
|
|
20
|
+
- Don't: Do not put actions in it. Rows change state; a menu that mixes "Effort ›" with "Delete conversation" makes destructive commands look like preferences. That is `Menu` or `DotsMenu`.
|
|
21
|
+
- Don't: Do not nest more than one level deep. Past that the reader loses the path back, and the thing you have is a settings dialog wearing a menu.
|
|
22
|
+
- Don't: Do not override the upward, end-aligned default without reason. Its home is the bottom of a composer or toolbar, where a downward menu opens off-screen.
|
|
23
|
+
|
|
24
|
+
### Anatomy
|
|
25
|
+
|
|
26
|
+
| Part | Required | Description |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Trigger | yes | The summary control. States the current settings and carries a chevron; quiet at rest, so it does not compete with the primary action beside it. |
|
|
29
|
+
| SettingsRow | yes | One setting: its name, its current value, and a submenu of options. Without children it renders flat, for a row that is a statement rather than a choice. |
|
|
30
|
+
| Submenu contents | no | Usually a MenuRadioGroup of MenuRadioItems, so the active option is checked. |
|
|
31
|
+
|
|
32
|
+
## Subcomponents
|
|
33
|
+
|
|
34
|
+
| Name | Type | Description |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| SettingsRow | { label: ReactNode; value?: ReactNode; disabled?: boolean; children?: ReactNode } | One setting. With children it opens a submenu; without them it renders as a flat statement row. |
|
|
37
|
+
| MenuRadioGroup | { value?: string; defaultValue?: string; onValueChange?: (value: string) => void } | Single-choice group for a row's submenu. Exported from Menu; re-used here rather than duplicated. |
|
|
38
|
+
| MenuRadioItem | { value: string; disabled?: boolean } | One option, with a check in a reserved leading column so labels do not shift as the highlight moves. |
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
| Name | Type | Default | Description |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| summary | ReactNode | — | What the trigger reads when closed. Give it the current values, not a noun — the control should answer before it is opened. |
|
|
45
|
+
| label | string | "Change settings" | Accessible name for the trigger, since the visible summary is data rather than a label. |
|
|
46
|
+
| variant | "ghost" \| "subtle" | "ghost" | Resting treatment. `ghost` disappears into the surface at rest and shows its affordance on hover — right for a toolbar. `subtle` adds a fill for surfaces where a ghost trigger would float unanchored. |
|
|
47
|
+
| size | "xs" \| "sm" | "sm" | Trigger height, read off the density ladder so a compact scope retunes it with every other control. |
|
|
48
|
+
| side | "top" \| "bottom" \| "inline-start" \| "inline-end" | "top" | Which side the menu opens on. Upward by default — its home is the bottom of a composer. |
|
|
49
|
+
| align | "start" \| "center" \| "end" | "end" | Alignment along that side. |
|
|
50
|
+
| disabled | boolean | — | Disable the trigger. |
|
|
51
|
+
| ...Menu props | Omit<ComponentProps<typeof Menu>, 'children'> | — | `open` / `defaultOpen` / `onOpenChange` and the rest of Base UI's menu root props. |
|
|
52
|
+
|
|
53
|
+
## Variants
|
|
54
|
+
|
|
55
|
+
- **variant**: ghost, subtle
|
|
56
|
+
- **size**: xs, sm
|
|
57
|
+
|
|
58
|
+
## Tokens used
|
|
59
|
+
|
|
60
|
+
`foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `popover`, `popover-foreground`, `border`, `ring`
|
|
61
|
+
|
|
62
|
+
## Accessibility
|
|
63
|
+
|
|
64
|
+
- Base UI's Menu underneath, so roving focus, typeahead, Esc and outside-dismissal, and the full menu/menuitemradio ARIA wiring come for free — including submenu traversal with the arrow keys.
|
|
65
|
+
- The trigger carries an explicit `label`, because its visible text is the current state rather than a name: without it, a screen reader announces "5.6 Sol High" with no indication that it is a control or what it changes.
|
|
66
|
+
- Radio rows are `menuitemradio` with a real checked state, so the current choice is conveyed to assistive tech rather than only by a visible tick.
|
|
67
|
+
- The check occupies a reserved column instead of appearing on selection, so labels never shift sideways as the highlight moves — a change that reads as motion to anyone tracking position.
|
|
68
|
+
- Submenus open on the inline-end side, which flips correctly in RTL rather than always opening right.
|
|
69
|
+
|
|
70
|
+
## Related
|
|
71
|
+
|
|
72
|
+
- Components: Menu, DotsMenu, Select, ChatComposer, Popover
|
|
73
|
+
- Patterns: ChatPanel
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### A model switcher in a composer
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { ChatComposer, MenuRadioGroup, MenuRadioItem, SettingsMenu, SettingsRow } from "@sableui/react"
|
|
81
|
+
|
|
82
|
+
<ChatComposer
|
|
83
|
+
onSend={send}
|
|
84
|
+
actions={
|
|
85
|
+
<SettingsMenu variant="subtle" size="xs" summary={<><span className="text-foreground">{model.name}</span>{" "}<span className="text-muted-foreground">{effort}</span></>}>
|
|
86
|
+
<SettingsRow label="Model" value={model.name}>
|
|
87
|
+
<MenuRadioGroup value={model.id} onValueChange={setModelId}>
|
|
88
|
+
<MenuRadioItem value="sol">5.6 Sol</MenuRadioItem>
|
|
89
|
+
<MenuRadioItem value="lite">5.6 Lite</MenuRadioItem>
|
|
90
|
+
</MenuRadioGroup>
|
|
91
|
+
</SettingsRow>
|
|
92
|
+
<SettingsRow label="Effort" value={effort}>
|
|
93
|
+
<MenuRadioGroup value={effort} onValueChange={setEffort}>
|
|
94
|
+
<MenuRadioItem value="Low">Low</MenuRadioItem>
|
|
95
|
+
<MenuRadioItem value="High">High</MenuRadioItem>
|
|
96
|
+
</MenuRadioGroup>
|
|
97
|
+
</SettingsRow>
|
|
98
|
+
</SettingsMenu>
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Export options — the same shape, no chat involved
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { MenuRadioGroup, MenuRadioItem, SettingsMenu, SettingsRow } from "@sableui/react"
|
|
107
|
+
|
|
108
|
+
<SettingsMenu summary={`${format} · ${quality}`} side="bottom" align="start">
|
|
109
|
+
<SettingsRow label="Format" value={format}>
|
|
110
|
+
<MenuRadioGroup value={format} onValueChange={setFormat}>
|
|
111
|
+
<MenuRadioItem value="PNG">PNG</MenuRadioItem>
|
|
112
|
+
<MenuRadioItem value="SVG">SVG</MenuRadioItem>
|
|
113
|
+
</MenuRadioGroup>
|
|
114
|
+
</SettingsRow>
|
|
115
|
+
<SettingsRow label="Quality" value={quality}>
|
|
116
|
+
<MenuRadioGroup value={quality} onValueChange={setQuality}>
|
|
117
|
+
<MenuRadioItem value="Draft">Draft</MenuRadioItem>
|
|
118
|
+
<MenuRadioItem value="Final">Final</MenuRadioItem>
|
|
119
|
+
</MenuRadioGroup>
|
|
120
|
+
</SettingsRow>
|
|
121
|
+
</SettingsMenu>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### A flat row for a setting with nothing to choose
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { MenuRadioGroup, MenuRadioItem, MenuSeparator, SettingsMenu, SettingsRow } from "@sableui/react"
|
|
128
|
+
|
|
129
|
+
<SettingsMenu summary="Standard">
|
|
130
|
+
<SettingsRow label="Speed" value="Standard">
|
|
131
|
+
<MenuRadioGroup value="standard">
|
|
132
|
+
<MenuRadioItem value="standard">Standard</MenuRadioItem>
|
|
133
|
+
<MenuRadioItem value="priority">Priority</MenuRadioItem>
|
|
134
|
+
</MenuRadioGroup>
|
|
135
|
+
</SettingsRow>
|
|
136
|
+
<MenuSeparator />
|
|
137
|
+
<SettingsRow label="Plan" value="Team" />
|
|
138
|
+
</SettingsMenu>
|
|
139
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Sheet
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Edge-anchored drawer over Base UI Drawer — focus trap, scroll lock, Esc/outside dismissal, portal backdrop, plus swipe-to-dismiss on touch. Slides in from a viewport edge via side and swipes back out toward the same edge.
|
|
6
|
+
|
|
7
|
+
**Keywords:** sheet, drawer, side panel, overlay, swipe to dismiss
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Sheet for secondary workflows that slide in while preserving context from the underlying page.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for inspect, edit, filter, or create flows that relate to the current page.
|
|
16
|
+
- Do: Include clear title, close path, and footer actions for task-oriented sheets.
|
|
17
|
+
- Don't: Do not use Sheet for simple confirmations or deeply nested workflows.
|
|
18
|
+
- Don't: Do not use it on desktop where a Dialog fits. A sheet's affordance is edge-anchored travel for thumb reach; on a wide viewport it becomes a dialog that arrives from the wrong place.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| SheetTrigger | no | The element that opens the sheet. |
|
|
25
|
+
| SheetContent | yes | The sliding panel content. |
|
|
26
|
+
| SheetClose | no | An explicit close control. |
|
|
27
|
+
| SheetHeader | no | The title and description region. |
|
|
28
|
+
| SheetTitle | no | The sheet heading. |
|
|
29
|
+
| SheetDescription | no | Supporting context for the sheet. |
|
|
30
|
+
| SheetFooter | no | Footer actions for the workflow. |
|
|
31
|
+
|
|
32
|
+
## Subcomponents
|
|
33
|
+
|
|
34
|
+
| Name | Type | Description |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| SheetContent | Base UI Drawer.Popup props (initialFocus, finalFocus, …) + side: "right" \| "left" \| "top" \| "bottom" + className | Backdrop + edge-anchored popup; portals to <body>. side picks which viewport edge the sheet anchors to and slides in from (defaults to "right"). Left/right are full-height side panels; top/bottom span the width up to 80vh. On touch it swipes closed toward that same edge (right swipes right, left swipes left, top swipes up, bottom swipes down). |
|
|
37
|
+
| SheetTrigger | Base UI Drawer.Trigger props (render) | Element that opens the sheet. Compose via render prop. |
|
|
38
|
+
| SheetClose | Base UI Drawer.Close props (render) | Element that dismisses the sheet. Compose via render prop. |
|
|
39
|
+
| SheetHeader | ComponentProps<"div"> | Top section holding SheetTitle/SheetDescription. |
|
|
40
|
+
| SheetTitle | Base UI Drawer.Title props + className | The sheet's accessible name — wires aria-labelledby on the popup. |
|
|
41
|
+
| SheetDescription | Base UI Drawer.Description props + className | Muted supporting line; wires aria-describedby. |
|
|
42
|
+
| SheetFooter | ComponentProps<"div"> | Bottom row for actions, pinned after the scrollable body. |
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
| Name | Type | Default | Description |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| Sheet (Root) | Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle) — minus swipeDirection and snap points | — | Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason "swipe" when a swipe closed it. |
|
|
49
|
+
|
|
50
|
+
## Variants
|
|
51
|
+
|
|
52
|
+
- **side**: right, left, top, bottom
|
|
53
|
+
|
|
54
|
+
## Tokens used
|
|
55
|
+
|
|
56
|
+
`popover`, `popover-foreground`, `border`, `muted-foreground`
|
|
57
|
+
|
|
58
|
+
## Accessibility
|
|
59
|
+
|
|
60
|
+
- Base UI provides focus trapping, scroll lock, aria-modal, and labelled-by wiring (SheetTitle).
|
|
61
|
+
- The panel scrolls its own content (thin scrollbar, contained overscroll) when it outgrows its bound — the full height on left/right, 80vh on top/bottom.
|
|
62
|
+
- Esc and click-outside dismiss by default; on touch, a swipe toward the sheet's own edge dismisses too. A short or slow drag springs back.
|
|
63
|
+
- Swipe is an extra path, never the only one: Esc, click-outside and SheetClose all still work.
|
|
64
|
+
- Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down); focus returns to the trigger on close.
|
|
65
|
+
- Render a SheetClose inside SheetContent so screen-reader users can escape the popup.
|
|
66
|
+
- Slide animation runs via data-[starting-style]/data-[ending-style] attributes on the motion-duration token; while swiping the panel tracks the finger directly, and a released swipe finishes on the same token (scaled by flick speed), so reduced motion makes it instant.
|
|
67
|
+
- A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge.
|
|
68
|
+
|
|
69
|
+
## Related
|
|
70
|
+
|
|
71
|
+
- Components: Dialog, Popover, Sidebar
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
### Basic (right)
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Button, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@sableui/react"
|
|
79
|
+
|
|
80
|
+
<Sheet><SheetTrigger render={<Button>Open</Button>} /><SheetContent><SheetHeader><SheetTitle>Filters</SheetTitle><SheetDescription>Narrow the feed.</SheetDescription></SheetHeader><SheetFooter><SheetClose render={<Button variant="secondary">Cancel</Button>} /><SheetClose render={<Button>Apply</Button>} /></SheetFooter></SheetContent></Sheet>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Left side
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@sableui/react"
|
|
87
|
+
|
|
88
|
+
<Sheet><SheetTrigger render={<Button variant="secondary">Menu</Button>} /><SheetContent side="left"><SheetHeader><SheetTitle>Navigation</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant="secondary">Close</Button>} /></SheetFooter></SheetContent></Sheet>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Bottom side
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@sableui/react"
|
|
95
|
+
|
|
96
|
+
<Sheet><SheetTrigger render={<Button variant="secondary">Details</Button>} /><SheetContent side="bottom"><SheetHeader><SheetTitle>Edition details</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant="secondary">Close</Button>} /></SheetFooter></SheetContent></Sheet>
|
|
97
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ShowMore
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Collapsible content region that clips children to a collapsed height, fades the cut edge, and reveals the rest with a toggle.
|
|
6
|
+
|
|
7
|
+
**Keywords:** show more, disclosure, truncate, expand
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use ShowMore to reveal additional content after an intentionally shortened preview.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it when the preview is useful on its own and the rest is optional.
|
|
16
|
+
- Do: Make the expanded state reversible when collapsing helps users regain context.
|
|
17
|
+
- Don't: Do not place it on a card or tinted surface without checking the fade. The cut-edge gradient fades to the page background color, so on any other surface it reads as a visible band.
|
|
18
|
+
- Don't: Do not hide required steps, important warnings, or primary actions behind ShowMore.
|
|
19
|
+
- Don't: Do not use it to hide content search engines or screen readers need. Collapsed content is still in the DOM here, but if the user must read it to act, it should not be behind a toggle.
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Name | Type | Default | Description |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| collapsedHeight | number | 96 | Collapsed height of the content region in px. The inner wrapper clips to this height until expanded. The toggle and fade only appear when the measured content is taller than this; content that already fits shows in full with no control. Expanding animates to the measured height. |
|
|
26
|
+
| collapsedLines | number | — | Collapsed height as a count of the content's own lines (measured from its line height). Wins over collapsedHeight, and is the one to prefer: the preview keeps the same number of lines when the type scale or density changes, where a px height shows fewer as type grows. |
|
|
27
|
+
| children | ReactNode | — | The content to collapse and reveal. |
|
|
28
|
+
|
|
29
|
+
## Tokens used
|
|
30
|
+
|
|
31
|
+
`background`, `foreground`, `ring`
|
|
32
|
+
|
|
33
|
+
## Accessibility
|
|
34
|
+
|
|
35
|
+
- The toggle is a real <button> with aria-expanded reflecting the open state.
|
|
36
|
+
- The fade overlay is pointer-events-none and decorative; it never traps clicks.
|
|
37
|
+
- Collapsed content stays in the DOM, so screen readers can still reach it.
|
|
38
|
+
|
|
39
|
+
## Related
|
|
40
|
+
|
|
41
|
+
- Components: Collapsible
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
### Long paragraph
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { ShowMore } from "@sableui/react"
|
|
49
|
+
|
|
50
|
+
<ShowMore><p className="text-body-md text-foreground">A long paragraph that exceeds the collapsed height…</p></ShowMore>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Collapsed to a line count
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { ShowMore } from "@sableui/react"
|
|
57
|
+
|
|
58
|
+
<ShowMore collapsedLines={6}><p className="text-body-md text-foreground">{post.description}</p></ShowMore>
|
|
59
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# SideNav
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A lightweight in-content vertical navigation list - the settings / docs sub-nav, distinct from the app-chrome Sidebar. Router-agnostic items, optional SideNavGroup sections, and an explicit active-state vocabulary for content rails or sidebar chrome.
|
|
6
|
+
|
|
7
|
+
**Keywords:** side nav, navigation, settings nav, sidebar, rail, docs nav, active item, group
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use SideNav for vertical navigation inside a settings page, documentation area, dashboard rail, or sidebar chrome. It is router-agnostic: compose items with render={<Link />} and choose activeVariant based on whether the nav sits in content or app chrome.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use aria-label on SideNav when a page contains more than one nav landmark.
|
|
16
|
+
- Do: Group related items with SideNavGroup when the rail has more than one conceptual area.
|
|
17
|
+
- Do: Use size='lg' and activeVariant='sidebar' when the nav is part of persistent app chrome.
|
|
18
|
+
- Do: Inside a rounded, padded frame, give each item rounded-concentric-<outer>/<inset> (e.g. rounded-concentric-lg/2 in a rounded-lg p-2 box). The rows' own radius is fixed per size, and a same-radius row inset into a frame reads as a second system.
|
|
19
|
+
- Don't: Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot.
|
|
20
|
+
- Don't: Mix content and sidebar active variants in the same rail unless individual items intentionally override the group.
|
|
21
|
+
|
|
22
|
+
### Anatomy
|
|
23
|
+
|
|
24
|
+
| Part | Required | Description |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Nav | yes | The landmark container for the vertical navigation list. |
|
|
27
|
+
| Group | no | Optional labelled cluster of related navigation items. |
|
|
28
|
+
| Item | yes | Interactive row with label, optional icon, optional badge, and active state. |
|
|
29
|
+
| Badge | no | Trailing count or state indicator aligned to the row end. |
|
|
30
|
+
|
|
31
|
+
## Subcomponents
|
|
32
|
+
|
|
33
|
+
| Name | Type | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| SideNavItem | { label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; size?: "md" \| "lg"; activeVariant?: "content" \| "sidebar"; render?: useRender.RenderProp } + button props | A single row. label is the row text; icon is an optional leading icon; active marks the current sub-section (active fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end; size and activeVariant override the enclosing SideNav per item; render composes another element, e.g. render={<Link to="/x" />}. |
|
|
36
|
+
| SideNavGroup | { label?: ReactNode } + div props | A labelled section within a SideNav - groups settings into areas. label renders an optional sentence-case caption heading above the group's items. |
|
|
37
|
+
|
|
38
|
+
## Props
|
|
39
|
+
|
|
40
|
+
| Name | Type | Default | Description |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| size | "md" \| "lg" | — | Density shared by all items. "md" (default) = the lightweight in-content sub-nav; "lg" = chrome-weight (larger label + icon, roomier rows) for a rail that pairs with the app Sidebar. |
|
|
43
|
+
| activeVariant | "content" \| "sidebar" | "content" | Active-state token vocabulary shared by all items. "content" uses bg-accent/text-accent-foreground for local rails inside the content area; "sidebar" uses bg-sidebar-accent/text-sidebar-accent-foreground for SideNavs rendered inside app chrome. |
|
|
44
|
+
|
|
45
|
+
## Tokens used
|
|
46
|
+
|
|
47
|
+
`muted`, `muted-foreground`, `foreground`, `accent`, `accent-foreground`, `ring`, `sidebar-foreground`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
|
|
48
|
+
|
|
49
|
+
## Accessibility
|
|
50
|
+
|
|
51
|
+
- SideNav renders a <nav> landmark; pair multiple navs with aria-label to distinguish them.
|
|
52
|
+
- The active item carries aria-current="page".
|
|
53
|
+
- Items expose a focus-visible ring; the active state is reinforced by weight + fill, not colour alone.
|
|
54
|
+
- Use activeVariant="content" for local in-page rails and activeVariant="sidebar" when a SideNav is placed on Sidebar chrome.
|
|
55
|
+
|
|
56
|
+
## Related
|
|
57
|
+
|
|
58
|
+
- Components: Sidebar, AppShell, BottomNav, Badge, Icon
|
|
59
|
+
- Patterns: SettingsPage, DashboardPage
|
|
60
|
+
|
|
61
|
+
## Examples
|
|
62
|
+
|
|
63
|
+
### Settings sub-nav
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { SideNav, SideNavItem } from "@sableui/react"
|
|
67
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
68
|
+
|
|
69
|
+
<SideNav><SideNavItem label="Profile" active render={<Link to="/settings/profile" />} /><SideNavItem label="Account" render={<Link to="/settings/account" />} /><SideNavItem label="Notifications" render={<Link to="/settings/notifications" />} /></SideNav>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Grouped
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { SideNav, SideNavGroup, SideNavItem } from "@sableui/react"
|
|
76
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
77
|
+
|
|
78
|
+
<SideNav><SideNavGroup label="Account"><SideNavItem label="Profile" active render={<Link to="/settings/profile" />} /><SideNavItem label="Security" render={<Link to="/settings/security" />} /></SideNavGroup><SideNavGroup label="Workspace"><SideNavItem label="Members" render={<Link to="/settings/members" />} /></SideNavGroup></SideNav>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Chrome-weight rail (size=lg)
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { SideNav, SideNavItem } from "@sableui/react"
|
|
85
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
86
|
+
|
|
87
|
+
<SideNav size="lg"><SideNavItem label="Profile" active render={<Link to="/settings/profile" />} /><SideNavItem label="Wallets" render={<Link to="/settings/wallets" />} /><SideNavItem label="Security" render={<Link to="/settings/security" />} /></SideNav>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Inside Sidebar chrome
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { SideNav, SideNavItem } from "@sableui/react"
|
|
94
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
95
|
+
|
|
96
|
+
<SideNav size="lg" activeVariant="sidebar"><SideNavItem label="Overview" active render={<Link to="/" />} /><SideNavItem label="Reports" render={<Link to="/reports" />} /><SideNavItem label="Settings" render={<Link to="/settings" />} /></SideNav>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Grouped with icons and badges
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Badge, Icon, SideNav, SideNavGroup, SideNavItem } from "@sableui/react"
|
|
103
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
104
|
+
|
|
105
|
+
<SideNav aria-label="Settings sections"><SideNavGroup label="Account"><SideNavItem icon={<Icon name="user" />} label="Profile" active render={<Link to="/settings/profile" />} /><SideNavItem icon={<Icon name="lock" />} label="Security" badge={<Badge tone="warning" size="sm">2</Badge>} render={<Link to="/settings/security" />} /></SideNavGroup><SideNavGroup label="Workspace"><SideNavItem icon={<Icon name="bell" />} label="Notifications" render={<Link to="/settings/notifications" />} /></SideNavGroup></SideNav>
|
|
106
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Sidebar
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarNav / SidebarItem / SidebarFooter parts. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.
|
|
6
|
+
|
|
7
|
+
**Keywords:** sidebar, navigation, app chrome, rail
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Sidebar for persistent desktop navigation or application chrome outside the main content region.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for stable app destinations that should remain visible while content changes.
|
|
16
|
+
- Do: Keep item labels predictable and active state obvious.
|
|
17
|
+
- Don't: Do not use Sidebar for page-local settings, filters, or temporary action groups.
|
|
18
|
+
- Don't: Do not put page content in it. It is fixed-width chrome OUTSIDE the content grid, so anything in it is exempt from the page's measure and rhythm.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Sidebar | yes | The persistent navigation container. |
|
|
25
|
+
| SidebarItem | yes | One navigation destination or action. |
|
|
26
|
+
|
|
27
|
+
## Subcomponents
|
|
28
|
+
|
|
29
|
+
| Name | Type | Description |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| SidebarItem | { label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; render?: useRender.RenderProp } + button props | A single nav row. label is the row text (inline when expanded, in a hover tooltip when collapsed); icon is the leading icon node (the consumer swaps fill weight for the active state); active marks the current route (accent fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end and hidden when collapsed; render composes another element, e.g. render={<Link to="/x" />}. |
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Name | Type | Default | Description |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| collapsed | boolean | false | Collapse the rail to an icon-only width (--sidebar-width-collapsed). Items hide their label + badge and reveal the label in a hover tooltip; the content area reflows. Controlled — wire a toggle from app state. Read the state in custom header/footer content via useSidebar(). |
|
|
38
|
+
| icons | boolean | true | Render the leading icon on each SidebarItem. Set false for a text-only rail. Ignored while collapsed (icon-only by definition). |
|
|
39
|
+
| variant | "app" \| "inline" | "app" | Where the rail lives. "app" is the page's chrome column — sticky, full viewport height, hidden below lg where AppHeader + BottomNav take over. "inline" is always visible and fills its parent's height, for a rail inside a bounded frame (a nested shell, an embedded preview). Use it instead of overriding the layout with !important classes. |
|
|
40
|
+
|
|
41
|
+
## Variants
|
|
42
|
+
|
|
43
|
+
- **variant**: app, inline
|
|
44
|
+
|
|
45
|
+
## Tokens used
|
|
46
|
+
|
|
47
|
+
`sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
|
|
48
|
+
|
|
49
|
+
## Accessibility
|
|
50
|
+
|
|
51
|
+
- SidebarNav renders a <nav> landmark.
|
|
52
|
+
- Active item carries aria-current="page".
|
|
53
|
+
- Items expose a focus-visible ring (sidebar-ring).
|
|
54
|
+
- Collapsed: items take their accessible name from a string label (or aria-label) and reveal the visible label via the tooltip, so the rail stays usable by keyboard and screen readers.
|
|
55
|
+
|
|
56
|
+
## Related
|
|
57
|
+
|
|
58
|
+
- Components: SideNav, BottomNav, AppShell
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
### Rail with nav
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { Icon, NotificationBadge, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
|
|
66
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
67
|
+
|
|
68
|
+
<Sidebar><SidebarHeader>{brand}</SidebarHeader><SidebarNav><SidebarItem icon={<Icon name="bell" />} label="Notifications" badge={<NotificationBadge count={3} />} render={<Link to="/notifications" />} /></SidebarNav><SidebarFooter>{wallet}</SidebarFooter></Sidebar>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Collapsible (icon rail)
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { Button, Icon, Sidebar, SidebarFooter, SidebarItem, SidebarNav } from "@sableui/react"
|
|
75
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
76
|
+
|
|
77
|
+
const [collapsed, setCollapsed] = useState(false)
|
|
78
|
+
<Sidebar collapsed={collapsed}><SidebarNav><SidebarItem icon={<Icon name="home" />} label="Home" active render={<Link to="/" />} /></SidebarNav><SidebarFooter><Button variant="ghost" size="icon" onClick={() => setCollapsed((c) => !c)} aria-label="Toggle sidebar"><Icon name="chevron-left" /></Button></SidebarFooter></Sidebar>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Inline (inside a bounded frame)
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { Icon, Sidebar, SidebarItem, SidebarNav } from "@sableui/react"
|
|
85
|
+
|
|
86
|
+
<div className="flex h-80 overflow-hidden rounded-lg border border-border"><Sidebar variant="inline"><SidebarNav><SidebarItem icon={<Icon name="home" />} label="Home" active /></SidebarNav></Sidebar><div className="flex-1 p-6">Content</div></div>
|
|
87
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Skeleton
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Loading placeholder — a muted, shimmering block (rounded-md bg-muted motion-shimmer). Shape it with utility classes; rounded-full for circles.
|
|
6
|
+
|
|
7
|
+
**Keywords:** skeleton, loading, placeholder, pending
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Skeleton to reserve space and hint at the shape of content while data is loading.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Match the approximate size and rhythm of the eventual content.
|
|
16
|
+
- Do: Use it for content regions where layout stability matters during loading.
|
|
17
|
+
- Don't: Do not leave Skeleton visible after an error or empty state; switch to EmptyState (tone='destructive' for an error).
|
|
18
|
+
- Don't: Do not use a skeleton whose shape differs from the content that replaces it. A mismatched placeholder causes a visible jump, which is worse than a brief spinner.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Name | Type | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| ...props | ComponentPropsWithRef<"div"> | — | All native div props plus className. Size and shape the placeholder with utilities (h-*, w-*, size-*, rounded-full). |
|
|
25
|
+
|
|
26
|
+
## Tokens used
|
|
27
|
+
|
|
28
|
+
`muted`
|
|
29
|
+
|
|
30
|
+
## Motion
|
|
31
|
+
|
|
32
|
+
- **arrival** · Root · _no motion_ — A skeleton stands in for content that has not arrived; fading the placeholder in adds a wait before the wait.
|
|
33
|
+
- **response** · Root · _no motion_ — Nothing here is interactive. A skeleton that reacted to the pointer would invite a click on a thing that cannot be clicked.
|
|
34
|
+
- **transition** · Root · _no motion_ — The skeleton has one state. Swapping it for real content is the parent's job, and the parent decides whether that swap is animated.
|
|
35
|
+
- **activity** · Root · `motion-shimmer` — The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page.
|
|
36
|
+
|
|
37
|
+
## Accessibility
|
|
38
|
+
|
|
39
|
+
- Purely presentational; the shimmer sweep is decorative, and it stops under reduced motion because motion-shimmer follows --motion-scale.
|
|
40
|
+
- Mark the loading region with aria-busy="true" on a parent, and swap in real content (with the same layout) once loaded to avoid shift.
|
|
41
|
+
|
|
42
|
+
## Related
|
|
43
|
+
|
|
44
|
+
- Components: Spinner, PageLoader
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
### Shapes
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { Skeleton } from "@sableui/react"
|
|
52
|
+
|
|
53
|
+
<Skeleton className="h-4 w-40" /><Skeleton className="size-10 rounded-full" /><Skeleton className="h-40 w-full" />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Post-card composition
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Skeleton } from "@sableui/react"
|
|
60
|
+
|
|
61
|
+
<div className="flex items-center gap-3"><Skeleton className="size-10 rounded-full" /><div className="space-y-2"><Skeleton className="h-3 w-32" /><Skeleton className="h-3 w-20" /></div></div>
|
|
62
|
+
```
|