@sableui/react 0.11.0 → 0.13.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 (90) hide show
  1. package/AGENTS.md +10 -4
  2. package/CHANGELOG.md +199 -0
  3. package/DESIGN.md +3 -3
  4. package/README.md +15 -1
  5. package/bin/sable.mjs +254 -10
  6. package/dist/components/app-header/app-header.meta.js +2 -1
  7. package/dist/components/button/button.js +3 -0
  8. package/dist/components/button/button.meta.js +1 -1
  9. package/dist/components/button/button.variants.js +1 -1
  10. package/dist/components/button-group/button-group.js +17 -0
  11. package/dist/components/button-group/button-group.meta.js +72 -0
  12. package/dist/components/button-group/index.js +7 -0
  13. package/dist/components/caret-menu/caret-menu.js +221 -0
  14. package/dist/components/caret-menu/caret-menu.meta.js +119 -0
  15. package/dist/components/caret-menu/index.js +8 -0
  16. package/dist/components/chat-composer/chat-composer.js +30 -7
  17. package/dist/components/chat-composer/chat-composer.meta.js +27 -8
  18. package/dist/components/combobox/combobox.meta.js +26 -0
  19. package/dist/components/input-group/index.js +7 -0
  20. package/dist/components/input-group/input-group.js +66 -0
  21. package/dist/components/input-group/input-group.meta.js +106 -0
  22. package/dist/components/menu/index.js +2 -0
  23. package/dist/components/menu/menu.js +51 -9
  24. package/dist/components/menu/menu.meta.js +11 -2
  25. package/dist/components/menubar/index.js +8 -0
  26. package/dist/components/menubar/menubar.js +42 -0
  27. package/dist/components/menubar/menubar.meta.js +99 -0
  28. package/dist/components/message-scroller/message-scroller.meta.js +4 -0
  29. package/dist/components/navigation-menu/index.js +19 -0
  30. package/dist/components/navigation-menu/navigation-menu.js +108 -0
  31. package/dist/components/navigation-menu/navigation-menu.meta.js +141 -0
  32. package/dist/components/overflow-menu/overflow-menu.meta.js +13 -1
  33. package/dist/components/pagination/pagination.js +11 -5
  34. package/dist/components/pagination/pagination.meta.js +21 -3
  35. package/dist/components/scroller/scroller.js +8 -4
  36. package/dist/components/scroller/scroller.meta.js +6 -0
  37. package/dist/components/sheet/sheet.js +82 -9
  38. package/dist/components/sheet/sheet.meta.js +41 -4
  39. package/dist/components/sheet/sheet.variants.js +8 -1
  40. package/dist/components/sidebar/sidebar.js +13 -2
  41. package/dist/components/sidebar/sidebar.meta.js +10 -0
  42. package/dist/components/streaming-text/streaming-text.meta.js +5 -1
  43. package/dist/file-hashes.json +49 -33
  44. package/dist/icon-sets/reicon.js +2 -0
  45. package/dist/index.d.ts +660 -164
  46. package/dist/index.js +12 -1
  47. package/dist/lib/palette.js +46 -6
  48. package/dist/lib/taxonomy.js +5 -0
  49. package/dist/lib/use-caret-trigger.js +187 -0
  50. package/dist/lib/use-copy.js +6 -1
  51. package/dist/lib/use-smooth-wheel.js +164 -0
  52. package/dist/palette.css +243 -165
  53. package/dist/patterns/chat-panel.js +1 -1
  54. package/dist/patterns/chat-panel.meta.js +1 -0
  55. package/dist/styles.css +25 -0
  56. package/llms/components/app-header.md +1 -1
  57. package/llms/components/button-group.md +55 -0
  58. package/llms/components/button.md +1 -1
  59. package/llms/components/caret-menu.md +100 -0
  60. package/llms/components/chat-composer.md +24 -7
  61. package/llms/components/combobox.md +12 -0
  62. package/llms/components/input-group.md +78 -0
  63. package/llms/components/menu.md +16 -1
  64. package/llms/components/menubar.md +110 -0
  65. package/llms/components/message-scroller.md +1 -0
  66. package/llms/components/navigation-menu.md +113 -0
  67. package/llms/components/overflow-menu.md +26 -1
  68. package/llms/components/pagination.md +15 -3
  69. package/llms/components/scroller.md +1 -0
  70. package/llms/components/sheet.md +18 -3
  71. package/llms/components/sidebar.md +2 -0
  72. package/llms/components/streaming-text.md +2 -1
  73. package/llms/patterns/chat-panel.md +4 -1
  74. package/llms/reference/token-intent.md +1 -1
  75. package/llms/tasks/building-a-chat.md +158 -0
  76. package/llms/tasks/building-with-sable.md +4 -1
  77. package/llms/tasks/converting-an-app.md +7 -0
  78. package/llms/tasks/extending-a-component.md +170 -0
  79. package/llms/tasks/getting-started.md +22 -4
  80. package/llms/tasks/migrating-from-shadcn.md +351 -0
  81. package/llms-full.txt +1322 -27
  82. package/llms.txt +13 -4
  83. package/package.json +3 -2
  84. package/registry.json +979 -23
  85. package/scripts/lint-rules.mjs +35 -0
  86. package/scripts/wiring.mjs +541 -0
  87. package/skills/entry/SKILL.md +3 -2
  88. package/skills/sable/SKILL.md +26 -5
  89. package/targets/ios.json +45 -0
  90. package/dist/lib/use-wheel-to-horizontal.js +0 -65
package/AGENTS.md CHANGED
@@ -51,10 +51,14 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
51
51
 
52
52
  Pick a theme as a starting point, then override tokens on top of it. Every token a theme sets can be reset by the app on `:root` (or any subtree) after the theme CSS loads — radius, density, type scale, motion scale, accent — and components follow, because they only read tokens. The docs site's **Customize** panel does exactly this live and exports the result as CSS; paste it after your Sable imports. Retrieve the overridable set with `npx sable token --list`. When you retune a filled role (`--primary`, `--sidebar-primary`, `--highlight`, `--destructive`, `--success`, `--tone-warning`, `--tone-info`), check the label on it: `sable lint` measures the pair against the installed theme and suggests one that reads. If the pair is a deliberate brand choice, keep it with `/* design-lint-disable-next-line label-contrast -- <reason> */`. Status colours have a second token for the colour drawn on a surface — `--destructive-text`, `--success-text`, `--tone-warning-text`, `--tone-info-text` — which `text-`, `border-` and `ring-<status>` read; only `bg-<status>` reads the fill. Retune both.
53
53
 
54
+ When a component doesn't do something you need — be a link, carry a control, take a key, offer @mentions or /commands — read `sable docs extending-a-component` before restyling or copying it: `render`, slots, controlled state and refs, `useCaretTrigger` + `CaretMenu`, and how to tell a library gap from app-owned work.
55
+
54
56
  ## Wire it up
55
57
 
56
58
  The machine-readable version of this section is the `consume` block in `registry.json`.
57
59
 
60
+ **Start with `sable init`.** After `pnpm add @sableui/react`, run `pnpm exec sable init` (`npx sable init` once the package is installed — never before, when `npx sable` would fetch an unrelated package) from the app root. It runs steps 2–5 and 7 below: installs missing packages with the project's package manager, puts the CSS imports in order, adds dedupe and the Tailwind plugin to a Vite config (or a PostCSS config for Next.js), loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill, and finishes with `sable doctor`. What it cannot change safely it prints as a snippet with the file it belongs in; apply those, then run `sable doctor` again. `--dry-run` previews every change. In an app that is already wired, `sable doctor --fix` applies the same fixes to whatever doctor flags. The numbered steps are the manual route and what `init` checks against.
61
+
58
62
  1. **Install** from npm: `pnpm add @sableui/react` (or the project's package manager). No registry configuration or token is needed. Sable depends on no router, data or form library, and keeps whatever the project already uses: links, menus and nav items take any router's link through `render` (`render={<Link to="/inbox" />}` is TanStack Router's or React Router's `Link` alike). For a blank-slate app, the suggested stack is Vite + TypeScript with TanStack Router, TanStack Query, TanStack Form for forms with validation logic, and TanStack Table once a table outgrows `DataTable` (`sable docs tables-with-tanstack`).
59
63
  2. **Dedupe React** in the bundler (`resolve.dedupe: ["react","react-dom"]`) — Base UI must not resolve a second React, or you get an "invalid hook call" blank page.
60
64
  3. **CSS, in order:** `@import "tailwindcss";` → `@import "@sableui/react/styles.css";` → `@source "<node_modules>/@sableui/react/dist/**/*.{js,mjs}";` so Tailwind scans the package's classes. That ships every component's classes (about 32 KB gzipped); before shipping, `sable sources --write src/sable-sources.css` lists just the modules the app imports, imported in place of that `@source`, and `sable check` fails when it goes stale.
@@ -73,7 +77,8 @@ The machine-readable version of this section is the `consume` block in `registry
73
77
  npx sable pattern SettingsPage --skeleton # composition tree, fixtures stripped
74
78
  npx sable block --list # section-sized blocks: cards, forms, lists
75
79
  npx sable component DataTable --props
76
- npx sable search "empty state" # across components, patterns, layout, tokens, docs
80
+ npx sable search "empty state" # across components, examples, patterns, blocks, layout, tokens, docs
81
+ npx sable component Form --example "Server errors" # one example a search hit names, imports included
77
82
  npx sable docs converting-an-app # the task walkthroughs
78
83
  npx sable manifest --json # the whole command surface, machine-readable
79
84
  ```
@@ -150,9 +155,10 @@ package with `--root` naming the repository root (for example
150
155
  there and resolve Sable through the package.
151
156
  Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
152
157
  upgrades also update the workflow. Other agents can read that file directly.
153
- Use `$sable build <idea>`, `inspire <references>`, `audit <target>`, or
154
- `polish <target>` in Codex; use `/sable` with the same modes in Claude Code or
155
- Cursor. `create` is a build alias. These are agent modes, not shell commands.
158
+ Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
159
+ `polish <target>`, or `fix <target>` in Codex; use `/sable` with the same modes in
160
+ Claude Code or Cursor. `create` is a build alias. `fix` takes the short path for
161
+ one scoped defect and escalates to `polish` when the change grows. These are agent modes, not shell commands.
156
162
 
157
163
  Upgrade with `pnpm update @sableui/react --latest`, then
158
164
  `pnpm exec sable setup --agent --update`. Setup only replaces managed files
package/CHANGELOG.md CHANGED
@@ -1,5 +1,204 @@
1
1
  # What's new
2
2
 
3
+ ## 0.13.0
4
+
5
+ Tone scales that keep their steps apart, whatever the tone.
6
+
7
+ - **Breaking: tone scales are one ladder, and the tone sits on its nearest
8
+ step.** Every tone scale (`tone-1-*` … `tone-3-*`, `tone-info-*`,
9
+ `tone-warning-*`, `success-*`, `destructive-*`) now uses one shared
10
+ ladder of depths per mode, so `bg-tone-1-100` and `bg-success-100` are
11
+ the same depth. The tone lands exactly on the step nearest its own
12
+ lightness, not always on 500. Before, a tone near one end of the range
13
+ squeezed five steps into the little room left past it: in dark mode,
14
+ light status colours (Poppy, Prism) had 600–950 almost
15
+ indistinguishable, and deep tones in light mode crowded 800–950. Now the
16
+ steps either side stretch to keep the ladder's ends. Migration: where you
17
+ used `*-500` to mean the tone, use the tone token (`bg-success`,
18
+ `text-tone-1`). The other steps keep their roles (100 a wash, 700/800
19
+ text), but may be a little lighter or deeper than before.
20
+ - **Tone steps stay in sRGB for any hue.** A tone scale's chroma falls from
21
+ the tone toward each end of the ladder, under a bound fitted per step
22
+ over the whole hue circle, so a customised or app-defined tone can't push
23
+ a step out of gamut. This replaces the fixed caps on the light steps.
24
+ - **Yellow sits a little deeper in dark mode** (500 at L 0.78, was 0.82), so
25
+ its 800–950 no longer crowd against the ink.
26
+ - **The palette tests hold spacing.** Neighbouring steps of every scale, in
27
+ every theme and mode, are at least 3 apart in OKLab (the 50→100 washes
28
+ 1.4), and every tone sits exactly on its nearest step. Before, the only
29
+ check was that each step moved 0.005 in lightness, and 73 of 224 scales
30
+ had a pair of neighbouring steps under 3 apart.
31
+
32
+ ## 0.12.0
33
+
34
+ Getting to a working first component, finding the right example, moving over from shadcn, and the components that move called for: Menubar, NavigationMenu, InputGroup, ButtonGroup, CaretMenu and bottom-sheet snap points.
35
+
36
+ - **New: `sable init` wires an app in one step.** Run it after `pnpm add
37
+ @sableui/react`. It installs what is missing with your package manager
38
+ (React, Tailwind and its build plugin), puts the CSS imports in order, adds
39
+ React dedupe and the Tailwind plugin to a Vite config (a
40
+ `postcss.config.mjs` for Next.js), loads Figtree and DM Mono, and wraps a
41
+ Vite starter's `<App />` in `IconProvider`. Then it installs the agent
42
+ skill and runs `sable doctor`. What it can't change safely, such as a
43
+ Next.js layout or an unfamiliar config, it prints as the snippet and the
44
+ file it belongs in. `--dry-run` shows every change first.
45
+ - **New: `sable doctor --fix`** applies the same fixes to whatever doctor
46
+ flags in an app that's already set up, then checks again. A fix that
47
+ didn't take is reported as unverified, not done. Doctor also gains two
48
+ warnings: Tailwind missing from the Vite or Next.js build (`css:build`),
49
+ and the theme's fonts never loaded (`fonts`).
50
+ - **`sable search` finds examples.** Every component example is searchable
51
+ by what it shows: `sable search "multiple selection"` or `"server errors"`.
52
+ A hit names its own retrieve command, `sable component Combobox --example
53
+ "Multiple selection"`, which prints that one example with its imports.
54
+ Examples are capped per component and per list, so a component name still
55
+ lands on the component.
56
+ - **New: a migration guide from shadcn/ui and Radix.** `sable docs
57
+ migrating-from-shadcn` maps what changes everywhere (`asChild` becomes
58
+ `render`, values become arrays, menu rows take `onClick`,
59
+ `data-[state=open]:` becomes `sable-open:`), the tokens, lucide icons, and
60
+ every shadcn component to its Sable counterpart, or what to compose where
61
+ there isn't one. `converting-an-app` points to it.
62
+ - **New lint rule: `shadcn-leftover`.** Flags imports still coming from the
63
+ old stack (`@radix-ui/*`, `@/components/ui`, `lucide-react`, `sonner`,
64
+ `vaul`, `cmdk`, `input-otp`, `next-themes`) and any `asChild` prop, which a
65
+ Sable trigger silently ignores. A deliberate keeper takes a reasoned disable.
66
+ - **The agent skill has a `fix` mode.** `$sable fix <target>` (or `/sable
67
+ fix`) repairs one scoped defect on a short path: it reads only the
68
+ contracts the change touches, then runs `sable check` on the changed files
69
+ and looks at the affected state. It escalates to `polish`, and its full
70
+ finish checklist, when the change grows. Run `sable setup --agent --update`
71
+ to refresh installed entries.
72
+ - **ChatComposer can send attachments without text.** Pass
73
+ `allowEmpty={files.length > 0}` and send enables on the files alone;
74
+ `onSend` receives `""`. Tie it to the pending files: a constant
75
+ `allowEmpty` leaves send enabled with nothing to send.
76
+ - **ChatComposer: on a phone or tablet, Return starts a new line.** A soft
77
+ keyboard has no Shift+Return, so on a touch device (`pointer: coarse`) the
78
+ default `sendOn="enter"` now behaves as `mod-enter`: Return breaks the
79
+ line, the send button sends, and ⌘/Ctrl+Enter still sends from a hardware
80
+ keyboard. `aria-keyshortcuts` reports whichever is in effect. `footer` now
81
+ also takes a function of the send key in effect, so a keyboard hint can
82
+ stay true: `footer={({ sendOn }) => sendOn === "enter" ? "Enter to send" :
83
+ null}` (null removes the row). *Migration:* a fixed "Enter to send" footer
84
+ is wrong on touch; switch it to the function form, as ChatPanel has.
85
+ - **Combobox documents server-backed search.** A new "Async search" example
86
+ and the props it uses (`filter={null}`, `onInputValueChange`,
87
+ `itemToStringLabel`) show how to show results a server already filtered.
88
+ - **New: `Menubar`.** An application's row of menus (File, Edit, View) over
89
+ Base UI's Menubar. Each menu is an ordinary `Menu` with a `MenubarTrigger`
90
+ as its title. The bar is one tab stop with ←/→ between titles, and once a
91
+ menu is open, pointing at a neighbour opens it.
92
+ - **New: `NavigationMenu`.** A site header's primary nav: a bar of links and
93
+ triggers, where a trigger opens a panel of links on hover, click or Enter.
94
+ One shared panel moves along the bar and resizes to each trigger's
95
+ content. Links take your router's link through `render` and mark the
96
+ current page with `active`; panel rows take a `description`. Below `md`,
97
+ hand the same destinations to a Sheet with a SideNav, as the docs show.
98
+ - **Pagination can render real links.** `pageLink={(page) => <Link
99
+ to={`?page=${page}`} />}` makes every page control a link that crawlers
100
+ follow and readers can open in a new tab. The current page keeps
101
+ `aria-current`; the arrows at the ends stay disabled buttons.
102
+ `onPageChange` is optional with `pageLink`.
103
+ - **New: `InputGroup`.** An Input with addons inside the same field: a
104
+ prefix or suffix ("https://", ".com"), an icon, or a borderless Button
105
+ such as copy or reveal. One shell, one focus ring, one error state; inside
106
+ a Field, the label, description and error reach the Input.
107
+ - **OverflowMenu documents responsive actions.** Secondary actions inline
108
+ when the container is wide, folded into the menu when it's narrow, the
109
+ primary action always inline. `sable search "responsive action menu"`
110
+ finds it.
111
+ - **Sheet: snap points for bottom sheets.** `<Sheet snapPoints={[0.4, 1]}>`
112
+ gives a bottom sheet stops between closed and fully open; a swipe moves
113
+ between them, and `snapPoint` / `onSnapPointChange` control the active
114
+ one (new `SheetSnapPoint` type). With stops the sheet shows a drag handle,
115
+ and the part below the screen becomes padding, so content scrolls and
116
+ `SheetFooter` stays visible. Other sides ignore stops and a development
117
+ build says so. Every bottom sheet now also rides above the on-screen
118
+ keyboard (measured from `visualViewport`).
119
+ - **New: `CaretMenu` and `useCaretTrigger`, mentions and slash commands on
120
+ any text field.** The hook watches a Textarea, Input or ChatComposer
121
+ (`fieldRef`) for a trigger character at the caret (`@`, `/` at the start,
122
+ `#`) and reports the query. CaretMenu shows suggestions there, driven by
123
+ ↑/↓, Enter, Tab and Escape while focus stays in the field. The app
124
+ supplies the items and what choosing one does: `trigger.replace("@Sam
125
+ Rivera ")` inserts a mention as text, and a command runs and `replace("")`
126
+ clears it. The value stays a plain string; chips inside the field need a
127
+ rich-text editor, which Sable doesn't ship.
128
+ - **ChatComposer: an Enter someone else handled doesn't send.** A listener
129
+ on `fieldRef` that handles Enter and calls `preventDefault()` owns it. It's
130
+ how CaretMenu picks a suggestion instead of sending, and how an app adds
131
+ key behaviour of its own.
132
+ - **ChatComposer keeps focus after Stop.** Stop turns back into send in
133
+ place, and with an empty field that send is disabled, which dropped focus
134
+ to the page. Focus now moves to the field, except on touch, where it would
135
+ raise the keyboard unasked.
136
+ - **Scroller eases the mouse wheel.** New `smoothWheel` (on by default): a
137
+ notch eases toward its target instead of jumping, on either axis, the
138
+ inertia a library like Lenis adds but scoped to the region. Only a mouse
139
+ wheel is eased: trackpads and touch keep their own momentum, keys and
140
+ scrollbar drags stay native, it releases at the ends so the page scrolls
141
+ on, a nested area that can still scroll (a code block, a textarea) keeps
142
+ the wheel, a region that smooths or snaps in CSS is left to the browser,
143
+ and anything else that moves the region (`scrollIntoView`, a scrollbar
144
+ drag) stops it at once. Reduced motion turns it off. New
145
+ `useSmoothWheel()` gives a scroll container of your own the same thing.
146
+ - **New guide: `sable docs building-a-chat`.** Wires Sable's chat parts to
147
+ any streaming source: which part owns what and what stays with the app, a
148
+ turn's lifecycle mapped onto `busy`, `onStop`, `streaming` and `disabled`,
149
+ where each failure goes, announcing a turn once rather than per token, and
150
+ rendering model output without injecting HTML.
151
+ - **New guide: `sable docs extending-a-component`.** What to do when a
152
+ component doesn't do what you need: compose with `render`, fill a slot,
153
+ hold the state and reach the element, add behaviour on top (mentions as
154
+ the worked example), change the look through tokens and presets rather
155
+ than overrides, patch a defect, and tell a library gap from app work.
156
+ - **The shadcn migration guide now maps every component.** Menubar,
157
+ NavigationMenu, InputGroup, ButtonGroup, link Pagination and drawer snap
158
+ points all have counterparts.
159
+ - **Fix: a horizontal Scroller no longer widens its container.** Its outer
160
+ box kept the default `min-width: auto`, so inside a grid or flex column a
161
+ strip of cards grew to its full length and never scrolled. It now shrinks
162
+ to the column and scrolls with its edge fade. The Scrolling page's own
163
+ example was showing exactly this, and also lacked `orientation="horizontal"`.
164
+ - **`useCopy` takes a function.** `useCopy(() => fetch(url).then((r) =>
165
+ r.text()))` produces the text only on click; a pending promise goes to the
166
+ clipboard as a ClipboardItem so Safari still counts it as part of the click.
167
+ New `CopySource` type.
168
+ - **New: `ButtonGroup`.** Buttons joined into one control, such as a split
169
+ button (an action beside the chevron that opens its menu) or a short run of
170
+ related actions. The children stay ordinary Buttons with their own variants
171
+ and sizes. The group squares their inner corners, collapses bordered seams
172
+ into one hairline and draws a seam between filled ones. Button now reads a
173
+ per-side radius (`--button-radius-s` / `--button-radius-e`, falling back to
174
+ `--button-radius`) and renders `data-variant`, which is how the group
175
+ does it without overriding a class.
176
+ - **Menu rows can carry an icon and a description.** `MenuItem` takes `icon`
177
+ and `description`, a quieter second line saying what the action does; with
178
+ a description the icon sits in a small tile spanning both lines.
179
+ - **New: `MenuLinkItem`.** A menu row that navigates: a real `<a>`, so it opens
180
+ in a new tab, middle-clicks and copies as a link. With `target="_blank"`
181
+ it adds `rel="noopener noreferrer"` and a trailing ↗.
182
+ - **Icon contract: `arrow-up-right`.** Added for links that leave the app.
183
+ An app with its own icon set should map it; Reicon's set already does.
184
+ - **sableui.com: a Copy page button on every page.** Each page with a
185
+ Markdown version has a split button in its header. Copy page copies the
186
+ page as Markdown, fetched only when you click. The menu behind the chevron
187
+ opens the Markdown file, starts a ChatGPT or Claude chat that reads the
188
+ page (Claude tries the desktop app first and falls back to claude.ai), and
189
+ copies the setup command (`pnpm add @sableui/react && pnpm exec
190
+ sable init`). The Markdown files are the same ones the site publishes at
191
+ `/<page>.md`; both now come from one table
192
+ (`apps/docs/src/markdown-twins.json`).
193
+ - **New: `Sidebar surface="page"`.** The rail paints the page's own surface
194
+ and ink, with no border, instead of the theme's `--sidebar-*` set. Use it
195
+ when a full-width header sits above the rail: a tinted or dark rail
196
+ (Verdant, Meridian) starting under a page-coloured bar reads as pasted on.
197
+ The default, `surface="rail"`, is unchanged and still right for a
198
+ full-height rail. Overriding `bg-background text-foreground` on a Sidebar
199
+ never worked, because the rail re-points those tokens at its own;
200
+ use the prop instead. sableui.com's docs rail now uses it.
201
+
3
202
  ## 0.11.0
4
203
 
5
204
  - **New: `PropertyGroup`, a collapsible group inside a PanelSection.** The
package/DESIGN.md CHANGED
@@ -85,9 +85,9 @@ Roles are the contract, and roles are not enough for a product: tags, avatars, l
85
85
  - **Nine families:** `neutral`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple` and `pink`, each 50–950, as Tailwind utilities (`bg-blue-100`, `text-pink-700`) and variables (`--teal-500`). Tailwind's other families are switched off.
86
86
  - **The lean.** A family keeps its own hue and leans toward the brand by half the gap, at most 15°, and only as far as the brand has colour (an ink-black primary pulls nothing). It's Material 3's harmonize, and the same lean the customizer gives status colours. `--theme-palette-harmony` scales it: 0 keeps the families' own hues, 2 doubles the pull.
87
87
  - **Neutral** is the theme's own `--background` mixed toward its `--foreground`, so it carries the surfaces' tint.
88
- - **Tone scales.** Every tone (tone-1…3, info, warning, success, destructive) gets a scale anchored on itself: step 500 is the tone, so `bg-tone-1-100` is its wash.
88
+ - **Tone scales.** Every tone (tone-1…3, info, warning, success, destructive) gets a scale on one shared ladder of depths, so `bg-tone-1-100` and `bg-success-100` are the same depth. The tone sits exactly on the step nearest its own lightness: a mid tone is 500, a light status colour in dark mode is 800. The steps either side stretch to keep the ladder's ends, so a tone near one end does not crush five steps into the little room left past it. Use `--success` itself for the tone, and the steps for its washes and deeper inks. Chroma falls from the tone toward each end, under an sRGB bound fitted per step over the whole hue circle, since a tone can be any hue.
89
89
  - **Steps follow the mode.** A low step is always near the page and a high step always far from it, so a pair like `bg-blue-100 text-blue-800` reads in both modes with no `dark:`.
90
- - **Held, not hoped.** It's all relative-colour CSS computed where it is declared, so a preset, a customizer look or an app's `--primary` carries into it with no regeneration. Chroma is fitted under sRGB across the whole lean (piecewise, a knot every 5°), and `palette.test.ts` evaluates the shipped CSS for every built-in theme and mode: in gamut, steadily away from the page, `700` readable on the page, `800` readable on its own `100`, and neighbouring families apart.
90
+ - **Held, not hoped.** It's all relative-colour CSS computed where it is declared, so a preset, a customizer look or an app's `--primary` carries into it with no regeneration. Chroma is fitted under sRGB across the whole lean (piecewise, a knot every 5°), and `palette.test.ts` evaluates the shipped CSS for every built-in theme and mode: in gamut, steadily away from the page, `700` readable on the page, `800` readable on its own `100`, neighbouring steps at least 3 apart in OKLab (the 50→100 washes 1.4), every tone exactly on its nearest step, and neighbouring families apart.
91
91
 
92
92
  Components never name a step. They paint roles, which is what lets a theme restyle them. Where a component takes a family (Badge, Pill and StatusDot accept `tone="blue"`, a category with no meaning attached), it sets `data-palette` to the family and paints from the slots that fills: `palette-wash` (100), `palette-wash-strong` (200), `palette-edge` (300), `palette-dot` (500), `palette-solid` (700), `palette-text` (800) and `palette-solid-ink` (50). The classes are the same for every family, and each ink-on-fill pair is held to 4.5:1. An app can use `data-palette` the same way.
93
93
 
@@ -469,7 +469,7 @@ Reference via `z-(--z-overlay)`, never a raw `z-[9999]`. Base UI overlays portal
469
469
 
470
470
  **Vertical gets a bar; horizontal gets a carousel.** A vertical list needs position feedback — how far through a long transcript am I — and a thin bar is the only thing that gives it. A horizontal strip does not: what a reader needs there is "there is more this way", which an **edge fade says better than a 4px rail**, and which the rail obscures by adding chrome under the content. So `Scroller` draws a bar on its vertical axis and none on its horizontal one, where its alpha-masked edges already do the work. Reach for the same split in anything strip-shaped: media rows, chip rows, tab overflow.
471
471
 
472
- **A horizontal region must be operable without a horizontal input.** Hiding the bar removed the last non-drag affordance on that axis, and most mice have no horizontal wheel — so `Scroller` maps a vertical wheel to sideways movement, and is focusable with arrow / Home / End / PageUp / PageDown handling. Both are on by default; the wheel mapping is `wheelScroll`, the name is `label`. The four things that make a naive wheel mapping feel broken, all handled in `useWheelToHorizontal`: it **releases at both ends** so it never traps a reader scrolling the page, it **ignores trackpad `deltaX` gestures** (`|deltaX| > |deltaY|` means the browser is already doing it, and handling `deltaY` too double-scrolls a diagonal swipe), it **ignores `shift+wheel`** (already native), and it **normalises `deltaMode`** (mice report *lines*, so the raw value moves ~3px per notch). Keyboard focus is declared rather than inherited: Chrome 128+ and Firefox make scroll regions focusable on their own, Safari does not.
472
+ **A horizontal region must be operable without a horizontal input.** Hiding the bar removed the last non-drag affordance on that axis, and most mice have no horizontal wheel — so `Scroller` maps a vertical wheel to sideways movement, and is focusable with arrow / Home / End / PageUp / PageDown handling. Both are on by default; the wheel mapping is `wheelScroll`, the name is `label`. The four things that make a naive wheel mapping feel broken, all handled in `useSmoothWheel` (lib/use-smooth-wheel.ts): it **releases at both ends** so it never traps a reader scrolling the page, it **ignores trackpad `deltaX` gestures** (`|deltaX| > |deltaY|` means the browser is already doing it, and handling `deltaY` too double-scrolls a diagonal swipe), it **ignores `shift+wheel`** (already native), and it **normalises `deltaMode`** (mice report *lines*, so the raw value moves ~3px per notch). The same hook eases a mouse wheel on either axis (`smoothWheel`, on by default): only notches, never trackpads or touch, it gives way to a nested area that can still scroll, it leaves regions that smooth or snap in CSS to the browser, and anything else that moves the region stops the ease. Keyboard focus is declared rather than inherited: Chrome 128+ and Firefox make scroll regions focusable on their own, Safari does not.
473
473
 
474
474
  **Reveal is driven by scrolling, not by hover.** Hover is a decent proxy and a bad one for the cases that matter: a touch drag hovers nothing, and neither does a programmatic scroll — a chat auto-following a streamed reply, a jump-to-message, a restored read position — which are exactly the moments a reader needs to see where they now are. `useScrollActivity` sets `data-scrolling` on the element for ~600ms after each scroll event and `scrollbar-quiet` keys off it. Hover and `:focus-within` remain in the CSS as the no-JS baseline, so the bare utility still behaves for a consumer who never reaches for the hook. The fade honours `--motion-scale`, so reduced motion collapses it to an instant swap.
475
475
 
package/README.md CHANGED
@@ -62,6 +62,17 @@ retrieval is the default. These tools do not update model weights. See
62
62
 
63
63
  > **Agents:** the machine-readable version of these steps is the `consume` block in [`registry.json`](./registry.json).
64
64
 
65
+ **Quick start** — install, then let `sable init` do steps 2–5:
66
+
67
+ ```bash
68
+ pnpm add @sableui/react
69
+ pnpm exec sable init # or: npx sable init — once the package is installed
70
+ ```
71
+
72
+ `init` reads your package manager from the lockfile and your framework from the project. It installs what is missing (React, Tailwind and its build plugin), puts the CSS imports below in order, adds React dedupe and the Tailwind plugin to a Vite config, loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill (`sable setup --agent`), then runs `sable doctor`. Anything it cannot change safely — an unfamiliar config, a Next.js layout — it prints as the exact snippet and the file it belongs in. Vite and Next.js (App Router) get every step; other setups get the packages and the stylesheet. `--dry-run` shows every change first; `--no-agent` skips the skill. Already set up? `sable doctor --fix` applies the same fixes to whatever doctor flags.
73
+
74
+ The steps below are what `init` does, for doing it by hand or checking its work.
75
+
65
76
  **1. Install:**
66
77
 
67
78
  ```bash
@@ -138,8 +149,11 @@ npx sable pattern --list # the eight canonical page shells
138
149
  npx sable pattern DashboardPage --skeleton # its composition tree — 127 lines of source, 45 of structure
139
150
  npx sable block PricingCardBlock --code # a section-sized block's source, imports included
140
151
  npx sable component Button --props
141
- npx sable search "date range" # one ranked set across components, patterns, layout, tokens, docs
152
+ npx sable search "date range" # one ranked set across components, examples, patterns, layout, tokens, docs
153
+ npx sable component Calendar --example Range # just the example a search hit names
154
+ npx sable init # wire a new app: packages, CSS, fonts, IconProvider, agent skill, then doctor
142
155
  npx sable doctor # is this project wired correctly? exits 1 if not
156
+ npx sable doctor --fix # apply the fixes doctor can make safely; print the rest
143
157
  npx sable docs designing-a-surface # brief, direction, composition, interaction, adaptation
144
158
  npx sable docs developing-taste # references, rendered directions, scoped judgment
145
159
  npx sable atlas --search "editorial culture" --contrast --limit 3 --json # a project's own atlas, if it has one