@sableui/react 0.1.1 → 0.3.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 (137) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +120 -0
  3. package/CONTRIBUTING.md +8 -0
  4. package/README.md +1 -0
  5. package/bin/sable-lint.mjs +0 -0
  6. package/bin/sable.mjs +68 -26
  7. package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
  8. package/dist/blocks/index.d.ts +396 -0
  9. package/dist/blocks/index.js +1997 -0
  10. package/dist/chunk-6LCIBMMG.js +4908 -0
  11. package/dist/{chunk-J23ELQQZ.js → chunk-CKNM7KSB.js} +1664 -4270
  12. package/dist/chunk-K6LII3OE.js +5093 -0
  13. package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
  14. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  15. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  16. package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
  17. package/dist/foundations/index.js +1 -1
  18. package/dist/icon/index.js +2 -2
  19. package/dist/icon-sets/reicon.js +1 -1
  20. package/dist/index.d.ts +624 -133
  21. package/dist/index.js +767 -4964
  22. package/dist/ios/index.d.ts +14 -2
  23. package/dist/ios/index.js +10 -9
  24. package/dist/ios/preview.d.ts +11 -1
  25. package/dist/ios/preview.js +14 -9
  26. package/dist/ios/styles.css +26 -6
  27. package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
  28. package/dist/patterns/index.d.ts +1 -1
  29. package/dist/patterns/index.js +1684 -471
  30. package/dist/presets/index.d.ts +1 -1
  31. package/dist/presets/index.js +1 -1
  32. package/dist/styles.css +238 -40
  33. package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
  34. package/dist/themes/atelier/atelier.css +3 -0
  35. package/dist/themes/atelier/index.d.ts +1 -1
  36. package/dist/themes/base/base.css +34 -4
  37. package/dist/themes/base/index.d.ts +1 -1
  38. package/dist/themes/ember/ember.css +3 -0
  39. package/dist/themes/ember/index.d.ts +1 -1
  40. package/dist/themes/index.d.ts +2 -2
  41. package/dist/themes/index.js +2 -2
  42. package/dist/themes/meridian/index.d.ts +1 -1
  43. package/dist/themes/meridian/meridian.css +10 -5
  44. package/dist/themes/poppy/index.d.ts +1 -1
  45. package/dist/themes/poppy/poppy.css +16 -0
  46. package/dist/themes/prism/index.d.ts +1 -1
  47. package/dist/themes/prism/prism.css +4 -1
  48. package/dist/themes/verdant/index.d.ts +1 -1
  49. package/dist/themes/verdant/verdant.css +7 -2
  50. package/llms/blocks/api-keys-block.md +79 -0
  51. package/llms/blocks/chat-thread-block.md +51 -0
  52. package/llms/blocks/command-palette-block.md +48 -0
  53. package/llms/blocks/cta-band-block.md +62 -0
  54. package/llms/blocks/danger-zone-block.md +69 -0
  55. package/llms/blocks/device-comparison-block.md +91 -0
  56. package/llms/blocks/event-card-block.md +84 -0
  57. package/llms/blocks/faq-block.md +72 -0
  58. package/llms/blocks/feature-grid-block.md +75 -0
  59. package/llms/blocks/feedback-form-block.md +66 -0
  60. package/llms/blocks/goal-progress-block.md +54 -0
  61. package/llms/blocks/hero-block.md +65 -0
  62. package/llms/blocks/invite-team-block.md +61 -0
  63. package/llms/blocks/invoice-history-block.md +84 -0
  64. package/llms/blocks/kpi-sparkline-block.md +57 -0
  65. package/llms/blocks/media-player-block.md +100 -0
  66. package/llms/blocks/newsletter-block.md +64 -0
  67. package/llms/blocks/not-found-block.md +56 -0
  68. package/llms/blocks/notification-list-block.md +63 -0
  69. package/llms/blocks/notification-settings-block.md +54 -0
  70. package/llms/blocks/order-tracking-block.md +62 -0
  71. package/llms/blocks/pricing-card-block.md +82 -0
  72. package/llms/blocks/pricing-table-block.md +129 -0
  73. package/llms/blocks/product-card-block.md +69 -0
  74. package/llms/blocks/profile-card-block.md +103 -0
  75. package/llms/blocks/profile-form-block.md +114 -0
  76. package/llms/blocks/revenue-breakdown-block.md +70 -0
  77. package/llms/blocks/sign-in-block.md +64 -0
  78. package/llms/blocks/sign-up-block.md +78 -0
  79. package/llms/blocks/site-footer-block.md +96 -0
  80. package/llms/blocks/stat-row-block.md +59 -0
  81. package/llms/blocks/steps-block.md +73 -0
  82. package/llms/blocks/task-list-block.md +81 -0
  83. package/llms/blocks/team-members-block.md +91 -0
  84. package/llms/blocks/testimonial-block.md +78 -0
  85. package/llms/blocks/traffic-sources-block.md +63 -0
  86. package/llms/blocks/traffic-trend-block.md +95 -0
  87. package/llms/blocks/upload-list-block.md +66 -0
  88. package/llms/blocks/verify-code-block.md +59 -0
  89. package/llms/blocks/wallet-block.md +84 -0
  90. package/llms/components/area-chart.md +112 -0
  91. package/llms/components/bar-chart.md +131 -0
  92. package/llms/components/bubble.md +5 -5
  93. package/llms/components/button.md +1 -1
  94. package/llms/components/card.md +2 -2
  95. package/llms/components/checkbox-card.md +1 -1
  96. package/llms/components/context-card.md +2 -2
  97. package/llms/components/context-menu.md +1 -1
  98. package/llms/components/date-tile.md +79 -0
  99. package/llms/components/dialog.md +1 -1
  100. package/llms/components/empty-state.md +4 -4
  101. package/llms/components/entity.md +5 -3
  102. package/llms/components/fieldset.md +1 -1
  103. package/llms/components/kbd.md +18 -6
  104. package/llms/components/line-chart.md +101 -0
  105. package/llms/components/menu.md +1 -1
  106. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  107. package/llms/components/pie-chart.md +103 -0
  108. package/llms/components/pill.md +1 -1
  109. package/llms/components/radar-chart.md +106 -0
  110. package/llms/components/radial-chart.md +102 -0
  111. package/llms/components/segmented-bar.md +102 -0
  112. package/llms/components/settings-menu.md +2 -2
  113. package/llms/components/side-nav.md +1 -1
  114. package/llms/components/skeleton.md +1 -1
  115. package/llms/components/sparkline.md +80 -0
  116. package/llms/components/table.md +1 -1
  117. package/llms/components/theme-selector.md +1 -1
  118. package/llms/foundations/shape.md +1 -1
  119. package/llms/patterns/auth-page.md +132 -42
  120. package/llms/patterns/brand-landing-page.md +271 -175
  121. package/llms/patterns/chat-panel.md +120 -39
  122. package/llms/patterns/dashboard-page.md +276 -65
  123. package/llms/patterns/feed-page.md +245 -64
  124. package/llms/patterns/landing-page.md +473 -75
  125. package/llms/patterns/list-detail-page.md +377 -68
  126. package/llms/patterns/settings-page.md +268 -67
  127. package/llms/reference/token-intent.md +4 -2
  128. package/llms/reference/utilities.md +20 -5
  129. package/llms/tasks/building-an-ios-prototype.md +4 -1
  130. package/llms/tasks/choosing-a-component.md +2 -2
  131. package/llms-full.txt +6428 -892
  132. package/llms.txt +67 -16
  133. package/package.json +5 -1
  134. package/registry.json +3217 -552
  135. package/targets/ios.json +88 -18
  136. package/llms/components/description.md +0 -96
  137. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
package/AGENTS.md CHANGED
@@ -12,6 +12,7 @@ How to **build with** Sable. This is the whole consumer contract an agent needs;
12
12
  |---|---|
13
13
  | Which component, and its props/examples | `npx sable search "<need>"` → `npx sable component <Name> --detail full` |
14
14
  | A page composition to start from | `npx sable pattern --list` → `npx sable pattern <Name> --skeleton` |
15
+ | A card, form or list to start from (a block) | `npx sable block --list` → `npx sable block <Name> --code` |
15
16
  | The rules for type, motion, spacing, shape, surfaces, a11y | `npx sable docs typography` (also `motion`, `spacing`, `shape`, `surfaces`, `accessibility`) |
16
17
  | Which token to reach for | `npx sable token <name>` · `npx sable docs token-intent` |
17
18
  | A task walkthrough | `npx sable docs --list --kind tasks` |
@@ -61,7 +62,7 @@ The machine-readable version of this section is the `consume` block in `registry
61
62
  `styles.css` carries **Base only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as Base — no error, just the wrong theme.
62
63
  4. **Fonts:** load Figtree (sans) + DM Mono (mono) — the theme declares the stacks, the host provides the files. Apply `antialiased` (`-webkit-font-smoothing: antialiased` / `-moz-osx-font-smoothing: grayscale`) **once on the app root**: Sable's type scale was drawn against smoothed text, and without it macOS renders every weight heavier than designed. The package deliberately doesn't set it — a library has no business styling the host's whole page — so it has to happen here.
63
64
  5. **Providers (all optional):** components render with **Reicon** by default, so `<IconProvider set={…}>` is only needed to replace it with another icon package or extend it using `composeIconSets(appExtras, reiconIconSet)`; `<MotionProvider>` is optional too. Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
64
- 6. Import from the root barrel: `import { Button } from "@sableui/react"`. Page shells live on the `patterns` subpath (`import { SettingsPage } from "@sableui/react/patterns"`) — preview one, then copy its source from `registry.json → patterns` when the route needs to diverge.
65
+ 6. Import from the root barrel: `import { Button } from "@sableui/react"`. Page shells live on the `patterns` subpath (`import { SettingsPage } from "@sableui/react/patterns"`) — preview one, then copy its source from `registry.json → patterns` when the route needs to diverge. Blocks — section-sized compositions such as a sign-in card or a pricing card — live on the `blocks` subpath (`import { SignInBlock } from "@sableui/react/blocks"`) and ship the same way (`registry.json → blocks`); copy one and edit its content constants. A block's props are only `className`, `framed` and its primary action's callback, never content.
65
66
 
66
67
  7. **Check the wiring:** `npx sable doctor`. Every check it runs corresponds to a way Sable fails *silently* — a second React (blank page, "invalid hook call"), a missing `@source` (Tailwind emits none of the package's classes, everything renders unstyled), or `data-sable-theme` set with no theme CSS imported (every theme renders as Base and the switcher looks broken). Exits 1 on any failure; `--json` for the structured report.
67
68
 
@@ -70,6 +71,7 @@ The machine-readable version of this section is the `consume` block in `registry
70
71
  ```bash
71
72
  npx sable pattern --list # the eight page shells
72
73
  npx sable pattern SettingsPage --skeleton # composition tree, fixtures stripped
74
+ npx sable block --list # section-sized blocks: cards, forms, lists
73
75
  npx sable component DataTable --props
74
76
  npx sable search "empty state" # across components, patterns, layout, tokens, docs
75
77
  npx sable docs converting-an-app # the task walkthroughs
package/CHANGELOG.md CHANGED
@@ -1,5 +1,125 @@
1
1
  # What's new
2
2
 
3
+ ## 0.3.0
4
+
5
+ Blocks, charts, and the components they needed.
6
+
7
+ ### Breaking
8
+
9
+ - **`DotsMenu` is now `OverflowMenu`** (`overflowMenuMeta`, same props). The
10
+ name says what it is for — the "more actions" menu — rather than how its
11
+ trigger is drawn.
12
+
13
+ ### New
14
+
15
+ - **Blocks** — section-sized compositions on a new `@sableui/react/blocks`
16
+ subpath and in `registry.json → blocks`: 40 of them across Auth, Settings &
17
+ team, Commerce, Dashboard, Marketing, Social & messaging, Media & files,
18
+ Navigation and System (sign-in, pricing table, team members, invoice
19
+ history, FAQ, 404, chart cards…). Copy one and edit its content constants;
20
+ props are only `className`, `framed` and the primary action's callbacks.
21
+ `sable block --list`, `--family`, `--skeleton` and `--code` retrieve them,
22
+ and the docs have a Blocks page with live previews and copyable code.
23
+ - **Charts** — `AreaChart`, `BarChart`, `LineChart`, `PieChart`,
24
+ `RadarChart`, `RadialChart` and `Sparkline`, in SVG with no dependency, for
25
+ dashboards, landing pages and decks. Stacked and 100% stacked, grouped,
26
+ horizontal and diverging bars, smooth/linear/step curves, gradients, dots and
27
+ labels, donuts with a centre value, gauges, legends with icons, and tooltips
28
+ with dot/line/dashed indicators, formatters and totals. Lines draw on, bars
29
+ rise and slices sweep in as a chart scrolls into view, and new data eases
30
+ from the old shape — all on the motion tokens. Every chart carries its data
31
+ as a table for assistive tech and steps through it from the keyboard.
32
+ - **Chart palette** — `--chart-1` … `--chart-8`, eight series colours
33
+ validated for colour-blind separation, a normal-vision floor, a lightness
34
+ band and a chroma floor, in light and dark. Every theme inherits them; a
35
+ theme may override the set. `chartColor(i)` hands the same colours to a
36
+ charting library.
37
+ - **`DateTile`** — a calendar-page date (month band over the day), with
38
+ lining figures so a display face never drops the day below centre.
39
+ - **`SegmentedBar`** — one bar split into the parts of a whole, read aloud as a
40
+ single labelled image.
41
+ - `CardTitle` takes `render`, so a card's name can be a heading
42
+ (`render={<h2 />}`).
43
+ - `Entity` takes `selected` for the current row of a selectable list.
44
+ - Icons: `stop`, `key`, `wallet`, `thumbs-up`, `thumbs-down`.
45
+ - `sable-lint` lints a single file as well as a folder.
46
+
47
+ ### Components
48
+
49
+ - **Sidebar:** components placed in the rail ink with the rail's own tokens, so
50
+ a dark rail on a light page keeps its account row legible.
51
+ - **Entity:** hover and selected fills step off whatever surface the row sits
52
+ on.
53
+ - **ChatComposer:** the stop control is a filled square, not an ✕.
54
+ - **CommandMenu:** a long detail no longer squeezes the label.
55
+
56
+ ### Fixes
57
+
58
+ - The registry's derived imports for patterns and blocks include hooks called
59
+ with a type argument (`useRef<…>(`) — `ListDetailPage`'s copied source was
60
+ missing its `useRef` import.
61
+
62
+ ## 0.2.0
63
+
64
+ A design and polish pass across the library, its page patterns, and the site.
65
+
66
+ ### Breaking
67
+
68
+ - **`Description` is removed.** Label/value lists are now typography on the
69
+ native element: `<dl className="sable-list-dl">` with `<dt>`/`<dd>` children,
70
+ and `sable-list-dl-2` for two pairs a row inside an `@container`. Colour a
71
+ status value with a `text-*` utility on its `<dd>`. `DescriptionItem`,
72
+ `DescriptionTerm`, `DescriptionDetail`, `descriptionVariants` and
73
+ `descriptionMeta` go with it.
74
+
75
+ ### New
76
+
77
+ - List utilities for semantic HTML: `sable-list` (bullets),
78
+ `sable-list-numbered`, `sable-list-dl` and `sable-list-dl-2`, documented under
79
+ Typography → Lists.
80
+ - `sable-frame-tool`: a frame for edge-to-edge tool chrome that keeps the
81
+ theme's radius and passes concentric geometry to what's inside. PanelSection
82
+ rows grow their inset with the corner and compact fields curve parallel to
83
+ it; unframed, panels keep their 8px inset.
84
+ - `KbdGroup` for keyboard combinations.
85
+
86
+ ### Components
87
+
88
+ - **Bubble:** the tail is a drawn ear on every corner style, flat through fully
89
+ pilled, with a softened tip and a filleted inner corner; square themes keep it
90
+ sharp. Bubbles reserve the tail's space so grouped runs line up.
91
+ - **Checkbox / CheckboxCard:** the check and dash read at a consistent ~1.6px
92
+ weight on the filled box.
93
+ - **EmptyState:** the glyph stands on its own, with no disc, and can be an image
94
+ or illustration.
95
+ - **Skeleton:** one eased sheen passes through every skeleton on screen as a
96
+ single wave, a highlight in light and dark, and rests off-screen under reduced
97
+ motion.
98
+ - **Stepper:** indicators no longer rise above sticky headers or navigation bars.
99
+ - **BottomNav / Button:** badge dots sit on the icon's corner.
100
+ - **Toast:** enters from the edge it is anchored to, and portals to `<body>` so a
101
+ transformed ancestor can't trap it.
102
+ - Refinements to Card (concentric inner radius, footer seams), Table and
103
+ DataTable, Avatar, Badge, Pill, Kbd, Combobox, Select, NumberField, SideNav,
104
+ ThemeSelector, ChatComposer and more.
105
+
106
+ ### Presets
107
+
108
+ - Verdant and Meridian lift cards and popovers above the page in light mode, so
109
+ fields read as boxes to type in. A theme test now holds every preset to it.
110
+
111
+ ### iOS
112
+
113
+ - `IosSheet` follows iOS 26: Liquid Glass at the medium detent, floating and
114
+ concentric with the screen; opaque at the large detent; a glass close button
115
+ and a centred title.
116
+
117
+ ### Page patterns
118
+
119
+ - All eight rebuilt as fuller examples. Dashboard, Settings, Auth, List–detail
120
+ and Feed now tell one product story; Landing shows the library working;
121
+ Brand landing and Chat are polished.
122
+
3
123
  ## 0.1.1
4
124
 
5
125
  - Published with npm trusted publishing: releases come from `sableui/sable`'s
package/CONTRIBUTING.md CHANGED
@@ -195,6 +195,14 @@ not. The brief determines the result, not resemblance to an existing shell.
195
195
 
196
196
  The **source is the contract**: each pattern is real TSX (typechecked, design-linted, rendered live by the docs app), split as `<name>.meta.ts` (a `PatternMeta`: name/summary/intent/notes/a11y) + `<name>.tsx` whose code after the `// ---pattern-code---` marker is emitted verbatim into the registry by `build-registry.mjs` — so the shipped recipe can never drift from code that compiles. Keep patterns hook-free and self-contained (inline sample data, `<a href>` for links); the generator auto-derives the consumer import block. To add one: write the pair, export both from `src/patterns/index.ts`, regenerate the registry, and add it to the docs `PatternPreview` map.
197
197
 
198
+ ## Blocks (section-sized compositions)
199
+
200
+ `src/blocks/` holds compositions smaller than a page — a sign-in card, a pricing card, an order tracker — built from components with sample content and no behaviour of their own. They ship exactly like patterns (`@sableui/react/blocks` previews + `registry.json → blocks` source, `sable block <Name>`), from the same `<name>.meta.ts` (a `BlockMeta`) + `<name>.tsx` pair split by a `// ---block-code---` marker. The docs canvas and the Blocks page render them, so the scenes a theme is judged on are the source a consumer copies.
201
+
202
+ **Component or block?** If it owns behaviour, state or an a11y contract that should arrive on upgrade, it is a component. If it is an arrangement of components plus copy, it is a block. A block that needs something the library lacks is a library gap: add the component first (`DateTile` and `SegmentedBar` arrived this way), then compose it.
203
+
204
+ **The block contract** (checked by `build-registry`): the name ends in `Block`; the meta declares a `family`, a `kind` (`card` | `section`) and, for cards, the designed `width` ({ min, max }); the root takes `className` and renders `w-full` capped at the designed max; a card block built on Card takes `framed` (Card `default` / `flat`); the only other props are the primary action's callbacks. Content is never a prop — it is the constant at the top of the file, edited after copying. Variants are separate blocks, not a `variant` prop. To add one: write the pair, export both from `src/blocks/index.ts`, and regenerate the registry; the Blocks docs page picks it up from the catalog.
205
+
198
206
  ## Promoting from an app (and the drift back down)
199
207
 
200
208
  The system grows **out of the apps**, not ahead of them. A component earns its way in by being built in a consuming app first, measured in place, and only then lifted here — which is how the workspace register (v0.31.1, out of Glitch's tool chrome) and `Tree` (v0.32.0, out of Glitch's layers list) landed. Both deleted more app code than they added: Glitch's `globals.css` went 229 → 49 lines and its local `components/workspace/` disappeared. That deletion is the signal you're promoting something real rather than relocating it.
package/README.md CHANGED
@@ -122,6 +122,7 @@ Two binaries ship with the package, both zero-dependency and both reading the co
122
122
  ```bash
123
123
  npx sable pattern --list # the eight canonical page shells
124
124
  npx sable pattern DashboardPage --skeleton # its composition tree — 127 lines of source, 45 of structure
125
+ npx sable block PricingCardBlock --code # a section-sized block's source, imports included
125
126
  npx sable component Button --props
126
127
  npx sable search "date range" # one ranked set across components, patterns, layout, tokens, docs
127
128
  npx sable doctor # is this project wired correctly? exits 1 if not
Binary file
package/bin/sable.mjs CHANGED
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * npx sable pattern --list # the eight page shells, one line each
6
6
  * npx sable pattern DashboardPage --skeleton
7
+ * npx sable block --list # section-sized blocks: cards, forms, lists
7
8
  * npx sable component Button --props
8
9
  * npx sable search "date range"
9
10
  * npx sable doctor
@@ -109,10 +110,10 @@ const COMMANDS = [
109
110
  },
110
111
  {
111
112
  name: "search",
112
- summary: "One ranked result set across components, patterns, layout primitives, utilities, tokens, and docs.",
113
+ summary: "One ranked result set across components, patterns, blocks, layout primitives, utilities, tokens, and docs.",
113
114
  arg: { name: "query", required: true },
114
115
  flags: [
115
- { name: "--type", value: "component|pattern|layout|utility|token|doc", describe: "Restrict to one kind." },
116
+ { name: "--type", value: "component|pattern|block|layout|utility|token|doc", describe: "Restrict to one kind." },
116
117
  { name: "--limit", value: "n", describe: "Cap results. Default 20." },
117
118
  ],
118
119
  returns: ["search"],
@@ -142,6 +143,18 @@ const COMMANDS = [
142
143
  ],
143
144
  returns: ["pattern.list", "pattern.detail", "pattern.skeleton", "pattern.code"],
144
145
  },
146
+ {
147
+ name: "block",
148
+ summary: "Section-sized compositions — a sign-in card, a pricing card. Smaller than a page shell; copy the code and edit its content.",
149
+ arg: { name: "Name", required: false },
150
+ flags: [
151
+ { name: "--list", value: null, describe: "List the blocks instead." },
152
+ { name: "--family", value: "family", describe: "With --list: one family only (e.g. \"Commerce\")." },
153
+ { name: "--skeleton", value: null, describe: "Composition tree only — no fixtures, no copy." },
154
+ { name: "--code", value: null, describe: "Full source with its import line." },
155
+ ],
156
+ returns: ["block.list", "block.detail", "block.skeleton", "block.code"],
157
+ },
145
158
  {
146
159
  name: "layout",
147
160
  summary: "Layout primitives and the layout doctrine.",
@@ -175,7 +188,7 @@ const COMMANDS = [
175
188
  arg: { name: "slug", required: false },
176
189
  flags: [
177
190
  { name: "--list", value: null, describe: "List every doc slug." },
178
- { name: "--kind", value: "components|layout|patterns|reference|tasks|taste", describe: "With --list: one kind." },
191
+ { name: "--kind", value: "components|layout|patterns|blocks|reference|tasks|taste", describe: "With --list: one kind." },
179
192
  ],
180
193
  returns: ["docs.list", "docs.detail"],
181
194
  },
@@ -271,6 +284,7 @@ const ERROR_CODES = [
271
284
  { code: "ERR_MISSING_ARGUMENT", when: "A command that requires an argument got none." },
272
285
  { code: "ERR_UNKNOWN_COMPONENT", when: "No component by that name. `suggestions` carries near matches." },
273
286
  { code: "ERR_UNKNOWN_PATTERN", when: "No pattern by that name." },
287
+ { code: "ERR_UNKNOWN_BLOCK", when: "No block by that name." },
274
288
  { code: "ERR_UNKNOWN_LAYOUT", when: "No layout primitive by that name." },
275
289
  { code: "ERR_UNKNOWN_TOKEN_GROUP", when: "No token group by that name." },
276
290
  { code: "ERR_UNKNOWN_UTILITY", when: "No utility or variant by that name. `suggestions` carries near matches." },
@@ -413,6 +427,7 @@ const section = (title, lines) => (lines.length ? `\n${DENSE ? title : `${title}
413
427
 
414
428
  const COMPONENTS = R.components ?? []
415
429
  const PATTERNS = R.patterns?.items ?? []
430
+ const BLOCKS = R.blocks?.items ?? []
416
431
  const PRIMITIVES = R.layoutPrimitives?.primitives ?? []
417
432
  const TOKEN_GROUPS = Object.keys(R.tokens ?? {})
418
433
  const UTILITIES = R.utilities?.items ?? []
@@ -440,7 +455,7 @@ function cmdManifest() {
440
455
  name: "sable",
441
456
  version: R.version,
442
457
  package: R.name,
443
- describes: { components: COMPONENTS.length, patterns: PATTERNS.length, layoutPrimitives: PRIMITIVES.length },
458
+ describes: { components: COMPONENTS.length, patterns: PATTERNS.length, blocks: BLOCKS.length, layoutPrimitives: PRIMITIVES.length },
444
459
  envelope: { success: "{ type, data }", error: "{ type: 'error', code, message, suggestions }" },
445
460
  globalFlags: GLOBAL_FLAGS,
446
461
  commands: COMMANDS,
@@ -448,7 +463,7 @@ function cmdManifest() {
448
463
  }
449
464
  ok("manifest", data, () =>
450
465
  [
451
- `sable ${R.version} — ${COMPONENTS.length} components, ${PATTERNS.length} patterns, ${PRIMITIVES.length} layout primitives.`,
466
+ `sable ${R.version} — ${COMPONENTS.length} components, ${PATTERNS.length} patterns, ${BLOCKS.length} blocks, ${PRIMITIVES.length} layout primitives.`,
452
467
  "",
453
468
  "Commands:",
454
469
  ...COMMANDS.map((c) => ` ${(c.name + (c.arg ? ` <${c.arg.name}>` : "")).padEnd(22)}${c.summary}`),
@@ -502,6 +517,9 @@ function cmdSearch() {
502
517
  if (!type || type === "pattern")
503
518
  for (const p of PATTERNS)
504
519
  pool.push({ kind: "pattern", name: p.name, summary: p.summary, _s: score({ name: p.name, text: `${p.summary} ${p.intent ?? ""}` }) })
520
+ if (!type || type === "block")
521
+ for (const b of BLOCKS)
522
+ pool.push({ kind: "block", name: b.name, summary: b.summary, family: b.family, _s: score({ name: b.name, keywords: [b.family.toLowerCase()], text: `${b.summary} ${b.intent ?? ""}` }) })
505
523
  if (!type || type === "layout")
506
524
  for (const m of PRIMITIVES)
507
525
  pool.push({ kind: "layout", name: m.name, summary: m.summary, _s: score({ name: m.name, keywords: m.keywords, text: m.summary }) })
@@ -518,7 +536,7 @@ function cmdSearch() {
518
536
  // Ties resolve in the order AGENTS.md tells an agent to resolve a need:
519
537
  // a component first, then a pattern or primitive, then a utility, then a
520
538
  // token. "segmented" should land on ToggleGroup before the well behind it.
521
- const KIND_ORDER = ["component", "pattern", "layout", "utility", "token", "doc"]
539
+ const KIND_ORDER = ["component", "pattern", "block", "layout", "utility", "token", "doc"]
522
540
  const hits = pool
523
541
  .filter((h) => h._s > 0)
524
542
  .sort((a, b) => b._s - a._s || KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind))
@@ -679,25 +697,38 @@ function docSlugFor(name) {
679
697
  const entry =
680
698
  COMPONENTS.find((c) => c.name.toLowerCase() === name.toLowerCase()) ??
681
699
  PRIMITIVES.find((m) => m.name.toLowerCase() === name.toLowerCase()) ??
682
- PATTERNS.find((p) => p.name.toLowerCase() === name.toLowerCase())
700
+ PATTERNS.find((p) => p.name.toLowerCase() === name.toLowerCase()) ??
701
+ BLOCKS.find((b) => b.name.toLowerCase() === name.toLowerCase())
683
702
  if (!entry?.path) return slugify(name)
684
703
  return entry.path.split("/").pop().replace(/\.tsx$/, "")
685
704
  }
686
705
 
687
- /* ── pattern ────────────────────────────────────────────────────────────────── */
706
+ /* ── pattern / block ────────────────────────────────────────────────────────── */
688
707
 
689
- function cmdPattern() {
708
+ /**
709
+ * The copy-in tiers share one retrieval shape: list, detail, skeleton, code.
710
+ * Patterns are page shells; blocks are section-sized (a card, a form) and add
711
+ * a family and a designed width.
712
+ */
713
+ function cmdComposition({ kind, items, about, noun, errorCode }) {
690
714
  if (flags.has("list") || !rest[0]) {
691
- const data = PATTERNS.map((p) => ({ name: p.name, summary: p.summary, intent: p.intent }))
692
- return ok("pattern.list", { count: data.length, about: R.patterns?.about, patterns: data }, () =>
715
+ const family = flags.get("family")
716
+ const list = family ? items.filter((x) => x.family?.toLowerCase() === String(family).toLowerCase()) : items
717
+ const data = list.map((x) => ({
718
+ name: x.name,
719
+ ...(x.family ? { family: x.family } : {}),
720
+ summary: x.summary,
721
+ intent: x.intent,
722
+ }))
723
+ return ok(`${kind}.list`, { count: data.length, about, [`${kind}s`]: data }, () =>
693
724
  [
694
- DENSE ? "" : `${data.length} canonical page shells. Read a skeleton before composing anything:\n`,
695
- ...data.map((p) =>
725
+ DENSE ? "" : `${data.length} ${noun}${family ? ` in ${family}` : ""}. Read a skeleton before composing anything:\n`,
726
+ ...data.map((x) =>
696
727
  DENSE
697
- ? `${p.name} ${firstSentence(p.summary)}`
698
- : ` ${p.name.padEnd(20)}${firstSentence(p.intent ?? p.summary)}`,
728
+ ? `${x.name} ${firstSentence(x.summary)}`
729
+ : ` ${x.name.padEnd(kind === "block" ? 28 : 20)}${x.family ? x.family.padEnd(20) : ""}${firstSentence(x.intent ?? x.summary)}`,
699
730
  ),
700
- DENSE ? "" : "\n sable pattern <Name> --skeleton the composition tree, ~25 lines",
731
+ DENSE ? "" : `\n sable ${kind} <Name> --skeleton the composition tree, ~25 lines`,
701
732
  ]
702
733
  .filter(Boolean)
703
734
  .join("\n"),
@@ -705,30 +736,31 @@ function cmdPattern() {
705
736
  }
706
737
 
707
738
  const name = rest[0]
708
- const p = PATTERNS.find((x) => x.name.toLowerCase() === name.toLowerCase())
739
+ const p = items.find((x) => x.name.toLowerCase() === name.toLowerCase())
709
740
  if (!p)
710
- fail("ERR_UNKNOWN_PATTERN", `No pattern named "${name}".`, [
711
- ...nearest(name, PATTERNS.map((x) => x.name)).map((n) => `Did you mean ${n}?`),
712
- "sable pattern --list",
741
+ fail(errorCode, `No ${kind} named "${name}".`, [
742
+ ...nearest(name, items.map((x) => x.name)).map((n) => `Did you mean ${n}?`),
743
+ `sable ${kind} --list`,
713
744
  ])
714
745
 
715
746
  if (flags.has("skeleton")) {
716
747
  const { tree, elided } = skeletonize(p.code)
717
- return ok("pattern.skeleton", { name: p.name, tree, elidedFixtures: elided }, () =>
748
+ return ok(`${kind}.skeleton`, { name: p.name, tree, elidedFixtures: elided }, () =>
718
749
  [
719
750
  DENSE ? "" : `${p.name} — composition tree${elided ? ` (${elided} demo fixtures elided)` : ""}\n`,
720
751
  tree,
721
- DENSE ? "" : `\nFull source: sable pattern ${p.name} --code`,
752
+ DENSE ? "" : `\nFull source: sable ${kind} ${p.name} --code`,
722
753
  ]
723
754
  .filter(Boolean)
724
755
  .join("\n"),
725
756
  )
726
757
  }
727
758
  if (flags.has("code"))
728
- return ok("pattern.code", { name: p.name, imports: p.imports, code: p.code }, () => `${p.imports}\n\n${p.code}`)
759
+ return ok(`${kind}.code`, { name: p.name, imports: p.imports, code: p.code }, () => `${p.imports}\n\n${p.code}`)
729
760
 
730
761
  const level = detailLevel("compact")
731
762
  const data = { name: p.name, path: p.path, summary: p.summary, intent: p.intent }
763
+ if (p.family) Object.assign(data, { family: p.family, kind: p.kind, ...(p.width ? { width: p.width } : {}) })
732
764
  if (level !== "brief") {
733
765
  data.notes = p.notes ?? []
734
766
  data.a11y = p.a11y ?? []
@@ -738,21 +770,30 @@ function cmdPattern() {
738
770
  data.imports = p.imports
739
771
  data.code = p.code
740
772
  }
741
- ok("pattern.detail", data, () =>
773
+ const shape = p.family ? ` ${p.family} · ${p.kind}${p.width ? ` · ${p.width.min}–${p.width.max}` : ""}` : ""
774
+ ok(`${kind}.detail`, data, () =>
742
775
  [
743
- `${p.name} (${p.path})`,
776
+ `${p.name} (${p.path})${shape}`,
744
777
  p.summary,
745
778
  p.intent ? `\nReach for it when:\n ${p.intent}` : "",
746
779
  level === "brief" ? "" : section("notes", (p.notes ?? []).map(bullet)),
747
780
  level === "brief" ? "" : section("a11y", (p.a11y ?? []).map(bullet)),
748
781
  level === "brief" ? "" : `\nComposition:\n${data.skeleton}`,
749
- level === "full" ? `\n${p.imports}\n\n${p.code}` : `\nFull source: sable pattern ${p.name} --code`,
782
+ level === "full" ? `\n${p.imports}\n\n${p.code}` : `\nFull source: sable ${kind} ${p.name} --code`,
750
783
  ]
751
784
  .filter(Boolean)
752
785
  .join("\n"),
753
786
  )
754
787
  }
755
788
 
789
+ function cmdPattern() {
790
+ cmdComposition({ kind: "pattern", items: PATTERNS, about: R.patterns?.about, noun: "canonical page shells", errorCode: "ERR_UNKNOWN_PATTERN" })
791
+ }
792
+
793
+ function cmdBlock() {
794
+ cmdComposition({ kind: "block", items: BLOCKS, about: R.blocks?.about, noun: "blocks", errorCode: "ERR_UNKNOWN_BLOCK" })
795
+ }
796
+
756
797
  /* ── layout ─────────────────────────────────────────────────────────────────── */
757
798
 
758
799
  function cmdLayout() {
@@ -1183,6 +1224,7 @@ const HANDLERS = {
1183
1224
  search: cmdSearch,
1184
1225
  component: cmdComponent,
1185
1226
  pattern: cmdPattern,
1227
+ block: cmdBlock,
1186
1228
  layout: cmdLayout,
1187
1229
  token: cmdToken,
1188
1230
  utility: cmdUtility,
@@ -7,7 +7,7 @@
7
7
  * To author a new theme: copy `themes/base/`, 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", "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", "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-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", "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