@sableui/react 0.0.0-stage → 0.1.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 (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +50 -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
@@ -0,0 +1,113 @@
1
+ # Task: adding a component to Sable
2
+
3
+ Contributing *to the library*, not consuming it. Every step below is enforced by
4
+ `pnpm build:registry` or `pnpm design-lint` — the checks are the specification,
5
+ so run them early rather than at the end.
6
+
7
+ ## The files
8
+
9
+ ```
10
+ src/components/<kebab-name>/
11
+ <kebab-name>.tsx the component
12
+ <kebab-name>.meta.ts the machine-readable contract ← the important one
13
+ <kebab-name>.test.tsx behaviour, not snapshots
14
+ index.ts named exports + the meta
15
+ ```
16
+
17
+ Then: export from `src/index.ts` (the single public barrel), file it in
18
+ `src/lib/taxonomy.ts`, and add `apps/docs/src/pages/components/<Name>Page.tsx`
19
+ plus its route in `apps/docs/src/App.tsx`.
20
+
21
+ ## The meta is the deliverable
22
+
23
+ `registry.json` is generated from it, and that catalog is what every agent reads.
24
+ A component whose meta is thin is a component nobody can use correctly.
25
+
26
+ Enforced minima — the build fails below them:
27
+
28
+ - `usage.description` — why it exists and when to reach for it
29
+ - **≥1 `do`, ≥2 `dont`, ≥2 examples, ≥2 a11y notes, ≥1 related component**
30
+ - every prop described; `props` holds the ROOT's props only (a capitalised entry
31
+ that is not the root is a subcomponent hiding in `props`)
32
+ - a component with `subcomponents` must show the composed JSX in an example
33
+ - examples must be **complete and copy-pasteable** — no `…` elisions, no phantom
34
+ components, only on-contract `<Icon name>` values
35
+ - `tokensUsed` entries must exist in the token contract
36
+
37
+ A second `dont` must name a **different** failure, not restate the first. The
38
+ highest-value guidance answers *which of these near-identical components do I
39
+ actually want* — AppHeader vs PageHeader, Code vs CodeBlock (`prompt` for a
40
+ command), Meter vs Progress.
41
+
42
+ ## Declare how it moves
43
+
44
+ `meta.motion` answers four questions, one entry per slot. Fill the ones that
45
+ apply and **decline the rest explicitly** — `recipe: "none"` with a `why`. Use
46
+ `recipe: "custom"` when a rAF loop or canvas fills the slot and no CSS utility
47
+ could (ThinkingOrb draws a point cloud per frame).
48
+
49
+ | slot | the question |
50
+ |---|---|
51
+ | `arrival` | how does it enter, or does it belong to a layout that already staged it? |
52
+ | `response` | what happens under the pointer? |
53
+ | `transition` | how does it move between its own states? |
54
+ | `activity` | what does it do while working, indeterminately? |
55
+
56
+ The `why` states what the motion is *for* — what a reader would misread without
57
+ it. `"motion-pop on the panel"` restates the recipe and teaches nothing; *"the
58
+ transform-origin follows the anchor, so on a form with several selects in a
59
+ column the origin is the only thing identifying which field the open list belongs
60
+ to"* is the sentence worth writing, because it also tells the next person when
61
+ **not** to copy it.
62
+
63
+ This exists because every other motion rule is prohibitive — `raw-duration`,
64
+ `builtin-animate`, `unknown-motion` all fire on motion that is present and wrong,
65
+ and none of them can fire on motion that is absent. A component with no motion at
66
+ all used to clear the entire contract, which is exactly what gets written when
67
+ nobody is asked for it by name. `pnpm build:registry -- --motion-report` shows
68
+ current coverage; `static-interactive` catches the element-level half in lint.
69
+
70
+ ## Taxonomy: two orthogonal axes
71
+
72
+ `src/lib/taxonomy.ts` declares a **group** (role) and a **register**
73
+ (`page` / `workspace` / `either`). Keep them separate: a register-based *group*
74
+ was tried and made every workspace form control have two valid homes. Group by
75
+ KIND, not by co-occurrence — the `related` graph tracks composition as much as
76
+ kinship, and grouping by it measurably made cohesion worse.
77
+
78
+ Guidance lives in the component's own meta. `lib/component-guidance.ts` is for
79
+ the 13 layout primitives only; a component in both places fails the build,
80
+ because the meta wins *whole* and silently discards the other.
81
+
82
+ ## The hard rules
83
+
84
+ 1. **Semantic tokens only** — never hex/oklch, never a raw palette step.
85
+ 2. **Paired typography utilities** — never `text-{size}` + `font-{weight}`.
86
+ 3. **`primitives/` is behaviour, `components/` is the public API.** Base UI is
87
+ wrapped and re-exported; consumers import only from the root barrel.
88
+ 4. **No icon library in the package.** `<Icon name>` resolves through the
89
+ injected provider. Two narrow amendments exist (`src/icon-sets/`, and
90
+ size-matched structural marks) — read them before assuming you qualify.
91
+ 5. **Tailwind v4 packaging discipline** — `src/styles.css` must not
92
+ `@import "tailwindcss"`.
93
+
94
+ ## Two failure modes that do not announce themselves
95
+
96
+ - **A custom `text-*` utility unregistered in `lib/cn.ts`** lands in
97
+ tailwind-merge's *colour* group and silently evicts the real colour class. This
98
+ shipped white text on a white button. Register the tier, or do not name it
99
+ `text-*`. The same applies to `bg-*`.
100
+ - **An unmapped icon name renders nothing** — no error, no warning.
101
+
102
+ ## Verify
103
+
104
+ ```bash
105
+ pnpm typecheck && pnpm design-lint && pnpm build:registry && pnpm test:run
106
+ pnpm build:registry -- --doc-report # guidance depth, weakest entries first
107
+ pnpm build:registry -- --motion-report # motion slots: filled, declined, absent
108
+ ```
109
+
110
+ ## Read next
111
+
112
+ - `CONTRIBUTING.md` — the hard rules, the recipe, theme + icon + motion contracts
113
+ - `DESIGN.md` — the system spec, with intent and do/don't per layer
@@ -0,0 +1,216 @@
1
+ # Task: auditing existing work against Sable
2
+
3
+ This is Sable's native contract-and-taste audit, used by `$sable audit <target>`.
4
+ `$sable critique <target>` emphasizes design judgment. These are agent skill
5
+ modes, not shell subcommands. No other design skill is required.
6
+
7
+ ## Establish the comparison basis
8
+
9
+ Identify source paths, rendered routes, installed Sable version, audience, primary
10
+ task, selected identity and supported device/color modes. Reuse the existing brief;
11
+ label assumptions. For a whole app, inventory routes and shared controls, inspect
12
+ each distinct layout and critical journey, and state which routes were sampled.
13
+ Audit the installed version using its registry, AGENTS.md and DESIGN.md. Library
14
+ contribution rules do not all apply to consumers: original themes, fonts, layouts
15
+ and native elements are not automatically violations.
16
+
17
+ An audit reports findings by default. An explicit "audit and fix" request already
18
+ authorizes scoped repairs and verification. Do not automatically upgrade packages,
19
+ migrate the app, change its identity, generate variants or alter CI for a review.
20
+ Resolve the project's installed CLI before using `npx`; in this repository use
21
+ `node packages/sable/bin/sable.mjs`. Below, `sable` means that resolved CLI.
22
+
23
+ Inspect the interface before reading previous verdicts. Follow
24
+ [reviewing-a-design.md](./reviewing-a-design.md) for desktop/mobile, full-route,
25
+ keyboard, interaction and motion evidence. A screenshot-only critique leaves code
26
+ and live behavior untested. Compare three layers explicitly:
27
+
28
+ | Layer | Evidence and judgment |
29
+ |---|---|
30
+ | System contract | Component props/anatomy/states, semantic tokens, layout primitives, pattern intent, icons, motion and accessibility behavior. |
31
+ | Taste and craft | Brief fit, hierarchy, typography, spacing/rhythm, color/material, imagery, story, interaction, motion and mobile composition. |
32
+ | Learned context | Applicable human preferences, separately identified agent observations, conditional lessons and comparable previous builds. |
33
+
34
+ If the project keeps a visual atlas, retrieve a close analogue and a useful
35
+ contrast through `sable atlas --search "<audience and design problem>" --contrast
36
+ --limit 3 --json`. Open useful entries
37
+ with `sable atlas --show <id> --json` and actually inspect their images. Explain
38
+ which relationships transfer; resemblance to Sable's default theme is not the goal.
39
+ Use `sable prefer --search "<decision>" --scope project:<project-id> --json` in an
40
+ applicable scope and `sable learn --search "<problem>" --json`. Agent preferences
41
+ require explicit `--reviewer agent` retrieval and agent attribution. Empty memory
42
+ is valid. When prior builds matter, inspect their actual captures for repeated
43
+ compositions and range; state when comparison evidence is absent.
44
+
45
+ Inventory imported Sable components, locally recreated controls, native elements,
46
+ consumer tokens and repeated overrides. Query the installed catalog before
47
+ recommending a replacement. Check the API, composed anatomy, supported states,
48
+ keyboard/focus behavior, semantic token roles and motion policy. Explain which
49
+ behavior a replacement gains and what must remain. An unused page pattern is not
50
+ drift; repeated overrides can indicate a missing system hook rather than app misuse.
51
+ Run relevant `sable doctor`, `sable lint <paths> --json` and `sable grade <paths>
52
+ --json` checks. Lint is lexical; source grades and pattern similarity do not judge
53
+ taste. Deliberately static motion slots remain valid with appropriate feedback.
54
+
55
+ ## Deliver evidence-backed findings
56
+
57
+ Save the audit under `design/<surface>/audit/` or the existing review location.
58
+ Record source/version, routes/states inspected, commands, reference/memory IDs and
59
+ untested areas. Use the source/artifact binding and review template described in
60
+ [reviewing-a-design.md](./reviewing-a-design.md) for structured review records.
61
+
62
+ For each finding, record location, observed consequence, severity, evidence,
63
+ comparison basis, concrete repair, owner (app or Sable) and verification method.
64
+ Distinguish **contract violation**, **taste/craft issue**, **intentional choice**
65
+ and **Sable system gap**. Separate objective failures from agent judgments and
66
+ suspected gaps from verified library defects. Keep component/token findings
67
+ separate from visual quality; report the eight review dimensions as `pass`,
68
+ `revise` or `untested`, never a fabricated premium score. Retain what already works.
69
+
70
+ For authorized fixes, repair observed issues, run relevant checks and recapture
71
+ affected states. Preserve before/after evidence and resolved/remaining findings.
72
+ Use [reviewing-a-journey.md](./reviewing-a-journey.md) for connected flows and
73
+ [reviewing-motion.md](./reviewing-motion.md) for temporal behavior. Save useful
74
+ conditional lessons through [learning-from-builds.md](./learning-from-builds.md).
75
+ Completing an audit supplies no human preference and does not authorize inventing
76
+ or automatically saving one.
77
+
78
+ ## Additional checks after an upgrade
79
+
80
+ An app converted to Sable is correct against the contract *as it stood on the day
81
+ it was converted*. The contract keeps moving — new rules, new components,
82
+ deprecations, new doctrine — and none of that reaches an app that merely has the
83
+ package installed. This is the pass that closes the gap.
84
+
85
+ Use the following drift checks after an actual upgrade, or when the user asks to
86
+ compare versions. For an audit-only request, report suggested changes rather than
87
+ performing the fixes or CI edits illustrated below.
88
+
89
+ ## The failure this prevents
90
+
91
+ Enforcement travels with the package: `sable-lint` reads the token vocabulary and
92
+ rule table out of the `registry.json` of the version you installed, so upgrading
93
+ Sable silently widens what counts as a violation. That is the design — it means a
94
+ new rule reaches every app without anyone republishing a checklist — but it has a
95
+ predictable failure mode. An app upgrades, CI goes red, the findings are a mix of
96
+ brand-new requirements and drift that has been sitting there for months, nobody
97
+ can tell which is which, and the team pins the version. A pinned version is a
98
+ contract that stopped applying.
99
+
100
+ So audit in the order below: **find out what changed, then measure, then fix.**
101
+
102
+ ## 0. Know what moved before you look at your code
103
+
104
+ ```bash
105
+ npx sable-lint --since 0.54.2 # the version you were on
106
+ ```
107
+
108
+ `--since` lists only the rules added after that version, with the reason each
109
+ exists. Everything else `sable-lint` reports is pre-existing drift — still worth
110
+ fixing, but it is not what the upgrade did to you, and separating the two is what
111
+ keeps the upgrade reviewable.
112
+
113
+ For changes that are not lint rules, diff the catalog:
114
+
115
+ ```bash
116
+ # Components that gained a deprecation, and what replaced them.
117
+ node -e "const r=require('@sableui/react/registry.json');
118
+ r.components.filter(c=>c.status==='deprecated')
119
+ .forEach(c=>console.log(c.name,'→',c.supersededBy))"
120
+ ```
121
+
122
+ Deprecation is structured, not prose — `status: "deprecated"` always carries
123
+ `supersededBy`, and the build refuses a deprecation without a forward path. Grep
124
+ your app for each name on the left.
125
+
126
+ ## 1. Run the linter, and read the new rules first
127
+
128
+ ```bash
129
+ npx sable-lint # ./src by default
130
+ npx sable-lint app components # or pass your dirs
131
+ ```
132
+
133
+ Fix the `--since` rules first: those are the ones your reviewers have not seen
134
+ before, and they are the ones with a fresh rationale worth reading. A genuine
135
+ exception takes an auditable
136
+ `// design-lint-disable-next-line <ruleId> -- <reason>`; a reasonless disable is
137
+ itself a violation.
138
+
139
+ ## 2. Motion — the part the linter can only half-see
140
+
141
+ `static-interactive` (new in 0.55.0) is the first **prescriptive** rule in the
142
+ system. Every other motion rule fires on motion that is present and wrong;
143
+ none of them could ever fire on motion that is *absent*, so an app that simply
144
+ snaps between every state passed the entire contract. Most apps converted before
145
+ 0.55.0 will have findings here, and they are real: they are the places the
146
+ interface has no reaction.
147
+
148
+ The linter catches static interactive **elements**. It cannot see the three
149
+ motion slots that are about a component's behaviour over time, so check those by
150
+ hand, per surface:
151
+
152
+ | slot | ask | usual fix |
153
+ |---|---|---|
154
+ | arrival | does new content appear, or cut in? | `motion-fade`, `motion-slide-*`, `motion-pop`, `Reveal` |
155
+ | response | does anything happen under the pointer? | `motion-interactive`, `motion-press` |
156
+ | transition | does it move between its own states, or teleport? | `motion-disclosure`, or ease the transform |
157
+ | activity | can you tell "working" from "frozen"? | `motion-spin`, `motion-pulse`, `motion-shimmer` |
158
+
159
+ Sable's own components declare their slots — the `## Motion` section of each
160
+ entry in `llms/components/` says which slots are filled, which were *declined*,
161
+ and why. Read the declined ones too: "this does not animate, and here is the
162
+ reason" is what stops you adding motion somewhere it was ruled out.
163
+
164
+ Anything you compose by hand owns its own slots. The question is not "should this
165
+ animate" but "which of the four does this need, and what fills them".
166
+
167
+ ## 3. Hand-rolled components that Sable now ships
168
+
169
+ The most valuable finding in an audit is usually a deletion. Search for the
170
+ shapes that tend to get built locally before the library has them, then check the
171
+ catalog:
172
+
173
+ ```bash
174
+ grep -rEl "role=\"(dialog|listbox|tablist|menu)\"|aria-expanded" src/ | head -40
175
+ ```
176
+
177
+ Every conversion so far has deleted more than it added at this step. One tool's
178
+ layers panel lost 174 lines by adopting `Tree`; its whole local
179
+ `components/workspace/` directory went away.
180
+
181
+ ## 4. Review the rendered product
182
+
183
+ Contract drift is only one part of an audit. A valid token can still be used in
184
+ the wrong hierarchy; a page full of correct cards can still obscure the task.
185
+ Follow [reviewing-a-design.md](./reviewing-a-design.md) across purpose/story,
186
+ typography, spacing, color/material, interaction, motion, responsive behavior,
187
+ and accessibility. Compare against the surface brief, not the default theme.
188
+ Inspect actual desktop/mobile states and record repairs with before/after evidence.
189
+ Capture a reusable finding through [learning-from-builds.md](./learning-from-builds.md).
190
+
191
+ ## 5. Make the contract audit a gate
192
+
193
+ An audit you have to remember to run is an audit that happens once. Two lines:
194
+
195
+ ```jsonc
196
+ // package.json
197
+ "scripts": { "lint:design": "sable-lint src" }
198
+ ```
199
+
200
+ ```yaml
201
+ # CI, alongside tsc and tests
202
+ - run: pnpm lint:design
203
+ ```
204
+
205
+ With that in place the loop closes itself: the dependency bot opens the Sable
206
+ upgrade PR, the new rules ship inside it, CI fails on that PR rather than three
207
+ weeks later, and the diff that introduced the requirement is the diff that has to
208
+ satisfy it. That is the only arrangement where "we should audit the apps" stops
209
+ being a thing anyone has to say.
210
+
211
+ ## Read next
212
+
213
+ - [converting-an-app.md](./converting-an-app.md) — the first-pass migration
214
+ - [building-a-page.md](./building-a-page.md) ·
215
+ [building-a-tool.md](./building-a-tool.md)
216
+ - `DESIGN.md → Motion`
@@ -0,0 +1,109 @@
1
+ # Task: building a page (page register)
2
+
3
+ Marketing, docs, settings, a feed, a dashboard — anything whose primary
4
+ interaction is reading and navigating. This is the default register; if you are
5
+ building an editor or a canvas app, see
6
+ [building-a-tool.md](./building-a-tool.md).
7
+
8
+ ## Start from the brief, then choose composition
9
+
10
+ Read [designing-a-surface.md](./designing-a-surface.md) for the user's outcome,
11
+ content, visual direction, story, interaction, and adaptation. Retrieve relevant
12
+ lessons with `sable learn --search "<context>"`.
13
+
14
+ `sable pattern --list` offers complete canonical compositions with imports.
15
+ Inspect a matching skeleton and reuse it when its information architecture fits.
16
+ For an original page, compose layout primitives and semantic HTML around the
17
+ brief. Patterns encode useful defaults; they do not define the space of good
18
+ designs. Record the reason for the chosen composition.
19
+
20
+ ## Layout, in the order it matters
21
+
22
+ ```tsx
23
+ <AppShell header={<AppHeader title="…" />} sidebar={<Sidebar>…</Sidebar>}>
24
+ <Region max="content"> {/* width capped BY ROLE, never globally */}
25
+ <PageHeader title="Invoices" description="…" actions={<Button>Export</Button>} />
26
+ <Stack gap={3}> {/* gap counts 8px rhythm units, not pixels */}
27
+ …
28
+ </Stack>
29
+ </Region>
30
+ </AppShell>
31
+ ```
32
+
33
+ - **Never a global `max-width`.** Width is capped per region by role:
34
+ `prose` / `form` / `feed` / `content` / `wide` / `full`. A form region is narrow
35
+ because a 700px text input is harder to use, not more generous.
36
+ - **Measure ≠ width.** Cap prose line length in `ch` (`--measure-prose`)
37
+ independently of the region's layout width.
38
+ - **The 12-column grid lives *inside* a region**, for genuinely multi-column
39
+ content. A column span is not a page width.
40
+ - **Chrome is off the grid.** `Sidebar` and rails are fixed-width, outside the
41
+ content grid.
42
+
43
+ ## Typography: pick the tier by ROLE, not by size
44
+
45
+ | tier | for | not |
46
+ |---|---|---|
47
+ | `text-display-*` | a hero, one per view at most | page titles — that is `heading-1` |
48
+ | `text-heading-1..4` | the document outline; pick by depth | size — if it belongs in a table of contents it is a heading |
49
+ | `text-title-*` | a *component's* title: card, dialog, panel section | a heading tier — a card title is not part of the page outline |
50
+ | `text-body-*` | prose you read a sentence of | labels |
51
+ | `text-label-*` | field labels, column headers, button text | body at the same size |
52
+ | `text-caption` | metadata *beneath* something | `text-overline`, which sits *above* and categorises |
53
+
54
+ Always the paired utility. Never `text-sm font-medium` — CI rejects it.
55
+
56
+ ## Colour: role, not appearance
57
+
58
+ `accent`, `secondary` and `muted` are near-identical in most themes and are
59
+ chosen by what the surface *does*:
60
+
61
+ - `muted` — recessed and inert (a well, a disabled field, a zebra stripe)
62
+ - `secondary` — a control fill (something you can press)
63
+ - `accent` — a transient state (hover, selected, current)
64
+
65
+ Pick by appearance and it works in Base and breaks in Meridian. Every token's
66
+ intent, and what it gets confused with, is in
67
+ `llms/reference/token-intent.md`.
68
+
69
+ Ink is never chosen independently of its surface: `bg-card text-card-foreground`,
70
+ as a pair.
71
+
72
+ ## The traps
73
+
74
+ - **A `Card` is a container, not a heading or a landmark.** If its title is the
75
+ only heading in the region, render a real heading element.
76
+ - **One `h1` per route** — `PageHeader` renders it. A second breaks the outline a
77
+ screen-reader user navigates by.
78
+ - **`AppHeader` is app chrome; `PageHeader` is content.** Route-specific actions
79
+ in `AppHeader` persist onto routes where they mean nothing.
80
+ - **Tone is never the only signal.** Pair a tone with an icon or text; colour
81
+ alone fails for colour-blind users.
82
+
83
+ ## Verify
84
+
85
+ ```bash
86
+ npx sable-lint # lints ./src; pass dirs to change (npx sable-lint app components)
87
+ ```
88
+
89
+ `sable-lint` ships with the package and reads the token vocabulary from the
90
+ `registry.json` of the version you installed, so it checks your app against your
91
+ exact tokens. It exits 1 on any violation. `--rules` explains each one; `--ignore
92
+ '<glob>'` skips a data/fixture layer, where a hex value is content rather than a
93
+ style. A rare, genuine exception takes an auditable
94
+ `// design-lint-disable-next-line <ruleId> -- <reason>`.
95
+
96
+ Then your own `tsc` and tests. (`pnpm design-lint && pnpm build:registry &&
97
+ pnpm test:run` are the *in-repo* gates — they only run inside the Sable repo.)
98
+
99
+ Then inspect the rendered page on desktop and mobile, operate its primary flow,
100
+ use the keyboard, and check reduced motion. Follow
101
+ [reviewing-a-design.md](./reviewing-a-design.md) for evidence and repair. Passing
102
+ source checks alone says nothing about hierarchy, story, or the resulting polish.
103
+
104
+ ## Read next
105
+
106
+ - `sable pattern --list` — the current compositions
107
+ - `registry.json → layoutPrimitives` — full contracts plus the layout doctrine
108
+ - `DESIGN.md` — spacing, dimensions, shape, elevation, layering, motion
109
+ - `llms/reference/token-intent.md`
@@ -0,0 +1,141 @@
1
+ # Task: building a tool (workspace register)
2
+
3
+ An editor, a canvas app, a console — anything whose primary interaction is
4
+ *operating on something* rather than reading and navigating.
5
+
6
+ If the primary interaction is reading down a page, stop: you want
7
+ [building-a-page.md](./building-a-page.md). A dashboard that scrolls is a page
8
+ with dense content, not a workspace.
9
+
10
+ ## The shell
11
+
12
+ First read [designing-a-surface.md](./designing-a-surface.md). Define the operator's
13
+ primary task, selection/navigation model, most frequent actions, and narrower
14
+ viewport behavior before choosing panel furniture. Retrieve applicable precedents
15
+ with `sable learn --search "workspace interaction"`; editorial motion and display
16
+ hierarchy do not automatically transfer to dense operating surfaces.
17
+
18
+ ```tsx
19
+ <Workspace {/* h-dvh, sets data-density="compact" */}
20
+ toolbar={<WorkspaceBar>…</WorkspaceBar>}
21
+ statusBar={<StatusBar>…</StatusBar>}
22
+ >
23
+ <PanelGroup>
24
+ <Panel label="Layers" width={leftWidth}>…</Panel>
25
+ <PanelResizer side="left" width={leftWidth} onWidthChange={setLeftWidth}
26
+ label="Resize layers panel" />
27
+ <WorkspaceCanvas>{/* the work surface — clips, never scrolls */}</WorkspaceCanvas>
28
+ </PanelGroup>
29
+ </Workspace>
30
+ ```
31
+
32
+ `Workspace` sets the compact density scope for its whole subtree. Nothing
33
+ hardcodes a height: every control reads `--control-*`, so the same
34
+ `<Button size="md">` is 40px on a page and 32px here. A `Dialog` opened from
35
+ inside resets to comfortable, because a modal is a page-register surface even
36
+ when a tool launched it.
37
+
38
+ ## What you will need, and what to avoid reaching for
39
+
40
+ | you want | use | NOT |
41
+ |---|---|---|
42
+ | a property grid | `PanelSection` + `PropertyRow` / `PropertyPair` | `Card` — in 240px it spends its border and inset on nothing |
43
+ | a numeric property | `ScrubField` | `NumberField` — its two steppers put a ~112px floor on a ~44px slot |
44
+ | a layers / object list | `Tree` | a hand-nested list — no selection model or keyboard |
45
+ | an empty panel section | `TreeEmpty` | `EmptyState` — `px-6 py-12` around a `size-12` disc |
46
+ | a colour property | `ColorField` | `<input type="color">` — OS dialog, ignores the theme, wrong size |
47
+ | ambient document facts | `StatusBar` | a toolbar — status is glanced at, not operated |
48
+ | chrome typography | `text-ui-md` / `-sm` / `-body` / `-section` / `-micro` / `-num` | `text-body-*` — page tiers, ~1/3 too large here |
49
+
50
+ ## Menus, selects and popovers adapt on their own
51
+
52
+ `Menu`, `Select`, `Popover`, `Tooltip` and `ContextMenu` pick up the workspace's
53
+ density automatically — including **across the portal**. Their popups mount at
54
+ the document root, so CSS could never reach them; they re-apply `data-density`
55
+ from React context instead.
56
+
57
+ **Do not size them per call site.** That was necessary before v0.41 — Glitch
58
+ carried 14 such overrides and had them inconsistent — and it is now drift.
59
+
60
+ A **modal** is deliberately different: `Dialog`, `Sheet` and
61
+ `DestructiveActionModal` stay page-register even when a tool opens them, because
62
+ a modal is its own surface rather than part of the control that opened it.
63
+
64
+ ## The traps, all of which have shipped
65
+
66
+ - **A portaled surface still fires your canvas's pointer handlers.** Every Sable
67
+ popup — `Dialog`, `Menu`, `Select`, `Popover`, `Tooltip`, `ContextMenu` —
68
+ portals to `document.body`. But React propagates events through the **component
69
+ tree, not the DOM tree**, so if the component that renders your canvas also
70
+ renders the dialog, every press inside that dialog re-enters your pan/drag/
71
+ marquee handler.
72
+
73
+ The symptom is specific and misleading: **pointerdown-driven controls work while
74
+ click-driven ones do not.** A combobox opens (pointerdown) but you cannot pick
75
+ an option (click); buttons, switches and Cancel are dead; typing and Escape are
76
+ fine. That is a global handler calling `setPointerCapture` on pointerdown, which
77
+ retargets `pointerup` away from the button, so the browser never synthesises a
78
+ `click`. Nothing is prevented and nothing errors — it reads exactly like a
79
+ broken component.
80
+
81
+ Guard by **containment in your own surface**, not by a marker on ancestors:
82
+
83
+ ```tsx
84
+ // ✗ walks DOM ancestors — a portaled popup's ancestors are <body>/<html>,
85
+ // so the marker is never found and the guard passes everything through
86
+ if ((e.target as Element).closest("[data-no-pan]")) return
87
+
88
+ // ✓ a pan is a press inside the thing being panned. Portals fail this by
89
+ // construction, and so does every future popup you have not written yet
90
+ if (!viewportRef.current?.contains(e.target as Node)) return
91
+ ```
92
+
93
+ Test it with a popup **open over a live document**, not on an empty canvas — a
94
+ disabled pan cannot exhibit the bug, so a green run there proves nothing.
95
+
96
+ - **`min-h-0` is the precondition for scrolling.** `overflow-y-auto` only
97
+ activates if every flex ancestor can shrink. One missing `min-h-0` up the chain
98
+ and the panel grows instead of scrolling — silently.
99
+ - **Never override a control height with a selector.** `[data-density] button {
100
+ height: … }` loses to any equal-specificity utility. Redefine `--control-*` in a
101
+ density scope instead. CI-enforced (`raw-control-height`).
102
+ - **Tree geometry has one invariant:** `--tree-indent` must equal
103
+ `--tree-column`. A smaller step puts a child's content LEFT of its parent's and
104
+ the nesting reads inverted. Use `treeIndent(depth)`; never hand-write an inset.
105
+ - **No pills in chrome.** The compact scope flattens `--button-radius` to ~5px —
106
+ one of two page-register rules the workspace register deliberately reverses.
107
+ - **Drag with window listeners, not `setPointerCapture`.** Capture throws for an
108
+ inactive pointer id and the throw aborts the handler mid-gesture, silently.
109
+ - **An unmapped `<Icon name>` renders nothing.** No error. Check the contract.
110
+
111
+ ## Verify
112
+
113
+ ```bash
114
+ npx sable-lint # lints ./src; pass dirs to change (npx sable-lint app components)
115
+ ```
116
+
117
+ `sable-lint` ships with the package and reads the token vocabulary from the
118
+ `registry.json` of the version you installed, so it checks your app against your
119
+ exact tokens. It exits 1 on any violation. `--rules` explains each one; `--ignore
120
+ '<glob>'` skips a data/fixture layer, where a hex value is content rather than a
121
+ style. A rare, genuine exception takes an auditable
122
+ `// design-lint-disable-next-line <ruleId> -- <reason>`.
123
+
124
+ Then your own `tsc` and tests. (`pnpm design-lint && pnpm build:registry &&
125
+ pnpm test:run` are the *in-repo* gates — they only run inside the Sable repo.)
126
+
127
+ Then measure in the browser. Every number in the workspace register came from
128
+ measuring Figma and Paper in-page, and every register bug found so far was found
129
+ by reading computed values, not by looking at a screenshot.
130
+
131
+ Also inspect the rendered hierarchy and complete a real flow with a live document,
132
+ keyboard navigation, a narrow viewport, and reduced motion. Use
133
+ [reviewing-a-design.md](./reviewing-a-design.md) to record evidence, repair defects,
134
+ and distinguish observed behavior from untested claims.
135
+
136
+ ## Read next
137
+
138
+ - `DESIGN.md → Registers` — doctrine, measured numbers, the two reversals
139
+ - `llms/components/workspace.md`, `panel-group.md`, `panel-section.md`,
140
+ `tree.md`, `scrub-field.md`, `status-bar.md`, `color-field.md`
141
+ - `llms/reference/token-intent.md` — which token to reach for