@sableui/react 0.9.1 → 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 +95 -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.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.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/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.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/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.meta.js +1 -1
- package/dist/components/tooltip/tooltip.meta.js +2 -2
- package/dist/file-hashes.json +120 -118
- package/dist/foundations/spacing.js +8 -8
- package/dist/index.d.ts +137 -72
- package/dist/index.js +12 -1
- package/dist/lib/chart/types.js +3 -3
- 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 +115 -50
- 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/converting-an-app.md +3 -1
- package/llms/tasks/using-presets.md +4 -0
- package/llms-full.txt +429 -347
- package/llms.txt +7 -7
- package/package.json +4 -4
- package/registry.json +369 -233
- package/scripts/lint-rules.mjs +26 -5
- 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
|
|
@@ -1224,7 +1267,9 @@ loudest and least clearly ("invalid hook call").
|
|
|
1224
1267
|
|
|
1225
1268
|
The most reliable signal that a conversion is real: the app's own CSS variable
|
|
1226
1269
|
block shrinks to almost nothing. Anything that remains should be *domain* colour,
|
|
1227
|
-
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.
|
|
1228
1273
|
|
|
1229
1274
|
Grep for orphaned references afterwards. A CSS variable that no longer exists
|
|
1230
1275
|
resolves to nothing and the declaration is dropped — **silently**. A checkerboard
|
|
@@ -2510,6 +2555,10 @@ prints a complete starting record; the public `PRESET_TOKEN_NAMES` is authoritat
|
|
|
2510
2555
|
Font stacks are requirements, not downloaded fonts. Declare matching stacks in
|
|
2511
2556
|
the font metadata and tokens. Resolve internal palette variables before saving;
|
|
2512
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.
|
|
2513
2562
|
The validator rejects unknown tokens and CSS that can escape its declaration.
|
|
2514
2563
|
`sable preset --check` also holds labels on brand and status fills to 4.5:1 and APCA Lc 60
|
|
2515
2564
|
and suggests a pair that reads; a preset may keep a pair deliberately through
|
|
@@ -2825,9 +2874,9 @@ muted-foreground is contrast-tested on every surface; a 60% alpha is not.
|
|
|
2825
2874
|
|
|
2826
2875
|
# Spacing & sizing
|
|
2827
2876
|
|
|
2828
|
-
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.
|
|
2829
2878
|
|
|
2830
|
-
**
|
|
2879
|
+
**Baseline from the type · 8px steps · 3 roles · 1 control ladder**
|
|
2831
2880
|
|
|
2832
2881
|
## Spacing by role
|
|
2833
2882
|
|
|
@@ -2836,8 +2885,8 @@ Steps below 2 (8px) are optical correction only.
|
|
|
2836
2885
|
| Name | Value | Use |
|
|
2837
2886
|
|---|---|---|
|
|
2838
2887
|
| `Inset p-4` | 16px | Padding inside a surface. p-3 for a dense card, px-3 py-2 for a row. |
|
|
2839
|
-
| `Stack, within a group gap-
|
|
2840
|
-
| `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. |
|
|
2841
2890
|
| `Stack, between sections gap-12+` | 48px+ | Major regions of a page. |
|
|
2842
2891
|
| `Inline gap-2 – gap-3` | 8–12px | Icon to label, control to control. |
|
|
2843
2892
|
|
|
@@ -2863,8 +2912,8 @@ Comfortable values; data-density="compact" (or <Density>) redefines every step f
|
|
|
2863
2912
|
|
|
2864
2913
|
## Rules
|
|
2865
2914
|
|
|
2866
|
-
1. **Every margin, padding and gap is a step on the
|
|
2867
|
-
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).*
|
|
2868
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.
|
|
2869
2918
|
4. **Space lives on the container (gap), not on children (margins).** gap survives reordering, conditional children and RTL; per-child margins do not.
|
|
2870
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.*
|
|
@@ -2902,7 +2951,7 @@ Every control, menu and popup inside retunes, including portaled overlays.
|
|
|
2902
2951
|
<Stack gap={3}> between groups (24px), <Stack gap={1}> inside a group (8px)
|
|
2903
2952
|
```
|
|
2904
2953
|
|
|
2905
|
-
|
|
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.
|
|
2906
2955
|
|
|
2907
2956
|
**Related:** `sable docs shape` · `sable docs typography` · `sable docs building-a-tool` · `sable token layout`
|
|
2908
2957
|
|
|
@@ -3265,7 +3314,7 @@ The region is in the accessibility tree before its text changes, so the change i
|
|
|
3265
3314
|
|
|
3266
3315
|
# AuthPage
|
|
3267
3316
|
|
|
3268
|
-
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.
|
|
3269
3318
|
|
|
3270
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).
|
|
3271
3320
|
|
|
@@ -3276,14 +3325,14 @@ Split sign-in page: a form column (brand mark, heading, email + password, primar
|
|
|
3276
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.
|
|
3277
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.
|
|
3278
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.
|
|
3279
|
-
- 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.
|
|
3280
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.
|
|
3281
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.
|
|
3282
3331
|
- The panel is `hidden lg:flex`: on a phone the form is the whole screen, with no decorative scroll above it.
|
|
3283
3332
|
|
|
3284
3333
|
## Accessibility
|
|
3285
3334
|
|
|
3286
|
-
- 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".
|
|
3287
3336
|
- Field auto-wires labels, describedby, and invalid state onto the inputs; on submit, the first invalid field is focused.
|
|
3288
3337
|
- Autocomplete attributes (`email webauthn`, `current-password`) keep password managers and passkey autofill working.
|
|
3289
3338
|
- The Forgot password and Create one links use `hit-target` to meet the 44px touch floor without changing layout.
|
|
@@ -3314,7 +3363,7 @@ export function AuthPage() {
|
|
|
3314
3363
|
<Stack gap={1.5}>
|
|
3315
3364
|
<h1 className="text-heading-1">Welcome back</h1>
|
|
3316
3365
|
<p className="text-body-lg text-muted-foreground">
|
|
3317
|
-
Sign in to
|
|
3366
|
+
Sign in to pick up where you and your team left off.
|
|
3318
3367
|
</p>
|
|
3319
3368
|
</Stack>
|
|
3320
3369
|
|
|
@@ -3352,8 +3401,8 @@ export function AuthPage() {
|
|
|
3352
3401
|
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="key" />}>
|
|
3353
3402
|
Sign in with a passkey
|
|
3354
3403
|
</Button>
|
|
3355
|
-
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="
|
|
3356
|
-
|
|
3404
|
+
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="mail" />}>
|
|
3405
|
+
Email me a sign-in link
|
|
3357
3406
|
</Button>
|
|
3358
3407
|
</Stack>
|
|
3359
3408
|
|
|
@@ -3380,23 +3429,23 @@ export function AuthPage() {
|
|
|
3380
3429
|
</Region>
|
|
3381
3430
|
|
|
3382
3431
|
{/* The brand panel is decoration: on a phone the form is the whole screen. */}
|
|
3383
|
-
<aside aria-label="
|
|
3432
|
+
<aside aria-label="Customer story" className="hidden p-3 lg:flex">
|
|
3384
3433
|
{/* Art painted from the theme's tones over its brand, so it follows every look. */}
|
|
3385
3434
|
<GradientArt seed="Untitled (gold rush)" className="flex flex-1 flex-col justify-end rounded-xl p-8">
|
|
3386
3435
|
<Card className="max-w-md">
|
|
3387
3436
|
<CardContent>
|
|
3388
3437
|
<Stack gap={3}>
|
|
3389
|
-
<Badge tone="
|
|
3390
|
-
|
|
3438
|
+
<Badge tone="1" className="self-start">
|
|
3439
|
+
Customer story
|
|
3391
3440
|
</Badge>
|
|
3392
3441
|
<p className="text-title-lg">
|
|
3393
|
-
“
|
|
3442
|
+
“We moved three teams over in a week, and nobody asked for the old tool back.”
|
|
3394
3443
|
</p>
|
|
3395
3444
|
<Entity
|
|
3396
3445
|
className="p-0"
|
|
3397
3446
|
leading={<Avatar variant="art" fallback="MO" />}
|
|
3398
3447
|
title="Mara Okafor"
|
|
3399
|
-
subtitle="
|
|
3448
|
+
subtitle="Head of Design, Northwind"
|
|
3400
3449
|
/>
|
|
3401
3450
|
</Stack>
|
|
3402
3451
|
</CardContent>
|
|
@@ -3457,7 +3506,7 @@ const spaces = [
|
|
|
3457
3506
|
|
|
3458
3507
|
const routes = [
|
|
3459
3508
|
{ label: "Intake", state: "Open", tone: "info" as const },
|
|
3460
|
-
{ label: "Review", state: "Live", tone: "
|
|
3509
|
+
{ label: "Review", state: "Live", tone: "1" as const },
|
|
3461
3510
|
{ label: "Archive", state: "Safe", tone: "success" as const },
|
|
3462
3511
|
]
|
|
3463
3512
|
|
|
@@ -3635,7 +3684,7 @@ export function BrandLandingPage() {
|
|
|
3635
3684
|
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
3636
3685
|
<Row gap={1}>
|
|
3637
3686
|
<StatusDot tone="warning" />
|
|
3638
|
-
<StatusDot tone="
|
|
3687
|
+
<StatusDot tone="1" />
|
|
3639
3688
|
<StatusDot tone="success" />
|
|
3640
3689
|
</Row>
|
|
3641
3690
|
<span className="text-label-sm text-muted-foreground">Live workspace</span>
|
|
@@ -4029,7 +4078,7 @@ export function ChatPanel() {
|
|
|
4029
4078
|
render={<section aria-labelledby="chat-title" />}
|
|
4030
4079
|
className="flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden"
|
|
4031
4080
|
>
|
|
4032
|
-
<CardHeader className="
|
|
4081
|
+
<CardHeader className="pb-(--card-p) shadow-[inset_0_-1px_0_var(--border)]">
|
|
4033
4082
|
{/* CardTitle carries the type; the h1 inside it is the page heading. */}
|
|
4034
4083
|
<CardTitle>
|
|
4035
4084
|
<h1 id="chat-title">Scroll behaviour</h1>
|
|
@@ -4168,7 +4217,7 @@ export function ChatPanel() {
|
|
|
4168
4217
|
</Message>
|
|
4169
4218
|
</MessageScroller>
|
|
4170
4219
|
|
|
4171
|
-
<CardFooter className="
|
|
4220
|
+
<CardFooter className="pt-(--card-p) shadow-[inset_0_1px_0_var(--border)]">
|
|
4172
4221
|
<ChatComposer
|
|
4173
4222
|
className="flex-1"
|
|
4174
4223
|
placeholder="Ask a follow-up…"
|
|
@@ -4228,18 +4277,18 @@ export function ChatPanel() {
|
|
|
4228
4277
|
|
|
4229
4278
|
# DashboardPage
|
|
4230
4279
|
|
|
4231
|
-
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.
|
|
4232
4281
|
|
|
4233
|
-
**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).
|
|
4234
4283
|
|
|
4235
4284
|
## Notes
|
|
4236
4285
|
|
|
4237
4286
|
- Region cap is `wide` (1280px): dashboards hold tables and multi-column grids, never a reading column.
|
|
4238
|
-
- 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.
|
|
4239
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.
|
|
4240
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.
|
|
4241
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.
|
|
4242
|
-
-
|
|
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.
|
|
4243
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`.
|
|
4244
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.
|
|
4245
4294
|
- The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.
|
|
@@ -4251,7 +4300,7 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
4251
4300
|
- AppShell provides the skip link and <main id="main-content"> landmark.
|
|
4252
4301
|
- The active SidebarItem and BottomNavItem carry aria-current via `active`.
|
|
4253
4302
|
- DataTable headers are real <th>; sortable columns are announced.
|
|
4254
|
-
- Each
|
|
4303
|
+
- Each seat-usage Meter has an aria-label naming the account and aria-valuetext reading "184 of 250 seats used".
|
|
4255
4304
|
- Avatars beside a visible name use an empty alt so the name is not read twice.
|
|
4256
4305
|
|
|
4257
4306
|
## Source
|
|
@@ -4259,11 +4308,11 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
4259
4308
|
```tsx
|
|
4260
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"
|
|
4261
4310
|
|
|
4262
|
-
//
|
|
4311
|
+
// Account status → Badge tone. Sable has no status vocabulary of its own;
|
|
4263
4312
|
// an app maps ITS states onto tones once, here, and every badge follows.
|
|
4264
|
-
const STATUS_TONE = {
|
|
4265
|
-
const STATUS_LABEL = {
|
|
4266
|
-
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
|
|
4267
4316
|
|
|
4268
4317
|
const MIN = 60 * 1000
|
|
4269
4318
|
const HOUR = 60 * MIN
|
|
@@ -4276,54 +4325,54 @@ const shortDate = (offset: number) =>
|
|
|
4276
4325
|
|
|
4277
4326
|
const stats = [
|
|
4278
4327
|
{ label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
|
|
4279
|
-
{ label: "
|
|
4280
|
-
{ label: "
|
|
4281
|
-
{ 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" },
|
|
4282
4331
|
]
|
|
4283
4332
|
|
|
4284
|
-
const
|
|
4285
|
-
{ id: "
|
|
4286
|
-
{ id: "
|
|
4287
|
-
{ id: "
|
|
4288
|
-
{ id: "
|
|
4289
|
-
{ id: "
|
|
4290
|
-
{ id: "
|
|
4291
|
-
{ id: "
|
|
4292
|
-
{ id: "
|
|
4293
|
-
{ 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 },
|
|
4294
4343
|
]
|
|
4295
|
-
const
|
|
4344
|
+
const accountCount = 312
|
|
4296
4345
|
|
|
4297
4346
|
const payout = {
|
|
4298
|
-
amount:
|
|
4347
|
+
amount: 8862.64,
|
|
4299
4348
|
date: 5 * DAY,
|
|
4300
4349
|
lines: [
|
|
4301
|
-
{ label: "
|
|
4302
|
-
{ label: "
|
|
4350
|
+
{ label: "Subscriptions", value: 9127.33 },
|
|
4351
|
+
{ label: "Processing (2.9%)", value: -264.69 },
|
|
4303
4352
|
{ label: "Deposits to", value: "Checking ••4821" },
|
|
4304
4353
|
],
|
|
4305
4354
|
}
|
|
4306
4355
|
|
|
4307
|
-
const
|
|
4308
|
-
{ id: "
|
|
4309
|
-
{ id: "
|
|
4310
|
-
{ id: "
|
|
4311
|
-
{ 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 },
|
|
4312
4361
|
]
|
|
4313
4362
|
|
|
4314
4363
|
const upcoming = [
|
|
4315
|
-
{ id: "up_01", title: "
|
|
4316
|
-
{ id: "up_02", title: "
|
|
4317
|
-
{ id: "up_03", title: "
|
|
4318
|
-
{ 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 },
|
|
4319
4368
|
]
|
|
4320
4369
|
|
|
4321
4370
|
const activity = [
|
|
4322
|
-
{ id: "
|
|
4323
|
-
{ id: "
|
|
4324
|
-
{ id: "
|
|
4325
|
-
{ id: "
|
|
4326
|
-
{ 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 },
|
|
4327
4376
|
]
|
|
4328
4377
|
|
|
4329
4378
|
export function DashboardPage() {
|
|
@@ -4345,8 +4394,8 @@ export function DashboardPage() {
|
|
|
4345
4394
|
bottomNav={
|
|
4346
4395
|
<BottomNav aria-label="Primary">
|
|
4347
4396
|
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel active render={<a href="/" />} />
|
|
4348
|
-
<BottomNavItem icon={<Icon name="
|
|
4349
|
-
<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" />} />
|
|
4350
4399
|
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
4351
4400
|
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel render={<a href="/settings" />} />
|
|
4352
4401
|
</BottomNav>
|
|
@@ -4358,8 +4407,8 @@ export function DashboardPage() {
|
|
|
4358
4407
|
</SidebarHeader>
|
|
4359
4408
|
<SidebarNav>
|
|
4360
4409
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" active render={<a href="/" />} />
|
|
4361
|
-
<SidebarItem icon={<Icon name="
|
|
4362
|
-
<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" />} />
|
|
4363
4412
|
<SidebarItem
|
|
4364
4413
|
icon={<Icon name="bell" />}
|
|
4365
4414
|
label="Activity"
|
|
@@ -4390,7 +4439,7 @@ export function DashboardPage() {
|
|
|
4390
4439
|
<Page max="wide" gap={4} className="py-6">
|
|
4391
4440
|
<PageHeader
|
|
4392
4441
|
title="Overview"
|
|
4393
|
-
description="
|
|
4442
|
+
description="Revenue and account activity for the last 30 days."
|
|
4394
4443
|
actions={
|
|
4395
4444
|
<Row gap={2}>
|
|
4396
4445
|
<Button variant="outline">
|
|
@@ -4399,7 +4448,7 @@ export function DashboardPage() {
|
|
|
4399
4448
|
</Button>
|
|
4400
4449
|
<Button>
|
|
4401
4450
|
<Icon name="plus" />
|
|
4402
|
-
New
|
|
4451
|
+
New account
|
|
4403
4452
|
</Button>
|
|
4404
4453
|
</Row>
|
|
4405
4454
|
}
|
|
@@ -4429,43 +4478,43 @@ export function DashboardPage() {
|
|
|
4429
4478
|
<Grid cols={{ base: 1, xl: 3 }} gap={3}>
|
|
4430
4479
|
<Card className="flex flex-col xl:col-span-2">
|
|
4431
4480
|
<CardHeader>
|
|
4432
|
-
<CardTitle>
|
|
4433
|
-
<CardDescription>
|
|
4481
|
+
<CardTitle>Accounts</CardTitle>
|
|
4482
|
+
<CardDescription>Seats across your customers, newest first.</CardDescription>
|
|
4434
4483
|
</CardHeader>
|
|
4435
4484
|
<CardContent flush className="mt-4 flex-1">
|
|
4436
4485
|
<DataTable
|
|
4437
|
-
data={
|
|
4486
|
+
data={accounts}
|
|
4438
4487
|
rowKey={(row) => row.id}
|
|
4439
|
-
defaultSort={{ key: "
|
|
4488
|
+
defaultSort={{ key: "started", direction: "desc" }}
|
|
4440
4489
|
columns={[
|
|
4441
|
-
{ key: "title", header: "
|
|
4490
|
+
{ key: "title", header: "Account", sortable: true, className: "text-label-md whitespace-nowrap" },
|
|
4442
4491
|
{
|
|
4443
4492
|
key: "status",
|
|
4444
4493
|
header: "Status",
|
|
4445
4494
|
cell: (row) => (
|
|
4446
|
-
<Badge appearance="plain" tone={STATUS_TONE[row.status as
|
|
4447
|
-
{STATUS_LABEL[row.status as
|
|
4495
|
+
<Badge appearance="plain" tone={STATUS_TONE[row.status as AccountStatus]} dot>
|
|
4496
|
+
{STATUS_LABEL[row.status as AccountStatus]}
|
|
4448
4497
|
</Badge>
|
|
4449
4498
|
),
|
|
4450
4499
|
},
|
|
4451
4500
|
{
|
|
4452
|
-
key: "
|
|
4453
|
-
header: "
|
|
4501
|
+
key: "used",
|
|
4502
|
+
header: "Seats",
|
|
4454
4503
|
sortable: true,
|
|
4455
|
-
sortValue: (row) => row.
|
|
4504
|
+
sortValue: (row) => row.used / row.seats,
|
|
4456
4505
|
cell: (row) => (
|
|
4457
4506
|
<Row gap={3} align="center" className="min-w-40">
|
|
4458
4507
|
<Meter
|
|
4459
|
-
value={row.
|
|
4460
|
-
max={row.
|
|
4508
|
+
value={row.used}
|
|
4509
|
+
max={row.seats}
|
|
4461
4510
|
size="sm"
|
|
4462
|
-
tone={row.
|
|
4463
|
-
aria-label={`${row.title}
|
|
4464
|
-
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`}
|
|
4465
4514
|
className="flex-1"
|
|
4466
4515
|
/>
|
|
4467
4516
|
<span className="text-mono-caption whitespace-nowrap text-muted-foreground">
|
|
4468
|
-
{row.
|
|
4517
|
+
{row.used}/{row.seats}
|
|
4469
4518
|
</span>
|
|
4470
4519
|
</Row>
|
|
4471
4520
|
),
|
|
@@ -4475,21 +4524,21 @@ export function DashboardPage() {
|
|
|
4475
4524
|
header: "Revenue",
|
|
4476
4525
|
align: "end",
|
|
4477
4526
|
sortable: true,
|
|
4478
|
-
sortValue: (row) => row.
|
|
4527
|
+
sortValue: (row) => row.used * row.price,
|
|
4479
4528
|
cell: (row) =>
|
|
4480
|
-
row.
|
|
4481
|
-
<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>
|
|
4482
4531
|
) : (
|
|
4483
4532
|
<span className="text-mono-sm text-muted-foreground">—</span>
|
|
4484
4533
|
),
|
|
4485
4534
|
},
|
|
4486
4535
|
{
|
|
4487
|
-
key: "
|
|
4488
|
-
header: "
|
|
4536
|
+
key: "started",
|
|
4537
|
+
header: "Started",
|
|
4489
4538
|
align: "end",
|
|
4490
4539
|
sortable: true,
|
|
4491
4540
|
cell: (row) => (
|
|
4492
|
-
<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>
|
|
4493
4542
|
),
|
|
4494
4543
|
},
|
|
4495
4544
|
]}
|
|
@@ -4497,10 +4546,10 @@ export function DashboardPage() {
|
|
|
4497
4546
|
</CardContent>
|
|
4498
4547
|
<CardFooter className="justify-between">
|
|
4499
4548
|
<span className="text-caption text-muted-foreground">
|
|
4500
|
-
Showing {
|
|
4549
|
+
Showing {accounts.length} of {accountCount} accounts
|
|
4501
4550
|
</span>
|
|
4502
|
-
<Button variant="ghost" size="sm" bleed="end" render={<a href="/
|
|
4503
|
-
View all
|
|
4551
|
+
<Button variant="ghost" size="sm" bleed="end" render={<a href="/accounts" />}>
|
|
4552
|
+
View all accounts
|
|
4504
4553
|
<Icon name="arrow-right" />
|
|
4505
4554
|
</Button>
|
|
4506
4555
|
</CardFooter>
|
|
@@ -4531,21 +4580,21 @@ export function DashboardPage() {
|
|
|
4531
4580
|
</Card>
|
|
4532
4581
|
<Card>
|
|
4533
4582
|
<CardHeader>
|
|
4534
|
-
<CardTitle>Top
|
|
4583
|
+
<CardTitle>Top customers</CardTitle>
|
|
4535
4584
|
<CardDescription>By spend this month.</CardDescription>
|
|
4536
4585
|
<CardAction>
|
|
4537
|
-
<Button variant="ghost" size="sm" render={<a href="/
|
|
4586
|
+
<Button variant="ghost" size="sm" render={<a href="/customers" />}>
|
|
4538
4587
|
View all
|
|
4539
4588
|
</Button>
|
|
4540
4589
|
</CardAction>
|
|
4541
4590
|
</CardHeader>
|
|
4542
4591
|
<CardList>
|
|
4543
|
-
{
|
|
4592
|
+
{customers.map((person) => (
|
|
4544
4593
|
<Entity
|
|
4545
4594
|
key={person.id}
|
|
4546
4595
|
leading={<Avatar variant="art" size="sm" fallback={person.initials} alt="" />}
|
|
4547
4596
|
title={person.name}
|
|
4548
|
-
subtitle={`${person.handle} · ${person.
|
|
4597
|
+
subtitle={`${person.handle} · ${person.seats} seats`}
|
|
4549
4598
|
action={<span className="text-mono-sm">{usd.format(person.spent)}</span>}
|
|
4550
4599
|
/>
|
|
4551
4600
|
))}
|
|
@@ -4557,7 +4606,7 @@ export function DashboardPage() {
|
|
|
4557
4606
|
<Card className="flex flex-col">
|
|
4558
4607
|
<CardHeader>
|
|
4559
4608
|
<CardTitle>Upcoming releases</CardTitle>
|
|
4560
|
-
<CardDescription>Scheduled to
|
|
4609
|
+
<CardDescription>Scheduled to ship to customers.</CardDescription>
|
|
4561
4610
|
</CardHeader>
|
|
4562
4611
|
<CardList>
|
|
4563
4612
|
{upcoming.map((release) => (
|
|
@@ -4570,7 +4619,7 @@ export function DashboardPage() {
|
|
|
4570
4619
|
))}
|
|
4571
4620
|
</CardList>
|
|
4572
4621
|
<CardFooter className="mt-auto">
|
|
4573
|
-
<Button variant="outline" size="sm" render={<a href="/
|
|
4622
|
+
<Button variant="outline" size="sm" render={<a href="/releases/new" />}>
|
|
4574
4623
|
<Icon name="calendar" />
|
|
4575
4624
|
Schedule a release
|
|
4576
4625
|
</Button>
|
|
@@ -4579,7 +4628,7 @@ export function DashboardPage() {
|
|
|
4579
4628
|
<Card className="xl:col-span-2">
|
|
4580
4629
|
<CardHeader>
|
|
4581
4630
|
<CardTitle>Activity</CardTitle>
|
|
4582
|
-
<CardDescription>
|
|
4631
|
+
<CardDescription>Seats, upgrades, and invoices.</CardDescription>
|
|
4583
4632
|
<CardAction>
|
|
4584
4633
|
<Button variant="ghost" size="sm" render={<a href="/activity" />}>
|
|
4585
4634
|
View all
|
|
@@ -4609,7 +4658,7 @@ export function DashboardPage() {
|
|
|
4609
4658
|
|
|
4610
4659
|
# FeedPage
|
|
4611
4660
|
|
|
4612
|
-
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.
|
|
4613
4662
|
|
|
4614
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).
|
|
4615
4664
|
|
|
@@ -4619,7 +4668,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
|
|
|
4619
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.
|
|
4620
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.
|
|
4621
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.
|
|
4622
|
-
- 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.
|
|
4623
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.
|
|
4624
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.
|
|
4625
4674
|
|
|
@@ -4628,7 +4677,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
|
|
|
4628
4677
|
- Each post is an <article>, so AT users can jump between posts; the rail is a labelled <aside> landmark.
|
|
4629
4678
|
- Rail card titles render as h2 (CardTitle `render={<h2 />}`) under the page's one h1.
|
|
4630
4679
|
- Icon-only buttons and every Follow / Remind button carry aria-labels naming their target; counts stay in the accessible name.
|
|
4631
|
-
- The
|
|
4680
|
+
- The capacity Meter is role=meter with a getAriaValueText reading ("28 of 40 seats taken").
|
|
4632
4681
|
- The Skeleton card is aria-busy; the loading Button keeps its label ("Loading posts").
|
|
4633
4682
|
|
|
4634
4683
|
## Source
|
|
@@ -4645,7 +4694,7 @@ type Post = {
|
|
|
4645
4694
|
following: boolean
|
|
4646
4695
|
body: string
|
|
4647
4696
|
media?: { src: string; alt: string }
|
|
4648
|
-
|
|
4697
|
+
event?: { title: string; price: string; registered: number; capacity: number }
|
|
4649
4698
|
likes: number
|
|
4650
4699
|
comments: number
|
|
4651
4700
|
}
|
|
@@ -4658,9 +4707,9 @@ const posts: Post[] = [
|
|
|
4658
4707
|
initials: "AL",
|
|
4659
4708
|
time: "2h",
|
|
4660
4709
|
following: true,
|
|
4661
|
-
body: "
|
|
4662
|
-
media: { src: "/media/edition-cover.svg", alt: "
|
|
4663
|
-
|
|
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 },
|
|
4664
4713
|
likes: 128,
|
|
4665
4714
|
comments: 24,
|
|
4666
4715
|
},
|
|
@@ -4683,7 +4732,7 @@ const posts: Post[] = [
|
|
|
4683
4732
|
initials: "NS",
|
|
4684
4733
|
time: "8h",
|
|
4685
4734
|
following: false,
|
|
4686
|
-
body: "
|
|
4735
|
+
body: "Thanks to everyone at the Dusk listening session — the full recording is now in your library. Headphones on.",
|
|
4687
4736
|
likes: 214,
|
|
4688
4737
|
comments: 31,
|
|
4689
4738
|
},
|
|
@@ -4695,8 +4744,8 @@ const suggestions = [
|
|
|
4695
4744
|
]
|
|
4696
4745
|
|
|
4697
4746
|
const upcoming = [
|
|
4698
|
-
{ id: "
|
|
4699
|
-
{ 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" },
|
|
4700
4749
|
]
|
|
4701
4750
|
|
|
4702
4751
|
const renderPost = (post: Post) => (
|
|
@@ -4722,35 +4771,35 @@ const renderPost = (post: Post) => (
|
|
|
4722
4771
|
{post.media ? (
|
|
4723
4772
|
<AspectBox ratio={16 / 9} className="overflow-hidden rounded-concentric-lg/6 bg-muted">
|
|
4724
4773
|
<img src={post.media.src} alt={post.media.alt} className="size-full object-cover" />
|
|
4725
|
-
{post.
|
|
4774
|
+
{post.event ? (
|
|
4726
4775
|
<div className="absolute start-3 top-3">
|
|
4727
4776
|
<Badge appearance="overlay" size="sm" dot>
|
|
4728
|
-
|
|
4777
|
+
Open now
|
|
4729
4778
|
</Badge>
|
|
4730
4779
|
</div>
|
|
4731
4780
|
) : null}
|
|
4732
4781
|
</AspectBox>
|
|
4733
4782
|
) : null}
|
|
4734
|
-
{post.
|
|
4783
|
+
{post.event ? (
|
|
4735
4784
|
<Stack gap={1.5}>
|
|
4736
4785
|
<Row gap={2} align="center" justify="between">
|
|
4737
4786
|
<div className="min-w-0">
|
|
4738
|
-
<p className="text-title-md text-foreground">{post.
|
|
4787
|
+
<p className="text-title-md text-foreground">{post.event.title}</p>
|
|
4739
4788
|
<p className="text-body-sm text-muted-foreground">
|
|
4740
|
-
{`${post.
|
|
4789
|
+
{`${post.event.price} · ${post.event.capacity} seats`}
|
|
4741
4790
|
</p>
|
|
4742
4791
|
</div>
|
|
4743
|
-
<Button>{`
|
|
4792
|
+
<Button>{`Register for ${post.event.price}`}</Button>
|
|
4744
4793
|
</Row>
|
|
4745
4794
|
<Meter
|
|
4746
|
-
value={post.
|
|
4747
|
-
max={post.
|
|
4795
|
+
value={post.event.registered}
|
|
4796
|
+
max={post.event.capacity}
|
|
4748
4797
|
size="sm"
|
|
4749
|
-
getAriaValueText={(_, value) => `${value} of ${post.
|
|
4798
|
+
getAriaValueText={(_, value) => `${value} of ${post.event?.capacity} seats taken`}
|
|
4750
4799
|
>
|
|
4751
4800
|
<Row gap={2} align="center" justify="between" className="mb-2">
|
|
4752
|
-
<MeterLabel>{`${post.
|
|
4753
|
-
<MeterValue>{(_, value) => `${(post.
|
|
4801
|
+
<MeterLabel>{`${post.event.registered} registered`}</MeterLabel>
|
|
4802
|
+
<MeterValue>{(_, value) => `${(post.event?.capacity ?? 0) - value} left`}</MeterValue>
|
|
4754
4803
|
</Row>
|
|
4755
4804
|
<MeterTrack />
|
|
4756
4805
|
</Meter>
|
|
@@ -4791,15 +4840,15 @@ export function FeedPage() {
|
|
|
4791
4840
|
autoResize
|
|
4792
4841
|
rows={2}
|
|
4793
4842
|
aria-label="New post"
|
|
4794
|
-
placeholder="Share a process shot or an upcoming
|
|
4843
|
+
placeholder="Share a process shot or an upcoming event"
|
|
4795
4844
|
/>
|
|
4796
4845
|
<Row gap={1} align="center" justify="between">
|
|
4797
4846
|
<Row gap={0.5}>
|
|
4798
4847
|
<Button variant="ghost" size="icon-sm" aria-label="Add images">
|
|
4799
4848
|
<Icon name="image-stack" />
|
|
4800
4849
|
</Button>
|
|
4801
|
-
<Button variant="ghost" size="icon-sm" aria-label="
|
|
4802
|
-
<Icon name="
|
|
4850
|
+
<Button variant="ghost" size="icon-sm" aria-label="Add an event">
|
|
4851
|
+
<Icon name="calendar" />
|
|
4803
4852
|
</Button>
|
|
4804
4853
|
</Row>
|
|
4805
4854
|
<Button variant="secondary" size="sm">
|
|
@@ -4866,17 +4915,17 @@ export function FeedPage() {
|
|
|
4866
4915
|
</Card>
|
|
4867
4916
|
<Card>
|
|
4868
4917
|
<CardHeader>
|
|
4869
|
-
<CardTitle render={<h2 />}>Upcoming
|
|
4918
|
+
<CardTitle render={<h2 />}>Upcoming events</CardTitle>
|
|
4870
4919
|
</CardHeader>
|
|
4871
4920
|
<CardList>
|
|
4872
|
-
{upcoming.map((
|
|
4921
|
+
{upcoming.map((event) => (
|
|
4873
4922
|
<Entity
|
|
4874
|
-
key={
|
|
4875
|
-
leading={<Avatar variant="art" fallback={
|
|
4876
|
-
title={
|
|
4877
|
-
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}`}
|
|
4878
4927
|
action={
|
|
4879
|
-
<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}`}>
|
|
4880
4929
|
<Icon name="bell" />
|
|
4881
4930
|
</Button>
|
|
4882
4931
|
}
|
|
@@ -5181,7 +5230,7 @@ export function LandingPage() {
|
|
|
5181
5230
|
<CardDescription>Team plan</CardDescription>
|
|
5182
5231
|
</CardHeader>
|
|
5183
5232
|
<CardContent>
|
|
5184
|
-
<Meter value={64} tone="
|
|
5233
|
+
<Meter value={64} tone="3">
|
|
5185
5234
|
<Row justify="between" wrap className="mb-2">
|
|
5186
5235
|
<MeterLabel>Used</MeterLabel>
|
|
5187
5236
|
<MeterValue>{(_, value) => `${(value * 30).toLocaleString("en-US")} of 3,000`}</MeterValue>
|
|
@@ -5464,9 +5513,9 @@ const roleLabel: Record<Role, string> = {
|
|
|
5464
5513
|
|
|
5465
5514
|
const roleSummary: Record<Role, string> = {
|
|
5466
5515
|
owner: "Full access, including payouts and billing. Transfer ownership in studio settings.",
|
|
5467
|
-
admin: "Manages members,
|
|
5468
|
-
editor: "Drafts,
|
|
5469
|
-
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.",
|
|
5470
5519
|
}
|
|
5471
5520
|
|
|
5472
5521
|
type Activity = { id: string; icon: string; text: string; when: string }
|
|
@@ -5481,7 +5530,7 @@ type Member = {
|
|
|
5481
5530
|
handle?: string
|
|
5482
5531
|
joined?: string
|
|
5483
5532
|
lastActive?: string
|
|
5484
|
-
|
|
5533
|
+
projects?: number
|
|
5485
5534
|
invitedBy?: string
|
|
5486
5535
|
sent?: string
|
|
5487
5536
|
expires?: string
|
|
@@ -5494,40 +5543,40 @@ const seats = 10
|
|
|
5494
5543
|
const initialMembers: Member[] = [
|
|
5495
5544
|
{
|
|
5496
5545
|
id: "u_01", name: "Ada Lovelace", handle: "@ada", email: "ada@atelier.art", initials: "AL", role: "owner", pending: false,
|
|
5497
|
-
joined: "Mar 4, 2024", lastActive: "Online now",
|
|
5546
|
+
joined: "Mar 4, 2024", lastActive: "Online now", projects: 12,
|
|
5498
5547
|
activity: [
|
|
5499
|
-
{ id: "a1", icon: "
|
|
5548
|
+
{ id: "a1", icon: "file", text: "Published the Analytical Engine No. 3 roadmap", when: "2 hours ago" },
|
|
5500
5549
|
{ id: "a2", icon: "mail", text: "Invited louise@bourgeois.studio as Editor", when: "2 days ago" },
|
|
5501
5550
|
],
|
|
5502
5551
|
},
|
|
5503
5552
|
{
|
|
5504
5553
|
id: "u_02", name: "Grace Hopper", handle: "@grace", email: "grace@atelier.art", initials: "GH", role: "admin", pending: false,
|
|
5505
|
-
joined: "Mar 9, 2024", lastActive: "20 minutes ago",
|
|
5554
|
+
joined: "Mar 9, 2024", lastActive: "20 minutes ago", projects: 4,
|
|
5506
5555
|
activity: [
|
|
5507
5556
|
{ id: "a1", icon: "user", text: "Changed Frida Kahlo's role to Viewer", when: "Yesterday" },
|
|
5508
|
-
{ id: "a2", icon: "edit", text: "Updated
|
|
5509
|
-
{ 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" },
|
|
5510
5559
|
],
|
|
5511
5560
|
},
|
|
5512
5561
|
{
|
|
5513
5562
|
id: "u_03", name: "Mary Shelley", handle: "@mary", email: "mary@atelier.art", initials: "MS", role: "editor", pending: false,
|
|
5514
|
-
joined: "Jun 18, 2024", lastActive: "3 hours ago",
|
|
5563
|
+
joined: "Jun 18, 2024", lastActive: "3 hours ago", projects: 7,
|
|
5515
5564
|
activity: [
|
|
5516
5565
|
{ id: "a1", icon: "calendar", text: "Scheduled Frankenstein Studies for Oct 14", when: "3 hours ago" },
|
|
5517
|
-
{ id: "a2", icon: "
|
|
5566
|
+
{ id: "a2", icon: "file", text: "Published The Last Man report", when: "Sep 21" },
|
|
5518
5567
|
],
|
|
5519
5568
|
},
|
|
5520
5569
|
{
|
|
5521
5570
|
id: "u_04", name: "Nina Simone", handle: "@nina", email: "nina@atelier.art", initials: "NS", role: "editor", pending: false,
|
|
5522
|
-
joined: "Jan 12, 2025", lastActive: "Yesterday",
|
|
5571
|
+
joined: "Jan 12, 2025", lastActive: "Yesterday", projects: 3,
|
|
5523
5572
|
activity: [
|
|
5524
|
-
{ id: "a1", icon: "upload", text: "Uploaded
|
|
5525
|
-
{ 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" },
|
|
5526
5575
|
],
|
|
5527
5576
|
},
|
|
5528
5577
|
{
|
|
5529
5578
|
id: "u_05", name: "Frida Kahlo", handle: "@frida", email: "frida@atelier.art", initials: "FK", role: "viewer", pending: false,
|
|
5530
|
-
joined: "Jul 2, 2025", lastActive: "4 days ago",
|
|
5579
|
+
joined: "Jul 2, 2025", lastActive: "4 days ago", projects: 0,
|
|
5531
5580
|
activity: [{ id: "a1", icon: "comment", text: "Commented on the Las Dos Fridas draft", when: "4 days ago" }],
|
|
5532
5581
|
},
|
|
5533
5582
|
{
|
|
@@ -5760,8 +5809,8 @@ export function ListDetailPage() {
|
|
|
5760
5809
|
<dd className={selected.lastActive === "Online now" ? "text-success" : undefined}>
|
|
5761
5810
|
{selected.lastActive}
|
|
5762
5811
|
</dd>
|
|
5763
|
-
<dt>
|
|
5764
|
-
<dd>{selected.
|
|
5812
|
+
<dt>Projects owned</dt>
|
|
5813
|
+
<dd>{selected.projects}</dd>
|
|
5765
5814
|
</dl>
|
|
5766
5815
|
)}
|
|
5767
5816
|
</div>
|
|
@@ -5814,7 +5863,7 @@ export function ListDetailPage() {
|
|
|
5814
5863
|
description={
|
|
5815
5864
|
selected.pending
|
|
5816
5865
|
? `${selected.email} won't be able to join with the link they were sent.`
|
|
5817
|
-
: "They lose access to the
|
|
5866
|
+
: "They lose access to the workspace right away. Projects they own stay with the team."
|
|
5818
5867
|
}
|
|
5819
5868
|
confirmLabel={selected.pending ? "Revoke invite" : "Remove member"}
|
|
5820
5869
|
onConfirm={() => remove(selected.id)}
|
|
@@ -5879,17 +5928,17 @@ const timezones = [
|
|
|
5879
5928
|
]
|
|
5880
5929
|
|
|
5881
5930
|
const notifications = [
|
|
5882
|
-
{ name: "
|
|
5883
|
-
{ name: "
|
|
5884
|
-
{ name: "
|
|
5885
|
-
{ 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 },
|
|
5886
5935
|
{ name: "notifyProduct", title: "Product updates", description: "New Atelier features", on: false },
|
|
5887
5936
|
]
|
|
5888
5937
|
|
|
5889
5938
|
const payouts = [
|
|
5890
5939
|
{ label: "Next payout", value: "$1,284.60", note: "Friday, Oct 16" },
|
|
5891
5940
|
{ label: "Pending", value: "$412.00", note: "Clears in 7 days" },
|
|
5892
|
-
{ label: "Paid this year", value: "$18,920.00", note: "Across 214
|
|
5941
|
+
{ label: "Paid this year", value: "$18,920.00", note: "Across 214 invoices" },
|
|
5893
5942
|
]
|
|
5894
5943
|
|
|
5895
5944
|
export function SettingsPage() {
|
|
@@ -5914,8 +5963,8 @@ export function SettingsPage() {
|
|
|
5914
5963
|
bottomNav={
|
|
5915
5964
|
<BottomNav aria-label="Primary">
|
|
5916
5965
|
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel render={<a href="/" />} />
|
|
5917
|
-
<BottomNavItem icon={<Icon name="
|
|
5918
|
-
<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" />} />
|
|
5919
5968
|
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
5920
5969
|
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel active render={<a href="/settings" />} />
|
|
5921
5970
|
</BottomNav>
|
|
@@ -5927,8 +5976,8 @@ export function SettingsPage() {
|
|
|
5927
5976
|
</SidebarHeader>
|
|
5928
5977
|
<SidebarNav>
|
|
5929
5978
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" render={<a href="/" />} />
|
|
5930
|
-
<SidebarItem icon={<Icon name="
|
|
5931
|
-
<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" />} />
|
|
5932
5981
|
<SidebarItem
|
|
5933
5982
|
icon={<Icon name="bell" />}
|
|
5934
5983
|
label="Activity"
|
|
@@ -5982,7 +6031,7 @@ export function SettingsPage() {
|
|
|
5982
6031
|
<Card id="profile" className="scroll-mt-6">
|
|
5983
6032
|
<CardHeader>
|
|
5984
6033
|
<CardTitle render={<h2 />}>Profile</CardTitle>
|
|
5985
|
-
<CardDescription>How
|
|
6034
|
+
<CardDescription>How your team sees you on Atelier.</CardDescription>
|
|
5986
6035
|
</CardHeader>
|
|
5987
6036
|
{/* The one explicit save on the page: profile text is drafted, then
|
|
5988
6037
|
committed. Toggles below apply the moment they change. */}
|
|
@@ -6013,14 +6062,14 @@ export function SettingsPage() {
|
|
|
6013
6062
|
<Input defaultValue="ada" autoComplete="username" spellCheck={false} />
|
|
6014
6063
|
</Field>
|
|
6015
6064
|
</Grid>
|
|
6016
|
-
<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.">
|
|
6017
6066
|
<Textarea
|
|
6018
6067
|
rows={3}
|
|
6019
|
-
defaultValue="
|
|
6068
|
+
defaultValue="Product designer working on tools for small teams. Writing about process every month."
|
|
6020
6069
|
/>
|
|
6021
6070
|
</Field>
|
|
6022
6071
|
<Grid cols={{ base: 1, sm: 2 }} gap={3}>
|
|
6023
|
-
<Field label="Timezone" name="timezone" description="
|
|
6072
|
+
<Field label="Timezone" name="timezone" description="Due dates use this zone.">
|
|
6024
6073
|
<Select defaultValue="europe-london" items={timezones}>
|
|
6025
6074
|
<SelectTrigger />
|
|
6026
6075
|
<SelectContent>
|
|
@@ -6045,7 +6094,7 @@ export function SettingsPage() {
|
|
|
6045
6094
|
<CardHeader>
|
|
6046
6095
|
<CardTitle render={<h2 />}>Notifications</CardTitle>
|
|
6047
6096
|
<CardDescription>
|
|
6048
|
-
Email updates about your
|
|
6097
|
+
Email updates about your projects. Changes save automatically.
|
|
6049
6098
|
</CardDescription>
|
|
6050
6099
|
</CardHeader>
|
|
6051
6100
|
<CardList>
|
|
@@ -6108,7 +6157,7 @@ export function SettingsPage() {
|
|
|
6108
6157
|
<Entity
|
|
6109
6158
|
leading={<Icon name="download" className="text-lg text-muted-foreground" />}
|
|
6110
6159
|
title="Export data"
|
|
6111
|
-
subtitle="
|
|
6160
|
+
subtitle="Projects, invoices, and members"
|
|
6112
6161
|
action={
|
|
6113
6162
|
<Button variant="outline" size="sm">
|
|
6114
6163
|
Export
|
|
@@ -6122,8 +6171,8 @@ export function SettingsPage() {
|
|
|
6122
6171
|
<CardHeader>
|
|
6123
6172
|
<CardTitle render={<h2 />}>Delete account</CardTitle>
|
|
6124
6173
|
<CardDescription>
|
|
6125
|
-
Permanently remove your profile,
|
|
6126
|
-
|
|
6174
|
+
Permanently remove your profile, projects, and billing history. Files you shared
|
|
6175
|
+
stay with your team.
|
|
6127
6176
|
</CardDescription>
|
|
6128
6177
|
</CardHeader>
|
|
6129
6178
|
<CardFooter className="justify-end">
|
|
@@ -6140,7 +6189,7 @@ export function SettingsPage() {
|
|
|
6140
6189
|
open={deleteOpen}
|
|
6141
6190
|
onOpenChange={setDeleteOpen}
|
|
6142
6191
|
title="Delete your account?"
|
|
6143
|
-
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."
|
|
6144
6193
|
confirmLabel="Delete account"
|
|
6145
6194
|
confirmingLabel="Deleting…"
|
|
6146
6195
|
confirmText="ada"
|
|
@@ -6417,7 +6466,7 @@ A delete-account card: what deleting removes and what survives, one destructive
|
|
|
6417
6466
|
|
|
6418
6467
|
- Copied from the SettingsPage page shell's Delete account section.
|
|
6419
6468
|
- The button never acts directly: it opens the modal, whose confirm stays disabled until the handle is typed.
|
|
6420
|
-
- 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.
|
|
6421
6470
|
|
|
6422
6471
|
## Accessibility
|
|
6423
6472
|
|
|
@@ -6430,7 +6479,7 @@ A delete-account card: what deleting removes and what survives, one destructive
|
|
|
6430
6479
|
import { useState } from "react"
|
|
6431
6480
|
import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, cn, DestructiveActionModal } from "@sableui/react"
|
|
6432
6481
|
|
|
6433
|
-
const account = { handle: "ada",
|
|
6482
|
+
const account = { handle: "ada", projects: 12 }
|
|
6434
6483
|
|
|
6435
6484
|
export interface DangerZoneBlockProps {
|
|
6436
6485
|
className?: string
|
|
@@ -6448,7 +6497,7 @@ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZo
|
|
|
6448
6497
|
<CardHeader>
|
|
6449
6498
|
<CardTitle render={<h2 />}>Delete account</CardTitle>
|
|
6450
6499
|
<CardDescription>
|
|
6451
|
-
Permanently remove your profile,
|
|
6500
|
+
Permanently remove your profile, projects, and billing history. Files you shared stay with your team.
|
|
6452
6501
|
</CardDescription>
|
|
6453
6502
|
</CardHeader>
|
|
6454
6503
|
<CardFooter actions className="justify-end">
|
|
@@ -6461,7 +6510,7 @@ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZo
|
|
|
6461
6510
|
open={open}
|
|
6462
6511
|
onOpenChange={setOpen}
|
|
6463
6512
|
title="Delete your account?"
|
|
6464
|
-
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.`}
|
|
6465
6514
|
confirmLabel="Delete account"
|
|
6466
6515
|
confirmingLabel="Deleting…"
|
|
6467
6516
|
confirmText={account.handle}
|
|
@@ -6690,10 +6739,10 @@ const intro = {
|
|
|
6690
6739
|
}
|
|
6691
6740
|
|
|
6692
6741
|
const questions = [
|
|
6693
|
-
{ 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." },
|
|
6694
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." },
|
|
6695
|
-
{ q: "Who owns the
|
|
6696
|
-
{ 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." },
|
|
6697
6746
|
]
|
|
6698
6747
|
|
|
6699
6748
|
export interface FaqBlockProps {
|
|
@@ -6906,7 +6955,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
|
|
|
6906
6955
|
|
|
6907
6956
|
const goals = [
|
|
6908
6957
|
{ goal: "Releases", progress: 82 },
|
|
6909
|
-
{ goal: "
|
|
6958
|
+
{ goal: "Customers", progress: 64 },
|
|
6910
6959
|
{ goal: "Revenue", progress: 91 },
|
|
6911
6960
|
]
|
|
6912
6961
|
|
|
@@ -7470,7 +7519,7 @@ An activity list: Entity rows with the actor's Avatar, who did what, and a compa
|
|
|
7470
7519
|
import { Avatar, Card, CardList, cn, Entity, StatusDot } from "@sableui/react"
|
|
7471
7520
|
|
|
7472
7521
|
const notifications = [
|
|
7473
|
-
{ initials: "MO", name: "Mara Okafor", what: "
|
|
7522
|
+
{ initials: "MO", name: "Mara Okafor", what: "invited you to Q3 planning", when: "2m", unread: true },
|
|
7474
7523
|
{ initials: "SR", name: "Sam Rivera", what: "left a comment on Dusk", when: "1h", unread: false },
|
|
7475
7524
|
]
|
|
7476
7525
|
|
|
@@ -7747,32 +7796,32 @@ const tiers = [
|
|
|
7747
7796
|
{
|
|
7748
7797
|
id: "starter",
|
|
7749
7798
|
name: "Starter",
|
|
7750
|
-
blurb: "For one person
|
|
7799
|
+
blurb: "For one person getting a first project going.",
|
|
7751
7800
|
monthly: 0,
|
|
7752
7801
|
yearly: 0,
|
|
7753
7802
|
cta: "Start for free",
|
|
7754
7803
|
featured: false,
|
|
7755
|
-
features: ["3
|
|
7804
|
+
features: ["3 active projects", "Shared docs", "Community support"],
|
|
7756
7805
|
},
|
|
7757
7806
|
{
|
|
7758
7807
|
id: "studio",
|
|
7759
7808
|
name: "Studio",
|
|
7760
|
-
blurb: "For
|
|
7809
|
+
blurb: "For small teams shipping on a steady schedule.",
|
|
7761
7810
|
monthly: 20,
|
|
7762
7811
|
yearly: 16,
|
|
7763
7812
|
cta: "Start free trial",
|
|
7764
7813
|
featured: true,
|
|
7765
|
-
features: ["Unlimited
|
|
7814
|
+
features: ["Unlimited projects", "Scheduled reports", "Usage analytics", "Priority support"],
|
|
7766
7815
|
},
|
|
7767
7816
|
{
|
|
7768
|
-
id: "
|
|
7769
|
-
name: "
|
|
7770
|
-
blurb: "For
|
|
7817
|
+
id: "business",
|
|
7818
|
+
name: "Business",
|
|
7819
|
+
blurb: "For companies running several teams at once.",
|
|
7771
7820
|
monthly: 48,
|
|
7772
7821
|
yearly: 40,
|
|
7773
7822
|
cta: "Talk to sales",
|
|
7774
7823
|
featured: false,
|
|
7775
|
-
features: ["Everything in Studio", "Up to 10 seats", "
|
|
7824
|
+
features: ["Everything in Studio", "Up to 10 seats", "SSO and audit log", "Roles and approvals"],
|
|
7776
7825
|
},
|
|
7777
7826
|
]
|
|
7778
7827
|
|
|
@@ -7792,10 +7841,10 @@ export function PricingTableBlock({ className, onChoose }: PricingTableBlockProp
|
|
|
7792
7841
|
<Stack gap={10}>
|
|
7793
7842
|
<Stack gap={4} align="center" className="text-center">
|
|
7794
7843
|
<h2 id={titleId} className="text-balance text-heading-1">
|
|
7795
|
-
Pricing that grows with your
|
|
7844
|
+
Pricing that grows with your team
|
|
7796
7845
|
</h2>
|
|
7797
7846
|
<p className="max-w-(--measure-prose) text-body-lg text-muted-foreground">
|
|
7798
|
-
Every plan includes unlimited
|
|
7847
|
+
Every plan includes unlimited guests, and you can switch plans at any time.
|
|
7799
7848
|
</p>
|
|
7800
7849
|
<ToggleGroup value={[period]} onValueChange={(v: unknown[]) => v[0] && setPeriod(v[0] as Period)} aria-label="Billing period">
|
|
7801
7850
|
<Toggle value="monthly">Monthly</Toggle>
|
|
@@ -7807,14 +7856,14 @@ export function PricingTableBlock({ className, onChoose }: PricingTableBlockProp
|
|
|
7807
7856
|
<Card
|
|
7808
7857
|
key={tier.id}
|
|
7809
7858
|
render={<article />}
|
|
7810
|
-
className={cn("flex flex-col", tier.featured && "
|
|
7859
|
+
className={cn("flex flex-col", tier.featured && "outline-primary ring-1 ring-primary")}
|
|
7811
7860
|
>
|
|
7812
7861
|
<CardHeader>
|
|
7813
7862
|
<CardTitle render={<h3 />}>{tier.name}</CardTitle>
|
|
7814
7863
|
<CardDescription>{tier.blurb}</CardDescription>
|
|
7815
7864
|
{tier.featured ? (
|
|
7816
7865
|
<CardAction>
|
|
7817
|
-
<Badge tone="
|
|
7866
|
+
<Badge tone="1" size="sm">
|
|
7818
7867
|
Most popular
|
|
7819
7868
|
</Badge>
|
|
7820
7869
|
</CardAction>
|
|
@@ -7852,15 +7901,15 @@ export function PricingTableBlock({ className, onChoose }: PricingTableBlockProp
|
|
|
7852
7901
|
|
|
7853
7902
|
# ProductCardBlock
|
|
7854
7903
|
|
|
7855
|
-
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.
|
|
7856
7905
|
|
|
7857
|
-
**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.
|
|
7858
7907
|
|
|
7859
7908
|
**Block:** Commerce · card · designed for 16rem–20rem wide (fills its container up to the max)
|
|
7860
7909
|
|
|
7861
7910
|
## Notes
|
|
7862
7911
|
|
|
7863
|
-
- The
|
|
7912
|
+
- The image runs to the card's edges; the card clips it to its own corners (overflow-hidden).
|
|
7864
7913
|
- Prices are `text-mono-lg` — the metric mono tier with tabular figures — never a heading tier.
|
|
7865
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.
|
|
7866
7915
|
|
|
@@ -7875,7 +7924,7 @@ A product card: edge-to-edge artwork, title and maker, an edition Badge, and the
|
|
|
7875
7924
|
import { AspectBox, Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn, MediaImage } from "@sableui/react"
|
|
7876
7925
|
|
|
7877
7926
|
// `image` is the product shot URL; left empty, it paints art seeded by the title.
|
|
7878
|
-
const product = { title: "
|
|
7927
|
+
const product = { title: "Linen throw blanket", maker: "Northfield Home", image: "", badge: "New", price: "$64.00" }
|
|
7879
7928
|
|
|
7880
7929
|
|
|
7881
7930
|
export interface ProductCardBlockProps {
|
|
@@ -7896,12 +7945,12 @@ export function ProductCardBlock({ className, framed = true, onCollect }: Produc
|
|
|
7896
7945
|
<CardTitle>{product.title}</CardTitle>
|
|
7897
7946
|
<CardDescription>{product.maker}</CardDescription>
|
|
7898
7947
|
<CardAction>
|
|
7899
|
-
<Badge tone="
|
|
7948
|
+
<Badge tone="1">{product.badge}</Badge>
|
|
7900
7949
|
</CardAction>
|
|
7901
7950
|
</CardHeader>
|
|
7902
7951
|
<CardFooter className="justify-between pt-4">
|
|
7903
7952
|
<span className="text-mono-lg">{product.price}</span>
|
|
7904
|
-
<Button onClick={onCollect}>
|
|
7953
|
+
<Button onClick={onCollect}>Add to cart</Button>
|
|
7905
7954
|
</CardFooter>
|
|
7906
7955
|
</Card>
|
|
7907
7956
|
)
|
|
@@ -7939,7 +7988,7 @@ const profile = {
|
|
|
7939
7988
|
name: "Juno Avery",
|
|
7940
7989
|
initials: "JA",
|
|
7941
7990
|
handle: "@juno · Sound and light",
|
|
7942
|
-
bio: "Field recordings, slowed down. New
|
|
7991
|
+
bio: "Field recordings, slowed down. New track every full moon.",
|
|
7943
7992
|
stats: [
|
|
7944
7993
|
{ value: "128", label: "posts" },
|
|
7945
7994
|
{ value: "4.2k", label: "followers" },
|
|
@@ -8031,7 +8080,7 @@ const profile = {
|
|
|
8031
8080
|
name: "Ada Lovelace",
|
|
8032
8081
|
initials: "AL",
|
|
8033
8082
|
handle: "ada",
|
|
8034
|
-
bio: "
|
|
8083
|
+
bio: "Product designer working on tools for small teams. Writing about process every month.",
|
|
8035
8084
|
timezone: "europe-london",
|
|
8036
8085
|
}
|
|
8037
8086
|
|
|
@@ -8056,7 +8105,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
|
|
|
8056
8105
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-2xl", className)}>
|
|
8057
8106
|
<CardHeader>
|
|
8058
8107
|
<CardTitle render={<h2 />}>Profile</CardTitle>
|
|
8059
|
-
<CardDescription>How
|
|
8108
|
+
<CardDescription>How your team sees you.</CardDescription>
|
|
8060
8109
|
</CardHeader>
|
|
8061
8110
|
{/* The one explicit save: profile text is drafted, then committed. */}
|
|
8062
8111
|
<Form
|
|
@@ -8090,7 +8139,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
|
|
|
8090
8139
|
<Input defaultValue={profile.handle} autoComplete="username" spellCheck={false} />
|
|
8091
8140
|
</Field>
|
|
8092
8141
|
</Grid>
|
|
8093
|
-
<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.">
|
|
8094
8143
|
<Textarea rows={3} defaultValue={profile.bio} />
|
|
8095
8144
|
</Field>
|
|
8096
8145
|
<Grid cols={{ base: 1, sm: 2 }} gap={3}>
|
|
@@ -8147,10 +8196,10 @@ import { Badge, Card, CardAction, CardContent, CardDescription, CardHeader, cn,
|
|
|
8147
8196
|
const revenue = { label: "Revenue this month", total: "$12,480", change: "+8.2%" }
|
|
8148
8197
|
|
|
8149
8198
|
const segments = [
|
|
8150
|
-
{ label: "
|
|
8151
|
-
{ label: "
|
|
8152
|
-
{ label: "
|
|
8153
|
-
{ 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" },
|
|
8154
8203
|
] as const
|
|
8155
8204
|
|
|
8156
8205
|
export interface RevenueBreakdownBlockProps {
|
|
@@ -8172,7 +8221,7 @@ export function RevenueBreakdownBlock({ className, framed = true }: RevenueBreak
|
|
|
8172
8221
|
</CardHeader>
|
|
8173
8222
|
<CardContent className="flex flex-col gap-4 pt-2">
|
|
8174
8223
|
<span className="text-mono-xl">{revenue.total}</span>
|
|
8175
|
-
<SegmentedBar label="Revenue by
|
|
8224
|
+
<SegmentedBar label="Revenue by channel" segments={[...segments]} size="lg" />
|
|
8176
8225
|
{/* Two columns while each entry fits whole, one when they would not:
|
|
8177
8226
|
sized in em, so a larger type or a narrower card drops to one.
|
|
8178
8227
|
aria-hidden: the bar already reads every share aloud. */}
|
|
@@ -8467,9 +8516,9 @@ import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, G
|
|
|
8467
8516
|
|
|
8468
8517
|
const stats = [
|
|
8469
8518
|
{ label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
|
|
8470
|
-
{ label: "
|
|
8471
|
-
{ label: "
|
|
8472
|
-
{ 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" },
|
|
8473
8522
|
]
|
|
8474
8523
|
|
|
8475
8524
|
export interface StatRowBlockProps {
|
|
@@ -8604,9 +8653,9 @@ import { useState, useId } from "react"
|
|
|
8604
8653
|
import { Badge, Button, Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle, Checkbox, cn, Icon, Label } from "@sableui/react"
|
|
8605
8654
|
|
|
8606
8655
|
const tasks = [
|
|
8607
|
-
{ title: "Pick the cover type", tag: "Design", tone: "
|
|
8656
|
+
{ title: "Pick the cover type", tag: "Design", tone: "2", done: true },
|
|
8608
8657
|
{ title: "Wire up checkout", tag: "Engineering", tone: "info", done: false },
|
|
8609
|
-
{ title: "Draft launch post", tag: "Launch", tone: "
|
|
8658
|
+
{ title: "Draft launch post", tag: "Launch", tone: "1", done: false },
|
|
8610
8659
|
] as const
|
|
8611
8660
|
|
|
8612
8661
|
export interface TaskListBlockProps {
|
|
@@ -8723,7 +8772,7 @@ export function TeamMembersBlock({ className, framed = true, onInvite, onMemberA
|
|
|
8723
8772
|
subtitle={m.email}
|
|
8724
8773
|
action={
|
|
8725
8774
|
<div className="flex items-center gap-2">
|
|
8726
|
-
<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">
|
|
8727
8776
|
{m.role}
|
|
8728
8777
|
</Badge>
|
|
8729
8778
|
{/* The owner is not managed from here; a placeholder keeps the column aligned. */}
|
|
@@ -9138,7 +9187,7 @@ export function VerifyCodeBlock({ className, framed = true, onVerify, onResend }
|
|
|
9138
9187
|
|
|
9139
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.
|
|
9140
9189
|
|
|
9141
|
-
**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.
|
|
9142
9191
|
|
|
9143
9192
|
**Block:** Commerce · card · designed for 18rem–20rem wide (fills its container up to the max)
|
|
9144
9193
|
|
|
@@ -9146,24 +9195,24 @@ A balance card: the balance in the metric mono tier, Send and Receive as an equa
|
|
|
9146
9195
|
|
|
9147
9196
|
- The balance is CardTitle at `text-mono-xl` — tabular figures, never a heading tier — under its label as CardDescription.
|
|
9148
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.
|
|
9149
|
-
- 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.
|
|
9150
9199
|
|
|
9151
9200
|
## Accessibility
|
|
9152
9201
|
|
|
9153
9202
|
- Each activity row's leading swatch is aria-hidden; the title and detail carry the meaning.
|
|
9154
|
-
- 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.
|
|
9155
9204
|
|
|
9156
9205
|
## Source
|
|
9157
9206
|
|
|
9158
9207
|
```tsx
|
|
9159
9208
|
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Entity, Icon } from "@sableui/react"
|
|
9160
9209
|
|
|
9161
|
-
const balance = "
|
|
9210
|
+
const balance = "$2,418.50"
|
|
9162
9211
|
|
|
9163
9212
|
const activity = [
|
|
9164
|
-
{ title: "
|
|
9165
|
-
{ title: "
|
|
9166
|
-
{ 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" },
|
|
9167
9216
|
]
|
|
9168
9217
|
|
|
9169
9218
|
export interface WalletBlockProps {
|
|
@@ -9273,7 +9322,7 @@ import { PageHeader, Region } from "@sableui/react"
|
|
|
9273
9322
|
```tsx
|
|
9274
9323
|
import { Card, CardHeader, CardTitle, Grid, Region } from "@sableui/react"
|
|
9275
9324
|
|
|
9276
|
-
<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>
|
|
9277
9326
|
```
|
|
9278
9327
|
|
|
9279
9328
|
### Fluid workspace: the queue's table takes the extra width
|
|
@@ -9323,7 +9372,7 @@ Use Page as a route content shell that combines Region sizing with vertical rhyt
|
|
|
9323
9372
|
```tsx
|
|
9324
9373
|
import { Card, CardContent, Page, PageHeader } from "@sableui/react"
|
|
9325
9374
|
|
|
9326
|
-
<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>
|
|
9327
9376
|
```
|
|
9328
9377
|
|
|
9329
9378
|
---
|
|
@@ -9394,7 +9443,7 @@ import { Card, CardDescription, CardHeader, Grid } from "@sableui/react"
|
|
|
9394
9443
|
```tsx
|
|
9395
9444
|
import { Badge, Grid } from "@sableui/react"
|
|
9396
9445
|
|
|
9397
|
-
<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>
|
|
9398
9447
|
```
|
|
9399
9448
|
|
|
9400
9449
|
---
|
|
@@ -9733,7 +9782,7 @@ Use SplitPane for responsive two-pane layouts where a primary pane and supportin
|
|
|
9733
9782
|
```tsx
|
|
9734
9783
|
import { AspectBox, Button, SplitPane, Stack } from "@sableui/react"
|
|
9735
9784
|
|
|
9736
|
-
<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>} />
|
|
9737
9786
|
```
|
|
9738
9787
|
|
|
9739
9788
|
---
|
|
@@ -9896,7 +9945,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
|
|
|
9896
9945
|
```tsx
|
|
9897
9946
|
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sableui/react"
|
|
9898
9947
|
|
|
9899
|
-
<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>
|
|
9900
9949
|
```
|
|
9901
9950
|
|
|
9902
9951
|
### Multiple open
|
|
@@ -9912,7 +9961,7 @@ import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sab
|
|
|
9912
9961
|
```tsx
|
|
9913
9962
|
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Icon } from "@sableui/react"
|
|
9914
9963
|
|
|
9915
|
-
<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>
|
|
9916
9965
|
```
|
|
9917
9966
|
|
|
9918
9967
|
---
|
|
@@ -9942,7 +9991,7 @@ Use AppHeader for compact application chrome at the top of a screen, usually wit
|
|
|
9942
9991
|
| --- | --- | --- | --- |
|
|
9943
9992
|
| leading | ReactNode | — | Leading slot — back button, create action, or menu trigger. |
|
|
9944
9993
|
| title | ReactNode | — | Center slot — brand wordmark or contextual page title. |
|
|
9945
|
-
| actions | ReactNode | — | Trailing slot —
|
|
9994
|
+
| actions | ReactNode | — | Trailing slot — account menu, action buttons. |
|
|
9946
9995
|
|
|
9947
9996
|
## Tokens used
|
|
9948
9997
|
|
|
@@ -10064,7 +10113,7 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
|
|
|
10064
10113
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
10065
10114
|
|
|
10066
10115
|
<AppShell
|
|
10067
|
-
banners={<Banner tone="warning">
|
|
10116
|
+
banners={<Banner tone="warning">Sync is delayed — changes may be slow to appear.</Banner>}
|
|
10068
10117
|
header={<AppHeader title="Studio" />}
|
|
10069
10118
|
sidebar={
|
|
10070
10119
|
<Sidebar>
|
|
@@ -10083,7 +10132,7 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
|
|
|
10083
10132
|
}
|
|
10084
10133
|
>
|
|
10085
10134
|
<Region max="feed">
|
|
10086
|
-
<PageHeader title="Home" description="Latest posts from
|
|
10135
|
+
<PageHeader title="Home" description="Latest posts from people you follow." />
|
|
10087
10136
|
</Region>
|
|
10088
10137
|
</AppShell>
|
|
10089
10138
|
```
|
|
@@ -10185,7 +10234,7 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
10185
10234
|
|
|
10186
10235
|
## Tokens used
|
|
10187
10236
|
|
|
10188
|
-
`tone-
|
|
10237
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
10189
10238
|
|
|
10190
10239
|
## Motion
|
|
10191
10240
|
|
|
@@ -10544,7 +10593,7 @@ import { Avatar, Entity, StatusDot } from "@sableui/react"
|
|
|
10544
10593
|
|
|
10545
10594
|
**Status:** stable
|
|
10546
10595
|
|
|
10547
|
-
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.
|
|
10548
10597
|
|
|
10549
10598
|
**Keywords:** badge, tag, label, status, metadata, chip, pill, tone, category
|
|
10550
10599
|
|
|
@@ -10555,7 +10604,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10555
10604
|
### Best practices
|
|
10556
10605
|
|
|
10557
10606
|
- Do: Keep badge text short: one to three words or a compact count.
|
|
10558
|
-
- 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.
|
|
10559
10608
|
- Do: Pair icons with text; the icon reinforces meaning but should not be the only cue.
|
|
10560
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.
|
|
10561
10610
|
- Don't: Use a Badge as a button. If it triggers an action, use Button, Toggle, or a menu item.
|
|
@@ -10574,7 +10623,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10574
10623
|
| Name | Type | Default | Description |
|
|
10575
10624
|
| --- | --- | --- | --- |
|
|
10576
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. |
|
|
10577
|
-
| 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. |
|
|
10578
10627
|
| size | "sm" \| "md" | "md" | "sm" uses tighter padding + text-label-sm; "md" is the default look. |
|
|
10579
10628
|
| icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. Ignored when dot is set. |
|
|
10580
10629
|
| dot | boolean | false | A leading status dot in the badge's tone. With appearance="plain" it is a status line (● Ready). |
|
|
@@ -10583,12 +10632,12 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10583
10632
|
## Variants
|
|
10584
10633
|
|
|
10585
10634
|
- **appearance**: soft, outline, solid, overlay, plain
|
|
10586
|
-
- **tone**: neutral,
|
|
10635
|
+
- **tone**: neutral, 1, 2, 3, info, success, warning, destructive, red, orange, yellow, green, teal, blue, purple, pink
|
|
10587
10636
|
- **size**: sm, md
|
|
10588
10637
|
|
|
10589
10638
|
## Tokens used
|
|
10590
10639
|
|
|
10591
|
-
`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`
|
|
10592
10641
|
|
|
10593
10642
|
## Accessibility
|
|
10594
10643
|
|
|
@@ -10607,7 +10656,15 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
10607
10656
|
```tsx
|
|
10608
10657
|
import { Badge, Row } from "@sableui/react"
|
|
10609
10658
|
|
|
10610
|
-
<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>
|
|
10611
10668
|
```
|
|
10612
10669
|
|
|
10613
10670
|
### Solid
|
|
@@ -10647,7 +10704,7 @@ import { Badge } from "@sableui/react"
|
|
|
10647
10704
|
```tsx
|
|
10648
10705
|
import { Badge, Icon, Row } from "@sableui/react"
|
|
10649
10706
|
|
|
10650
|
-
<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>
|
|
10651
10708
|
```
|
|
10652
10709
|
|
|
10653
10710
|
### Semantic state
|
|
@@ -10664,7 +10721,7 @@ import { Badge, Row } from "@sableui/react"
|
|
|
10664
10721
|
|
|
10665
10722
|
**Status:** stable
|
|
10666
10723
|
|
|
10667
|
-
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.
|
|
10668
10725
|
|
|
10669
10726
|
**Keywords:** banner, announcement, alert, status, message, warning, success, offline, sticky
|
|
10670
10727
|
|
|
@@ -10693,7 +10750,7 @@ Use Banner for page-level or app-level messages that affect the current experien
|
|
|
10693
10750
|
|
|
10694
10751
|
| Name | Type | Default | Description |
|
|
10695
10752
|
| --- | --- | --- | --- |
|
|
10696
|
-
| 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). |
|
|
10697
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). |
|
|
10698
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. |
|
|
10699
10756
|
| action | ReactNode | — | A trailing action (e.g. a Button). Switches the bar to leading text + trailing action instead of centered text. |
|
|
@@ -10704,12 +10761,12 @@ Use Banner for page-level or app-level messages that affect the current experien
|
|
|
10704
10761
|
|
|
10705
10762
|
## Variants
|
|
10706
10763
|
|
|
10707
|
-
- **tone**: neutral, info, success, warning,
|
|
10764
|
+
- **tone**: neutral, info, success, warning, 1, destructive
|
|
10708
10765
|
- **appearance**: soft, solid
|
|
10709
10766
|
|
|
10710
10767
|
## Tokens used
|
|
10711
10768
|
|
|
10712
|
-
`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`
|
|
10713
10770
|
|
|
10714
10771
|
## Accessibility
|
|
10715
10772
|
|
|
@@ -10737,7 +10794,7 @@ import { Banner } from "@sableui/react"
|
|
|
10737
10794
|
```tsx
|
|
10738
10795
|
import { Banner } from "@sableui/react"
|
|
10739
10796
|
|
|
10740
|
-
<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>
|
|
10741
10798
|
```
|
|
10742
10799
|
|
|
10743
10800
|
### Success
|
|
@@ -10769,7 +10826,7 @@ import { Banner } from "@sableui/react"
|
|
|
10769
10826
|
```tsx
|
|
10770
10827
|
import { Banner, Button } from "@sableui/react"
|
|
10771
10828
|
|
|
10772
|
-
<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>
|
|
10773
10830
|
```
|
|
10774
10831
|
|
|
10775
10832
|
### App-shell outage
|
|
@@ -10826,13 +10883,13 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
10826
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. |
|
|
10827
10884
|
| labels | boolean \| "inside" | false | Print each bar's value at its end, or (`"inside"`, horizontal) the category name inside the bar as well. |
|
|
10828
10885
|
| colorBy | "series" \| "index" | "series" | Colour by series, or give each category its own tone (one series). |
|
|
10829
|
-
| negativeTone | "
|
|
10886
|
+
| negativeTone | "1" \| "2" \| "3" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | — | Colour negative values with this tone. |
|
|
10830
10887
|
| radius | number | 4 | Corner radius of each bar's far end, in px. Stacks round only the outer segment. |
|
|
10831
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. |
|
|
10832
10889
|
|
|
10833
10890
|
## Tokens used
|
|
10834
10891
|
|
|
10835
|
-
`tone-
|
|
10892
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
10836
10893
|
|
|
10837
10894
|
## Motion
|
|
10838
10895
|
|
|
@@ -11064,7 +11121,7 @@ Use Breadcrumbs to show the user's location inside a hierarchy and provide light
|
|
|
11064
11121
|
```tsx
|
|
11065
11122
|
import { BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, Breadcrumbs, BreadcrumbSeparator } from "@sableui/react"
|
|
11066
11123
|
|
|
11067
|
-
<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>
|
|
11068
11125
|
```
|
|
11069
11126
|
|
|
11070
11127
|
### Custom separator
|
|
@@ -11359,7 +11416,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
11359
11416
|
```tsx
|
|
11360
11417
|
import { Button } from "@sableui/react"
|
|
11361
11418
|
|
|
11362
|
-
<Button>
|
|
11419
|
+
<Button>Save changes</Button>
|
|
11363
11420
|
```
|
|
11364
11421
|
|
|
11365
11422
|
### Secondary + ghost
|
|
@@ -11416,7 +11473,7 @@ import { Button, Row } from "@sableui/react"
|
|
|
11416
11473
|
```tsx
|
|
11417
11474
|
import { Button, Row } from "@sableui/react"
|
|
11418
11475
|
|
|
11419
|
-
<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>
|
|
11420
11477
|
```
|
|
11421
11478
|
|
|
11422
11479
|
### Leading icon
|
|
@@ -11499,7 +11556,7 @@ import { Button } from "@sableui/react"
|
|
|
11499
11556
|
```tsx
|
|
11500
11557
|
import { Banner, Button } from "@sableui/react"
|
|
11501
11558
|
|
|
11502
|
-
<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>
|
|
11503
11560
|
```
|
|
11504
11561
|
|
|
11505
11562
|
---
|
|
@@ -11667,7 +11724,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
|
|
|
11667
11724
|
```tsx
|
|
11668
11725
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sableui/react"
|
|
11669
11726
|
|
|
11670
|
-
<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>
|
|
11671
11728
|
```
|
|
11672
11729
|
|
|
11673
11730
|
### With a header action
|
|
@@ -11675,7 +11732,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sabl
|
|
|
11675
11732
|
```tsx
|
|
11676
11733
|
import { Button, Card, CardAction, CardContent, CardHeader, CardTitle, Icon } from "@sableui/react"
|
|
11677
11734
|
|
|
11678
|
-
<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>
|
|
11679
11736
|
```
|
|
11680
11737
|
|
|
11681
11738
|
### Compact section panel
|
|
@@ -11691,7 +11748,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@sableui/react"
|
|
|
11691
11748
|
```tsx
|
|
11692
11749
|
import { Badge, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@sableui/react"
|
|
11693
11750
|
|
|
11694
|
-
<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>
|
|
11695
11752
|
```
|
|
11696
11753
|
|
|
11697
11754
|
### Whole card as a link
|
|
@@ -11707,7 +11764,7 @@ import { Card, CardContent, CardDescription, CardTitle, Icon } from "@sableui/re
|
|
|
11707
11764
|
```tsx
|
|
11708
11765
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sableui/react"
|
|
11709
11766
|
|
|
11710
|
-
<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>
|
|
11711
11768
|
```
|
|
11712
11769
|
|
|
11713
11770
|
### List card
|
|
@@ -12075,17 +12132,17 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
|
|
|
12075
12132
|
| title | ReactNode | — | The card's primary label (text-title-sm). |
|
|
12076
12133
|
| description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
|
|
12077
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. |
|
|
12078
|
-
| tone | "neutral" \| "
|
|
12135
|
+
| tone | "neutral" \| "3" \| "2" \| "1" \| "info" \| "warning" | "neutral" | Semantic colour the checked state carries across the border, checkbox box, and icon. |
|
|
12079
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. |
|
|
12080
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. |
|
|
12081
12138
|
|
|
12082
12139
|
## Variants
|
|
12083
12140
|
|
|
12084
|
-
- **tone**: neutral,
|
|
12141
|
+
- **tone**: neutral, 3, 2, 1, info, warning
|
|
12085
12142
|
|
|
12086
12143
|
## Tokens used
|
|
12087
12144
|
|
|
12088
|
-
`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`
|
|
12089
12146
|
|
|
12090
12147
|
## Accessibility
|
|
12091
12148
|
|
|
@@ -12121,7 +12178,7 @@ import { Checkbox, CheckboxCard, CheckboxGroup, Field } from "@sableui/react"
|
|
|
12121
12178
|
```tsx
|
|
12122
12179
|
import { CheckboxCard } from "@sableui/react"
|
|
12123
12180
|
|
|
12124
|
-
<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} />
|
|
12125
12182
|
```
|
|
12126
12183
|
|
|
12127
12184
|
### Toned with description
|
|
@@ -12129,7 +12186,7 @@ import { CheckboxCard } from "@sableui/react"
|
|
|
12129
12186
|
```tsx
|
|
12130
12187
|
import { CheckboxCard } from "@sableui/react"
|
|
12131
12188
|
|
|
12132
|
-
<CheckboxCard tone="
|
|
12189
|
+
<CheckboxCard tone="1" title="Reports" description="Weekly usage reports by email" defaultChecked />
|
|
12133
12190
|
```
|
|
12134
12191
|
|
|
12135
12192
|
---
|
|
@@ -12215,7 +12272,7 @@ Use CodeBlock for multiline code samples that users may read, inspect, or copy.
|
|
|
12215
12272
|
|
|
12216
12273
|
- Do: Give the snippet a filename, or a sentence of surrounding copy, when it helps readers and agents place it.
|
|
12217
12274
|
- Do: Keep examples complete enough to teach the intended pattern.
|
|
12218
|
-
- 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.
|
|
12219
12276
|
- Don't: Do not put short inline identifiers in a CodeBlock; use Code for those.
|
|
12220
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.
|
|
12221
12278
|
|
|
@@ -12708,7 +12765,7 @@ import { CommandMenu, CommandMenuGroup, CommandMenuItem } from "@sableui/react"
|
|
|
12708
12765
|
```tsx
|
|
12709
12766
|
import { CommandMenu, CommandMenuGroup, CommandMenuItem } from "@sableui/react"
|
|
12710
12767
|
|
|
12711
|
-
<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>
|
|
12712
12769
|
```
|
|
12713
12770
|
|
|
12714
12771
|
### Consumer-owned ranking
|
|
@@ -12759,7 +12816,7 @@ useCommandMenuShortcut(() => setOpen((value) => !value))
|
|
|
12759
12816
|
|
|
12760
12817
|
**Status:** stable
|
|
12761
12818
|
|
|
12762
|
-
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.
|
|
12763
12820
|
|
|
12764
12821
|
**Keywords:** context card, rich tooltip, hover card, preview
|
|
12765
12822
|
|
|
@@ -12818,7 +12875,7 @@ Use ContextCard to show richer contextual content near a trigger without taking
|
|
|
12818
12875
|
```tsx
|
|
12819
12876
|
import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger, Entity } from "@sableui/react"
|
|
12820
12877
|
|
|
12821
|
-
<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>
|
|
12822
12879
|
```
|
|
12823
12880
|
|
|
12824
12881
|
### With stat row
|
|
@@ -13304,7 +13361,7 @@ import { DestructiveActionModal } from "@sableui/react"
|
|
|
13304
13361
|
```tsx
|
|
13305
13362
|
import { DestructiveActionModal, Note } from "@sableui/react"
|
|
13306
13363
|
|
|
13307
|
-
<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>
|
|
13308
13365
|
```
|
|
13309
13366
|
|
|
13310
13367
|
---
|
|
@@ -13418,7 +13475,7 @@ import { Button, Carousel, Dialog, DialogContent, DialogTrigger } from "@sableui
|
|
|
13418
13475
|
```tsx
|
|
13419
13476
|
import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@sableui/react"
|
|
13420
13477
|
|
|
13421
|
-
<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>
|
|
13422
13479
|
```
|
|
13423
13480
|
|
|
13424
13481
|
### Settings form
|
|
@@ -13434,7 +13491,7 @@ import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFo
|
|
|
13434
13491
|
```tsx
|
|
13435
13492
|
import { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button } from "@sableui/react"
|
|
13436
13493
|
|
|
13437
|
-
<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>
|
|
13438
13495
|
```
|
|
13439
13496
|
|
|
13440
13497
|
---
|
|
@@ -13482,7 +13539,7 @@ Use DisclosurePicker for one-of-many choices in a docked panel or inspector wher
|
|
|
13482
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. |
|
|
13483
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. |
|
|
13484
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. |
|
|
13485
|
-
| 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. |
|
|
13486
13543
|
| DisclosurePickerEmpty | { children?: ReactNode } | The no-match line, rendered in place of the list. |
|
|
13487
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. |
|
|
13488
13545
|
|
|
@@ -13514,6 +13571,7 @@ Use DisclosurePicker for one-of-many choices in a docked panel or inspector wher
|
|
|
13514
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.
|
|
13515
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.
|
|
13516
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.
|
|
13517
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.
|
|
13518
13576
|
|
|
13519
13577
|
## Related
|
|
@@ -13624,7 +13682,7 @@ Use EmptyState when a content area has no items, no search results, or no first-
|
|
|
13624
13682
|
|
|
13625
13683
|
### Best practices
|
|
13626
13684
|
|
|
13627
|
-
- 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".
|
|
13628
13686
|
- Do: Offer one next step when the user can recover, create, clear filters, or start setup.
|
|
13629
13687
|
- Do: Keep the icon decorative and make the title carry the meaning.
|
|
13630
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.
|
|
@@ -13697,7 +13755,7 @@ import { Button, EmptyState, Icon } from "@sableui/react"
|
|
|
13697
13755
|
```tsx
|
|
13698
13756
|
import { Button, EmptyState, Icon, Row } from "@sableui/react"
|
|
13699
13757
|
|
|
13700
|
-
<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>} />
|
|
13701
13759
|
```
|
|
13702
13760
|
|
|
13703
13761
|
### Page-level error
|
|
@@ -13713,7 +13771,7 @@ import { Button, EmptyState } from "@sableui/react"
|
|
|
13713
13771
|
```tsx
|
|
13714
13772
|
import { Button, EmptyState } from "@sableui/react"
|
|
13715
13773
|
|
|
13716
|
-
<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>} />
|
|
13717
13775
|
```
|
|
13718
13776
|
|
|
13719
13777
|
### Empty table body
|
|
@@ -13721,7 +13779,7 @@ import { Button, EmptyState } from "@sableui/react"
|
|
|
13721
13779
|
```tsx
|
|
13722
13780
|
import { DataTable, EmptyState } from "@sableui/react"
|
|
13723
13781
|
|
|
13724
|
-
<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." />} />
|
|
13725
13783
|
```
|
|
13726
13784
|
|
|
13727
13785
|
---
|
|
@@ -14459,7 +14517,7 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
|
|
|
14459
14517
|
| Name | Type | Default | Description |
|
|
14460
14518
|
| --- | --- | --- | --- |
|
|
14461
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. |
|
|
14462
|
-
| 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. |
|
|
14463
14521
|
| seed | string \| number | — | Fixes the composition: the same seed paints the same art in every theme. Pass a stable id. |
|
|
14464
14522
|
| children | ReactNode | — | Rendered above the art — initials, a glyph, a short label. |
|
|
14465
14523
|
| className | string | — | Size and shape: aspect, height, rounding. |
|
|
@@ -14470,7 +14528,7 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
|
|
|
14470
14528
|
|
|
14471
14529
|
## Tokens used
|
|
14472
14530
|
|
|
14473
|
-
`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`
|
|
14474
14532
|
|
|
14475
14533
|
## Motion
|
|
14476
14534
|
|
|
@@ -14503,7 +14561,7 @@ import { GradientArt } from "@sableui/react"
|
|
|
14503
14561
|
```tsx
|
|
14504
14562
|
import { GradientArt } from "@sableui/react"
|
|
14505
14563
|
|
|
14506
|
-
<GradientArt variant="grain" seed="juno" tones={["info", "
|
|
14564
|
+
<GradientArt variant="grain" seed="juno" tones={["info", "1"]} className="h-20 rounded-md" />
|
|
14507
14565
|
```
|
|
14508
14566
|
|
|
14509
14567
|
### Avatar with initials
|
|
@@ -14854,7 +14912,7 @@ import { Field, Input } from "@sableui/react"
|
|
|
14854
14912
|
```tsx
|
|
14855
14913
|
import { Label } from "@sableui/react"
|
|
14856
14914
|
|
|
14857
|
-
<Label htmlFor="
|
|
14915
|
+
<Label htmlFor="discount" hint="Taken off every order this code is used on.">Discount</Label>
|
|
14858
14916
|
```
|
|
14859
14917
|
|
|
14860
14918
|
---
|
|
@@ -14906,7 +14964,7 @@ Use LineChart to compare how two or more measures change over the same sequence,
|
|
|
14906
14964
|
|
|
14907
14965
|
## Tokens used
|
|
14908
14966
|
|
|
14909
|
-
`tone-
|
|
14967
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
14910
14968
|
|
|
14911
14969
|
## Motion
|
|
14912
14970
|
|
|
@@ -15813,7 +15871,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
|
|
|
15813
15871
|
| value | number | — | The current reading, between min and max. Required. |
|
|
15814
15872
|
| min | number | 0 | The bottom of the range. |
|
|
15815
15873
|
| max | number | 100 | The top of the range. |
|
|
15816
|
-
| tone | "primary" \| "
|
|
15874
|
+
| tone | "primary" \| "3" \| "success" \| "warning" \| "destructive" | "primary" | Fill colour, mapped to a semantic token. Choose it from your own thresholds. |
|
|
15817
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. |
|
|
15818
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. |
|
|
15819
15877
|
| showValue | boolean | — | Print the value in the ring's centre (formatted like MeterValue). |
|
|
@@ -15824,7 +15882,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
|
|
|
15824
15882
|
|
|
15825
15883
|
## Variants
|
|
15826
15884
|
|
|
15827
|
-
- **tone**: primary,
|
|
15885
|
+
- **tone**: primary, 3, success, warning, destructive
|
|
15828
15886
|
- **shape**: bar, ring
|
|
15829
15887
|
- **size**: sm, md, lg
|
|
15830
15888
|
|
|
@@ -15933,12 +15991,12 @@ Use MiddleTruncate when both the beginning and end of a long identifier must rem
|
|
|
15933
15991
|
|
|
15934
15992
|
## Examples
|
|
15935
15993
|
|
|
15936
|
-
###
|
|
15994
|
+
### API key
|
|
15937
15995
|
|
|
15938
15996
|
```tsx
|
|
15939
15997
|
import { MiddleTruncate } from "@sableui/react"
|
|
15940
15998
|
|
|
15941
|
-
<MiddleTruncate text="
|
|
15999
|
+
<MiddleTruncate text="demo_key_7f3a9c2e5b8d1f4a6c0e9b3d7a2f5c8e1b4" />
|
|
15942
16000
|
```
|
|
15943
16001
|
|
|
15944
16002
|
### Custom start/end counts
|
|
@@ -15946,7 +16004,7 @@ import { MiddleTruncate } from "@sableui/react"
|
|
|
15946
16004
|
```tsx
|
|
15947
16005
|
import { MiddleTruncate } from "@sableui/react"
|
|
15948
16006
|
|
|
15949
|
-
<MiddleTruncate text="
|
|
16007
|
+
<MiddleTruncate text="9f2c4e7a1b3d5f60c8e2a4b6d8f0e1c3a5b7d9f2" startChars={4} endChars={4} />
|
|
15950
16008
|
```
|
|
15951
16009
|
|
|
15952
16010
|
### Copyable value
|
|
@@ -15954,8 +16012,8 @@ import { MiddleTruncate } from "@sableui/react"
|
|
|
15954
16012
|
```tsx
|
|
15955
16013
|
import { Button, Icon, MiddleTruncate, Row, useCopy } from "@sableui/react"
|
|
15956
16014
|
|
|
15957
|
-
const { copy, copied } = useCopy(
|
|
15958
|
-
<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>
|
|
15959
16017
|
```
|
|
15960
16018
|
|
|
15961
16019
|
---
|
|
@@ -16015,7 +16073,7 @@ Use Note for helpful supporting information that should sit near the relevant co
|
|
|
16015
16073
|
```tsx
|
|
16016
16074
|
import { Note } from "@sableui/react"
|
|
16017
16075
|
|
|
16018
|
-
<Note tone="info">
|
|
16076
|
+
<Note tone="info">Reports refresh every hour.</Note>
|
|
16019
16077
|
```
|
|
16020
16078
|
|
|
16021
16079
|
### With title
|
|
@@ -16023,7 +16081,7 @@ import { Note } from "@sableui/react"
|
|
|
16023
16081
|
```tsx
|
|
16024
16082
|
import { Note } from "@sableui/react"
|
|
16025
16083
|
|
|
16026
|
-
<Note tone="warning" title="Low balance">
|
|
16084
|
+
<Note tone="warning" title="Low balance">Add credits before your next export.</Note>
|
|
16027
16085
|
```
|
|
16028
16086
|
|
|
16029
16087
|
### Warning with a remedy, inside a dialog
|
|
@@ -16039,7 +16097,7 @@ import { Note } from "@sableui/react"
|
|
|
16039
16097
|
```tsx
|
|
16040
16098
|
import { Note } from "@sableui/react"
|
|
16041
16099
|
|
|
16042
|
-
<Note tone="neutral" icon="lock">Pricing
|
|
16100
|
+
<Note tone="neutral" icon="lock">Pricing cannot be changed after an order is placed.</Note>
|
|
16043
16101
|
```
|
|
16044
16102
|
|
|
16045
16103
|
---
|
|
@@ -16523,7 +16581,7 @@ Use Pagination when a result set is split into discrete pages and the user needs
|
|
|
16523
16581
|
### Best practices
|
|
16524
16582
|
|
|
16525
16583
|
- Do: Keep page state controlled by the parent route or data layer so URL, filters, and result fetches stay in sync.
|
|
16526
|
-
- 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".
|
|
16527
16585
|
- Do: Reset to page 1 when filters or search terms materially change the result set.
|
|
16528
16586
|
- Don't: Use Pagination for tiny lists where all items fit comfortably on one page.
|
|
16529
16587
|
- Don't: Hide disabled previous/next buttons at the bounds; disabled controls make the current position clearer.
|
|
@@ -16589,7 +16647,7 @@ const [page, setPage] = useState(8)
|
|
|
16589
16647
|
```tsx
|
|
16590
16648
|
import { DataTable, Pagination, Stack } from "@sableui/react"
|
|
16591
16649
|
|
|
16592
|
-
<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>
|
|
16593
16651
|
```
|
|
16594
16652
|
|
|
16595
16653
|
### Server query state
|
|
@@ -16899,7 +16957,7 @@ Use Phone to frame a mobile product preview when the device context is useful.
|
|
|
16899
16957
|
```tsx
|
|
16900
16958
|
import { Phone } from "@sableui/react"
|
|
16901
16959
|
|
|
16902
|
-
<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>
|
|
16903
16961
|
```
|
|
16904
16962
|
|
|
16905
16963
|
### Mockup, not a responsive preview
|
|
@@ -16969,7 +17027,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
|
|
|
16969
17027
|
|
|
16970
17028
|
## Tokens used
|
|
16971
17029
|
|
|
16972
|
-
`tone-
|
|
17030
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
16973
17031
|
|
|
16974
17032
|
## Motion
|
|
16975
17033
|
|
|
@@ -17057,6 +17115,7 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
17057
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. |
|
|
17058
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. |
|
|
17059
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. |
|
|
17060
17119
|
| icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. |
|
|
17061
17120
|
|
|
17062
17121
|
## Variants
|
|
@@ -17064,10 +17123,11 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
17064
17123
|
- **appearance**: solid, outline
|
|
17065
17124
|
- **size**: md, sm
|
|
17066
17125
|
- **selected**: true, false
|
|
17126
|
+
- **tone**: neutral, red, orange, yellow, green, teal, blue, purple, pink
|
|
17067
17127
|
|
|
17068
17128
|
## Tokens used
|
|
17069
17129
|
|
|
17070
|
-
`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`
|
|
17071
17131
|
|
|
17072
17132
|
## Accessibility
|
|
17073
17133
|
|
|
@@ -17080,12 +17140,20 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
17080
17140
|
|
|
17081
17141
|
## Examples
|
|
17082
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
|
+
|
|
17083
17151
|
### Filter row
|
|
17084
17152
|
|
|
17085
17153
|
```tsx
|
|
17086
17154
|
import { Pill, Row } from "@sableui/react"
|
|
17087
17155
|
|
|
17088
|
-
<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>
|
|
17089
17157
|
```
|
|
17090
17158
|
|
|
17091
17159
|
### Outline, small (reactions)
|
|
@@ -17175,7 +17243,7 @@ Use Popover for anchored secondary content that needs more room than a tooltip b
|
|
|
17175
17243
|
```tsx
|
|
17176
17244
|
import { Button, Popover, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "@sableui/react"
|
|
17177
17245
|
|
|
17178
|
-
<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>
|
|
17179
17247
|
```
|
|
17180
17248
|
|
|
17181
17249
|
### With close
|
|
@@ -17183,7 +17251,7 @@ import { Button, Popover, PopoverContent, PopoverDescription, PopoverTitle, Popo
|
|
|
17183
17251
|
```tsx
|
|
17184
17252
|
import { Button, Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "@sableui/react"
|
|
17185
17253
|
|
|
17186
|
-
<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>
|
|
17187
17255
|
```
|
|
17188
17256
|
|
|
17189
17257
|
---
|
|
@@ -17333,7 +17401,7 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
|
|
|
17333
17401
|
|
|
17334
17402
|
## Tokens used
|
|
17335
17403
|
|
|
17336
|
-
`tone-
|
|
17404
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
17337
17405
|
|
|
17338
17406
|
## Motion
|
|
17339
17407
|
|
|
@@ -17444,7 +17512,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
|
|
|
17444
17512
|
|
|
17445
17513
|
## Tokens used
|
|
17446
17514
|
|
|
17447
|
-
`tone-
|
|
17515
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
17448
17516
|
|
|
17449
17517
|
## Motion
|
|
17450
17518
|
|
|
@@ -17568,7 +17636,7 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
|
|
|
17568
17636
|
<FieldsetContent>
|
|
17569
17637
|
<RadioGroup defaultValue="standard">
|
|
17570
17638
|
<Field label="Standard" orientation="horizontal"><Radio value="standard" /></Field>
|
|
17571
|
-
<Field label="
|
|
17639
|
+
<Field label="Poll" orientation="horizontal"><Radio value="poll" /></Field>
|
|
17572
17640
|
</RadioGroup>
|
|
17573
17641
|
</FieldsetContent>
|
|
17574
17642
|
</Fieldset>
|
|
@@ -17584,7 +17652,7 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
|
|
|
17584
17652
|
<FieldsetContent>
|
|
17585
17653
|
<RadioGroup defaultValue="standard">
|
|
17586
17654
|
<Field label="Standard" orientation="horizontal"><Radio value="standard" /></Field>
|
|
17587
|
-
<Field label="
|
|
17655
|
+
<Field label="Poll" orientation="horizontal" disabled><Radio value="poll" /></Field>
|
|
17588
17656
|
</RadioGroup>
|
|
17589
17657
|
</FieldsetContent>
|
|
17590
17658
|
</Fieldset>
|
|
@@ -17633,18 +17701,18 @@ Use RadioCard for visual choice cards when users compare a small set of named op
|
|
|
17633
17701
|
| description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
|
|
17634
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. |
|
|
17635
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. |
|
|
17636
|
-
| 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. |
|
|
17637
17705
|
| disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
|
|
17638
17706
|
| titleClassName | string | — | Merged onto the title span (default text-title-sm) — e.g. to tighten copy in a two-column layout. |
|
|
17639
17707
|
| descriptionClassName | string | — | Merged onto the description span (default text-body-sm text-muted-foreground). |
|
|
17640
17708
|
|
|
17641
17709
|
## Variants
|
|
17642
17710
|
|
|
17643
|
-
- **tone**: neutral,
|
|
17711
|
+
- **tone**: neutral, 3, 2, 1, info, warning
|
|
17644
17712
|
|
|
17645
17713
|
## Tokens used
|
|
17646
17714
|
|
|
17647
|
-
`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`
|
|
17648
17716
|
|
|
17649
17717
|
## Accessibility
|
|
17650
17718
|
|
|
@@ -17665,7 +17733,7 @@ Use RadioCard for visual choice cards when users compare a small set of named op
|
|
|
17665
17733
|
```tsx
|
|
17666
17734
|
import { Icon, RadioCard, RadioGroup } from "@sableui/react"
|
|
17667
17735
|
|
|
17668
|
-
<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>
|
|
17669
17737
|
```
|
|
17670
17738
|
|
|
17671
17739
|
### Plan picker (plain)
|
|
@@ -17811,7 +17879,7 @@ Use Scroller to create a contained overflow region for content that naturally ex
|
|
|
17811
17879
|
```tsx
|
|
17812
17880
|
import { Scroller } from "@sableui/react"
|
|
17813
17881
|
|
|
17814
|
-
<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>
|
|
17815
17883
|
```
|
|
17816
17884
|
|
|
17817
17885
|
### Capped, scrolls past the cap
|
|
@@ -18014,7 +18082,7 @@ import { SearchInput } from "@sableui/react"
|
|
|
18014
18082
|
```tsx
|
|
18015
18083
|
import { Field, SearchInput } from "@sableui/react"
|
|
18016
18084
|
|
|
18017
|
-
<Field label="Search posts"><SearchInput defaultValue="
|
|
18085
|
+
<Field label="Search posts"><SearchInput defaultValue="roadmap" className="max-w-80" /></Field>
|
|
18018
18086
|
```
|
|
18019
18087
|
|
|
18020
18088
|
### Disabled
|
|
@@ -18212,11 +18280,11 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
|
|
|
18212
18280
|
## Variants
|
|
18213
18281
|
|
|
18214
18282
|
- **size**: sm, md, lg
|
|
18215
|
-
- **tone**:
|
|
18283
|
+
- **tone**: 1, 2, 3, info, primary, neutral, success, warning, destructive
|
|
18216
18284
|
|
|
18217
18285
|
## Tokens used
|
|
18218
18286
|
|
|
18219
|
-
`tone-
|
|
18287
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`
|
|
18220
18288
|
|
|
18221
18289
|
## Motion
|
|
18222
18290
|
|
|
@@ -18227,7 +18295,7 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
|
|
|
18227
18295
|
|
|
18228
18296
|
## Accessibility
|
|
18229
18297
|
|
|
18230
|
-
- 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.
|
|
18231
18299
|
- Colour alone does not identify a segment: always show a legend with each segment's label beside the bar.
|
|
18232
18300
|
|
|
18233
18301
|
## Related
|
|
@@ -18243,13 +18311,13 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
|
|
|
18243
18311
|
import { SegmentedBar, Stack, StatusDot } from "@sableui/react"
|
|
18244
18312
|
|
|
18245
18313
|
const sales = [
|
|
18246
|
-
{ label: "
|
|
18247
|
-
{ label: "
|
|
18248
|
-
{ label: "
|
|
18314
|
+
{ label: "Direct", value: 42, tone: "1" },
|
|
18315
|
+
{ label: "Partners", value: 27, tone: "2" },
|
|
18316
|
+
{ label: "Referrals", value: 19, tone: "3" },
|
|
18249
18317
|
] as const
|
|
18250
18318
|
|
|
18251
18319
|
<Stack gap={3}>
|
|
18252
|
-
<SegmentedBar label="Revenue by
|
|
18320
|
+
<SegmentedBar label="Revenue by channel" segments={[...sales]} size="lg" />
|
|
18253
18321
|
<ul className="flex flex-col gap-1.5">
|
|
18254
18322
|
{sales.map((s) => (
|
|
18255
18323
|
<li key={s.label} className="flex items-center gap-2 text-body-sm">
|
|
@@ -18375,7 +18443,7 @@ import { SelectContent, SelectGroup, SelectGroupLabel, SelectItem } from "@sable
|
|
|
18375
18443
|
```tsx
|
|
18376
18444
|
import { Field, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
|
|
18377
18445
|
|
|
18378
|
-
<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>
|
|
18379
18447
|
```
|
|
18380
18448
|
|
|
18381
18449
|
### Rich item labels
|
|
@@ -18383,7 +18451,7 @@ import { Field, Select, SelectContent, SelectItem, SelectTrigger } from "@sableu
|
|
|
18383
18451
|
```tsx
|
|
18384
18452
|
import { Badge, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
|
|
18385
18453
|
|
|
18386
|
-
<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>
|
|
18387
18455
|
```
|
|
18388
18456
|
|
|
18389
18457
|
---
|
|
@@ -18708,7 +18776,7 @@ import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, Shee
|
|
|
18708
18776
|
```tsx
|
|
18709
18777
|
import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@sableui/react"
|
|
18710
18778
|
|
|
18711
|
-
<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>
|
|
18712
18780
|
```
|
|
18713
18781
|
|
|
18714
18782
|
---
|
|
@@ -18844,7 +18912,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
18844
18912
|
import { Icon, NotificationBadge, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
|
|
18845
18913
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
18846
18914
|
|
|
18847
|
-
<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>
|
|
18848
18916
|
```
|
|
18849
18917
|
|
|
18850
18918
|
### Collapsible (icon rail)
|
|
@@ -18953,7 +19021,7 @@ import { SideNav, SideNavGroup, SideNavItem } from "@sableui/react"
|
|
|
18953
19021
|
import { SideNav, SideNavItem } from "@sableui/react"
|
|
18954
19022
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
18955
19023
|
|
|
18956
|
-
<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>
|
|
18957
19025
|
```
|
|
18958
19026
|
|
|
18959
19027
|
### Inside Sidebar chrome
|
|
@@ -19196,7 +19264,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
|
|
|
19196
19264
|
| data | number[] | — | The values, oldest first. |
|
|
19197
19265
|
| label | string | — | What the line tracks ("Revenue, last 30 days"). The spoken summary adds the first and last values, low and high. |
|
|
19198
19266
|
| variant | "line" \| "area" \| "bar" | "line" | A line, a line over a soft fill, or columns. |
|
|
19199
|
-
| tone | "
|
|
19267
|
+
| tone | "1" \| "2" \| "3" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | "primary" | Colour by role. |
|
|
19200
19268
|
| className | string | "h-8 w-24" | Sizes it; the drawing stretches to fit. |
|
|
19201
19269
|
| animate | boolean | true | Draw on (or rise) when it scrolls into view. |
|
|
19202
19270
|
| locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
|
|
@@ -19207,7 +19275,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
|
|
|
19207
19275
|
|
|
19208
19276
|
## Tokens used
|
|
19209
19277
|
|
|
19210
|
-
`primary`, `tone-
|
|
19278
|
+
`primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
|
|
19211
19279
|
|
|
19212
19280
|
## Motion
|
|
19213
19281
|
|
|
@@ -19338,7 +19406,7 @@ import { Spinner } from "@sableui/react"
|
|
|
19338
19406
|
```tsx
|
|
19339
19407
|
import { Button } from "@sableui/react"
|
|
19340
19408
|
|
|
19341
|
-
<Button loading>
|
|
19409
|
+
<Button loading>Saving</Button>
|
|
19342
19410
|
```
|
|
19343
19411
|
|
|
19344
19412
|
---
|
|
@@ -19467,18 +19535,18 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
|
|
|
19467
19535
|
|
|
19468
19536
|
| Name | Type | Default | Description |
|
|
19469
19537
|
| --- | --- | --- | --- |
|
|
19470
|
-
| 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. |
|
|
19471
19539
|
| size | "sm" \| "md" \| "lg" | "md" | Dot diameter. |
|
|
19472
19540
|
| pulse | boolean | false | Wrap the dot in an animated ping ring to signal a live state. |
|
|
19473
19541
|
|
|
19474
19542
|
## Variants
|
|
19475
19543
|
|
|
19476
|
-
- **tone**: neutral,
|
|
19544
|
+
- **tone**: neutral, 1, 2, 3, success, warning, info, destructive, red, orange, yellow, green, teal, blue, purple, pink
|
|
19477
19545
|
- **size**: sm, md, lg
|
|
19478
19546
|
|
|
19479
19547
|
## Tokens used
|
|
19480
19548
|
|
|
19481
|
-
`muted-foreground`, `tone-
|
|
19549
|
+
`palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`
|
|
19482
19550
|
|
|
19483
19551
|
## Accessibility
|
|
19484
19552
|
|
|
@@ -19497,7 +19565,7 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
|
|
|
19497
19565
|
```tsx
|
|
19498
19566
|
import { Row, StatusDot } from "@sableui/react"
|
|
19499
19567
|
|
|
19500
|
-
<Row gap={2}><StatusDot tone="
|
|
19568
|
+
<Row gap={2}><StatusDot tone="3" /><StatusDot tone="info" /><StatusDot tone="warning" /><StatusDot tone="destructive" /></Row>
|
|
19501
19569
|
```
|
|
19502
19570
|
|
|
19503
19571
|
### With label — use Badge
|
|
@@ -19882,25 +19950,25 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
|
|
|
19882
19950
|
|
|
19883
19951
|
## Examples
|
|
19884
19952
|
|
|
19885
|
-
###
|
|
19953
|
+
### Orders
|
|
19886
19954
|
|
|
19887
19955
|
```tsx
|
|
19888
19956
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
19889
19957
|
|
|
19890
19958
|
<Table>
|
|
19891
|
-
<TableCaption>Recent
|
|
19959
|
+
<TableCaption>Recent orders</TableCaption>
|
|
19892
19960
|
<TableHeader>
|
|
19893
19961
|
<TableRow>
|
|
19894
|
-
<TableHead>
|
|
19895
|
-
<TableHead>
|
|
19962
|
+
<TableHead>Customer</TableHead>
|
|
19963
|
+
<TableHead>Order</TableHead>
|
|
19896
19964
|
<TableHead>Paid</TableHead>
|
|
19897
19965
|
</TableRow>
|
|
19898
19966
|
</TableHeader>
|
|
19899
19967
|
<TableBody>
|
|
19900
19968
|
<TableRow>
|
|
19901
19969
|
<TableCell>@nova</TableCell>
|
|
19902
|
-
<TableCell
|
|
19903
|
-
<TableCell
|
|
19970
|
+
<TableCell>1042</TableCell>
|
|
19971
|
+
<TableCell>$48.00</TableCell>
|
|
19904
19972
|
</TableRow>
|
|
19905
19973
|
</TableBody>
|
|
19906
19974
|
</Table>
|
|
@@ -19919,7 +19987,7 @@ import { Avatar, Badge, Button, Row, Table, TableBody, TableCaption, TableCell,
|
|
|
19919
19987
|
```tsx
|
|
19920
19988
|
import { Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
19921
19989
|
|
|
19922
|
-
<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>
|
|
19923
19991
|
```
|
|
19924
19992
|
|
|
19925
19993
|
### Summary footer
|
|
@@ -19927,7 +19995,7 @@ import { Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell,
|
|
|
19927
19995
|
```tsx
|
|
19928
19996
|
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
19929
19997
|
|
|
19930
|
-
<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>
|
|
19931
19999
|
```
|
|
19932
20000
|
|
|
19933
20001
|
---
|
|
@@ -20014,7 +20082,7 @@ import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
|
|
|
20014
20082
|
```tsx
|
|
20015
20083
|
import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
|
|
20016
20084
|
|
|
20017
|
-
<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>
|
|
20018
20086
|
```
|
|
20019
20087
|
|
|
20020
20088
|
---
|
|
@@ -20081,7 +20149,7 @@ import { Textarea } from "@sableui/react"
|
|
|
20081
20149
|
```tsx
|
|
20082
20150
|
import { Textarea } from "@sableui/react"
|
|
20083
20151
|
|
|
20084
|
-
<Textarea rows={6} placeholder="Tell
|
|
20152
|
+
<Textarea rows={6} placeholder="Tell your team what this project is for…" />
|
|
20085
20153
|
```
|
|
20086
20154
|
|
|
20087
20155
|
### Auto-resize
|
|
@@ -20105,7 +20173,7 @@ import { Textarea } from "@sableui/react"
|
|
|
20105
20173
|
```tsx
|
|
20106
20174
|
import { Textarea } from "@sableui/react"
|
|
20107
20175
|
|
|
20108
|
-
<Textarea disabled defaultValue="
|
|
20176
|
+
<Textarea disabled defaultValue="Notes lock once the invoice is sent." />
|
|
20109
20177
|
```
|
|
20110
20178
|
|
|
20111
20179
|
---
|
|
@@ -20522,7 +20590,7 @@ toast.promise(publish(), { loading: "Publishing…", success: "Published", error
|
|
|
20522
20590
|
```tsx
|
|
20523
20591
|
import { toast } from "@sableui/react"
|
|
20524
20592
|
|
|
20525
|
-
const id = toast.loading("
|
|
20593
|
+
const id = toast.loading("Uploading report…"); /* later */ toast.update(id, { title: "Uploaded", type: "success", timeout: 4000 })
|
|
20526
20594
|
```
|
|
20527
20595
|
|
|
20528
20596
|
### Custom toast markup (composed parts)
|
|
@@ -20904,7 +20972,7 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
|
|
|
20904
20972
|
```tsx
|
|
20905
20973
|
import { Button, Icon, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@sableui/react"
|
|
20906
20974
|
|
|
20907
|
-
<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>
|
|
20908
20976
|
```
|
|
20909
20977
|
|
|
20910
20978
|
### Sides
|
|
@@ -20920,7 +20988,7 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@sableui/react"
|
|
|
20920
20988
|
```tsx
|
|
20921
20989
|
import { Button, Icon, SimpleTooltip } from "@sableui/react"
|
|
20922
20990
|
|
|
20923
|
-
<SimpleTooltip content="
|
|
20991
|
+
<SimpleTooltip content="Saved to your workspace."><Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button></SimpleTooltip>
|
|
20924
20992
|
```
|
|
20925
20993
|
|
|
20926
20994
|
---
|
|
@@ -21417,23 +21485,23 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
21417
21485
|
- **Not** `tone-info`. Success asserts an outcome; info asserts a fact. 'Saved' is success, '3 items' is info.
|
|
21418
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.
|
|
21419
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.
|
|
21420
|
-
- **`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'.
|
|
21421
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.
|
|
21422
|
-
- **`tone-
|
|
21423
|
-
- **Not** `
|
|
21424
|
-
- **`tone-
|
|
21425
|
-
- **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.
|
|
21426
21494
|
- **`tone-info`** — Domain tone: neutral, factual information. *Use for:* An informational note or banner, a neutral count, a 'did you know' surface.
|
|
21427
21495
|
- **Not** `success`. Info states a fact; success states an outcome. Also not `primary` — an info banner is not an action.
|
|
21428
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.
|
|
21429
21497
|
- **Not** `destructive`. Warning is *advisory*; destructive is *consequential*. If nothing is lost by continuing, it is a warning.
|
|
21430
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.
|
|
21431
21499
|
- **Not** `accent`. `accent` is UI state that follows the pointer or selection; `highlight` marks the *content* and persists independently of interaction.
|
|
21432
|
-
- **`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.
|
|
21433
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.
|
|
21434
|
-
- **`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.
|
|
21435
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.
|
|
21436
|
-
- **`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.
|
|
21437
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.
|
|
21438
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.
|
|
21439
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.
|
|
@@ -21540,11 +21608,11 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
21540
21608
|
|
|
21541
21609
|
### layout (11)
|
|
21542
21610
|
|
|
21543
|
-
- **`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.
|
|
21544
21612
|
- **Not** arbitrary vertical spacing. The rhythm is what makes unrelated sections of a page feel like one document.
|
|
21545
21613
|
- **`grid-columns`** — The column count of the content grid — 12. *Use for:* `Grid`/`Columns`, WITHIN a region, for genuinely multi-column content.
|
|
21546
21614
|
- **Not** using a column span as the page's reading width. A span is not a measure — see `measure-prose`.
|
|
21547
|
-
- **`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.
|
|
21548
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.
|
|
21549
21617
|
- **Not** a global max-width. Sable caps width per REGION by role, never once for the whole page.
|
|
21550
21618
|
- **`measure-prose`** — The maximum comfortable line length for prose, in `ch`. *Use for:* Any long-form text column.
|
|
@@ -21704,6 +21772,20 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
21704
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`.
|
|
21705
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.
|
|
21706
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
|
+
|
|
21707
21789
|
---
|
|
21708
21790
|
|
|
21709
21791
|
## Utilities
|
|
@@ -21809,7 +21891,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
21809
21891
|
- `<ol className="sable-list-numbered"><li>Install</li><li>Import the styles</li></ol>`
|
|
21810
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.
|
|
21811
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.
|
|
21812
|
-
- `<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>`
|
|
21813
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.
|
|
21814
21896
|
- **Instead of** A viewport breakpoint, which gives a narrow card on a wide screen four columns it cannot fit.
|
|
21815
21897
|
- `<div className="@container"><dl className="sable-list-dl sable-list-dl-2">…</dl></div>`
|