@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
package/registry.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://sable.design/registry.schema.json",
3
3
  "name": "@sableui/react",
4
- "version": "0.16.0",
4
+ "version": "0.18.0",
5
5
  "description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
6
6
  "engine": {
7
7
  "behavior": "@base-ui/react",
@@ -63,6 +63,7 @@
63
63
  "composition": "Inspect a relevant pattern skeleton. Reuse when its information architecture fits; original compositions from layout primitives are equally valid.",
64
64
  "review": "sable docs reviewing-a-design",
65
65
  "finish": "sable check <source paths> runs doctor, lint and grade in one pass and exits 1 on any failure, then lists the review steps no tool can verify. Optional Claude Code Stop hook: sable setup --agent --target claude --hook",
66
+ "adoption": "sable check reports how much of the UI composes Sable and the app-built components Sable already ships (grade --json → adoption). Never a failure; at level none or low, ask the user before moving working screens: sable docs auditing-an-app → Adoption.",
66
67
  "evidenceTemplate": "sable grade --template",
67
68
  "learning": "sable docs learning-from-builds",
68
69
  "localMemory": ".sable/lessons/ for causal observations; .sable/preferences/ for scoped judgments; both project-owned and preserved across package upgrades",
@@ -78,8 +79,9 @@
78
79
  "claude": ".claude/skills/sable"
79
80
  },
80
81
  "canonical": "skills/sable/SKILL.md in the installed package; entries resolve it on every task",
81
- "upgrade": "pnpm update @sableui/react --latest; pnpm exec sable setup --agent --update",
82
- "preserves": "Local edits, project instructions, presets and preferences; setup only refreshes unchanged managed entries and refuses linked destinations."
82
+ "instructions": "A short managed block in AGENTS.md (Codex, Cursor) and CLAUDE.md (Claude Code), between <!-- sable:begin --> and <!-- sable:end -->, that sends every UI request through the skill even when it doesn't name Sable. --no-instructions skips it.",
83
+ "upgrade": "pnpm exec sable upgrade (updates the package, then runs the new version's init). From 0.17 or earlier: pnpm update @sableui/react --latest; pnpm exec sable init",
84
+ "preserves": "Local edits, project instructions outside the Sable block, presets and preferences; setup only refreshes unchanged managed entries and blocks, and refuses linked destinations."
83
85
  },
84
86
  "distributionStatus": "Public on npm as @sableui/react; install with pnpm add @sableui/react.",
85
87
  "ios": {
@@ -96,7 +98,7 @@
96
98
  "command": "pnpm exec sable init",
97
99
  "preview": "pnpm exec sable init --dry-run",
98
100
  "after": "Run it once the package is installed; before that `npx sable` resolves an unrelated package.",
99
- "does": "Installs missing packages with the lockfile's package manager (React, Tailwind and its build plugin), writes the css imports in order into the global stylesheet, adds bundlerDedupe and the Tailwind plugin to a Vite config (a postcss.config.mjs for Next.js), loads the fonts, wraps a Vite starter's <App /> in IconProvider, runs agentSetup, then sable doctor.",
101
+ "does": "Installs missing packages with the lockfile's package manager (React, Tailwind and its build plugin), writes the css imports in order into the global stylesheet, adds bundlerDedupe and the Tailwind plugin to a Vite config (a postcss.config.mjs for Next.js), loads the fonts, wraps a Vite starter's <App /> in IconProvider, runs agentSetup (skill entries plus the AGENTS.md / CLAUDE.md block; in a terminal it offers the Stop hook), then sable doctor.",
100
102
  "frameworks": "Vite and Next.js (App Router) get every step. Other setups get the packages and the stylesheet; the bundler and provider steps are printed.",
101
103
  "manual": "Each edit it cannot make safely (an unfamiliar config shape, a Next.js layout, a stylesheet split across files) is printed as the exact snippet and the file it belongs in. Apply them, then run sable doctor.",
102
104
  "flags": "--dry-run previews; --no-agent skips the skill; --no-install prints install commands instead of running them.",
@@ -1886,7 +1888,15 @@
1886
1888
  "at-sign",
1887
1889
  "replay",
1888
1890
  "motion",
1889
- "emoji"
1891
+ "emoji",
1892
+ "briefcase",
1893
+ "layers",
1894
+ "microphone",
1895
+ "video",
1896
+ "broadcast",
1897
+ "newspaper",
1898
+ "palette",
1899
+ "gamepad"
1890
1900
  ]
1891
1901
  },
1892
1902
  "layoutPrimitives": {
@@ -1903,7 +1913,7 @@
1903
1913
  "name": "Region",
1904
1914
  "status": "stable",
1905
1915
  "path": "layout",
1906
- "summary": "The page region: fluid, edge-to-edge with the fluid page inset (--page-inset). Opt-in centered max cap picked by ROLE (prose/form/feed/content/wide/full), never a global page max-width. The root of nearly every route.",
1916
+ "summary": "The page region: fluid, edge-to-edge with the fluid page inset (--page-inset). Opt-in centered max cap picked by ROLE (prose/form/feed/content/wide/full), never a global page max-width. The root of nearly every route. The inset and cap are classes (px-(--page-inset), max-w-…), so className overrides them at a breakpoint (max-sm:px-0); only safeArea padding is inline.",
1907
1917
  "keywords": [
1908
1918
  "region",
1909
1919
  "page width",
@@ -2072,7 +2082,7 @@
2072
2082
  "name": "Grid",
2073
2083
  "status": "stable",
2074
2084
  "path": "layout",
2075
- "summary": "CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track=\"fill\"), so one result stays one cell wide; track=\"fit\" stretches a fixed set across the row.",
2085
+ "summary": "CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track=\"fill\"), so one result stays one cell wide; track=\"fit\" stretches a fixed set across the row. display is the grid class and the gap a CSS variable, so className utilities win at any breakpoint (hidden md:grid, md:gap-6, lg:grid-cols-3).",
2076
2086
  "keywords": [
2077
2087
  "grid",
2078
2088
  "card grid",
@@ -2168,7 +2178,7 @@
2168
2178
  "name": "Columns",
2169
2179
  "status": "stable",
2170
2180
  "path": "layout",
2171
- "summary": "An explicit N-column track (default 12) for proportioned layouts — pair with Col spans (list-detail, asymmetric editorial layouts). For uniform card grids prefer Grid minItemWidth.",
2181
+ "summary": "An explicit N-column track (default 12) for proportioned layouts — pair with Col spans (list-detail, asymmetric editorial layouts). For uniform card grids prefer Grid minItemWidth. display is the grid class, so hidden md:grid works; a gap-* utility replaces both gaps and the column-gap cap.",
2172
2182
  "keywords": [
2173
2183
  "columns",
2174
2184
  "12 column",
@@ -2245,7 +2255,7 @@
2245
2255
  "name": "Stack",
2246
2256
  "status": "stable",
2247
2257
  "path": "layout",
2248
- "summary": "Vertical flow on the 8px rhythm — the default container for stacked content. Gap by role: tight within a group (1.5–2), looser between groups (3–4), generous between sections (6+).",
2258
+ "summary": "Vertical flow on the 8px rhythm — the default container for stacked content. Gap by role: tight within a group (1.5–2), looser between groups (3–4), generous between sections (6+). Layout lives in classes (flex flex-col items-* justify-*) and the gap in a CSS variable, never inline style, so className utilities override any prop at a breakpoint (max-lg:hidden, md:flex-row, md:gap-8).",
2249
2259
  "keywords": [
2250
2260
  "stack",
2251
2261
  "vertical rhythm",
@@ -2309,7 +2319,7 @@
2309
2319
  "name": "Row",
2310
2320
  "status": "stable",
2311
2321
  "path": "layout",
2312
- "summary": "Horizontal flex flow — inline rows of controls, toolbars, action clusters. Defaults to center-aligned, no wrap.",
2322
+ "summary": "Horizontal flex flow — inline rows of controls, toolbars, action clusters. Defaults to center-aligned, no wrap. Layout lives in classes (flex flex-row items-* justify-*) and the gap in a CSS variable, never inline style, so className utilities override any prop at a breakpoint (hidden md:flex, max-md:flex-col, md:gap-6).",
2313
2323
  "keywords": [
2314
2324
  "row",
2315
2325
  "toolbar",
@@ -2372,6 +2382,11 @@
2372
2382
  "label": "Action cluster",
2373
2383
  "code": "<Row gap={2} justify=\"end\"><Button variant=\"ghost\">Cancel</Button><Button>Save changes</Button></Row>",
2374
2384
  "imports": "import { Button, Row } from \"@sableui/react\""
2385
+ },
2386
+ {
2387
+ "label": "Responsive through className",
2388
+ "code": "<Row justify=\"between\"><span className=\"text-title-sm\">Acme</span><Row gap={1} className=\"max-md:hidden\"><Button variant=\"ghost\" size=\"sm\">Docs</Button><Button variant=\"ghost\" size=\"sm\">Pricing</Button></Row></Row>",
2389
+ "imports": "import { Button, Row } from \"@sableui/react\""
2375
2390
  }
2376
2391
  ]
2377
2392
  },
@@ -2379,7 +2394,7 @@
2379
2394
  "name": "Spacer",
2380
2395
  "status": "stable",
2381
2396
  "path": "layout",
2382
- "summary": "Explicit space on the rhythm, or a flexible grow pusher between siblings (e.g. pushing a footer item to the end of a Row).",
2397
+ "summary": "Explicit space on the rhythm, or a flexible grow pusher between siblings (e.g. pushing a footer item to the end of a Row). The size is a class reading a CSS variable, so className resizes or hides it at a breakpoint.",
2383
2398
  "keywords": [
2384
2399
  "spacer",
2385
2400
  "spacing",
@@ -2441,7 +2456,7 @@
2441
2456
  "name": "Center",
2442
2457
  "status": "stable",
2443
2458
  "path": "layout",
2444
- "summary": "Horizontally center a block, optionally capping its width (commonly \"prose\" for a reading column inside a wider layout).",
2459
+ "summary": "Horizontally center a block, optionally capping its width (commonly \"prose\" for a reading column inside a wider layout). Centering and cap are classes, so className overrides them at a breakpoint.",
2445
2460
  "keywords": [
2446
2461
  "center",
2447
2462
  "measure",
@@ -2492,7 +2507,7 @@
2492
2507
  "name": "AspectBox",
2493
2508
  "status": "stable",
2494
2509
  "path": "layout",
2495
- "summary": "Reserves space via aspect-ratio so async media loads with zero layout shift. Wrap images, video, embeds.",
2510
+ "summary": "Reserves space via aspect-ratio so async media loads with zero layout shift. Wrap images, video, embeds. The ratio is a class reading a CSS variable, so className reshapes it at a breakpoint (md:aspect-video).",
2496
2511
  "keywords": [
2497
2512
  "aspect box",
2498
2513
  "aspect ratio",
@@ -2544,7 +2559,7 @@
2544
2559
  "name": "Bleed",
2545
2560
  "status": "stable",
2546
2561
  "path": "layout",
2547
- "summary": "Escape a width-capped or inset ancestor to span the full CONTENT area (container-query units, so it stops at a fixed Sidebar instead of bleeding under it). Hero bands, profile covers, full-bleed media inside a capped Page.",
2562
+ "summary": "Escape a width-capped or inset ancestor to span the full CONTENT area (container-query units, so it stops at a fixed Sidebar instead of bleeding under it). Hero bands, profile covers, full-bleed media inside a capped Page. Width and margins are classes, so a breakpoint can turn the bleed off (lg:mx-0 lg:w-auto).",
2548
2563
  "keywords": [
2549
2564
  "bleed",
2550
2565
  "full bleed",
@@ -2589,7 +2604,7 @@
2589
2604
  "name": "SplitPane",
2590
2605
  "status": "stable",
2591
2606
  "path": "layout",
2592
- "summary": "Responsive two-pane layout: side-by-side at/above a breakpoint, stacked (primary first) below. List-detail, media + metadata.",
2607
+ "summary": "Responsive two-pane layout: side-by-side at/above a breakpoint, stacked (primary first) below. List-detail, media + metadata. display is the grid class and the breakpoint rules are layered, so className utilities (hidden lg:grid, gap-8) win.",
2593
2608
  "keywords": [
2594
2609
  "split pane",
2595
2610
  "two pane",
@@ -2955,6 +2970,308 @@
2955
2970
  }
2956
2971
  ]
2957
2972
  },
2973
+ {
2974
+ "name": "ActivityHeatmap",
2975
+ "status": "stable",
2976
+ "group": "Charts",
2977
+ "register": "either",
2978
+ "path": "components/activity-heatmap",
2979
+ "summary": "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.",
2980
+ "keywords": [
2981
+ "heatmap",
2982
+ "calendar heatmap",
2983
+ "contribution graph",
2984
+ "contributions",
2985
+ "activity",
2986
+ "github",
2987
+ "streak",
2988
+ "habit",
2989
+ "daily",
2990
+ "commits",
2991
+ "uptime"
2992
+ ],
2993
+ "usage": {
2994
+ "description": "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.",
2995
+ "bestPractices": [
2996
+ {
2997
+ "guidance": "do",
2998
+ "description": "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."
2999
+ },
3000
+ {
3001
+ "guidance": "do",
3002
+ "description": "Give it a `unit` (\"contribution\" / \"contributions\") so the tooltip and every cell's name say what the number counts."
3003
+ },
3004
+ {
3005
+ "guidance": "do",
3006
+ "description": "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)."
3007
+ },
3008
+ {
3009
+ "guidance": "dont",
3010
+ "description": "Use it for a handful of values or for values a reader must compare exactly; a heatmap shows rhythm, not amounts. Use BarChart.",
3011
+ "instead": [
3012
+ "BarChart",
3013
+ "AreaChart"
3014
+ ]
3015
+ },
3016
+ {
3017
+ "guidance": "dont",
3018
+ "description": "Use it for a single month of marked days with a streak; that is Calendar with modifiers.",
3019
+ "instead": [
3020
+ "Calendar"
3021
+ ]
3022
+ },
3023
+ {
3024
+ "guidance": "dont",
3025
+ "description": "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."
3026
+ }
3027
+ ],
3028
+ "anatomy": [
3029
+ {
3030
+ "name": "Weekday labels",
3031
+ "required": false,
3032
+ "description": "Short weekday names beside the rows, outside the scroller so the weeks never slide under them."
3033
+ },
3034
+ {
3035
+ "name": "Month labels",
3036
+ "required": false,
3037
+ "description": "Short month names over each month's first week; they scroll with their weeks."
3038
+ },
3039
+ {
3040
+ "name": "Cell grid",
3041
+ "required": true,
3042
+ "description": "A role=grid table, one gridcell per day, inside a horizontal Scroller that opens on the latest week."
3043
+ },
3044
+ {
3045
+ "name": "Tooltip",
3046
+ "required": false,
3047
+ "description": "The chart tooltip card, anchored to the hovered, focused or tapped day."
3048
+ },
3049
+ {
3050
+ "name": "Legend",
3051
+ "required": false,
3052
+ "description": "A Less–More ramp for intensity, or swatches for categories."
3053
+ }
3054
+ ]
3055
+ },
3056
+ "related": {
3057
+ "components": [
3058
+ "Card",
3059
+ "NumberTicker",
3060
+ "BarChart",
3061
+ "Calendar",
3062
+ "Scroller"
3063
+ ]
3064
+ },
3065
+ "props": [
3066
+ {
3067
+ "name": "label",
3068
+ "type": "string",
3069
+ "description": "What the grid shows (\"Contributions in 2026\") — its caption and accessible name."
3070
+ },
3071
+ {
3072
+ "name": "data",
3073
+ "type": "ActivityDay[] | Record<string, number>",
3074
+ "description": "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."
3075
+ },
3076
+ {
3077
+ "name": "start",
3078
+ "type": "string",
3079
+ "default": "53 weeks before end",
3080
+ "description": "First day drawn, \"YYYY-MM-DD\". The default starts at the beginning of that week, as GitHub's does."
3081
+ },
3082
+ {
3083
+ "name": "end",
3084
+ "type": "string",
3085
+ "default": "today",
3086
+ "description": "Last day drawn. Days after today inside the range are drawn hollow and skipped by the keyboard."
3087
+ },
3088
+ {
3089
+ "name": "year",
3090
+ "type": "number",
3091
+ "description": "Draw Jan 1 – Dec 31 of this year. Wins over start / end."
3092
+ },
3093
+ {
3094
+ "name": "weekStart",
3095
+ "type": "0 | 1",
3096
+ "default": "the locale's",
3097
+ "description": "0 puts Sunday on the top row, 1 Monday."
3098
+ },
3099
+ {
3100
+ "name": "levels",
3101
+ "type": "number",
3102
+ "default": "5",
3103
+ "description": "How many levels, including the empty level 0."
3104
+ },
3105
+ {
3106
+ "name": "levelScale",
3107
+ "type": "\"quantile\" | \"linear\" | number[] | ((value, stats) => number)",
3108
+ "default": "\"quantile\"",
3109
+ "description": "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."
3110
+ },
3111
+ {
3112
+ "name": "tone",
3113
+ "type": "ChartTone",
3114
+ "default": "\"1\"",
3115
+ "description": "The hue of the intensity ramp. Decorative tones lead, as on every chart; \"success\" gives the GitHub look."
3116
+ },
3117
+ {
3118
+ "name": "categories",
3119
+ "type": "ChartCategories",
3120
+ "description": "Colour days by `day.category` (label, tone or colour per key). Pair with `levels={2}` for flat category colours."
3121
+ },
3122
+ {
3123
+ "name": "unit",
3124
+ "type": "string | { one: string; other: string }",
3125
+ "description": "Names the value in the tooltip and each cell's accessible name, pluralised with Intl.PluralRules."
3126
+ },
3127
+ {
3128
+ "name": "formatValue",
3129
+ "type": "(value: number) => string",
3130
+ "default": "full to 9,999, compact past",
3131
+ "description": "Formats a value wherever it is shown or read."
3132
+ },
3133
+ {
3134
+ "name": "tooltip",
3135
+ "type": "boolean | ((day: ActivityHeatmapDay) => ReactNode)",
3136
+ "default": "true",
3137
+ "description": "Show the tooltip on hover, focus and tap; a function replaces its content on the same card."
3138
+ },
3139
+ {
3140
+ "name": "legend",
3141
+ "type": "boolean",
3142
+ "default": "true",
3143
+ "description": "The Less–More ramp for intensity, swatches for categories."
3144
+ },
3145
+ {
3146
+ "name": "legendLabels",
3147
+ "type": "[string, string]",
3148
+ "default": "[\"Less\", \"More\"]",
3149
+ "description": "The ramp's end words."
3150
+ },
3151
+ {
3152
+ "name": "monthLabels",
3153
+ "type": "boolean",
3154
+ "default": "true",
3155
+ "description": "Short month names over each month's first week."
3156
+ },
3157
+ {
3158
+ "name": "weekdayLabels",
3159
+ "type": "\"alternate\" | \"all\" | \"none\"",
3160
+ "default": "\"alternate\"",
3161
+ "description": "Which weekday names to show beside the rows; alternate is Mon, Wed, Fri."
3162
+ },
3163
+ {
3164
+ "name": "size",
3165
+ "type": "\"sm\" | \"md\" | \"lg\"",
3166
+ "default": "\"md\"",
3167
+ "description": "The smallest a cell may get (10, 12, 14 px) before the grid scrolls instead of shrinking."
3168
+ },
3169
+ {
3170
+ "name": "stretch",
3171
+ "type": "boolean",
3172
+ "default": "true",
3173
+ "description": "Grow cells to fill the width, up to 20 px. false keeps them at `size`."
3174
+ },
3175
+ {
3176
+ "name": "activeDate",
3177
+ "type": "string | null",
3178
+ "description": "The hovered or focused day, controlled. Pair with defaultActiveDate / onActiveDateChange."
3179
+ },
3180
+ {
3181
+ "name": "selectedDate",
3182
+ "type": "string | null",
3183
+ "description": "The selected day, controlled. Selection is on only when onSelectDate is set."
3184
+ },
3185
+ {
3186
+ "name": "onSelectDate",
3187
+ "type": "(date: string) => void",
3188
+ "description": "Called when a day is clicked or chosen with Enter / Space — to filter a feed below, as GitHub does."
3189
+ },
3190
+ {
3191
+ "name": "renderCell",
3192
+ "type": "(day, cell) => ReactNode",
3193
+ "description": "Draw something over a cell's fill — an icon for a milestone, a reward, an outage."
3194
+ },
3195
+ {
3196
+ "name": "loading",
3197
+ "type": "boolean",
3198
+ "default": "false",
3199
+ "description": "Draw the empty grid, shimmering, while data loads."
3200
+ },
3201
+ {
3202
+ "name": "motion",
3203
+ "type": "\"full\" | \"subtle\" | \"off\"",
3204
+ "default": "\"full\"",
3205
+ "description": "Turns the arrival down; `subtle` shows the finished grid at once."
3206
+ },
3207
+ {
3208
+ "name": "locale",
3209
+ "type": "string",
3210
+ "default": "runtime locale",
3211
+ "description": "Month and weekday names, numbers and plurals."
3212
+ }
3213
+ ],
3214
+ "tokensUsed": [
3215
+ "foreground",
3216
+ "muted-foreground",
3217
+ "border-strong",
3218
+ "ring",
3219
+ "tone-1-mark",
3220
+ "tone-2-mark",
3221
+ "tone-3-mark",
3222
+ "success-mark",
3223
+ "tone-info-mark"
3224
+ ],
3225
+ "motion": [
3226
+ {
3227
+ "slot": "arrival",
3228
+ "recipe": "custom",
3229
+ "part": "Cell grid",
3230
+ "why": "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."
3231
+ },
3232
+ {
3233
+ "slot": "response",
3234
+ "recipe": "custom",
3235
+ "part": "Cell",
3236
+ "why": "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."
3237
+ },
3238
+ {
3239
+ "slot": "transition",
3240
+ "recipe": "custom",
3241
+ "part": "Cell",
3242
+ "why": "When data or the range changes, each cell's fill cross-fades on the base duration token, so the reader sees which days changed."
3243
+ },
3244
+ {
3245
+ "slot": "activity",
3246
+ "recipe": "motion-shimmer",
3247
+ "part": "Cell grid",
3248
+ "why": "`loading` shimmers the empty grid until days arrive."
3249
+ }
3250
+ ],
3251
+ "a11y": [
3252
+ "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`.",
3253
+ "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.",
3254
+ "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.",
3255
+ "Levels rise in lightness, so they read without colour; under forced colours each level draws as a larger inner square."
3256
+ ],
3257
+ "examples": [
3258
+ {
3259
+ "label": "Contributions, last year",
3260
+ "code": "const days = [\n\t{ date: \"2026-10-12\", value: 4 },\n\t{ date: \"2026-10-13\", value: 11 },\n\t{ date: \"2026-10-14\", value: 2 },\n]\n\n<ActivityHeatmap label=\"Contributions in the last year\" data={days} tone=\"success\" unit={{ one: \"contribution\", other: \"contributions\" }} />",
3261
+ "imports": "import { ActivityHeatmap } from \"@sableui/react\""
3262
+ },
3263
+ {
3264
+ "label": "Two sources, in a card",
3265
+ "code": "const github = [{ date: \"2026-10-13\", value: 6 }]\nconst codex = [{ date: \"2026-10-13\", value: 1 }, { date: \"2026-10-14\", value: 3 }]\n\n<Card>\n\t<CardHeader>\n\t\t<CardTitle>Building, day by day</CardTitle>\n\t</CardHeader>\n\t<CardContent>\n\t\t<ActivityHeatmap\n\t\t\tlabel=\"GitHub and Codex activity in 2026\"\n\t\t\tyear={2026}\n\t\t\tlevels={2}\n\t\t\tdata={mergeActivity({ github, codex })}\n\t\t\tcategories={{ github: { label: \"GitHub\", tone: \"success\" }, codex: { label: \"Codex\", tone: \"info\" }, both: { label: \"Both\", tone: \"2\" } }}\n\t\t/>\n\t</CardContent>\n</Card>",
3266
+ "imports": "import { ActivityHeatmap, Card, CardContent, CardHeader, CardTitle, mergeActivity } from \"@sableui/react\""
3267
+ },
3268
+ {
3269
+ "label": "Selecting a day",
3270
+ "code": "const [day, setDay] = useState<string | null>(null)\n\n<ActivityHeatmap label=\"Orders per day\" data={{ \"2026-10-13\": 18, \"2026-10-14\": 25 }} unit=\"orders\" selectedDate={day} onSelectDate={setDay} />",
3271
+ "imports": "import { ActivityHeatmap } from \"@sableui/react\""
3272
+ }
3273
+ ]
3274
+ },
2958
3275
  {
2959
3276
  "name": "AppHeader",
2960
3277
  "status": "stable",
@@ -7252,6 +7569,12 @@
7252
7569
  "name": "prompt",
7253
7570
  "type": "string",
7254
7571
  "description": "A shell prompt (\"$\", \">\"). Makes it a one-line command: the prompt is dimmed and unselectable, the line truncates, and copy takes only code."
7572
+ },
7573
+ {
7574
+ "name": "wrap",
7575
+ "type": "boolean",
7576
+ "default": "false",
7577
+ "description": "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."
7255
7578
  }
7256
7579
  ],
7257
7580
  "subcomponents": [
@@ -9501,6 +9824,18 @@
9501
9824
  "InlineStatus"
9502
9825
  ]
9503
9826
  },
9827
+ {
9828
+ "guidance": "do",
9829
+ "description": "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."
9830
+ },
9831
+ {
9832
+ "guidance": "do",
9833
+ "description": "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."
9834
+ },
9835
+ {
9836
+ "guidance": "dont",
9837
+ "description": "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."
9838
+ },
9504
9839
  {
9505
9840
  "guidance": "do",
9506
9841
  "description": "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."
@@ -9554,14 +9889,30 @@
9554
9889
  {
9555
9890
  "name": "Dialog (Root)",
9556
9891
  "type": "Base UI Dialog.Root props (open, defaultOpen, onOpenChange, modal)",
9557
- "description": "Controls open state. Uncontrolled by default."
9892
+ "description": "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."
9893
+ },
9894
+ {
9895
+ "name": "open",
9896
+ "type": "boolean",
9897
+ "description": "Controlled open state. Pair with onOpenChange — for a dialog opened from your own button instead of a DialogTrigger."
9898
+ },
9899
+ {
9900
+ "name": "defaultOpen",
9901
+ "type": "boolean",
9902
+ "default": "false",
9903
+ "description": "Initial open state when uncontrolled."
9904
+ },
9905
+ {
9906
+ "name": "onOpenChange",
9907
+ "type": "(open: boolean, eventDetails) => void",
9908
+ "description": "Called when the dialog asks to open or close (a trigger, Esc, an outside click, DialogClose)."
9558
9909
  }
9559
9910
  ],
9560
9911
  "subcomponents": [
9561
9912
  {
9562
9913
  "name": "DialogContent",
9563
- "type": "Base UI Popup props + size?: \"default\" | \"fullscreen\" + showCloseButton?: boolean + scroll?: boolean + className",
9564
- "description": "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."
9914
+ "type": "Base UI Popup props (initialFocus, finalFocus) + size?: \"default\" | \"fullscreen\" + showCloseButton?: boolean + scroll?: boolean + keepMounted?: boolean + className",
9915
+ "description": "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."
9565
9916
  },
9566
9917
  {
9567
9918
  "name": "DialogOverlay",
@@ -9601,7 +9952,7 @@
9601
9952
  {
9602
9953
  "name": "AlertDialog (Root)",
9603
9954
  "type": "Base UI AlertDialog.Root props (open, defaultOpen, onOpenChange)",
9604
- "description": "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."
9955
+ "description": "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."
9605
9956
  },
9606
9957
  {
9607
9958
  "name": "AlertDialogTrigger",
@@ -9666,6 +10017,8 @@
9666
10017
  "a11y": [
9667
10018
  "Base UI provides focus trapping, scroll lock, aria-modal, and labelled-by wiring (DialogTitle).",
9668
10019
  "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.",
10020
+ "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.",
10021
+ "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.",
9669
10022
  "Esc and click-outside dismiss by default. AlertDialog keeps Esc but ignores click-outside, and its popup is role=\"alertdialog\".",
9670
10023
  "Animate via data-[starting-style]/data-[ending-style] attributes.",
9671
10024
  "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)."
@@ -9691,6 +10044,11 @@
9691
10044
  "code": "<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>",
9692
10045
  "imports": "import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Field, Form, Input } from \"@sableui/react\""
9693
10046
  },
10047
+ {
10048
+ "label": "Opened from your own button, keeping a draft",
10049
+ "code": "const [open, setOpen] = useState(false)\n\n<Button variant=\"secondary\" onClick={() => setOpen(true)}>New message</Button>\n<Dialog open={open} onOpenChange={setOpen}>\n\t<DialogContent keepMounted showCloseButton>\n\t\t<DialogHeader>\n\t\t\t<DialogTitle>New message</DialogTitle>\n\t\t\t<DialogDescription>Close it any time. Your draft is here when you come back.</DialogDescription>\n\t\t</DialogHeader>\n\t\t<Field label=\"Message\"><Textarea /></Field>\n\t\t<DialogFooter>\n\t\t\t<DialogClose render={<Button variant=\"secondary\">Close</Button>} />\n\t\t\t<Button onClick={() => setOpen(false)}>Send</Button>\n\t\t</DialogFooter>\n\t</DialogContent>\n</Dialog>",
10050
+ "imports": "import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Textarea } from \"@sableui/react\""
10051
+ },
9694
10052
  {
9695
10053
  "label": "Destructive confirmation",
9696
10054
  "code": "<AlertDialog><AlertDialogTrigger render={<Button variant=\"destructive\">Delete project</Button>} /><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Delete project?</AlertDialogTitle><AlertDialogDescription>This removes the project and cannot be undone.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogClose render={<Button variant=\"secondary\">Cancel</Button>} /><Button variant=\"destructive\" onClick={deleteProject}>Delete project</Button></AlertDialogFooter></AlertDialogContent></AlertDialog>",
@@ -12865,6 +13223,10 @@
12865
13223
  "guidance": "do",
12866
13224
  "description": "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."
12867
13225
  },
13226
+ {
13227
+ "guidance": "do",
13228
+ "description": "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."
13229
+ },
12868
13230
  {
12869
13231
  "guidance": "dont",
12870
13232
  "description": "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."
@@ -12911,6 +13273,12 @@
12911
13273
  "default": "\"cover\"",
12912
13274
  "description": "`cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped)."
12913
13275
  },
13276
+ {
13277
+ "name": "position",
13278
+ "type": "CSSProperties[\"objectPosition\"]",
13279
+ "default": "\"center\"",
13280
+ "description": "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."
13281
+ },
12914
13282
  {
12915
13283
  "name": "srcSet",
12916
13284
  "type": "string",
@@ -13000,6 +13368,11 @@
13000
13368
  "label": "Logo, never cropped",
13001
13369
  "code": "<AspectBox ratio={16 / 9} className=\"rounded-lg\">\n\t<MediaImage src={brand.logo} alt={brand.name} fit=\"contain\" />\n</AspectBox>",
13002
13370
  "imports": "import { AspectBox, MediaImage } from \"@sableui/react\""
13371
+ },
13372
+ {
13373
+ "label": "Crop focus per image",
13374
+ "code": "<AspectBox ratio={1} className=\"rounded-md\">\n\t<MediaImage src={project.thumbnail} seed={project.title} position={project.focus ?? \"center\"} />\n</AspectBox>",
13375
+ "imports": "import { AspectBox, MediaImage } from \"@sableui/react\""
13003
13376
  }
13004
13377
  ]
13005
13378
  },
@@ -13139,6 +13512,12 @@
13139
13512
  "type": "string | number",
13140
13513
  "description": "Seeds the fallback art. Defaults to `title` when it is a string."
13141
13514
  },
13515
+ {
13516
+ "name": "position",
13517
+ "type": "CSSProperties[\"objectPosition\"]",
13518
+ "default": "\"center\"",
13519
+ "description": "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."
13520
+ },
13142
13521
  {
13143
13522
  "name": "aspect",
13144
13523
  "type": "\"square\" | \"video\" | \"poster\" | number",
@@ -13273,6 +13652,11 @@
13273
13652
  "code": "<Grid minItemWidth=\"8rem\" gap={2}>\n\t{photos.map((photo) => (\n\t\t<MediaTile key={photo.id} size=\"sm\" title={photo.caption} src={photo.url} alt={photo.description} />\n\t))}\n</Grid>",
13274
13653
  "imports": "import { Grid, MediaTile } from \"@sableui/react\""
13275
13654
  },
13655
+ {
13656
+ "label": "Portfolio grid with crop focus",
13657
+ "code": "<Grid minItemWidth=\"16rem\" gap={4}>\n\t{projects.map((project) => (\n\t\t<MediaTile\n\t\t\tkey={project.slug}\n\t\t\taspect=\"video\"\n\t\t\ttitle={project.title}\n\t\t\tsubtitle={project.client}\n\t\t\tsrc={project.thumbnail}\n\t\t\tposition={project.focus}\n\t\t\thref={`/work/${project.slug}`}\n\t\t/>\n\t))}\n</Grid>",
13658
+ "imports": "import { Grid, MediaTile } from \"@sableui/react\""
13659
+ },
13276
13660
  {
13277
13661
  "label": "Poster shelf",
13278
13662
  "code": "<MediaTile aspect=\"poster\" size=\"lg\" titleAs=\"h3\" title=\"The Long Dark\" subtitle=\"2026 · Drama\" href=\"/films/the-long-dark\" />",
@@ -15485,7 +15869,11 @@
15485
15869
  "page header",
15486
15870
  "title",
15487
15871
  "lede",
15488
- "page actions"
15872
+ "page actions",
15873
+ "page title size",
15874
+ "display title",
15875
+ "editorial header",
15876
+ "hero title"
15489
15877
  ],
15490
15878
  "usage": {
15491
15879
  "description": "Use PageHeader to establish a page's title, supporting description, and primary page actions.",
@@ -15498,6 +15886,14 @@
15498
15886
  "guidance": "do",
15499
15887
  "description": "Keep the title literal and put explanation in the lede or nearby content."
15500
15888
  },
15889
+ {
15890
+ "guidance": "do",
15891
+ "description": "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."
15892
+ },
15893
+ {
15894
+ "guidance": "dont",
15895
+ "description": "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."
15896
+ },
15501
15897
  {
15502
15898
  "guidance": "dont",
15503
15899
  "description": "Do not use PageHeader inside repeated cards, dialogs, or compact panels."
@@ -15546,8 +15942,20 @@
15546
15942
  "type": "\"h1\" | \"h2\"",
15547
15943
  "default": "\"h1\"",
15548
15944
  "description": "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."
15945
+ },
15946
+ {
15947
+ "name": "size",
15948
+ "type": "\"md\" | \"lg\"",
15949
+ "default": "\"md\"",
15950
+ "description": "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."
15549
15951
  }
15550
15952
  ],
15953
+ "variants": {
15954
+ "size": [
15955
+ "md",
15956
+ "lg"
15957
+ ]
15958
+ },
15551
15959
  "tokensUsed": [
15552
15960
  "foreground",
15553
15961
  "muted-foreground"
@@ -15562,6 +15970,11 @@
15562
15970
  "code": "<PageHeader title=\"Storage credits\" description=\"Fund permanent storage.\" actions={<Button>Top up</Button>} />",
15563
15971
  "imports": "import { Button, PageHeader } from \"@sableui/react\""
15564
15972
  },
15973
+ {
15974
+ "label": "Editorial page title (size lg)",
15975
+ "code": "<PageHeader size=\"lg\" title=\"Field notes\" description=\"Essays on making things slowly, from a studio by the sea.\" actions={<Button variant=\"outline\">Subscribe</Button>} />",
15976
+ "imports": "import { Button, PageHeader } from \"@sableui/react\""
15977
+ },
15565
15978
  {
15566
15979
  "label": "One per page, inside the region",
15567
15980
  "code": "// PageHeader introduces the current route: title, optional description, and the\n// actions that act on THIS page.\n\n<Region max=\"content\">\n <PageHeader\n title=\"Invoices\"\n description=\"Every charge on this account.\"\n actions={<Button>Export</Button>}\n />\n <p className=\"text-body-md text-muted-foreground\">Page content.</p>\n</Region>\n\n// ✗ Outside the Region. The header then sits at a different measure from the\n// content it introduces, and the page reads as two documents.\n// ✗ Two on one page. A second one is a section heading: a SectionHeader\n// (an h2 on text-heading-3, same title row and actions), not another PageHeader.\n// ✗ Global actions in it (account, search). Those persist across routes and\n// belong in AppHeader.",
@@ -16486,7 +16899,7 @@
16486
16899
  "group": "Badges & pills",
16487
16900
  "register": "page",
16488
16901
  "path": "components/pill",
16489
- "summary": "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.",
16902
+ "summary": "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.",
16490
16903
  "keywords": [
16491
16904
  "pill",
16492
16905
  "chip",
@@ -16506,6 +16919,10 @@
16506
16919
  "guidance": "do",
16507
16920
  "description": "Reflect the current choice with selected, so the active filter reads at a glance."
16508
16921
  },
16922
+ {
16923
+ "guidance": "do",
16924
+ "description": "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."
16925
+ },
16509
16926
  {
16510
16927
  "guidance": "dont",
16511
16928
  "description": "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.",
@@ -16519,14 +16936,15 @@
16519
16936
  },
16520
16937
  {
16521
16938
  "guidance": "dont",
16522
- "description": "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."
16939
+ "description": "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."
16523
16940
  }
16524
16941
  ]
16525
16942
  },
16526
16943
  "related": {
16527
16944
  "components": [
16528
16945
  "Badge",
16529
- "ToggleGroup"
16946
+ "ToggleGroup",
16947
+ "SearchInput"
16530
16948
  ]
16531
16949
  },
16532
16950
  "props": [
@@ -16544,9 +16962,9 @@
16544
16962
  },
16545
16963
  {
16546
16964
  "name": "size",
16547
- "type": "\"md\" | \"sm\"",
16965
+ "type": "\"lg\" | \"md\" | \"sm\"",
16548
16966
  "default": "\"md\"",
16549
- "description": "md sits on the density ladder's xs control height (28px); sm is 24px, for chips under or inside other content."
16967
+ "description": "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."
16550
16968
  },
16551
16969
  {
16552
16970
  "name": "tone",
@@ -16566,6 +16984,7 @@
16566
16984
  "outline"
16567
16985
  ],
16568
16986
  "size": [
16987
+ "lg",
16569
16988
  "md",
16570
16989
  "sm"
16571
16990
  ],
@@ -16650,6 +17069,11 @@
16650
17069
  "code": "<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Projects</Pill><Pill onClick={() => {}}>Reports</Pill></Row>",
16651
17070
  "imports": "import { Pill, Row } from \"@sableui/react\""
16652
17071
  },
17072
+ {
17073
+ "label": "Filters with search",
17074
+ "code": "const [filter, setFilter] = useState(\"all\")\nconst [q, setQ] = useState(\"\")\n\n<Row gap={2} align=\"center\" justify=\"between\" wrap>\n\t<Row gap={1}>\n\t\t<Pill size=\"lg\" selected={filter === \"all\"} onClick={() => setFilter(\"all\")}>All 8</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"products\"} onClick={() => setFilter(\"products\")}>Products 5</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"oss\"} onClick={() => setFilter(\"oss\")}>Open source 2</Pill>\n\t</Row>\n\t<SearchInput size=\"sm\" value={q} onValueChange={setQ} placeholder=\"Find a project…\" aria-label=\"Find a project\" className=\"max-w-64\" />\n</Row>",
17075
+ "imports": "import { Pill, Row, SearchInput } from \"@sableui/react\""
17076
+ },
16653
17077
  {
16654
17078
  "label": "Outline, small (reactions)",
16655
17079
  "code": "<Row gap={1}><Pill appearance=\"outline\" size=\"sm\" selected onClick={() => {}}>🎉 3</Pill><Pill appearance=\"outline\" size=\"sm\" onClick={() => {}}>👀</Pill></Row>",
@@ -18418,7 +18842,7 @@
18418
18842
  "group": "Forms",
18419
18843
  "register": "either",
18420
18844
  "path": "components/search-input",
18421
- "summary": "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.",
18845
+ "summary": "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).",
18422
18846
  "keywords": [
18423
18847
  "search",
18424
18848
  "input",
@@ -18444,6 +18868,14 @@
18444
18868
  "guidance": "do",
18445
18869
  "description": "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."
18446
18870
  },
18871
+ {
18872
+ "guidance": "do",
18873
+ "description": "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."
18874
+ },
18875
+ {
18876
+ "guidance": "dont",
18877
+ "description": "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."
18878
+ },
18447
18879
  {
18448
18880
  "guidance": "dont",
18449
18881
  "description": "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."
@@ -18458,10 +18890,17 @@
18458
18890
  "components": [
18459
18891
  "Input",
18460
18892
  "CommandMenu",
18461
- "Combobox"
18893
+ "Combobox",
18894
+ "Pill"
18462
18895
  ]
18463
18896
  },
18464
18897
  "props": [
18898
+ {
18899
+ "name": "size",
18900
+ "type": "\"md\" | \"sm\" | \"xs\"",
18901
+ "default": "\"md\"",
18902
+ "description": "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."
18903
+ },
18465
18904
  {
18466
18905
  "name": "value",
18467
18906
  "type": "string",
@@ -18507,7 +18946,7 @@
18507
18946
  {
18508
18947
  "name": "...props",
18509
18948
  "type": "ComponentPropsWithRef<\"input\">",
18510
- "description": "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."
18949
+ "description": "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."
18511
18950
  }
18512
18951
  ],
18513
18952
  "tokensUsed": [
@@ -18516,7 +18955,9 @@
18516
18955
  "foreground",
18517
18956
  "muted-foreground",
18518
18957
  "ring",
18519
- "destructive"
18958
+ "destructive",
18959
+ "panel-field",
18960
+ "panel-field-hover"
18520
18961
  ],
18521
18962
  "motion": [
18522
18963
  {
@@ -18572,6 +19013,16 @@
18572
19013
  "code": "const [q, setQ] = useState(\"\")\nconst results = useMatchHighlight<HTMLUListElement>(q, { selector: \"[data-title]\" })\nconst shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().toLowerCase()))\n\n<SearchInput value={q} onValueChange={setQ} placeholder=\"Search posts\" />\n<ul ref={results}>\n\t{shown.map((post) => (\n\t\t<li key={post.id}><span data-title>{post.title}</span></li>\n\t))}\n</ul>",
18573
19014
  "imports": "import { SearchInput, useMatchHighlight } from \"@sableui/react\""
18574
19015
  },
19016
+ {
19017
+ "label": "Filters with search",
19018
+ "code": "const [filter, setFilter] = useState(\"all\")\nconst [q, setQ] = useState(\"\")\n\n<Row gap={2} align=\"center\" justify=\"between\" wrap>\n\t<Row gap={1}>\n\t\t<Pill size=\"lg\" selected={filter === \"all\"} onClick={() => setFilter(\"all\")}>All 8</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"products\"} onClick={() => setFilter(\"products\")}>Products 5</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"oss\"} onClick={() => setFilter(\"oss\")}>Open source 2</Pill>\n\t</Row>\n\t<SearchInput size=\"sm\" value={q} onValueChange={setQ} placeholder=\"Find a project…\" aria-label=\"Find a project\" className=\"max-w-64\" />\n</Row>",
19019
+ "imports": "import { Pill, Row, SearchInput } from \"@sableui/react\""
19020
+ },
19021
+ {
19022
+ "label": "Sizes",
19023
+ "code": "<Stack gap={1}>\n\t<SearchInput size=\"md\" placeholder=\"Search posts\" aria-label=\"Search posts\" />\n\t<SearchInput size=\"sm\" placeholder=\"Find a project…\" aria-label=\"Find a project\" />\n\t<SearchInput size=\"xs\" placeholder=\"Filter layers\" aria-label=\"Filter layers\" />\n</Stack>",
19024
+ "imports": "import { SearchInput, Stack } from \"@sableui/react\""
19025
+ },
18575
19026
  {
18576
19027
  "label": "Disabled",
18577
19028
  "code": "<SearchInput disabled placeholder=\"Search\" />",
@@ -18597,7 +19048,9 @@
18597
19048
  "h2",
18598
19049
  "subsection",
18599
19050
  "optical alignment",
18600
- "bleed"
19051
+ "bleed",
19052
+ "editorial section",
19053
+ "large section title"
18601
19054
  ],
18602
19055
  "usage": {
18603
19056
  "description": "Use SectionHeader to head each section below the page title — a home-page shelf with View all, a settings group, a list with its own actions. PageHeader is the route's single h1; every section under it takes a SectionHeader, with titleId wired to the section's aria-labelledby.",
@@ -18608,7 +19061,7 @@
18608
19061
  },
18609
19062
  {
18610
19063
  "guidance": "do",
18611
- "description": "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."
19064
+ "description": "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."
18612
19065
  },
18613
19066
  {
18614
19067
  "guidance": "do",
@@ -18696,13 +19149,14 @@
18696
19149
  },
18697
19150
  {
18698
19151
  "name": "size",
18699
- "type": "\"md\" | \"sm\"",
19152
+ "type": "\"lg\" | \"md\" | \"sm\"",
18700
19153
  "default": "\"md\"",
18701
- "description": "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."
19154
+ "description": "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."
18702
19155
  }
18703
19156
  ],
18704
19157
  "variants": {
18705
19158
  "size": [
19159
+ "lg",
18706
19160
  "md",
18707
19161
  "sm"
18708
19162
  ]
@@ -18758,6 +19212,11 @@
18758
19212
  "code": "<section aria-labelledby=\"playlists\" className=\"flex flex-col gap-4\">\n\t<SectionHeader\n\t\ttitleId=\"playlists\"\n\t\ttitle=\"Your playlists\"\n\t\tactions={\n\t\t\t<>\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" iconStart={<Icon name=\"filter\" />}>Filter</Button>\n\t\t\t\t<Button size=\"sm\" iconStart={<Icon name=\"plus\" />}>New playlist</Button>\n\t\t\t</>\n\t\t}\n\t/>\n\t<Stack gap={1}>\n\t\t<Entity className=\"[--entity-px:0]\" title=\"Late drives\" subtitle=\"42 tracks\" />\n\t\t<Entity className=\"[--entity-px:0]\" title=\"Focus\" subtitle=\"18 tracks\" />\n\t</Stack>\n</section>",
18759
19213
  "imports": "import { Button, Entity, Icon, SectionHeader, Stack } from \"@sableui/react\""
18760
19214
  },
19215
+ {
19216
+ "label": "An editorial section (size lg)",
19217
+ "code": "<section aria-labelledby=\"selected-work\" className=\"flex flex-col gap-6\">\n\t<SectionHeader\n\t\ttitleId=\"selected-work\"\n\t\tsize=\"lg\"\n\t\ttitle=\"Selected work\"\n\t\tdescription=\"Five projects from the last two years.\"\n\t\tactions={\n\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/work\" />} aria-label=\"View all work\">\n\t\t\t\tView all\n\t\t\t</Button>\n\t\t}\n\t/>\n\t<Grid minItemWidth=\"16rem\" gap={3}>\n\t\t<Card><CardHeader><CardTitle>Tidewater</CardTitle><CardDescription>Brand identity</CardDescription></CardHeader></Card>\n\t\t<Card><CardHeader><CardTitle>Northlight</CardTitle><CardDescription>Product design</CardDescription></CardHeader></Card>\n\t</Grid>\n</section>",
19218
+ "imports": "import { Button, Card, CardDescription, CardHeader, CardTitle, Grid, SectionHeader } from \"@sableui/react\""
19219
+ },
18761
19220
  {
18762
19221
  "label": "A subsection inside a section",
18763
19222
  "code": "<SectionHeader titleAs=\"h3\" size=\"sm\" title=\"Appears on\" />",
@@ -18765,6 +19224,201 @@
18765
19224
  }
18766
19225
  ]
18767
19226
  },
19227
+ {
19228
+ "name": "SectionRail",
19229
+ "status": "stable",
19230
+ "group": "Navigation",
19231
+ "register": "page",
19232
+ "path": "components/section-rail",
19233
+ "summary": "A slim vertical rail of dots, one per section of a long page, that shows where the reader is and jumps to any section. The current section's bar glides as the page scrolls; a shared tooltip names each dot.",
19234
+ "keywords": [
19235
+ "section rail",
19236
+ "scroll spy",
19237
+ "scrollspy",
19238
+ "dot navigation",
19239
+ "page navigation",
19240
+ "table of contents",
19241
+ "on this page",
19242
+ "chapter nav",
19243
+ "scroll progress",
19244
+ "case study nav"
19245
+ ],
19246
+ "usage": {
19247
+ "description": "Use SectionRail beside a long, single-column read — a case study, an essay, a long landing page — so the reader can see which section they are in and jump to another without leaving the text. It follows the scroll by itself (useActiveSection); pass value when the parent already knows the current section.",
19248
+ "bestPractices": [
19249
+ {
19250
+ "guidance": "do",
19251
+ "description": "Give every section an id that matches its entry, and keep the list to the page's top-level sections — about ten at most, so the rail stays shorter than the viewport."
19252
+ },
19253
+ {
19254
+ "guidance": "do",
19255
+ "description": "Place it with layout classes — usually `fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2` — and hide it below the width where it would overlap the text (`max-lg:hidden`). Offer the same links another way on narrow screens, such as a Collapsible list at the top of the page."
19256
+ },
19257
+ {
19258
+ "guidance": "do",
19259
+ "description": "Pass `root` when the content scrolls inside an element (a Scroller, an inset AppShell main) rather than the window, and give sections `scroll-mt-*` when a sticky header would cover their tops after a jump."
19260
+ },
19261
+ {
19262
+ "guidance": "do",
19263
+ "description": "Set `side=\"right\"` when the rail sits on the right edge, so labels open toward the content."
19264
+ },
19265
+ {
19266
+ "guidance": "dont",
19267
+ "description": "Use it for app navigation between pages; that is SideNav or Sidebar. A SectionRail only moves within one page.",
19268
+ "instead": [
19269
+ "SideNav",
19270
+ "Sidebar"
19271
+ ]
19272
+ },
19273
+ {
19274
+ "guidance": "dont",
19275
+ "description": "Use it to show the steps of a task or a flow; that is Stepper, which shows progress through steps rather than position in a read.",
19276
+ "instead": [
19277
+ "Stepper"
19278
+ ]
19279
+ },
19280
+ {
19281
+ "guidance": "dont",
19282
+ "description": "Use it for a handful of switchable views in one place; that is Tabs.",
19283
+ "instead": [
19284
+ "Tabs"
19285
+ ]
19286
+ }
19287
+ ],
19288
+ "anatomy": [
19289
+ {
19290
+ "name": "Rail",
19291
+ "required": true,
19292
+ "description": "The `<nav>` landmark: a rounded muted track holding one link per section."
19293
+ },
19294
+ {
19295
+ "name": "Dot",
19296
+ "required": true,
19297
+ "description": "A link to `#id`, drawn as a small dot that grows under the pointer or focus."
19298
+ },
19299
+ {
19300
+ "name": "Marker",
19301
+ "required": true,
19302
+ "description": "One primary bar that sits on the current section's dot and glides to the next."
19303
+ },
19304
+ {
19305
+ "name": "Label",
19306
+ "required": true,
19307
+ "description": "A shared tooltip that names the hovered or focused dot and travels between dots."
19308
+ }
19309
+ ]
19310
+ },
19311
+ "related": {
19312
+ "components": [
19313
+ "SideNav",
19314
+ "Tooltip",
19315
+ "Stepper",
19316
+ "Tabs",
19317
+ "GlidingHighlight"
19318
+ ]
19319
+ },
19320
+ "props": [
19321
+ {
19322
+ "name": "sections",
19323
+ "type": "readonly { id: string; label: string }[]",
19324
+ "description": "The sections, in document order. `id` is the section element's id (the link goes to `#id`); `label` is the tooltip and the link's accessible name."
19325
+ },
19326
+ {
19327
+ "name": "value",
19328
+ "type": "string",
19329
+ "description": "The current section's id, when the parent decides it. Leave it unset and the rail follows the scroll itself."
19330
+ },
19331
+ {
19332
+ "name": "onValueChange",
19333
+ "type": "(id: string) => void",
19334
+ "description": "Called with the new id when the reader scrolls into a section or picks one."
19335
+ },
19336
+ {
19337
+ "name": "offset",
19338
+ "type": "number",
19339
+ "default": "0.35",
19340
+ "description": "Where the reading line sits, as a fraction of the viewport from its top. A section becomes current once its top crosses it."
19341
+ },
19342
+ {
19343
+ "name": "root",
19344
+ "type": "RefObject<HTMLElement | null>",
19345
+ "description": "The element that scrolls, when it is not the window."
19346
+ },
19347
+ {
19348
+ "name": "side",
19349
+ "type": "\"left\" | \"right\"",
19350
+ "default": "\"left\"",
19351
+ "description": "Which edge of the page the rail sits on. The labels open on the other side, toward the content."
19352
+ },
19353
+ {
19354
+ "name": "motion",
19355
+ "type": "\"full\" | \"subtle\" | \"off\"",
19356
+ "default": "\"full\"",
19357
+ "description": "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the marker's glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
19358
+ },
19359
+ {
19360
+ "name": "aria-label",
19361
+ "type": "string",
19362
+ "default": "\"Sections\"",
19363
+ "description": "The nav landmark's name. Name it for the page (\"Case study sections\") when it has other navs."
19364
+ }
19365
+ ],
19366
+ "tokensUsed": [
19367
+ "muted",
19368
+ "muted-foreground",
19369
+ "foreground",
19370
+ "primary",
19371
+ "ring",
19372
+ "popover",
19373
+ "popover-foreground",
19374
+ "border-floating"
19375
+ ],
19376
+ "motion": [
19377
+ {
19378
+ "slot": "arrival",
19379
+ "recipe": "none",
19380
+ "part": "Root",
19381
+ "why": "Page chrome arrives with the page; the marker lands on the current section with no transition, because a bar sliding in on load narrates a move nobody made."
19382
+ },
19383
+ {
19384
+ "slot": "response",
19385
+ "recipe": "motion-interactive",
19386
+ "part": "Dot",
19387
+ "why": "The dot darkens and grows under the pointer or focus, and the label travels between dots as one tooltip (TooltipGroup) rather than closing and reopening at each."
19388
+ },
19389
+ {
19390
+ "slot": "transition",
19391
+ "recipe": "motion-glide",
19392
+ "part": "Marker",
19393
+ "why": "One bar travels to the new section as the page scrolls, so the change reads as movement down the page. A picked section holds the bar until the reader scrolls by hand, so it goes straight there instead of stepping through every section in between, and stays on the pick even when the page ends before that section can reach the reading line. Expressive: `subtle` moves it at once."
19394
+ },
19395
+ {
19396
+ "slot": "activity",
19397
+ "recipe": "none",
19398
+ "part": "Root",
19399
+ "why": "Navigation chrome has no working state."
19400
+ }
19401
+ ],
19402
+ "a11y": [
19403
+ "Renders a <nav> landmark (default name \"Sections\") of real links to `#id`, so it works with no script, middle-click and copy-link.",
19404
+ "Each link is named by its section's label; the current one carries aria-current=\"location\".",
19405
+ "Activating a link from the keyboard moves focus into the section, as following an anchor does, so the next Tab continues from there.",
19406
+ "The tooltip opens on focus as well as hover; the dots are 44×32 targets.",
19407
+ "Under reduced motion the jump is instant and the marker moves without travel."
19408
+ ],
19409
+ "examples": [
19410
+ {
19411
+ "label": "Fixed beside a long read",
19412
+ "code": "<SectionRail\n aria-label=\"Case study sections\"\n sections={[\n { id: \"overview\", label: \"Overview\" },\n { id: \"problem\", label: \"The problem\" },\n { id: \"process\", label: \"Process\" },\n { id: \"outcome\", label: \"Outcome\" },\n ]}\n className=\"fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden\"\n/>",
19413
+ "imports": "import { SectionRail } from \"@sableui/react\""
19414
+ },
19415
+ {
19416
+ "label": "On the right edge",
19417
+ "code": "<SectionRail\n side=\"right\"\n sections={[\n { id: \"intro\", label: \"Introduction\" },\n { id: \"usage\", label: \"Usage\" },\n { id: \"api\", label: \"API\" },\n ]}\n className=\"fixed top-1/2 right-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden\"\n/>",
19418
+ "imports": "import { SectionRail } from \"@sableui/react\""
19419
+ }
19420
+ ]
19421
+ },
18768
19422
  {
18769
19423
  "name": "SegmentedBar",
18770
19424
  "status": "stable",
@@ -19526,7 +20180,7 @@
19526
20180
  {
19527
20181
  "name": "Sheet (Root)",
19528
20182
  "type": "Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle, snapPoints, snapPoint, defaultSnapPoint, onSnapPointChange, snapToSequentialPoints) — minus swipeDirection",
19529
- "description": "Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason \"swipe\" when a swipe closed it."
20183
+ "description": "Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason \"swipe\" when a swipe closed it. Records the element focused when the sheet opens, trigger or not, and returns focus there on close."
19530
20184
  },
19531
20185
  {
19532
20186
  "name": "snapPoints",
@@ -19614,7 +20268,7 @@
19614
20268
  "The panel scrolls its own content (thin scrollbar, contained overscroll) when it outgrows its bound — the full height on left/right, 80vh on top/bottom.",
19615
20269
  "Esc and click-outside dismiss by default; on touch, a swipe toward the sheet's own edge dismisses too. A short or slow drag springs back.",
19616
20270
  "Swipe is an extra path, never the only one: Esc, click-outside and SheetClose all still work.",
19617
- "Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down); focus returns to the trigger on close.",
20271
+ "Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down). On close, focus returns to the element that held it when the sheet opened: the SheetTrigger, or your own button when it drives a controlled open. It is captured before the content mounts, so an autoFocus field inside cannot displace it; an explicit finalFocus on SheetContent wins.",
19618
20272
  "Render a SheetClose inside SheetContent so screen-reader users can escape the popup.",
19619
20273
  "Slide animation runs via data-[starting-style]/data-[ending-style] attributes on the motion-duration token; while swiping the panel tracks the finger directly, and a released swipe finishes on the same token (scaled by flick speed), so reduced motion makes it instant.",
19620
20274
  "A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge.",
@@ -24505,8 +25159,8 @@
24505
25159
  {
24506
25160
  "name": "ApprovalBlock",
24507
25161
  "path": "blocks/approval-block.tsx",
24508
- "summary": "An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an \"allow for the rest of this chat\" Checkbox, and Deny / Allow.",
24509
- "intent": "Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status=\"awaiting\", then \"complete\" or \"denied\". A destructive action outside an agent is DestructiveActionModal.",
25162
+ "summary": "An agent asking permission for one tool call: the question, what is at stake, the call itself as a ToolCall with its exact input, and Deny / Always allow / Allow once.",
25163
+ "intent": "Reach for this in a chat or agent run when a tool call waits on the person — running a command, sending, paying, deleting, publishing. It wraps the call's own ToolCall, which shows status=\"awaiting\" until the person answers and then \"complete\" or \"denied\". A destructive action outside an agent is DestructiveActionModal.",
24510
25164
  "family": "AI & agents",
24511
25165
  "kind": "card",
24512
25166
  "width": {
@@ -24514,18 +25168,17 @@
24514
25168
  "max": "28rem"
24515
25169
  },
24516
25170
  "notes": [
24517
- "The title is the question in plain words (\"Send the invoice to Ada?\"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.",
24518
- "The description says what can't be undone. That sentence is what the person is deciding on.",
24519
- "Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
24520
- "After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
25171
+ "The title is the question in plain words (\"Run this command?\"); the description says what is at stake. That sentence is what the person is deciding on.",
25172
+ "The call is shown as the agent made it — tool name and raw input, open on arrival — because the exact arguments are what a careful person checks. Do not paraphrase them into a form.",
25173
+ "Three answers, as agent products settle on: Deny on the start edge, then Always allow and Allow once, the primary on the end. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
25174
+ "After a decision the buttons give way to one InlineStatus line saying what happened, and the ToolCall shows the outcome, so the thread keeps a record of the answer."
24521
25175
  ],
24522
25176
  "a11y": [
24523
25177
  "Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.",
24524
- "The details are a dl, so each value is read with its label.",
24525
- "The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
25178
+ "The ToolCall announces its own status change, and its input is a labelled code view."
24526
25179
  ],
24527
- "imports": "import { useState, useEffect, useRef, useId } from \"react\"\nimport { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from \"@sableui/react\"",
24528
- "code": "const request = {\n\ttool: \"send_email\",\n\ttitle: \"Send the invoice to Ada?\",\n\tdescription: \"The agent wants to email invoice INV-1042. An email can't be unsent.\",\n\tdetails: [\n\t\t{ label: \"To\", value: \"ada@example.com\" },\n\t\t{ label: \"Subject\", value: \"INV-1042 for October\" },\n\t\t{ label: \"Attached\", value: \"INV-1042.pdf\" },\n\t],\n\tallowed: \"Allowed. Sending the email.\",\n\tdenied: \"Denied. The agent won't send it.\",\n}\n\nexport interface ApprovalBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a chat column that already frames it. */\n\tframed?: boolean\n\t/** The person allowed the call. `remember` is the \"for the rest of this chat\" choice. */\n\tonApprove?: (options: { remember: boolean }) => void\n\t/** The person declined the call. */\n\tonDeny?: () => void\n}\n\nexport function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {\n\tconst [decision, setDecision] = useState<\"allowed\" | \"denied\" | null>(null)\n\tconst [remember, setRemember] = useState(false)\n\tconst id = useId()\n\tconst outcome = useRef<HTMLDivElement>(null)\n\t// The buttons leave with the decision, so focus moves to the line that\n\t// replaces them instead of falling back to the top of the page.\n\tuseEffect(() => {\n\t\tif (decision) outcome.current?.focus()\n\t}, [decision])\n\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-md\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>{request.title}</CardTitle>\n\t\t\t\t<CardDescription>{request.description}</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t<dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm\">\n\t\t\t\t\t{request.details.map((d) => (\n\t\t\t\t\t\t<div key={d.label} className=\"contents\">\n\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">{d.label}</dt>\n\t\t\t\t\t\t\t<dd className=\"truncate text-foreground\">{d.value}</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t\t<div className=\"contents\">\n\t\t\t\t\t\t<dt className=\"text-muted-foreground\">Tool</dt>\n\t\t\t\t\t\t<dd className=\"min-w-0\">\n\t\t\t\t\t\t\t<Code>{request.tool}</Code>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t{decision ? null : (\n\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />\n\t\t\t\t\t\t<Label htmlFor={`${id}-remember`} className=\"text-body-sm\">\n\t\t\t\t\t\t\tAllow <Code>{request.tool}</Code> for the rest of this chat\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardContent>\n\t\t\t<CardFooter actions className=\"justify-end\">\n\t\t\t\t{decision ? (\n\t\t\t\t\t<InlineStatus\n\t\t\t\t\t\tref={outcome}\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\ttone={decision === \"allowed\" ? \"success\" : \"neutral\"}\n\t\t\t\t\t\tclassName=\"me-auto outline-none\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{decision === \"allowed\" ? request.allowed : request.denied}\n\t\t\t\t\t</InlineStatus>\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"denied\")\n\t\t\t\t\t\t\t\tonDeny?.()\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tDeny\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"allowed\")\n\t\t\t\t\t\t\t\tonApprove?.({ remember })\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tAllow\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
25180
+ "imports": "import { useState, useEffect, useRef } from \"react\"\nimport { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, InlineStatus, ToolCall } from \"@sableui/react\"",
25181
+ "code": "const request = {\n\ttitle: \"Run this command?\",\n\tdescription: \"The agent wants to migrate the production database in acme/api. It changes live data.\",\n\ttool: \"run_command\",\n\taction: \"Run a command\",\n\t// A command reads as the command; a structured call would pass its arguments object.\n\tinput: \"pnpm db:migrate --env production\",\n\tallowed: \"Allowed. Running the migration.\",\n\tdenied: \"Denied. The agent won't run it.\",\n}\n\nexport interface ApprovalBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a chat column that already frames it. */\n\tframed?: boolean\n\t/** The person allowed the call. `remember` is true for \"Always allow\". */\n\tonApprove?: (options: { remember: boolean }) => void\n\t/** The person declined the call. */\n\tonDeny?: () => void\n}\n\nexport function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {\n\tconst [decision, setDecision] = useState<\"allowed\" | \"denied\" | null>(null)\n\tconst outcome = useRef<HTMLDivElement>(null)\n\t// The buttons leave with the decision, so focus moves to the line that\n\t// replaces them instead of falling back to the top of the page.\n\tuseEffect(() => {\n\t\tif (decision) outcome.current?.focus()\n\t}, [decision])\n\n\tconst approve = (remember: boolean) => {\n\t\tsetDecision(\"allowed\")\n\t\tonApprove?.({ remember })\n\t}\n\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-md\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>{request.title}</CardTitle>\n\t\t\t\t<CardDescription>{request.description}</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent>\n\t\t\t\t{/* The call itself, exactly as the agent made it. It answers with the\n\t\t\t\t person: held, then done or denied. */}\n\t\t\t\t<ToolCall\n\t\t\t\t\tname={request.tool}\n\t\t\t\t\ttitle={request.action}\n\t\t\t\t\tstatus={decision === \"allowed\" ? \"complete\" : decision === \"denied\" ? \"denied\" : \"awaiting\"}\n\t\t\t\t\tstatusLabel={decision === \"allowed\" ? \"Allowed\" : undefined}\n\t\t\t\t\tinput={request.input}\n\t\t\t\t\tdefaultOpen\n\t\t\t\t/>\n\t\t\t</CardContent>\n\t\t\t<CardFooter actions>\n\t\t\t\t{decision ? (\n\t\t\t\t\t<InlineStatus\n\t\t\t\t\t\tref={outcome}\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\ttone={decision === \"allowed\" ? \"success\" : \"neutral\"}\n\t\t\t\t\t\tclassName=\"me-auto outline-none\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{decision === \"allowed\" ? request.allowed : request.denied}\n\t\t\t\t\t</InlineStatus>\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tclassName=\"me-auto\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"denied\")\n\t\t\t\t\t\t\t\tonDeny?.()\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tDeny\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button variant=\"outline\" onClick={() => approve(true)}>\n\t\t\t\t\t\t\tAlways allow\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button onClick={() => approve(false)}>Allow once</Button>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
24529
25182
  },
24530
25183
  {
24531
25184
  "name": "ChatThreadBlock",
@@ -27955,6 +28608,36 @@
27955
28608
  "render",
27956
28609
  "style"
27957
28610
  ],
28611
+ "ActivityHeatmap": [
28612
+ "activeDate",
28613
+ "categories",
28614
+ "data",
28615
+ "defaultActiveDate",
28616
+ "end",
28617
+ "formatValue",
28618
+ "label",
28619
+ "legend",
28620
+ "legendLabels",
28621
+ "levelScale",
28622
+ "levels",
28623
+ "loading",
28624
+ "locale",
28625
+ "monthLabels",
28626
+ "motion",
28627
+ "onActiveDateChange",
28628
+ "onSelectDate",
28629
+ "renderCell",
28630
+ "selectedDate",
28631
+ "size",
28632
+ "start",
28633
+ "stretch",
28634
+ "tone",
28635
+ "tooltip",
28636
+ "unit",
28637
+ "weekStart",
28638
+ "weekdayLabels",
28639
+ "year"
28640
+ ],
27958
28641
  "AlertDialog": [
27959
28642
  "actionsRef",
27960
28643
  "children",
@@ -27976,6 +28659,7 @@
27976
28659
  "className",
27977
28660
  "finalFocus",
27978
28661
  "initialFocus",
28662
+ "keepMounted",
27979
28663
  "render",
27980
28664
  "scroll",
27981
28665
  "showCloseButton",
@@ -28436,7 +29120,8 @@
28436
29120
  "filename",
28437
29121
  "highlight",
28438
29122
  "prompt",
28439
- "showCopy"
29123
+ "showCopy",
29124
+ "wrap"
28440
29125
  ],
28441
29126
  "CodeHighlightProvider": [
28442
29127
  "children",
@@ -28870,6 +29555,7 @@
28870
29555
  "className",
28871
29556
  "finalFocus",
28872
29557
  "initialFocus",
29558
+ "keepMounted",
28873
29559
  "render",
28874
29560
  "scroll",
28875
29561
  "showCloseButton",
@@ -29186,6 +29872,7 @@
29186
29872
  "fallback",
29187
29873
  "fit",
29188
29874
  "loading",
29875
+ "position",
29189
29876
  "seed",
29190
29877
  "sizes",
29191
29878
  "src",
@@ -29199,6 +29886,7 @@
29199
29886
  "href",
29200
29887
  "loading",
29201
29888
  "meta",
29889
+ "position",
29202
29890
  "render",
29203
29891
  "seed",
29204
29892
  "size",
@@ -29616,6 +30304,7 @@
29616
30304
  "actions",
29617
30305
  "back",
29618
30306
  "description",
30307
+ "size",
29619
30308
  "title",
29620
30309
  "titleAs"
29621
30310
  ],
@@ -29984,6 +30673,7 @@
29984
30673
  "inputClassName",
29985
30674
  "onClear",
29986
30675
  "onValueChange",
30676
+ "size",
29987
30677
  "value"
29988
30678
  ],
29989
30679
  "SectionHeader": [
@@ -29994,6 +30684,15 @@
29994
30684
  "titleAs",
29995
30685
  "titleId"
29996
30686
  ],
30687
+ "SectionRail": [
30688
+ "motion",
30689
+ "offset",
30690
+ "onValueChange",
30691
+ "root",
30692
+ "sections",
30693
+ "side",
30694
+ "value"
30695
+ ],
29997
30696
  "SegmentedBar": [
29998
30697
  "label",
29999
30698
  "locale",