@sableui/react 0.10.0 → 0.12.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 (91) hide show
  1. package/AGENTS.md +10 -4
  2. package/CHANGELOG.md +184 -0
  3. package/DESIGN.md +1 -1
  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/panel-section/index.js +2 -1
  36. package/dist/components/panel-section/panel-section.js +15 -0
  37. package/dist/components/panel-section/panel-section.meta.js +32 -0
  38. package/dist/components/scroller/scroller.js +8 -4
  39. package/dist/components/scroller/scroller.meta.js +6 -0
  40. package/dist/components/sheet/sheet.js +82 -9
  41. package/dist/components/sheet/sheet.meta.js +41 -4
  42. package/dist/components/sheet/sheet.variants.js +8 -1
  43. package/dist/components/sidebar/sidebar.js +13 -2
  44. package/dist/components/sidebar/sidebar.meta.js +10 -0
  45. package/dist/components/streaming-text/streaming-text.meta.js +5 -1
  46. package/dist/file-hashes.json +50 -34
  47. package/dist/icon-sets/reicon.js +2 -0
  48. package/dist/index.d.ts +687 -164
  49. package/dist/index.js +12 -1
  50. package/dist/lib/taxonomy.js +5 -0
  51. package/dist/lib/use-caret-trigger.js +187 -0
  52. package/dist/lib/use-copy.js +6 -1
  53. package/dist/lib/use-smooth-wheel.js +164 -0
  54. package/dist/patterns/chat-panel.js +1 -1
  55. package/dist/patterns/chat-panel.meta.js +1 -0
  56. package/dist/styles.css +25 -0
  57. package/llms/components/app-header.md +1 -1
  58. package/llms/components/button-group.md +55 -0
  59. package/llms/components/button.md +1 -1
  60. package/llms/components/caret-menu.md +100 -0
  61. package/llms/components/chat-composer.md +24 -7
  62. package/llms/components/combobox.md +12 -0
  63. package/llms/components/input-group.md +78 -0
  64. package/llms/components/menu.md +16 -1
  65. package/llms/components/menubar.md +110 -0
  66. package/llms/components/message-scroller.md +1 -0
  67. package/llms/components/navigation-menu.md +113 -0
  68. package/llms/components/overflow-menu.md +26 -1
  69. package/llms/components/pagination.md +15 -3
  70. package/llms/components/panel-section.md +28 -0
  71. package/llms/components/scroller.md +1 -0
  72. package/llms/components/sheet.md +18 -3
  73. package/llms/components/sidebar.md +2 -0
  74. package/llms/components/streaming-text.md +2 -1
  75. package/llms/patterns/chat-panel.md +4 -1
  76. package/llms/tasks/building-a-chat.md +158 -0
  77. package/llms/tasks/building-with-sable.md +4 -1
  78. package/llms/tasks/converting-an-app.md +7 -0
  79. package/llms/tasks/extending-a-component.md +170 -0
  80. package/llms/tasks/getting-started.md +22 -4
  81. package/llms/tasks/migrating-from-shadcn.md +351 -0
  82. package/llms-full.txt +1349 -26
  83. package/llms.txt +14 -5
  84. package/package.json +3 -2
  85. package/registry.json +1037 -53
  86. package/scripts/lint-rules.mjs +35 -0
  87. package/scripts/wiring.mjs +541 -0
  88. package/skills/entry/SKILL.md +3 -2
  89. package/skills/sable/SKILL.md +26 -5
  90. package/targets/ios.json +45 -0
  91. 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,189 @@
1
1
  # What's new
2
2
 
3
+ ## 0.12.0
4
+
5
+ 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.
6
+
7
+ - **New: `sable init` wires an app in one step.** Run it after `pnpm add
8
+ @sableui/react`. It installs what is missing with your package manager
9
+ (React, Tailwind and its build plugin), puts the CSS imports in order, adds
10
+ React dedupe and the Tailwind plugin to a Vite config (a
11
+ `postcss.config.mjs` for Next.js), loads Figtree and DM Mono, and wraps a
12
+ Vite starter's `<App />` in `IconProvider`. Then it installs the agent
13
+ skill and runs `sable doctor`. What it can't change safely, such as a
14
+ Next.js layout or an unfamiliar config, it prints as the snippet and the
15
+ file it belongs in. `--dry-run` shows every change first.
16
+ - **New: `sable doctor --fix`** applies the same fixes to whatever doctor
17
+ flags in an app that's already set up, then checks again. A fix that
18
+ didn't take is reported as unverified, not done. Doctor also gains two
19
+ warnings: Tailwind missing from the Vite or Next.js build (`css:build`),
20
+ and the theme's fonts never loaded (`fonts`).
21
+ - **`sable search` finds examples.** Every component example is searchable
22
+ by what it shows: `sable search "multiple selection"` or `"server errors"`.
23
+ A hit names its own retrieve command, `sable component Combobox --example
24
+ "Multiple selection"`, which prints that one example with its imports.
25
+ Examples are capped per component and per list, so a component name still
26
+ lands on the component.
27
+ - **New: a migration guide from shadcn/ui and Radix.** `sable docs
28
+ migrating-from-shadcn` maps what changes everywhere (`asChild` becomes
29
+ `render`, values become arrays, menu rows take `onClick`,
30
+ `data-[state=open]:` becomes `sable-open:`), the tokens, lucide icons, and
31
+ every shadcn component to its Sable counterpart, or what to compose where
32
+ there isn't one. `converting-an-app` points to it.
33
+ - **New lint rule: `shadcn-leftover`.** Flags imports still coming from the
34
+ old stack (`@radix-ui/*`, `@/components/ui`, `lucide-react`, `sonner`,
35
+ `vaul`, `cmdk`, `input-otp`, `next-themes`) and any `asChild` prop, which a
36
+ Sable trigger silently ignores. A deliberate keeper takes a reasoned disable.
37
+ - **The agent skill has a `fix` mode.** `$sable fix <target>` (or `/sable
38
+ fix`) repairs one scoped defect on a short path: it reads only the
39
+ contracts the change touches, then runs `sable check` on the changed files
40
+ and looks at the affected state. It escalates to `polish`, and its full
41
+ finish checklist, when the change grows. Run `sable setup --agent --update`
42
+ to refresh installed entries.
43
+ - **ChatComposer can send attachments without text.** Pass
44
+ `allowEmpty={files.length > 0}` and send enables on the files alone;
45
+ `onSend` receives `""`. Tie it to the pending files: a constant
46
+ `allowEmpty` leaves send enabled with nothing to send.
47
+ - **ChatComposer: on a phone or tablet, Return starts a new line.** A soft
48
+ keyboard has no Shift+Return, so on a touch device (`pointer: coarse`) the
49
+ default `sendOn="enter"` now behaves as `mod-enter`: Return breaks the
50
+ line, the send button sends, and ⌘/Ctrl+Enter still sends from a hardware
51
+ keyboard. `aria-keyshortcuts` reports whichever is in effect. `footer` now
52
+ also takes a function of the send key in effect, so a keyboard hint can
53
+ stay true: `footer={({ sendOn }) => sendOn === "enter" ? "Enter to send" :
54
+ null}` (null removes the row). *Migration:* a fixed "Enter to send" footer
55
+ is wrong on touch; switch it to the function form, as ChatPanel has.
56
+ - **Combobox documents server-backed search.** A new "Async search" example
57
+ and the props it uses (`filter={null}`, `onInputValueChange`,
58
+ `itemToStringLabel`) show how to show results a server already filtered.
59
+ - **New: `Menubar`.** An application's row of menus (File, Edit, View) over
60
+ Base UI's Menubar. Each menu is an ordinary `Menu` with a `MenubarTrigger`
61
+ as its title. The bar is one tab stop with ←/→ between titles, and once a
62
+ menu is open, pointing at a neighbour opens it.
63
+ - **New: `NavigationMenu`.** A site header's primary nav: a bar of links and
64
+ triggers, where a trigger opens a panel of links on hover, click or Enter.
65
+ One shared panel moves along the bar and resizes to each trigger's
66
+ content. Links take your router's link through `render` and mark the
67
+ current page with `active`; panel rows take a `description`. Below `md`,
68
+ hand the same destinations to a Sheet with a SideNav, as the docs show.
69
+ - **Pagination can render real links.** `pageLink={(page) => <Link
70
+ to={`?page=${page}`} />}` makes every page control a link that crawlers
71
+ follow and readers can open in a new tab. The current page keeps
72
+ `aria-current`; the arrows at the ends stay disabled buttons.
73
+ `onPageChange` is optional with `pageLink`.
74
+ - **New: `InputGroup`.** An Input with addons inside the same field: a
75
+ prefix or suffix ("https://", ".com"), an icon, or a borderless Button
76
+ such as copy or reveal. One shell, one focus ring, one error state; inside
77
+ a Field, the label, description and error reach the Input.
78
+ - **OverflowMenu documents responsive actions.** Secondary actions inline
79
+ when the container is wide, folded into the menu when it's narrow, the
80
+ primary action always inline. `sable search "responsive action menu"`
81
+ finds it.
82
+ - **Sheet: snap points for bottom sheets.** `<Sheet snapPoints={[0.4, 1]}>`
83
+ gives a bottom sheet stops between closed and fully open; a swipe moves
84
+ between them, and `snapPoint` / `onSnapPointChange` control the active
85
+ one (new `SheetSnapPoint` type). With stops the sheet shows a drag handle,
86
+ and the part below the screen becomes padding, so content scrolls and
87
+ `SheetFooter` stays visible. Other sides ignore stops and a development
88
+ build says so. Every bottom sheet now also rides above the on-screen
89
+ keyboard (measured from `visualViewport`).
90
+ - **New: `CaretMenu` and `useCaretTrigger`, mentions and slash commands on
91
+ any text field.** The hook watches a Textarea, Input or ChatComposer
92
+ (`fieldRef`) for a trigger character at the caret (`@`, `/` at the start,
93
+ `#`) and reports the query. CaretMenu shows suggestions there, driven by
94
+ ↑/↓, Enter, Tab and Escape while focus stays in the field. The app
95
+ supplies the items and what choosing one does: `trigger.replace("@Sam
96
+ Rivera ")` inserts a mention as text, and a command runs and `replace("")`
97
+ clears it. The value stays a plain string; chips inside the field need a
98
+ rich-text editor, which Sable doesn't ship.
99
+ - **ChatComposer: an Enter someone else handled doesn't send.** A listener
100
+ on `fieldRef` that handles Enter and calls `preventDefault()` owns it. It's
101
+ how CaretMenu picks a suggestion instead of sending, and how an app adds
102
+ key behaviour of its own.
103
+ - **ChatComposer keeps focus after Stop.** Stop turns back into send in
104
+ place, and with an empty field that send is disabled, which dropped focus
105
+ to the page. Focus now moves to the field, except on touch, where it would
106
+ raise the keyboard unasked.
107
+ - **Scroller eases the mouse wheel.** New `smoothWheel` (on by default): a
108
+ notch eases toward its target instead of jumping, on either axis, the
109
+ inertia a library like Lenis adds but scoped to the region. Only a mouse
110
+ wheel is eased: trackpads and touch keep their own momentum, keys and
111
+ scrollbar drags stay native, it releases at the ends so the page scrolls
112
+ on, a nested area that can still scroll (a code block, a textarea) keeps
113
+ the wheel, a region that smooths or snaps in CSS is left to the browser,
114
+ and anything else that moves the region (`scrollIntoView`, a scrollbar
115
+ drag) stops it at once. Reduced motion turns it off. New
116
+ `useSmoothWheel()` gives a scroll container of your own the same thing.
117
+ - **New guide: `sable docs building-a-chat`.** Wires Sable's chat parts to
118
+ any streaming source: which part owns what and what stays with the app, a
119
+ turn's lifecycle mapped onto `busy`, `onStop`, `streaming` and `disabled`,
120
+ where each failure goes, announcing a turn once rather than per token, and
121
+ rendering model output without injecting HTML.
122
+ - **New guide: `sable docs extending-a-component`.** What to do when a
123
+ component doesn't do what you need: compose with `render`, fill a slot,
124
+ hold the state and reach the element, add behaviour on top (mentions as
125
+ the worked example), change the look through tokens and presets rather
126
+ than overrides, patch a defect, and tell a library gap from app work.
127
+ - **The shadcn migration guide now maps every component.** Menubar,
128
+ NavigationMenu, InputGroup, ButtonGroup, link Pagination and drawer snap
129
+ points all have counterparts.
130
+ - **Fix: a horizontal Scroller no longer widens its container.** Its outer
131
+ box kept the default `min-width: auto`, so inside a grid or flex column a
132
+ strip of cards grew to its full length and never scrolled. It now shrinks
133
+ to the column and scrolls with its edge fade. The Scrolling page's own
134
+ example was showing exactly this, and also lacked `orientation="horizontal"`.
135
+ - **`useCopy` takes a function.** `useCopy(() => fetch(url).then((r) =>
136
+ r.text()))` produces the text only on click; a pending promise goes to the
137
+ clipboard as a ClipboardItem so Safari still counts it as part of the click.
138
+ New `CopySource` type.
139
+ - **New: `ButtonGroup`.** Buttons joined into one control, such as a split
140
+ button (an action beside the chevron that opens its menu) or a short run of
141
+ related actions. The children stay ordinary Buttons with their own variants
142
+ and sizes. The group squares their inner corners, collapses bordered seams
143
+ into one hairline and draws a seam between filled ones. Button now reads a
144
+ per-side radius (`--button-radius-s` / `--button-radius-e`, falling back to
145
+ `--button-radius`) and renders `data-variant`, which is how the group
146
+ does it without overriding a class.
147
+ - **Menu rows can carry an icon and a description.** `MenuItem` takes `icon`
148
+ and `description`, a quieter second line saying what the action does; with
149
+ a description the icon sits in a small tile spanning both lines.
150
+ - **New: `MenuLinkItem`.** A menu row that navigates: a real `<a>`, so it opens
151
+ in a new tab, middle-clicks and copies as a link. With `target="_blank"`
152
+ it adds `rel="noopener noreferrer"` and a trailing ↗.
153
+ - **Icon contract: `arrow-up-right`.** Added for links that leave the app.
154
+ An app with its own icon set should map it; Reicon's set already does.
155
+ - **sableui.com: a Copy page button on every page.** Each page with a
156
+ Markdown version has a split button in its header. Copy page copies the
157
+ page as Markdown, fetched only when you click. The menu behind the chevron
158
+ opens the Markdown file, starts a ChatGPT or Claude chat that reads the
159
+ page (Claude tries the desktop app first and falls back to claude.ai), and
160
+ copies the setup command (`pnpm add @sableui/react && pnpm exec
161
+ sable init`). The Markdown files are the same ones the site publishes at
162
+ `/<page>.md`; both now come from one table
163
+ (`apps/docs/src/markdown-twins.json`).
164
+ - **New: `Sidebar surface="page"`.** The rail paints the page's own surface
165
+ and ink, with no border, instead of the theme's `--sidebar-*` set. Use it
166
+ when a full-width header sits above the rail: a tinted or dark rail
167
+ (Verdant, Meridian) starting under a page-coloured bar reads as pasted on.
168
+ The default, `surface="rail"`, is unchanged and still right for a
169
+ full-height rail. Overriding `bg-background text-foreground` on a Sidebar
170
+ never worked, because the rail re-points those tokens at its own;
171
+ use the prop instead. sableui.com's docs rail now uses it.
172
+
173
+ ## 0.11.0
174
+
175
+ - **New: `PropertyGroup`, a collapsible group inside a PanelSection.** The
176
+ row names the group, summarises its value while closed (a word, a few
177
+ swatches, or both), and opens with +/− to the rows beneath. It's sized for
178
+ a tool panel, where an Accordion is page furniture. Groups open
179
+ independently and stack with a hairline between them.
180
+ - **sableui.com's Customize panel: the Color section is rebuilt on it.**
181
+ It now reads Brand color, then Primary action (with colour dots), then
182
+ three groups: Color palette (the families and Pull to brand), Tones (tones
183
+ and status) and Neutrals. Each group summarises itself with its own
184
+ swatches, and Tones and Neutrals are chosen from a list that previews what
185
+ each option would paint. The section has its own Reset.
186
+
3
187
  ## 0.10.0
4
188
 
5
189
  A palette generated from the theme, numbered extra tones, and sample content that isn't a marketplace.
package/DESIGN.md CHANGED
@@ -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