@sableui/react 0.0.0-stage → 0.1.1

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 (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +55 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. package/third-party/reicon-LICENSE.txt +20 -0
package/AGENTS.md ADDED
@@ -0,0 +1,157 @@
1
+ # AGENTS.md — @sableui/react
2
+
3
+ How to **build with** Sable. This is the whole consumer contract an agent needs; read it top to bottom once, then retrieve details through the CLI. Changing the package itself? Read [`CONTRIBUTING.md`](./CONTRIBUTING.md). Why the system is shaped this way: [`DESIGN.md`](./DESIGN.md).
4
+
5
+ ## What this is
6
+
7
+ **Sable** is an owned, themeable React component system: about 100 components (`npx sable manifest` has the live count), 13 layout primitives, 8 page shells, on [Base UI](https://base-ui.com) behavior with Tailwind v4 + semantic CSS-variable tokens. The default theme is **Base**; the other featured presets are Meridian, Prism, Verdant, Ember, and Poppy.
8
+
9
+ **Where things are**
10
+
11
+ | You need | Ask |
12
+ |---|---|
13
+ | Which component, and its props/examples | `npx sable search "<need>"` → `npx sable component <Name> --detail full` |
14
+ | A page composition to start from | `npx sable pattern --list` → `npx sable pattern <Name> --skeleton` |
15
+ | The rules for type, motion, spacing, shape, surfaces, a11y | `npx sable docs typography` (also `motion`, `spacing`, `shape`, `surfaces`, `accessibility`) |
16
+ | Which token to reach for | `npx sable token <name>` · `npx sable docs token-intent` |
17
+ | A task walkthrough | `npx sable docs --list --kind tasks` |
18
+ | Is my wiring right? Is my code on-contract? | `npx sable doctor` · `npx sable-lint` |
19
+
20
+ ## The rules for code you write
21
+
22
+ For an explicit iOS/iPhone app prototype, read `sable docs building-an-ios-prototype`
23
+ first. **Sable first: every screen is built from Sable components** — rows, inputs,
24
+ switches, sliders, search, buttons, forms, toasts, cards — exactly as on the web,
25
+ and every rule below applies. `@sableui/react/ios` supplies only the iOS system
26
+ chrome Sable has no equivalent for: `NativeApp`, `IosPage`, `IosNavbar`,
27
+ `IosBackLink`, `IosIconButton`, `IosTabbar`, `IosTab`, `IosSheet`,
28
+ `NavigationStack`. Never import from `konsta` (sable-lint `native-ui-import`);
29
+ install it only as the adapter's peer for an iOS prototype. A missing piece is a
30
+ library gap to report, not a reason for a native control.
31
+
32
+ These are short on purpose. Each links to a foundations doc with the full vocabulary, the reasoning, and before/after examples; `sable-lint` catches most violations.
33
+
34
+ 0. **Compose from Sable components; never rebuild one.** Before writing any interactive or recognisable element — a button, a field, a toggle, tabs, a menu, a badge, a card, a search box, a dialog — run `npx sable search "<what you need>"` and use what it returns. A raw `<button>`, `<input>` or `<div>` dressed in a component's classes is a reinvention, and it is a bug even when every class is a semantic token: it misses the component's states, density, focus and keyboard contract, and drifts the moment the component changes. A link that looks like a button is `<Button render={<a href=… />}>`. If nothing fits, compose from the nearest component's parts; if it genuinely does not exist, say so in your summary — that is a library gap to report, not a license to invent. The rules below are for styling what you compose *around* components, not for re-creating them. `sable-lint` flags the common cases (`hand-rolled-control`, `lookalike-control`).
35
+
36
+ **The same goes for everything below a component. Resolve in this order, and stop at the first that fits:** (1) a **component** → (2) a Sable **utility or recipe** — relative surfaces (`sable-surface*`, `sable-well`, `sable-surface-interactive`), state variants (`sable-selected:`, `sable-highlighted:`…), geometry (`rounded-concentric-*`, `popup-inset`, `rounded-popup-item`, `popup-bleed`), scroll policies, `motion-*` recipes → (3) a **semantic token** → (4) hand-written CSS, last, and reported as a gap. `npx sable search "<what you need>"` searches all of these; `registry.json → utilities` lists the utilities with intent and what each replaces. Mixing ink into a surface by hand (`color-mix(… var(--foreground) …)`) is the tell that you skipped step 2 — that is what the relative-surface utilities compute, in every theme and mode.
37
+
38
+ **Never restyle a component with override classes to get a look it lacks.** `className` on a Sable component is for layout (width, margin, flex and grid placement, position); its look — fill, edge, corner, ring, shadow, ink — comes from a variant. Use the one that exists, and if none fits, that is a library gap to report, not a `className`. The common cases: a control floating over content is `<Button variant="floating">`, one on a coloured band `variant="on-color"`; a quiet chip is `<Pill appearance="outline" size="sm">`; a status label is `<Badge appearance="plain" tone="…" dot>`; a destructive menu row is `<MenuItem variant="destructive">` (never `className="text-destructive"`); a list of `Entity` rows in a card is `<CardList>`, and a table that runs edge to edge in a card is `<CardContent flush>`, which insets the outer columns to the card's padding (never `pl-6`/`pr-6` on cells). `sable-lint` flags these (`component-restyle`, `destructive-item-ink`, `table-edge-padding`, `popup-separator-bleed`).
39
+ 1. **Semantic tokens only.** `bg-card`, `text-muted-foreground`, `border-border`, `bg-primary` — never a palette step, never inline hex/`oklch()`. Tone fills take their paired ink (`bg-tone-success` + `text-tone-success-foreground`). → `sable docs token-intent`
40
+ 2. **Type is a closed vocabulary.** One paired utility per text node (`text-heading-2`, `text-body-md`, `text-label-lg`, `text-mono-md`); never compose `text-* font-* tracking-*` by hand. Pick by **role**, not size. One `heading-1` per route. Two colors: `foreground` and `muted-foreground` — never opacity on text. → `sable docs typography`
41
+ 3. **Space on the 4px scale, by role.** Inset, stack, inline; related things closer than unrelated things. No off-scale `p-[15px]`. Padding, gaps and insets stay on the scale (a step, or `--spacing(n)` inside an arbitrary value) so the density control moves them; fixed `rem`/`px` only for what must not scale, like a hairline. Single-column content in a card is a flex column, not a grid. → `sable docs spacing`
42
+ 4. **Controls read the ladder.** Heights come from `--control-*`, so `<Density>` can retune a region at once. Never `h-[40px]`. → `sable docs spacing`
43
+ 5. **Shape and depth are roles.** Radii from the `rounded-*` ladder, concentric when nested (inner = outer − inset; `rounded-concentric-*`, `sable-frame-inner`); elevation by transience (resting → raised → overlay → modal → toast), one shadow per surface. → `sable docs shape` · `sable docs surfaces`
44
+ 6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. → `sable docs motion`
45
+ 7. **Icons by semantic name.** `<Icon name="check" />`; size with `text-{size}`, never type utilities. A mark attached to a line of type (dot, bullet, leading icon) sits in an `h-[1lh] items-center` box and is sized in em, never nudged with px. → `sable docs typography`
46
+ 8. **Every scroll container declares a policy:** `scrollbar-quiet` (vertical) or `scrollbar-hidden` (horizontal, with an edge fade carrying the affordance).
47
+ 9. **Accessible by construction.** WCAG AA contrast, 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
48
+
49
+ ## Customizing: themes are presets
50
+
51
+ 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`.
52
+
53
+ ## Wire it up
54
+
55
+ The machine-readable version of this section is the `consume` block in `registry.json`.
56
+
57
+ 1. **Install** from npm: `pnpm add @sableui/react` (or the project's package manager). No registry configuration or token is needed.
58
+ 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.
59
+ 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.
60
+
61
+ `styles.css` carries **Base only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as Base — no error, just the wrong theme.
62
+ 4. **Fonts:** load Figtree (sans) + DM Mono (mono) — the theme declares the stacks, the host provides the files. Apply `antialiased` (`-webkit-font-smoothing: antialiased` / `-moz-osx-font-smoothing: grayscale`) **once on the app root**: Sable's type scale was drawn against smoothed text, and without it macOS renders every weight heavier than designed. The package deliberately doesn't set it — a library has no business styling the host's whole page — so it has to happen here.
63
+ 5. **Providers (all optional):** components render with **Reicon** by default, so `<IconProvider set={…}>` is only needed to replace it with another icon package or extend it using `composeIconSets(appExtras, reiconIconSet)`; `<MotionProvider>` is optional too. Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
64
+ 6. Import from the root barrel: `import { Button } from "@sableui/react"`. Page shells live on the `patterns` subpath (`import { SettingsPage } from "@sableui/react/patterns"`) — preview one, then copy its source from `registry.json → patterns` when the route needs to diverge.
65
+
66
+ 7. **Check the wiring:** `npx sable doctor`. Every check it runs corresponds to a way Sable fails *silently* — a second React (blank page, "invalid hook call"), a missing `@source` (Tailwind emits none of the package's classes, everything renders unstyled), or `data-sable-theme` set with no theme CSS imported (every theme renders as Base and the switcher looks broken). Exits 1 on any failure; `--json` for the structured report.
67
+
68
+ 8. **Read the catalog through the CLI, not the file.** `registry.json` is about 1 MB and `llms-full.txt` about 800 KB — complete and unreadable in a working context. `npx sable` serves the same generated contract in pieces, offline, at your installed version:
69
+
70
+ ```bash
71
+ npx sable pattern --list # the eight page shells
72
+ npx sable pattern SettingsPage --skeleton # composition tree, fixtures stripped
73
+ npx sable component DataTable --props
74
+ npx sable search "empty state" # across components, patterns, layout, tokens, docs
75
+ npx sable docs converting-an-app # the task walkthroughs
76
+ npx sable manifest --json # the whole command surface, machine-readable
77
+ ```
78
+
79
+ `--json` wraps output as `{ type, data }` with a stable discriminator; `--detail brief|compact|full` picks a size; `--dense` drops prose. Errors carry a stable `code` and `suggestions` (`ERR_UNKNOWN_COMPONENT` for `Buton` suggests `Button`), so a wrong name costs one call, not a guess.
80
+
81
+ **Choose composition before components.** `--skeleton` prints a pattern's element tree with demo data removed, making comparison cheap. Reuse a shell when its information architecture fits. Compose an original surface from layout primitives when the brief needs a different structure; a shell resemblance score is not a quality score.
82
+
83
+ 9. **Audit the app against the contract:** `npx sable-lint` (lints `./src`; `--rules` lists the rules, `--ignore <glob>` skips paths). It reads the token vocabulary from the **installed** package's `registry.json`, so upgrading Sable moves the linter with it. Exits 1 on any violation — put it in the build script. Scope: a lexical pass over class strings, catching the drift Tailwind silently no-ops and `tsc` cannot see. It is not a type checker, and it does **not** know whether your code still matches a pattern you copied (see "Promoting from an app").
84
+
85
+ 10. **Build with a brief and a learning loop.** `npx sable docs designing-a-surface` covers purpose, story, visual direction, interaction, and adaptation. `npx sable learn --search "editorial hierarchy"` retrieves conditional precedents from the installed package and project-owned `.sable/lessons/`. `npx sable grade --template` supplies an evidence review record; `npx sable docs reviewing-a-design` explains the rendered checks. Record a useful discovery with `npx sable learn --template` and `--record <file.json>`. Local records survive package upgrades and remain provisional observations. Neither source conformance nor a complete evidence record independently proves taste.
86
+
87
+ For a substantial new visual direction, read `sable docs developing-taste`. If the project keeps a
88
+ visual atlas (`.sable/atlas/`), use `sable atlas --search "<context>" --contrast
89
+ --limit 3 --json`, then `--show <id>`, and actually view the returned capture paths. When meaningful alternatives need comparison, use bounded rendered
90
+ directions with `sable explore`. Keep the factual brief and primary flow fixed.
91
+ Retrieve `sable prefer --search "<decision>" --scope project:<id> --json` only in
92
+ the applicable context. Record an actual user's judgment as human; agent choices
93
+ remain agent observations. These tools improve context and process, not model
94
+ weights, and no structural validator can certify taste.
95
+
96
+ Full walkthrough: the package `README.md`.
97
+
98
+ ## Copy
99
+
100
+ Sable ships a lot of canonical copy — pattern fixtures, meta examples, docs samples — and agents copy it verbatim into real products. That makes interface copy part of the contract, not decoration. These rules apply to every user-facing string the package ships.
101
+
102
+ - **Sentence case everywhere** — buttons, headings, labels, menu items. Not title case. It is calmer, has no per-word case rules to get wrong, and localizes cleanly. "Save Changes" sitting beside "Discard changes" reads as sloppiness, and mixed casing is how that happens.
103
+ - **Buttons name the verb, not the answer.** "Delete this project?" offers `Delete project` and `Cancel` — never `Yes`/`No`. A confirmation repeats the consequence so the button still makes sense to someone who skipped the sentence. (`DestructiveActionModal` already defaults this way; it is the pattern, not an exception.)
104
+ - **Toggles describe the ON state.** "Send read receipts", never "Don't send read receipts" — a negated label plus an off switch is a double negative the reader has to solve.
105
+ - **Links name their destination.** Never "Click here". Never five bare "Learn more" links on one page: suffix each — "Learn more about exports". This is also an accessibility rule, because screen readers can list links out of context.
106
+ - **Errors say how to fix it, next to what broke.** "Use only letters for your name", not "Invalid name". "Unable to save. Check your connection and try again.", not "Oops! Something went wrong." No blame, no "oops", no exclamation marks, and phrase the hint as the thing to do rather than the thing that failed.
107
+ - **Never concatenate fragments around a variable.** `"You have " + n + " messages"` cannot be translated — word order and pluralization differ per language. Ship one templated string per plural form.
108
+ - **One vocabulary per flow.** Pick "Continue" or "Next" and use it for every step; two words for one action reads as two different actions.
109
+ - **Tone flexes with stakes.** Warm for success and onboarding; neutral for routine work; calm, plain and entirely unplayful for errors and destructive confirmations; serious for anything touching data loss, billing, or security. A joke in a delete dialog is a bug.
110
+
111
+ ## Conventions
112
+
113
+ - **`cn()`** is the single class merger; it knows the paired typography group, so `cn("text-heading-1", "text-body-md")` dedupes correctly.
114
+ - **Dark by default.** Build dark first; light is the counterpart.
115
+ - **Accessibility floor:** WCAG AA. Base UI provides behavior; don't strip its ARIA/roles.
116
+ - **Never** introduce a new external font or a hard icon dep. Never write `oklch()`/hex inline.
117
+ - **Logical properties for direction-dependent position.** `ms-*`/`me-*`/`ps-*`/`pe-*`/`text-start`/`inset-inline-*`, not `ml-*`/`pr-*`/`text-left`/`left-*`. Flex and grid mirror for free under `dir="rtl"`; hand-positioned elements do not. Reserve physical properties for genuinely physical geometry — `SafeArea`'s notch edges are physical and stay that way. `sable-lint` flags it.
118
+ - **Text containers size to the text, not to English.** No fixed width sized to an English label and no fixed height on a text container — `max-width` plus wrapping, and `min-height` as a floor. Buttons take their width from `padding-inline`, never a hardcoded `w-[…]`. German and Finnish routinely run 30–50% longer than English, and a translated string that doesn't fit is a layout bug shipped to everyone but the language it was designed in.
119
+ - **A decorative layer over interactive content takes `pointer-events-none` and `aria-hidden`.** Scrims, glows, gradient films and edge fades paint over live controls; without it the control looks live and does nothing, which is invisible in review and obvious to a user. A scrim the user *clicks to dismiss* is a control, not decoration, and keeps its events.
120
+ - **Text inputs never render below 16px on mobile.** iOS Safari zooms the page on focus under that threshold, which yanks the viewport mid-form. The field components handle this themselves; a consumer building a custom field must too.
121
+ - **Prefer the CSS property over the raw font axis tag.** `font-weight`, `font-variant-numeric: tabular-nums`, `font-variant-numeric: slashed-zero` — never `font-variation-settings: "wght"` or `font-feature-settings: "tnum"`. Raw tags silently do nothing on a fallback font, and every Sable theme declares a fallback stack.
122
+ - **Pair `text-wrap` to the job.** `balance` for headings (short, few lines), `pretty` for ledes and descriptions (kills the orphan), neither for long-form prose — browsers stop balancing past a few lines anyway. `text-body-lg` carries `pretty` already.
123
+
124
+ ### The form submit contract
125
+
126
+ Base UI wires `aria-invalid`/`aria-describedby` when you use `Field`; the behavior around it is ours and is easy to get wrong:
127
+
128
+ - Keep submit **enabled** until the request actually starts. A disabled submit button hides *why* it's disabled; validate on submit and say what's wrong instead.
129
+ - On submit failure, move focus to the first invalid field. A user who can't see the page has no other way to find it.
130
+ - While the request is in flight, disable and show the spinner but **keep the label** — a button that becomes a bare spinner loses what it was doing.
131
+ - Never block paste, on any field. Blocking paste on a password or code field breaks password managers and helps nobody.
132
+ - Get the keyboard right: `autocomplete="one-time-code"` for OTP, `new-password` vs `current-password` (never both as `password`), `type="text" inputmode="numeric"` for codes and card numbers (never `type="number"`, whose spinners and scroll-to-change are wrong for an identifier), and `spellcheck="false"` on emails and codes.
133
+
134
+ ### Announcing and focusing
135
+
136
+ - **A live region must exist before it has anything to say.** Render `role="status"` empty from the start and swap its text; a polite region inserted *together with* its content is announced inconsistently or not at all. In order of preference: move focus > `aria-describedby` on the control > `role="status"` > `role="alert"` (which interrupts, so reserve it).
137
+ - **Client-side navigation is a11y-invisible unless you make it visible.** On route change, update `document.title` most-specific-first, move focus to the new view's `h1[tabindex="-1"]` (or `#main-content`), and restore scroll on back/forward. Base UI can't own this and `AppShell` only supplies the skip link and landmark — the app has to do the rest.
138
+ - **Verify focus rings and disabled states under `forced-colors: active`.** Windows High Contrast overrides `ring-ring` and erases opacity-based disabled styling, which is exactly how Sable expresses both. Never reach for `forced-color-adjust: none` to "fix" it.
139
+
140
+ ## The Sable agent skill
141
+
142
+ After installing Sable, run `pnpm exec sable setup --agent` in the target
143
+ project. It installs discoverable entry files in `.agents/skills/sable` for
144
+ Codex/Cursor and `.claude/skills/sable` for Claude Code; `--target` selects a host.
145
+ Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
146
+ upgrades also update the workflow. Other agents can read that file directly.
147
+ Use `$sable build <idea>`, `inspire <references>`, `audit <target>`, or
148
+ `polish <target>` in Codex; use `/sable` with the same modes in Claude Code or
149
+ Cursor. `create` is a build alias. These are agent modes, not shell commands.
150
+
151
+ Upgrade with `pnpm update @sableui/react --latest`, then
152
+ `pnpm exec sable setup --agent --update`. Setup only replaces managed files
153
+ that have not been locally edited; `--dry-run` checks without writing. Keep
154
+ project preferences in project instructions. Package installation alone does not
155
+ change agent settings. See [building-with-sable.md](./llms/tasks/building-with-sable.md),
156
+ [creating-a-preset.md](./llms/tasks/creating-a-preset.md), and
157
+ [using-presets.md](./llms/tasks/using-presets.md).
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # What's new
2
+
3
+ ## 0.1.1
4
+
5
+ - Published with npm trusted publishing: releases come from `sableui/sable`'s
6
+ release workflow with provenance and no stored token. No library changes.
7
+
8
+ ## 0.1.0 — first public release
9
+
10
+ Sable is on npm as `@sableui/react`: `pnpm add @sableui/react`. No registry setup
11
+ or token is needed. Follow the [installation guide](/getting-started) to wire
12
+ React, Tailwind CSS 4 and the stylesheet.
13
+
14
+ ### Components and layout
15
+
16
+ - About 100 components on Base UI behavior: forms, selection, menus, dialogs and
17
+ sheets, data display, tables, navigation, feedback, messaging, media and
18
+ workspace tools. Every example on the site is built from Sable's own components.
19
+ - 13 layout primitives and 8 page patterns (auth, dashboard, feed, settings,
20
+ list–detail, landing, brand landing, chat).
21
+ - Semantic tokens for color, type, spacing, shape, surfaces and motion; spacing
22
+ follows the density control, and motion honors reduced-motion settings.
23
+ - `styles.css` paints the page ground on `<html>`, so dark mode is readable from
24
+ the first render.
25
+
26
+ ### Six presets
27
+
28
+ - **Base:** neutral surfaces, soft corners, and Figtree typography.
29
+ - **Meridian:** cream and ink, square panels and buttons, and flat surfaces.
30
+ - **Prism:** compact spacing, IBM Plex typography, crisp corners, and blue actions.
31
+ - **Verdant:** forest tones, warm paper, and serif headings.
32
+ - **Ember:** warm surfaces, light headings, and orange-red actions.
33
+ - **Poppy:** bold headings, cream and plum, rounded panels, and offset shadows.
34
+
35
+ Every preset supports light and dark modes. Try them on the homepage or tune them
36
+ in the canvas, then download a portable preset or copy its CSS.
37
+
38
+ ### Build with your agent
39
+
40
+ - One package-owned skill for Codex, Claude Code, Cursor and compatible agents:
41
+ build an idea, develop a preset from inspiration, audit existing work, and
42
+ polish the result. Run `pnpm exec sable setup --agent` to install it.
43
+ - The `sable` CLI serves the catalog in pieces (components, patterns, tokens,
44
+ utilities, guides), checks a project's wiring with `sable doctor`, and lints
45
+ with `sable lint`, which catches restyled components, hand-built lookalikes,
46
+ raw colors and fixed spacing.
47
+ - Presets are portable files the package validates and turns into scoped CSS.
48
+
49
+ ### iOS prototypes
50
+
51
+ - Optional `@sableui/react/ios` for clickable iPhone prototypes: screens are Sable
52
+ components in your preset, inside iOS chrome (status bar, navigation bar, tab
53
+ bar, sheets) drawn by Konsta UI, an optional peer installed only for iOS.
54
+ - `sable target ios` gives each component's decision; `sable-lint` flags direct
55
+ Konsta imports. `@sableui/react/ios/preview` frames a prototype at iPhone size.
@@ -0,0 +1,221 @@
1
+ # CONTRIBUTING.md — @sableui/react
2
+
3
+ Rules for changing **this package**. Building an app *with* Sable? Read [`AGENTS.md`](./AGENTS.md) instead — it is the consumer contract and is all an agent building with Sable needs. Design rationale lives in [`DESIGN.md`](./DESIGN.md); the typed foundations rules (typography, motion, spacing, shape, surfaces, accessibility) live in `src/foundations/` and are served as `sable docs <topic>`.
4
+
5
+ The checks are the specification. Every rule below names the gate that enforces it; when prose and a gate disagree, the gate wins and the prose is the bug.
6
+
7
+ ## The six hard rules
8
+
9
+ > **These are enforced, not honor-system.** `pnpm design-lint` (`scripts/design-lint.mjs`, also a CI gate) fails the build on the drift below. A genuine rare exception needs an *auditable* comment that **requires** a reason: `// design-lint-disable-next-line <ruleId> -- <why>` (also `-line`, and block `disable`/`enable`). A disable with no reason is itself an error. Full table: `DESIGN.md → Enforcement`.
10
+
11
+ 1. **Components reference semantic tokens only.** Use `bg-card`, `text-muted-foreground`, `bg-tone-edition`, `text-primary`, etc. — never raw palette steps (`--purple-heart-700`) and never inline `oklch()`/hex. For text/icons on a tone fill use the theme-adaptive `text-tone-*-foreground` ink (no `dark:` variant); for scrims, media letterbox, and pills on imagery use `bg-overlay`/`text-overlay-foreground`. Palette names are theme-internal; a component that names a palette breaks themeability — and `design-lint`'s `palette-ref` rule now has **zero** sanctioned exceptions.
12
+ 2. **Paired typography utilities, never decomposed.** Use `text-heading-1`, `text-body-md`, `text-label-lg`, … Each bundles size + weight + line-height + tracking. Do **not** combine `text-{size}` + `font-{weight}` + `tracking-{x}` by hand. (Icons are the exception — size them with `text-{size}` scales, never type utilities.)
13
+ 3. **Base UI is behavior; `components/` is the public API.** Each component wraps `@base-ui/react` directly inside its own folder (use the **`render` prop** for composition; read state from `data-[state=…]` attributes) and owns 100% of the styling. There is no separate primitives tier. Consumers import only from the package root barrel.
14
+ 4. **Semantic icons in components.** Use `<Icon name="…">`; Reicon is the default. Concrete icon-package imports belong only in `src/icon-sets/`. Apps may override or extend the set through `<IconProvider>`.
15
+ 5. **Every scroll container declares a scrollbar policy.** An `overflow-*-auto|scroll` utility must be paired with `scrollbar-quiet` (a 4px thumb, invisible at rest, fading in while the region scrolls) or `scrollbar-hidden` (no bar — only when something else carries the affordance, and you should be able to name it). Left bare, the platform draws its own: on Windows that means stepper arrows hard against the padding edge, poking out of any rounded corner. Vertical scrolling takes a bar; **horizontal takes none** — an edge fade says "there is more this way" better than a rail does, which is why `Scroller` splits by axis. New scroll container? Reach for `useScrollActivity` so the bar appears on touch drags and programmatic scrolls, which hover cannot see. Enforced by `design-lint`'s `unmanaged-overflow` **and** by `build-registry` over every registry example. Full rationale: `DESIGN.md → Scrolling`.
16
+ 6. **Tailwind v4 packaging discipline.** `src/styles.css` must **not** `@import "tailwindcss"` (the consumer owns that). It carries only token blocks, `@theme inline` mappings, `@utility` typography, layout vars, and `@source`. `@source` points at `src` (never the gitignored `dist`).
17
+
18
+ **Variants, not overrides — and spacing on the scale.** A component's look comes from its variants, never from override classes a caller (or another component) passes in, and its own padding moves with density. Gated by:
19
+
20
+ - `component-restyle` — `border*` / `bg-*` / `rounded*` / `ring-*` / `shadow-*` in a `Button`, `Pill` or `Badge` `className`. Use or add a variant (Button `floating` for a control over content). Layout classes are fine.
21
+ - `destructive-item-ink` — `text-destructive` (or a tone ink) on `MenuItem` / `ContextMenuItem`. Use `variant="destructive"`.
22
+ - `table-edge-padding` — `pl-*` / `pr-*` / `ps-*` / `pe-*` on `TableHead` / `TableCell`. Use `CardContent flush` (it sets `--table-edge-px`).
23
+ - `popup-separator-bleed` — a fixed `-mx-*` on a popup separator. Use `popup-bleed`.
24
+ - `fixed-spacing` (package only) — a `rem`/`px` padding, margin, gap or inset (`p-[1.5rem]`, `padding: "12px"`, `"--card-p": "1.5rem"`). Write a step or `--spacing(n)`; hairlines, em values and positional offsets are exempt.
25
+
26
+ ## File structure
27
+
28
+ ```
29
+ src/
30
+ index.ts # public barrel — the only consumer entry
31
+ styles.css # CSS entry: theme import + @theme map + @utility typography + layout vars + @source
32
+ lib/cn.ts # cn() = clsx + tailwind-merge (with ds-typography paired-utility group)
33
+ themes/
34
+ _contract.ts # SemanticTokens — the token names every theme MUST define
35
+ registry.ts # public theme manifests, ids, and attribute helpers
36
+ theme-provider.tsx # SableThemeProvider + useSableTheme for app theme switching
37
+ <id>/ # one folder per theme: <id>.css (:root/.dark vars) + index.ts (TS manifest) + art-direction.md
38
+ foundations/ # the typed rules (typography, motion, spacing, shape, surfaces, a11y) → docs pages + llms/foundations/*.md
39
+ layout/ # fluid grid/layout primitives: Region, Grid, Columns, Stack, Row, Spacer, Center, AspectBox, Bleed, SplitPane, Page, SafeArea, GridOverlay
40
+ icon/ # Icon, IconProvider, IconRegistry — the semantic adapter and provider overrides
41
+ motion/ # owned zero-dep motion layer: MotionProvider, useMotion, useReducedMotion, Presence, Reveal, TextReveal, useInView, View Transitions
42
+ components/<name>/ # <name>.tsx + <name>.variants.ts + <name>.meta.ts + index.ts
43
+ patterns/<name>.tsx # canonical page shells (+ <name>.meta.ts) — see "Patterns"
44
+ ```
45
+
46
+ ## How to add a component (the recipe)
47
+
48
+ The maintained agent workflow is `skills/sable/SKILL.md` in this package. Update
49
+ its task guides under `llms/tasks/` alongside library changes. Repository and
50
+ consumer agent directories contain entry files that resolve this canonical
51
+ source; do not fork the workflow into host-specific skills. `skills/entry/SKILL.md`
52
+ is the portable loader template; `scripts/agent-setup.mjs` installs and manages it.
53
+ Verify setup/update/local-edit protection and the packed consumer when changing
54
+ the installer. User-defined presets belong to apps, not the bundled theme registry.
55
+
56
+ 1. If it needs non-trivial behavior (overlay, focus trap, keyboard nav), wrap the Base UI part inside the component folder — never re-implement focus management Base UI already owns.
57
+ 2. Create `components/<name>/<name>.variants.ts` — a `cva()` over **semantic tokens**.
58
+ 3. Create `components/<name>/<name>.tsx` — the themed component composing the primitive + variants + `cn()`.
59
+ 4. Create `components/<name>/<name>.meta.ts` — machine-readable metadata (see "Component meta").
60
+ 5. `components/<name>/index.ts` re-exports; add to the root `src/index.ts` barrel.
61
+ 6. File it in `src/lib/taxonomy.ts` and add a docs page at `apps/docs/src/pages/components/<Name>Page.tsx` with live examples (the build fails without both).
62
+ 7. Gate: `pnpm design-lint` clean, `pnpm registry:check` in sync (regenerate with `pnpm build:registry`), package `tsc` clean (`pnpm --filter @sableui/react typecheck`), `pnpm test` green, and the docs app renders it in light + dark. (Tooling runs on **Node 22** — the registry generator imports the `.meta.ts` files via Node's built-in type-stripping.)
63
+
64
+ ## Registers & density
65
+
66
+ The optional `src/ios/` adapter is outside the core component catalog. It wraps
67
+ Konsta for iOS system chrome only (app frame, navigation bar, tab bar, sheet
68
+ presentation); it must never export a control Sable already has. Keep Konsta external and optional;
69
+ neither the root barrel nor the `ios/preview` entry may import it. Chrome reads
70
+ the preset through `ios/styles.css`; keep that mapping, and the geometry pins on
71
+ Konsta chrome, in CSS rather than per-app.
72
+ Native geometry follows the iOS target contract rather than web density. Both
73
+ docs examples must consume the published adapter instead of maintaining local
74
+ copies. Verify the packed web consumer without Konsta, then the opt-in native
75
+ consumer with it, alongside the two interactive demos.
76
+
77
+ Sable has **two registers** — the page register (default) and the workspace register for
78
+ tool chrome. Full doctrine, the measured numbers, and the two rules the workspace register
79
+ deliberately reverses: `DESIGN.md → Registers`. What a contributor needs to know:
80
+
81
+ 1. **Never hardcode a control height.** Size variants read the ladder —
82
+ `h-(--control-md)`, `h-(--control-xs)` — never `h-[40px]`. A `data-density` scope
83
+ redefines the ladder, so one attribute retunes every control at once and nothing has to
84
+ override a height after the fact. CI-enforced (`raw-control-height`); the rule only
85
+ flags the ladder's own values, so a genuine non-control measure (a gauge diameter, the
86
+ WCAG touch floor) is a real exception — disable it with a reason.
87
+ 2. **`--control-*` values are set in `styles.css`, not per theme.** Density is a register
88
+ concern, not a palette one. A theme *may* override them (same status as
89
+ `MOTION_TOKENS`/`RADIUS_TOKENS`) but doesn't have to.
90
+ 3. **Workspace surfaces are derived.** `canvas` / `panel` / `panel-raised` / `hairline`
91
+ come from each theme's existing semantic tokens via `color-mix`, with a `--theme-*`
92
+ override hook. Don't hand-author them per theme.
93
+ 4. **Paired type tiers must be registered in `lib/cn.ts`.** Adding a `@utility text-foo`
94
+ without adding `"foo"` to `dsTypographyTokens` means tailwind-merge treats it as a
95
+ **colour** utility and silently drops the real colour class — a shipped bug once (white
96
+ text on a white button) and invisible in review. `themes.test.ts` asserts every
97
+ `text-ui-*` tier is registered.
98
+ 5. **Tool chrome components go in the workspace family**, not into `AppShell`'s: `Workspace`,
99
+ `PanelGroup`, `PanelSection`, `ScrubField`, `StatusBar`, `Tree`. `SplitPane` stays
100
+ page-register (responsive, not resizable) — the two are deliberately separate. `Tree`
101
+ is the one hierarchy, for a layers panel and a file listing alike.
102
+ 6. **Never compute a tree inset.** Rows, section headers, add affordances and empty states
103
+ all read `treeIndent(depth)` or `var(--tree-rail)`. And `--tree-indent` must always equal
104
+ `--tree-column`: a smaller step puts a child's content left of its parent's and the
105
+ nesting reads inverted. Asserted in `themes.test.ts` for both densities.
106
+ 7. **A new component must be filed in `lib/taxonomy.ts` and have a docs page.** The
107
+ taxonomy declares a **group** (role) and a **register** (`page` / `workspace` /
108
+ `either`), and `build-registry` fails on a component missing from it, a stale entry
109
+ naming a component that no longer exists, or a component with no
110
+ `apps/docs/src/pages/components/<Name>Page.tsx`. The grouping used to live only in the
111
+ docs nav array, which is exactly how `PanelGroup`, `PanelSection`, `ScrubField` and
112
+ `StatusBar` shipped in 0.31.0 unfiled and undocumented and stayed that way for four
113
+ releases. The docs nav is now generated from the contract, so the two cannot drift.
114
+ **Group by KIND, not by co-occurrence** — see the note in `taxonomy.ts`: the
115
+ `related.components` graph tracks composition as much as kinship, and grouping by it
116
+ measurably made things worse when tried.
117
+
118
+ ## Theme contract
119
+
120
+ A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/base/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
121
+
122
+ **Themes are presets, not the unit of customization.** Every token a theme sets can also be set by an app (or by the docs customizer) on `:root` after the theme loads. A theme is the starting point a consumer picks; the customizer exports the deltas on top of it. Keep that working: a theme must not set a token with `!important` or under a selector more specific than `[data-sable-theme="<id>"]`, or a consumer override cannot win.
123
+
124
+ `_contract.ts` exports five token groups: `SEMANTIC_TOKENS` (colors — **light + dark**, e.g. `--background`, `--primary`, `--tone-edition`), `MOTION_TOKENS` (duration/easing ladder), `RADIUS_TOKENS` (the 7 `--theme-radius-*` steps bridged to Tailwind's `radius-*` scale), `TYPE_SCALE_TOKENS` (display/heading/title size, weight, leading, and tracking vars consumed by the paired `text-*` typography utilities in styles.css), and `TYPE_OVERLINE_TOKENS` (the small, uppercase, letter-spaced eyebrow/overline/kicker tier — size/weight/leading/tracking/transform — consumed by the `text-overline` utility). `MOTION_TOKENS`, `RADIUS_TOKENS`, `TYPE_SCALE_TOKENS`, and `TYPE_OVERLINE_TOKENS` are **mode-independent** — declared once per theme (not duplicated under `.dark`), unlike `SEMANTIC_TOKENS`.
125
+
126
+ `TYPE_REFINEMENT_TOKENS` additionally exposes optional typography hooks for body,
127
+ label, caption, mono, title sizes, and the largest display size. Retrieve
128
+ `sable token typographyOverrides` for their intent. These preserve paired utilities
129
+ while allowing a consumer to tune the whole type system, rather than making only
130
+ headings reflect its art direction. Existing defaults remain the fallback; themes
131
+ do not need to declare every optional hook.
132
+
133
+ `SEMANTIC_TOKENS` includes `border-strong` alongside `border` — a visually-stronger structural divider (grid lines, table rules, section separators) distinct from `border`, which stays tuned for subtle chrome (badge/input outlines). Both need light + dark values, derived from the same neutral ramp as `border` but pulled from a higher-contrast step (e.g. Base: `border` = `--zinc-200` light / low-alpha `--zinc-700` dark → `border-strong` = `--zinc-300` light / higher-alpha `--zinc-500` dark). `styles.css`'s `@theme inline` block maps it to `--color-border-strong`, so components/consumers use it via the `border-strong` Tailwind color utility, same pattern as `border`.
134
+
135
+ `TYPE_OVERLINE_TOKENS` (`type-overline-size/weight/leading/tracking/transform`) back the `text-overline` utility — the eyebrow/badge-label/stat-caption text role with no home in `TYPE_SCALE_TOKENS`'s display/heading/title ramp. `type-overline-transform` is themeable rather than a hardcoded `uppercase`, so a quieter reference (e.g. Cove) can opt into sentence case. Like the other type tiers, every built-in theme declares explicit values rather than relying on styles.css's fallback defaults (`0.75rem` / weight `600` / leading `1.2` / tracking `0.08em` / `uppercase`).
136
+
137
+ The manifest's `fonts` field is `{ sans: string; mono: string; display?: string }` — `display` is **optional**, for a theme whose reference uses a distinct display/headline typeface separate from its body sans (e.g. a Chalet-style display face over an Instrument Sans body). When a theme sets `display`, its CSS also defines `--font-display` (alongside `--font-sans`/`--font-mono`) in the same `:root, [data-sable-theme="…"]` block. styles.css's `text-display-*` and `text-heading-*` utilities read `font-family: var(--font-display, var(--font-sans))` — a theme that omits `display`/`--font-display` falls through to `--font-sans` and renders identically to before this field existed. `text-title-*`/body-register utilities are unaffected and keep inheriting `--font-sans` only.
138
+
139
+ ## Icon contract
140
+
141
+ Components reference semantic names through `<Icon name="…" />`. Reicon (MIT)
142
+ is the default, installed as a runtime dependency and kept external to the build.
143
+ Concrete icon imports belong only in `src/icon-sets/`; components never import
144
+ an icon package or duplicate its SVG artwork.
145
+
146
+ - **Use the default:** render components without an icon provider.
147
+ - **Replace the package:** pass your own `IconSet` to `<IconProvider set={mySet}>`.
148
+ Map semantic names to that package's components; preserve className, variant,
149
+ spin and accessible labels. Return `null` for unknown names.
150
+ - **Extend Reicon:** import `composeIconSets` and `reiconIconSet` from Sable and
151
+ use `<IconProvider set={composeIconSets(appExtras, reiconIconSet)}>`.
152
+ The first set returning a non-null node wins.
153
+ - `@sableui/react/icons/reicon` remains an alternative import for the same adapter.
154
+ `fallbackIconSet` is a deprecated alias of Reicon, with no separate artwork.
155
+ - Brand marks are a separate additive set at `@sableui/react/icons/brands`.
156
+ The 23 marks are generated from licensed `reicon-brands` npm artwork by
157
+ `pnpm icons:brands`, normalized to the 24-unit grid, and inherit currentColor.
158
+ Their MIT notice is shipped; trademark usage remains subject to each brand's rules.
159
+ - Coverage tests enforce that Reicon maps every contract name and no others.
160
+ All interface glyphs, including tree controls and the color eyedropper, use Icon.
161
+ Progress meters and animated loading indicators remain component visuals.
162
+
163
+ ## Motion
164
+
165
+ Motion is a first-class layer — **subtle by default, tasteful spring welcome, zero runtime dependency** (CSS tokens/recipes + tiny browser-native JS). When a component has any interaction, wire it to the system; don't leave it static or hand-roll a transition.
166
+
167
+ **Four slots.** Every component answers all four — by filling the slot, or by declining it in `meta.motion` with `recipe: "none"` and a reason:
168
+
169
+ | slot | the question | typical fill |
170
+ |---|---|---|
171
+ | `arrival` | how does it enter? | `motion-fade` · `motion-slide-*` · `motion-pop` · `Reveal` |
172
+ | `response` | what happens under the pointer? | `motion-interactive` · `motion-press` |
173
+ | `transition` | how does it move between its own states? | `motion-disclosure`, or ease the transform |
174
+ | `activity` | how do you tell "working" from "frozen"? | `motion-spin` · `motion-pulse` · `motion-shimmer` |
175
+
176
+ This paragraph said "don't leave it static" for many releases while **nothing checked it**. The four motion rules below are all prohibitive — they fire on motion that is present and wrong, and none can fire on motion that is absent — so a component that snapped between every state passed the whole contract, and motion stayed a thing a human had to request by name. Two gates now close that: `static-interactive` (lint, fires on an element styled for pointer states with no recipe on it) and `meta.motion` + `pnpm build:registry -- --motion-report` (declaration and coverage).
177
+
178
+ 1. **Reach for a recipe first** — apply one named class instead of a bespoke transition string: `motion-interactive` (default hover/focus/press chrome — **replaces** `transition-colors`), `motion-press` (tactile press; always pair with `motion-interactive`), `motion-fade`/`motion-pop`/`motion-slide-{up,down,left,right}` (overlay enter/exit, on Base UI `data-[starting-style]`/`data-[ending-style]`), `motion-disclosure` (height), and loaders `motion-{spin,shimmer,pulse,dot-bounce,ping}`.
179
+ 2. **Drop to tokens only when a recipe doesn't fit.** Durations via `duration-(--motion-duration-{instant|fast|base|slow|slower})` — **never** a raw `duration-150` (it bypasses the kill-switch). Easings via `ease-{standard|decelerate|emphasized|spring|bounce}`. Spring/bounce overshoot is **enter-only**; exits stay clean.
180
+ 3. **Never** use Tailwind's built-in `animate-spin`/`animate-pulse`/`animate-ping`/`animate-bounce` — they ignore `--motion-scale`, so the on/off policy can't freeze them. Use the scale-aware `motion-*` loader recipes.
181
+ 4. **Compositor-first & subtle:** animate opacity + transform (scale/translate) only; short durations; no exit overshoot; no gaudy motion.
182
+ 5. **Policy:** all token-driven motion collapses to static under `prefers-reduced-motion` or `<MotionProvider>` off (one `--motion-scale: 0`). JS primitives read `useMotion().enabled` and must no-op when motion is off.
183
+ 6. **JS motion lives in `src/motion/`** (zero-dep, browser-native: WAAPI / IntersectionObserver / View Transitions). Do **not** add the `motion` library to core — drag/gesture, FLIP layout, and velocity springs are the future optional `@sableui/motion` adapter (peer-dep `motion`).
184
+
185
+ Full token table + rationale: `DESIGN.md → Motion`. Live demo: docs → Foundations → Motion (also the on/off toggle).
186
+
187
+ ## Patterns (canonical page shells)
188
+
189
+ `src/patterns/` holds full-page compositions for common routes. They ship two ways:
190
+ **importable previews** (`import { SettingsPage } from "@sableui/react/patterns"`)
191
+ and **copy-pasteable source** (`registry.json → patterns`, imports included).
192
+ Inspect them before choosing composition. Reuse and diverge when the information
193
+ architecture fits; compose an original route from layout primitives when it does
194
+ not. The brief determines the result, not resemblance to an existing shell.
195
+
196
+ The **source is the contract**: each pattern is real TSX (typechecked, design-linted, rendered live by the docs app), split as `<name>.meta.ts` (a `PatternMeta`: name/summary/intent/notes/a11y) + `<name>.tsx` whose code after the `// ---pattern-code---` marker is emitted verbatim into the registry by `build-registry.mjs` — so the shipped recipe can never drift from code that compiles. Keep patterns hook-free and self-contained (inline sample data, `<a href>` for links); the generator auto-derives the consumer import block. To add one: write the pair, export both from `src/patterns/index.ts`, regenerate the registry, and add it to the docs `PatternPreview` map.
197
+
198
+ ## Promoting from an app (and the drift back down)
199
+
200
+ The system grows **out of the apps**, not ahead of them. A component earns its way in by being built in a consuming app first, measured in place, and only then lifted here — which is how the workspace register (v0.31.1, out of Glitch's tool chrome) and `Tree` (v0.32.0, out of Glitch's layers list) landed. Both deleted more app code than they added: Glitch's `globals.css` went 229 → 49 lines and its local `components/workspace/` disappeared. That deletion is the signal you're promoting something real rather than relocating it.
201
+
202
+ **The route up.** Build it in the app. Measure the actual numbers in the running product — control heights, radii, type sizes at their real weights (`DESIGN.md → Registers` is what measured numbers look like written down). Then promote with three things, none optional: **tokens** (so the geometry is retunable, never hardcoded), **a contract test** (so the invariant you discovered can't silently break — `Tree`'s is `--tree-indent` must equal `--tree-column`, because a smaller step renders nesting *inverted*), and **a docs page that reproduces the bug you hit**, so the next agent meets the failure before they meet the fix.
203
+
204
+ **The over-fit check.** One consumer proves a thing works; it does not prove the shape is general. Before promoting, write down which parts you expect a *second* consumer to need — those predictions are the point, and whichever fail are the over-fits to the first app. When only one consumer exists, say so and prefer the narrower API; widening later is cheap, and unwinding an abstraction shaped around one app's accident is not.
205
+
206
+ **The drift down is not automatic, and nothing detects it.** Components live in the package, so an upgrade delivers them. **Patterns are copy-in** — the moment an app adapts one it stops tracking, and no tooling notices. `sable-lint` is a lexical token linter (see "Consuming this package"); it has no idea a given file was ever meant to follow a pattern. So when a pattern changes here, the consuming app needs a deliberate read-and-diff: `CHANGELOG.md`, the pattern's `llms/patterns/<name>.md`, and the current implementation, side by side. Most of the work is usually small, because patterns are mostly *composition* over components the app already got on upgrade — what the app misses is the **wiring**, which component to reach for and how to connect it (0.56.1's ChatPanel is the example: the streaming behavior shipped in `StreamingText`; the pattern only showed how to drive it from one `pending` flag).
207
+
208
+ ## Component meta (the AI-readable surface)
209
+
210
+ Each component ships `<name>.meta.ts` exporting a `ComponentMeta` object: `name`, `summary`, `keywords`, `usage` (intent, do/don't guidance, anatomy), `related`, `props` (name/type/default/description), `subcomponents`, `variants`, `tokensUsed` (semantic tokens it reads), `a11y` notes, and `examples` (label + code). Inline meta is the preferred **source of truth** and the contract other agents read; keep it in sync when you change a component. Existing guidance backfills may also live in `src/lib/component-guidance.ts`; the docs and registry generator merge that overlay so consumers read one resolved catalog.
211
+
212
+ `usage.bestPractices` is where design judgment lives. Include concrete "do" and "dont" items that teach agents when to reach for the component, when to choose a neighboring component instead, and which visual/interaction anti-patterns to avoid. For composite components, include `usage.anatomy` so agents understand the required parts and slots before composing JSX.
213
+
214
+ Two rules that keep the contract unambiguous for consumers reading the registry:
215
+
216
+ - **Stable components ship guidance.** A stable component needs `keywords`, `usage.description`, at least one `usage.bestPractices` "do" item, at least one "dont" item, and `usage.anatomy` when it exposes `subcomponents`. `build-registry.mjs` enforces this against the resolved inline-plus-overlay metadata.
217
+ - **`props` holds the root's props only.** Composed child components (SelectItem, ProgressTrack, …) go in `subcomponents` — a capitalized entry in `props` reads as a prop-object API (`<Progress ProgressTrack={{…}}/>`) and misleads agents.
218
+ - **Every composite shows its composed form.** A meta with `subcomponents` must include at least one example whose code uses them as JSX children — the shape of the API must be visible in the examples, not only in prose.
219
+ - **Examples are complete, copy-pasteable code.** The generator **fails** on an example that elides its content (`>…<`) or references a component the package doesn't export (a phantom like `<NetworkBanner>`); components consumers genuinely supply (a router `<Link>`) live in the generator's `EXTERNAL_COMPONENTS` allowlist. Every emitted example carries an auto-derived `imports` block — don't hand-author imports in meta code.
220
+
221
+ `registry.json`'s `components[]` (with each component's full props/variants/examples) is **generated from the metas** by `scripts/build-registry.mjs` — do **not** hand-edit it. `pnpm build` regenerates it; CI runs `pnpm registry:check` (`build:registry:check`) and **fails** if the committed file is out of sync, so the catalog can't silently drift. Adding a component is therefore: write its `*.meta.ts`, run the build (or `pnpm build:registry`), commit the regenerated `registry.json`. (The static blocks — `consume`, `tokens`, `enforcement`, `iconContract`, `layoutPrimitives` — are hand-authored and preserved by the generator.)