@sableui/react 0.9.0 → 0.10.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 +1 -1
- package/CHANGELOG.md +124 -0
- package/CONTRIBUTING.md +2 -2
- package/DESIGN.md +55 -16
- package/README.md +12 -0
- package/dist/{_contract-C2M1SjBf.d.ts → _contract-CkqXDVww.d.ts} +25 -2
- package/dist/blocks/danger-zone-block.js +3 -3
- package/dist/blocks/danger-zone-block.meta.js +1 -1
- package/dist/blocks/faq-block.js +3 -3
- package/dist/blocks/goal-progress-block.js +1 -1
- package/dist/blocks/notification-list-block.js +1 -1
- package/dist/blocks/pricing-table-block.js +12 -12
- package/dist/blocks/product-card-block.js +3 -3
- package/dist/blocks/product-card-block.meta.js +3 -3
- package/dist/blocks/profile-card-block.js +1 -1
- package/dist/blocks/profile-form-block.js +3 -3
- package/dist/blocks/revenue-breakdown-block.js +5 -5
- package/dist/blocks/stat-row-block.js +3 -3
- package/dist/blocks/task-list-block.js +2 -2
- package/dist/blocks/team-members-block.js +1 -1
- package/dist/blocks/wallet-block.js +4 -4
- package/dist/blocks/wallet-block.meta.js +3 -3
- package/dist/components/accordion/accordion.meta.js +2 -2
- package/dist/components/app-header/app-header.meta.js +1 -1
- package/dist/components/app-shell/app-shell.meta.js +2 -2
- package/dist/components/area-chart/area-chart.meta.js +3 -3
- package/dist/components/badge/badge.js +13 -4
- package/dist/components/badge/badge.meta.js +24 -13
- package/dist/components/badge/badge.variants.js +37 -23
- package/dist/components/banner/banner.meta.js +8 -8
- package/dist/components/banner/banner.variants.js +3 -3
- package/dist/components/bar-chart/bar-chart.meta.js +4 -4
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +1 -1
- package/dist/components/button/button.js +13 -2
- package/dist/components/button/button.meta.js +3 -3
- package/dist/components/card/card.js +16 -7
- package/dist/components/card/card.meta.js +4 -4
- package/dist/components/checkbox-card/checkbox-card.meta.js +10 -10
- package/dist/components/checkbox-card/checkbox-card.variants.js +15 -15
- package/dist/components/code-block/code-block.meta.js +1 -1
- package/dist/components/command-menu/command-menu.js +2 -0
- package/dist/components/command-menu/command-menu.meta.js +1 -1
- package/dist/components/context-card/context-card.meta.js +2 -2
- package/dist/components/destructive-action-modal/destructive-action-modal.meta.js +1 -1
- package/dist/components/dialog/dialog.meta.js +2 -2
- package/dist/components/disclosure-picker/disclosure-picker.js +48 -2
- package/dist/components/disclosure-picker/disclosure-picker.meta.js +3 -2
- package/dist/components/empty-state/empty-state.meta.js +4 -4
- package/dist/components/form/form.js +10 -5
- package/dist/components/gradient-art/gradient-art.meta.js +3 -3
- package/dist/components/label/label.meta.js +1 -1
- package/dist/components/line-chart/line-chart.meta.js +3 -3
- package/dist/components/meter/meter.js +1 -1
- package/dist/components/meter/meter.meta.js +2 -2
- package/dist/components/meter/meter.variants.js +1 -1
- package/dist/components/middle-truncate/middle-truncate.meta.js +4 -4
- package/dist/components/note/note.meta.js +3 -3
- package/dist/components/page-header/page-header.js +1 -1
- package/dist/components/pagination/pagination.meta.js +2 -2
- package/dist/components/phone/phone.meta.js +1 -1
- package/dist/components/pie-chart/pie-chart.meta.js +3 -3
- package/dist/components/pill/pill.js +4 -2
- package/dist/components/pill/pill.meta.js +19 -2
- package/dist/components/pill/pill.variants.js +23 -2
- package/dist/components/popover/popover.meta.js +2 -2
- package/dist/components/radar-chart/radar-chart.meta.js +3 -3
- package/dist/components/radial-chart/radial-chart.meta.js +3 -3
- package/dist/components/radio/radio.meta.js +2 -2
- package/dist/components/radio-card/radio-card.meta.js +6 -6
- package/dist/components/radio-card/radio-card.variants.js +15 -15
- package/dist/components/scroller/scroller.meta.js +1 -1
- package/dist/components/search-input/search-input.meta.js +1 -1
- package/dist/components/section-header/section-header.js +1 -1
- package/dist/components/segmented-bar/segmented-bar.js +1 -1
- package/dist/components/segmented-bar/segmented-bar.meta.js +6 -6
- package/dist/components/segmented-bar/segmented-bar.variants.js +4 -4
- package/dist/components/select/select.meta.js +2 -2
- package/dist/components/sheet/sheet.js +15 -1
- package/dist/components/sheet/sheet.meta.js +1 -1
- package/dist/components/side-nav/side-nav.meta.js +1 -1
- package/dist/components/sidebar/sidebar.meta.js +1 -1
- package/dist/components/sparkline/sparkline.meta.js +4 -4
- package/dist/components/spinner/spinner.meta.js +1 -1
- package/dist/components/status-dot/status-dot.js +4 -1
- package/dist/components/status-dot/status-dot.meta.js +8 -16
- package/dist/components/status-dot/status-dot.variants.js +13 -4
- package/dist/components/stepper/stepper.js +11 -2
- package/dist/components/table/table.meta.js +4 -4
- package/dist/components/tabs/tabs.meta.js +1 -1
- package/dist/components/textarea/textarea.meta.js +2 -2
- package/dist/components/toast/toast.js +1 -0
- package/dist/components/toast/toast.meta.js +1 -1
- package/dist/components/tooltip/tooltip.meta.js +2 -2
- package/dist/file-hashes.json +127 -124
- package/dist/foundations/spacing.js +8 -8
- package/dist/index.d.ts +138 -73
- package/dist/index.js +12 -1
- package/dist/lib/chart/types.js +3 -3
- package/dist/lib/is-promise.js +5 -0
- package/dist/lib/palette.js +103 -0
- package/dist/lib/selection-card.js +4 -4
- package/dist/palette.css +669 -0
- package/dist/patterns/auth-page.js +6 -6
- package/dist/patterns/auth-page.meta.js +3 -3
- package/dist/patterns/brand-landing-page.js +2 -2
- package/dist/patterns/chat-panel.js +2 -2
- package/dist/patterns/dashboard-page.js +71 -71
- package/dist/patterns/dashboard-page.meta.js +5 -5
- package/dist/patterns/feed-page.js +25 -25
- package/dist/patterns/feed-page.meta.js +3 -3
- package/dist/patterns/landing-page.js +1 -1
- package/dist/patterns/list-detail-page.js +17 -17
- package/dist/patterns/settings-page.js +17 -17
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +17 -2
- package/dist/styles.css +163 -55
- package/dist/{theme-provider-S0NVJs2Y.d.ts → theme-provider-CdSicAad.d.ts} +1 -1
- package/dist/themes/_contract.js +22 -8
- package/dist/themes/atelier/atelier.css +13 -13
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +13 -13
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +13 -13
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +19 -16
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +18 -15
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +18 -22
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +15 -15
- package/llms/blocks/danger-zone-block.md +4 -4
- package/llms/blocks/faq-block.md +3 -3
- package/llms/blocks/goal-progress-block.md +1 -1
- package/llms/blocks/notification-list-block.md +1 -1
- package/llms/blocks/pricing-table-block.md +12 -12
- package/llms/blocks/product-card-block.md +6 -6
- package/llms/blocks/profile-card-block.md +1 -1
- package/llms/blocks/profile-form-block.md +3 -3
- package/llms/blocks/revenue-breakdown-block.md +5 -5
- package/llms/blocks/stat-row-block.md +3 -3
- package/llms/blocks/task-list-block.md +2 -2
- package/llms/blocks/team-members-block.md +1 -1
- package/llms/blocks/wallet-block.md +7 -7
- package/llms/components/accordion.md +2 -2
- package/llms/components/app-header.md +1 -1
- package/llms/components/app-shell.md +2 -2
- package/llms/components/area-chart.md +1 -1
- package/llms/components/badge.md +15 -7
- package/llms/components/banner.md +6 -6
- package/llms/components/bar-chart.md +2 -2
- package/llms/components/breadcrumbs.md +1 -1
- package/llms/components/button.md +3 -3
- package/llms/components/card.md +4 -4
- package/llms/components/checkbox-card.md +5 -5
- package/llms/components/code-block.md +1 -1
- package/llms/components/command-menu.md +1 -1
- package/llms/components/context-card.md +2 -2
- package/llms/components/destructive-action-modal.md +1 -1
- package/llms/components/dialog.md +2 -2
- package/llms/components/disclosure-picker.md +2 -1
- package/llms/components/empty-state.md +4 -4
- package/llms/components/gradient-art.md +3 -3
- package/llms/components/label.md +1 -1
- package/llms/components/line-chart.md +1 -1
- package/llms/components/meter.md +2 -2
- package/llms/components/middle-truncate.md +5 -5
- package/llms/components/note.md +3 -3
- package/llms/components/pagination.md +2 -2
- package/llms/components/phone.md +1 -1
- package/llms/components/pie-chart.md +1 -1
- package/llms/components/pill.md +12 -2
- package/llms/components/popover.md +2 -2
- package/llms/components/radar-chart.md +1 -1
- package/llms/components/radial-chart.md +1 -1
- package/llms/components/radio-card.md +4 -4
- package/llms/components/radio.md +2 -2
- package/llms/components/scroller.md +1 -1
- package/llms/components/search-input.md +1 -1
- package/llms/components/segmented-bar.md +7 -7
- package/llms/components/select.md +2 -2
- package/llms/components/sheet.md +1 -1
- package/llms/components/side-nav.md +1 -1
- package/llms/components/sidebar.md +1 -1
- package/llms/components/sparkline.md +2 -2
- package/llms/components/spinner.md +1 -1
- package/llms/components/status-dot.md +4 -4
- package/llms/components/table.md +8 -8
- package/llms/components/tabs.md +1 -1
- package/llms/components/textarea.md +2 -2
- package/llms/components/toast.md +1 -1
- package/llms/components/tooltip.md +2 -2
- package/llms/foundations/spacing.md +7 -7
- package/llms/layout/grid.md +1 -1
- package/llms/layout/page.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/patterns/auth-page.md +11 -11
- package/llms/patterns/brand-landing-page.md +2 -2
- package/llms/patterns/chat-panel.md +2 -2
- package/llms/patterns/dashboard-page.md +78 -78
- package/llms/patterns/feed-page.md +31 -31
- package/llms/patterns/landing-page.md +1 -1
- package/llms/patterns/list-detail-page.md +18 -18
- package/llms/patterns/settings-page.md +18 -18
- package/llms/reference/token-intent.md +25 -11
- package/llms/reference/utilities.md +1 -1
- package/llms/tasks/adding-a-component.md +4 -1
- package/llms/tasks/building-a-page.md +40 -0
- package/llms/tasks/building-with-sable.md +4 -0
- package/llms/tasks/converting-an-app.md +3 -1
- package/llms/tasks/using-presets.md +4 -0
- package/llms-full.txt +433 -347
- package/llms.txt +7 -7
- package/package.json +4 -4
- package/registry.json +369 -233
- package/scripts/lint-rules.mjs +26 -5
- package/scripts/sources.mjs +4 -1
- package/skills/sable/SKILL.md +14 -3
package/llms-full.txt
CHANGED
|
@@ -205,7 +205,7 @@ composes with `render`).
|
|
|
205
205
|
|
|
206
206
|
## How it works
|
|
207
207
|
|
|
208
|
-
- Components reference **semantic tokens** (`bg-card`, `text-muted-foreground`, `bg-tone-
|
|
208
|
+
- Components reference **semantic tokens** (`bg-card`, `text-muted-foreground`, `bg-tone-1`), never raw palette steps or inline colors. Text/icons on a tone fill use the theme-adaptive `text-tone-*-foreground` ink; scrims and media surfaces use `bg-overlay`/`text-overlay-foreground`. Positive/success states use the flat `--success` / `--success-foreground` token (mirrors `--destructive`; there is **no** `tone-success`). A theme = a CSS variable set; re-skinning never touches component code. App-level theme switching is first-class via `<SableThemeProvider>` and `useSableTheme()`.
|
|
209
209
|
- Typography is **paired utilities** (`text-heading-1`, `text-body-md`) bundling size+weight+line-height+tracking.
|
|
210
210
|
- Layout is **fluid-first / no global max-width**: an edge-to-edge page with a fluid `--page-inset`, an 8px rhythm unit, a 12-col grid that folds, and opt-in per-region max-widths. Primitives: `Region`, `Page` (route shell = Region + Stack), `Bleed` (content-area-aware full-bleed, via container units), `SplitPane`, `Stack`/`Row`/`Center`, `Grid`/`Columns`/`Col`, `SafeArea`. `Columns count` / `Col span`+`start` / `Grid cols` accept **responsive breakpoint maps** (`{ base, md, lg }`).
|
|
211
211
|
- `<Icon name="…">` resolves a semantic name through an injected `<IconProvider>`; with no provider it renders Reicon. Replace the set with IconProvider or extend it with composeIconSets(appExtras, reiconIconSet). Concrete imports stay in the shared adapter; never copy SVG paths into an app.
|
|
@@ -337,7 +337,10 @@ because the meta wins *whole* and silently discards the other.
|
|
|
337
337
|
|
|
338
338
|
## The hard rules
|
|
339
339
|
|
|
340
|
-
1. **Semantic tokens only** — never hex/oklch, never a raw palette step
|
|
340
|
+
1. **Semantic tokens only** — never hex/oklch, never a raw palette step, not
|
|
341
|
+
even the generated palette's (`bg-blue-500`; that is for apps). A component
|
|
342
|
+
that takes a palette family as `tone` sets `data-palette` to it and paints
|
|
343
|
+
from the slots that fills (`bg-palette-wash text-palette-text`), as Badge does.
|
|
341
344
|
2. **Paired typography utilities** — never `text-{size}` + `font-{weight}`.
|
|
342
345
|
3. **`primitives/` is behaviour, `components/` is the public API.** Base UI is
|
|
343
346
|
wrapped and re-exported; consumers import only from the root barrel.
|
|
@@ -668,6 +671,25 @@ Pick each route's `max` by what the person mainly does on it:
|
|
|
668
671
|
(a copy in `light` or `dark` wins over it); keep the role names and change
|
|
669
672
|
the values.
|
|
670
673
|
|
|
674
|
+
## Columns and rhythm
|
|
675
|
+
|
|
676
|
+
- **Flex or grid.** One direction is `Stack` (column) or `Row` (row). Content
|
|
677
|
+
that has to line up across rows *and* columns is `Grid`/`Columns`. A card's
|
|
678
|
+
single column is a `Stack`, never a one-track grid.
|
|
679
|
+
- **Columns are a choice per route.** Record it in the brief next to the width:
|
|
680
|
+
which content spans which of the 12 columns, and where it folds
|
|
681
|
+
(`<Col span={{ base: 12, lg: 8 }}>`). The grid does not fold on its own. A
|
|
682
|
+
route that is one reading measure has no columns, and the brief says so.
|
|
683
|
+
- **Vertical space is measured in lines.** The baseline is one line of body
|
|
684
|
+
text split into steps (8px at the default type). Gap numbers on `Stack`/
|
|
685
|
+
`Row`/`Grid` are steps; in Tailwind, even steps (`gap-4`, `py-6`, `mt-12`)
|
|
686
|
+
are whole steps and odd ones (`py-3`, `gap-5`) are half a step. Title to
|
|
687
|
+
description is one step, between groups one to two lines, between sections
|
|
688
|
+
three or more.
|
|
689
|
+
- **It is a rhythm, not a lock.** A card with a button or a code block is as
|
|
690
|
+
tall as its content. Keep the spacing between blocks in steps and let
|
|
691
|
+
components be; never pad or nudge them to pin text to lines.
|
|
692
|
+
|
|
671
693
|
## Typography: pick the tier by ROLE, not by size
|
|
672
694
|
|
|
673
695
|
| tier | for | not |
|
|
@@ -697,6 +719,27 @@ intent, and what it gets confused with, is in
|
|
|
697
719
|
Ink is never chosen independently of its surface: `bg-card text-card-foreground`,
|
|
698
720
|
as a pair.
|
|
699
721
|
|
|
722
|
+
### More colour: the generated palette
|
|
723
|
+
|
|
724
|
+
When a page needs colour that is not a role (tags, avatars, project labels, a
|
|
725
|
+
chart legend, illustration), reach for the generated palette, not hex and not a
|
|
726
|
+
theme's own ramp. Every theme has `neutral`, `red`, `orange`, `yellow`,
|
|
727
|
+
`green`, `teal`, `blue`, `purple` and `pink` (50–950), plus a scale under
|
|
728
|
+
every tone (`tone-1-100`, `success-700`). Each family leans toward the brand, so
|
|
729
|
+
the set belongs to the theme, and neutral is the theme's own page-to-ink ramp.
|
|
730
|
+
|
|
731
|
+
- On a component, the family is the tone: `<Badge tone="teal">Research</Badge>`,
|
|
732
|
+
`<Pill tone="blue">`, `<StatusDot tone="pink" />`. A family is a category with
|
|
733
|
+
no meaning attached; a status is still `success`, `warning` or `destructive`.
|
|
734
|
+
- On your own element, use the pairs: `bg-blue-100 text-blue-800` (a tag),
|
|
735
|
+
`text-blue-700` (text on the page), `bg-blue-700 text-blue-50` (a solid
|
|
736
|
+
fill), `border-blue-300` (a hairline). Steps follow the mode (a low step is
|
|
737
|
+
always near the page), so these need no `dark:`.
|
|
738
|
+
- Surfaces, body text and actions still take roles. The palette never replaces
|
|
739
|
+
`bg-card` or `bg-primary`.
|
|
740
|
+
|
|
741
|
+
The full reference is `llms/reference/token-intent.md` → Generated palette.
|
|
742
|
+
|
|
700
743
|
## The traps
|
|
701
744
|
|
|
702
745
|
- **A `Card` is a container, not a heading or a landmark.** If its title is the
|
|
@@ -1040,6 +1083,10 @@ for each recognizable element with `sable search "<need>" --json`; inspect the
|
|
|
1040
1083
|
matching `component <Name> --props` and `--examples` before implementation.
|
|
1041
1084
|
Use Sable's composed anatomy and supported variants. A hand-built button, field,
|
|
1042
1085
|
menu, dialog, table, or card that imitates a shipped component violates the contract.
|
|
1086
|
+
That binds behaviour and parts, not layouts. Results marked ◇ (patterns, blocks,
|
|
1087
|
+
and compositions such as MediaTile) are references: use one when its layout fits
|
|
1088
|
+
the brief, otherwise compose your own from Sable's parts. The brief's signature
|
|
1089
|
+
surfaces, where the product's identity lives, are designed from its direction.
|
|
1043
1090
|
|
|
1044
1091
|
Resolve styling through component variants, documented utilities/recipes, then
|
|
1045
1092
|
semantic tokens. App-owned CSS handles uncovered content and composition; it
|
|
@@ -1220,7 +1267,9 @@ loudest and least clearly ("invalid hook call").
|
|
|
1220
1267
|
|
|
1221
1268
|
The most reliable signal that a conversion is real: the app's own CSS variable
|
|
1222
1269
|
block shrinks to almost nothing. Anything that remains should be *domain* colour,
|
|
1223
|
-
not UI colour.
|
|
1270
|
+
not UI colour. Most domain colour has a home too: a tag or category colour maps
|
|
1271
|
+
to a palette family (`<Badge tone="blue">`, `bg-blue-100 text-blue-800`), which
|
|
1272
|
+
follows the theme where the app's own hex never did.
|
|
1224
1273
|
|
|
1225
1274
|
Grep for orphaned references afterwards. A CSS variable that no longer exists
|
|
1226
1275
|
resolves to nothing and the declaration is dropped — **silently**. A checkerboard
|
|
@@ -2506,6 +2555,10 @@ prints a complete starting record; the public `PRESET_TOKEN_NAMES` is authoritat
|
|
|
2506
2555
|
Font stacks are requirements, not downloaded fonts. Declare matching stacks in
|
|
2507
2556
|
the font metadata and tokens. Resolve internal palette variables before saving;
|
|
2508
2557
|
portable presets may reference public Sable tokens, not private palette names.
|
|
2558
|
+
A preset does not carry the generated palette (`bg-blue-500` and the rest): it is
|
|
2559
|
+
computed from the preset's own primary, page and ink. `theme-palette-harmony`
|
|
2560
|
+
(0 keeps the families' own hues, 1 is the default lean toward the brand) is the
|
|
2561
|
+
one palette token a preset may set.
|
|
2509
2562
|
The validator rejects unknown tokens and CSS that can escape its declaration.
|
|
2510
2563
|
`sable preset --check` also holds labels on brand and status fills to 4.5:1 and APCA Lc 60
|
|
2511
2564
|
and suggests a pair that reads; a preset may keep a pair deliberately through
|
|
@@ -2821,9 +2874,9 @@ muted-foreground is contrast-tested on every surface; a 60% alpha is not.
|
|
|
2821
2874
|
|
|
2822
2875
|
# Spacing & sizing
|
|
2823
2876
|
|
|
2824
|
-
One
|
|
2877
|
+
One scale, derived from the type: the baseline is one line of body text, split into steps every gap, inset and line height lands on. Space is assigned by role. Controls read a height ladder that a density scope can retune all at once.
|
|
2825
2878
|
|
|
2826
|
-
**
|
|
2879
|
+
**Baseline from the type · 8px steps · 3 roles · 1 control ladder**
|
|
2827
2880
|
|
|
2828
2881
|
## Spacing by role
|
|
2829
2882
|
|
|
@@ -2832,8 +2885,8 @@ Steps below 2 (8px) are optical correction only.
|
|
|
2832
2885
|
| Name | Value | Use |
|
|
2833
2886
|
|---|---|---|
|
|
2834
2887
|
| `Inset p-4` | 16px | Padding inside a surface. p-3 for a dense card, px-3 py-2 for a row. |
|
|
2835
|
-
| `Stack, within a group gap-
|
|
2836
|
-
| `Stack, between groups gap-4 – gap-6` | 16–24px | Field to field, card to card. |
|
|
2888
|
+
| `Stack, within a group gap-2` | 1 step · 8px | Label to field, title to description. |
|
|
2889
|
+
| `Stack, between groups gap-4 – gap-6` | 2–3 steps · 16–24px | Field to field, card to card. |
|
|
2837
2890
|
| `Stack, between sections gap-12+` | 48px+ | Major regions of a page. |
|
|
2838
2891
|
| `Inline gap-2 – gap-3` | 8–12px | Icon to label, control to control. |
|
|
2839
2892
|
|
|
@@ -2859,8 +2912,8 @@ Comfortable values; data-density="compact" (or <Density>) redefines every step f
|
|
|
2859
2912
|
|
|
2860
2913
|
## Rules
|
|
2861
2914
|
|
|
2862
|
-
1. **Every margin, padding and gap is a step on the
|
|
2863
|
-
2. **Padding, gaps and insets in a component sit on the spacing scale: a step (p-4, gap-2) or --spacing(n) inside an arbitrary value (p-[var(--card-p,--spacing(6))]).**
|
|
2915
|
+
1. **Every margin, padding and gap is a step on the scale, and vertical space between blocks is whole baseline steps: an even Tailwind step (2, 4, 6, 8, 12) or a Stack/Row/Grid gap number.** The baseline is one line of body text split into steps; line heights round to it. An odd step (3, 5) is half a baseline step. It is a rhythm to keep, not a grid to force: components stay as tall as their content.
|
|
2916
|
+
2. **Padding, gaps and insets in a component sit on the spacing scale: a step (p-4, gap-2) or --spacing(n) inside an arbitrary value (p-[var(--card-p,--spacing(6))]).** --spacing derives from the baseline step and the density (Character) control moves the leading it comes from, so anything on the scale moves with it. A literal p-[1.5rem] or padding: "12px" stays put while everything around it tightens. Keep fixed rem/px for what must not scale: a 1px hairline, a ring width. *Enforced by design-lint: fixed-spacing (library source).*
|
|
2864
2917
|
3. **Related things sit closer than unrelated things.** Proximity groups content without a divider line. Use space first; reach for a border only when space is not enough.
|
|
2865
2918
|
4. **Space lives on the container (gap), not on children (margins).** gap survives reordering, conditional children and RTL; per-child margins do not.
|
|
2866
2919
|
5. **Control heights read the --control-* ladder.** A density scope retunes every control at once. A hardcoded height has to be found and overridden by hand. *Enforced by sable-lint: raw-control-height.*
|
|
@@ -2898,7 +2951,7 @@ Every control, menu and popup inside retunes, including portaled overlays.
|
|
|
2898
2951
|
<Stack gap={3}> between groups (24px), <Stack gap={1}> inside a group (8px)
|
|
2899
2952
|
```
|
|
2900
2953
|
|
|
2901
|
-
|
|
2954
|
+
Spacing steps do the work of every divider, with less ink. Stack's numeric gap counts --rhythm-unit — one baseline step — so it follows the type and the density control like every other spacing value.
|
|
2902
2955
|
|
|
2903
2956
|
**Related:** `sable docs shape` · `sable docs typography` · `sable docs building-a-tool` · `sable token layout`
|
|
2904
2957
|
|
|
@@ -3261,7 +3314,7 @@ The region is in the accessibility tree before its text changes, so the change i
|
|
|
3261
3314
|
|
|
3262
3315
|
# AuthPage
|
|
3263
3316
|
|
|
3264
|
-
Split sign-in page: a form column (brand mark, heading, email + password, primary sign-in, passkey and
|
|
3317
|
+
Split sign-in page: a form column (brand mark, heading, email + password, primary sign-in, passkey and email-link alternatives, sign-up switch, terms) beside a brand panel painted from the theme's tones with a customer story card. Below lg the panel drops away and the form is the whole screen.
|
|
3265
3318
|
|
|
3266
3319
|
**Intent:** Reach for this for sign-in, sign-up, forgot-password, and any single-purpose one-column form route (invite accept, unsubscribe). Not for multi-section forms (use SettingsPage).
|
|
3267
3320
|
|
|
@@ -3272,14 +3325,14 @@ Split sign-in page: a form column (brand mark, heading, email + password, primar
|
|
|
3272
3325
|
- The heading is `text-heading-1` in an h1 — it is the page outline, so it takes a heading tier, not a card title.
|
|
3273
3326
|
- Brand mark, form and terms are a three-part column: mark pinned to the top, form vertically centred in the remaining space, terms pinned to the bottom.
|
|
3274
3327
|
- Forgot password sits under the password field, right-aligned, before the submit button — so it comes right after the field in tab order. Field has no label-row action slot yet.
|
|
3275
|
-
- One primary action. Passkey and
|
|
3328
|
+
- One primary action. Passkey and email link are outline buttons under an `or` Separator; switching to sign-up is a quiet `link-underline` link.
|
|
3276
3329
|
- Validation uses Field `validate` for product copy instead of the browser's generic required message. A rejected sign-in comes back through Form `errors` on the password field; the submit Button shows `loading` while the request is in flight.
|
|
3277
3330
|
- The brand panel artwork is a mesh of the theme's decorative tones over `--primary`, so it re-paints for every theme and mode. The relative-colour `oklch(from var(--tone-*) …)` lift needs an `inline-color` disable with a reason.
|
|
3278
3331
|
- The panel is `hidden lg:flex`: on a phone the form is the whole screen, with no decorative scroll above it.
|
|
3279
3332
|
|
|
3280
3333
|
## Accessibility
|
|
3281
3334
|
|
|
3282
|
-
- The route renders inside <main> with one h1; the brand panel is an <aside> labelled "
|
|
3335
|
+
- The route renders inside <main> with one h1; the brand panel is an <aside> labelled "Customer story".
|
|
3283
3336
|
- Field auto-wires labels, describedby, and invalid state onto the inputs; on submit, the first invalid field is focused.
|
|
3284
3337
|
- Autocomplete attributes (`email webauthn`, `current-password`) keep password managers and passkey autofill working.
|
|
3285
3338
|
- The Forgot password and Create one links use `hit-target` to meet the 44px touch floor without changing layout.
|
|
@@ -3310,7 +3363,7 @@ export function AuthPage() {
|
|
|
3310
3363
|
<Stack gap={1.5}>
|
|
3311
3364
|
<h1 className="text-heading-1">Welcome back</h1>
|
|
3312
3365
|
<p className="text-body-lg text-muted-foreground">
|
|
3313
|
-
Sign in to
|
|
3366
|
+
Sign in to pick up where you and your team left off.
|
|
3314
3367
|
</p>
|
|
3315
3368
|
</Stack>
|
|
3316
3369
|
|
|
@@ -3348,8 +3401,8 @@ export function AuthPage() {
|
|
|
3348
3401
|
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="key" />}>
|
|
3349
3402
|
Sign in with a passkey
|
|
3350
3403
|
</Button>
|
|
3351
|
-
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="
|
|
3352
|
-
|
|
3404
|
+
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="mail" />}>
|
|
3405
|
+
Email me a sign-in link
|
|
3353
3406
|
</Button>
|
|
3354
3407
|
</Stack>
|
|
3355
3408
|
|
|
@@ -3376,23 +3429,23 @@ export function AuthPage() {
|
|
|
3376
3429
|
</Region>
|
|
3377
3430
|
|
|
3378
3431
|
{/* The brand panel is decoration: on a phone the form is the whole screen. */}
|
|
3379
|
-
<aside aria-label="
|
|
3432
|
+
<aside aria-label="Customer story" className="hidden p-3 lg:flex">
|
|
3380
3433
|
{/* Art painted from the theme's tones over its brand, so it follows every look. */}
|
|
3381
3434
|
<GradientArt seed="Untitled (gold rush)" className="flex flex-1 flex-col justify-end rounded-xl p-8">
|
|
3382
3435
|
<Card className="max-w-md">
|
|
3383
3436
|
<CardContent>
|
|
3384
3437
|
<Stack gap={3}>
|
|
3385
|
-
<Badge tone="
|
|
3386
|
-
|
|
3438
|
+
<Badge tone="1" className="self-start">
|
|
3439
|
+
Customer story
|
|
3387
3440
|
</Badge>
|
|
3388
3441
|
<p className="text-title-lg">
|
|
3389
|
-
“
|
|
3442
|
+
“We moved three teams over in a week, and nobody asked for the old tool back.”
|
|
3390
3443
|
</p>
|
|
3391
3444
|
<Entity
|
|
3392
3445
|
className="p-0"
|
|
3393
3446
|
leading={<Avatar variant="art" fallback="MO" />}
|
|
3394
3447
|
title="Mara Okafor"
|
|
3395
|
-
subtitle="
|
|
3448
|
+
subtitle="Head of Design, Northwind"
|
|
3396
3449
|
/>
|
|
3397
3450
|
</Stack>
|
|
3398
3451
|
</CardContent>
|
|
@@ -3453,7 +3506,7 @@ const spaces = [
|
|
|
3453
3506
|
|
|
3454
3507
|
const routes = [
|
|
3455
3508
|
{ label: "Intake", state: "Open", tone: "info" as const },
|
|
3456
|
-
{ label: "Review", state: "Live", tone: "
|
|
3509
|
+
{ label: "Review", state: "Live", tone: "1" as const },
|
|
3457
3510
|
{ label: "Archive", state: "Safe", tone: "success" as const },
|
|
3458
3511
|
]
|
|
3459
3512
|
|
|
@@ -3631,7 +3684,7 @@ export function BrandLandingPage() {
|
|
|
3631
3684
|
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
3632
3685
|
<Row gap={1}>
|
|
3633
3686
|
<StatusDot tone="warning" />
|
|
3634
|
-
<StatusDot tone="
|
|
3687
|
+
<StatusDot tone="1" />
|
|
3635
3688
|
<StatusDot tone="success" />
|
|
3636
3689
|
</Row>
|
|
3637
3690
|
<span className="text-label-sm text-muted-foreground">Live workspace</span>
|
|
@@ -4025,7 +4078,7 @@ export function ChatPanel() {
|
|
|
4025
4078
|
render={<section aria-labelledby="chat-title" />}
|
|
4026
4079
|
className="flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden"
|
|
4027
4080
|
>
|
|
4028
|
-
<CardHeader className="
|
|
4081
|
+
<CardHeader className="pb-(--card-p) shadow-[inset_0_-1px_0_var(--border)]">
|
|
4029
4082
|
{/* CardTitle carries the type; the h1 inside it is the page heading. */}
|
|
4030
4083
|
<CardTitle>
|
|
4031
4084
|
<h1 id="chat-title">Scroll behaviour</h1>
|
|
@@ -4164,7 +4217,7 @@ export function ChatPanel() {
|
|
|
4164
4217
|
</Message>
|
|
4165
4218
|
</MessageScroller>
|
|
4166
4219
|
|
|
4167
|
-
<CardFooter className="
|
|
4220
|
+
<CardFooter className="pt-(--card-p) shadow-[inset_0_1px_0_var(--border)]">
|
|
4168
4221
|
<ChatComposer
|
|
4169
4222
|
className="flex-1"
|
|
4170
4223
|
placeholder="Ask a follow-up…"
|
|
@@ -4224,18 +4277,18 @@ export function ChatPanel() {
|
|
|
4224
4277
|
|
|
4225
4278
|
# DashboardPage
|
|
4226
4279
|
|
|
4227
|
-
App-shell
|
|
4280
|
+
App-shell admin dashboard: a branded Sidebar with an account footer (AppHeader + BottomNav below lg), a KPI row, an edge-to-edge accounts table with seat-usage Meters, and supporting payout, top-customer, release and activity cards — the default shape for an operator or analytics home route.
|
|
4228
4281
|
|
|
4229
|
-
**Intent:** Reach for this when a route's job is monitoring or managing many records (sales, orders,
|
|
4282
|
+
**Intent:** Reach for this when a route's job is monitoring or managing many records (sales, orders, accounts, posts) with a few headline numbers. Not for reading-focused routes (use FeedPage) or single-record routes (use ListDetailPage's detail side).
|
|
4230
4283
|
|
|
4231
4284
|
## Notes
|
|
4232
4285
|
|
|
4233
4286
|
- Region cap is `wide` (1280px): dashboards hold tables and multi-column grids, never a reading column.
|
|
4234
|
-
- Hierarchy runs headline → records → context: KPI row, then the primary table beside its two closest companions (payout, top
|
|
4287
|
+
- Hierarchy runs headline → records → context: KPI row, then the primary table beside its two closest companions (payout, top customers), then the lower-priority lists (upcoming, activity). Every row of the grid is filled so the route ends on content, not empty canvas.
|
|
4235
4288
|
- Stat cards: CardHeader carries the label (CardDescription) and the value; KPI values are `text-mono-xl` — the metric mono tier, tabular figures — never a heading tier. The trend Badge sits with its comparison note in CardContent, so the value keeps the full card width when the row folds to 2×2 on a phone.
|
|
4236
4289
|
- Stat cards use an explicit cols map (2 → 4) — a 4-stat row must fold to 2×2, never strand an orphan the way auto-fit can.
|
|
4237
4290
|
- The table runs flush to the card edges with `<CardContent flush>`, which hands the card's padding to the table (`--table-edge-px`), so the first and last columns line up with the CardHeader at any card density — never hand-set pl-6/pr-6. Numeric columns align end in mono; Table scrolls horizontally inside the card on narrow screens.
|
|
4238
|
-
-
|
|
4291
|
+
- Seats used / seats is a `Meter` (how full a known range is), not `Progress` (task progress). Status is `Badge appearance="plain" dot`, with the app's own states mapped to tones once at the top of the file.
|
|
4239
4292
|
- Lists are Entity rows in CardList, inset to the card padding and divided by hairlines — never CardContent with hand-tuned px. A card that shares a grid row with a taller one becomes `flex flex-col` and pins its CardFooter with `mt-auto`.
|
|
4240
4293
|
- Brand and account live in the Sidebar: SidebarHeader holds the mark + wordmark, SidebarFooter holds the signed-in Entity with an OverflowMenu. Sidebar rebinds the page ink tokens to its own, so the Entity and OverflowMenu read correctly even when a theme paints the rail dark on a light page.
|
|
4241
4294
|
- The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.
|
|
@@ -4247,7 +4300,7 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
4247
4300
|
- AppShell provides the skip link and <main id="main-content"> landmark.
|
|
4248
4301
|
- The active SidebarItem and BottomNavItem carry aria-current via `active`.
|
|
4249
4302
|
- DataTable headers are real <th>; sortable columns are announced.
|
|
4250
|
-
- Each
|
|
4303
|
+
- Each seat-usage Meter has an aria-label naming the account and aria-valuetext reading "184 of 250 seats used".
|
|
4251
4304
|
- Avatars beside a visible name use an empty alt so the name is not read twice.
|
|
4252
4305
|
|
|
4253
4306
|
## Source
|
|
@@ -4255,11 +4308,11 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
4255
4308
|
```tsx
|
|
4256
4309
|
import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, OverflowMenu, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from "@sableui/react"
|
|
4257
4310
|
|
|
4258
|
-
//
|
|
4311
|
+
// Account status → Badge tone. Sable has no status vocabulary of its own;
|
|
4259
4312
|
// an app maps ITS states onto tones once, here, and every badge follows.
|
|
4260
|
-
const STATUS_TONE = {
|
|
4261
|
-
const STATUS_LABEL = {
|
|
4262
|
-
type
|
|
4313
|
+
const STATUS_TONE = { active: "success", onboarding: "info", full: "1", cancelled: "neutral" } as const
|
|
4314
|
+
const STATUS_LABEL = { active: "Active", onboarding: "Onboarding", full: "Seats full", cancelled: "Cancelled" } as const
|
|
4315
|
+
type AccountStatus = keyof typeof STATUS_TONE
|
|
4263
4316
|
|
|
4264
4317
|
const MIN = 60 * 1000
|
|
4265
4318
|
const HOUR = 60 * MIN
|
|
@@ -4272,54 +4325,54 @@ const shortDate = (offset: number) =>
|
|
|
4272
4325
|
|
|
4273
4326
|
const stats = [
|
|
4274
4327
|
{ label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
|
|
4275
|
-
{ label: "
|
|
4276
|
-
{ label: "
|
|
4277
|
-
{ label: "
|
|
4328
|
+
{ label: "Active seats", value: "1,284", delta: "+3.1%", up: true, note: "182 added" },
|
|
4329
|
+
{ label: "Accounts", value: "312", delta: "+12.4%", up: true, note: "Best month yet" },
|
|
4330
|
+
{ label: "Seat usage", value: "64%", delta: "−1.8%", up: false, note: "6 near their limit" },
|
|
4278
4331
|
]
|
|
4279
4332
|
|
|
4280
|
-
const
|
|
4281
|
-
{ id: "
|
|
4282
|
-
{ id: "
|
|
4283
|
-
{ id: "
|
|
4284
|
-
{ id: "
|
|
4285
|
-
{ id: "
|
|
4286
|
-
{ id: "
|
|
4287
|
-
{ id: "
|
|
4288
|
-
{ id: "
|
|
4289
|
-
{ id: "
|
|
4333
|
+
const accounts = [
|
|
4334
|
+
{ id: "ac_01", title: "Northwind", status: "active" as const, used: 184, seats: 250, price: 16, started: -6 * DAY },
|
|
4335
|
+
{ id: "ac_02", title: "Harbor Labs", status: "full" as const, used: 100, seats: 100, price: 20, started: -19 * DAY },
|
|
4336
|
+
{ id: "ac_03", title: "Brightline", status: "active" as const, used: 61, seats: 150, price: 16, started: -2 * DAY },
|
|
4337
|
+
{ id: "ac_04", title: "Tidewater Co.", status: "active" as const, used: 212, seats: 300, price: 12, started: -27 * DAY },
|
|
4338
|
+
{ id: "ac_05", title: "Saltworks", status: "cancelled" as const, used: 38, seats: 75, price: 20, started: -44 * DAY },
|
|
4339
|
+
{ id: "ac_06", title: "Paper Moon", status: "full" as const, used: 50, seats: 50, price: 24, started: -58 * DAY },
|
|
4340
|
+
{ id: "ac_07", title: "Signal Studio", status: "onboarding" as const, used: 0, seats: 200, price: 16, started: 3 * DAY },
|
|
4341
|
+
{ id: "ac_08", title: "Bluewater", status: "cancelled" as const, used: 72, seats: 120, price: 12, started: -71 * DAY },
|
|
4342
|
+
{ id: "ac_09", title: "Ember & Field", status: "full" as const, used: 80, seats: 80, price: 20, started: -86 * DAY },
|
|
4290
4343
|
]
|
|
4291
|
-
const
|
|
4344
|
+
const accountCount = 312
|
|
4292
4345
|
|
|
4293
4346
|
const payout = {
|
|
4294
|
-
amount:
|
|
4347
|
+
amount: 8862.64,
|
|
4295
4348
|
date: 5 * DAY,
|
|
4296
4349
|
lines: [
|
|
4297
|
-
{ label: "
|
|
4298
|
-
{ label: "
|
|
4350
|
+
{ label: "Subscriptions", value: 9127.33 },
|
|
4351
|
+
{ label: "Processing (2.9%)", value: -264.69 },
|
|
4299
4352
|
{ label: "Deposits to", value: "Checking ••4821" },
|
|
4300
4353
|
],
|
|
4301
4354
|
}
|
|
4302
4355
|
|
|
4303
|
-
const
|
|
4304
|
-
{ id: "
|
|
4305
|
-
{ id: "
|
|
4306
|
-
{ id: "
|
|
4307
|
-
{ id: "
|
|
4356
|
+
const customers = [
|
|
4357
|
+
{ id: "cu_01", name: "Northwind", handle: "northwind.io", initials: "NW", seats: 184, spent: 2944 },
|
|
4358
|
+
{ id: "cu_02", name: "Tidewater Co.", handle: "tidewater.co", initials: "TC", seats: 212, spent: 2544 },
|
|
4359
|
+
{ id: "cu_03", name: "Harbor Labs", handle: "harborlabs.com", initials: "HL", seats: 100, spent: 2000 },
|
|
4360
|
+
{ id: "cu_04", name: "Ember & Field", handle: "emberfield.com", initials: "EF", seats: 80, spent: 1600 },
|
|
4308
4361
|
]
|
|
4309
4362
|
|
|
4310
4363
|
const upcoming = [
|
|
4311
|
-
{ id: "up_01", title: "
|
|
4312
|
-
{ id: "up_02", title: "
|
|
4313
|
-
{ id: "up_03", title: "
|
|
4314
|
-
{ id: "up_04", title: "
|
|
4364
|
+
{ id: "up_01", title: "Version 2.4", detail: "Shared reports · All plans", at: 3 * DAY },
|
|
4365
|
+
{ id: "up_02", title: "Audit log export", detail: "Business plan · Beta", at: 12 * DAY },
|
|
4366
|
+
{ id: "up_03", title: "Version 2.5", detail: "Offline mode · All plans", at: 26 * DAY },
|
|
4367
|
+
{ id: "up_04", title: "Usage-based billing", detail: "Studio and Business · Beta", at: 34 * DAY },
|
|
4315
4368
|
]
|
|
4316
4369
|
|
|
4317
4370
|
const activity = [
|
|
4318
|
-
{ id: "
|
|
4319
|
-
{ id: "
|
|
4320
|
-
{ id: "
|
|
4321
|
-
{ id: "
|
|
4322
|
-
{ id: "
|
|
4371
|
+
{ id: "ev_01", who: "Nina Simone", initials: "NS", what: "added 12 seats to Northwind · 184 of 250", when: 2 * MIN },
|
|
4372
|
+
{ id: "ev_02", who: "Grace Hopper", initials: "GH", what: "upgraded Brightline to Studio", when: 26 * MIN },
|
|
4373
|
+
{ id: "ev_03", who: "Frida Kahlo", initials: "FK", what: "paid invoice INV-0042 for Tidewater Co.", when: 1 * HOUR },
|
|
4374
|
+
{ id: "ev_04", who: "Mary Shelley", initials: "MS", what: "joined the Harbor Labs workspace", when: 3 * HOUR },
|
|
4375
|
+
{ id: "ev_05", who: "Nina Simone", initials: "NS", what: "booked onboarding for Signal Studio", when: 6 * HOUR },
|
|
4323
4376
|
]
|
|
4324
4377
|
|
|
4325
4378
|
export function DashboardPage() {
|
|
@@ -4341,8 +4394,8 @@ export function DashboardPage() {
|
|
|
4341
4394
|
bottomNav={
|
|
4342
4395
|
<BottomNav aria-label="Primary">
|
|
4343
4396
|
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel active render={<a href="/" />} />
|
|
4344
|
-
<BottomNavItem icon={<Icon name="
|
|
4345
|
-
<BottomNavItem icon={<Icon name="user" />} label="
|
|
4397
|
+
<BottomNavItem icon={<Icon name="folder" />} label="Accounts" showLabel render={<a href="/accounts" />} />
|
|
4398
|
+
<BottomNavItem icon={<Icon name="user" />} label="Customers" showLabel render={<a href="/customers" />} />
|
|
4346
4399
|
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
4347
4400
|
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel render={<a href="/settings" />} />
|
|
4348
4401
|
</BottomNav>
|
|
@@ -4354,8 +4407,8 @@ export function DashboardPage() {
|
|
|
4354
4407
|
</SidebarHeader>
|
|
4355
4408
|
<SidebarNav>
|
|
4356
4409
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" active render={<a href="/" />} />
|
|
4357
|
-
<SidebarItem icon={<Icon name="
|
|
4358
|
-
<SidebarItem icon={<Icon name="user" />} label="
|
|
4410
|
+
<SidebarItem icon={<Icon name="folder" />} label="Accounts" render={<a href="/accounts" />} />
|
|
4411
|
+
<SidebarItem icon={<Icon name="user" />} label="Customers" render={<a href="/customers" />} />
|
|
4359
4412
|
<SidebarItem
|
|
4360
4413
|
icon={<Icon name="bell" />}
|
|
4361
4414
|
label="Activity"
|
|
@@ -4386,7 +4439,7 @@ export function DashboardPage() {
|
|
|
4386
4439
|
<Page max="wide" gap={4} className="py-6">
|
|
4387
4440
|
<PageHeader
|
|
4388
4441
|
title="Overview"
|
|
4389
|
-
description="
|
|
4442
|
+
description="Revenue and account activity for the last 30 days."
|
|
4390
4443
|
actions={
|
|
4391
4444
|
<Row gap={2}>
|
|
4392
4445
|
<Button variant="outline">
|
|
@@ -4395,7 +4448,7 @@ export function DashboardPage() {
|
|
|
4395
4448
|
</Button>
|
|
4396
4449
|
<Button>
|
|
4397
4450
|
<Icon name="plus" />
|
|
4398
|
-
New
|
|
4451
|
+
New account
|
|
4399
4452
|
</Button>
|
|
4400
4453
|
</Row>
|
|
4401
4454
|
}
|
|
@@ -4425,43 +4478,43 @@ export function DashboardPage() {
|
|
|
4425
4478
|
<Grid cols={{ base: 1, xl: 3 }} gap={3}>
|
|
4426
4479
|
<Card className="flex flex-col xl:col-span-2">
|
|
4427
4480
|
<CardHeader>
|
|
4428
|
-
<CardTitle>
|
|
4429
|
-
<CardDescription>
|
|
4481
|
+
<CardTitle>Accounts</CardTitle>
|
|
4482
|
+
<CardDescription>Seats across your customers, newest first.</CardDescription>
|
|
4430
4483
|
</CardHeader>
|
|
4431
4484
|
<CardContent flush className="mt-4 flex-1">
|
|
4432
4485
|
<DataTable
|
|
4433
|
-
data={
|
|
4486
|
+
data={accounts}
|
|
4434
4487
|
rowKey={(row) => row.id}
|
|
4435
|
-
defaultSort={{ key: "
|
|
4488
|
+
defaultSort={{ key: "started", direction: "desc" }}
|
|
4436
4489
|
columns={[
|
|
4437
|
-
{ key: "title", header: "
|
|
4490
|
+
{ key: "title", header: "Account", sortable: true, className: "text-label-md whitespace-nowrap" },
|
|
4438
4491
|
{
|
|
4439
4492
|
key: "status",
|
|
4440
4493
|
header: "Status",
|
|
4441
4494
|
cell: (row) => (
|
|
4442
|
-
<Badge appearance="plain" tone={STATUS_TONE[row.status as
|
|
4443
|
-
{STATUS_LABEL[row.status as
|
|
4495
|
+
<Badge appearance="plain" tone={STATUS_TONE[row.status as AccountStatus]} dot>
|
|
4496
|
+
{STATUS_LABEL[row.status as AccountStatus]}
|
|
4444
4497
|
</Badge>
|
|
4445
4498
|
),
|
|
4446
4499
|
},
|
|
4447
4500
|
{
|
|
4448
|
-
key: "
|
|
4449
|
-
header: "
|
|
4501
|
+
key: "used",
|
|
4502
|
+
header: "Seats",
|
|
4450
4503
|
sortable: true,
|
|
4451
|
-
sortValue: (row) => row.
|
|
4504
|
+
sortValue: (row) => row.used / row.seats,
|
|
4452
4505
|
cell: (row) => (
|
|
4453
4506
|
<Row gap={3} align="center" className="min-w-40">
|
|
4454
4507
|
<Meter
|
|
4455
|
-
value={row.
|
|
4456
|
-
max={row.
|
|
4508
|
+
value={row.used}
|
|
4509
|
+
max={row.seats}
|
|
4457
4510
|
size="sm"
|
|
4458
|
-
tone={row.
|
|
4459
|
-
aria-label={`${row.title}
|
|
4460
|
-
getAriaValueText={(_, v) => `${v} of ${row.
|
|
4511
|
+
tone={row.used === row.seats ? "success" : "primary"}
|
|
4512
|
+
aria-label={`${row.title} seats used`}
|
|
4513
|
+
getAriaValueText={(_, v) => `${v} of ${row.seats} seats used`}
|
|
4461
4514
|
className="flex-1"
|
|
4462
4515
|
/>
|
|
4463
4516
|
<span className="text-mono-caption whitespace-nowrap text-muted-foreground">
|
|
4464
|
-
{row.
|
|
4517
|
+
{row.used}/{row.seats}
|
|
4465
4518
|
</span>
|
|
4466
4519
|
</Row>
|
|
4467
4520
|
),
|
|
@@ -4471,21 +4524,21 @@ export function DashboardPage() {
|
|
|
4471
4524
|
header: "Revenue",
|
|
4472
4525
|
align: "end",
|
|
4473
4526
|
sortable: true,
|
|
4474
|
-
sortValue: (row) => row.
|
|
4527
|
+
sortValue: (row) => row.used * row.price,
|
|
4475
4528
|
cell: (row) =>
|
|
4476
|
-
row.
|
|
4477
|
-
<span className="text-mono-sm">{usd.format(row.
|
|
4529
|
+
row.used > 0 ? (
|
|
4530
|
+
<span className="text-mono-sm">{usd.format(row.used * row.price)}</span>
|
|
4478
4531
|
) : (
|
|
4479
4532
|
<span className="text-mono-sm text-muted-foreground">—</span>
|
|
4480
4533
|
),
|
|
4481
4534
|
},
|
|
4482
4535
|
{
|
|
4483
|
-
key: "
|
|
4484
|
-
header: "
|
|
4536
|
+
key: "started",
|
|
4537
|
+
header: "Started",
|
|
4485
4538
|
align: "end",
|
|
4486
4539
|
sortable: true,
|
|
4487
4540
|
cell: (row) => (
|
|
4488
|
-
<span className="text-body-sm whitespace-nowrap text-muted-foreground">{shortDate(row.
|
|
4541
|
+
<span className="text-body-sm whitespace-nowrap text-muted-foreground">{shortDate(row.started)}</span>
|
|
4489
4542
|
),
|
|
4490
4543
|
},
|
|
4491
4544
|
]}
|
|
@@ -4493,10 +4546,10 @@ export function DashboardPage() {
|
|
|
4493
4546
|
</CardContent>
|
|
4494
4547
|
<CardFooter className="justify-between">
|
|
4495
4548
|
<span className="text-caption text-muted-foreground">
|
|
4496
|
-
Showing {
|
|
4549
|
+
Showing {accounts.length} of {accountCount} accounts
|
|
4497
4550
|
</span>
|
|
4498
|
-
<Button variant="ghost" size="sm" bleed="end" render={<a href="/
|
|
4499
|
-
View all
|
|
4551
|
+
<Button variant="ghost" size="sm" bleed="end" render={<a href="/accounts" />}>
|
|
4552
|
+
View all accounts
|
|
4500
4553
|
<Icon name="arrow-right" />
|
|
4501
4554
|
</Button>
|
|
4502
4555
|
</CardFooter>
|
|
@@ -4527,21 +4580,21 @@ export function DashboardPage() {
|
|
|
4527
4580
|
</Card>
|
|
4528
4581
|
<Card>
|
|
4529
4582
|
<CardHeader>
|
|
4530
|
-
<CardTitle>Top
|
|
4583
|
+
<CardTitle>Top customers</CardTitle>
|
|
4531
4584
|
<CardDescription>By spend this month.</CardDescription>
|
|
4532
4585
|
<CardAction>
|
|
4533
|
-
<Button variant="ghost" size="sm" render={<a href="/
|
|
4586
|
+
<Button variant="ghost" size="sm" render={<a href="/customers" />}>
|
|
4534
4587
|
View all
|
|
4535
4588
|
</Button>
|
|
4536
4589
|
</CardAction>
|
|
4537
4590
|
</CardHeader>
|
|
4538
4591
|
<CardList>
|
|
4539
|
-
{
|
|
4592
|
+
{customers.map((person) => (
|
|
4540
4593
|
<Entity
|
|
4541
4594
|
key={person.id}
|
|
4542
4595
|
leading={<Avatar variant="art" size="sm" fallback={person.initials} alt="" />}
|
|
4543
4596
|
title={person.name}
|
|
4544
|
-
subtitle={`${person.handle} · ${person.
|
|
4597
|
+
subtitle={`${person.handle} · ${person.seats} seats`}
|
|
4545
4598
|
action={<span className="text-mono-sm">{usd.format(person.spent)}</span>}
|
|
4546
4599
|
/>
|
|
4547
4600
|
))}
|
|
@@ -4553,7 +4606,7 @@ export function DashboardPage() {
|
|
|
4553
4606
|
<Card className="flex flex-col">
|
|
4554
4607
|
<CardHeader>
|
|
4555
4608
|
<CardTitle>Upcoming releases</CardTitle>
|
|
4556
|
-
<CardDescription>Scheduled to
|
|
4609
|
+
<CardDescription>Scheduled to ship to customers.</CardDescription>
|
|
4557
4610
|
</CardHeader>
|
|
4558
4611
|
<CardList>
|
|
4559
4612
|
{upcoming.map((release) => (
|
|
@@ -4566,7 +4619,7 @@ export function DashboardPage() {
|
|
|
4566
4619
|
))}
|
|
4567
4620
|
</CardList>
|
|
4568
4621
|
<CardFooter className="mt-auto">
|
|
4569
|
-
<Button variant="outline" size="sm" render={<a href="/
|
|
4622
|
+
<Button variant="outline" size="sm" render={<a href="/releases/new" />}>
|
|
4570
4623
|
<Icon name="calendar" />
|
|
4571
4624
|
Schedule a release
|
|
4572
4625
|
</Button>
|
|
@@ -4575,7 +4628,7 @@ export function DashboardPage() {
|
|
|
4575
4628
|
<Card className="xl:col-span-2">
|
|
4576
4629
|
<CardHeader>
|
|
4577
4630
|
<CardTitle>Activity</CardTitle>
|
|
4578
|
-
<CardDescription>
|
|
4631
|
+
<CardDescription>Seats, upgrades, and invoices.</CardDescription>
|
|
4579
4632
|
<CardAction>
|
|
4580
4633
|
<Button variant="ghost" size="sm" render={<a href="/activity" />}>
|
|
4581
4634
|
View all
|
|
@@ -4605,7 +4658,7 @@ export function DashboardPage() {
|
|
|
4605
4658
|
|
|
4606
4659
|
# FeedPage
|
|
4607
4660
|
|
|
4608
|
-
Social reading feed with a side rail: a feed-width main column (composer, For you / Following Tabs, one Card per post, an
|
|
4661
|
+
Social reading feed with a side rail: a feed-width main column (composer, For you / Following Tabs, one Card per post, an event post with price, capacity Meter and Register, a Skeleton for the next page) beside a sticky rail of who-to-follow and upcoming events that drops away below lg.
|
|
4609
4662
|
|
|
4610
4663
|
**Intent:** Reach for this for timelines, activity streams, and any reverse-chronological reading surface where people also act on what they read. Not for record management (DashboardPage) or side-by-side browsing (ListDetailPage).
|
|
4611
4664
|
|
|
@@ -4615,7 +4668,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
|
|
|
4615
4668
|
- Feed scope is `Tabs` (underline, stretched to the column with `flex`): peer views of the same feed. The composer sits above them because posting applies to both.
|
|
4616
4669
|
- The composer is a Card holding `Textarea autoResize` plus ghost icon Buttons and a secondary `Post` — not ChatComposer, which submits on Enter and is for conversations.
|
|
4617
4670
|
- Each post is a Card: an Entity byline in a CardList (with an OverflowMenu for post actions), body copy, then media in an AspectBox at the asset's own ratio, inset with `rounded-concentric-lg/6` so its corners follow the card's in every theme.
|
|
4618
|
-
- An
|
|
4671
|
+
- An event post adds a `Badge appearance="overlay"` on the image, title and price, the one primary Button on the page (`Register`), and a `Meter` for capacity — seats taken against a known maximum is a level, not task progress.
|
|
4619
4672
|
- Actions are ghost sm Buttons (icon + count) in a Row pulled out by the button's own padding (`-ms-3`) so the first icon sits on the text edge.
|
|
4620
4673
|
- Loading the next page shows a Skeleton post in the shape the post will land in, above the explicit load-more Button in its `loading` state — predictable for keyboard and AT users, unlike infinite scroll.
|
|
4621
4674
|
|
|
@@ -4624,7 +4677,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
|
|
|
4624
4677
|
- Each post is an <article>, so AT users can jump between posts; the rail is a labelled <aside> landmark.
|
|
4625
4678
|
- Rail card titles render as h2 (CardTitle `render={<h2 />}`) under the page's one h1.
|
|
4626
4679
|
- Icon-only buttons and every Follow / Remind button carry aria-labels naming their target; counts stay in the accessible name.
|
|
4627
|
-
- The
|
|
4680
|
+
- The capacity Meter is role=meter with a getAriaValueText reading ("28 of 40 seats taken").
|
|
4628
4681
|
- The Skeleton card is aria-busy; the loading Button keeps its label ("Loading posts").
|
|
4629
4682
|
|
|
4630
4683
|
## Source
|
|
@@ -4641,7 +4694,7 @@ type Post = {
|
|
|
4641
4694
|
following: boolean
|
|
4642
4695
|
body: string
|
|
4643
4696
|
media?: { src: string; alt: string }
|
|
4644
|
-
|
|
4697
|
+
event?: { title: string; price: string; registered: number; capacity: number }
|
|
4645
4698
|
likes: number
|
|
4646
4699
|
comments: number
|
|
4647
4700
|
}
|
|
@@ -4654,9 +4707,9 @@ const posts: Post[] = [
|
|
|
4654
4707
|
initials: "AL",
|
|
4655
4708
|
time: "2h",
|
|
4656
4709
|
following: true,
|
|
4657
|
-
body: "
|
|
4658
|
-
media: { src: "/media/edition-cover.svg", alt: "
|
|
4659
|
-
|
|
4710
|
+
body: "Registration for the spring workshop is open. Two days of plotter drawing in a small group. See you there.",
|
|
4711
|
+
media: { src: "/media/edition-cover.svg", alt: "Workshop poster, a gold-leaf study in overlapping arcs" },
|
|
4712
|
+
event: { title: "Spring plotter workshop", price: "$120", registered: 28, capacity: 40 },
|
|
4660
4713
|
likes: 128,
|
|
4661
4714
|
comments: 24,
|
|
4662
4715
|
},
|
|
@@ -4679,7 +4732,7 @@ const posts: Post[] = [
|
|
|
4679
4732
|
initials: "NS",
|
|
4680
4733
|
time: "8h",
|
|
4681
4734
|
following: false,
|
|
4682
|
-
body: "
|
|
4735
|
+
body: "Thanks to everyone at the Dusk listening session — the full recording is now in your library. Headphones on.",
|
|
4683
4736
|
likes: 214,
|
|
4684
4737
|
comments: 31,
|
|
4685
4738
|
},
|
|
@@ -4691,8 +4744,8 @@ const suggestions = [
|
|
|
4691
4744
|
]
|
|
4692
4745
|
|
|
4693
4746
|
const upcoming = [
|
|
4694
|
-
{ id: "
|
|
4695
|
-
{ id: "
|
|
4747
|
+
{ id: "e_01", title: "Quiet Harbor talk", host: "Mary Shelley", initials: "MS", date: "Sat, Oct 12" },
|
|
4748
|
+
{ id: "e_02", title: "Dusk, live", host: "Nina Simone", initials: "NS", date: "Tue, Oct 15" },
|
|
4696
4749
|
]
|
|
4697
4750
|
|
|
4698
4751
|
const renderPost = (post: Post) => (
|
|
@@ -4718,35 +4771,35 @@ const renderPost = (post: Post) => (
|
|
|
4718
4771
|
{post.media ? (
|
|
4719
4772
|
<AspectBox ratio={16 / 9} className="overflow-hidden rounded-concentric-lg/6 bg-muted">
|
|
4720
4773
|
<img src={post.media.src} alt={post.media.alt} className="size-full object-cover" />
|
|
4721
|
-
{post.
|
|
4774
|
+
{post.event ? (
|
|
4722
4775
|
<div className="absolute start-3 top-3">
|
|
4723
4776
|
<Badge appearance="overlay" size="sm" dot>
|
|
4724
|
-
|
|
4777
|
+
Open now
|
|
4725
4778
|
</Badge>
|
|
4726
4779
|
</div>
|
|
4727
4780
|
) : null}
|
|
4728
4781
|
</AspectBox>
|
|
4729
4782
|
) : null}
|
|
4730
|
-
{post.
|
|
4783
|
+
{post.event ? (
|
|
4731
4784
|
<Stack gap={1.5}>
|
|
4732
4785
|
<Row gap={2} align="center" justify="between">
|
|
4733
4786
|
<div className="min-w-0">
|
|
4734
|
-
<p className="text-title-md text-foreground">{post.
|
|
4787
|
+
<p className="text-title-md text-foreground">{post.event.title}</p>
|
|
4735
4788
|
<p className="text-body-sm text-muted-foreground">
|
|
4736
|
-
{`${post.
|
|
4789
|
+
{`${post.event.price} · ${post.event.capacity} seats`}
|
|
4737
4790
|
</p>
|
|
4738
4791
|
</div>
|
|
4739
|
-
<Button>{`
|
|
4792
|
+
<Button>{`Register for ${post.event.price}`}</Button>
|
|
4740
4793
|
</Row>
|
|
4741
4794
|
<Meter
|
|
4742
|
-
value={post.
|
|
4743
|
-
max={post.
|
|
4795
|
+
value={post.event.registered}
|
|
4796
|
+
max={post.event.capacity}
|
|
4744
4797
|
size="sm"
|
|
4745
|
-
getAriaValueText={(_, value) => `${value} of ${post.
|
|
4798
|
+
getAriaValueText={(_, value) => `${value} of ${post.event?.capacity} seats taken`}
|
|
4746
4799
|
>
|
|
4747
4800
|
<Row gap={2} align="center" justify="between" className="mb-2">
|
|
4748
|
-
<MeterLabel>{`${post.
|
|
4749
|
-
<MeterValue>{(_, value) => `${(post.
|
|
4801
|
+
<MeterLabel>{`${post.event.registered} registered`}</MeterLabel>
|
|
4802
|
+
<MeterValue>{(_, value) => `${(post.event?.capacity ?? 0) - value} left`}</MeterValue>
|
|
4750
4803
|
</Row>
|
|
4751
4804
|
<MeterTrack />
|
|
4752
4805
|
</Meter>
|
|
@@ -4787,15 +4840,15 @@ export function FeedPage() {
|
|
|
4787
4840
|
autoResize
|
|
4788
4841
|
rows={2}
|
|
4789
4842
|
aria-label="New post"
|
|
4790
|
-
placeholder="Share a process shot or an upcoming
|
|
4843
|
+
placeholder="Share a process shot or an upcoming event"
|
|
4791
4844
|
/>
|
|
4792
4845
|
<Row gap={1} align="center" justify="between">
|
|
4793
4846
|
<Row gap={0.5}>
|
|
4794
4847
|
<Button variant="ghost" size="icon-sm" aria-label="Add images">
|
|
4795
4848
|
<Icon name="image-stack" />
|
|
4796
4849
|
</Button>
|
|
4797
|
-
<Button variant="ghost" size="icon-sm" aria-label="
|
|
4798
|
-
<Icon name="
|
|
4850
|
+
<Button variant="ghost" size="icon-sm" aria-label="Add an event">
|
|
4851
|
+
<Icon name="calendar" />
|
|
4799
4852
|
</Button>
|
|
4800
4853
|
</Row>
|
|
4801
4854
|
<Button variant="secondary" size="sm">
|
|
@@ -4862,17 +4915,17 @@ export function FeedPage() {
|
|
|
4862
4915
|
</Card>
|
|
4863
4916
|
<Card>
|
|
4864
4917
|
<CardHeader>
|
|
4865
|
-
<CardTitle render={<h2 />}>Upcoming
|
|
4918
|
+
<CardTitle render={<h2 />}>Upcoming events</CardTitle>
|
|
4866
4919
|
</CardHeader>
|
|
4867
4920
|
<CardList>
|
|
4868
|
-
{upcoming.map((
|
|
4921
|
+
{upcoming.map((event) => (
|
|
4869
4922
|
<Entity
|
|
4870
|
-
key={
|
|
4871
|
-
leading={<Avatar variant="art" fallback={
|
|
4872
|
-
title={
|
|
4873
|
-
subtitle={`${
|
|
4923
|
+
key={event.id}
|
|
4924
|
+
leading={<Avatar variant="art" fallback={event.initials} alt={event.host} />}
|
|
4925
|
+
title={event.title}
|
|
4926
|
+
subtitle={`${event.host} · ${event.date}`}
|
|
4874
4927
|
action={
|
|
4875
|
-
<Button variant="ghost" size="icon-sm" aria-label={`Remind me about ${
|
|
4928
|
+
<Button variant="ghost" size="icon-sm" aria-label={`Remind me about ${event.title}`}>
|
|
4876
4929
|
<Icon name="bell" />
|
|
4877
4930
|
</Button>
|
|
4878
4931
|
}
|
|
@@ -5177,7 +5230,7 @@ export function LandingPage() {
|
|
|
5177
5230
|
<CardDescription>Team plan</CardDescription>
|
|
5178
5231
|
</CardHeader>
|
|
5179
5232
|
<CardContent>
|
|
5180
|
-
<Meter value={64} tone="
|
|
5233
|
+
<Meter value={64} tone="3">
|
|
5181
5234
|
<Row justify="between" wrap className="mb-2">
|
|
5182
5235
|
<MeterLabel>Used</MeterLabel>
|
|
5183
5236
|
<MeterValue>{(_, value) => `${(value * 30).toLocaleString("en-US")} of 3,000`}</MeterValue>
|
|
@@ -5460,9 +5513,9 @@ const roleLabel: Record<Role, string> = {
|
|
|
5460
5513
|
|
|
5461
5514
|
const roleSummary: Record<Role, string> = {
|
|
5462
5515
|
owner: "Full access, including payouts and billing. Transfer ownership in studio settings.",
|
|
5463
|
-
admin: "Manages members,
|
|
5464
|
-
editor: "Drafts,
|
|
5465
|
-
viewer: "Sees
|
|
5516
|
+
admin: "Manages members, projects and billing.",
|
|
5517
|
+
editor: "Drafts, edits and publishes projects.",
|
|
5518
|
+
viewer: "Sees projects and reports, but can't change them.",
|
|
5466
5519
|
}
|
|
5467
5520
|
|
|
5468
5521
|
type Activity = { id: string; icon: string; text: string; when: string }
|
|
@@ -5477,7 +5530,7 @@ type Member = {
|
|
|
5477
5530
|
handle?: string
|
|
5478
5531
|
joined?: string
|
|
5479
5532
|
lastActive?: string
|
|
5480
|
-
|
|
5533
|
+
projects?: number
|
|
5481
5534
|
invitedBy?: string
|
|
5482
5535
|
sent?: string
|
|
5483
5536
|
expires?: string
|
|
@@ -5490,40 +5543,40 @@ const seats = 10
|
|
|
5490
5543
|
const initialMembers: Member[] = [
|
|
5491
5544
|
{
|
|
5492
5545
|
id: "u_01", name: "Ada Lovelace", handle: "@ada", email: "ada@atelier.art", initials: "AL", role: "owner", pending: false,
|
|
5493
|
-
joined: "Mar 4, 2024", lastActive: "Online now",
|
|
5546
|
+
joined: "Mar 4, 2024", lastActive: "Online now", projects: 12,
|
|
5494
5547
|
activity: [
|
|
5495
|
-
{ id: "a1", icon: "
|
|
5548
|
+
{ id: "a1", icon: "file", text: "Published the Analytical Engine No. 3 roadmap", when: "2 hours ago" },
|
|
5496
5549
|
{ id: "a2", icon: "mail", text: "Invited louise@bourgeois.studio as Editor", when: "2 days ago" },
|
|
5497
5550
|
],
|
|
5498
5551
|
},
|
|
5499
5552
|
{
|
|
5500
5553
|
id: "u_02", name: "Grace Hopper", handle: "@grace", email: "grace@atelier.art", initials: "GH", role: "admin", pending: false,
|
|
5501
|
-
joined: "Mar 9, 2024", lastActive: "20 minutes ago",
|
|
5554
|
+
joined: "Mar 9, 2024", lastActive: "20 minutes ago", projects: 4,
|
|
5502
5555
|
activity: [
|
|
5503
5556
|
{ id: "a1", icon: "user", text: "Changed Frida Kahlo's role to Viewer", when: "Yesterday" },
|
|
5504
|
-
{ id: "a2", icon: "edit", text: "Updated
|
|
5505
|
-
{ id: "a3", icon: "
|
|
5557
|
+
{ id: "a2", icon: "edit", text: "Updated the budget on First Compiler", when: "3 days ago" },
|
|
5558
|
+
{ id: "a3", icon: "file", text: "Created the First Compiler project", when: "Sep 28" },
|
|
5506
5559
|
],
|
|
5507
5560
|
},
|
|
5508
5561
|
{
|
|
5509
5562
|
id: "u_03", name: "Mary Shelley", handle: "@mary", email: "mary@atelier.art", initials: "MS", role: "editor", pending: false,
|
|
5510
|
-
joined: "Jun 18, 2024", lastActive: "3 hours ago",
|
|
5563
|
+
joined: "Jun 18, 2024", lastActive: "3 hours ago", projects: 7,
|
|
5511
5564
|
activity: [
|
|
5512
5565
|
{ id: "a1", icon: "calendar", text: "Scheduled Frankenstein Studies for Oct 14", when: "3 hours ago" },
|
|
5513
|
-
{ id: "a2", icon: "
|
|
5566
|
+
{ id: "a2", icon: "file", text: "Published The Last Man report", when: "Sep 21" },
|
|
5514
5567
|
],
|
|
5515
5568
|
},
|
|
5516
5569
|
{
|
|
5517
5570
|
id: "u_04", name: "Nina Simone", handle: "@nina", email: "nina@atelier.art", initials: "NS", role: "editor", pending: false,
|
|
5518
|
-
joined: "Jan 12, 2025", lastActive: "Yesterday",
|
|
5571
|
+
joined: "Jan 12, 2025", lastActive: "Yesterday", projects: 3,
|
|
5519
5572
|
activity: [
|
|
5520
|
-
{ id: "a1", icon: "upload", text: "Uploaded
|
|
5521
|
-
{ id: "a2", icon: "
|
|
5573
|
+
{ id: "a1", icon: "upload", text: "Uploaded files for Little Girl Blue", when: "Yesterday" },
|
|
5574
|
+
{ id: "a2", icon: "file", text: "Published the Feeling Good brief", when: "Aug 30" },
|
|
5522
5575
|
],
|
|
5523
5576
|
},
|
|
5524
5577
|
{
|
|
5525
5578
|
id: "u_05", name: "Frida Kahlo", handle: "@frida", email: "frida@atelier.art", initials: "FK", role: "viewer", pending: false,
|
|
5526
|
-
joined: "Jul 2, 2025", lastActive: "4 days ago",
|
|
5579
|
+
joined: "Jul 2, 2025", lastActive: "4 days ago", projects: 0,
|
|
5527
5580
|
activity: [{ id: "a1", icon: "comment", text: "Commented on the Las Dos Fridas draft", when: "4 days ago" }],
|
|
5528
5581
|
},
|
|
5529
5582
|
{
|
|
@@ -5756,8 +5809,8 @@ export function ListDetailPage() {
|
|
|
5756
5809
|
<dd className={selected.lastActive === "Online now" ? "text-success" : undefined}>
|
|
5757
5810
|
{selected.lastActive}
|
|
5758
5811
|
</dd>
|
|
5759
|
-
<dt>
|
|
5760
|
-
<dd>{selected.
|
|
5812
|
+
<dt>Projects owned</dt>
|
|
5813
|
+
<dd>{selected.projects}</dd>
|
|
5761
5814
|
</dl>
|
|
5762
5815
|
)}
|
|
5763
5816
|
</div>
|
|
@@ -5810,7 +5863,7 @@ export function ListDetailPage() {
|
|
|
5810
5863
|
description={
|
|
5811
5864
|
selected.pending
|
|
5812
5865
|
? `${selected.email} won't be able to join with the link they were sent.`
|
|
5813
|
-
: "They lose access to the
|
|
5866
|
+
: "They lose access to the workspace right away. Projects they own stay with the team."
|
|
5814
5867
|
}
|
|
5815
5868
|
confirmLabel={selected.pending ? "Revoke invite" : "Remove member"}
|
|
5816
5869
|
onConfirm={() => remove(selected.id)}
|
|
@@ -5875,17 +5928,17 @@ const timezones = [
|
|
|
5875
5928
|
]
|
|
5876
5929
|
|
|
5877
5930
|
const notifications = [
|
|
5878
|
-
{ name: "
|
|
5879
|
-
{ name: "
|
|
5880
|
-
{ name: "
|
|
5881
|
-
{ name: "notifyDigest", title: "Weekly digest", description: "
|
|
5931
|
+
{ name: "notifyComments", title: "New comments", description: "When someone comments on your work", on: true },
|
|
5932
|
+
{ name: "notifyInvoicePaid", title: "Invoice paid", description: "When a customer pays an invoice", on: true },
|
|
5933
|
+
{ name: "notifyMembers", title: "New members", description: "When someone joins your workspace", on: false },
|
|
5934
|
+
{ name: "notifyDigest", title: "Weekly digest", description: "Activity and usage, every Monday", on: true },
|
|
5882
5935
|
{ name: "notifyProduct", title: "Product updates", description: "New Atelier features", on: false },
|
|
5883
5936
|
]
|
|
5884
5937
|
|
|
5885
5938
|
const payouts = [
|
|
5886
5939
|
{ label: "Next payout", value: "$1,284.60", note: "Friday, Oct 16" },
|
|
5887
5940
|
{ label: "Pending", value: "$412.00", note: "Clears in 7 days" },
|
|
5888
|
-
{ label: "Paid this year", value: "$18,920.00", note: "Across 214
|
|
5941
|
+
{ label: "Paid this year", value: "$18,920.00", note: "Across 214 invoices" },
|
|
5889
5942
|
]
|
|
5890
5943
|
|
|
5891
5944
|
export function SettingsPage() {
|
|
@@ -5910,8 +5963,8 @@ export function SettingsPage() {
|
|
|
5910
5963
|
bottomNav={
|
|
5911
5964
|
<BottomNav aria-label="Primary">
|
|
5912
5965
|
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel render={<a href="/" />} />
|
|
5913
|
-
<BottomNavItem icon={<Icon name="
|
|
5914
|
-
<BottomNavItem icon={<Icon name="user" />} label="
|
|
5966
|
+
<BottomNavItem icon={<Icon name="folder" />} label="Accounts" showLabel render={<a href="/accounts" />} />
|
|
5967
|
+
<BottomNavItem icon={<Icon name="user" />} label="Customers" showLabel render={<a href="/customers" />} />
|
|
5915
5968
|
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
5916
5969
|
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel active render={<a href="/settings" />} />
|
|
5917
5970
|
</BottomNav>
|
|
@@ -5923,8 +5976,8 @@ export function SettingsPage() {
|
|
|
5923
5976
|
</SidebarHeader>
|
|
5924
5977
|
<SidebarNav>
|
|
5925
5978
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" render={<a href="/" />} />
|
|
5926
|
-
<SidebarItem icon={<Icon name="
|
|
5927
|
-
<SidebarItem icon={<Icon name="user" />} label="
|
|
5979
|
+
<SidebarItem icon={<Icon name="folder" />} label="Accounts" render={<a href="/accounts" />} />
|
|
5980
|
+
<SidebarItem icon={<Icon name="user" />} label="Customers" render={<a href="/customers" />} />
|
|
5928
5981
|
<SidebarItem
|
|
5929
5982
|
icon={<Icon name="bell" />}
|
|
5930
5983
|
label="Activity"
|
|
@@ -5978,7 +6031,7 @@ export function SettingsPage() {
|
|
|
5978
6031
|
<Card id="profile" className="scroll-mt-6">
|
|
5979
6032
|
<CardHeader>
|
|
5980
6033
|
<CardTitle render={<h2 />}>Profile</CardTitle>
|
|
5981
|
-
<CardDescription>How
|
|
6034
|
+
<CardDescription>How your team sees you on Atelier.</CardDescription>
|
|
5982
6035
|
</CardHeader>
|
|
5983
6036
|
{/* The one explicit save on the page: profile text is drafted, then
|
|
5984
6037
|
committed. Toggles below apply the moment they change. */}
|
|
@@ -6009,14 +6062,14 @@ export function SettingsPage() {
|
|
|
6009
6062
|
<Input defaultValue="ada" autoComplete="username" spellCheck={false} />
|
|
6010
6063
|
</Field>
|
|
6011
6064
|
</Grid>
|
|
6012
|
-
<Field label="Bio" name="bio" description="Shown on your profile and
|
|
6065
|
+
<Field label="Bio" name="bio" description="Shown on your profile and in comments.">
|
|
6013
6066
|
<Textarea
|
|
6014
6067
|
rows={3}
|
|
6015
|
-
defaultValue="
|
|
6068
|
+
defaultValue="Product designer working on tools for small teams. Writing about process every month."
|
|
6016
6069
|
/>
|
|
6017
6070
|
</Field>
|
|
6018
6071
|
<Grid cols={{ base: 1, sm: 2 }} gap={3}>
|
|
6019
|
-
<Field label="Timezone" name="timezone" description="
|
|
6072
|
+
<Field label="Timezone" name="timezone" description="Due dates use this zone.">
|
|
6020
6073
|
<Select defaultValue="europe-london" items={timezones}>
|
|
6021
6074
|
<SelectTrigger />
|
|
6022
6075
|
<SelectContent>
|
|
@@ -6041,7 +6094,7 @@ export function SettingsPage() {
|
|
|
6041
6094
|
<CardHeader>
|
|
6042
6095
|
<CardTitle render={<h2 />}>Notifications</CardTitle>
|
|
6043
6096
|
<CardDescription>
|
|
6044
|
-
Email updates about your
|
|
6097
|
+
Email updates about your projects. Changes save automatically.
|
|
6045
6098
|
</CardDescription>
|
|
6046
6099
|
</CardHeader>
|
|
6047
6100
|
<CardList>
|
|
@@ -6104,7 +6157,7 @@ export function SettingsPage() {
|
|
|
6104
6157
|
<Entity
|
|
6105
6158
|
leading={<Icon name="download" className="text-lg text-muted-foreground" />}
|
|
6106
6159
|
title="Export data"
|
|
6107
|
-
subtitle="
|
|
6160
|
+
subtitle="Projects, invoices, and members"
|
|
6108
6161
|
action={
|
|
6109
6162
|
<Button variant="outline" size="sm">
|
|
6110
6163
|
Export
|
|
@@ -6118,8 +6171,8 @@ export function SettingsPage() {
|
|
|
6118
6171
|
<CardHeader>
|
|
6119
6172
|
<CardTitle render={<h2 />}>Delete account</CardTitle>
|
|
6120
6173
|
<CardDescription>
|
|
6121
|
-
Permanently remove your profile,
|
|
6122
|
-
|
|
6174
|
+
Permanently remove your profile, projects, and billing history. Files you shared
|
|
6175
|
+
stay with your team.
|
|
6123
6176
|
</CardDescription>
|
|
6124
6177
|
</CardHeader>
|
|
6125
6178
|
<CardFooter className="justify-end">
|
|
@@ -6136,7 +6189,7 @@ export function SettingsPage() {
|
|
|
6136
6189
|
open={deleteOpen}
|
|
6137
6190
|
onOpenChange={setDeleteOpen}
|
|
6138
6191
|
title="Delete your account?"
|
|
6139
|
-
description="This permanently removes your profile, 12
|
|
6192
|
+
description="This permanently removes your profile, 12 projects, and billing history. Your final invoice is still sent. This can't be undone."
|
|
6140
6193
|
confirmLabel="Delete account"
|
|
6141
6194
|
confirmingLabel="Deleting…"
|
|
6142
6195
|
confirmText="ada"
|
|
@@ -6413,7 +6466,7 @@ A delete-account card: what deleting removes and what survives, one destructive
|
|
|
6413
6466
|
|
|
6414
6467
|
- Copied from the SettingsPage page shell's Delete account section.
|
|
6415
6468
|
- The button never acts directly: it opens the modal, whose confirm stays disabled until the handle is typed.
|
|
6416
|
-
- The description says what is lost AND what is kept ("
|
|
6469
|
+
- The description says what is lost AND what is kept ("Files you shared stay with your team"), which is what people want to know before they decide.
|
|
6417
6470
|
|
|
6418
6471
|
## Accessibility
|
|
6419
6472
|
|
|
@@ -6426,7 +6479,7 @@ A delete-account card: what deleting removes and what survives, one destructive
|
|
|
6426
6479
|
import { useState } from "react"
|
|
6427
6480
|
import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, cn, DestructiveActionModal } from "@sableui/react"
|
|
6428
6481
|
|
|
6429
|
-
const account = { handle: "ada",
|
|
6482
|
+
const account = { handle: "ada", projects: 12 }
|
|
6430
6483
|
|
|
6431
6484
|
export interface DangerZoneBlockProps {
|
|
6432
6485
|
className?: string
|
|
@@ -6444,7 +6497,7 @@ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZo
|
|
|
6444
6497
|
<CardHeader>
|
|
6445
6498
|
<CardTitle render={<h2 />}>Delete account</CardTitle>
|
|
6446
6499
|
<CardDescription>
|
|
6447
|
-
Permanently remove your profile,
|
|
6500
|
+
Permanently remove your profile, projects, and billing history. Files you shared stay with your team.
|
|
6448
6501
|
</CardDescription>
|
|
6449
6502
|
</CardHeader>
|
|
6450
6503
|
<CardFooter actions className="justify-end">
|
|
@@ -6457,7 +6510,7 @@ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZo
|
|
|
6457
6510
|
open={open}
|
|
6458
6511
|
onOpenChange={setOpen}
|
|
6459
6512
|
title="Delete your account?"
|
|
6460
|
-
description={`This permanently removes your profile, ${account.
|
|
6513
|
+
description={`This permanently removes your profile, ${account.projects} projects, and billing history. Your final invoice is still sent. This can't be undone.`}
|
|
6461
6514
|
confirmLabel="Delete account"
|
|
6462
6515
|
confirmingLabel="Deleting…"
|
|
6463
6516
|
confirmText={account.handle}
|
|
@@ -6686,10 +6739,10 @@ const intro = {
|
|
|
6686
6739
|
}
|
|
6687
6740
|
|
|
6688
6741
|
const questions = [
|
|
6689
|
-
{ q: "Is there a free plan?", a: "Yes. Starter is free forever for three
|
|
6742
|
+
{ q: "Is there a free plan?", a: "Yes. Starter is free forever for three active projects, with no card needed to sign up." },
|
|
6690
6743
|
{ q: "Can I change plans later?", a: "Any time. Upgrades apply immediately and are prorated; downgrades take effect at the end of the billing period." },
|
|
6691
|
-
{ q: "Who owns the
|
|
6692
|
-
{ q: "How
|
|
6744
|
+
{ q: "Who owns the data I add?", a: "You do. You can export everything in your workspace at any time, and we never sell it." },
|
|
6745
|
+
{ q: "How does billing work?", a: "Plans are billed per seat at the start of each period, to the card or account you add in settings." },
|
|
6693
6746
|
]
|
|
6694
6747
|
|
|
6695
6748
|
export interface FaqBlockProps {
|
|
@@ -6902,7 +6955,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
|
|
|
6902
6955
|
|
|
6903
6956
|
const goals = [
|
|
6904
6957
|
{ goal: "Releases", progress: 82 },
|
|
6905
|
-
{ goal: "
|
|
6958
|
+
{ goal: "Customers", progress: 64 },
|
|
6906
6959
|
{ goal: "Revenue", progress: 91 },
|
|
6907
6960
|
]
|
|
6908
6961
|
|
|
@@ -7466,7 +7519,7 @@ An activity list: Entity rows with the actor's Avatar, who did what, and a compa
|
|
|
7466
7519
|
import { Avatar, Card, CardList, cn, Entity, StatusDot } from "@sableui/react"
|
|
7467
7520
|
|
|
7468
7521
|
const notifications = [
|
|
7469
|
-
{ initials: "MO", name: "Mara Okafor", what: "
|
|
7522
|
+
{ initials: "MO", name: "Mara Okafor", what: "invited you to Q3 planning", when: "2m", unread: true },
|
|
7470
7523
|
{ initials: "SR", name: "Sam Rivera", what: "left a comment on Dusk", when: "1h", unread: false },
|
|
7471
7524
|
]
|
|
7472
7525
|
|
|
@@ -7743,32 +7796,32 @@ const tiers = [
|
|
|
7743
7796
|
{
|
|
7744
7797
|
id: "starter",
|
|
7745
7798
|
name: "Starter",
|
|
7746
|
-
blurb: "For one person
|
|
7799
|
+
blurb: "For one person getting a first project going.",
|
|
7747
7800
|
monthly: 0,
|
|
7748
7801
|
yearly: 0,
|
|
7749
7802
|
cta: "Start for free",
|
|
7750
7803
|
featured: false,
|
|
7751
|
-
features: ["3
|
|
7804
|
+
features: ["3 active projects", "Shared docs", "Community support"],
|
|
7752
7805
|
},
|
|
7753
7806
|
{
|
|
7754
7807
|
id: "studio",
|
|
7755
7808
|
name: "Studio",
|
|
7756
|
-
blurb: "For
|
|
7809
|
+
blurb: "For small teams shipping on a steady schedule.",
|
|
7757
7810
|
monthly: 20,
|
|
7758
7811
|
yearly: 16,
|
|
7759
7812
|
cta: "Start free trial",
|
|
7760
7813
|
featured: true,
|
|
7761
|
-
features: ["Unlimited
|
|
7814
|
+
features: ["Unlimited projects", "Scheduled reports", "Usage analytics", "Priority support"],
|
|
7762
7815
|
},
|
|
7763
7816
|
{
|
|
7764
|
-
id: "
|
|
7765
|
-
name: "
|
|
7766
|
-
blurb: "For
|
|
7817
|
+
id: "business",
|
|
7818
|
+
name: "Business",
|
|
7819
|
+
blurb: "For companies running several teams at once.",
|
|
7767
7820
|
monthly: 48,
|
|
7768
7821
|
yearly: 40,
|
|
7769
7822
|
cta: "Talk to sales",
|
|
7770
7823
|
featured: false,
|
|
7771
|
-
features: ["Everything in Studio", "Up to 10 seats", "
|
|
7824
|
+
features: ["Everything in Studio", "Up to 10 seats", "SSO and audit log", "Roles and approvals"],
|
|
7772
7825
|
},
|
|
7773
7826
|
]
|
|
7774
7827
|
|
|
@@ -7788,10 +7841,10 @@ export function PricingTableBlock({ className, onChoose }: PricingTableBlockProp
|
|
|
7788
7841
|
<Stack gap={10}>
|
|
7789
7842
|
<Stack gap={4} align="center" className="text-center">
|
|
7790
7843
|
<h2 id={titleId} className="text-balance text-heading-1">
|
|
7791
|
-
Pricing that grows with your
|
|
7844
|
+
Pricing that grows with your team
|
|
7792
7845
|
</h2>
|
|
7793
7846
|
<p className="max-w-(--measure-prose) text-body-lg text-muted-foreground">
|
|
7794
|
-
Every plan includes unlimited
|
|
7847
|
+
Every plan includes unlimited guests, and you can switch plans at any time.
|
|
7795
7848
|
</p>
|
|
7796
7849
|
<ToggleGroup value={[period]} onValueChange={(v: unknown[]) => v[0] && setPeriod(v[0] as Period)} aria-label="Billing period">
|
|
7797
7850
|
<Toggle value="monthly">Monthly</Toggle>
|
|
@@ -7803,14 +7856,14 @@ export function PricingTableBlock({ className, onChoose }: PricingTableBlockProp
|
|
|
7803
7856
|
<Card
|
|
7804
7857
|
key={tier.id}
|
|
7805
7858
|
render={<article />}
|
|
7806
|
-
className={cn("flex flex-col", tier.featured && "
|
|
7859
|
+
className={cn("flex flex-col", tier.featured && "outline-primary ring-1 ring-primary")}
|
|
7807
7860
|
>
|
|
7808
7861
|
<CardHeader>
|
|
7809
7862
|
<CardTitle render={<h3 />}>{tier.name}</CardTitle>
|
|
7810
7863
|
<CardDescription>{tier.blurb}</CardDescription>
|
|
7811
7864
|
{tier.featured ? (
|
|
7812
7865
|
<CardAction>
|
|
7813
|
-
<Badge tone="
|
|
7866
|
+
<Badge tone="1" size="sm">
|
|
7814
7867
|
Most popular
|
|
7815
7868
|
</Badge>
|
|
7816
7869
|
</CardAction>
|
|
@@ -7848,15 +7901,15 @@ export function PricingTableBlock({ className, onChoose }: PricingTableBlockProp
|
|
|
7848
7901
|
|
|
7849
7902
|
# ProductCardBlock
|
|
7850
7903
|
|
|
7851
|
-
A product card: edge-to-edge
|
|
7904
|
+
A product card: edge-to-edge product image, title and maker, a status Badge, and the price beside a single buy action.
|
|
7852
7905
|
|
|
7853
|
-
**Intent:** Reach for this for a product
|
|
7906
|
+
**Intent:** Reach for this for a product or listing in a grid or a featured slot. For a row in a dense list, use an Entity with the image as `leading` instead.
|
|
7854
7907
|
|
|
7855
7908
|
**Block:** Commerce · card · designed for 16rem–20rem wide (fills its container up to the max)
|
|
7856
7909
|
|
|
7857
7910
|
## Notes
|
|
7858
7911
|
|
|
7859
|
-
- The
|
|
7912
|
+
- The image runs to the card's edges; the card clips it to its own corners (overflow-hidden).
|
|
7860
7913
|
- Prices are `text-mono-lg` — the metric mono tier with tabular figures — never a heading tier.
|
|
7861
7914
|
- The placeholder art is painted from the palette's decorative tones so it follows the theme; swap it for the product image with real alt text.
|
|
7862
7915
|
|
|
@@ -7871,7 +7924,7 @@ A product card: edge-to-edge artwork, title and maker, an edition Badge, and the
|
|
|
7871
7924
|
import { AspectBox, Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn, MediaImage } from "@sableui/react"
|
|
7872
7925
|
|
|
7873
7926
|
// `image` is the product shot URL; left empty, it paints art seeded by the title.
|
|
7874
|
-
const product = { title: "
|
|
7927
|
+
const product = { title: "Linen throw blanket", maker: "Northfield Home", image: "", badge: "New", price: "$64.00" }
|
|
7875
7928
|
|
|
7876
7929
|
|
|
7877
7930
|
export interface ProductCardBlockProps {
|
|
@@ -7892,12 +7945,12 @@ export function ProductCardBlock({ className, framed = true, onCollect }: Produc
|
|
|
7892
7945
|
<CardTitle>{product.title}</CardTitle>
|
|
7893
7946
|
<CardDescription>{product.maker}</CardDescription>
|
|
7894
7947
|
<CardAction>
|
|
7895
|
-
<Badge tone="
|
|
7948
|
+
<Badge tone="1">{product.badge}</Badge>
|
|
7896
7949
|
</CardAction>
|
|
7897
7950
|
</CardHeader>
|
|
7898
7951
|
<CardFooter className="justify-between pt-4">
|
|
7899
7952
|
<span className="text-mono-lg">{product.price}</span>
|
|
7900
|
-
<Button onClick={onCollect}>
|
|
7953
|
+
<Button onClick={onCollect}>Add to cart</Button>
|
|
7901
7954
|
</CardFooter>
|
|
7902
7955
|
</Card>
|
|
7903
7956
|
)
|
|
@@ -7935,7 +7988,7 @@ const profile = {
|
|
|
7935
7988
|
name: "Juno Avery",
|
|
7936
7989
|
initials: "JA",
|
|
7937
7990
|
handle: "@juno · Sound and light",
|
|
7938
|
-
bio: "Field recordings, slowed down. New
|
|
7991
|
+
bio: "Field recordings, slowed down. New track every full moon.",
|
|
7939
7992
|
stats: [
|
|
7940
7993
|
{ value: "128", label: "posts" },
|
|
7941
7994
|
{ value: "4.2k", label: "followers" },
|
|
@@ -8027,7 +8080,7 @@ const profile = {
|
|
|
8027
8080
|
name: "Ada Lovelace",
|
|
8028
8081
|
initials: "AL",
|
|
8029
8082
|
handle: "ada",
|
|
8030
|
-
bio: "
|
|
8083
|
+
bio: "Product designer working on tools for small teams. Writing about process every month.",
|
|
8031
8084
|
timezone: "europe-london",
|
|
8032
8085
|
}
|
|
8033
8086
|
|
|
@@ -8052,7 +8105,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
|
|
|
8052
8105
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-2xl", className)}>
|
|
8053
8106
|
<CardHeader>
|
|
8054
8107
|
<CardTitle render={<h2 />}>Profile</CardTitle>
|
|
8055
|
-
<CardDescription>How
|
|
8108
|
+
<CardDescription>How your team sees you.</CardDescription>
|
|
8056
8109
|
</CardHeader>
|
|
8057
8110
|
{/* The one explicit save: profile text is drafted, then committed. */}
|
|
8058
8111
|
<Form
|
|
@@ -8086,7 +8139,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
|
|
|
8086
8139
|
<Input defaultValue={profile.handle} autoComplete="username" spellCheck={false} />
|
|
8087
8140
|
</Field>
|
|
8088
8141
|
</Grid>
|
|
8089
|
-
<Field label="Bio" name="bio" description="Shown on your profile and
|
|
8142
|
+
<Field label="Bio" name="bio" description="Shown on your profile and in comments.">
|
|
8090
8143
|
<Textarea rows={3} defaultValue={profile.bio} />
|
|
8091
8144
|
</Field>
|
|
8092
8145
|
<Grid cols={{ base: 1, sm: 2 }} gap={3}>
|
|
@@ -8143,10 +8196,10 @@ import { Badge, Card, CardAction, CardContent, CardDescription, CardHeader, cn,
|
|
|
8143
8196
|
const revenue = { label: "Revenue this month", total: "$12,480", change: "+8.2%" }
|
|
8144
8197
|
|
|
8145
8198
|
const segments = [
|
|
8146
|
-
{ label: "
|
|
8147
|
-
{ label: "
|
|
8148
|
-
{ label: "
|
|
8149
|
-
{ label: "
|
|
8199
|
+
{ label: "Direct", value: 42, tone: "1" },
|
|
8200
|
+
{ label: "Partners", value: 27, tone: "2" },
|
|
8201
|
+
{ label: "Referrals", value: 19, tone: "3" },
|
|
8202
|
+
{ label: "Resellers", value: 12, tone: "info" },
|
|
8150
8203
|
] as const
|
|
8151
8204
|
|
|
8152
8205
|
export interface RevenueBreakdownBlockProps {
|
|
@@ -8168,7 +8221,7 @@ export function RevenueBreakdownBlock({ className, framed = true }: RevenueBreak
|
|
|
8168
8221
|
</CardHeader>
|
|
8169
8222
|
<CardContent className="flex flex-col gap-4 pt-2">
|
|
8170
8223
|
<span className="text-mono-xl">{revenue.total}</span>
|
|
8171
|
-
<SegmentedBar label="Revenue by
|
|
8224
|
+
<SegmentedBar label="Revenue by channel" segments={[...segments]} size="lg" />
|
|
8172
8225
|
{/* Two columns while each entry fits whole, one when they would not:
|
|
8173
8226
|
sized in em, so a larger type or a narrower card drops to one.
|
|
8174
8227
|
aria-hidden: the bar already reads every share aloud. */}
|
|
@@ -8463,9 +8516,9 @@ import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, G
|
|
|
8463
8516
|
|
|
8464
8517
|
const stats = [
|
|
8465
8518
|
{ label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
|
|
8466
|
-
{ label: "
|
|
8467
|
-
{ label: "
|
|
8468
|
-
{ label: "
|
|
8519
|
+
{ label: "Orders", value: "1,284", delta: "+3.1%", up: true, note: "182 first-time" },
|
|
8520
|
+
{ label: "New customers", value: "312", delta: "+12.4%", up: true, note: "Best month yet" },
|
|
8521
|
+
{ label: "Conversion", value: "3.4%", delta: "−0.2%", up: false, note: "6 open trials" },
|
|
8469
8522
|
]
|
|
8470
8523
|
|
|
8471
8524
|
export interface StatRowBlockProps {
|
|
@@ -8600,9 +8653,9 @@ import { useState, useId } from "react"
|
|
|
8600
8653
|
import { Badge, Button, Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle, Checkbox, cn, Icon, Label } from "@sableui/react"
|
|
8601
8654
|
|
|
8602
8655
|
const tasks = [
|
|
8603
|
-
{ title: "Pick the cover type", tag: "Design", tone: "
|
|
8656
|
+
{ title: "Pick the cover type", tag: "Design", tone: "2", done: true },
|
|
8604
8657
|
{ title: "Wire up checkout", tag: "Engineering", tone: "info", done: false },
|
|
8605
|
-
{ title: "Draft launch post", tag: "Launch", tone: "
|
|
8658
|
+
{ title: "Draft launch post", tag: "Launch", tone: "1", done: false },
|
|
8606
8659
|
] as const
|
|
8607
8660
|
|
|
8608
8661
|
export interface TaskListBlockProps {
|
|
@@ -8719,7 +8772,7 @@ export function TeamMembersBlock({ className, framed = true, onInvite, onMemberA
|
|
|
8719
8772
|
subtitle={m.email}
|
|
8720
8773
|
action={
|
|
8721
8774
|
<div className="flex items-center gap-2">
|
|
8722
|
-
<Badge appearance={m.role === "Pending" ? "outline" : "plain"} tone={m.role === "Owner" ? "
|
|
8775
|
+
<Badge appearance={m.role === "Pending" ? "outline" : "plain"} tone={m.role === "Owner" ? "1" : "neutral"} size="sm">
|
|
8723
8776
|
{m.role}
|
|
8724
8777
|
</Badge>
|
|
8725
8778
|
{/* The owner is not managed from here; a placeholder keeps the column aligned. */}
|
|
@@ -9134,7 +9187,7 @@ export function VerifyCodeBlock({ className, framed = true, onVerify, onResend }
|
|
|
9134
9187
|
|
|
9135
9188
|
A balance card: the balance in the metric mono tier, Send and Receive as an equal pair, and recent activity as Entity rows with signed amounts.
|
|
9136
9189
|
|
|
9137
|
-
**Intent:** Reach for this for
|
|
9190
|
+
**Intent:** Reach for this for an account balance, store credit or credits summary with its latest movements. For a full transaction history with filters, use a DataTable.
|
|
9138
9191
|
|
|
9139
9192
|
**Block:** Commerce · card · designed for 18rem–20rem wide (fills its container up to the max)
|
|
9140
9193
|
|
|
@@ -9142,24 +9195,24 @@ A balance card: the balance in the metric mono tier, Send and Receive as an equa
|
|
|
9142
9195
|
|
|
9143
9196
|
- The balance is CardTitle at `text-mono-xl` — tabular figures, never a heading tier — under its label as CardDescription.
|
|
9144
9197
|
- Send is primary and Receive secondary, side by side at equal width: two directions of one action, with sending the riskier and more frequent.
|
|
9145
|
-
- Incoming amounts are `text-success`; outgoing stay muted rather than red, because a
|
|
9198
|
+
- Incoming amounts are `text-success`; outgoing stay muted rather than red, because a payout or refund is not an error.
|
|
9146
9199
|
|
|
9147
9200
|
## Accessibility
|
|
9148
9201
|
|
|
9149
9202
|
- Each activity row's leading swatch is aria-hidden; the title and detail carry the meaning.
|
|
9150
|
-
- Amounts keep their sign in text (+
|
|
9203
|
+
- Amounts keep their sign in text (+49.00 / -850.00), so the direction does not depend on colour.
|
|
9151
9204
|
|
|
9152
9205
|
## Source
|
|
9153
9206
|
|
|
9154
9207
|
```tsx
|
|
9155
9208
|
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Entity, Icon } from "@sableui/react"
|
|
9156
9209
|
|
|
9157
|
-
const balance = "
|
|
9210
|
+
const balance = "$2,418.50"
|
|
9158
9211
|
|
|
9159
9212
|
const activity = [
|
|
9160
|
-
{ title: "
|
|
9161
|
-
{ title: "
|
|
9162
|
-
{ title: "
|
|
9213
|
+
{ title: "Subscription", detail: "Team plan · 2h ago", amount: "+49.00", tone: "1" },
|
|
9214
|
+
{ title: "Refund", detail: "Order 1042 · Yesterday", amount: "-24.00", tone: "2" },
|
|
9215
|
+
{ title: "Payout", detail: "To checking · Mon", amount: "-850.00", tone: "3" },
|
|
9163
9216
|
]
|
|
9164
9217
|
|
|
9165
9218
|
export interface WalletBlockProps {
|
|
@@ -9269,7 +9322,7 @@ import { PageHeader, Region } from "@sableui/react"
|
|
|
9269
9322
|
```tsx
|
|
9270
9323
|
import { Card, CardHeader, CardTitle, Grid, Region } from "@sableui/react"
|
|
9271
9324
|
|
|
9272
|
-
<Region max="wide"><Grid minItemWidth="16rem"><Card><CardHeader><CardTitle>Revenue</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>
|
|
9325
|
+
<Region max="wide"><Grid minItemWidth="16rem"><Card><CardHeader><CardTitle>Revenue</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>Orders</CardTitle></CardHeader></Card></Grid></Region>
|
|
9273
9326
|
```
|
|
9274
9327
|
|
|
9275
9328
|
### Fluid workspace: the queue's table takes the extra width
|
|
@@ -9319,7 +9372,7 @@ Use Page as a route content shell that combines Region sizing with vertical rhyt
|
|
|
9319
9372
|
```tsx
|
|
9320
9373
|
import { Card, CardContent, Page, PageHeader } from "@sableui/react"
|
|
9321
9374
|
|
|
9322
|
-
<Page as="main" max="feed"><PageHeader title="Home" description="Latest posts from
|
|
9375
|
+
<Page as="main" max="feed"><PageHeader title="Home" description="Latest posts from people you follow." /><Card><CardContent><p className="text-body-md">First post.</p></CardContent></Card></Page>
|
|
9323
9376
|
```
|
|
9324
9377
|
|
|
9325
9378
|
---
|
|
@@ -9390,7 +9443,7 @@ import { Card, CardDescription, CardHeader, Grid } from "@sableui/react"
|
|
|
9390
9443
|
```tsx
|
|
9391
9444
|
import { Badge, Grid } from "@sableui/react"
|
|
9392
9445
|
|
|
9393
|
-
<Grid cols={{ base: 1, md: 2, lg: 4 }}><Badge appearance="plain" tone="
|
|
9446
|
+
<Grid cols={{ base: 1, md: 2, lg: 4 }}><Badge appearance="plain" tone="3" dot /><Badge appearance="plain" tone="warning" dot /><Badge appearance="plain" tone="neutral" dot /><Badge appearance="plain" tone="destructive" dot /></Grid>
|
|
9394
9447
|
```
|
|
9395
9448
|
|
|
9396
9449
|
---
|
|
@@ -9729,7 +9782,7 @@ Use SplitPane for responsive two-pane layouts where a primary pane and supportin
|
|
|
9729
9782
|
```tsx
|
|
9730
9783
|
import { AspectBox, Button, SplitPane, Stack } from "@sableui/react"
|
|
9731
9784
|
|
|
9732
|
-
<SplitPane ratio="2/1" primary={<AspectBox ratio={1}><img src={post.imageUrl} alt={post.title} className="size-full object-cover" /></AspectBox>} secondary={<Stack gap={2}><h1 className="text-heading-1">{post.title}</h1><p className="text-body-md text-muted-foreground">{post.description}</p><Button>
|
|
9785
|
+
<SplitPane ratio="2/1" primary={<AspectBox ratio={1}><img src={post.imageUrl} alt={post.title} className="size-full object-cover" /></AspectBox>} secondary={<Stack gap={2}><h1 className="text-heading-1">{post.title}</h1><p className="text-body-md text-muted-foreground">{post.description}</p><Button>Add to cart</Button></Stack>} />
|
|
9733
9786
|
```
|
|
9734
9787
|
|
|
9735
9788
|
---
|
|
@@ -9892,7 +9945,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
|
|
|
9892
9945
|
```tsx
|
|
9893
9946
|
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sableui/react"
|
|
9894
9947
|
|
|
9895
|
-
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger>
|
|
9948
|
+
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem><AccordionItem value="b"><AccordionTrigger>How much?</AccordionTrigger><AccordionPanel>$16 per seat each month.</AccordionPanel></AccordionItem></Accordion>
|
|
9896
9949
|
```
|
|
9897
9950
|
|
|
9898
9951
|
### Multiple open
|
|
@@ -9908,7 +9961,7 @@ import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sab
|
|
|
9908
9961
|
```tsx
|
|
9909
9962
|
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Icon } from "@sableui/react"
|
|
9910
9963
|
|
|
9911
|
-
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground transition-transform [[data-panel-open]_&]:rotate-45" />}>
|
|
9964
|
+
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground transition-transform [[data-panel-open]_&]:rotate-45" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>
|
|
9912
9965
|
```
|
|
9913
9966
|
|
|
9914
9967
|
---
|
|
@@ -9938,7 +9991,7 @@ Use AppHeader for compact application chrome at the top of a screen, usually wit
|
|
|
9938
9991
|
| --- | --- | --- | --- |
|
|
9939
9992
|
| leading | ReactNode | — | Leading slot — back button, create action, or menu trigger. |
|
|
9940
9993
|
| title | ReactNode | — | Center slot — brand wordmark or contextual page title. |
|
|
9941
|
-
| actions | ReactNode | — | Trailing slot —
|
|
9994
|
+
| actions | ReactNode | — | Trailing slot — account menu, action buttons. |
|
|
9942
9995
|
|
|
9943
9996
|
## Tokens used
|
|
9944
9997
|
|
|
@@ -10060,7 +10113,7 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
|
|
|
10060
10113
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
10061
10114
|
|
|
10062
10115
|
<AppShell
|
|
10063
|
-
banners={<Banner tone="warning">
|
|
10116
|
+
banners={<Banner tone="warning">Sync is delayed — changes may be slow to appear.</Banner>}
|
|
10064
10117
|
header={<AppHeader title="Studio" />}
|
|
10065
10118
|
sidebar={
|
|
10066
10119
|
<Sidebar>
|
|
@@ -10079,7 +10132,7 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
|
|
|
10079
10132
|
}
|
|
10080
10133
|
>
|
|
10081
10134
|
<Region max="feed">
|
|
10082
|
-
<PageHeader title="Home" description="Latest posts from
|
|
10135
|
+
<PageHeader title="Home" description="Latest posts from people you follow." />
|
|
10083
10136
|
</Region>
|
|
10084
10137
|
</AppShell>
|
|
10085
10138
|
```
|
|
@@ -10181,7 +10234,7 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
10181
10234
|
|
|
10182
10235
|
## Tokens used
|
|
10183
10236
|
|
|
10184
|
-
`tone-
|
|
10237
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
10185
10238
|
|
|
10186
10239
|
## Motion
|
|
10187
10240
|
|
|
@@ -10540,7 +10593,7 @@ import { Avatar, Entity, StatusDot } from "@sableui/react"
|
|
|
10540
10593
|
|
|
10541
10594
|
**Status:** stable
|
|
10542
10595
|
|
|
10543
|
-
Compact static label, set on two axes: appearance (soft, outline, solid, overlay on media, plain text) and tone (neutral or a
|
|
10596
|
+
Compact static label, set on two axes: appearance (soft, outline, solid, overlay on media, plain text) and tone (neutral, a semantic tone, or a palette family like blue for a category with no meaning attached). An optional leading icon, or a status dot (with pulse) for state.
|
|
10544
10597
|
|
|
10545
10598
|
**Keywords:** badge, tag, label, status, metadata, chip, pill, tone, category
|
|
10546
10599
|
|
|
@@ -10551,7 +10604,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10551
10604
|
### Best practices
|
|
10552
10605
|
|
|
10553
10606
|
- Do: Keep badge text short: one to three words or a compact count.
|
|
10554
|
-
- Do: Use tone variants when the badge carries meaning, such as success, warning
|
|
10607
|
+
- Do: Use tone variants when the badge carries meaning, such as success, warning or destructive, or a decorative tone for a category or plan.
|
|
10555
10608
|
- Do: Pair icons with text; the icon reinforces meaning but should not be the only cue.
|
|
10556
10609
|
- Do: Choose appearance and tone separately: appearance is how loud it is (soft by default, solid for emphasis, overlay on images, plain for a status line); tone is what it means.
|
|
10557
10610
|
- Don't: Use a Badge as a button. If it triggers an action, use Button, Toggle, or a menu item.
|
|
@@ -10570,7 +10623,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10570
10623
|
| Name | Type | Default | Description |
|
|
10571
10624
|
| --- | --- | --- | --- |
|
|
10572
10625
|
| appearance | "soft" \| "outline" \| "solid" \| "overlay" \| "plain" | "soft" | How it is drawn. soft: a tinted fill (neutral is the secondary surface). outline: a hairline. solid: the tone at full strength (neutral is the inverted chip). overlay: legible on images — a dark scrim, or the solid tone. plain: no fill or padding, a status line. |
|
|
10573
|
-
| tone | "neutral" \| "
|
|
10626
|
+
| tone | "neutral" \| "1" \| "2" \| "3" \| "info" \| "success" \| "warning" \| "destructive" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "purple" \| "pink" | "neutral" | What it means. Every appearance takes every tone. The palette families (red … pink) are categories with no meaning attached — tags, labels, project colours — painted from the theme's generated palette, so they lean toward the brand and read in both modes. |
|
|
10574
10627
|
| size | "sm" \| "md" | "md" | "sm" uses tighter padding + text-label-sm; "md" is the default look. |
|
|
10575
10628
|
| icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. Ignored when dot is set. |
|
|
10576
10629
|
| dot | boolean | false | A leading status dot in the badge's tone. With appearance="plain" it is a status line (● Ready). |
|
|
@@ -10579,12 +10632,12 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10579
10632
|
## Variants
|
|
10580
10633
|
|
|
10581
10634
|
- **appearance**: soft, outline, solid, overlay, plain
|
|
10582
|
-
- **tone**: neutral,
|
|
10635
|
+
- **tone**: neutral, 1, 2, 3, info, success, warning, destructive, red, orange, yellow, green, teal, blue, purple, pink
|
|
10583
10636
|
- **size**: sm, md
|
|
10584
10637
|
|
|
10585
10638
|
## Tokens used
|
|
10586
10639
|
|
|
10587
|
-
`secondary`, `secondary-foreground`, `border`, `foreground`, `background`, `muted`, `muted-foreground`, `tone-
|
|
10640
|
+
`palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-dot`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `border`, `foreground`, `background`, `muted`, `muted-foreground`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `success`, `tone-warning`, `destructive`, `overlay`, `overlay-foreground`, `tone-1-foreground`, `tone-2-foreground`, `tone-3-foreground`, `tone-info-foreground`, `success-foreground`, `tone-warning-foreground`, `destructive-foreground`
|
|
10588
10641
|
|
|
10589
10642
|
## Accessibility
|
|
10590
10643
|
|
|
@@ -10603,7 +10656,15 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10603
10656
|
```tsx
|
|
10604
10657
|
import { Badge, Row } from "@sableui/react"
|
|
10605
10658
|
|
|
10606
|
-
<Row gap={1}><Badge tone="
|
|
10659
|
+
<Row gap={1}><Badge tone="1">Business</Badge><Badge tone="2">Studio</Badge><Badge tone="3">Starter</Badge></Row>
|
|
10660
|
+
```
|
|
10661
|
+
|
|
10662
|
+
### Palette colours (categories)
|
|
10663
|
+
|
|
10664
|
+
```tsx
|
|
10665
|
+
import { Badge, Row } from "@sableui/react"
|
|
10666
|
+
|
|
10667
|
+
<Row gap={1}><Badge tone="blue">Design</Badge><Badge tone="teal">Research</Badge><Badge tone="orange">Marketing</Badge><Badge tone="pink" appearance="outline">Ops</Badge><Badge tone="purple" appearance="solid">Launch</Badge></Row>
|
|
10607
10668
|
```
|
|
10608
10669
|
|
|
10609
10670
|
### Solid
|
|
@@ -10643,7 +10704,7 @@ import { Badge } from "@sableui/react"
|
|
|
10643
10704
|
```tsx
|
|
10644
10705
|
import { Badge, Icon, Row } from "@sableui/react"
|
|
10645
10706
|
|
|
10646
|
-
<Row gap={1}><Badge>12
|
|
10707
|
+
<Row gap={1}><Badge>12 projects</Badge><Badge appearance="outline">Draft</Badge><Badge tone="info" icon={<Icon name="info" />}>Curated</Badge></Row>
|
|
10647
10708
|
```
|
|
10648
10709
|
|
|
10649
10710
|
### Semantic state
|
|
@@ -10660,7 +10721,7 @@ import { Badge, Row } from "@sableui/react"
|
|
|
10660
10721
|
|
|
10661
10722
|
**Status:** stable
|
|
10662
10723
|
|
|
10663
|
-
Full-width, page-level announcement bar. A tone (neutral, info, success, warning,
|
|
10724
|
+
Full-width, page-level announcement bar. A tone (neutral, info, success, warning, the decorative 1, destructive) × an appearance (soft tint or loud solid fill), and an optional leading scope label. Centers its message, or — with a trailing action — switches to a leading-text + trailing-button layout. Optionally sticky and dismissible.
|
|
10664
10725
|
|
|
10665
10726
|
**Keywords:** banner, announcement, alert, status, message, warning, success, offline, sticky
|
|
10666
10727
|
|
|
@@ -10689,7 +10750,7 @@ Use Banner for page-level or app-level messages that affect the current experien
|
|
|
10689
10750
|
|
|
10690
10751
|
| Name | Type | Default | Description |
|
|
10691
10752
|
| --- | --- | --- | --- |
|
|
10692
|
-
| tone | "neutral" \| "info" \| "success" \| "warning" \| "
|
|
10753
|
+
| tone | "neutral" \| "info" \| "success" \| "warning" \| "1" \| "destructive" | "neutral" | Neutral surface or a tone that encodes the announcement's intent (info / success / warning / the decorative 1 / destructive). |
|
|
10693
10754
|
| appearance | "soft" \| "solid" | "soft" | "soft" is a quiet tinted fill; "solid" is the full tone with its paired ink — for loud, can't-miss states (offline, chain-down, RPC errors). |
|
|
10694
10755
|
| label | ReactNode | — | A short scope label ("Preview", "Staging", a project name) in a leading chip — a Badge that reads on soft and solid fills. |
|
|
10695
10756
|
| action | ReactNode | — | A trailing action (e.g. a Button). Switches the bar to leading text + trailing action instead of centered text. |
|
|
@@ -10700,12 +10761,12 @@ Use Banner for page-level or app-level messages that affect the current experien
|
|
|
10700
10761
|
|
|
10701
10762
|
## Variants
|
|
10702
10763
|
|
|
10703
|
-
- **tone**: neutral, info, success, warning,
|
|
10764
|
+
- **tone**: neutral, info, success, warning, 1, destructive
|
|
10704
10765
|
- **appearance**: soft, solid
|
|
10705
10766
|
|
|
10706
10767
|
## Tokens used
|
|
10707
10768
|
|
|
10708
|
-
`secondary`, `secondary-foreground`, `foreground`, `background`, `tone-info`, `tone-info-foreground`, `success`, `success-foreground`, `tone-warning`, `tone-warning-foreground`, `tone-
|
|
10769
|
+
`secondary`, `secondary-foreground`, `foreground`, `background`, `tone-info`, `tone-info-foreground`, `success`, `success-foreground`, `tone-warning`, `tone-warning-foreground`, `tone-1`, `tone-1-foreground`, `destructive`, `destructive-foreground`
|
|
10709
10770
|
|
|
10710
10771
|
## Accessibility
|
|
10711
10772
|
|
|
@@ -10733,7 +10794,7 @@ import { Banner } from "@sableui/react"
|
|
|
10733
10794
|
```tsx
|
|
10734
10795
|
import { Banner } from "@sableui/react"
|
|
10735
10796
|
|
|
10736
|
-
<Banner tone="destructive" appearance="solid" live="assertive">
|
|
10797
|
+
<Banner tone="destructive" appearance="solid" live="assertive">Payments are down. Checkout and upgrades are temporarily disabled.</Banner>
|
|
10737
10798
|
```
|
|
10738
10799
|
|
|
10739
10800
|
### Success
|
|
@@ -10765,7 +10826,7 @@ import { Banner } from "@sableui/react"
|
|
|
10765
10826
|
```tsx
|
|
10766
10827
|
import { Banner, Button } from "@sableui/react"
|
|
10767
10828
|
|
|
10768
|
-
<Banner tone="warning" action={<Button variant="outline" size="sm">Reconnect</Button>}>
|
|
10829
|
+
<Banner tone="warning" action={<Button variant="outline" size="sm">Reconnect</Button>}>Calendar sync paused.</Banner>
|
|
10769
10830
|
```
|
|
10770
10831
|
|
|
10771
10832
|
### App-shell outage
|
|
@@ -10822,13 +10883,13 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
10822
10883
|
| stacked | boolean \| "expand" | false | Stack the series in one bar, or `"expand"` to show each category as 100%. Unstacked series sit side by side. |
|
|
10823
10884
|
| labels | boolean \| "inside" | false | Print each bar's value at its end, or (`"inside"`, horizontal) the category name inside the bar as well. |
|
|
10824
10885
|
| colorBy | "series" \| "index" | "series" | Colour by series, or give each category its own tone (one series). |
|
|
10825
|
-
| negativeTone | "
|
|
10886
|
+
| negativeTone | "1" \| "2" \| "3" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | — | Colour negative values with this tone. |
|
|
10826
10887
|
| radius | number | 4 | Corner radius of each bar's far end, in px. Stacks round only the outer segment. |
|
|
10827
10888
|
| barWidth | number | 24 | Maximum thickness of a bar, in px: the band's leftover is air, not a fatter bar. Raise it for a bolder presentation look. |
|
|
10828
10889
|
|
|
10829
10890
|
## Tokens used
|
|
10830
10891
|
|
|
10831
|
-
`tone-
|
|
10892
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
10832
10893
|
|
|
10833
10894
|
## Motion
|
|
10834
10895
|
|
|
@@ -11060,7 +11121,7 @@ Use Breadcrumbs to show the user's location inside a hierarchy and provide light
|
|
|
11060
11121
|
```tsx
|
|
11061
11122
|
import { BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, Breadcrumbs, BreadcrumbSeparator } from "@sableui/react"
|
|
11062
11123
|
|
|
11063
|
-
<Breadcrumbs><BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbLink href="/library">Library</BreadcrumbLink></BreadcrumbItem><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbPage>
|
|
11124
|
+
<Breadcrumbs><BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbLink href="/library">Library</BreadcrumbLink></BreadcrumbItem><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbPage>Reports</BreadcrumbPage></BreadcrumbItem></Breadcrumbs>
|
|
11064
11125
|
```
|
|
11065
11126
|
|
|
11066
11127
|
### Custom separator
|
|
@@ -11355,7 +11416,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
11355
11416
|
```tsx
|
|
11356
11417
|
import { Button } from "@sableui/react"
|
|
11357
11418
|
|
|
11358
|
-
<Button>
|
|
11419
|
+
<Button>Save changes</Button>
|
|
11359
11420
|
```
|
|
11360
11421
|
|
|
11361
11422
|
### Secondary + ghost
|
|
@@ -11412,7 +11473,7 @@ import { Button, Row } from "@sableui/react"
|
|
|
11412
11473
|
```tsx
|
|
11413
11474
|
import { Button, Row } from "@sableui/react"
|
|
11414
11475
|
|
|
11415
|
-
<Row gap={1}><Button variant="secondary">Cancel</Button><Button variant="destructive">Delete
|
|
11476
|
+
<Row gap={1}><Button variant="secondary">Cancel</Button><Button variant="destructive">Delete project</Button></Row>
|
|
11416
11477
|
```
|
|
11417
11478
|
|
|
11418
11479
|
### Leading icon
|
|
@@ -11495,7 +11556,7 @@ import { Button } from "@sableui/react"
|
|
|
11495
11556
|
```tsx
|
|
11496
11557
|
import { Banner, Button } from "@sableui/react"
|
|
11497
11558
|
|
|
11498
|
-
<Banner tone="warning" appearance="solid" action={<Button variant="on-color" size="sm">Reconnect</Button>}>
|
|
11559
|
+
<Banner tone="warning" appearance="solid" action={<Button variant="on-color" size="sm">Reconnect</Button>}>Calendar sync paused.</Banner>
|
|
11499
11560
|
```
|
|
11500
11561
|
|
|
11501
11562
|
---
|
|
@@ -11663,7 +11724,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
|
|
|
11663
11724
|
```tsx
|
|
11664
11725
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sableui/react"
|
|
11665
11726
|
|
|
11666
|
-
<Card><CardHeader><CardTitle>
|
|
11727
|
+
<Card><CardHeader><CardTitle>Studio plan</CardTitle><CardDescription>Up to 10 seats.</CardDescription></CardHeader><CardContent><p className="text-body-md">Every seat gets unlimited projects and shared reports.</p></CardContent></Card>
|
|
11667
11728
|
```
|
|
11668
11729
|
|
|
11669
11730
|
### With a header action
|
|
@@ -11671,7 +11732,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sabl
|
|
|
11671
11732
|
```tsx
|
|
11672
11733
|
import { Button, Card, CardAction, CardContent, CardHeader, CardTitle, Icon } from "@sableui/react"
|
|
11673
11734
|
|
|
11674
|
-
<Card><CardHeader><CardTitle>
|
|
11735
|
+
<Card><CardHeader><CardTitle>Seats</CardTitle><CardAction><Button variant="ghost" size="icon-sm" aria-label="More"><Icon name="ellipsis" /></Button></CardAction></CardHeader><CardContent><p className="text-body-md text-muted-foreground">24 of 100 seats in use.</p></CardContent></Card>
|
|
11675
11736
|
```
|
|
11676
11737
|
|
|
11677
11738
|
### Compact section panel
|
|
@@ -11687,7 +11748,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@sableui/react"
|
|
|
11687
11748
|
```tsx
|
|
11688
11749
|
import { Badge, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@sableui/react"
|
|
11689
11750
|
|
|
11690
|
-
<Card><CardHeader><CardTitle>Midnight Archive</CardTitle><CardDescription>
|
|
11751
|
+
<Card><CardHeader><CardTitle>Midnight Archive</CardTitle><CardDescription>Template · 24 pages</CardDescription><CardAction><Badge tone="1">New</Badge></CardAction></CardHeader><CardContent><p className="text-body-sm text-muted-foreground">A project template with a brief, a timeline and a review board.</p></CardContent><CardFooter><Button size="sm">Use template</Button><Button size="sm" variant="ghost">Preview</Button></CardFooter></Card>
|
|
11691
11752
|
```
|
|
11692
11753
|
|
|
11693
11754
|
### Whole card as a link
|
|
@@ -11703,7 +11764,7 @@ import { Card, CardContent, CardDescription, CardTitle, Icon } from "@sableui/re
|
|
|
11703
11764
|
```tsx
|
|
11704
11765
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sableui/react"
|
|
11705
11766
|
|
|
11706
|
-
<Card density="compact"><CardHeader><CardTitle>Revenue</CardTitle><CardDescription>Last 30 days</CardDescription></CardHeader><CardContent><p className="text-heading-3"
|
|
11767
|
+
<Card density="compact"><CardHeader><CardTitle>Revenue</CardTitle><CardDescription>Last 30 days</CardDescription></CardHeader><CardContent><p className="text-heading-3">$12,840</p><p className="text-body-sm text-success">+12.8% from previous period</p></CardContent></Card>
|
|
11707
11768
|
```
|
|
11708
11769
|
|
|
11709
11770
|
### List card
|
|
@@ -12071,17 +12132,17 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
|
|
|
12071
12132
|
| title | ReactNode | — | The card's primary label (text-title-sm). |
|
|
12072
12133
|
| description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
|
|
12073
12134
|
| icon | ReactNode | — | Optional glyph (a solid <Icon>) in the tone colour. Its presence switches to the two-dimensional card RadioCard uses — icon top-left, checkbox top-right, copy below — so the two cards match in one form. |
|
|
12074
|
-
| tone | "neutral" \| "
|
|
12135
|
+
| tone | "neutral" \| "3" \| "2" \| "1" \| "info" \| "warning" | "neutral" | Semantic colour the checked state carries across the border, checkbox box, and icon. |
|
|
12075
12136
|
| value | string | — | Names the card inside a group-owned CheckboxGroup: the card is checked when the group's value array includes it. Also the value submitted with a form. Inside a CheckboxGroup, `name` stands in for `value` when `value` is omitted. |
|
|
12076
12137
|
| checked / onCheckedChange / defaultChecked / disabled | Base UI Checkbox.Root props | — | Standard checkbox state. Without a group-owned value, each card is independent — drive checked/onCheckedChange per item for multi-select. |
|
|
12077
12138
|
|
|
12078
12139
|
## Variants
|
|
12079
12140
|
|
|
12080
|
-
- **tone**: neutral,
|
|
12141
|
+
- **tone**: neutral, 3, 2, 1, info, warning
|
|
12081
12142
|
|
|
12082
12143
|
## Tokens used
|
|
12083
12144
|
|
|
12084
|
-
`input`, `card`, `ring`, `primary`, `primary-foreground`, `accent`, `foreground`, `muted-foreground`, `tone-
|
|
12145
|
+
`input`, `card`, `ring`, `primary`, `primary-foreground`, `accent`, `foreground`, `muted-foreground`, `tone-3`, `tone-3-foreground`, `tone-2`, `tone-2-foreground`, `tone-1`, `tone-1-foreground`, `tone-info`, `tone-info-foreground`, `tone-warning`, `tone-warning-foreground`
|
|
12085
12146
|
|
|
12086
12147
|
## Accessibility
|
|
12087
12148
|
|
|
@@ -12117,7 +12178,7 @@ import { Checkbox, CheckboxCard, CheckboxGroup, Field } from "@sableui/react"
|
|
|
12117
12178
|
```tsx
|
|
12118
12179
|
import { CheckboxCard } from "@sableui/react"
|
|
12119
12180
|
|
|
12120
|
-
<CheckboxCard title="Email me a receipt" description="Sent when the
|
|
12181
|
+
<CheckboxCard title="Email me a receipt" description="Sent when the payment settles" checked={receipt} onCheckedChange={setReceipt} />
|
|
12121
12182
|
```
|
|
12122
12183
|
|
|
12123
12184
|
### Toned with description
|
|
@@ -12125,7 +12186,7 @@ import { CheckboxCard } from "@sableui/react"
|
|
|
12125
12186
|
```tsx
|
|
12126
12187
|
import { CheckboxCard } from "@sableui/react"
|
|
12127
12188
|
|
|
12128
|
-
<CheckboxCard tone="
|
|
12189
|
+
<CheckboxCard tone="1" title="Reports" description="Weekly usage reports by email" defaultChecked />
|
|
12129
12190
|
```
|
|
12130
12191
|
|
|
12131
12192
|
---
|
|
@@ -12211,7 +12272,7 @@ Use CodeBlock for multiline code samples that users may read, inspect, or copy.
|
|
|
12211
12272
|
|
|
12212
12273
|
- Do: Give the snippet a filename, or a sentence of surrounding copy, when it helps readers and agents place it.
|
|
12213
12274
|
- Do: Keep examples complete enough to teach the intended pattern.
|
|
12214
|
-
- Do: Wire highlighting once with CodeHighlightProvider and map token colors to semantic tone tokens (keywords → tone-
|
|
12275
|
+
- Do: Wire highlighting once with CodeHighlightProvider and map token colors to semantic tone tokens (keywords → tone-1, strings → success, comments → muted-foreground), so code follows the theme instead of a fixed palette.
|
|
12215
12276
|
- Don't: Do not put short inline identifiers in a CodeBlock; use Code for those.
|
|
12216
12277
|
- Don't: Do not paste code with the indentation the surrounding file had. A block indented to sit inside your JSX shows the reader leading whitespace that is not part of the example.
|
|
12217
12278
|
|
|
@@ -12704,7 +12765,7 @@ import { CommandMenu, CommandMenuGroup, CommandMenuItem } from "@sableui/react"
|
|
|
12704
12765
|
```tsx
|
|
12705
12766
|
import { CommandMenu, CommandMenuGroup, CommandMenuItem } from "@sableui/react"
|
|
12706
12767
|
|
|
12707
|
-
<CommandMenu open={open} onOpenChange={setOpen}><CommandMenuGroup label="Create"><CommandMenuItem icon="plus" onSelect={() => createPost()}>New post</CommandMenuItem><CommandMenuItem icon="sparkles" onSelect={() =>
|
|
12768
|
+
<CommandMenu open={open} onOpenChange={setOpen}><CommandMenuGroup label="Create"><CommandMenuItem icon="plus" onSelect={() => createPost()}>New post</CommandMenuItem><CommandMenuItem icon="sparkles" onSelect={() => createProject()}>New project</CommandMenuItem></CommandMenuGroup><CommandMenuGroup label="Navigation"><CommandMenuItem icon="home" onSelect={() => navigate("/")}>Home</CommandMenuItem><CommandMenuItem icon="gear" onSelect={() => navigate("/settings")}>Settings</CommandMenuItem></CommandMenuGroup></CommandMenu>
|
|
12708
12769
|
```
|
|
12709
12770
|
|
|
12710
12771
|
### Consumer-owned ranking
|
|
@@ -12755,7 +12816,7 @@ useCommandMenuShortcut(() => setOpen((value) => !value))
|
|
|
12755
12816
|
|
|
12756
12817
|
**Status:** stable
|
|
12757
12818
|
|
|
12758
|
-
Hover/focus-triggered rich preview card over Base UI PreviewCard. Reveals contextual content (a profile,
|
|
12819
|
+
Hover/focus-triggered rich preview card over Base UI PreviewCard. Reveals contextual content (a profile, a project summary) from an inline anchor; 20rem-wide surface with delayed open and collision-aware positioning.
|
|
12759
12820
|
|
|
12760
12821
|
**Keywords:** context card, rich tooltip, hover card, preview
|
|
12761
12822
|
|
|
@@ -12814,7 +12875,7 @@ Use ContextCard to show richer contextual content near a trigger without taking
|
|
|
12814
12875
|
```tsx
|
|
12815
12876
|
import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger, Entity } from "@sableui/react"
|
|
12816
12877
|
|
|
12817
|
-
<ContextCard><ContextCardTrigger render={<a href='/u/ada' className='text-title-sm underline-offset-2 hover:underline'>@ada</a>} /><ContextCardContent><Entity className='[--entity-px:0] [--entity-py:0]' leading={<Avatar fallback='AL' size='lg' />} title='Ada Lovelace' subtitle='@ada' /><p className='mt-3 text-body-sm text-muted-foreground'>
|
|
12878
|
+
<ContextCard><ContextCardTrigger render={<a href='/u/ada' className='text-title-sm underline-offset-2 hover:underline'>@ada</a>} /><ContextCardContent><Entity className='[--entity-px:0] [--entity-py:0]' leading={<Avatar fallback='AL' size='lg' />} title='Ada Lovelace' subtitle='@ada' /><p className='mt-3 text-body-sm text-muted-foreground'>Product designer. Building tools for small teams.</p></ContextCardContent></ContextCard>
|
|
12818
12879
|
```
|
|
12819
12880
|
|
|
12820
12881
|
### With stat row
|
|
@@ -13300,7 +13361,7 @@ import { DestructiveActionModal } from "@sableui/react"
|
|
|
13300
13361
|
```tsx
|
|
13301
13362
|
import { DestructiveActionModal, Note } from "@sableui/react"
|
|
13302
13363
|
|
|
13303
|
-
<DestructiveActionModal open={open} onOpenChange={setOpen} title='Delete this post?' confirmText='gold rush' confirmLabel='Delete forever' onConfirm={handleDelete}><Note tone='warning'>
|
|
13364
|
+
<DestructiveActionModal open={open} onOpenChange={setOpen} title='Delete this post?' confirmText='gold rush' confirmLabel='Delete forever' onConfirm={handleDelete}><Note tone='warning'>People you shared this post with lose access to it.</Note></DestructiveActionModal>
|
|
13304
13365
|
```
|
|
13305
13366
|
|
|
13306
13367
|
---
|
|
@@ -13414,7 +13475,7 @@ import { Button, Carousel, Dialog, DialogContent, DialogTrigger } from "@sableui
|
|
|
13414
13475
|
```tsx
|
|
13415
13476
|
import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@sableui/react"
|
|
13416
13477
|
|
|
13417
|
-
<Dialog><DialogTrigger render={<Button>Open</Button>} /><DialogContent><DialogHeader><DialogTitle>
|
|
13478
|
+
<Dialog><DialogTrigger render={<Button>Open</Button>} /><DialogContent><DialogHeader><DialogTitle>Plan details</DialogTitle><DialogDescription>Up to 10 seats, billed monthly per seat.</DialogDescription></DialogHeader><DialogFooter><DialogClose render={<Button variant="secondary">Close</Button>} /><DialogClose render={<Button>Upgrade</Button>} /></DialogFooter></DialogContent></Dialog>
|
|
13418
13479
|
```
|
|
13419
13480
|
|
|
13420
13481
|
### Settings form
|
|
@@ -13430,7 +13491,7 @@ import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFo
|
|
|
13430
13491
|
```tsx
|
|
13431
13492
|
import { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button } from "@sableui/react"
|
|
13432
13493
|
|
|
13433
|
-
<AlertDialog><AlertDialogTrigger render={<Button variant="destructive">Delete
|
|
13494
|
+
<AlertDialog><AlertDialogTrigger render={<Button variant="destructive">Delete project</Button>} /><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Delete project?</AlertDialogTitle><AlertDialogDescription>This removes the project and cannot be undone.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogClose render={<Button variant="secondary">Cancel</Button>} /><Button variant="destructive" onClick={deleteProject}>Delete project</Button></AlertDialogFooter></AlertDialogContent></AlertDialog>
|
|
13434
13495
|
```
|
|
13435
13496
|
|
|
13436
13497
|
---
|
|
@@ -13478,7 +13539,7 @@ Use DisclosurePicker for one-of-many choices in a docked panel or inspector wher
|
|
|
13478
13539
|
| DisclosurePickerTrigger | { label?: ReactNode; media?: ReactNode; children?: ReactNode } + Base UI Collapsible.Trigger props | The row. `label` is a muted leading caption on a fixed column so stacked rows align; `media` a leading visual (a ThemePreview tile); children the value, truncated on one line. ArrowDown opens it, and steps into the list once open. |
|
|
13479
13540
|
| DisclosurePickerPanel | { search?: { value: string; onValueChange(value: string): void; placeholder?: string; label?: string } } + Collapsible.Panel props | Opens beneath the row. `search` opts in to a SearchInput above the list (off by default); it is controlled because the consumer filters, and it is cleared when the row closes. Escape clears a query first, then closes; ArrowDown in the field steps into the list. |
|
|
13480
13541
|
| DisclosurePickerList | { value: string; onValueChange(value: string): void; aria-label?: string } + div props | A listbox. ArrowUp/ArrowDown, Home/End and type-ahead move focus without applying anything (ArrowUp from the first option returns to the search field); Enter, Space or a click picks the focused option — onValueChange fires once, only when it differs from `value` — and closes the row. Escape closes without a pick. |
|
|
13481
|
-
| DisclosurePickerOption | { value: string; children?: ReactNode; media?: ReactNode; note?: ReactNode; disabled?: boolean; textValue?: string } | One option row. `media` is a leading visual (a ThemePreview tile); `note` a muted trailing annotation — options without one show a check when chosen. `disabled` skips it for the keyboard and refuses the pick; `textValue` is what type-ahead matches when the name is not plain text. |
|
|
13542
|
+
| DisclosurePickerOption | { value: string; children?: ReactNode; media?: ReactNode; note?: ReactNode; disabled?: boolean; textValue?: string; onRemove?: () => void; removeLabel?: string } | One option row. `media` is a leading visual (a ThemePreview tile); `note` a muted trailing annotation — options without one show a check when chosen. `disabled` skips it for the keyboard and refuses the pick; `textValue` is what type-ahead matches when the name is not plain text. `onRemove` makes the option removable — for entries the person added (an imported preset, a saved font): a trash glyph at the row's end on hover or focus (always on touch), and Delete or Backspace on the focused row; removing never picks, and focus moves to a neighbour first. `removeLabel` is the glyph's tooltip. |
|
|
13482
13543
|
| DisclosurePickerEmpty | { children?: ReactNode } | The no-match line, rendered in place of the list. |
|
|
13483
13544
|
| useDisclosurePicker | () => { open: boolean; close(): void; size: "md" \| "xs" } | For a list the picker does not own (a Tree) that should close the row when something is picked. |
|
|
13484
13545
|
|
|
@@ -13510,6 +13571,7 @@ Use DisclosurePicker for one-of-many choices in a docked panel or inspector wher
|
|
|
13510
13571
|
- Why a listbox and not a radio group: a radio group's checked state follows focus, so every arrow press would be a pick — wrong for a choice that opens a confirm, loads a font or resets settings. Base UI ships no standalone listbox (Select is popup-only, Combobox's inline list is focus-in-the-input, Composite is internal), so the keyboard model lives here.
|
|
13511
13572
|
- Opening from the keyboard (Enter, Space, ArrowDown) moves focus into the panel: the search field when there is one, otherwise the chosen option (or the first). A pointer open focuses the search field on a fine pointer only, so touch does not raise the on-screen keyboard.
|
|
13512
13573
|
- Escape closes the row without changing the value (clearing a query first) and returns focus to the trigger; a pick does the same. Focus is not pulled back when it has already moved on, e.g. to a confirm dialog the pick opened.
|
|
13574
|
+
- A removable option (onRemove) announces aria-keyshortcuts="Delete"; Delete or Backspace removes it and focus moves to the next option (or the previous). The trash glyph is a pointer target only (aria-hidden, not focusable): an option's children are presentational, so a nested button would be unreachable by assistive tech anyway.
|
|
13513
13575
|
- The panel animates with motion-disclosure, which is static under prefers-reduced-motion or MotionProvider off; the scroll that keeps the row in view is instant, never smooth.
|
|
13514
13576
|
|
|
13515
13577
|
## Related
|
|
@@ -13620,7 +13682,7 @@ Use EmptyState when a content area has no items, no search results, or no first-
|
|
|
13620
13682
|
|
|
13621
13683
|
### Best practices
|
|
13622
13684
|
|
|
13623
|
-
- Do: Explain what is empty in plain language, such as "No posts yet" or "No matching
|
|
13685
|
+
- Do: Explain what is empty in plain language, such as "No posts yet" or "No matching customers".
|
|
13624
13686
|
- Do: Offer one next step when the user can recover, create, clear filters, or start setup.
|
|
13625
13687
|
- Do: Keep the icon decorative and make the title carry the meaning.
|
|
13626
13688
|
- Do: For a search or filter result, name the query back to the user and offer the way out, such as "No results for 'quarterly'" with a Clear filters action. An empty result that does not say what was searched leaves the user unsure whether the query or the data was wrong.
|
|
@@ -13693,7 +13755,7 @@ import { Button, EmptyState, Icon } from "@sableui/react"
|
|
|
13693
13755
|
```tsx
|
|
13694
13756
|
import { Button, EmptyState, Icon, Row } from "@sableui/react"
|
|
13695
13757
|
|
|
13696
|
-
<EmptyState surface="card" icon={<Icon name="sparkles" />} title="Start your
|
|
13758
|
+
<EmptyState surface="card" icon={<Icon name="sparkles" />} title="Start your first project" description="Create a project, import one from a file, or invite collaborators." action={<Row gap={1}><Button>Create project</Button><Button variant="ghost">Import</Button></Row>} />
|
|
13697
13759
|
```
|
|
13698
13760
|
|
|
13699
13761
|
### Page-level error
|
|
@@ -13709,7 +13771,7 @@ import { Button, EmptyState } from "@sableui/react"
|
|
|
13709
13771
|
```tsx
|
|
13710
13772
|
import { Button, EmptyState } from "@sableui/react"
|
|
13711
13773
|
|
|
13712
|
-
<EmptyState tone="destructive" surface="card" title="Could not load this
|
|
13774
|
+
<EmptyState tone="destructive" surface="card" title="Could not load this project" description="Check your connection and try again." action={<Button variant="secondary">Retry</Button>} />
|
|
13713
13775
|
```
|
|
13714
13776
|
|
|
13715
13777
|
### Empty table body
|
|
@@ -13717,7 +13779,7 @@ import { Button, EmptyState } from "@sableui/react"
|
|
|
13717
13779
|
```tsx
|
|
13718
13780
|
import { DataTable, EmptyState } from "@sableui/react"
|
|
13719
13781
|
|
|
13720
|
-
<DataTable data={[]} columns={[{ key: "name", header: "
|
|
13782
|
+
<DataTable data={[]} columns={[{ key: "name", header: "Customer" }, { key: "status", header: "Status" }]} empty={<EmptyState title="No customers yet" description="Customers appear here after the first order." />} />
|
|
13721
13783
|
```
|
|
13722
13784
|
|
|
13723
13785
|
---
|
|
@@ -14455,7 +14517,7 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
|
|
|
14455
14517
|
| Name | Type | Default | Description |
|
|
14456
14518
|
| --- | --- | --- | --- |
|
|
14457
14519
|
| variant | "mesh" \| "grain" | "mesh" | mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture. |
|
|
14458
|
-
| tones | ("info" \| "
|
|
14520
|
+
| tones | ("info" \| "3" \| "2" \| "1" \| "warning")[] | — | Paint these `--tone-*` colours instead of the preset's art palette. Leave unset to follow the preset's art direction. |
|
|
14459
14521
|
| seed | string \| number | — | Fixes the composition: the same seed paints the same art in every theme. Pass a stable id. |
|
|
14460
14522
|
| children | ReactNode | — | Rendered above the art — initials, a glyph, a short label. |
|
|
14461
14523
|
| className | string | — | Size and shape: aspect, height, rounding. |
|
|
@@ -14466,7 +14528,7 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
|
|
|
14466
14528
|
|
|
14467
14529
|
## Tokens used
|
|
14468
14530
|
|
|
14469
|
-
`art-1`, `art-2`, `art-3`, `art-4`, `art-5`, `art-6`, `art-ground`, `art-warp`, `art-softness`, `art-grain`, `primary`, `tone-info`, `tone-
|
|
14531
|
+
`art-1`, `art-2`, `art-3`, `art-4`, `art-5`, `art-6`, `art-ground`, `art-warp`, `art-softness`, `art-grain`, `primary`, `tone-info`, `tone-3`, `tone-2`, `tone-1`, `tone-warning`
|
|
14470
14532
|
|
|
14471
14533
|
## Motion
|
|
14472
14534
|
|
|
@@ -14499,7 +14561,7 @@ import { GradientArt } from "@sableui/react"
|
|
|
14499
14561
|
```tsx
|
|
14500
14562
|
import { GradientArt } from "@sableui/react"
|
|
14501
14563
|
|
|
14502
|
-
<GradientArt variant="grain" seed="juno" tones={["info", "
|
|
14564
|
+
<GradientArt variant="grain" seed="juno" tones={["info", "1"]} className="h-20 rounded-md" />
|
|
14503
14565
|
```
|
|
14504
14566
|
|
|
14505
14567
|
### Avatar with initials
|
|
@@ -14850,7 +14912,7 @@ import { Field, Input } from "@sableui/react"
|
|
|
14850
14912
|
```tsx
|
|
14851
14913
|
import { Label } from "@sableui/react"
|
|
14852
14914
|
|
|
14853
|
-
<Label htmlFor="
|
|
14915
|
+
<Label htmlFor="discount" hint="Taken off every order this code is used on.">Discount</Label>
|
|
14854
14916
|
```
|
|
14855
14917
|
|
|
14856
14918
|
---
|
|
@@ -14902,7 +14964,7 @@ Use LineChart to compare how two or more measures change over the same sequence,
|
|
|
14902
14964
|
|
|
14903
14965
|
## Tokens used
|
|
14904
14966
|
|
|
14905
|
-
`tone-
|
|
14967
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
14906
14968
|
|
|
14907
14969
|
## Motion
|
|
14908
14970
|
|
|
@@ -15809,7 +15871,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
|
|
|
15809
15871
|
| value | number | — | The current reading, between min and max. Required. |
|
|
15810
15872
|
| min | number | 0 | The bottom of the range. |
|
|
15811
15873
|
| max | number | 100 | The top of the range. |
|
|
15812
|
-
| tone | "primary" \| "
|
|
15874
|
+
| tone | "primary" \| "3" \| "success" \| "warning" \| "destructive" | "primary" | Fill colour, mapped to a semantic token. Choose it from your own thresholds. |
|
|
15813
15875
|
| shape | "bar" \| "ring" | "bar" | bar is a horizontal track; ring is a circular gauge for a small, glanceable reading — the same value, min, max and tones. |
|
|
15814
15876
|
| size | "sm" \| "md" \| "lg" | "md" | Ring diameter (40 / 64 / 96px at the default spacing; the centre value steps with it), or bar thickness on the same sm / md / lg rails as Progress. |
|
|
15815
15877
|
| showValue | boolean | — | Print the value in the ring's centre (formatted like MeterValue). |
|
|
@@ -15820,7 +15882,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
|
|
|
15820
15882
|
|
|
15821
15883
|
## Variants
|
|
15822
15884
|
|
|
15823
|
-
- **tone**: primary,
|
|
15885
|
+
- **tone**: primary, 3, success, warning, destructive
|
|
15824
15886
|
- **shape**: bar, ring
|
|
15825
15887
|
- **size**: sm, md, lg
|
|
15826
15888
|
|
|
@@ -15929,12 +15991,12 @@ Use MiddleTruncate when both the beginning and end of a long identifier must rem
|
|
|
15929
15991
|
|
|
15930
15992
|
## Examples
|
|
15931
15993
|
|
|
15932
|
-
###
|
|
15994
|
+
### API key
|
|
15933
15995
|
|
|
15934
15996
|
```tsx
|
|
15935
15997
|
import { MiddleTruncate } from "@sableui/react"
|
|
15936
15998
|
|
|
15937
|
-
<MiddleTruncate text="
|
|
15999
|
+
<MiddleTruncate text="demo_key_7f3a9c2e5b8d1f4a6c0e9b3d7a2f5c8e1b4" />
|
|
15938
16000
|
```
|
|
15939
16001
|
|
|
15940
16002
|
### Custom start/end counts
|
|
@@ -15942,7 +16004,7 @@ import { MiddleTruncate } from "@sableui/react"
|
|
|
15942
16004
|
```tsx
|
|
15943
16005
|
import { MiddleTruncate } from "@sableui/react"
|
|
15944
16006
|
|
|
15945
|
-
<MiddleTruncate text="
|
|
16007
|
+
<MiddleTruncate text="9f2c4e7a1b3d5f60c8e2a4b6d8f0e1c3a5b7d9f2" startChars={4} endChars={4} />
|
|
15946
16008
|
```
|
|
15947
16009
|
|
|
15948
16010
|
### Copyable value
|
|
@@ -15950,8 +16012,8 @@ import { MiddleTruncate } from "@sableui/react"
|
|
|
15950
16012
|
```tsx
|
|
15951
16013
|
import { Button, Icon, MiddleTruncate, Row, useCopy } from "@sableui/react"
|
|
15952
16014
|
|
|
15953
|
-
const { copy, copied } = useCopy(
|
|
15954
|
-
<Row gap={1}><MiddleTruncate text={
|
|
16015
|
+
const { copy, copied } = useCopy(commitHash)
|
|
16016
|
+
<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button></Row>
|
|
15955
16017
|
```
|
|
15956
16018
|
|
|
15957
16019
|
---
|
|
@@ -16011,7 +16073,7 @@ Use Note for helpful supporting information that should sit near the relevant co
|
|
|
16011
16073
|
```tsx
|
|
16012
16074
|
import { Note } from "@sableui/react"
|
|
16013
16075
|
|
|
16014
|
-
<Note tone="info">
|
|
16076
|
+
<Note tone="info">Reports refresh every hour.</Note>
|
|
16015
16077
|
```
|
|
16016
16078
|
|
|
16017
16079
|
### With title
|
|
@@ -16019,7 +16081,7 @@ import { Note } from "@sableui/react"
|
|
|
16019
16081
|
```tsx
|
|
16020
16082
|
import { Note } from "@sableui/react"
|
|
16021
16083
|
|
|
16022
|
-
<Note tone="warning" title="Low balance">
|
|
16084
|
+
<Note tone="warning" title="Low balance">Add credits before your next export.</Note>
|
|
16023
16085
|
```
|
|
16024
16086
|
|
|
16025
16087
|
### Warning with a remedy, inside a dialog
|
|
@@ -16035,7 +16097,7 @@ import { Note } from "@sableui/react"
|
|
|
16035
16097
|
```tsx
|
|
16036
16098
|
import { Note } from "@sableui/react"
|
|
16037
16099
|
|
|
16038
|
-
<Note tone="neutral" icon="lock">Pricing
|
|
16100
|
+
<Note tone="neutral" icon="lock">Pricing cannot be changed after an order is placed.</Note>
|
|
16039
16101
|
```
|
|
16040
16102
|
|
|
16041
16103
|
---
|
|
@@ -16519,7 +16581,7 @@ Use Pagination when a result set is split into discrete pages and the user needs
|
|
|
16519
16581
|
### Best practices
|
|
16520
16582
|
|
|
16521
16583
|
- Do: Keep page state controlled by the parent route or data layer so URL, filters, and result fetches stay in sync.
|
|
16522
|
-
- Do: Use a specific label when multiple pagers appear on a page, such as "
|
|
16584
|
+
- Do: Use a specific label when multiple pagers appear on a page, such as "Customer pages".
|
|
16523
16585
|
- Do: Reset to page 1 when filters or search terms materially change the result set.
|
|
16524
16586
|
- Don't: Use Pagination for tiny lists where all items fit comfortably on one page.
|
|
16525
16587
|
- Don't: Hide disabled previous/next buttons at the bounds; disabled controls make the current position clearer.
|
|
@@ -16585,7 +16647,7 @@ const [page, setPage] = useState(8)
|
|
|
16585
16647
|
```tsx
|
|
16586
16648
|
import { DataTable, Pagination, Stack } from "@sableui/react"
|
|
16587
16649
|
|
|
16588
|
-
<Stack gap={2}><DataTable data={rows} columns={columns} rowKey={(row) => row.id} /><Pagination label="
|
|
16650
|
+
<Stack gap={2}><DataTable data={rows} columns={columns} rowKey={(row) => row.id} /><Pagination label="Customer pages" page={page} total={12} onPageChange={setPage} /></Stack>
|
|
16589
16651
|
```
|
|
16590
16652
|
|
|
16591
16653
|
### Server query state
|
|
@@ -16895,7 +16957,7 @@ Use Phone to frame a mobile product preview when the device context is useful.
|
|
|
16895
16957
|
```tsx
|
|
16896
16958
|
import { Phone } from "@sableui/react"
|
|
16897
16959
|
|
|
16898
|
-
<Phone><div className="p-4"><h2 className="text-title-lg">Activity</h2><p className="text-body-sm text-muted-foreground">3 new
|
|
16960
|
+
<Phone><div className="p-4"><h2 className="text-title-lg">Activity</h2><p className="text-body-sm text-muted-foreground">3 new orders today.</p></div></Phone>
|
|
16899
16961
|
```
|
|
16900
16962
|
|
|
16901
16963
|
### Mockup, not a responsive preview
|
|
@@ -16965,7 +17027,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
|
|
|
16965
17027
|
|
|
16966
17028
|
## Tokens used
|
|
16967
17029
|
|
|
16968
|
-
`tone-
|
|
17030
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
16969
17031
|
|
|
16970
17032
|
## Motion
|
|
16971
17033
|
|
|
@@ -17053,6 +17115,7 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
17053
17115
|
| 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. |
|
|
17054
17116
|
| 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. |
|
|
17055
17117
|
| size | "md" \| "sm" | "md" | md sits on the density ladder's xs control height (28px); sm is 24px, for chips under or inside other content. |
|
|
17118
|
+
| tone | "neutral" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "purple" \| "pink" | "neutral" | neutral is the chip as described. A palette family colours it as a category, like the choices in a tag picker: a wash at rest, the solid family colour when selected. Painted from the theme's generated palette. |
|
|
17056
17119
|
| icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. |
|
|
17057
17120
|
|
|
17058
17121
|
## Variants
|
|
@@ -17060,10 +17123,11 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
17060
17123
|
- **appearance**: solid, outline
|
|
17061
17124
|
- **size**: md, sm
|
|
17062
17125
|
- **selected**: true, false
|
|
17126
|
+
- **tone**: neutral, red, orange, yellow, green, teal, blue, purple, pink
|
|
17063
17127
|
|
|
17064
17128
|
## Tokens used
|
|
17065
17129
|
|
|
17066
|
-
`secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `primary`, `primary-foreground`, `card`, `border`, `muted-foreground`, `foreground`
|
|
17130
|
+
`palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `primary`, `primary-foreground`, `card`, `border`, `muted-foreground`, `foreground`
|
|
17067
17131
|
|
|
17068
17132
|
## Accessibility
|
|
17069
17133
|
|
|
@@ -17076,12 +17140,20 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
17076
17140
|
|
|
17077
17141
|
## Examples
|
|
17078
17142
|
|
|
17143
|
+
### Tag picker
|
|
17144
|
+
|
|
17145
|
+
```tsx
|
|
17146
|
+
import { Pill, Row } from "@sableui/react"
|
|
17147
|
+
|
|
17148
|
+
<Row gap={1}><Pill tone="blue" selected onClick={() => {}}>Design</Pill><Pill tone="teal" onClick={() => {}}>Research</Pill><Pill tone="orange" onClick={() => {}}>Marketing</Pill></Row>
|
|
17149
|
+
```
|
|
17150
|
+
|
|
17079
17151
|
### Filter row
|
|
17080
17152
|
|
|
17081
17153
|
```tsx
|
|
17082
17154
|
import { Pill, Row } from "@sableui/react"
|
|
17083
17155
|
|
|
17084
|
-
<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>
|
|
17156
|
+
<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Projects</Pill><Pill onClick={() => {}}>Reports</Pill></Row>
|
|
17085
17157
|
```
|
|
17086
17158
|
|
|
17087
17159
|
### Outline, small (reactions)
|
|
@@ -17171,7 +17243,7 @@ Use Popover for anchored secondary content that needs more room than a tooltip b
|
|
|
17171
17243
|
```tsx
|
|
17172
17244
|
import { Button, Popover, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "@sableui/react"
|
|
17173
17245
|
|
|
17174
|
-
<Popover><PopoverTrigger render={<Button variant="secondary">Details</Button>} /><PopoverContent><PopoverTitle>
|
|
17246
|
+
<Popover><PopoverTrigger render={<Button variant="secondary">Details</Button>} /><PopoverContent><PopoverTitle>Plan details</PopoverTitle><PopoverDescription>Up to 10 seats, billed monthly per seat.</PopoverDescription></PopoverContent></Popover>
|
|
17175
17247
|
```
|
|
17176
17248
|
|
|
17177
17249
|
### With close
|
|
@@ -17179,7 +17251,7 @@ import { Button, Popover, PopoverContent, PopoverDescription, PopoverTitle, Popo
|
|
|
17179
17251
|
```tsx
|
|
17180
17252
|
import { Button, Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "@sableui/react"
|
|
17181
17253
|
|
|
17182
|
-
<Popover><PopoverTrigger render={<Button variant="secondary">Open</Button>} /><PopoverContent><PopoverTitle>Heads up</PopoverTitle><PopoverDescription>
|
|
17254
|
+
<Popover><PopoverTrigger render={<Button variant="secondary">Open</Button>} /><PopoverContent><PopoverTitle>Heads up</PopoverTitle><PopoverDescription>Upgrading starts a new billing period today.</PopoverDescription><PopoverClose render={<Button size="sm">Got it</Button>} /></PopoverContent></Popover>
|
|
17183
17255
|
```
|
|
17184
17256
|
|
|
17185
17257
|
---
|
|
@@ -17329,7 +17401,7 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
|
|
|
17329
17401
|
|
|
17330
17402
|
## Tokens used
|
|
17331
17403
|
|
|
17332
|
-
`tone-
|
|
17404
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
17333
17405
|
|
|
17334
17406
|
## Motion
|
|
17335
17407
|
|
|
@@ -17440,7 +17512,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
|
|
|
17440
17512
|
|
|
17441
17513
|
## Tokens used
|
|
17442
17514
|
|
|
17443
|
-
`tone-
|
|
17515
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
17444
17516
|
|
|
17445
17517
|
## Motion
|
|
17446
17518
|
|
|
@@ -17564,7 +17636,7 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
|
|
|
17564
17636
|
<FieldsetContent>
|
|
17565
17637
|
<RadioGroup defaultValue="standard">
|
|
17566
17638
|
<Field label="Standard" orientation="horizontal"><Radio value="standard" /></Field>
|
|
17567
|
-
<Field label="
|
|
17639
|
+
<Field label="Poll" orientation="horizontal"><Radio value="poll" /></Field>
|
|
17568
17640
|
</RadioGroup>
|
|
17569
17641
|
</FieldsetContent>
|
|
17570
17642
|
</Fieldset>
|
|
@@ -17580,7 +17652,7 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
|
|
|
17580
17652
|
<FieldsetContent>
|
|
17581
17653
|
<RadioGroup defaultValue="standard">
|
|
17582
17654
|
<Field label="Standard" orientation="horizontal"><Radio value="standard" /></Field>
|
|
17583
|
-
<Field label="
|
|
17655
|
+
<Field label="Poll" orientation="horizontal" disabled><Radio value="poll" /></Field>
|
|
17584
17656
|
</RadioGroup>
|
|
17585
17657
|
</FieldsetContent>
|
|
17586
17658
|
</Fieldset>
|
|
@@ -17629,18 +17701,18 @@ Use RadioCard for visual choice cards when users compare a small set of named op
|
|
|
17629
17701
|
| description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
|
|
17630
17702
|
| trailing | ReactNode | — | End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row. |
|
|
17631
17703
|
| 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. |
|
|
17632
|
-
| tone | "neutral" \| "
|
|
17704
|
+
| tone | "neutral" \| "3" \| "2" \| "1" \| "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. |
|
|
17633
17705
|
| disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
|
|
17634
17706
|
| titleClassName | string | — | Merged onto the title span (default text-title-sm) — e.g. to tighten copy in a two-column layout. |
|
|
17635
17707
|
| descriptionClassName | string | — | Merged onto the description span (default text-body-sm text-muted-foreground). |
|
|
17636
17708
|
|
|
17637
17709
|
## Variants
|
|
17638
17710
|
|
|
17639
|
-
- **tone**: neutral,
|
|
17711
|
+
- **tone**: neutral, 3, 2, 1, info, warning
|
|
17640
17712
|
|
|
17641
17713
|
## Tokens used
|
|
17642
17714
|
|
|
17643
|
-
`input`, `card`, `foreground`, `primary`, `muted`, `ring`, `muted-foreground`, `tone-
|
|
17715
|
+
`input`, `card`, `foreground`, `primary`, `muted`, `ring`, `muted-foreground`, `tone-3`, `tone-2`, `tone-1`, `tone-info`, `tone-warning`
|
|
17644
17716
|
|
|
17645
17717
|
## Accessibility
|
|
17646
17718
|
|
|
@@ -17661,7 +17733,7 @@ Use RadioCard for visual choice cards when users compare a small set of named op
|
|
|
17661
17733
|
```tsx
|
|
17662
17734
|
import { Icon, RadioCard, RadioGroup } from "@sableui/react"
|
|
17663
17735
|
|
|
17664
|
-
<RadioGroup defaultValue="
|
|
17736
|
+
<RadioGroup defaultValue="starter" className="grid sm:grid-cols-3"><RadioCard value="starter" tone="3" icon={<Icon name="plus" />} title="Starter" description="Free for one person" /><RadioCard value="studio" tone="2" icon={<Icon name="gem" />} title="Studio" description="For small teams" /><RadioCard value="business" tone="1" icon={<Icon name="image-stack" />} title="Business" description="SSO and audit log" /></RadioGroup>
|
|
17665
17737
|
```
|
|
17666
17738
|
|
|
17667
17739
|
### Plan picker (plain)
|
|
@@ -17807,7 +17879,7 @@ Use Scroller to create a contained overflow region for content that naturally ex
|
|
|
17807
17879
|
```tsx
|
|
17808
17880
|
import { Scroller } from "@sableui/react"
|
|
17809
17881
|
|
|
17810
|
-
<Scroller label="
|
|
17882
|
+
<Scroller label="Projects" className="h-64"><ul className="space-y-2 p-4">{items.map((item) => (<li key={item.id} className="text-body-sm">{item.title}</li>))}</ul></Scroller>
|
|
17811
17883
|
```
|
|
17812
17884
|
|
|
17813
17885
|
### Capped, scrolls past the cap
|
|
@@ -18010,7 +18082,7 @@ import { SearchInput } from "@sableui/react"
|
|
|
18010
18082
|
```tsx
|
|
18011
18083
|
import { Field, SearchInput } from "@sableui/react"
|
|
18012
18084
|
|
|
18013
|
-
<Field label="Search posts"><SearchInput defaultValue="
|
|
18085
|
+
<Field label="Search posts"><SearchInput defaultValue="roadmap" className="max-w-80" /></Field>
|
|
18014
18086
|
```
|
|
18015
18087
|
|
|
18016
18088
|
### Disabled
|
|
@@ -18208,11 +18280,11 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
|
|
|
18208
18280
|
## Variants
|
|
18209
18281
|
|
|
18210
18282
|
- **size**: sm, md, lg
|
|
18211
|
-
- **tone**:
|
|
18283
|
+
- **tone**: 1, 2, 3, info, primary, neutral, success, warning, destructive
|
|
18212
18284
|
|
|
18213
18285
|
## Tokens used
|
|
18214
18286
|
|
|
18215
|
-
`tone-
|
|
18287
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`
|
|
18216
18288
|
|
|
18217
18289
|
## Motion
|
|
18218
18290
|
|
|
@@ -18223,7 +18295,7 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
|
|
|
18223
18295
|
|
|
18224
18296
|
## Accessibility
|
|
18225
18297
|
|
|
18226
|
-
- Rendered role=img with an aria-label built from `label` and each segment's share ("Revenue by
|
|
18298
|
+
- Rendered role=img with an aria-label built from `label` and each segment's share ("Revenue by channel: Direct 42%, Partners 27%"), so the whole breakdown is read in one stop.
|
|
18227
18299
|
- Colour alone does not identify a segment: always show a legend with each segment's label beside the bar.
|
|
18228
18300
|
|
|
18229
18301
|
## Related
|
|
@@ -18239,13 +18311,13 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
|
|
|
18239
18311
|
import { SegmentedBar, Stack, StatusDot } from "@sableui/react"
|
|
18240
18312
|
|
|
18241
18313
|
const sales = [
|
|
18242
|
-
{ label: "
|
|
18243
|
-
{ label: "
|
|
18244
|
-
{ label: "
|
|
18314
|
+
{ label: "Direct", value: 42, tone: "1" },
|
|
18315
|
+
{ label: "Partners", value: 27, tone: "2" },
|
|
18316
|
+
{ label: "Referrals", value: 19, tone: "3" },
|
|
18245
18317
|
] as const
|
|
18246
18318
|
|
|
18247
18319
|
<Stack gap={3}>
|
|
18248
|
-
<SegmentedBar label="Revenue by
|
|
18320
|
+
<SegmentedBar label="Revenue by channel" segments={[...sales]} size="lg" />
|
|
18249
18321
|
<ul className="flex flex-col gap-1.5">
|
|
18250
18322
|
{sales.map((s) => (
|
|
18251
18323
|
<li key={s.label} className="flex items-center gap-2 text-body-sm">
|
|
@@ -18371,7 +18443,7 @@ import { SelectContent, SelectGroup, SelectGroupLabel, SelectItem } from "@sable
|
|
|
18371
18443
|
```tsx
|
|
18372
18444
|
import { Field, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
|
|
18373
18445
|
|
|
18374
|
-
<Field label="Default visibility" description="Controls how new
|
|
18446
|
+
<Field label="Default visibility" description="Controls how new projects appear."><Select defaultValue="public"><SelectTrigger placeholder="Choose visibility" /><SelectContent><SelectItem value="public">Public</SelectItem><SelectItem value="private">Private</SelectItem><SelectItem value="unlisted">Unlisted</SelectItem></SelectContent></Select></Field>
|
|
18375
18447
|
```
|
|
18376
18448
|
|
|
18377
18449
|
### Rich item labels
|
|
@@ -18379,7 +18451,7 @@ import { Field, Select, SelectContent, SelectItem, SelectTrigger } from "@sableu
|
|
|
18379
18451
|
```tsx
|
|
18380
18452
|
import { Badge, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
|
|
18381
18453
|
|
|
18382
|
-
<Select defaultValue="
|
|
18454
|
+
<Select defaultValue="business" items={[{ value: "business", label: "Business" }, { value: "studio", label: "Studio" }]}><SelectTrigger /><SelectContent><SelectItem value="business" label="Business"><span className="flex items-center gap-2"><Badge tone="1">Business</Badge><span>Business plan</span></span></SelectItem><SelectItem value="studio" label="Studio"><span className="flex items-center gap-2"><Badge tone="2">Studio</Badge><span>Studio</span></span></SelectItem></SelectContent></Select>
|
|
18383
18455
|
```
|
|
18384
18456
|
|
|
18385
18457
|
---
|
|
@@ -18704,7 +18776,7 @@ import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, Shee
|
|
|
18704
18776
|
```tsx
|
|
18705
18777
|
import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@sableui/react"
|
|
18706
18778
|
|
|
18707
|
-
<Sheet><SheetTrigger render={<Button variant="secondary">Details</Button>} /><SheetContent side="bottom"><SheetHeader><SheetTitle>
|
|
18779
|
+
<Sheet><SheetTrigger render={<Button variant="secondary">Details</Button>} /><SheetContent side="bottom"><SheetHeader><SheetTitle>Order details</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant="secondary">Close</Button>} /></SheetFooter></SheetContent></Sheet>
|
|
18708
18780
|
```
|
|
18709
18781
|
|
|
18710
18782
|
---
|
|
@@ -18840,7 +18912,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
18840
18912
|
import { Icon, NotificationBadge, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
|
|
18841
18913
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
18842
18914
|
|
|
18843
|
-
<Sidebar><SidebarHeader><SidebarBrand mark={<Icon name="music" variant="solid" />} label="Undertow" render={<Link to="/" />} /></SidebarHeader><SidebarNav><SidebarItem icon={<Icon name="bell" />} label="Notifications" badge={<NotificationBadge count={3} />} render={<Link to="/notifications" />} /></SidebarNav><SidebarFooter>{
|
|
18915
|
+
<Sidebar><SidebarHeader><SidebarBrand mark={<Icon name="music" variant="solid" />} label="Undertow" render={<Link to="/" />} /></SidebarHeader><SidebarNav><SidebarItem icon={<Icon name="bell" />} label="Notifications" badge={<NotificationBadge count={3} />} render={<Link to="/notifications" />} /></SidebarNav><SidebarFooter>{account}</SidebarFooter></Sidebar>
|
|
18844
18916
|
```
|
|
18845
18917
|
|
|
18846
18918
|
### Collapsible (icon rail)
|
|
@@ -18949,7 +19021,7 @@ import { SideNav, SideNavGroup, SideNavItem } from "@sableui/react"
|
|
|
18949
19021
|
import { SideNav, SideNavItem } from "@sableui/react"
|
|
18950
19022
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
18951
19023
|
|
|
18952
|
-
<SideNav size="lg"><SideNavItem label="Profile" active render={<Link to="/settings/profile" />} /><SideNavItem label="
|
|
19024
|
+
<SideNav size="lg"><SideNavItem label="Profile" active render={<Link to="/settings/profile" />} /><SideNavItem label="Billing" render={<Link to="/settings/billing" />} /><SideNavItem label="Security" render={<Link to="/settings/security" />} /></SideNav>
|
|
18953
19025
|
```
|
|
18954
19026
|
|
|
18955
19027
|
### Inside Sidebar chrome
|
|
@@ -19192,7 +19264,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
|
|
|
19192
19264
|
| data | number[] | — | The values, oldest first. |
|
|
19193
19265
|
| label | string | — | What the line tracks ("Revenue, last 30 days"). The spoken summary adds the first and last values, low and high. |
|
|
19194
19266
|
| variant | "line" \| "area" \| "bar" | "line" | A line, a line over a soft fill, or columns. |
|
|
19195
|
-
| tone | "
|
|
19267
|
+
| tone | "1" \| "2" \| "3" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | "primary" | Colour by role. |
|
|
19196
19268
|
| className | string | "h-8 w-24" | Sizes it; the drawing stretches to fit. |
|
|
19197
19269
|
| animate | boolean | true | Draw on (or rise) when it scrolls into view. |
|
|
19198
19270
|
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
@@ -19203,7 +19275,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
|
|
|
19203
19275
|
|
|
19204
19276
|
## Tokens used
|
|
19205
19277
|
|
|
19206
|
-
`primary`, `tone-
|
|
19278
|
+
`primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
|
|
19207
19279
|
|
|
19208
19280
|
## Motion
|
|
19209
19281
|
|
|
@@ -19334,7 +19406,7 @@ import { Spinner } from "@sableui/react"
|
|
|
19334
19406
|
```tsx
|
|
19335
19407
|
import { Button } from "@sableui/react"
|
|
19336
19408
|
|
|
19337
|
-
<Button loading>
|
|
19409
|
+
<Button loading>Saving</Button>
|
|
19338
19410
|
```
|
|
19339
19411
|
|
|
19340
19412
|
---
|
|
@@ -19463,18 +19535,18 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
|
|
|
19463
19535
|
|
|
19464
19536
|
| Name | Type | Default | Description |
|
|
19465
19537
|
| --- | --- | --- | --- |
|
|
19466
|
-
| tone | "neutral" \| "
|
|
19538
|
+
| tone | "neutral" \| "1" \| "2" \| "3" \| "success" \| "warning" \| "info" \| "destructive" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "purple" \| "pink" | "neutral" | Semantic color of the dot, or a palette family (red … pink) for a category legend. |
|
|
19467
19539
|
| size | "sm" \| "md" \| "lg" | "md" | Dot diameter. |
|
|
19468
19540
|
| pulse | boolean | false | Wrap the dot in an animated ping ring to signal a live state. |
|
|
19469
19541
|
|
|
19470
19542
|
## Variants
|
|
19471
19543
|
|
|
19472
|
-
- **tone**: neutral,
|
|
19544
|
+
- **tone**: neutral, 1, 2, 3, success, warning, info, destructive, red, orange, yellow, green, teal, blue, purple, pink
|
|
19473
19545
|
- **size**: sm, md, lg
|
|
19474
19546
|
|
|
19475
19547
|
## Tokens used
|
|
19476
19548
|
|
|
19477
|
-
`muted-foreground`, `tone-
|
|
19549
|
+
`palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`
|
|
19478
19550
|
|
|
19479
19551
|
## Accessibility
|
|
19480
19552
|
|
|
@@ -19493,7 +19565,7 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
|
|
|
19493
19565
|
```tsx
|
|
19494
19566
|
import { Row, StatusDot } from "@sableui/react"
|
|
19495
19567
|
|
|
19496
|
-
<Row gap={2}><StatusDot tone="
|
|
19568
|
+
<Row gap={2}><StatusDot tone="3" /><StatusDot tone="info" /><StatusDot tone="warning" /><StatusDot tone="destructive" /></Row>
|
|
19497
19569
|
```
|
|
19498
19570
|
|
|
19499
19571
|
### With label — use Badge
|
|
@@ -19878,25 +19950,25 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
|
|
|
19878
19950
|
|
|
19879
19951
|
## Examples
|
|
19880
19952
|
|
|
19881
|
-
###
|
|
19953
|
+
### Orders
|
|
19882
19954
|
|
|
19883
19955
|
```tsx
|
|
19884
19956
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
19885
19957
|
|
|
19886
19958
|
<Table>
|
|
19887
|
-
<TableCaption>Recent
|
|
19959
|
+
<TableCaption>Recent orders</TableCaption>
|
|
19888
19960
|
<TableHeader>
|
|
19889
19961
|
<TableRow>
|
|
19890
|
-
<TableHead>
|
|
19891
|
-
<TableHead>
|
|
19962
|
+
<TableHead>Customer</TableHead>
|
|
19963
|
+
<TableHead>Order</TableHead>
|
|
19892
19964
|
<TableHead>Paid</TableHead>
|
|
19893
19965
|
</TableRow>
|
|
19894
19966
|
</TableHeader>
|
|
19895
19967
|
<TableBody>
|
|
19896
19968
|
<TableRow>
|
|
19897
19969
|
<TableCell>@nova</TableCell>
|
|
19898
|
-
<TableCell
|
|
19899
|
-
<TableCell
|
|
19970
|
+
<TableCell>1042</TableCell>
|
|
19971
|
+
<TableCell>$48.00</TableCell>
|
|
19900
19972
|
</TableRow>
|
|
19901
19973
|
</TableBody>
|
|
19902
19974
|
</Table>
|
|
@@ -19915,7 +19987,7 @@ import { Avatar, Badge, Button, Row, Table, TableBody, TableCaption, TableCell,
|
|
|
19915
19987
|
```tsx
|
|
19916
19988
|
import { Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
19917
19989
|
|
|
19918
|
-
<Card><CardHeader><CardTitle>
|
|
19990
|
+
<Card><CardHeader><CardTitle>Customers</CardTitle></CardHeader><CardContent flush><Table><TableHeader><TableRow><TableHead>Customer</TableHead><TableHead>Paid</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>@nova</TableCell><TableCell>$48.00</TableCell></TableRow></TableBody></Table></CardContent></Card>
|
|
19919
19991
|
```
|
|
19920
19992
|
|
|
19921
19993
|
### Summary footer
|
|
@@ -19923,7 +19995,7 @@ import { Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell,
|
|
|
19923
19995
|
```tsx
|
|
19924
19996
|
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
19925
19997
|
|
|
19926
|
-
<Table><TableHeader><TableRow><TableHead>Item</TableHead><TableHead className="text-end">Amount</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>
|
|
19998
|
+
<Table><TableHeader><TableRow><TableHead>Item</TableHead><TableHead className="text-end">Amount</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>Studio plan</TableCell><TableCell className="text-end">$192.00</TableCell></TableRow></TableBody><TableFooter><TableRow><TableCell>Total</TableCell><TableCell className="text-end">$192.00</TableCell></TableRow></TableFooter></Table>
|
|
19927
19999
|
```
|
|
19928
20000
|
|
|
19929
20001
|
---
|
|
@@ -20010,7 +20082,7 @@ import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
|
|
|
20010
20082
|
```tsx
|
|
20011
20083
|
import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
|
|
20012
20084
|
|
|
20013
|
-
<Tabs defaultValue="overview"><TabsList><TabsTrigger value="overview">Overview</TabsTrigger><TabsTrigger value="
|
|
20085
|
+
<Tabs defaultValue="overview"><TabsList><TabsTrigger value="overview">Overview</TabsTrigger><TabsTrigger value="billing" disabled>Billing</TabsTrigger></TabsList><TabsPanel value="overview">Overview content</TabsPanel></Tabs>
|
|
20014
20086
|
```
|
|
20015
20087
|
|
|
20016
20088
|
---
|
|
@@ -20077,7 +20149,7 @@ import { Textarea } from "@sableui/react"
|
|
|
20077
20149
|
```tsx
|
|
20078
20150
|
import { Textarea } from "@sableui/react"
|
|
20079
20151
|
|
|
20080
|
-
<Textarea rows={6} placeholder="Tell
|
|
20152
|
+
<Textarea rows={6} placeholder="Tell your team what this project is for…" />
|
|
20081
20153
|
```
|
|
20082
20154
|
|
|
20083
20155
|
### Auto-resize
|
|
@@ -20101,7 +20173,7 @@ import { Textarea } from "@sableui/react"
|
|
|
20101
20173
|
```tsx
|
|
20102
20174
|
import { Textarea } from "@sableui/react"
|
|
20103
20175
|
|
|
20104
|
-
<Textarea disabled defaultValue="
|
|
20176
|
+
<Textarea disabled defaultValue="Notes lock once the invoice is sent." />
|
|
20105
20177
|
```
|
|
20106
20178
|
|
|
20107
20179
|
---
|
|
@@ -20518,7 +20590,7 @@ toast.promise(publish(), { loading: "Publishing…", success: "Published", error
|
|
|
20518
20590
|
```tsx
|
|
20519
20591
|
import { toast } from "@sableui/react"
|
|
20520
20592
|
|
|
20521
|
-
const id = toast.loading("
|
|
20593
|
+
const id = toast.loading("Uploading report…"); /* later */ toast.update(id, { title: "Uploaded", type: "success", timeout: 4000 })
|
|
20522
20594
|
```
|
|
20523
20595
|
|
|
20524
20596
|
### Custom toast markup (composed parts)
|
|
@@ -20900,7 +20972,7 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
|
|
|
20900
20972
|
```tsx
|
|
20901
20973
|
import { Button, Icon, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@sableui/react"
|
|
20902
20974
|
|
|
20903
|
-
<TooltipProvider><Tooltip><TooltipTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button>} /><TooltipContent>
|
|
20975
|
+
<TooltipProvider><Tooltip><TooltipTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button>} /><TooltipContent>Saved to your workspace.</TooltipContent></Tooltip></TooltipProvider>
|
|
20904
20976
|
```
|
|
20905
20977
|
|
|
20906
20978
|
### Sides
|
|
@@ -20916,7 +20988,7 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@sableui/react"
|
|
|
20916
20988
|
```tsx
|
|
20917
20989
|
import { Button, Icon, SimpleTooltip } from "@sableui/react"
|
|
20918
20990
|
|
|
20919
|
-
<SimpleTooltip content="
|
|
20991
|
+
<SimpleTooltip content="Saved to your workspace."><Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button></SimpleTooltip>
|
|
20920
20992
|
```
|
|
20921
20993
|
|
|
20922
20994
|
---
|
|
@@ -21413,23 +21485,23 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
21413
21485
|
- **Not** `tone-info`. Success asserts an outcome; info asserts a fact. 'Saved' is success, '3 items' is info.
|
|
21414
21486
|
- **`success-foreground`** — Ink paired with `success`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `success`. Use them as a pair — `bg-success text-success-foreground` — so a theme can retune the surface and the ink together.
|
|
21415
21487
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
21416
|
-
- **`tone-
|
|
21488
|
+
- **`tone-3`** — Extra colour 3: the third, and usually the quietest, category in a classified set. *Use for:* A third category, or the baseline one that means 'nothing special'.
|
|
21417
21489
|
- **Not** `muted`. A tone is *semantic classification* that a reader is meant to decode; `muted` is de-emphasis. If a legend would help the user, it is a tone.
|
|
21418
|
-
- **`tone-
|
|
21419
|
-
- **Not** `
|
|
21420
|
-
- **`tone-
|
|
21421
|
-
- **Not** `
|
|
21490
|
+
- **`tone-2`** — Extra colour 2: the second category in a classified set. *Use for:* A second category, distinct from tone-1, in a badge, dot, card or chart.
|
|
21491
|
+
- **Not** `tone-1`. Distinct categories must stay visually distinct.
|
|
21492
|
+
- **`tone-1`** — Extra colour 1, the lead of the three a theme defines beyond its status colours. The numbered tones exist so a product can classify content (categories, plans, series) without inventing colours per feature; the product decides what each one means. *Use for:* The most prominent category in a classified set, or the first series of a chart.
|
|
21493
|
+
- **Not** `primary`. A tone classifies the *thing*; primary drives an *action*. A category badge is not a button.
|
|
21422
21494
|
- **`tone-info`** — Domain tone: neutral, factual information. *Use for:* An informational note or banner, a neutral count, a 'did you know' surface.
|
|
21423
21495
|
- **Not** `success`. Info states a fact; success states an outcome. Also not `primary` — an info banner is not an action.
|
|
21424
21496
|
- **`tone-warning`** — Domain tone: proceed with care, nothing is broken yet. *Use for:* A caution banner, a nearing-limit indicator, a soloed/overridden state in a tool.
|
|
21425
21497
|
- **Not** `destructive`. Warning is *advisory*; destructive is *consequential*. If nothing is lost by continuing, it is a warning.
|
|
21426
21498
|
- **`highlight`** — A marker on content the user is meant to notice — search hits, diffs, annotations. *Use for:* Highlighting matched text, a changed line, a called-out passage.
|
|
21427
21499
|
- **Not** `accent`. `accent` is UI state that follows the pointer or selection; `highlight` marks the *content* and persists independently of interaction.
|
|
21428
|
-
- **`tone-
|
|
21500
|
+
- **`tone-3-foreground`** — Ink paired with `tone-3`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-3`. Use them as a pair — `bg-tone-3 text-tone-3-foreground` — so a theme can retune the surface and the ink together.
|
|
21429
21501
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
21430
|
-
- **`tone-
|
|
21502
|
+
- **`tone-2-foreground`** — Ink paired with `tone-2`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-2`. Use them as a pair — `bg-tone-2 text-tone-2-foreground` — so a theme can retune the surface and the ink together.
|
|
21431
21503
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
21432
|
-
- **`tone-
|
|
21504
|
+
- **`tone-1-foreground`** — Ink paired with `tone-1`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-1`. Use them as a pair — `bg-tone-1 text-tone-1-foreground` — so a theme can retune the surface and the ink together.
|
|
21433
21505
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
21434
21506
|
- **`tone-info-foreground`** — Ink paired with `tone-info`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-info`. Use them as a pair — `bg-tone-info text-tone-info-foreground` — so a theme can retune the surface and the ink together.
|
|
21435
21507
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
@@ -21536,11 +21608,11 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
21536
21608
|
|
|
21537
21609
|
### layout (11)
|
|
21538
21610
|
|
|
21539
|
-
- **`rhythm-unit`** — The vertical rhythm base —
|
|
21611
|
+
- **`rhythm-unit`** — The vertical rhythm base — one baseline step (8px at the default scale). Every vertical measure is a multiple of it, and it follows the type and the density control. *Use for:* Through `Stack`/`Row`/`Grid` gap numbers, which are rhythm units rather than pixels. A gap of 2 is 16px at the default scale, and grows or tightens with density.
|
|
21540
21612
|
- **Not** arbitrary vertical spacing. The rhythm is what makes unrelated sections of a page feel like one document.
|
|
21541
21613
|
- **`grid-columns`** — The column count of the content grid — 12. *Use for:* `Grid`/`Columns`, WITHIN a region, for genuinely multi-column content.
|
|
21542
21614
|
- **Not** using a column span as the page's reading width. A span is not a measure — see `measure-prose`.
|
|
21543
|
-
- **`grid-gutter`** — The gap between grid columns. *Use for:* `Grid`/`Columns` spacing.
|
|
21615
|
+
- **`grid-gutter`** — The gap between grid columns and rows — 2 baseline steps on phones, 3 on desktop, snapped to the step. *Use for:* `Grid`/`Columns` spacing.
|
|
21544
21616
|
- **`page-inset`** — The page's outer breathing room, left and right. *Use for:* The gutter between content and the viewport edge, at every breakpoint.
|
|
21545
21617
|
- **Not** a global max-width. Sable caps width per REGION by role, never once for the whole page.
|
|
21546
21618
|
- **`measure-prose`** — The maximum comfortable line length for prose, in `ch`. *Use for:* Any long-form text column.
|
|
@@ -21700,6 +21772,20 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
21700
21772
|
- **`border-floating`** — The edge of a floating surface. Fainter than `border`: the shadow carries the elevation. *Use for:* Every floating surface — popover, menu, select, tooltip, dialog, sheet, toast, a floating panel — as `border-border-floating` or `ring-border-floating`, together with `bg-popover` and a `shadow-md`+ shadow. Derived for every theme (a faint alpha of the foreground); a theme may tune it with `--theme-border-floating`.
|
|
21701
21773
|
- **Not** `border`. A full hairline plus a shadow stacks into a heavy outline around anything that floats. `border` is for resting edges: inputs, cards, badges.
|
|
21702
21774
|
|
|
21775
|
+
### Generated palette
|
|
21776
|
+
|
|
21777
|
+
More colour for apps, generated from the active theme: neutral and eight hue families, plus a 50–950 scale under every tone. Each family keeps its hue and leans toward the brand (half the gap, at most 15°; --theme-palette-harmony scales it, 0 = own hues). Neutral is the theme's --background mixed toward its --foreground. Steps follow the mode: low steps sit near the page and high ones far from it in light and dark alike, so a pair needs no dark: variant. For decorative and categorical colour (tags, avatars, labels, legends, illustration); surfaces, text and actions take roles. Components never name a step.
|
|
21778
|
+
|
|
21779
|
+
- **Families:** `neutral`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple`, `pink`, each 50–950.
|
|
21780
|
+
- **Tone scales:** `tone-1-*`, `tone-2-*`, `tone-3-*`, `tone-info-*`, `tone-warning-*`, `success-*`, `destructive-*`; step 500 is the tone itself.
|
|
21781
|
+
- **Use as:** bg-blue-100, text-blue-800, border-neutral-200, bg-tone-1-100 — or var(--teal-500). Tailwind's other families (slate, gray, zinc, stone, amber, lime, emerald, cyan, sky, indigo, violet, fuchsia, rose) are switched off.
|
|
21782
|
+
- **tag / label:** bg-<family>-100 text-<family>-800 (held to 4.5:1 in every theme and mode)
|
|
21783
|
+
- **text on the page:** text-<family>-700
|
|
21784
|
+
- **solid fill:** bg-<family>-700 text-<family>-50
|
|
21785
|
+
- **hairline:** border-<family>-300
|
|
21786
|
+
- **Components:** Badge, Pill and StatusDot take a family as tone (tone="blue"): a category with no meaning attached.
|
|
21787
|
+
- **Slots:** data-palette="<family>" on an element fills these from that family, so one set of classes paints any family: bg-palette-wash text-palette-text.
|
|
21788
|
+
|
|
21703
21789
|
---
|
|
21704
21790
|
|
|
21705
21791
|
## Utilities
|
|
@@ -21805,7 +21891,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
21805
21891
|
- `<ol className="sable-list-numbered"><li>Install</li><li>Import the styles</li></ol>`
|
|
21806
21892
|
- **`sable-list-dl`** — Label/value pairs on a native `<dl>`: muted label-tier `<dt>`s and body-tier `<dd>`s on a baseline-aligned two-column grid; long values wrap inside their column. Colour a status value with a `text-*` utility on its `<dd>`. *Use for:* Read-only details: an order summary, a record's metadata, a settings summary, profile stats.
|
|
21807
21893
|
- **Instead of** A grid of `<div>`s (screen readers lose the term/value pairing), or Table for a single record. For editable values use Form and Field.
|
|
21808
|
-
- `<dl className="sable-list-dl"><dt>
|
|
21894
|
+
- `<dl className="sable-list-dl"><dt>Plan</dt><dd>Studio</dd><dt>Status</dt><dd className="text-success">Active</dd></dl>`
|
|
21809
21895
|
- **`sable-list-dl-2`** — Two label/value pairs a row once the nearest `@container` ancestor is at least 28rem wide. Add to `sable-list-dl`. *Use for:* A compact metadata block in a wide card or panel. Put `@container` on a parent so it measures the room it actually has.
|
|
21810
21896
|
- **Instead of** A viewport breakpoint, which gives a narrow card on a wide screen four columns it cannot fit.
|
|
21811
21897
|
- `<div className="@container"><dl className="sable-list-dl sable-list-dl-2">…</dl></div>`
|