@sableui/react 0.17.0 → 0.19.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 +10 -3
- package/CHANGELOG.md +235 -0
- package/README.md +11 -4
- package/bin/sable.mjs +266 -30
- package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/avatar/avatar.js +16 -7
- package/dist/components/avatar/avatar.meta.js +43 -10
- package/dist/components/avatar/avatar.variants.js +26 -2
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/collapsible/collapsible.js +6 -5
- package/dist/components/collapsible/collapsible.meta.js +14 -5
- package/dist/components/data-table/data-table.js +132 -45
- package/dist/components/data-table/data-table.meta.js +63 -4
- package/dist/components/date-picker/date-picker.js +13 -6
- package/dist/components/date-picker/date-picker.meta.js +22 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/menu/menu.js +2 -0
- package/dist/components/menu/menu.meta.js +2 -2
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/panel-group/panel-group.js +17 -1
- package/dist/components/panel-group/panel-group.meta.js +43 -3
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/components/side-nav/side-nav.variants.js +8 -3
- package/dist/components/sidebar/index.js +4 -0
- package/dist/components/sidebar/sidebar.js +90 -11
- package/dist/components/sidebar/sidebar.meta.js +38 -4
- package/dist/components/table/table.js +7 -3
- package/dist/components/tabs/tabs.js +10 -2
- package/dist/components/tabs/tabs.meta.js +4 -0
- package/dist/components/workspace/workspace.js +13 -3
- package/dist/components/workspace/workspace.meta.js +109 -1
- package/dist/file-hashes.json +94 -80
- package/dist/foundations/typography.js +13 -0
- package/dist/icon/Icon.js +21 -2
- package/dist/icon/index.d.ts +36 -1
- package/dist/icon/index.js +5 -0
- package/dist/icon/names.js +240 -0
- package/dist/icon-sets/reicon.js +41 -1
- package/dist/index.d.ts +540 -75
- package/dist/index.js +18 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/cn.js +1 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +1 -0
- package/dist/lib/use-media-query.js +41 -0
- package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
- package/dist/patterns/dashboard-page.js +23 -17
- package/dist/patterns/index.d.ts +7 -2
- package/dist/patterns/index.js +5 -1
- package/dist/patterns/tracker-page.js +392 -0
- package/dist/patterns/tracker-page.meta.js +40 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/styles.css +301 -188
- package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
- package/dist/themes/_contract.js +9 -1
- package/dist/themes/atelier/index.d.ts +1 -1
- 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/poppy/index.d.ts +1 -1
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/avatar.md +32 -9
- package/llms/components/code-block.md +1 -0
- package/llms/components/collapsible.md +12 -4
- package/llms/components/data-table.md +40 -3
- package/llms/components/date-picker.md +15 -1
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/page-header.md +16 -1
- package/llms/components/panel-group.md +34 -2
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/sheet.md +2 -2
- package/llms/components/sidebar.md +17 -4
- package/llms/components/tabs.md +1 -0
- package/llms/components/workspace.md +81 -1
- package/llms/foundations/typography.md +4 -1
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/patterns/dashboard-page.md +18 -15
- package/llms/patterns/tracker-page.md +450 -0
- package/llms/reference/token-intent.md +3 -1
- package/llms/reference/utilities.md +4 -1
- package/llms/tasks/auditing-an-app.md +35 -1
- package/llms/tasks/building-a-page.md +3 -2
- package/llms/tasks/building-a-tool.md +101 -5
- package/llms/tasks/building-with-sable.md +11 -3
- package/llms/tasks/creating-a-preset.md +9 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +1215 -89
- package/llms.txt +11 -9
- package/package.json +7 -1
- package/registry.json +1021 -70
- package/scripts/adoption.mjs +252 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/app-css.mjs +129 -0
- package/scripts/grade.mjs +178 -2
- package/scripts/lint-rules.mjs +126 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +64 -6
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +9 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { T as ThemeManifest } from './_contract-
|
|
3
|
+
import { T as ThemeManifest } from './_contract-DlklV0Im.js';
|
|
4
4
|
|
|
5
5
|
declare const SABLE_THEME_ATTRIBUTE: "data-sable-theme";
|
|
6
6
|
declare const DEFAULT_THEME_ID: string;
|
package/dist/themes/_contract.js
CHANGED
|
@@ -150,6 +150,10 @@ var TYPE_SCALE_TOKENS = [
|
|
|
150
150
|
"type-title-sm-tracking"
|
|
151
151
|
];
|
|
152
152
|
var TYPE_REFINEMENT_TOKENS = [
|
|
153
|
+
// The base the page scale is built on, as a unitless px number (default 16).
|
|
154
|
+
// `14` rebuilds every page tier, and the baseline rhythm, at 14 — see
|
|
155
|
+
// styles.css → Typography → "The base size".
|
|
156
|
+
"type-base",
|
|
153
157
|
"type-display-4xl-size",
|
|
154
158
|
"type-title-lg-size",
|
|
155
159
|
"type-title-sm-size",
|
|
@@ -331,7 +335,11 @@ var TYPE_UI_TOKENS = [
|
|
|
331
335
|
"type-ui-leading",
|
|
332
336
|
"type-ui-section-weight",
|
|
333
337
|
"type-ui-micro-size",
|
|
334
|
-
"type-ui-micro-weight"
|
|
338
|
+
"type-ui-micro-weight",
|
|
339
|
+
// The panel/inspector title, one step above ui-md (compact `text-title-lg`).
|
|
340
|
+
"type-ui-title-size",
|
|
341
|
+
"type-ui-title-weight",
|
|
342
|
+
"type-ui-title-leading"
|
|
335
343
|
];
|
|
336
344
|
export {
|
|
337
345
|
APP_SHELL_TOKENS,
|
package/dist/themes/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AnyThemeId, D as DEFAULT_THEME_ID, S as SABLE_COLOR_STORAGE_KEY, a as SABLE_THEME_ATTRIBUTE, b as SABLE_THEME_STORAGE_KEY, c as SableColorMode, d as SableThemeContextValue, e as SableThemePreference, f as SableThemeProvider, g as SableThemeProviderProps, T as ThemeId, h as applySableColorMode, i as applySableTheme, j as buildThemeManifestById, k as defaultThemeId, l as featuredThemeManifests, m as getThemeManifest, n as isKnownThemeId, o as isThemeId, t as themeIds, p as themeManifestById, q as themeManifests, u as useSableTheme } from '../theme-provider-
|
|
1
|
+
export { A as AnyThemeId, D as DEFAULT_THEME_ID, S as SABLE_COLOR_STORAGE_KEY, a as SABLE_THEME_ATTRIBUTE, b as SABLE_THEME_STORAGE_KEY, c as SableColorMode, d as SableThemeContextValue, e as SableThemePreference, f as SableThemeProvider, g as SableThemeProviderProps, T as ThemeId, h as applySableColorMode, i as applySableTheme, j as buildThemeManifestById, k as defaultThemeId, l as featuredThemeManifests, m as getThemeManifest, n as isKnownThemeId, o as isThemeId, t as themeIds, p as themeManifestById, q as themeManifests, u as useSableTheme } from '../theme-provider-BUBealk_.js';
|
|
2
2
|
export { atelierTheme } from './atelier/index.js';
|
|
3
3
|
export { default as emberTheme } from './ember/index.js';
|
|
4
4
|
export { default as meridianTheme } from './meridian/index.js';
|
|
@@ -7,4 +7,4 @@ export { default as prismTheme } from './prism/index.js';
|
|
|
7
7
|
export { default as sableTheme } from './sable/index.js';
|
|
8
8
|
export { default as verdantTheme } from './verdant/index.js';
|
|
9
9
|
import 'react';
|
|
10
|
-
import '../_contract-
|
|
10
|
+
import '../_contract-DlklV0Im.js';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# ActivityHeatmap
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A calendar heatmap: one value per day as a grid of weeks by weekdays — the GitHub contribution graph, for any daily measure. Levels by quartile, categories by source, a real ARIA grid with one tab stop, and a sideways scroll on narrow screens that opens on the latest week.
|
|
6
|
+
|
|
7
|
+
**Keywords:** heatmap, calendar heatmap, contribution graph, contributions, activity, github, streak, habit, daily, commits, uptime
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use ActivityHeatmap to show how often something happened day by day over weeks or months: commits, habits kept, orders, uptime. Pass days as ISO dates with a value; levels, colours, labels, keyboard and tooltips work with no further props. Put it inside a Card and compose any totals beside it (NumberTicker, StatRowBlock). For a value over time that a reader needs to compare precisely, use AreaChart or BarChart; for a month calendar with marked days (a streak calendar), use Calendar.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Pass dates as "YYYY-MM-DD" strings. The grid reads them as calendar days, so no time zone can shift a day — never build them with `new Date(...).toISOString()` from a local date.
|
|
16
|
+
- Do: Give it a `unit` ("contribution" / "contributions") so the tooltip and every cell's name say what the number counts.
|
|
17
|
+
- Do: Let the default quantile scale pick levels — one exceptional day then does not wash every other day out. Pass a day's own `level` when the source already has one (GitHub's API).
|
|
18
|
+
- Don't: Use it for a handful of values or for values a reader must compare exactly; a heatmap shows rhythm, not amounts. Use BarChart. → use **BarChart**, **AreaChart**
|
|
19
|
+
- Don't: Use it for a single month of marked days with a streak; that is Calendar with modifiers. → use **Calendar**
|
|
20
|
+
- Don't: Fetch inside the component or call GitHub from the browser with a token. Fetch on your server and pass days in; `@sableui/react/adapters/github` converts GitHub's response.
|
|
21
|
+
|
|
22
|
+
### Anatomy
|
|
23
|
+
|
|
24
|
+
| Part | Required | Description |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Weekday labels | no | Short weekday names beside the rows, outside the scroller so the weeks never slide under them. |
|
|
27
|
+
| Month labels | no | Short month names over each month's first week; they scroll with their weeks. |
|
|
28
|
+
| Cell grid | yes | A role=grid table, one gridcell per day, inside a horizontal Scroller that opens on the latest week. |
|
|
29
|
+
| Tooltip | no | The chart tooltip card, anchored to the hovered, focused or tapped day. |
|
|
30
|
+
| Legend | no | A Less–More ramp for intensity, or swatches for categories. |
|
|
31
|
+
|
|
32
|
+
## Props
|
|
33
|
+
|
|
34
|
+
| Name | Type | Default | Description |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| label | string | — | What the grid shows ("Contributions in 2026") — its caption and accessible name. |
|
|
37
|
+
| data | ActivityDay[] \| Record<string, number> | — | The days: `{ date: "YYYY-MM-DD", value, level?, category? }[]`, or a map of date to value. Sparse is fine — a missing day is 0; duplicates sum. |
|
|
38
|
+
| start | string | 53 weeks before end | First day drawn, "YYYY-MM-DD". The default starts at the beginning of that week, as GitHub's does. |
|
|
39
|
+
| end | string | today | Last day drawn. Days after today inside the range are drawn hollow and skipped by the keyboard. |
|
|
40
|
+
| year | number | — | Draw Jan 1 – Dec 31 of this year. Wins over start / end. |
|
|
41
|
+
| weekStart | 0 \| 1 | the locale's | 0 puts Sunday on the top row, 1 Monday. |
|
|
42
|
+
| levels | number | 5 | How many levels, including the empty level 0. |
|
|
43
|
+
| levelScale | "quantile" \| "linear" \| number[] \| ((value, stats) => number) | "quantile" | How a value becomes a level. Quantile splits the positive values evenly, as GitHub does; an array gives each level's lower bound from 1 up; a function decides. A day's own `level` always wins. |
|
|
44
|
+
| tone | ChartTone | "1" | The hue of the intensity ramp. Decorative tones lead, as on every chart; "success" gives the GitHub look. |
|
|
45
|
+
| categories | ChartCategories | — | Colour days by `day.category` (label, tone or colour per key). Pair with `levels={2}` for flat category colours. |
|
|
46
|
+
| unit | string \| { one: string; other: string } | — | Names the value in the tooltip and each cell's accessible name, pluralised with Intl.PluralRules. |
|
|
47
|
+
| formatValue | (value: number) => string | full to 9,999, compact past | Formats a value wherever it is shown or read. |
|
|
48
|
+
| tooltip | boolean \| ((day: ActivityHeatmapDay) => ReactNode) | true | Show the tooltip on hover, focus and tap; a function replaces its content on the same card. |
|
|
49
|
+
| legend | boolean | true | The Less–More ramp for intensity, swatches for categories. |
|
|
50
|
+
| legendLabels | [string, string] | ["Less", "More"] | The ramp's end words. |
|
|
51
|
+
| monthLabels | boolean | true | Short month names over each month's first week. |
|
|
52
|
+
| weekdayLabels | "alternate" \| "all" \| "none" | "alternate" | Which weekday names to show beside the rows; alternate is Mon, Wed, Fri. |
|
|
53
|
+
| size | "sm" \| "md" \| "lg" | "md" | The smallest a cell may get (10, 12, 14 px) before the grid scrolls instead of shrinking. |
|
|
54
|
+
| stretch | boolean | true | Grow cells to fill the width, up to 20 px. false keeps them at `size`. |
|
|
55
|
+
| activeDate | string \| null | — | The hovered or focused day, controlled. Pair with defaultActiveDate / onActiveDateChange. |
|
|
56
|
+
| selectedDate | string \| null | — | The selected day, controlled. Selection is on only when onSelectDate is set. |
|
|
57
|
+
| onSelectDate | (date: string) => void | — | Called when a day is clicked or chosen with Enter / Space — to filter a feed below, as GitHub does. |
|
|
58
|
+
| renderCell | (day, cell) => ReactNode | — | Draw something over a cell's fill — an icon for a milestone, a reward, an outage. |
|
|
59
|
+
| loading | boolean | false | Draw the empty grid, shimmering, while data loads. |
|
|
60
|
+
| motion | "full" \| "subtle" \| "off" | "full" | Turns the arrival down; `subtle` shows the finished grid at once. |
|
|
61
|
+
| locale | string | runtime locale | Month and weekday names, numbers and plurals. |
|
|
62
|
+
|
|
63
|
+
## Tokens used
|
|
64
|
+
|
|
65
|
+
`foreground`, `muted-foreground`, `border-strong`, `ring`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`, `success-mark`, `tone-info-mark`
|
|
66
|
+
|
|
67
|
+
## Motion
|
|
68
|
+
|
|
69
|
+
- **arrival** · Cell grid · _custom (outside the recipe layer)_ — Weeks fade in left to right as the grid first scrolls into view, staggered on the chart motion helpers; expressive, so subtle shows the finished grid.
|
|
70
|
+
- **response** · Cell · _custom (outside the recipe layer)_ — The active day takes a ring outline at once and the tooltip glides to it; the other cells stay still — dimming hundreds of cells reads as a flash.
|
|
71
|
+
- **transition** · Cell · _custom (outside the recipe layer)_ — When data or the range changes, each cell's fill cross-fades on the base duration token, so the reader sees which days changed.
|
|
72
|
+
- **activity** · Cell grid · `motion-shimmer` — `loading` shimmers the empty grid until days arrive.
|
|
73
|
+
|
|
74
|
+
## Accessibility
|
|
75
|
+
|
|
76
|
+
- The grid is a role=grid table, as GitHub's is: weekday row headers and month column headers carry full names for screen readers, and the caption is `label`.
|
|
77
|
+
- One tab stop. Arrows move a day or a week; Home / End go to the ends of the row, Page Up / Page Down to the ends of the column, Ctrl+Home / Ctrl+End to the first and last day. Enter or Space selects when onSelectDate is set; Escape hides the tooltip.
|
|
78
|
+
- Each cell is named with its date and value ("Tuesday, October 14, 2026: 12 contributions") and described by its level's words, which the legend repeats visually.
|
|
79
|
+
- Levels rise in lightness, so they read without colour; under forced colours each level draws as a larger inner square.
|
|
80
|
+
|
|
81
|
+
## Related
|
|
82
|
+
|
|
83
|
+
- Components: Card, NumberTicker, BarChart, Calendar, Scroller
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Contributions, last year
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { ActivityHeatmap } from "@sableui/react"
|
|
91
|
+
|
|
92
|
+
const days = [
|
|
93
|
+
{ date: "2026-10-12", value: 4 },
|
|
94
|
+
{ date: "2026-10-13", value: 11 },
|
|
95
|
+
{ date: "2026-10-14", value: 2 },
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
<ActivityHeatmap label="Contributions in the last year" data={days} tone="success" unit={{ one: "contribution", other: "contributions" }} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Two sources, in a card
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { ActivityHeatmap, Card, CardContent, CardHeader, CardTitle, mergeActivity } from "@sableui/react"
|
|
105
|
+
|
|
106
|
+
const github = [{ date: "2026-10-13", value: 6 }]
|
|
107
|
+
const codex = [{ date: "2026-10-13", value: 1 }, { date: "2026-10-14", value: 3 }]
|
|
108
|
+
|
|
109
|
+
<Card>
|
|
110
|
+
<CardHeader>
|
|
111
|
+
<CardTitle>Building, day by day</CardTitle>
|
|
112
|
+
</CardHeader>
|
|
113
|
+
<CardContent>
|
|
114
|
+
<ActivityHeatmap
|
|
115
|
+
label="GitHub and Codex activity in 2026"
|
|
116
|
+
year={2026}
|
|
117
|
+
levels={2}
|
|
118
|
+
data={mergeActivity({ github, codex })}
|
|
119
|
+
categories={{ github: { label: "GitHub", tone: "success" }, codex: { label: "Codex", tone: "info" }, both: { label: "Both", tone: "2" } }}
|
|
120
|
+
/>
|
|
121
|
+
</CardContent>
|
|
122
|
+
</Card>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Selecting a day
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { ActivityHeatmap } from "@sableui/react"
|
|
129
|
+
|
|
130
|
+
const [day, setDay] = useState<string | null>(null)
|
|
131
|
+
|
|
132
|
+
<ActivityHeatmap label="Orders per day" data={{ "2026-10-13": 18, "2026-10-14": 25 }} unit="orders" selectedDate={day} onSelectDate={setDay} />
|
|
133
|
+
```
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Circular profile image over Base UI Avatar — automatic image-load tracking with an initials/glyph fallback. Ergonomic Avatar plus low-level AvatarRoot/AvatarImage/AvatarFallback parts, and AvatarGroup for an overlapping stack with a "+N" overflow count.
|
|
5
|
+
Circular (or square) profile image over Base UI Avatar — automatic image-load tracking with an initials/glyph fallback. Ergonomic Avatar plus low-level AvatarRoot/AvatarImage/AvatarFallback parts, and AvatarGroup for an overlapping stack with a "+N" overflow count.
|
|
6
6
|
|
|
7
|
-
**Keywords:** avatar, profile, user, identity, initials, fallback, group, team, stack, overflow, collaborators, facepile
|
|
7
|
+
**Keywords:** avatar, profile, user, identity, initials, fallback, group, team, stack, overflow, collaborators, facepile, monogram, project mark, workspace icon, org logo, assignee
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -15,12 +15,13 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
15
15
|
- Do: Use `variant="art"` where people without photos sit side by side — a member list, an activity feed, a chat. Seeded by the person, each gets their own colour, so faces stay tellable apart at a glance; the neutral fallback makes everyone the same grey circle.
|
|
16
16
|
- Do: Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person.
|
|
17
17
|
- Do: Use initials or a simple glyph fallback so missing images still communicate identity.
|
|
18
|
-
- Do: Pick the size by the job: sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group.
|
|
18
|
+
- Do: Pick the size by the job: 2xs (20px) or xs (24px) in a compact workspace row (an assignee in an issue list, a project mark in a nav item), sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group.
|
|
19
19
|
- Do: Use AvatarGroup for a stack of people, and give it an aria-label with the real count ("12 collaborators"). Set `surface` (or `--avatar-group-ring`) to whatever the group sits on, so the separating ring matches a card or popover, not the page. When it shares a row with actions, set `fit` so it gives up avatars to the "+N" count before the row has to wrap.
|
|
20
|
+
- Do: Use `shape="square"` for what is not a person (a workspace, a project, an org, a team), with `tone` set to a palette family so each one keeps its own colour: `<Avatar shape="square" tone="teal" fallback="N" alt="Northwind storefront" size="xs" />`. Circles mean people; keep that distinction across the app.
|
|
20
21
|
- Don't: Hand-build a stack with negative margins and per-avatar ring classes, or fake the overflow as an Avatar with "+4" initials — screen readers hear "plus 4" with no context. Use AvatarGroup's `max`/`total`, or AvatarGroupCount.
|
|
21
22
|
- Don't: Use avatars as decoration without nearby identity context. They should help users recognize a person or account.
|
|
22
23
|
- Don't: Overload a dense table with large avatars; use sm or omit images when scanning data is more important.
|
|
23
|
-
- Don't:
|
|
24
|
+
- Don't: Resize an Avatar with size classes (`className="size-5"`, `className="size-40"`) — the initials, the status indicator and the image fade stay at the size they were drawn for. Use size="2xs"/"xs" for dense rows and size="xl"/"2xl" for portraits, which scale all three. Don't hand-build a tinted initials tile for a project or workspace mark either: that is Avatar shape="square" with a tone. Past 160px, or for a cover, it is media: an AspectBox, not an avatar.
|
|
24
25
|
|
|
25
26
|
### Anatomy
|
|
26
27
|
|
|
@@ -36,11 +37,11 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
36
37
|
|
|
37
38
|
| Name | Type | Description |
|
|
38
39
|
| --- | --- | --- |
|
|
39
|
-
| AvatarRoot | Base UI Avatar.Root props + size + className | Low-level circular frame for full composition control — clips the image and holds the `size` axis. Wraps AvatarImage and AvatarFallback. |
|
|
40
|
+
| AvatarRoot | Base UI Avatar.Root props + size + shape + tone + className | Low-level circular frame for full composition control — clips the image and holds the `size` axis. Wraps AvatarImage and AvatarFallback. |
|
|
40
41
|
| AvatarImage | Base UI Avatar.Image props + className | The profile image. Fills and crops the frame; hidden by Base UI until it loads. |
|
|
41
42
|
| AvatarFallback | Base UI Avatar.Fallback props (delay) + className | Shown when no image is available — typically initials or a glyph. Accepts a delay prop. |
|
|
42
|
-
| AvatarGroup | { size?: "sm" \| "md" \| "lg"; surface?: "background" \| "card" \| "popover" \| "muted"; max?: number; fit?: boolean; total?: number; countLabel?: (count: number) => string; "aria-label"?: string; className?: string; children: ReactNode } | Overlapping stack (role=group) — name it with aria-label ("12 collaborators"). `size` applies to every avatar inside. `max` shows that many and collapses the rest into a "+N" count; `total` is the real head-count when you only pass some people. `fit` shows only as many avatars as fit the width the group is given (it fills the space left in a flex row) and folds the rest into the count, so a tight row reads "MO SR +5" instead of wrapping. The separating ring reads the inherited `--avatar-group-ring` (default: the page background); `surface` sets it to card, popover, or muted. |
|
|
43
|
-
| AvatarGroupCount | { count: number; label?: string; size?: "sm" \| "md" \| "lg"; className?: string } | The "+N" avatar. Shows "+4" and reads "and 4 more" (override with label). AvatarGroup renders it for you from max/total; use it directly only when composing the stack yourself. |
|
|
43
|
+
| AvatarGroup | { size?: "2xs" \| "xs" \| "sm" \| "md" \| "lg"; surface?: "background" \| "card" \| "popover" \| "muted"; max?: number; fit?: boolean; total?: number; countLabel?: (count: number) => string; "aria-label"?: string; className?: string; children: ReactNode } | Overlapping stack (role=group) — name it with aria-label ("12 collaborators"). `size` applies to every avatar inside. `max` shows that many and collapses the rest into a "+N" count; `total` is the real head-count when you only pass some people. `fit` shows only as many avatars as fit the width the group is given (it fills the space left in a flex row) and folds the rest into the count, so a tight row reads "MO SR +5" instead of wrapping. The separating ring reads the inherited `--avatar-group-ring` (default: the page background); `surface` sets it to card, popover, or muted. |
|
|
44
|
+
| AvatarGroupCount | { count: number; label?: string; size?: "2xs" \| "xs" \| "sm" \| "md" \| "lg"; className?: string } | The "+N" avatar. Shows "+4" and reads "and 4 more" (override with label). AvatarGroup renders it for you from max/total; use it directly only when composing the stack yourself. |
|
|
44
45
|
|
|
45
46
|
## Props
|
|
46
47
|
|
|
@@ -51,13 +52,16 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
51
52
|
| fallback | ReactNode | — | Initials or a glyph rendered when no image is displayed. |
|
|
52
53
|
| fallbackDelay | number | — | Delay before showing the fallback, in ms — avoids a flash on fast loads. |
|
|
53
54
|
| status | ReactNode | — | A presence indicator pinned to the bottom-end edge, usually <StatusDot tone="success" />. It sits outside the clipped circle and wears a ring in the surface colour (--avatar-group-ring, else the page background). Use this instead of absolutely positioning a dot over an Avatar. |
|
|
54
|
-
| size | "sm" \| "md" \| "lg" \| "xl" \| "2xl" | "md" | Square footprint: 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface. |
|
|
55
|
+
| size | "2xs" \| "xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" | "md" | Square footprint: 20px / 24px for compact workspace rows; 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface. |
|
|
56
|
+
| shape | "circle" \| "square" | "circle" | The frame's corner. circle is a person. square is what is not a face — a workspace, a project, an org, a team — with a radius from the theme's steps that grows with the size (4px at 20–24px to 20px at 160px), so a square theme stays square. A status indicator sits on the square's corner. |
|
|
57
|
+
| tone | "neutral" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "purple" \| "pink" | "neutral" | The fallback's colour. neutral is the muted surface; a palette family paints its wash behind the initials and its deep ink on them (a soft Badge's pair, held to contrast in every theme), for a category colour such as one per project. Ignored by variant="art", which colours by seed. |
|
|
55
58
|
| variant | "neutral" \| "art" | "neutral" | What the fallback looks like. neutral: initials on the muted surface. art: initials over GradientArt seeded by the person, so each person without a photo gets their own stable colour from the theme. |
|
|
56
59
|
| seed | string \| number | — | The art variant's seed. Defaults to the fallback text, then `alt`, so a person keeps one colour wherever they appear; pass a user id to tell apart two people who share initials. |
|
|
57
60
|
|
|
58
61
|
## Variants
|
|
59
62
|
|
|
60
|
-
- **size**: sm, md, lg, xl, 2xl
|
|
63
|
+
- **size**: 2xs, xs, sm, md, lg, xl, 2xl
|
|
64
|
+
- **shape**: circle, square
|
|
61
65
|
- **variant**: neutral, art
|
|
62
66
|
|
|
63
67
|
## Tokens used
|
|
@@ -134,6 +138,25 @@ import { Avatar, StatusDot } from "@sableui/react"
|
|
|
134
138
|
</div>
|
|
135
139
|
```
|
|
136
140
|
|
|
141
|
+
### Dense row sizes
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { Avatar } from "@sableui/react"
|
|
145
|
+
|
|
146
|
+
<Avatar fallback="EL" alt="Emma Lee" size="2xs" />
|
|
147
|
+
<Avatar fallback="EL" alt="Emma Lee" size="xs" />
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Project marks
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { Avatar } from "@sableui/react"
|
|
154
|
+
|
|
155
|
+
<Avatar shape="square" tone="teal" size="xs" fallback="N" alt="Northwind storefront" />
|
|
156
|
+
<Avatar shape="square" tone="orange" size="xs" fallback="H" alt="Harbor brand refresh" />
|
|
157
|
+
<Avatar shape="square" tone="purple" size="xs" fallback="L" alt="Lumen editorial" />
|
|
158
|
+
```
|
|
159
|
+
|
|
137
160
|
### Low-level parts
|
|
138
161
|
|
|
139
162
|
```tsx
|
|
@@ -40,6 +40,7 @@ Use CodeBlock for multiline code samples that users may read, inspect, or copy.
|
|
|
40
40
|
| showCopy | boolean | true | Show the copy affordance — in the header bar with a filename, floating in the corner without one. |
|
|
41
41
|
| highlight | ((code: string) => ReactNode) \| false | — | Highlighter for this block; overrides CodeHighlightProvider. false renders plain text inside a provider. |
|
|
42
42
|
| prompt | string | — | A shell prompt ("$", ">"). Makes it a one-line command: the prompt is dimmed and unselectable, the line truncates, and copy takes only code. |
|
|
43
|
+
| wrap | boolean | false | Wrap long lines instead of scrolling them. For prose in a code face (a prompt to paste into an agent, a log line); code keeps scrolling, since wrapping breaks its indentation. |
|
|
43
44
|
|
|
44
45
|
## Tokens used
|
|
45
46
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Show/hide a panel of content over Base UI Collapsible — open/close state, ARIA expanded wiring, and the animated height transition. The minimal primitive behind Accordion: a trigger that toggles an overflow-clipped panel.
|
|
6
6
|
|
|
7
|
-
**Keywords:** collapsible, disclosure, expand, collapse
|
|
7
|
+
**Keywords:** collapsible, disclosure, expand, collapse, chevron, caret, rotate, group header, section toggle
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -31,7 +31,7 @@ Use Collapsible for a single piece of optional content that can expand in place.
|
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| CollapsibleTrigger | Base UI Collapsible.Trigger props + render | The button that toggles the panel. Compose with render={<Button variant="ghost" … />}. |
|
|
33
33
|
| CollapsiblePanel | Base UI Collapsible.Panel props + className (keepMounted, hiddenUntilFound) | The collapsible contents. Overflow-hidden; animates height via the --collapsible-panel-height var Base UI sets. |
|
|
34
|
-
| CollapsibleIndicator | { className? } | Auto-rotating disclosure chevron. Place it inside the trigger content
|
|
34
|
+
| CollapsibleIndicator | { direction?: "down" \| "end"; className? } | Auto-rotating disclosure chevron. Place it anywhere inside the trigger content; it reads the trigger's open state itself, no class to add. direction="down" (default) flips 180° on open; direction="end" points to the inline end while closed and turns down on open — the caret for a grouped list's section header or a tree row. Use it instead of rotating your own chevron with CSS, which snaps with no transition. |
|
|
35
35
|
|
|
36
36
|
## Props
|
|
37
37
|
|
|
@@ -50,7 +50,7 @@ Use Collapsible for a single piece of optional content that can expand in place.
|
|
|
50
50
|
- **arrival** · Root · _no motion_ — A collapsible is part of the page it sits in, present from first paint. Its trigger is what the reader notices, not an entrance.
|
|
51
51
|
- **response** · CollapsibleTrigger · _no motion_ — The trigger is whatever the consumer renders (usually a Button), and that control brings its own response. Adding one here would double it.
|
|
52
52
|
- **transition** · CollapsiblePanel · `motion-disclosure` — The height change ties the content that appears to the trigger that was pressed; snapped open, everything below jumps with nothing linking cause to effect.
|
|
53
|
-
- **transition** · CollapsibleIndicator · `motion-interactive` — The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180
|
|
53
|
+
- **transition** · CollapsibleIndicator · `motion-interactive` — The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 and rotate-0 write.
|
|
54
54
|
- **activity** · Root · _no motion_ — Nothing here is indeterminate; content that loads on open should put its own loader inside the panel.
|
|
55
55
|
|
|
56
56
|
## Accessibility
|
|
@@ -87,5 +87,13 @@ import { Button, Collapsible, CollapsiblePanel, CollapsibleTrigger } from "@sabl
|
|
|
87
87
|
```tsx
|
|
88
88
|
import { Button, Collapsible, CollapsibleIndicator, CollapsiblePanel, CollapsibleTrigger } from "@sableui/react"
|
|
89
89
|
|
|
90
|
-
<Collapsible><CollapsibleTrigger
|
|
90
|
+
<Collapsible><CollapsibleTrigger render={<Button variant="ghost">Details <CollapsibleIndicator /></Button>} /><CollapsiblePanel><p className="pt-3">Revealed on toggle.</p></CollapsiblePanel></Collapsible>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Group header with a side caret
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { Button, Collapsible, CollapsibleIndicator, CollapsiblePanel, CollapsibleTrigger } from "@sableui/react"
|
|
97
|
+
|
|
98
|
+
<Collapsible defaultOpen><CollapsibleTrigger render={<Button variant="ghost" size="xs"><CollapsibleIndicator direction="end" />In progress <span className="text-ui-num text-muted-foreground">5</span></Button>} /><CollapsiblePanel><ul className="text-ui-body"><li>Refine product detail page</li><li>Build the ingredient library</li></ul></CollapsiblePanel></Collapsible>
|
|
91
99
|
```
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
A data-driven table over the Table primitives — a columns def + data array with built-in client-side sorting (click a sortable header to cycle asc → desc → none), optional row click / selection, and an empty state. For server data or paging, sort/page the data and pair with Pagination.
|
|
5
|
+
A data-driven table over the Table primitives — a columns def + data array with built-in client-side sorting (click a sortable header to cycle asc → desc → none), optional row click / selection, rows grouped under collapsible header rows (groupBy), and an empty state. For server data or paging, sort/page the data and pair with Pagination.
|
|
6
6
|
|
|
7
|
-
**Keywords:** data table, table, sortable, columns, rows, empty state, pagination, list
|
|
7
|
+
**Keywords:** data table, table, sortable, columns, rows, empty state, pagination, list, grouped, group by, sections, issue list, issue tracker, status groups, collapsible groups
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -16,7 +16,8 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
16
16
|
- Do: Provide stable rowKey values for dynamic data so selection and sorting do not jump.
|
|
17
17
|
- Do: Use sortable only on columns where ordering is obvious and useful.
|
|
18
18
|
- Do: Pair server-side datasets with Pagination and pass already-sorted/paged rows.
|
|
19
|
-
- Do:
|
|
19
|
+
- Do: Group rows by status for an issue tracker (or by owner, by day) with groupBy + groups on ONE DataTable: every group shares the header and the column widths, each header row carries the group's count and an add action, and sorting applies within each group. Do not stack one DataTable per group inside Collapsibles; the header repeats and the columns stop lining up.
|
|
20
|
+
- Do: Move to TanStack Table, rendered with Sable's Table parts, once the table needs server-side sorting or filtering, column visibility or resizing, nested or aggregated grouping, or virtualised rows. DataTable stays dependency-free and does not grow into a data grid; `sable docs tables-with-tanstack` maps one onto the other.
|
|
20
21
|
- Don't: Make every row clickable and also fill it with competing buttons. Choose a clear row interaction model.
|
|
21
22
|
- Don't: Use DataTable for heterogeneous cards or timeline content. A list or card layout will scan better.
|
|
22
23
|
|
|
@@ -27,6 +28,7 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
27
28
|
| Columns | yes | Definitions for headers, cell renderers, sorting, alignment, and classes. |
|
|
28
29
|
| Rows | yes | The data array rendered through the column definitions. |
|
|
29
30
|
| Empty state | no | Content spanning all columns when the row array is empty. |
|
|
31
|
+
| Group header row | no | With groupBy: one row per group heading its own tbody (a th with scope=rowgroup across every column) holding the disclosure toggle, the group's icon, label and count, and an optional trailing action. |
|
|
30
32
|
| Caption | no | Accessible table caption rendered by the underlying Table. |
|
|
31
33
|
|
|
32
34
|
## Props
|
|
@@ -45,6 +47,11 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
45
47
|
| defaultSort | { key: string; direction: "asc" \| "desc" } | — | Initial sort column + direction. |
|
|
46
48
|
| empty | ReactNode | "No results." | Rendered spanning all columns when data is empty. Pass an EmptyState for the full treatment (icon, title, next step). |
|
|
47
49
|
| caption | ReactNode | — | Bottom table caption. |
|
|
50
|
+
| groupBy | (row) => string | — | Group the rows under header rows by the key this returns (a status, an owner, a day). One header serves every group, so the columns line up across them; sorting applies within each group. |
|
|
51
|
+
| groups | { key: string; label?: ReactNode; icon?: ReactNode; action?: ReactNode; defaultExpanded?: boolean }[] | — | Order and presentation of the groups, by key: the header row's label (defaults to the key), a leading icon such as a status mark, a trailing action such as an icon Button that adds a row to the group, and whether it starts open (default true). Groups groupBy returns that are not listed follow, in the order their first row appears in data. A listed group with no rows is not shown. |
|
|
52
|
+
| collapsible | boolean | true | Whether each group's header row is a toggle that opens and closes the group. A closed group keeps its header row and count; its rows are hidden and select-all skips them. |
|
|
53
|
+
| expandedGroups | ReadonlySet<string> | — | The open groups, by key (controlled). Without it each group starts at its defaultExpanded and the table holds the state. |
|
|
54
|
+
| onExpandedGroupsChange | (keys: Set<string>) => void | — | Called with the new set of open group keys when a group header is toggled. |
|
|
48
55
|
|
|
49
56
|
## Tokens used
|
|
50
57
|
|
|
@@ -53,9 +60,11 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
53
60
|
## Motion
|
|
54
61
|
|
|
55
62
|
- **arrival** · Root · _no motion_ — Rows render with the page or the query that produced them. Staggering rows in delays reading a table someone opened to scan.
|
|
63
|
+
- **arrival** · Group rows · _no motion_ — Opening a group shows its rows at once: table rows cannot animate their height without leaving the table layout, and the rows are the content the reader asked for.
|
|
56
64
|
- **response** · Row · `motion-interactive` — Body rows ease to a muted wash under the pointer (TableRow's recipe), which keeps the eye on the right line across a wide row of numbers. Sortable headers are ghost Buttons and answer on Button's recipe.
|
|
57
65
|
- **transition** · Sort chevron · `motion-interactive` — One chevron turns to show direction instead of two glyphs swapping, so ascending to descending reads as the same column reversing. Its opacity rises from the resting 60% as the column becomes the sort.
|
|
58
66
|
- **transition** · Row · `motion-interactive` — A checked row's fill eases in across its cells, so selecting by checkbox reads as the row being picked rather than a band stamped over it.
|
|
67
|
+
- **transition** · Group chevron · `motion-interactive` — A group's chevron turns from pointing along the reading direction (closed) to down (open), so the group reads as folding rather than a glyph being swapped.
|
|
59
68
|
- **activity** · Root · _no motion_ — DataTable has no loading state. While data is fetched, render Skeleton rows (motion-shimmer) in its place — only the caller knows a fetch is in flight.
|
|
60
69
|
|
|
61
70
|
## Accessibility
|
|
@@ -63,6 +72,7 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
63
72
|
- Sorted columns set aria-sort=ascending/descending on the header cell.
|
|
64
73
|
- Sortable headers are real <button>s with a focus-visible ring, keyboard-operable.
|
|
65
74
|
- Inherits the Table's scrollable frame and semantic <table>/<th>/<td> structure.
|
|
75
|
+
- Grouped, each group is its own <tbody> (a rowgroup) headed by a <th scope="rowgroup"> spanning the columns, so group headers are never read as data rows; the toggle is a real button with aria-expanded and aria-controls naming that tbody.
|
|
66
76
|
|
|
67
77
|
## Related
|
|
68
78
|
|
|
@@ -131,6 +141,33 @@ import { Card, CardContent, CardHeader, CardTitle, DataTable } from "@sableui/re
|
|
|
131
141
|
<Card><CardHeader><CardTitle>Deployments</CardTitle></CardHeader><CardContent flush><DataTable data={deployments} rowKey={(row) => row.id} columns={[{ key: "project", header: "Project", sortable: true }, { key: "duration", header: "Duration", align: "end" }]} /></CardContent></Card>
|
|
132
142
|
```
|
|
133
143
|
|
|
144
|
+
### Issues grouped by status
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Button, DataTable, Icon, StatusDot } from "@sableui/react"
|
|
148
|
+
|
|
149
|
+
const STATUSES = [
|
|
150
|
+
{ key: "in-progress", label: "In progress", icon: <StatusDot tone="warning" /> },
|
|
151
|
+
{ key: "in-review", label: "In review", icon: <StatusDot tone="purple" /> },
|
|
152
|
+
{ key: "todo", label: "Todo", icon: <StatusDot /> },
|
|
153
|
+
{ key: "done", label: "Done", icon: <StatusDot tone="success" />, defaultExpanded: false },
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
<DataTable
|
|
157
|
+
data={issues}
|
|
158
|
+
rowKey={(t) => t.id}
|
|
159
|
+
groupBy={(t) => t.status}
|
|
160
|
+
groups={STATUSES.map((s) => ({ ...s, action: <Button variant="ghost" size="icon-xs" aria-label={`Add issue to ${s.label}`} onClick={() => create(s.key)}><Icon name="plus" /></Button> }))}
|
|
161
|
+
onRowClick={(t) => setSelectedId(t.id)}
|
|
162
|
+
isRowSelected={(t) => t.id === selectedId}
|
|
163
|
+
columns={[
|
|
164
|
+
{ key: "id", header: "ID", className: "w-20", cell: (t) => <span className="text-mono-caption text-muted-foreground">{t.id}</span> },
|
|
165
|
+
{ key: "title", header: "Issue", sortable: true },
|
|
166
|
+
{ key: "due", header: "Due", sortable: true, sortValue: (t) => t.due.getTime(), cell: (t) => formatDate(t.due) },
|
|
167
|
+
]}
|
|
168
|
+
/>
|
|
169
|
+
```
|
|
170
|
+
|
|
134
171
|
### Empty state
|
|
135
172
|
|
|
136
173
|
```tsx
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Date (and optionally time) picker: a field-shaped trigger (the Input / SelectTrigger shell) opens a Popover holding Sable's Calendar. With granularity="day" selecting a day fires onValueChange and closes; granularity="minute" adds an hour + minute row of Selects and keeps the time-of-day when the day changes.
|
|
6
6
|
|
|
7
|
-
**Keywords:** date picker, calendar, date input, date time picker, time, schedule, form
|
|
7
|
+
**Keywords:** date picker, due date, property row, inspector, calendar, date input, date time picker, time, schedule, form
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -33,6 +33,8 @@ Use DatePicker when users need to choose one date — or, with granularity="minu
|
|
|
33
33
|
| disabled | boolean | — | Disable the trigger and block selection. |
|
|
34
34
|
| minDate | Date | — | Earliest selectable day (inclusive). Earlier days are disabled and navigation is bounded. |
|
|
35
35
|
| maxDate | Date | — | Latest selectable day (inclusive). Later days are disabled and navigation is bounded. |
|
|
36
|
+
| size | "md" \| "xs" | "md" | The trigger's rung. md is the form field beside Input and Select. xs is the property-grid rung (24px in a workspace): an inset panel-field well with no border, the same shape as SelectTrigger size="xs" and ScrubField on the neighbouring rows of an inspector — use it for a due date in a PropertyRow instead of a native date input. |
|
|
37
|
+
| formatOptions | Intl.DateTimeFormatOptions | — | How the trigger writes the chosen date, as Intl.DateTimeFormat options in the reader's locale — { month: "short", day: "numeric" } for "Oct 23" where the rest of a list writes dates that way. Omitted: "Oct 23, 2026" (with "at 14:30" under granularity="minute"). |
|
|
36
38
|
| className | string | — | Extra classes merged onto the trigger. |
|
|
37
39
|
| id / aria-label / aria-labelledby / aria-describedby / aria-invalid / aria-required | button attributes | — | Forwarded to the trigger, so a wrapping Field ties its label to the picker and announces its description and error. Pass them yourself only when labelling the picker without a Field. |
|
|
38
40
|
|
|
@@ -54,6 +56,18 @@ Use DatePicker when users need to choose one date — or, with granularity="minu
|
|
|
54
56
|
|
|
55
57
|
## Examples
|
|
56
58
|
|
|
59
|
+
### In an inspector row
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { DatePicker, PanelSection, PropertyRow } from "@sableui/react"
|
|
63
|
+
|
|
64
|
+
<PanelSection title="Properties">
|
|
65
|
+
<PropertyRow label="Due date">
|
|
66
|
+
<DatePicker size="xs" aria-label="Due date" value={due} onValueChange={setDue} formatOptions={{ month: "short", day: "numeric" }} />
|
|
67
|
+
</PropertyRow>
|
|
68
|
+
</PanelSection>
|
|
69
|
+
```
|
|
70
|
+
|
|
57
71
|
### Controlled
|
|
58
72
|
|
|
59
73
|
```tsx
|
|
@@ -17,11 +17,14 @@ Use Dialog for focused decisions, short modal forms, confirmations, and content
|
|
|
17
17
|
- Do: Use showCloseButton for dismissible informational dialogs; keep destructive confirmations action-led.
|
|
18
18
|
- Do: In a destructive confirmation, state the consequence in DialogDescription ("This is permanent.") and reserve the destructive button variant for actions that cannot be undone.
|
|
19
19
|
- Do: Use size="fullscreen" when an image, video, or gallery needs a larger view than the layout can give it. Give the popup an accessible name (aria-label on DialogContent, or a visually hidden DialogTitle) and alt text on the media.
|
|
20
|
+
- Do: Open a dialog from your own button with controlled open/onOpenChange when the trigger is not a DialogTrigger (a menu item, a row action, a keyboard shortcut). Focus returns on close to whatever held it when the dialog opened, with no finalFocus wiring; pass finalFocus only to send it somewhere else.
|
|
21
|
+
- Do: Set keepMounted on DialogContent for a draft the user may come back to (a compose form, a long comment, a half-filled request). Closing by Esc or a stray click then hides the dialog instead of unmounting it, so the fields are still filled when it reopens. Clear the draft yourself on submit or discard.
|
|
20
22
|
- Do: Use the AlertDialog family for a confirmation that must be answered (discard changes, leave without saving). It looks identical but is role="alertdialog" and a backdrop click does not dismiss it — only Cancel, Esc, or the action. For irreversible deletes, use DestructiveActionModal, which is built on it.
|
|
21
23
|
- Don't: Put large multipage workflows in a centered dialog. Use Sheet or a page route when the task needs space. → use **Sheet**
|
|
22
24
|
- Don't: Use size="fullscreen" for forms, confirmations, or anything that isn't media. A dialog that happens to need room is a Sheet or a page; the fullscreen affordances (dark scrim, corner close, centred content) all assume something to look at. → use **Sheet**
|
|
23
25
|
- Don't: Hide the close button at size="fullscreen" without another visible way out. A full-bleed viewer that only Esc dismisses is a dead end for anyone who doesn't know the key.
|
|
24
26
|
- Don't: Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead. → use **Toast**, **Banner**, **InlineStatus**
|
|
27
|
+
- Don't: Set keepMounted by default. A dialog that should open fresh (a confirmation, a create form after a successful save) would show stale input, and live content (polling, video) keeps running while hidden.
|
|
25
28
|
|
|
26
29
|
### Anatomy
|
|
27
30
|
|
|
@@ -37,7 +40,7 @@ Use Dialog for focused decisions, short modal forms, confirmations, and content
|
|
|
37
40
|
|
|
38
41
|
| Name | Type | Description |
|
|
39
42
|
| --- | --- | --- |
|
|
40
|
-
| DialogContent | Base UI Popup props + size?: "default" \| "fullscreen" + showCloseButton?: boolean + scroll?: boolean + className | Backdrop + popup; portals to <body>. Composes DialogOverlay by default. size="fullscreen" is the media viewer: a near-opaque scrim (bg-overlay/95), an edge-to-edge popup that centres its children with no card chrome, motion-fade instead of motion-pop, and a safe-area-aware floating close button (Button variant="floating"); at that size showCloseButton defaults to true and scroll to false, and the media fits itself with max-h-full max-w-full object-contain. At the default size, showCloseButton (defaults to false) renders a built-in close (X) button in the top-end corner, and the header leaves room for it (Icon name="xmark"). The popup caps at the viewport height less a 1rem margin and wraps its children in a scrolling region, so a tall dialog scrolls instead of overflowing a viewport whose own scroll is locked; pass scroll={false} when the dialog owns its internal layout (a pinned header over a scrolling body) and supplies its own min-h-0 scroll region. |
|
|
43
|
+
| DialogContent | Base UI Popup props (initialFocus, finalFocus) + size?: "default" \| "fullscreen" + showCloseButton?: boolean + scroll?: boolean + keepMounted?: boolean + className | Backdrop + popup; portals to <body>. Composes DialogOverlay by default. size="fullscreen" is the media viewer: a near-opaque scrim (bg-overlay/95), an edge-to-edge popup that centres its children with no card chrome, motion-fade instead of motion-pop, and a safe-area-aware floating close button (Button variant="floating"); at that size showCloseButton defaults to true and scroll to false, and the media fits itself with max-h-full max-w-full object-contain. At the default size, showCloseButton (defaults to false) renders a built-in close (X) button in the top-end corner, and the header leaves room for it (Icon name="xmark"). The popup caps at the viewport height less a 1rem margin and wraps its children in a scrolling region, so a tall dialog scrolls instead of overflowing a viewport whose own scroll is locked; pass scroll={false} when the dialog owns its internal layout (a pinned header over a scrolling body) and supplies its own min-h-0 scroll region. keepMounted (default false) keeps the content mounted and hidden while closed, so a draft survives close and reopen. finalFocus defaults to the element focused when the dialog opened, trigger or not; an explicit finalFocus (a ref, a function, or false) wins. |
|
|
41
44
|
| DialogOverlay | Base UI Backdrop props + size?: "default" \| "fullscreen" + className | The styleable scrim/backdrop — bg-overlay/(--scrim-alpha) by default (50% light, 72% dark, so it dims a near-black page as visibly as a white one), near-opaque bg-overlay/95 at size="fullscreen". Composed into DialogContent (at the matching size) by default; exported for manual styling. |
|
|
42
45
|
| DialogTrigger | Base UI Dialog.Trigger props (render) | Element that opens the dialog. Compose via render prop. |
|
|
43
46
|
| DialogClose | Base UI Dialog.Close props (render) | Element that dismisses the dialog. Compose via render prop. |
|
|
@@ -45,7 +48,7 @@ Use Dialog for focused decisions, short modal forms, confirmations, and content
|
|
|
45
48
|
| DialogTitle | Base UI Dialog.Title props + className | The dialog's accessible name — wires aria-labelledby on the popup. |
|
|
46
49
|
| DialogDescription | Base UI Dialog.Description props + className | Muted supporting line; wires aria-describedby. |
|
|
47
50
|
| DialogFooter | ComponentProps<"div"> | Bottom action row (right-aligned buttons). |
|
|
48
|
-
| AlertDialog (Root) | Base UI AlertDialog.Root props (open, defaultOpen, onOpenChange) | Root for a confirmation that needs an answer. Always modal; the popup is role="alertdialog" and a click outside does not dismiss — only an AlertDialogClose, an action that sets open to false, or Esc. |
|
|
51
|
+
| AlertDialog (Root) | Base UI AlertDialog.Root props (open, defaultOpen, onOpenChange) | Root for a confirmation that needs an answer. Always modal; the popup is role="alertdialog" and a click outside does not dismiss — only an AlertDialogClose, an action that sets open to false, or Esc. Returns focus on close to the element focused when it opened, exactly as Dialog does. |
|
|
49
52
|
| AlertDialogTrigger | Base UI AlertDialog.Trigger props (render) | Opens the alert dialog. |
|
|
50
53
|
| AlertDialogContent / AlertDialogOverlay / AlertDialogTitle / AlertDialogDescription / AlertDialogHeader / AlertDialogFooter / AlertDialogClose | Same props as the matching Dialog part | The Dialog parts under AlertDialog names — Base UI shares the popup, backdrop, title, description and close parts between the two, so the surface is identical. Pass initialFocus to AlertDialogContent (a ref to the Cancel button) to open focus on the safe action. |
|
|
51
54
|
|
|
@@ -53,7 +56,10 @@ Use Dialog for focused decisions, short modal forms, confirmations, and content
|
|
|
53
56
|
|
|
54
57
|
| Name | Type | Default | Description |
|
|
55
58
|
| --- | --- | --- | --- |
|
|
56
|
-
| Dialog (Root) | Base UI Dialog.Root props (open, defaultOpen, onOpenChange, modal) | — | Controls open state. Uncontrolled by default. |
|
|
59
|
+
| Dialog (Root) | Base UI Dialog.Root props (open, defaultOpen, onOpenChange, modal) | — | Controls open state. Uncontrolled by default. Records the element that holds focus when the dialog opens (through a DialogTrigger, or your own button flipping a controlled open) and returns focus there on close. |
|
|
60
|
+
| open | boolean | — | Controlled open state. Pair with onOpenChange — for a dialog opened from your own button instead of a DialogTrigger. |
|
|
61
|
+
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
|
|
62
|
+
| onOpenChange | (open: boolean, eventDetails) => void | — | Called when the dialog asks to open or close (a trigger, Esc, an outside click, DialogClose). |
|
|
57
63
|
|
|
58
64
|
## Variants
|
|
59
65
|
|
|
@@ -75,6 +81,8 @@ Use Dialog for focused decisions, short modal forms, confirmations, and content
|
|
|
75
81
|
|
|
76
82
|
- Base UI provides focus trapping, scroll lock, aria-modal, and labelled-by wiring (DialogTitle).
|
|
77
83
|
- Content taller than the viewport scrolls inside the dialog (thin scrollbar, contained overscroll) rather than overflowing off screen — the page behind is scroll-locked, so anything past the edge would be unreachable.
|
|
84
|
+
- On close, focus returns to the element that held it when the dialog opened, whether a DialogTrigger opened it or your own button set a controlled open. It is captured before the content mounts, so an autoFocus field inside cannot displace it. If that element has left the page, Base UI's default (the trigger, if any) applies; an explicit finalFocus on the content always wins. AlertDialog and Sheet behave the same.
|
|
85
|
+
- keepMounted hides closed content with the hidden attribute, so a kept draft is out of the accessibility tree and the tab order until the dialog reopens.
|
|
78
86
|
- Esc and click-outside dismiss by default. AlertDialog keeps Esc but ignores click-outside, and its popup is role="alertdialog".
|
|
79
87
|
- Animate via data-[starting-style]/data-[ending-style] attributes.
|
|
80
88
|
- size="fullscreen" has no title chrome, so name the popup yourself: aria-label on DialogContent (or a visually hidden DialogTitle), plus alt text on the media — the zoomed image is a separate image to assistive technology. Its built-in close button carries aria-label="Close" and clears the safe-area insets (notch, home indicator).
|
|
@@ -118,6 +126,29 @@ import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFo
|
|
|
118
126
|
<Dialog><DialogTrigger render={<Button variant="secondary">Rename collection</Button>} /><DialogContent showCloseButton><DialogHeader><DialogTitle>Rename collection</DialogTitle><DialogDescription>Update the display name shown to collectors.</DialogDescription></DialogHeader><Form><Field label="Collection name"><Input defaultValue="Midnight Archive" /></Field><DialogFooter><DialogClose render={<Button variant="secondary">Cancel</Button>} /><Button type="submit">Save changes</Button></DialogFooter></Form></DialogContent></Dialog>
|
|
119
127
|
```
|
|
120
128
|
|
|
129
|
+
### Opened from your own button, keeping a draft
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Textarea } from "@sableui/react"
|
|
133
|
+
|
|
134
|
+
const [open, setOpen] = useState(false)
|
|
135
|
+
|
|
136
|
+
<Button variant="secondary" onClick={() => setOpen(true)}>New message</Button>
|
|
137
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
138
|
+
<DialogContent keepMounted showCloseButton>
|
|
139
|
+
<DialogHeader>
|
|
140
|
+
<DialogTitle>New message</DialogTitle>
|
|
141
|
+
<DialogDescription>Close it any time. Your draft is here when you come back.</DialogDescription>
|
|
142
|
+
</DialogHeader>
|
|
143
|
+
<Field label="Message"><Textarea /></Field>
|
|
144
|
+
<DialogFooter>
|
|
145
|
+
<DialogClose render={<Button variant="secondary">Close</Button>} />
|
|
146
|
+
<Button onClick={() => setOpen(false)}>Send</Button>
|
|
147
|
+
</DialogFooter>
|
|
148
|
+
</DialogContent>
|
|
149
|
+
</Dialog>
|
|
150
|
+
```
|
|
151
|
+
|
|
121
152
|
### Destructive confirmation
|
|
122
153
|
|
|
123
154
|
```tsx
|