@sableui/react 0.9.1 → 0.11.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 (216) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +109 -0
  3. package/CONTRIBUTING.md +2 -2
  4. package/DESIGN.md +55 -16
  5. package/README.md +12 -0
  6. package/dist/{_contract-C2M1SjBf.d.ts → _contract-CkqXDVww.d.ts} +25 -2
  7. package/dist/blocks/danger-zone-block.js +3 -3
  8. package/dist/blocks/danger-zone-block.meta.js +1 -1
  9. package/dist/blocks/faq-block.js +3 -3
  10. package/dist/blocks/goal-progress-block.js +1 -1
  11. package/dist/blocks/notification-list-block.js +1 -1
  12. package/dist/blocks/pricing-table-block.js +12 -12
  13. package/dist/blocks/product-card-block.js +3 -3
  14. package/dist/blocks/product-card-block.meta.js +3 -3
  15. package/dist/blocks/profile-card-block.js +1 -1
  16. package/dist/blocks/profile-form-block.js +3 -3
  17. package/dist/blocks/revenue-breakdown-block.js +5 -5
  18. package/dist/blocks/stat-row-block.js +3 -3
  19. package/dist/blocks/task-list-block.js +2 -2
  20. package/dist/blocks/team-members-block.js +1 -1
  21. package/dist/blocks/wallet-block.js +4 -4
  22. package/dist/blocks/wallet-block.meta.js +3 -3
  23. package/dist/components/accordion/accordion.meta.js +2 -2
  24. package/dist/components/app-header/app-header.meta.js +1 -1
  25. package/dist/components/app-shell/app-shell.meta.js +2 -2
  26. package/dist/components/area-chart/area-chart.meta.js +3 -3
  27. package/dist/components/badge/badge.js +13 -4
  28. package/dist/components/badge/badge.meta.js +24 -13
  29. package/dist/components/badge/badge.variants.js +37 -23
  30. package/dist/components/banner/banner.meta.js +8 -8
  31. package/dist/components/banner/banner.variants.js +3 -3
  32. package/dist/components/bar-chart/bar-chart.meta.js +4 -4
  33. package/dist/components/breadcrumbs/breadcrumbs.meta.js +1 -1
  34. package/dist/components/button/button.meta.js +3 -3
  35. package/dist/components/card/card.js +16 -7
  36. package/dist/components/card/card.meta.js +4 -4
  37. package/dist/components/checkbox-card/checkbox-card.meta.js +10 -10
  38. package/dist/components/checkbox-card/checkbox-card.variants.js +15 -15
  39. package/dist/components/code-block/code-block.meta.js +1 -1
  40. package/dist/components/command-menu/command-menu.meta.js +1 -1
  41. package/dist/components/context-card/context-card.meta.js +2 -2
  42. package/dist/components/destructive-action-modal/destructive-action-modal.meta.js +1 -1
  43. package/dist/components/dialog/dialog.meta.js +2 -2
  44. package/dist/components/disclosure-picker/disclosure-picker.js +48 -2
  45. package/dist/components/disclosure-picker/disclosure-picker.meta.js +3 -2
  46. package/dist/components/empty-state/empty-state.meta.js +4 -4
  47. package/dist/components/gradient-art/gradient-art.meta.js +3 -3
  48. package/dist/components/label/label.meta.js +1 -1
  49. package/dist/components/line-chart/line-chart.meta.js +3 -3
  50. package/dist/components/meter/meter.js +1 -1
  51. package/dist/components/meter/meter.meta.js +2 -2
  52. package/dist/components/meter/meter.variants.js +1 -1
  53. package/dist/components/middle-truncate/middle-truncate.meta.js +4 -4
  54. package/dist/components/note/note.meta.js +3 -3
  55. package/dist/components/page-header/page-header.js +1 -1
  56. package/dist/components/pagination/pagination.meta.js +2 -2
  57. package/dist/components/panel-section/index.js +2 -1
  58. package/dist/components/panel-section/panel-section.js +15 -0
  59. package/dist/components/panel-section/panel-section.meta.js +32 -0
  60. package/dist/components/phone/phone.meta.js +1 -1
  61. package/dist/components/pie-chart/pie-chart.meta.js +3 -3
  62. package/dist/components/pill/pill.js +4 -2
  63. package/dist/components/pill/pill.meta.js +19 -2
  64. package/dist/components/pill/pill.variants.js +23 -2
  65. package/dist/components/popover/popover.meta.js +2 -2
  66. package/dist/components/radar-chart/radar-chart.meta.js +3 -3
  67. package/dist/components/radial-chart/radial-chart.meta.js +3 -3
  68. package/dist/components/radio/radio.meta.js +2 -2
  69. package/dist/components/radio-card/radio-card.meta.js +6 -6
  70. package/dist/components/radio-card/radio-card.variants.js +15 -15
  71. package/dist/components/scroller/scroller.meta.js +1 -1
  72. package/dist/components/search-input/search-input.meta.js +1 -1
  73. package/dist/components/section-header/section-header.js +1 -1
  74. package/dist/components/segmented-bar/segmented-bar.js +1 -1
  75. package/dist/components/segmented-bar/segmented-bar.meta.js +6 -6
  76. package/dist/components/segmented-bar/segmented-bar.variants.js +4 -4
  77. package/dist/components/select/select.meta.js +2 -2
  78. package/dist/components/sheet/sheet.meta.js +1 -1
  79. package/dist/components/side-nav/side-nav.meta.js +1 -1
  80. package/dist/components/sidebar/sidebar.meta.js +1 -1
  81. package/dist/components/sparkline/sparkline.meta.js +4 -4
  82. package/dist/components/spinner/spinner.meta.js +1 -1
  83. package/dist/components/status-dot/status-dot.js +4 -1
  84. package/dist/components/status-dot/status-dot.meta.js +8 -16
  85. package/dist/components/status-dot/status-dot.variants.js +13 -4
  86. package/dist/components/table/table.meta.js +4 -4
  87. package/dist/components/tabs/tabs.meta.js +1 -1
  88. package/dist/components/textarea/textarea.meta.js +2 -2
  89. package/dist/components/toast/toast.meta.js +1 -1
  90. package/dist/components/tooltip/tooltip.meta.js +2 -2
  91. package/dist/file-hashes.json +123 -121
  92. package/dist/foundations/spacing.js +8 -8
  93. package/dist/index.d.ts +171 -76
  94. package/dist/index.js +12 -1
  95. package/dist/lib/chart/types.js +3 -3
  96. package/dist/lib/palette.js +103 -0
  97. package/dist/lib/selection-card.js +4 -4
  98. package/dist/palette.css +669 -0
  99. package/dist/patterns/auth-page.js +6 -6
  100. package/dist/patterns/auth-page.meta.js +3 -3
  101. package/dist/patterns/brand-landing-page.js +2 -2
  102. package/dist/patterns/chat-panel.js +2 -2
  103. package/dist/patterns/dashboard-page.js +71 -71
  104. package/dist/patterns/dashboard-page.meta.js +5 -5
  105. package/dist/patterns/feed-page.js +25 -25
  106. package/dist/patterns/feed-page.meta.js +3 -3
  107. package/dist/patterns/landing-page.js +1 -1
  108. package/dist/patterns/list-detail-page.js +17 -17
  109. package/dist/patterns/settings-page.js +17 -17
  110. package/dist/presets/index.d.ts +1 -1
  111. package/dist/presets/index.js +17 -2
  112. package/dist/styles.css +115 -50
  113. package/dist/{theme-provider-S0NVJs2Y.d.ts → theme-provider-CdSicAad.d.ts} +1 -1
  114. package/dist/themes/_contract.js +22 -8
  115. package/dist/themes/atelier/atelier.css +13 -13
  116. package/dist/themes/atelier/index.d.ts +1 -1
  117. package/dist/themes/ember/ember.css +13 -13
  118. package/dist/themes/ember/index.d.ts +1 -1
  119. package/dist/themes/index.d.ts +2 -2
  120. package/dist/themes/meridian/index.d.ts +1 -1
  121. package/dist/themes/meridian/meridian.css +13 -13
  122. package/dist/themes/poppy/index.d.ts +1 -1
  123. package/dist/themes/poppy/poppy.css +19 -16
  124. package/dist/themes/prism/index.d.ts +1 -1
  125. package/dist/themes/prism/prism.css +18 -15
  126. package/dist/themes/sable/index.d.ts +1 -1
  127. package/dist/themes/sable/sable.css +18 -22
  128. package/dist/themes/verdant/index.d.ts +1 -1
  129. package/dist/themes/verdant/verdant.css +15 -15
  130. package/llms/blocks/danger-zone-block.md +4 -4
  131. package/llms/blocks/faq-block.md +3 -3
  132. package/llms/blocks/goal-progress-block.md +1 -1
  133. package/llms/blocks/notification-list-block.md +1 -1
  134. package/llms/blocks/pricing-table-block.md +12 -12
  135. package/llms/blocks/product-card-block.md +6 -6
  136. package/llms/blocks/profile-card-block.md +1 -1
  137. package/llms/blocks/profile-form-block.md +3 -3
  138. package/llms/blocks/revenue-breakdown-block.md +5 -5
  139. package/llms/blocks/stat-row-block.md +3 -3
  140. package/llms/blocks/task-list-block.md +2 -2
  141. package/llms/blocks/team-members-block.md +1 -1
  142. package/llms/blocks/wallet-block.md +7 -7
  143. package/llms/components/accordion.md +2 -2
  144. package/llms/components/app-header.md +1 -1
  145. package/llms/components/app-shell.md +2 -2
  146. package/llms/components/area-chart.md +1 -1
  147. package/llms/components/badge.md +15 -7
  148. package/llms/components/banner.md +6 -6
  149. package/llms/components/bar-chart.md +2 -2
  150. package/llms/components/breadcrumbs.md +1 -1
  151. package/llms/components/button.md +3 -3
  152. package/llms/components/card.md +4 -4
  153. package/llms/components/checkbox-card.md +5 -5
  154. package/llms/components/code-block.md +1 -1
  155. package/llms/components/command-menu.md +1 -1
  156. package/llms/components/context-card.md +2 -2
  157. package/llms/components/destructive-action-modal.md +1 -1
  158. package/llms/components/dialog.md +2 -2
  159. package/llms/components/disclosure-picker.md +2 -1
  160. package/llms/components/empty-state.md +4 -4
  161. package/llms/components/gradient-art.md +3 -3
  162. package/llms/components/label.md +1 -1
  163. package/llms/components/line-chart.md +1 -1
  164. package/llms/components/meter.md +2 -2
  165. package/llms/components/middle-truncate.md +5 -5
  166. package/llms/components/note.md +3 -3
  167. package/llms/components/pagination.md +2 -2
  168. package/llms/components/panel-section.md +28 -0
  169. package/llms/components/phone.md +1 -1
  170. package/llms/components/pie-chart.md +1 -1
  171. package/llms/components/pill.md +12 -2
  172. package/llms/components/popover.md +2 -2
  173. package/llms/components/radar-chart.md +1 -1
  174. package/llms/components/radial-chart.md +1 -1
  175. package/llms/components/radio-card.md +4 -4
  176. package/llms/components/radio.md +2 -2
  177. package/llms/components/scroller.md +1 -1
  178. package/llms/components/search-input.md +1 -1
  179. package/llms/components/segmented-bar.md +7 -7
  180. package/llms/components/select.md +2 -2
  181. package/llms/components/sheet.md +1 -1
  182. package/llms/components/side-nav.md +1 -1
  183. package/llms/components/sidebar.md +1 -1
  184. package/llms/components/sparkline.md +2 -2
  185. package/llms/components/spinner.md +1 -1
  186. package/llms/components/status-dot.md +4 -4
  187. package/llms/components/table.md +8 -8
  188. package/llms/components/tabs.md +1 -1
  189. package/llms/components/textarea.md +2 -2
  190. package/llms/components/toast.md +1 -1
  191. package/llms/components/tooltip.md +2 -2
  192. package/llms/foundations/spacing.md +7 -7
  193. package/llms/layout/grid.md +1 -1
  194. package/llms/layout/page.md +1 -1
  195. package/llms/layout/region.md +1 -1
  196. package/llms/layout/split-pane.md +1 -1
  197. package/llms/patterns/auth-page.md +11 -11
  198. package/llms/patterns/brand-landing-page.md +2 -2
  199. package/llms/patterns/chat-panel.md +2 -2
  200. package/llms/patterns/dashboard-page.md +78 -78
  201. package/llms/patterns/feed-page.md +31 -31
  202. package/llms/patterns/landing-page.md +1 -1
  203. package/llms/patterns/list-detail-page.md +18 -18
  204. package/llms/patterns/settings-page.md +18 -18
  205. package/llms/reference/token-intent.md +25 -11
  206. package/llms/reference/utilities.md +1 -1
  207. package/llms/tasks/adding-a-component.md +4 -1
  208. package/llms/tasks/building-a-page.md +40 -0
  209. package/llms/tasks/converting-an-app.md +3 -1
  210. package/llms/tasks/using-presets.md +4 -0
  211. package/llms-full.txt +457 -347
  212. package/llms.txt +8 -8
  213. package/package.json +4 -4
  214. package/registry.json +397 -233
  215. package/scripts/lint-rules.mjs +26 -5
  216. package/skills/sable/SKILL.md +14 -3
package/AGENTS.md CHANGED
@@ -37,7 +37,7 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
37
37
  **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.
38
38
 
39
39
  **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`). A component that is *broken*, not just missing a look, is patched in place with `sable patch <Component> --reason … --repro …` while the fix ships: the record is re-checked on every upgrade (`sable docs patching-a-component`).
40
- 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
+ 1. **Semantic tokens only.** `bg-card`, `text-muted-foreground`, `border-border`, `bg-primary` — never a theme's own palette step (`--zinc-500`), never inline hex/`oklch()`. For decorative or categorical colour beyond the tones (tags, avatars, labels, illustration), use the generated palette: `neutral`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple` and `pink`, plus a scale under every tone (`bg-tone-1-100`). It is built from the active theme and follows the mode, so `bg-blue-100 text-blue-800` reads in light and dark with no `dark:`. On Badge, Pill and StatusDot the family is the tone (`<Badge tone="teal">Research</Badge>`), never a `className` recolour. Surfaces, text and actions still take roles. → `sable token palette` Tone fills take their paired ink (`bg-tone-success` + `text-tone-success-foreground`). → `sable docs token-intent`
41
41
  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`
42
42
  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`
43
43
  4. **Controls read the ladder.** Heights come from `--control-*`, so `<Density>` can retune a region at once. Never `h-[40px]`. → `sable docs spacing`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # What's new
2
2
 
3
+ ## 0.11.0
4
+
5
+ - **New: `PropertyGroup`, a collapsible group inside a PanelSection.** The
6
+ row names the group, summarises its value while closed (a word, a few
7
+ swatches, or both), and opens with +/− to the rows beneath. It's sized for
8
+ a tool panel, where an Accordion is page furniture. Groups open
9
+ independently and stack with a hairline between them.
10
+ - **sableui.com's Customize panel: the Color section is rebuilt on it.**
11
+ It now reads Brand color, then Primary action (with colour dots), then
12
+ three groups: Color palette (the families and Pull to brand), Tones (tones
13
+ and status) and Neutrals. Each group summarises itself with its own
14
+ swatches, and Tones and Neutrals are chosen from a list that previews what
15
+ each option would paint. The section has its own Reset.
16
+
17
+ ## 0.10.0
18
+
19
+ A palette generated from the theme, numbered extra tones, and sample content that isn't a marketplace.
20
+
21
+ - **New: a generated palette for every theme.** Nine families (`neutral`,
22
+ `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple`, `pink`), each
23
+ 50–950, plus a scale under every tone (`tone-1`…`3`, `tone-info`,
24
+ `tone-warning`, `success`, `destructive`; step 500 is the tone). They come
25
+ as utilities (`bg-blue-100`, `text-tone-1-700`) and variables. Each family
26
+ keeps its hue and leans toward the brand (half the gap, at most 15°), and
27
+ neutral is the theme's own page mixed toward its ink. It's all CSS computed
28
+ from the theme's tokens, so presets, customizer looks and an app's own
29
+ `--primary` carry into it. Steps follow the mode: `bg-blue-100
30
+ text-blue-800` reads in light and dark. `--theme-palette-harmony` (new
31
+ `PALETTE_TOKENS`, accepted in presets) sets how far the families lean, and
32
+ `PALETTE_COLORS`, `PALETTE_SCALES` and `PALETTE_STEPS` export the names.
33
+ See DESIGN.md → Generated palette.
34
+ - **New: Badge, Pill and StatusDot take a palette family as `tone`.** For
35
+ a category with no meaning attached (a tag, a label, a project colour):
36
+ `<Badge tone="teal">Research</Badge>` or `<Pill tone="blue" selected>`.
37
+ Badge draws a family in every appearance. A Pill rests on the family's
38
+ wash and turns solid when selected. They paint from new `palette-*` slots
39
+ that `data-palette="<family>"` fills (`PALETTE_SLOT_TOKENS`), so no
40
+ component names a step, and an app can use the same hook. The
41
+ `sable token palette` command and `llms/reference/token-intent.md`
42
+ document the palette for agents.
43
+ - **sableui.com's Customize panel has a Harmony control:** how far the
44
+ generated families lean toward the brand, from 0% to 200%. It's exported
45
+ as `--theme-palette-harmony`.
46
+ - **Breaking: Tailwind's other colour families are switched off**, and
47
+ `neutral`, `red` and the rest now paint the theme's palette, not
48
+ Tailwind's. `slate`, `gray`, `zinc`, `stone`, `amber`, `lime`, `emerald`,
49
+ `cyan`, `sky`, `indigo`, `violet`, `fuchsia` and `rose` generate nothing.
50
+ `sable-lint`'s `palette-ref` now allows the generated palette in apps and
51
+ still flags theme-internal ramps and the switched-off families. Components
52
+ still use roles only.
53
+
54
+ - **Breaking: `edition`, `collectible` and `standard` are now tones `1`,
55
+ `2` and `3`.** They were named for one product. They are the three extra
56
+ colours a theme defines beyond its status colours, and the product decides
57
+ what each means. Rename tokens `--tone-edition` → `--tone-1`,
58
+ `--tone-collectible` → `--tone-2` and `--tone-standard` → `--tone-3`, with
59
+ their `-foreground` and `-dark` partners and utilities (`bg-tone-1`,
60
+ `text-tone-1-foreground`). Rename the `tone` prop values the same way
61
+ (`tone="edition"` → `tone="1"`) on Badge, Banner, StatusDot, Meter,
62
+ SegmentedBar, CheckboxCard, RadioCard, GradientArt's `tones` and the chart
63
+ series. Tone 1 is the lead: the strongest of the three, and the first in a
64
+ chart's default cycle. Saved presets that use the old names are read under
65
+ the new ones, so they keep working.
66
+ - **Blocks, patterns and examples use generic content.** The demo data was
67
+ written for an art marketplace (editions, collectors, mints, SOL prices).
68
+ It is now an ordinary SaaS workspace and store. Only copy changed.
69
+ - **sableui.com's Color page follows the customizer.** Its tone swatches
70
+ re-declared the preset on themselves, so a customized brand never reached
71
+ them.
72
+
73
+ Vertical rhythm: spacing is measured from the type.
74
+
75
+ - **The baseline comes from the type.** `--baseline` is one line of body
76
+ text (size × leading) and `--baseline-step` divides it into thirds, 8px at
77
+ the default 15/24. Line heights round to the step (they rounded to 0.25rem),
78
+ and `--spacing` is half a step, which at Sable's defaults is exactly the
79
+ 0.25rem it was, so the default theme does not move. `--rhythm-unit` is one
80
+ step and `--grid-gutter` is 2 or 3 steps, snapped. It is a rhythm, not a
81
+ lock: nothing pins text to lines. See DESIGN.md → Vertical rhythm.
82
+ - **A theme picks how open its text is and how finely a line divides:**
83
+ `--theme-leading-scale` and `--theme-baseline-split` (new `RHYTHM_TOKENS`,
84
+ accepted in presets). **Breaking (visual):** Prism and Poppy set
85
+ `--spacing` by hand; they now set a leading scale instead, so their gaps
86
+ follow their type. Prism's body is 16/21 on a 7px step, Poppy's 15/27 on
87
+ 9px. A preset that sets `--spacing` still works, but its gaps no longer
88
+ follow its type.
89
+ - **Card's edge and row rules take no space.** The edge is an inset outline
90
+ and CardList / CardFooter rules are inset shadows, so the padding is whole
91
+ steps. A Card `className` that set `border-*` colours should set
92
+ `outline-*` instead (the pricing table's featured tier does).
93
+ - **Section and page headers:** the title-to-description gap is one step
94
+ (`gap-2`, was `gap-1`).
95
+ - **Lists:** `sable-list` items are one step apart (were 6px), `sable-list-dl`
96
+ rows two steps (were 12px).
97
+ - **DisclosurePickerOption takes `onRemove`** (and `removeLabel`), for
98
+ options the person added: a trash glyph at the row's end on hover or focus,
99
+ and Delete or Backspace on the focused row. Removing never picks.
100
+ - **sableui.com:** an imported preset can be removed from the Customize
101
+ panel's preset list. A page whose code fails to load (a tab opened before a
102
+ deploy) reloads onto the new deploy instead of going blank, and any page
103
+ that still fails says so with a Reload button.
104
+ - **sableui.com:** pages load into a Skeleton of their own layout (the
105
+ header, the first section, the canvas grid, the homepage hero) instead of a
106
+ centred spinner, so the content lands where the placeholder stood.
107
+ - **sableui.com:** Customize's Character pad moves the leading (Compact ↔
108
+ Airy) and every gap follows; Advanced shows the line and step and its step
109
+ anchor sets the step directly. Grid & layout shows the line and step live,
110
+ and no longer claims the grid folds 12 → 6 → 4 → 2 → 1 by itself.
111
+
3
112
  ## 0.9.1
4
113
 
5
114
  Fixes from a review of 0.9.0.
package/CONTRIBUTING.md CHANGED
@@ -8,7 +8,7 @@ The checks are the specification. Every rule below names the gate that enforces
8
8
 
9
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
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.
11
+ 1. **Components reference semantic tokens only.** Use `bg-card`, `text-muted-foreground`, `bg-tone-1`, `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
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
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
14
  4. **Semantic icons in components.** Use `<Icon name="…">`. Concrete icon-package imports belong only in `src/icon-sets/`, which nothing in the core graph imports. Apps supply the set through `<IconProvider>` (Reicon from `@sableui/react/icons/reicon`, or their own).
@@ -121,7 +121,7 @@ A theme is a CSS file that defines every token in `themes/_contract.ts` for `:ro
121
121
 
122
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
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`.
124
+ `_contract.ts` exports five token groups: `SEMANTIC_TOKENS` (colors — **light + dark**, e.g. `--background`, `--primary`, `--tone-1`), `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
125
 
126
126
  `TYPE_REFINEMENT_TOKENS` additionally exposes optional typography hooks for body,
127
127
  label, caption, mono, title sizes, and the largest display size. Retrieve
package/DESIGN.md CHANGED
@@ -54,7 +54,7 @@ Three layers, outermost is what components use:
54
54
  | Layer | Example | Who uses it |
55
55
  |---|---|---|
56
56
  | **Palette** (theme-internal) | `--purple-heart-700`, `--zinc-950` | Only the theme CSS. Never components. |
57
- | **Semantic** (the contract) | `--primary`, `--card`, `--muted-foreground`, `--tone-edition`, `--ring` | **Components** (via `bg-*`/`text-*` utilities). |
57
+ | **Semantic** (the contract) | `--primary`, `--card`, `--muted-foreground`, `--tone-1`, `--ring` | **Components** (via `bg-*`/`text-*` utilities). |
58
58
  | **Component** (local) | a cva variant string | Inside one component. |
59
59
 
60
60
  `themes/_contract.ts` enumerates the semantic tokens every theme must define. The default (Sable) palette is authored in **OKLCH** for perceptually-uniform ramps, locked hue, gamut-safe chroma, and dark-mode vibrancy.
@@ -62,9 +62,9 @@ Three layers, outermost is what components use:
62
62
  ### Semantic tokens (the contract)
63
63
 
64
64
  Surfaces & text: `background`, `foreground`, `card`, `card-foreground`, `popover`, `popover-foreground`, `muted`, `muted-foreground`, `border`, `input`, `ring`.
65
- Actions: `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `success`, `success-foreground`. (`success` is the positive-state counterpart to `destructive` — a **flat** semantic token backed by caribbean-green; it is deliberately separate from the `tone-standard` post-type, and there is no `tone-success`.)
66
- Tones (semantic, carry meaning): `tone-standard`, `tone-collectible`, `tone-edition`, `tone-info`, `tone-warning`, `highlight`.
67
- Tone ink (legible text/icon on a tone fill, theme-adaptive): `tone-standard-foreground`, `tone-collectible-foreground`, `tone-edition-foreground`, `tone-info-foreground`, `tone-warning-foreground`, `highlight-foreground`.
65
+ Actions: `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `success`, `success-foreground`. (`success` is the positive-state counterpart to `destructive` — a **flat** semantic token backed by caribbean-green; it is deliberately separate from the numbered tones, and there is no `tone-success`.)
66
+ Tones (semantic, carry meaning): `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `highlight`.
67
+ Tone ink (legible text/icon on a tone fill, theme-adaptive): `tone-1-foreground`, `tone-2-foreground`, `tone-3-foreground`, `tone-info-foreground`, `tone-warning-foreground`, `highlight-foreground`.
68
68
  Overlay (fixed dark surface over content — scrims, media letterbox, pills on imagery): `overlay`, `overlay-foreground`.
69
69
  Sidebar set: `sidebar`, `sidebar-foreground`, `sidebar-primary(-foreground)`, `sidebar-accent(-foreground)`, `sidebar-border`, `sidebar-ring`.
70
70
 
@@ -72,12 +72,25 @@ Sidebar set: `sidebar`, `sidebar-foreground`, `sidebar-primary(-foreground)`, `s
72
72
 
73
73
  ### Tone system
74
74
 
75
- Tones encode meaning, not decoration. Choose item categories for your product:
76
- - `tone-standard` — the default item category.
77
- - `tone-collectible` — a distinct item or collection category.
78
- - `tone-edition` — a limited edition or premium category.
75
+ Tones encode meaning, not decoration. Beyond the status colours, a theme defines three extra colours, numbered so the product, not the library, decides what each means (plans, categories, series):
76
+ - `tone-1` — the lead extra colour: the most prominent category, and chart series 1.
77
+ - `tone-2` — a second, visually distinct category.
78
+ - `tone-3` — a third category, usually the quietest: the baseline that means "nothing special".
79
79
  Plus `tone-info`, `tone-warning`, and `highlight` (selection). Dark mode uses dedicated, higher-chroma values so tones stay vivid on a near-black canvas. Text/icons ON a tone fill use the matching `tone-*-foreground` ink — a theme-adaptive intent token that flips lightness with the fill (light ink over the dark 600/700 fills in light mode; dark ink over the lighter dark-tuned fills in dark mode). A component never names a palette step for this; there is no longer a "media-overlay palette exception."
80
80
 
81
+ ### Generated palette
82
+
83
+ Roles are the contract, and roles are not enough for a product: tags, avatars, labels and illustration need more colours than three tones. Raw Tailwind steps ignored the theme, so they were banned outright. Sable now generates a palette from whatever theme is painting (`src/lib/palette.ts`, written to `palette.css`):
84
+
85
+ - **Nine families:** `neutral`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple` and `pink`, each 50–950, as Tailwind utilities (`bg-blue-100`, `text-pink-700`) and variables (`--teal-500`). Tailwind's other families are switched off.
86
+ - **The lean.** A family keeps its own hue and leans toward the brand by half the gap, at most 15°, and only as far as the brand has colour (an ink-black primary pulls nothing). It's Material 3's harmonize, and the same lean the customizer gives status colours. `--theme-palette-harmony` scales it: 0 keeps the families' own hues, 2 doubles the pull.
87
+ - **Neutral** is the theme's own `--background` mixed toward its `--foreground`, so it carries the surfaces' tint.
88
+ - **Tone scales.** Every tone (tone-1…3, info, warning, success, destructive) gets a scale anchored on itself: step 500 is the tone, so `bg-tone-1-100` is its wash.
89
+ - **Steps follow the mode.** A low step is always near the page and a high step always far from it, so a pair like `bg-blue-100 text-blue-800` reads in both modes with no `dark:`.
90
+ - **Held, not hoped.** It's all relative-colour CSS computed where it is declared, so a preset, a customizer look or an app's `--primary` carries into it with no regeneration. Chroma is fitted under sRGB across the whole lean (piecewise, a knot every 5°), and `palette.test.ts` evaluates the shipped CSS for every built-in theme and mode: in gamut, steadily away from the page, `700` readable on the page, `800` readable on its own `100`, and neighbouring families apart.
91
+
92
+ Components never name a step. They paint roles, which is what lets a theme restyle them. Where a component takes a family (Badge, Pill and StatusDot accept `tone="blue"`, a category with no meaning attached), it sets `data-palette` to the family and paints from the slots that fills: `palette-wash` (100), `palette-wash-strong` (200), `palette-edge` (300), `palette-dot` (500), `palette-solid` (700), `palette-text` (800) and `palette-solid-ink` (50). The classes are the same for every family, and each ink-on-fill pair is held to 4.5:1. An app can use `data-palette` the same way.
93
+
81
94
  **Status-tone vocabulary.** Every status-signaling component (StatusDot, Badge with `dot`, Note, Banner, InlineStatus, EmptyState) takes one canonical severity set on its `tone` prop: **`neutral` · `info` · `success` · `warning` · `destructive`** — exported as `STATUS_TONES` / `StatusTone`, with one icon per tone in `STATUS_TONE_ICON`. There is no `error` alias anywhere: use `destructive`. A state label (live, failed, draft) is `<Badge appearance="plain" tone="…" dot>`; map domain states (`ready`, `building`, `failed`, …) to tones once in the app. Consumers can therefore feed one app-level tone straight into any of these components without a translation shim; new status-flavored components must accept the canonical five (`tone-vocabulary.test.ts` holds this).
82
95
 
83
96
  ## Registers
@@ -309,7 +322,7 @@ Default theme families: **Figtree** (sans) + **DM Mono** (mono). Two weights per
309
322
 
310
323
  ## Spacing
311
324
 
312
- One scale, built on a **4px base** with an **8px rhythm**. Every margin, padding, and gap is a step on it — never a hand-picked px value — so vertical and horizontal rhythm stay locked together system-wide. Spacing, not borders, is the primary tool for grouping: **related things sit closer than unrelated things** (proximity does the work a divider line otherwise would).
325
+ One scale, built on the **baseline**: one line of body text, split into steps (see *Vertical rhythm* below — 8px at the default 15/24 body, with Tailwind's spacing step half of it). Every margin, padding, and gap is a step on it — never a hand-picked px value — so vertical and horizontal rhythm stay locked together system-wide. Spacing, not borders, is the primary tool for grouping: **related things sit closer than unrelated things** (proximity does the work a divider line otherwise would).
313
326
 
314
327
  **The scale** (Tailwind's spacing utilities; the steps Sable actually leans on):
315
328
  `0` · `0.5` 2 (hairline / optical nudge) · `1` 4 · `1.5` 6 · `2` 8 (**the base rhythm unit**) · `3` 12 · `4` 16 (default control padding / gap) · `5` 20 · `6` 24 (group separation) · `8` 32 · `10` 40 · `12` 48 (sub-sections) · `16` 64 · `24` 96 (major sections). Steps below `2` are for optical correction only.
@@ -319,7 +332,8 @@ One scale, built on a **4px base** with an **8px rhythm**. Every margin, padding
319
332
  - **Stack** — vertical gap *between* blocks, the dominant rhythm: tight within a group (`gap-1.5`–`gap-2`), looser between groups (`gap-4`–`gap-6`), generous between sections (`gap-12`+).
320
333
  - **Inline** — horizontal gap between siblings (icon↔label, label↔control): `gap-2`–`gap-3`.
321
334
  - **Density** — a compact surface drops one step (inset `p-3`, stack `gap-1.5`); a spacious one adds one. Move in whole steps, never a custom px.
322
- - **On the scale, so density follows.** The docs' density ("Character") control and a consumer's override retune `--spacing`; every step and every `--spacing(n)` moves with it. A component's padding, gap or inset written as a literal (`p-[1.5rem]`, `padding: "12px"`) is the one measure that stays put. Even derived geometry sits on the scale: `popup-inset` floors at `--spacing(1)`, `sable-frame` pads `--spacing(4)`, Card's `--card-p` is `--spacing(6)` / `--spacing(4)`. Fixed units are for what must not scale (a 1px hairline, a ring width). *(CI-enforced in the package: `fixed-spacing`.)* Layout primitives follow too: `--rhythm-unit` is two spacing steps, so a `Stack`/`Row`/`Grid`/`Page` gap number moves with density like everything else.
335
+ - **On the scale, so density follows.** `--spacing` derives from the baseline step, and the docs' density ("Character") control moves the leading the step comes from; every step and every `--spacing(n)` moves with it. A component's padding, gap or inset written as a literal (`p-[1.5rem]`, `padding: "12px"`) is the one measure that stays put. Even derived geometry sits on the scale: `popup-inset` floors at `--spacing(1)`, `sable-frame` pads `--spacing(4)`, Card's `--card-p` is `--spacing(6)` / `--spacing(4)`. Fixed units are for what must not scale (a 1px hairline, a ring width). *(CI-enforced in the package: `fixed-spacing`.)* Layout primitives follow too: `--rhythm-unit` is one baseline step (two spacing steps), so a `Stack`/`Row`/`Grid`/`Page` gap number moves with density like everything else.
336
+ - **Vertical space in even steps, by habit.** With thirds, an even Tailwind step (`2`, `4`, `6`, `8`, `12`) is whole baseline steps; an odd one (`3`, `5`) is a half step. Inside a control it does not matter; between blocks prefer even steps.
323
337
 
324
338
  **Don't** invent off-scale values (`p-[15px]`), simulate a `gap` with per-child margins, or hang layout spacing on a text node (put it on the container).
325
339
 
@@ -342,7 +356,9 @@ The same 4px scale governs **sizes**, so controls, icons, and avatars all land o
342
356
 
343
357
  ## Grid & layout (fluid-first, no global max-width)
344
358
 
345
- Sable's layout foundation is deliberately fluid: **edge-to-edge, with a fluid page inset** rather than a centered fixed-width column. It keeps the surviving grid discipline — register every element to a column edge and an 8px (two spacing steps) vertical rhythm, cap the reading measure — while letting the canvas breathe across viewports and native shells.
359
+ Sable's layout foundation is deliberately fluid: **edge-to-edge, with a fluid page inset** rather than a centered fixed-width column. It keeps the surviving grid discipline — register every element to a column edge and to the baseline rhythm, cap the reading measure — while letting the canvas breathe across viewports and native shells.
360
+
361
+ **Flex or grid.** One direction is flex — `Stack` (column) and `Row` (row). Content that must line up across rows *and* columns is `Grid`/`Columns`. Width is a `Region`. A card's single column stacks with `Stack`, not a one-track grid (a grid track sizes to its widest min-content child). Grids are a tool for structure, not a requirement: a route with one reading measure needs no columns, and says so in its brief.
346
362
 
347
363
  **The doctrine — two roles, never conflated:**
348
364
  1. **A page's width is a `Region`** — a centered, opt-in, purpose-named cap. Pick by ROLE, not size.
@@ -351,9 +367,9 @@ Sable's layout foundation is deliberately fluid: **edge-to-edge, with a fluid pa
351
367
  4. **Measure ≠ width.** Cap text line-length (`prose`, in `ch`) independently of a region's layout width — a 1280px dashboard can still hold a 65ch reading column.
352
368
 
353
369
  **Layout tokens:**
354
- - `--rhythm-unit: 8px` — the relaxed baseline; every vertical measure is a multiple.
355
- - `--grid-columns: 12` — the proportioning resource; folds 12 → 6 → 4 → 2 → 1.
356
- - `--grid-gutter: clamp(1rem, …, 1.5rem)` — column gap, fluid 16 → 24px.
370
+ - `--rhythm-unit` — one baseline step (8px at the default type); every vertical measure is a multiple. Derived, never set: see *Vertical rhythm*.
371
+ - `--grid-columns: 12` — the proportioning resource. It does not fold on its own: pass a breakpoint map where the layout should change (`cols={{ base: 1, md: 6, lg: 12 }}`, `span={{ base: 12, lg: 8 }}`).
372
+ - `--grid-gutter` — column and row gap: 2 baseline steps on phones, 3 on desktop, snapped to the step.
357
373
  - `--page-inset: clamp(16px, 4vw, 32px)` — fluid page margin; honors `env(safe-area-inset-*)`.
358
374
 
359
375
  **Content-width scale** — opt-in per `Region` (`max="…"`), named by purpose (**not** page-global):
@@ -375,6 +391,29 @@ Sable's layout foundation is deliberately fluid: **edge-to-edge, with a fluid pa
375
391
 
376
392
  Breakpoints are content-driven: sm 640 / md 768 / lg 1024 / xl 1280 / 2xl 1536. Keep reading + focus order logical; survive 400% zoom.
377
393
 
394
+ ## Vertical rhythm
395
+
396
+ The page is set in **lines**, the way print is. The unit is one line of body text — body size × leading, `--baseline` — divided into equal **steps**, `--baseline-step`, that vertical space is measured in. It is derived, never set: a design with larger type has a larger line, and its gaps grow with it.
397
+
398
+ ```
399
+ --baseline-step = round(body size × body leading × --leading-scale ÷ --baseline-split, 1px)
400
+ --baseline = --baseline-step × --baseline-split
401
+ --spacing = --baseline-step ÷ 2 (so p-4 is two steps)
402
+ line-height = round(tier ratio × 1em, --baseline-step)
403
+ ```
404
+
405
+ At Sable's defaults (15px body on 1.55) that is a 24px line in 8px thirds, and `--spacing` comes out at exactly Tailwind's 0.25rem — nothing moved until the type does. On the docs' 14px root it is 21px in 7px steps.
406
+
407
+ **What a theme chooses.** `--theme-baseline-split` — steps per line, thirds by default; halves read calmer and more editorial, and finer than thirds stops giving structure unless every value is chosen by eye. `--theme-leading-scale` — how open the reading text is set; Prism sets 0.86 (a 7px step), Poppy 1.1 (a 9px step). Density *is* leading: the customizer's Compact ↔ Airy moves `--leading-scale`, the line grows, and every gap, gutter and control gap follows. Setting `--spacing` by hand pulls the gaps off the lines.
408
+
409
+ **A rhythm, not a lock.** The baseline is where spacing *comes from*, not a grid every letter is forced onto. Line heights are whole steps and the spacing scale is in steps, so a page built from Sable's tiers and gaps falls into a steady rhythm on its own. It is not enforced past that: content-sized things (a card with a 40px button, a code block, a demo stage) are as tall as they are, and pinning every baseline to a line on a responsive page breaks layouts rather than calming them.
410
+
411
+ **Habits that keep the rhythm:**
412
+ - Vertical space between blocks in even Tailwind steps (`gap-2`, `gap-4`, `py-6`, `mt-12`) or `Stack`/`Row`/`Grid` gap numbers. An odd step is half a baseline step — fine inside a control, a small drift between blocks.
413
+ - Title to description one step; between groups one to two lines; between sections three or more.
414
+ - A container's edge takes no space where it can: Card's edge is an inset outline and its row rules are inset shadows, so its padding stays whole steps.
415
+ - One type size per line of text: a span of another tier inside a paragraph makes the line taller than its line height.
416
+
378
417
  ## Shape & borders
379
418
 
380
419
  **Radii.** Tight radii, pill actions, soft-square containers. `none` (media) · `xs` 4 · `sm` 8 (inputs) · `md` 12 (popovers/toasts) · `lg` 16 (cards/dialogs/sheets) · `xl` 20 (hero) · `full` (buttons, avatars, pills). Never mix radii within one component; actions are always `full` by default. Radius reads as **tone**: tighter corners feel precise/technical, fuller corners feel soft/friendly — Sable stays tight on containers and full on actions, so the contrast itself signals "this is a control." `Button`'s corner radius is theme-driven through a dedicated `--button-radius` var (falls back to full pill; see `button.variants.ts`) — a reference with a rectilinear shape language can flatten buttons without touching every other `rounded-full` use (avatars, pills, badges, dots) that should stay round.
@@ -471,7 +510,7 @@ The rules above are not honor-system. **`design-lint`** (`scripts/design-lint.mj
471
510
  | Rule id | Catches |
472
511
  |---|---|
473
512
  | `inline-color` | inline `oklch()` / hex literals |
474
- | `palette-ref` | raw palette-step refs (`--zinc-950`, `text-(--purple-heart-700)`) — **zero sanctioned exceptions** since the tone-foreground + overlay tokens landed |
513
+ | `palette-ref` | palette-step refs. In a component, any of them (`--zinc-950`, `bg-blue-500`, `bg-tone-1-100`) — **zero sanctioned exceptions** since the tone-foreground + overlay tokens landed. In an app, only the ones that do not follow the theme: a theme's internal ramps and the Tailwind families Sable switches off (`bg-lime-500`) |
475
514
  | `raw-black-white` | `bg-black` / `text-white` etc. (use `overlay` / `overlay-foreground` or a semantic token) |
476
515
  | `transition-colors` | bare `transition-colors` (use the `motion-interactive` recipe) |
477
516
  | `transition-all` | `transition-all` / `transition: all` — it animates every property, including ones you never meant to move, and blocks the browser from optimizing. Name the properties, or use the recipe |
@@ -526,7 +565,7 @@ Not every removal is a regression; check for an **equivalent replacement** befor
526
565
  - Inline `oklch()`/hex or raw palette steps in components. *(CI-enforced: `inline-color`, `palette-ref`.)*
527
566
  - Decomposing paired typography utilities. *(CI-enforced: `decomposed-type`.)*
528
567
  - Pure `#000`/`#fff` (use `zinc-950`/`zinc-50`, or `overlay`/`overlay-foreground` for scrims). *(CI-enforced: `raw-black-white`.)*
529
- - In the Sable theme, purple/brand for primary CTAs (brand = editions + selection only). A different theme chooses its action palette through the semantic pair.
568
+ - In the Sable theme, purple/brand for primary CTAs (brand = tone-1 + selection only). A different theme chooses its action palette through the semantic pair.
530
569
  - Gradient text, side-stripe borders, nested cards, mixed radii.
531
570
  - Concrete icon-package imports scattered through components. Reicon is an opt-in set behind the shared adapter; the app's IconProvider supplies it or a replacement.
532
571
  - A global page max-width (use opt-in region caps).
package/README.md CHANGED
@@ -27,6 +27,18 @@ in Sable.
27
27
 
28
28
  A broad product component catalog, a fluid layout foundation, and a pluggable icon adapter — see [`registry.json`](./registry.json).
29
29
 
30
+ ## Colour
31
+
32
+ Components paint roles (`bg-card`, `bg-primary`, `text-muted-foreground`), so a
33
+ theme restyles everything at once. For colour beyond the roles, every theme also
34
+ generates a palette: `neutral`, `red`, `orange`, `yellow`, `green`, `teal`,
35
+ `blue`, `purple` and `pink` (50–950), plus a scale under every tone. The
36
+ families lean toward the theme's brand, neutral is the theme's own page-to-ink
37
+ ramp, and steps follow the mode, so `bg-blue-100 text-blue-800` reads in light and
38
+ dark. Use it for tags, avatars, labels and illustration; Badge, Pill and StatusDot
39
+ take a family as their tone (`<Badge tone="teal">`). Tailwind's other colour
40
+ families are switched off. See DESIGN.md → Generated palette.
41
+
30
42
  ## Taste, style, and variety
31
43
 
32
44
  A project can keep its own visual atlas at `.sable/atlas/atlas.json`: annotated
@@ -7,7 +7,7 @@
7
7
  * To author a new theme: copy `themes/sable/`, keep these token names, and
8
8
  * supply your own palette + values. See CONTRIBUTING.md → "Theme contract".
9
9
  */
10
- declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-standard", "tone-collectible", "tone-edition", "tone-info", "tone-warning", "highlight", "tone-standard-foreground", "tone-collectible-foreground", "tone-edition-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "destructive-text", "success-text", "tone-warning-text", "tone-info-text", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
10
+ declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-3", "tone-2", "tone-1", "tone-info", "tone-warning", "highlight", "tone-3-foreground", "tone-2-foreground", "tone-1-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "destructive-text", "success-text", "tone-warning-text", "tone-info-text", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
11
11
  type SemanticToken = (typeof SEMANTIC_TOKENS)[number];
12
12
  /**
13
13
  * Motion tokens (mode-independent). A theme MAY override these to retune its
@@ -59,6 +59,20 @@ type TypeRefinementToken = (typeof TYPE_REFINEMENT_TOKENS)[number];
59
59
  */
60
60
  declare const TYPE_OVERLINE_TOKENS: readonly ["type-overline-size", "type-overline-weight", "type-overline-leading", "type-overline-tracking", "type-overline-transform"];
61
61
  type TypeOverlineToken = (typeof TYPE_OVERLINE_TOKENS)[number];
62
+ /**
63
+ * Vertical rhythm hooks (mode-independent). The baseline itself is DERIVED in
64
+ * styles.css from the body text (size × leading), never set: a theme shapes it
65
+ * by how open it sets its reading text and how finely it divides a line.
66
+ *
67
+ * - `theme-leading-scale` — a multiple of the reading tiers' leading. Below 1
68
+ * is a denser theme, above 1 an airier one; the step and every gap follow.
69
+ * - `theme-baseline-split` — steps per line. 3 (thirds) by default; 2 reads
70
+ * calmer and more editorial.
71
+ *
72
+ * See DESIGN.md → Vertical rhythm.
73
+ */
74
+ declare const RHYTHM_TOKENS: readonly ["theme-leading-scale", "theme-baseline-split"];
75
+ type RhythmToken = (typeof RHYTHM_TOKENS)[number];
62
76
  /**
63
77
  * Density tokens (mode-independent) — the control ladder every sized component
64
78
  * renders from.
@@ -124,6 +138,15 @@ type WorkspaceSurfaceToken = (typeof WORKSPACE_SURFACE_TOKENS)[number];
124
138
  * …)`) where the art direction allows, so a brand change carries into the art.
125
139
  */
126
140
  declare const ART_TOKENS: readonly ["art-1", "art-2", "art-3", "art-4", "art-5", "art-6", "art-ground", "art-warp", "art-softness", "art-grain"];
141
+ /**
142
+ * The generated palette's one hook (src/lib/palette.ts). A theme sets it as
143
+ * `--theme-palette-harmony`; it is optional, and derived like the art tokens.
144
+ *
145
+ * - `palette-harmony` is how far the colour families lean toward the brand:
146
+ * 0 keeps their own hues, 1 (the default) is half the gap capped at 15°, and
147
+ * 2 doubles it, for a theme that wants every colour to read as its own.
148
+ */
149
+ declare const PALETTE_TOKENS: readonly ["palette-harmony"];
127
150
  /**
128
151
  * UI type tiers (mode-independent) — the chrome typography register.
129
152
  *
@@ -170,4 +193,4 @@ interface ThemeManifest {
170
193
  };
171
194
  }
172
195
 
173
- export { ART_TOKENS as A, DENSITY_TOKENS as D, MOTION_TOKENS as M, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, type DensityToken as b, type MotionToken as c, type RadiusToken as d, type SemanticToken as e, TYPE_OVERLINE_TOKENS as f, TYPE_REFINEMENT_TOKENS as g, TYPE_SCALE_TOKENS as h, TYPE_UI_TOKENS as i, type TypeOverlineToken as j, type TypeRefinementToken as k, type TypeScaleToken as l, type TypeUiToken as m, type WorkspaceSurfaceToken as n };
196
+ export { ART_TOKENS as A, DENSITY_TOKENS as D, MOTION_TOKENS as M, PALETTE_TOKENS as P, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, type DensityToken as b, type MotionToken as c, RHYTHM_TOKENS as d, type RadiusToken as e, type RhythmToken as f, type SemanticToken as g, TYPE_OVERLINE_TOKENS as h, TYPE_REFINEMENT_TOKENS as i, TYPE_SCALE_TOKENS as j, TYPE_UI_TOKENS as k, type TypeOverlineToken as l, type TypeRefinementToken as m, type TypeScaleToken as n, type TypeUiToken as o, type WorkspaceSurfaceToken as p };
@@ -5,14 +5,14 @@ import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "../com
5
5
  import { DestructiveActionModal } from "../components/destructive-action-modal/index.js";
6
6
  import { cn } from "../lib/cn.js";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
- var account = { handle: "ada", editions: 12 };
8
+ var account = { handle: "ada", projects: 12 };
9
9
  function DangerZoneBlock({ className, framed = true, onDelete }) {
10
10
  const [open, setOpen] = useState(false);
11
11
  return /* @__PURE__ */ jsxs(Fragment, { children: [
12
12
  /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-2xl", className), children: [
13
13
  /* @__PURE__ */ jsxs(CardHeader, { children: [
14
14
  /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Delete account" }),
15
- /* @__PURE__ */ jsx(CardDescription, { children: "Permanently remove your profile, editions, and sales history. Collectors keep the editions they own." })
15
+ /* @__PURE__ */ jsx(CardDescription, { children: "Permanently remove your profile, projects, and billing history. Files you shared stay with your team." })
16
16
  ] }),
17
17
  /* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => setOpen(true), children: "Delete account" }) })
18
18
  ] }),
@@ -22,7 +22,7 @@ function DangerZoneBlock({ className, framed = true, onDelete }) {
22
22
  open,
23
23
  onOpenChange: setOpen,
24
24
  title: "Delete your account?",
25
- description: `This permanently removes your profile, ${account.editions} editions, and sales history. Pending payouts are still sent. This can't be undone.`,
25
+ description: `This permanently removes your profile, ${account.projects} projects, and billing history. Your final invoice is still sent. This can't be undone.`,
26
26
  confirmLabel: "Delete account",
27
27
  confirmingLabel: "Deleting\u2026",
28
28
  confirmText: account.handle,
@@ -9,7 +9,7 @@ var dangerZoneBlockMeta = {
9
9
  notes: [
10
10
  "Copied from the SettingsPage page shell's Delete account section.",
11
11
  "The button never acts directly: it opens the modal, whose confirm stays disabled until the handle is typed.",
12
- 'The description says what is lost AND what is kept ("Collectors keep the editions they own"), which is what people want to know before they decide.'
12
+ 'The description says what is lost AND what is kept ("Files you shared stay with your team"), which is what people want to know before they decide.'
13
13
  ],
14
14
  a11y: [
15
15
  "DestructiveActionModal is an alertdialog: focus opens on Cancel and the backdrop does not dismiss it.",
@@ -10,10 +10,10 @@ var intro = {
10
10
  contact: { label: "Write to us", href: "/contact" }
11
11
  };
12
12
  var questions = [
13
- { q: "Is there a free plan?", a: "Yes. Starter is free forever for three live editions, with no fees on your first $1,000 in sales." },
13
+ { q: "Is there a free plan?", a: "Yes. Starter is free forever for three active projects, with no card needed to sign up." },
14
14
  { q: "Can I change plans later?", a: "Any time. Upgrades apply immediately and are prorated; downgrades take effect at the end of the billing period." },
15
- { q: "Who owns the work I sell?", a: "You do. Collectors own the editions they buy, and you keep the rights to the work itself." },
16
- { q: "How do payouts work?", a: "Sales clear after seven days and are paid out every Friday to the account you connect in settings." }
15
+ { q: "Who owns the data I add?", a: "You do. You can export everything in your workspace at any time, and we never sell it." },
16
+ { q: "How does billing work?", a: "Plans are billed per seat at the start of each period, to the card or account you add in settings." }
17
17
  ];
18
18
  function FaqBlock({ className }) {
19
19
  const titleId = useId();
@@ -5,7 +5,7 @@ import { cn } from "../lib/cn.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  var goals = [
7
7
  { goal: "Releases", progress: 82 },
8
- { goal: "Collectors", progress: 64 },
8
+ { goal: "Customers", progress: 64 },
9
9
  { goal: "Revenue", progress: 91 }
10
10
  ];
11
11
  function GoalProgressBlock({ className, framed = true }) {
@@ -6,7 +6,7 @@ import { StatusDot } from "../components/status-dot/index.js";
6
6
  import { cn } from "../lib/cn.js";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  var notifications = [
9
- { initials: "MO", name: "Mara Okafor", what: "collected your edition", when: "2m", unread: true },
9
+ { initials: "MO", name: "Mara Okafor", what: "invited you to Q3 planning", when: "2m", unread: true },
10
10
  { initials: "SR", name: "Sam Rivera", what: "left a comment on Dusk", when: "1h", unread: false }
11
11
  ];
12
12
  function NotificationListBlock({ className, framed = true }) {
@@ -12,32 +12,32 @@ var tiers = [
12
12
  {
13
13
  id: "starter",
14
14
  name: "Starter",
15
- blurb: "For one person shipping their first editions.",
15
+ blurb: "For one person getting a first project going.",
16
16
  monthly: 0,
17
17
  yearly: 0,
18
18
  cta: "Start for free",
19
19
  featured: false,
20
- features: ["3 live editions", "Collector profiles", "Community support"]
20
+ features: ["3 active projects", "Shared docs", "Community support"]
21
21
  },
22
22
  {
23
23
  id: "studio",
24
24
  name: "Studio",
25
- blurb: "For working artists with a steady release schedule.",
25
+ blurb: "For small teams shipping on a steady schedule.",
26
26
  monthly: 20,
27
27
  yearly: 16,
28
28
  cta: "Start free trial",
29
29
  featured: true,
30
- features: ["Unlimited editions", "Scheduled drops", "Sales analytics", "Priority support"]
30
+ features: ["Unlimited projects", "Scheduled reports", "Usage analytics", "Priority support"]
31
31
  },
32
32
  {
33
- id: "collective",
34
- name: "Collective",
35
- blurb: "For galleries and teams releasing together.",
33
+ id: "business",
34
+ name: "Business",
35
+ blurb: "For companies running several teams at once.",
36
36
  monthly: 48,
37
37
  yearly: 40,
38
38
  cta: "Talk to sales",
39
39
  featured: false,
40
- features: ["Everything in Studio", "Up to 10 seats", "Shared payouts", "Roles and approvals"]
40
+ features: ["Everything in Studio", "Up to 10 seats", "SSO and audit log", "Roles and approvals"]
41
41
  }
42
42
  ];
43
43
  function PricingTableBlock({ className, onChoose }) {
@@ -45,8 +45,8 @@ function PricingTableBlock({ className, onChoose }) {
45
45
  const titleId = useId();
46
46
  return /* @__PURE__ */ jsx(Region, { as: "section", "aria-labelledby": titleId, max: "wide", className: cn("w-full py-16", className), children: /* @__PURE__ */ jsxs(Stack, { gap: 10, children: [
47
47
  /* @__PURE__ */ jsxs(Stack, { gap: 4, align: "center", className: "text-center", children: [
48
- /* @__PURE__ */ jsx("h2", { id: titleId, className: "text-balance text-heading-1", children: "Pricing that grows with your practice" }),
49
- /* @__PURE__ */ jsx("p", { className: "max-w-(--measure-prose) text-body-lg text-muted-foreground", children: "Every plan includes unlimited collectors and no fees on your first $1,000 in sales." }),
48
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "text-balance text-heading-1", children: "Pricing that grows with your team" }),
49
+ /* @__PURE__ */ jsx("p", { className: "max-w-(--measure-prose) text-body-lg text-muted-foreground", children: "Every plan includes unlimited guests, and you can switch plans at any time." }),
50
50
  /* @__PURE__ */ jsxs(ToggleGroup, { value: [period], onValueChange: (v) => v[0] && setPeriod(v[0]), "aria-label": "Billing period", children: [
51
51
  /* @__PURE__ */ jsx(Toggle, { value: "monthly", children: "Monthly" }),
52
52
  /* @__PURE__ */ jsx(Toggle, { value: "yearly", children: "Yearly \xB7 save 20%" })
@@ -56,12 +56,12 @@ function PricingTableBlock({ className, onChoose }) {
56
56
  Card,
57
57
  {
58
58
  render: /* @__PURE__ */ jsx("article", {}),
59
- className: cn("flex flex-col", tier.featured && "border-primary ring-1 ring-primary"),
59
+ className: cn("flex flex-col", tier.featured && "outline-primary ring-1 ring-primary"),
60
60
  children: [
61
61
  /* @__PURE__ */ jsxs(CardHeader, { children: [
62
62
  /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h3", {}), children: tier.name }),
63
63
  /* @__PURE__ */ jsx(CardDescription, { children: tier.blurb }),
64
- tier.featured ? /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Badge, { tone: "edition", size: "sm", children: "Most popular" }) }) : null
64
+ tier.featured ? /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Badge, { tone: "1", size: "sm", children: "Most popular" }) }) : null
65
65
  ] }),
66
66
  /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-1 flex-col gap-5", children: [
67
67
  /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-1.5", children: [
@@ -6,18 +6,18 @@ import { MediaImage } from "../components/media-image/index.js";
6
6
  import { AspectBox } from "../layout/AspectBox.js";
7
7
  import { cn } from "../lib/cn.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
- var product = { title: "Untitled (gold rush)", maker: "Mara Okafor", image: "", edition: "12 of 50", price: "0.42 SOL" };
9
+ var product = { title: "Linen throw blanket", maker: "Northfield Home", image: "", badge: "New", price: "$64.00" };
10
10
  function ProductCardBlock({ className, framed = true, onCollect }) {
11
11
  return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: [
12
12
  /* @__PURE__ */ jsx(AspectBox, { ratio: 4 / 3, children: /* @__PURE__ */ jsx(MediaImage, { src: product.image, seed: product.title }) }),
13
13
  /* @__PURE__ */ jsxs(CardHeader, { children: [
14
14
  /* @__PURE__ */ jsx(CardTitle, { children: product.title }),
15
15
  /* @__PURE__ */ jsx(CardDescription, { children: product.maker }),
16
- /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Badge, { tone: "edition", children: product.edition }) })
16
+ /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Badge, { tone: "1", children: product.badge }) })
17
17
  ] }),
18
18
  /* @__PURE__ */ jsxs(CardFooter, { className: "justify-between pt-4", children: [
19
19
  /* @__PURE__ */ jsx("span", { className: "text-mono-lg", children: product.price }),
20
- /* @__PURE__ */ jsx(Button, { onClick: onCollect, children: "Collect" })
20
+ /* @__PURE__ */ jsx(Button, { onClick: onCollect, children: "Add to cart" })
21
21
  ] })
22
22
  ] });
23
23
  }
@@ -4,10 +4,10 @@ var productCardBlockMeta = {
4
4
  family: "Commerce",
5
5
  kind: "card",
6
6
  width: { min: "16rem", max: "20rem" },
7
- summary: "A product card: edge-to-edge artwork, title and maker, an edition Badge, and the price beside a single buy action.",
8
- intent: "Reach for this for a product, edition or listing in a grid or a featured slot. For a row in a dense list, use an Entity with the image as `leading` instead.",
7
+ summary: "A product card: edge-to-edge product image, title and maker, a status Badge, and the price beside a single buy action.",
8
+ intent: "Reach for this for a product or listing in a grid or a featured slot. For a row in a dense list, use an Entity with the image as `leading` instead.",
9
9
  notes: [
10
- "The artwork runs to the card's edges; the card clips it to its own corners (overflow-hidden).",
10
+ "The image runs to the card's edges; the card clips it to its own corners (overflow-hidden).",
11
11
  "Prices are `text-mono-lg` \u2014 the metric mono tier with tabular figures \u2014 never a heading tier.",
12
12
  "The placeholder art is painted from the palette's decorative tones so it follows the theme; swap it for the product image with real alt text."
13
13
  ],
@@ -11,7 +11,7 @@ var profile = {
11
11
  name: "Juno Avery",
12
12
  initials: "JA",
13
13
  handle: "@juno \xB7 Sound and light",
14
- bio: "Field recordings, slowed down. New edition every full moon.",
14
+ bio: "Field recordings, slowed down. New track every full moon.",
15
15
  stats: [
16
16
  { value: "128", label: "posts" },
17
17
  { value: "4.2k", label: "followers" }