@sableui/react 0.16.0 → 0.18.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.
Files changed (95) hide show
  1. package/AGENTS.md +10 -3
  2. package/CHANGELOG.md +132 -0
  3. package/README.md +11 -4
  4. package/bin/sable.mjs +160 -18
  5. package/dist/activity-DT8VK9fx.d.ts +25 -0
  6. package/dist/adapters/github.d.ts +41 -0
  7. package/dist/adapters/github.js +24 -0
  8. package/dist/blocks/approval-block.js +30 -48
  9. package/dist/blocks/approval-block.meta.js +7 -8
  10. package/dist/blocks/index.d.ts +1 -1
  11. package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
  12. package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
  13. package/dist/components/activity-heatmap/index.js +9 -0
  14. package/dist/components/code-block/code-block.js +5 -3
  15. package/dist/components/code-block/code-block.meta.js +6 -0
  16. package/dist/components/dialog/dialog.js +25 -5
  17. package/dist/components/dialog/dialog.meta.js +26 -5
  18. package/dist/components/media-image/media-image.js +3 -1
  19. package/dist/components/media-image/media-image.meta.js +15 -1
  20. package/dist/components/media-tile/media-tile.js +13 -1
  21. package/dist/components/media-tile/media-tile.meta.js +10 -0
  22. package/dist/components/page-header/page-header.js +9 -4
  23. package/dist/components/page-header/page-header.meta.js +26 -1
  24. package/dist/components/pill/pill.meta.js +15 -6
  25. package/dist/components/pill/pill.variants.js +1 -0
  26. package/dist/components/search-input/search-input.js +30 -5
  27. package/dist/components/search-input/search-input.meta.js +29 -4
  28. package/dist/components/section-header/section-header.js +27 -17
  29. package/dist/components/section-header/section-header.meta.js +27 -5
  30. package/dist/components/section-rail/index.js +7 -0
  31. package/dist/components/section-rail/section-rail.js +153 -0
  32. package/dist/components/section-rail/section-rail.meta.js +189 -0
  33. package/dist/components/sheet/sheet.js +15 -3
  34. package/dist/components/sheet/sheet.meta.js +2 -2
  35. package/dist/file-hashes.json +53 -39
  36. package/dist/icon-sets/reicon.js +23 -1
  37. package/dist/index.d.ts +364 -27
  38. package/dist/index.js +11 -1
  39. package/dist/layout/AspectBox.js +2 -5
  40. package/dist/layout/Bleed.js +10 -6
  41. package/dist/layout/Center.js +11 -3
  42. package/dist/layout/Columns.js +6 -3
  43. package/dist/layout/Grid.js +14 -6
  44. package/dist/layout/GridOverlay.js +7 -7
  45. package/dist/layout/Region.js +18 -3
  46. package/dist/layout/Row.js +27 -20
  47. package/dist/layout/Spacer.js +13 -2
  48. package/dist/layout/SplitPane.js +1 -1
  49. package/dist/layout/Stack.js +25 -18
  50. package/dist/layout/index.d.ts +57 -6
  51. package/dist/lib/chart/activity.js +32 -0
  52. package/dist/lib/chart/calendar.js +67 -0
  53. package/dist/lib/chart/chart-tooltip.js +2 -2
  54. package/dist/lib/chart/heat.js +55 -0
  55. package/dist/lib/focus-return.js +53 -0
  56. package/dist/lib/taxonomy.js +2 -0
  57. package/dist/lib/use-active-section.js +59 -0
  58. package/dist/styles.css +122 -114
  59. package/llms/blocks/approval-block.md +40 -55
  60. package/llms/components/activity-heatmap.md +133 -0
  61. package/llms/components/code-block.md +1 -0
  62. package/llms/components/dialog.md +34 -3
  63. package/llms/components/media-image.md +12 -0
  64. package/llms/components/media-tile.md +21 -0
  65. package/llms/components/page-header.md +16 -1
  66. package/llms/components/pill.md +24 -5
  67. package/llms/components/search-input.md +37 -4
  68. package/llms/components/section-header.md +28 -4
  69. package/llms/components/section-rail.md +101 -0
  70. package/llms/components/sheet.md +2 -2
  71. package/llms/layout/aspect-box.md +1 -1
  72. package/llms/layout/bleed.md +1 -1
  73. package/llms/layout/center.md +1 -1
  74. package/llms/layout/columns.md +1 -1
  75. package/llms/layout/grid.md +1 -1
  76. package/llms/layout/region.md +1 -1
  77. package/llms/layout/row.md +9 -1
  78. package/llms/layout/spacer.md +1 -1
  79. package/llms/layout/split-pane.md +1 -1
  80. package/llms/layout/stack.md +1 -1
  81. package/llms/tasks/auditing-an-app.md +31 -0
  82. package/llms/tasks/getting-started.md +10 -3
  83. package/llms/tasks/using-presets.md +1 -2
  84. package/llms-full.txt +516 -90
  85. package/llms.txt +5 -3
  86. package/package.json +6 -1
  87. package/registry.json +745 -46
  88. package/scripts/adoption.mjs +239 -0
  89. package/scripts/agent-setup.mjs +110 -18
  90. package/scripts/grade.mjs +17 -0
  91. package/scripts/wiring.mjs +2 -1
  92. package/skills/entry/SKILL.md +1 -1
  93. package/skills/sable/SKILL.md +33 -4
  94. package/skills/sable/agents/openai.yaml +1 -1
  95. package/targets/ios.json +18 -0
@@ -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
+ ```
@@ -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
 
@@ -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
@@ -15,6 +15,7 @@ Use MediaImage wherever content imagery may be missing or broken: a cover in a g
15
15
  - Do: Give the box the shape: `<AspectBox ratio={1} className="rounded-md"><MediaImage … /></AspectBox>`, or `size-14 rounded-md` on MediaImage itself for a thumbnail. It clips to whatever radius the box has.
16
16
  - Do: Pass `seed` (the title or an id) when `alt` is empty, so the fallback art is the item's own and stays the same in every list it appears in.
17
17
  - Do: Leave `alt` empty when a visible title sits next to the image — a tile, a row — and describe it when the image stands alone. A set `alt` also names the fallback, so the description survives a broken URL.
18
+ - Do: Set `position` when a cover crop would cut off the subject — a face high in a portrait, a product at one edge. Store the focus with the image (`"35% 50%"`) rather than re-cropping the file.
18
19
  - Don't: Do not hand-write `<img onError={() => setBroken(true)}>`. An image that fails before hydration never fires the handler on a prerendered page; MediaImage preloads off the DOM and catches it.
19
20
  - Don't: Do not use it for profile pictures. A person is a circle with initials — Avatar, whose `variant="art"` is the same fallback with the person's initials over it.
20
21
 
@@ -27,6 +28,7 @@ Use MediaImage wherever content imagery may be missing or broken: a cover in a g
27
28
  | seed | string \| number | — | Seeds the fallback art. Defaults to `alt`; pass the title or an id when `alt` is empty. |
28
29
  | fallback | ReactNode | — | Replaces the GradientArt fallback — a glyph or monogram. Fills the frame under the same clipping. |
29
30
  | fit | "cover" \| "contain" | "cover" | `cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped). |
31
+ | position | CSSProperties["objectPosition"] | "center" | Where the image sits in its frame — CSS `object-position`, the sibling of `fit`. Under `cover` it is the part the crop keeps (`"50% 30%"` for a face, `"bottom"` for a horizon); under `contain` it is the side the letterbox gives way to. |
30
32
  | srcSet | string | — | Responsive sources, passed to the `<img>`. |
31
33
  | sizes | string | — | The `sizes` attribute for `srcSet`. |
32
34
  | loading | "lazy" \| "eager" | "lazy" | Native lazy loading. Use `eager` for the first row of a grid above the fold. |
@@ -96,3 +98,13 @@ import { AspectBox, MediaImage } from "@sableui/react"
96
98
  <MediaImage src={brand.logo} alt={brand.name} fit="contain" />
97
99
  </AspectBox>
98
100
  ```
101
+
102
+ ### Crop focus per image
103
+
104
+ ```tsx
105
+ import { AspectBox, MediaImage } from "@sableui/react"
106
+
107
+ <AspectBox ratio={1} className="rounded-md">
108
+ <MediaImage src={project.thumbnail} seed={project.title} position={project.focus ?? "center"} />
109
+ </AspectBox>
110
+ ```
@@ -42,6 +42,7 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
42
42
  | sizes | string | — | The cover's layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`. |
43
43
  | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
44
44
  | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
45
+ | position | CSSProperties["objectPosition"] | "center" | Which part of the cover the crop keeps — CSS `object-position`, forwarded to the cover's MediaImage: `"50% 30%"` for a face, `"top"` for a poster's title. Store it with the image when each one frames differently. |
45
46
  | aspect | "square" \| "video" \| "poster" \| number | "square" | The cover's shape: square (albums, podcasts), video (16:9), poster (2:3 — films, books), or any width / height ratio. |
46
47
  | size | "sm" \| "md" \| "lg" | "md" | Moves the type tier, the cover-to-text gap, the corner and the overlay inset together. |
47
48
  | href | string | — | Where the tile goes. The title becomes the link, stretched over the whole tile. |
@@ -166,6 +167,26 @@ import { Grid, MediaTile } from "@sableui/react"
166
167
  </Grid>
167
168
  ```
168
169
 
170
+ ### Portfolio grid with crop focus
171
+
172
+ ```tsx
173
+ import { Grid, MediaTile } from "@sableui/react"
174
+
175
+ <Grid minItemWidth="16rem" gap={4}>
176
+ {projects.map((project) => (
177
+ <MediaTile
178
+ key={project.slug}
179
+ aspect="video"
180
+ title={project.title}
181
+ subtitle={project.client}
182
+ src={project.thumbnail}
183
+ position={project.focus}
184
+ href={`/work/${project.slug}`}
185
+ />
186
+ ))}
187
+ </Grid>
188
+ ```
189
+
169
190
  ### Poster shelf
170
191
 
171
192
  ```tsx
@@ -4,7 +4,7 @@
4
4
 
5
5
  In-flow page heading block: a title row (optional back control + actions) over an optional description, with room for children (filters, tabs) below.
6
6
 
7
- **Keywords:** page header, title, lede, page actions
7
+ **Keywords:** page header, title, lede, page actions, page title size, display title, editorial header, hero title
8
8
 
9
9
  ## Usage
10
10
 
@@ -14,6 +14,8 @@ Use PageHeader to establish a page's title, supporting description, and primary
14
14
 
15
15
  - Do: Use one PageHeader per major page or route.
16
16
  - Do: Keep the title literal and put explanation in the lede or nearby content.
17
+ - Do: Use size="lg" for an editorial page (a personal site, a marketing page, an article index): a display-tier title over a body-lg lede. Pair it with SectionHeader size="lg" for the sections below, so the steps stay display → heading-2 → body.
18
+ - Don't: Use size="lg" on product routes (settings, lists, dashboards). Display type is never product chrome: there it competes with the content it frames. Keep md, and do not restyle the title with a display class through className either.
17
19
  - Don't: Do not use PageHeader inside repeated cards, dialogs, or compact panels.
18
20
  - Don't: Do not use a second PageHeader for a section of the page. It is the route's one h1; each section below it takes a SectionHeader (an h2 with the same title row, description and actions).
19
21
  - Don't: Do not put it outside the Region that holds the page content. It then sits at a different measure from what it introduces, and the page reads as two documents.
@@ -27,6 +29,11 @@ Use PageHeader to establish a page's title, supporting description, and primary
27
29
  | back | ReactNode | — | Leading element before the title — typically a back control on drill-in routes. Keep it a ghost icon Button (size="icon", variant="ghost", aria-label="Back") so it stays quieter than the title. A ghost back control bleeds by its inset at the start, so its glyph lines up with the content edge below (Button bleed="none" opts out). |
28
30
  | actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the title would have less than 20rem beside them (on a phone, nearly always), and the title wraps rather than truncating. A ghost or link Button at either end of the group bleeds by its own padding, so its label meets the content edge (the end on the title row, the start once wrapped); filled buttons keep their edge on the line. Direct children only. |
29
31
  | titleAs | "h1" \| "h2" | "h1" | The title's heading element. Keep h1 — the route's one h1. Use h2 only when something above already owns the h1: a shell that renders the route title, or a page showing a PageHeader as an example. |
32
+ | size | "md" \| "lg" | "md" | The title's scale. md: text-heading-2 over a text-body-sm description, for product pages. lg: text-display-lg over a text-body-lg lede capped at the prose measure, with more space between and below, for an editorial page (personal site, marketing page, article index). Beside actions or a back control, a title line taller than the controls lifts by half the difference, so its first line stays centred on them. |
33
+
34
+ ## Variants
35
+
36
+ - **size**: md, lg
30
37
 
31
38
  ## Tokens used
32
39
 
@@ -51,6 +58,14 @@ import { Button, PageHeader } from "@sableui/react"
51
58
  <PageHeader title="Storage credits" description="Fund permanent storage." actions={<Button>Top up</Button>} />
52
59
  ```
53
60
 
61
+ ### Editorial page title (size lg)
62
+
63
+ ```tsx
64
+ import { Button, PageHeader } from "@sableui/react"
65
+
66
+ <PageHeader size="lg" title="Field notes" description="Essays on making things slowly, from a studio by the sea." actions={<Button variant="outline">Subscribe</Button>} />
67
+ ```
68
+
54
69
  ### One per page, inside the region
55
70
 
56
71
  ```tsx
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance="solid" (filters; selected lifts onto the primary surface) or "outline" (quiet chips that annotate content, like reactions); size md or sm. A static label is a Badge.
5
+ A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance="solid" (filters; selected lifts onto the primary surface) or "outline" (quiet chips that annotate content, like reactions); size lg, md or sm. A static label is a Badge.
6
6
 
7
7
  **Keywords:** pill, chip, filter, tag, toggle chip, category
8
8
 
@@ -14,9 +14,10 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
14
14
 
15
15
  - Do: Use short text that names the filter or choice.
16
16
  - Do: Reflect the current choice with selected, so the active filter reads at a glance.
17
+ - Do: In a toolbar where filter chips share a row with a search field ("All 8 · Products 5 · Open source 2 … [Find a project]"), pair Pill size="lg" with SearchInput size="sm", and Button size="sm" for any action beside them. All three stand on --control-sm (32px, 28 compact), the rung the spacing doctrine gives toolbar controls, so the row is one height in either density. Pill md (28px) has no text field of its height in the page register, so md beside a field always reads as a mismatch.
17
18
  - Don't: Do not use Pill as a static label. A pressable-looking chip that does nothing is the most common cause of dead clicks in a filter row — that is a Badge. → use **Badge**
18
19
  - Don't: Do not use Pill for primary actions or important system status.
19
- - Don't: Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance="outline" (and size="sm" for a small one); an override is a look that exists nowhere else in the system.
20
+ - Don't: Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance="outline" (and size="sm" for a small one); an override is a look that exists nowhere else in the system. Likewise never set h-* on a Pill to match a neighbour: pick the size that shares its rung.
20
21
 
21
22
  ## Props
22
23
 
@@ -24,14 +25,14 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
24
25
  | --- | --- | --- | --- |
25
26
  | selected | boolean | false | Marks the pill as chosen. Solid lifts onto the primary surface; outline takes a primary tint and soft primary edge. Sets aria-pressed. |
26
27
  | appearance | "solid" \| "outline" | "solid" | solid for a filter row; outline for quiet chips that annotate other content (a reaction under a message, a tag on a card), where a filled chip would compete with what it annotates. |
27
- | size | "md" \| "sm" | "md" | md sits on the density ladder's xs control height (28px); sm is 24px, for chips under or inside other content. |
28
+ | size | "lg" \| "md" \| "sm" | "md" | lg sits on --control-sm (32px, 28 compact), the toolbar rung it shares with SearchInput sm and Button sm, for a filter row beside a search field. md sits on --control-xs (28px, 24 compact), the chip in a row of its own or among content. sm is a fixed 24px (the target floor, which density does not shrink), for chips under or inside other content. |
28
29
  | 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. |
29
30
  | icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. |
30
31
 
31
32
  ## Variants
32
33
 
33
34
  - **appearance**: solid, outline
34
- - **size**: md, sm
35
+ - **size**: lg, md, sm
35
36
  - **selected**: true, false
36
37
  - **tone**: neutral, red, orange, yellow, green, teal, blue, purple, pink
37
38
 
@@ -54,7 +55,7 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
54
55
 
55
56
  ## Related
56
57
 
57
- - Components: Badge, ToggleGroup
58
+ - Components: Badge, ToggleGroup, SearchInput
58
59
 
59
60
  ## Examples
60
61
 
@@ -74,6 +75,24 @@ import { Pill, Row } from "@sableui/react"
74
75
  <Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Projects</Pill><Pill onClick={() => {}}>Reports</Pill></Row>
75
76
  ```
76
77
 
78
+ ### Filters with search
79
+
80
+ ```tsx
81
+ import { Pill, Row, SearchInput } from "@sableui/react"
82
+
83
+ const [filter, setFilter] = useState("all")
84
+ const [q, setQ] = useState("")
85
+
86
+ <Row gap={2} align="center" justify="between" wrap>
87
+ <Row gap={1}>
88
+ <Pill size="lg" selected={filter === "all"} onClick={() => setFilter("all")}>All 8</Pill>
89
+ <Pill size="lg" selected={filter === "products"} onClick={() => setFilter("products")}>Products 5</Pill>
90
+ <Pill size="lg" selected={filter === "oss"} onClick={() => setFilter("oss")}>Open source 2</Pill>
91
+ </Row>
92
+ <SearchInput size="sm" value={q} onValueChange={setQ} placeholder="Find a project…" aria-label="Find a project" className="max-w-64" />
93
+ </Row>
94
+ ```
95
+
77
96
  ### Outline, small (reactions)
78
97
 
79
98
  ```tsx
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value.
5
+ Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value. size md (form), sm (toolbar, matching Pill lg and Button sm) or xs (inset tool-panel field).
6
6
 
7
7
  **Keywords:** search, input, filter, query
8
8
 
@@ -16,6 +16,8 @@ Use SearchInput for text queries that filter or search a known content set.
16
16
  - Do: Provide a visible clear affordance when query state persists.
17
17
  - Do: When a button stands in for search (a header trigger that opens CommandMenu with ⌘K), use CommandMenuTrigger. It wears this field's shell from the same source, so the two cannot drift; a hand-styled button already did once, into a borderless chip that matched neither the field nor a button.
18
18
  - Do: Light the query in the results it filters with `useMatchHighlight(query)` — a callback ref for the results container, with `selector` to keep it to titles. It paints on the `highlight` pair without wrapping matches in `<mark>`, the same paint Combobox and CommandMenu use for their rows.
19
+ - Do: In a toolbar beside filter chips, use size="sm" with Pill size="lg" (and Button size="sm" for actions). All three stand on --control-sm (32px, 28 compact), the rung the spacing doctrine gives toolbar controls, so the row is one height in either density. Keep md for a search field in a form or a page header beside md buttons, and xs for a filter inside a workspace panel, where it matches Input xs's inset surface.
20
+ - Don't: Do not match a neighbour's height with h-* on className or inputClassName. The magnifier, padding and clear button would stay sized for the old rung; pick the size that shares the neighbour's rung.
19
21
  - Don't: Do not use SearchInput as a command palette; use CommandMenu for global commands, opened from the header by CommandMenuTrigger. A SearchInput that opens a dialog on focus is a text box you cannot type in.
20
22
  - Don't: Do not search on every keystroke against an expensive backend without debouncing. The user sees results flicker and reorder under them, and you send a request per character.
21
23
 
@@ -23,6 +25,7 @@ Use SearchInput for text queries that filter or search a known content set.
23
25
 
24
26
  | Name | Type | Default | Description |
25
27
  | --- | --- | --- | --- |
28
+ | size | "md" \| "sm" \| "xs" | "md" | The rung of the control ladder, named as on Input, Select and Combobox. md is the form field (--control-md, 40px / 32 compact). sm is the toolbar field (--control-sm, 32px / 28 compact), the rung Pill lg and Button sm share, so filters + search + actions in one row are one height. xs is the tool-panel field (--control-xs, 28px / 24 compact) on Input xs's inset panel-field surface. The magnifier, inline padding and clear button scale with the rung and stay vertically centred. |
26
29
  | value | string | — | Controlled value of the search field. |
27
30
  | defaultValue | string | — | Uncontrolled initial value. The field tracks its own text, so the clear button still appears and empties it. |
28
31
  | className | string | — | Sizes and places the whole field — magnifier, input and clear button together (e.g. max-w-80). A width cap here keeps the clear button inside the field. |
@@ -31,11 +34,11 @@ Use SearchInput for text queries that filter or search a known content set.
31
34
  | onClear | () => void | — | Called when the clear button is pressed. The component also fires onValueChange('') so a controlled value resets. |
32
35
  | placeholder | string | "Search" | Placeholder text shown when the field is empty. |
33
36
  | disabled | boolean | false | Dims the control and blocks pointer interaction. |
34
- | ...props | ComponentPropsWithRef<"input"> | — | All native input attributes (name, autoFocus, ref, aria-invalid, …) pass through to the <input> except value/onChange, which are owned by value/onValueChange, and className, which sizes the whole field. |
37
+ | ...props | ComponentPropsWithRef<"input"> | — | All native input attributes (name, autoFocus, ref, aria-invalid, …) pass through to the <input> except value/onChange, which are owned by value/onValueChange, className, which sizes the whole field, and the native numeric size attribute, which the size prop replaces. |
35
38
 
36
39
  ## Tokens used
37
40
 
38
- `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
41
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`, `panel-field`, `panel-field-hover`
39
42
 
40
43
  ## Motion
41
44
 
@@ -54,7 +57,7 @@ Use SearchInput for text queries that filter or search a known content set.
54
57
 
55
58
  ## Related
56
59
 
57
- - Components: Input, CommandMenu, Combobox
60
+ - Components: Input, CommandMenu, Combobox, Pill
58
61
 
59
62
  ## Examples
60
63
 
@@ -100,6 +103,36 @@ const shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().
100
103
  </ul>
101
104
  ```
102
105
 
106
+ ### Filters with search
107
+
108
+ ```tsx
109
+ import { Pill, Row, SearchInput } from "@sableui/react"
110
+
111
+ const [filter, setFilter] = useState("all")
112
+ const [q, setQ] = useState("")
113
+
114
+ <Row gap={2} align="center" justify="between" wrap>
115
+ <Row gap={1}>
116
+ <Pill size="lg" selected={filter === "all"} onClick={() => setFilter("all")}>All 8</Pill>
117
+ <Pill size="lg" selected={filter === "products"} onClick={() => setFilter("products")}>Products 5</Pill>
118
+ <Pill size="lg" selected={filter === "oss"} onClick={() => setFilter("oss")}>Open source 2</Pill>
119
+ </Row>
120
+ <SearchInput size="sm" value={q} onValueChange={setQ} placeholder="Find a project…" aria-label="Find a project" className="max-w-64" />
121
+ </Row>
122
+ ```
123
+
124
+ ### Sizes
125
+
126
+ ```tsx
127
+ import { SearchInput, Stack } from "@sableui/react"
128
+
129
+ <Stack gap={1}>
130
+ <SearchInput size="md" placeholder="Search posts" aria-label="Search posts" />
131
+ <SearchInput size="sm" placeholder="Find a project…" aria-label="Find a project" />
132
+ <SearchInput size="xs" placeholder="Filter layers" aria-label="Filter layers" />
133
+ </Stack>
134
+ ```
135
+
103
136
  ### Disabled
104
137
 
105
138
  ```tsx
@@ -4,7 +4,7 @@
4
4
 
5
5
  The heading block for one section of a page: a title (h2 by default), an optional line of description, and actions on the title row — a "View all" link or a couple of buttons — that drop beneath the title only when the row runs out of room.
6
6
 
7
- **Keywords:** section header, section heading, section title, shelf, view all, see all, show all, h2, subsection, optical alignment, bleed
7
+ **Keywords:** section header, section heading, section title, shelf, view all, see all, show all, h2, subsection, optical alignment, bleed, editorial section, large section title
8
8
 
9
9
  ## Usage
10
10
 
@@ -13,7 +13,7 @@ Use SectionHeader to head each section below the page title — a home-page shel
13
13
  ### Best practices
14
14
 
15
15
  - Do: Wrap the header and its content in a `<section aria-labelledby={titleId}>` (a flex column with a gap, or a Stack inside it) so the section is a named region and the header's title is its name.
16
- - Do: Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: md for a page's sections and shelves, sm for denser groups like settings.
16
+ - Do: Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: lg for an editorial page's sections under a PageHeader size="lg", md for a page's sections and shelves, sm for denser groups like settings.
17
17
  - Do: Keep actions to one or two `size="sm"` buttons — the title is centred on that height. A shelf's "View all" is a ghost Button rendered as a link (`render={<a href=… />}`), named for what it opens when the label alone is ambiguous (aria-label="View all new releases").
18
18
  - Do: Pass a ghost or link action as a plain Button and let the header align it: the last direct child bleeds by its own padding at the end, so "View all" sits on the edge of the grid below, and the first bleeds at the start, which is the edge once the actions wrap beneath the title. Filled buttons keep their edge on the line. Set bleed="none" on the Button to opt one out.
19
19
  - Don't: Nudge a View all onto the edge with -me-3, or wrap the actions in your own div: the header aligns direct children only, and only by each button's real padding. If you must wrap them, set bleed on the Button inside instead.
@@ -38,11 +38,11 @@ Use SectionHeader to head each section below the page title — a home-page shel
38
38
  | actions | ReactNode | — | Trailing actions on the title row — a View all link or a couple of buttons, size="sm". They stay on the title's row and drop beneath it only when the row runs out of room; the title wraps rather than truncating. A ghost or link Button that ends the group bleeds by its padding at the end (and one that starts it, at the start — the edge once wrapped), so its label lines up with the content below; Button bleed="none" opts out. Direct children only. |
39
39
  | titleAs | "h2" \| "h3" \| "h4" | "h2" | The heading element, chosen by the document outline: h2 for a section under the page's h1, h3 for a subsection inside one. |
40
40
  | titleId | string | — | The title's id, so the section it heads can point at it: <section aria-labelledby={titleId}>. Generated when omitted. |
41
- | size | "md" \| "sm" | "md" | Visual weight, independent of the heading level: md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections. |
41
+ | size | "lg" \| "md" \| "sm" | "md" | Visual weight, independent of the heading level: lg (text-heading-2 over a text-body-md description) for an editorial page's sections, under a PageHeader size="lg"; md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections. lg stops at heading-2: heading-1 is the page title's, one per route. The actions stay centred on the title's first line at every size. |
42
42
 
43
43
  ## Variants
44
44
 
45
- - **size**: md, sm
45
+ - **size**: lg, md, sm
46
46
 
47
47
  ## Tokens used
48
48
 
@@ -135,6 +135,30 @@ import { Button, Entity, Icon, SectionHeader, Stack } from "@sableui/react"
135
135
  </section>
136
136
  ```
137
137
 
138
+ ### An editorial section (size lg)
139
+
140
+ ```tsx
141
+ import { Button, Card, CardDescription, CardHeader, CardTitle, Grid, SectionHeader } from "@sableui/react"
142
+
143
+ <section aria-labelledby="selected-work" className="flex flex-col gap-6">
144
+ <SectionHeader
145
+ titleId="selected-work"
146
+ size="lg"
147
+ title="Selected work"
148
+ description="Five projects from the last two years."
149
+ actions={
150
+ <Button variant="ghost" size="sm" render={<a href="/work" />} aria-label="View all work">
151
+ View all
152
+ </Button>
153
+ }
154
+ />
155
+ <Grid minItemWidth="16rem" gap={3}>
156
+ <Card><CardHeader><CardTitle>Tidewater</CardTitle><CardDescription>Brand identity</CardDescription></CardHeader></Card>
157
+ <Card><CardHeader><CardTitle>Northlight</CardTitle><CardDescription>Product design</CardDescription></CardHeader></Card>
158
+ </Grid>
159
+ </section>
160
+ ```
161
+
138
162
  ### A subsection inside a section
139
163
 
140
164
  ```tsx