@sableui/react 0.18.0 → 0.19.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 (84) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/bin/sable.mjs +108 -14
  3. package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
  4. package/dist/blocks/index.d.ts +1 -1
  5. package/dist/components/avatar/avatar.js +16 -7
  6. package/dist/components/avatar/avatar.meta.js +43 -10
  7. package/dist/components/avatar/avatar.variants.js +26 -2
  8. package/dist/components/collapsible/collapsible.js +6 -5
  9. package/dist/components/collapsible/collapsible.meta.js +14 -5
  10. package/dist/components/data-table/data-table.js +132 -45
  11. package/dist/components/data-table/data-table.meta.js +63 -4
  12. package/dist/components/date-picker/date-picker.js +13 -6
  13. package/dist/components/date-picker/date-picker.meta.js +22 -0
  14. package/dist/components/menu/menu.js +2 -0
  15. package/dist/components/menu/menu.meta.js +2 -2
  16. package/dist/components/panel-group/panel-group.js +17 -1
  17. package/dist/components/panel-group/panel-group.meta.js +43 -3
  18. package/dist/components/side-nav/side-nav.variants.js +8 -3
  19. package/dist/components/sidebar/index.js +4 -0
  20. package/dist/components/sidebar/sidebar.js +90 -11
  21. package/dist/components/sidebar/sidebar.meta.js +38 -4
  22. package/dist/components/table/table.js +7 -3
  23. package/dist/components/tabs/tabs.js +10 -2
  24. package/dist/components/tabs/tabs.meta.js +4 -0
  25. package/dist/components/workspace/workspace.js +13 -3
  26. package/dist/components/workspace/workspace.meta.js +109 -1
  27. package/dist/file-hashes.json +53 -49
  28. package/dist/foundations/typography.js +13 -0
  29. package/dist/icon/Icon.js +21 -2
  30. package/dist/icon/index.d.ts +36 -1
  31. package/dist/icon/index.js +5 -0
  32. package/dist/icon/names.js +240 -0
  33. package/dist/icon-sets/reicon.js +19 -1
  34. package/dist/index.d.ts +272 -50
  35. package/dist/index.js +11 -1
  36. package/dist/lib/chart/heat.js +1 -1
  37. package/dist/lib/cn.js +1 -0
  38. package/dist/lib/use-media-query.js +41 -0
  39. package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
  40. package/dist/patterns/dashboard-page.js +23 -17
  41. package/dist/patterns/index.d.ts +7 -2
  42. package/dist/patterns/index.js +5 -1
  43. package/dist/patterns/tracker-page.js +392 -0
  44. package/dist/patterns/tracker-page.meta.js +40 -0
  45. package/dist/presets/index.d.ts +1 -1
  46. package/dist/styles.css +179 -74
  47. package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
  48. package/dist/themes/_contract.js +9 -1
  49. package/dist/themes/atelier/index.d.ts +1 -1
  50. package/dist/themes/ember/index.d.ts +1 -1
  51. package/dist/themes/index.d.ts +2 -2
  52. package/dist/themes/meridian/index.d.ts +1 -1
  53. package/dist/themes/poppy/index.d.ts +1 -1
  54. package/dist/themes/prism/index.d.ts +1 -1
  55. package/dist/themes/sable/index.d.ts +1 -1
  56. package/dist/themes/verdant/index.d.ts +1 -1
  57. package/llms/components/avatar.md +32 -9
  58. package/llms/components/collapsible.md +12 -4
  59. package/llms/components/data-table.md +40 -3
  60. package/llms/components/date-picker.md +15 -1
  61. package/llms/components/menu.md +1 -1
  62. package/llms/components/panel-group.md +34 -2
  63. package/llms/components/sidebar.md +17 -4
  64. package/llms/components/tabs.md +1 -0
  65. package/llms/components/workspace.md +81 -1
  66. package/llms/foundations/typography.md +4 -1
  67. package/llms/patterns/dashboard-page.md +18 -15
  68. package/llms/patterns/tracker-page.md +450 -0
  69. package/llms/reference/token-intent.md +3 -1
  70. package/llms/reference/utilities.md +4 -1
  71. package/llms/tasks/auditing-an-app.md +4 -1
  72. package/llms/tasks/building-a-page.md +3 -2
  73. package/llms/tasks/building-a-tool.md +101 -5
  74. package/llms/tasks/building-with-sable.md +11 -3
  75. package/llms/tasks/creating-a-preset.md +9 -0
  76. package/llms-full.txt +843 -54
  77. package/llms.txt +8 -7
  78. package/package.json +2 -1
  79. package/registry.json +491 -36
  80. package/scripts/adoption.mjs +15 -2
  81. package/scripts/app-css.mjs +129 -0
  82. package/scripts/grade.mjs +161 -2
  83. package/scripts/lint-rules.mjs +126 -0
  84. package/skills/sable/SKILL.md +33 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,134 @@
1
1
  # What's new
2
2
 
3
+ ## 0.19.0
4
+
5
+ A compact work tracker, built on Sable by an agent, rebuilt about thirty parts
6
+ Sable already ships. Every gap behind those rebuilds is fixed here: a lookup
7
+ that couldn't find parts, checks that couldn't see the app's own CSS, and
8
+ components that had no variant for a dense layout.
9
+
10
+ - **`--type-base` rebuilds the type scale at another base size.** Set
11
+ `--type-base: 14` (a unitless px number; the default is 16) on `:root`, a
12
+ `[data-sable-theme]` scope or in a preset's `tokens.shared`, and every page
13
+ tier scales by `base / 16`, including each theme's own tier values and any
14
+ per-tier override. Spacing follows, because the baseline step is derived from
15
+ body size. The workspace `ui-*` tiers stay absolute px, and compact still only
16
+ ever shrinks text. Don't change the root font-size to get a denser UI: it
17
+ scales every rem in the app, not just the type.
18
+ - **New `text-ui-title`** (14px / 600 / 20px leading, tokens
19
+ `--type-ui-title-*`). Inside a compact scope `text-title-lg` maps to it. It
20
+ used to collapse to 12px, the same size as the description under it.
21
+ - **A comfortable scope inside a compact one gets page type back.**
22
+ `<Density value="comfortable">` around a reading view (an overview, notes,
23
+ docs) in a Workspace used to reset the control sizes but not the text, which
24
+ stayed at 11px.
25
+ - **Tables follow density.** TableHead's height, TableCell's padding and the
26
+ table's text tier read density tokens. In a compact Workspace that is a 28px
27
+ head, 32px rows and `text-ui-body`, with no consumer CSS.
28
+ - **Compact on a touch screen uses the comfortable control ladder.** Under
29
+ `pointer: coarse` the 24/28/32 ladder, measured from mouse-driven editors,
30
+ becomes 28/32/40.
31
+ - **Workspace's body is its only stretching row.** The grid always assumed a
32
+ toolbar, so a Workspace without one put the body in the auto track and
33
+ clipped the status bar. The root is now a flex column, which holds for every
34
+ combination of slots. Migration: drop any `grid-template-rows` override on
35
+ the Workspace root.
36
+ - **Workspace and Panel on small screens.** Workspace takes `bottomNav`, a row
37
+ shown below `lg`. Panel takes `narrow="sheet" | "hidden"` with `open`,
38
+ `onOpenChange`, `side` and `below`: below the breakpoint an inspector becomes
39
+ a Sheet and its content renders once. A Sidebar moved into that sheet fills
40
+ it. New `useMediaQuery` and
41
+ `useBelowBreakpoint` hooks are exported. A WorkspaceBar can also sit at the
42
+ top of one column, so the inspector runs the full height beside it, as in
43
+ Linear.
44
+ - **DataTable groups rows.** `groupBy`, `groups` (each with an icon, an action
45
+ and `defaultExpanded`), `collapsible` and controlled `expandedGroups` give
46
+ one table with one header and a header row per group that carries a toggle,
47
+ a count and an add action. Sorting applies within each group, and select-all
48
+ acts on the visible rows.
49
+ - **The Sidebar rail is one grid, at either density.** New `SidebarGroup`
50
+ labels a run of items, so a rail no longer has to borrow SideNav for its
51
+ groups. New `SidebarAccount` is the account row: the avatar in the icon slot,
52
+ the label and one detail line on the label column, and an up-down chevron.
53
+ Make it the account Menu's trigger and put Settings, appearance and sign-out
54
+ in the menu. Inside a compact scope such as a Workspace, the whole rail takes
55
+ 32px rows and 20px icons, and the header matches the chrome bar's height.
56
+ Before this, a tool's footer mixed a ghost Button, an Entity patched with
57
+ `[--entity-py:0]` and a Badge, and the rail's icons and labels landed on four
58
+ columns. DashboardPage uses the new parts.
59
+ - **Underline tabs in a WorkspaceBar sit on the bar's hairline.** They used to
60
+ draw a second, shorter rule that stopped after the last tab, with the mark
61
+ floating above the bar's edge. Tabs guidance now steers a view switch in tool
62
+ chrome to `variant="pill"`.
63
+ - **`MenuSubmenuTrigger` takes `icon`**, as MenuItem does, so a submenu row
64
+ lines up with the icon rows around it.
65
+ - New icon `chevron-up-down` (Lucide's `chevrons-up-down` resolves to it).
66
+ - **Avatar:** `2xs` (20px) and `xs` (24px), `shape="square"` for a workspace,
67
+ project or org mark, and a palette `tone` for the initials fallback.
68
+ - **DatePicker:** `size="xs"` for a property row, and `formatOptions`
69
+ (`Intl.DateTimeFormatOptions`) for the trigger text.
70
+ - **CollapsibleIndicator needs no setup.** It no longer needs the
71
+ `group/collapsible` class on its trigger. New `direction="end"` gives a side
72
+ caret that turns down on open, mirrored in RTL.
73
+ - **Icons:** new `users`, `archive`, `kanban`, `sliders`, `help` and `sidebar`.
74
+ About 90 Lucide and Heroicons names now resolve to Sable's (`settings` →
75
+ `gear`, `menu` → `bars`, `check-circle` → `circle-check`). A development
76
+ build warns once for a name the set can't draw, and names the nearest ones.
77
+ - **A toast clears an open side sheet on a phone.** It used to move to the
78
+ top, over the sheet's close button. It now sits below the sheet's header
79
+ inset, tunable with `--sable-sheet-top-inset`.
80
+ - **Workspace surfaces derive in oklab.** The canvas, panel, field, hairline
81
+ and shell-ground mixes used `color-mix(in oklch, …)`. On hex colours Chrome
82
+ resolves that to a colour with no hue, painted as 0°: a brown cast on a cool
83
+ graphite preset's canvas. Bundled themes write oklch and never showed it, but
84
+ presets and apps write hex. The heatmap's empty cell moves to oklab too.
85
+ - **`--nav-height` is defined** (56px). AppShell's bottom padding read it with
86
+ no fallback, so the padding was dropped.
87
+ - **Agent tooling finds parts.** `sable component PropertyRow` (or `Panel`,
88
+ `WorkspaceBar`, `CollapsibleTrigger`) answers with the parent component;
89
+ it used to be ERR_UNKNOWN_COMPONENT. Search indexes parts, and an exact name
90
+ ranks first. New `sable icon [name]` lists the drawable names and resolves
91
+ aliases.
92
+ - **`sable check` sees rebuilt parts.** A new "Rebuilt parts" block reads class
93
+ names in the app's markup and the app's own CSS. It flags a breadcrumb, top
94
+ bar, property row, status dot or avatar stack built by hand; fixed bar and row
95
+ heights; selectors reaching into Sable's components; and a chevron rotated on
96
+ `aria-expanded`. Icon names passed through an app's own wrapper are checked
97
+ too. These are notes, and the skill asks the agent to answer each one.
98
+ - **`sable check` warns on one-mode colour.** Colour tokens set for light with
99
+ no dark value now produce a warning, and an app with no ThemeSwitcher gets a
100
+ note. The skill now ships both modes, as a preset with a ThemeSwitcher under
101
+ SableThemeProvider, unless the brief rules one out.
102
+ - **New pattern: `TrackerPage`.** A records workspace in the tool register,
103
+ the reference shape for an issue tracker, a support queue or a pipeline. It
104
+ has a Sidebar rail of grouped views ending in an account menu (with
105
+ appearance), Breadcrumbs and pill Tabs in WorkspaceBars, a filter row, one
106
+ grouped DataTable, a docked inspector of PanelSection property rows with a
107
+ pinned comment form, and a comfortable reading tab. Below lg the rail and the
108
+ inspector become sheets and BottomNav takes the last row. `sable pattern
109
+ TrackerPage --skeleton` has the tree. Pattern metadata takes `keywords` now,
110
+ so "issue tracker", "like Linear" or "support queue" lands on the shell first.
111
+ - **Guidance for a records workspace.** `building-a-tool` now covers the
112
+ shape of an issue tracker, a support queue or a pipeline: the shell, the
113
+ decisions that keep it one system (one rail grid, the account as a row and a
114
+ menu, one grouped table, an inspector that renders once, page-sized reading
115
+ tabs, pill tabs in chrome, both modes, `type-base` for "compact") and a
116
+ "you want / use / not" table built from what a real build rebuilt. The skill
117
+ routes a tool, tracker or "like Linear" brief there; `building-a-page` points
118
+ trackers to it; `creating-a-preset` explains `type-base`.
119
+ - **`sable check` notes compositions another part does better:** SideNav
120
+ inside a Sidebar, an Entity or Badge in the rail's footer, a DataTable per
121
+ group inside Collapsibles, underline tabs in a WorkspaceBar, and a native
122
+ date input. Notes, never failures. On the build that prompted this release
123
+ they flag four drifts, each at its line.
124
+ - **The skill searches before it builds.** It looks up a part by its plain name
125
+ and role before writing any class or component for it. A style or layout
126
+ disagreement means using the variant or prop, or reporting the missing
127
+ variant, never rebuilding the part.
128
+ - Migration: run `sable sources --write` after upgrading. Panel now imports
129
+ Sheet, so the generated sources file is stale until it is rewritten, and
130
+ doctor says so.
131
+
3
132
  ## 0.18.0
4
133
 
5
134
  A year at a glance: the contribution graph, built so a phone, a keyboard and a
package/bin/sable.mjs CHANGED
@@ -223,6 +223,13 @@ const COMMANDS = [
223
223
  ],
224
224
  returns: ["provider.list", "provider.detail", "provider.props", "provider.examples"],
225
225
  },
226
+ {
227
+ name: "icon",
228
+ summary: "The icon names <Icon name> renders with the bundled set. Check a name before using it: an unknown one renders nothing.",
229
+ arg: { name: "name", required: false },
230
+ flags: [],
231
+ returns: ["icon.list", "icon.detail"],
232
+ },
226
233
  {
227
234
  name: "token",
228
235
  summary: "The token contract, with intent — what each token means and what it is NOT.",
@@ -388,6 +395,7 @@ const ERROR_CODES = [
388
395
  { code: "ERR_UNKNOWN_UTILITY", when: "No utility or variant by that name. `suggestions` carries near matches." },
389
396
  { code: "ERR_UNKNOWN_DOC", when: "No package guide or doc mirror by that slug. `suggestions` carries near matches and the package guides." },
390
397
  { code: "ERR_UNKNOWN_PROVIDER", when: "No provider by that name. `suggestions` carries near matches." },
398
+ { code: "ERR_UNKNOWN_ICON", when: "No icon by that name in the contract. `suggestions` carries the names to use instead." },
391
399
  { code: "ERR_UNKNOWN_EXAMPLE", when: "The component has no example with that label. `suggestions` carries near matches as commands." },
392
400
  { code: "ERR_LINT_INPUT", when: "sable lint --json: a path to lint does not exist." },
393
401
  { code: "ERR_CHECK_FAILED", when: "sable check: doctor, lint, or grade failed. `data.checks` names which." },
@@ -571,14 +579,32 @@ const importLine = (name) => `import { ${name} } from "@sableui/react"`
571
579
  const PROVIDERS = R.providers?.items ?? []
572
580
  const findProvider = (name) => PROVIDERS.find((p) => p.name.toLowerCase() === String(name).toLowerCase())
573
581
 
582
+ /**
583
+ * Exported parts by name → the entry that documents them. `PropertyRow`,
584
+ * `WorkspaceBar`, `CollapsibleIndicator` and `Panel` are real exports with no
585
+ * entry of their own, and `sable component PropertyRow` used to answer
586
+ * ERR_UNKNOWN_COMPONENT — so an agent that knew the right name concluded the
587
+ * part did not exist and built its own. A part shared by two entries (a
588
+ * MenuRadioGroup under Menu and SettingsMenu) resolves to the first.
589
+ */
590
+ const PARTS = new Map()
591
+ for (const c of COMPONENTS)
592
+ for (const s of c.subcomponents ?? []) {
593
+ const sub = typeof s === "string" ? { name: s.split(/\s+[—-]\s+/)[0].trim(), description: s } : s
594
+ if (sub.name && !PARTS.has(sub.name.toLowerCase())) PARTS.set(sub.name.toLowerCase(), { parent: c, part: sub })
595
+ }
596
+ const findPart = (name) => PARTS.get(String(name).toLowerCase())
597
+
574
598
  function findComponent(name) {
575
599
  const hit = COMPONENTS.find((c) => c.name.toLowerCase() === name.toLowerCase())
576
600
  if (hit) return hit
601
+ const part = findPart(name)
602
+ if (part) return { ...part.parent, partOf: part.parent.name, part: part.part }
577
603
  fail(
578
604
  "ERR_UNKNOWN_COMPONENT",
579
605
  `No component named "${name}".`,
580
606
  [
581
- ...nearest(name, [...COMPONENTS, ...PROVIDERS].map((c) => c.name)).map((n) => `Did you mean ${n}?`),
607
+ ...nearest(name, [...COMPONENTS, ...PROVIDERS].map((c) => c.name).concat([...PARTS.values()].map((p) => p.part.name))).map((n) => `Did you mean ${n}?`),
582
608
  `sable search "${name}" — searches summaries and keywords too`,
583
609
  "sable component --list",
584
610
  ],
@@ -649,14 +675,23 @@ function cmdSearch() {
649
675
 
650
676
  const pool = []
651
677
  if (!type || type === "component")
652
- for (const c of COMPONENTS)
678
+ for (const c of COMPONENTS) {
653
679
  pool.push({ kind: "component", name: c.name, summary: c.summary, status: c.status, group: c.group, ...(c.composition ? { reference: true, parts: c.composition } : {}), _s: score({ name: c.name, keywords: c.keywords, text: `${c.summary} ${c.usage?.description ?? ""}` }) })
680
+ // Parts are found by their own name and description: `WorkspaceBar` and
681
+ // `PropertyRow` are what a need is called, and they used to match nothing
682
+ // (or rank ColorField first, for a word in its prose). A part's hit names
683
+ // its entry, which is where its contract lives.
684
+ for (const s of c.subcomponents ?? []) {
685
+ if (typeof s !== "object" || !s.name || PARTS.get(s.name.toLowerCase())?.parent !== c) continue
686
+ pool.push({ kind: "component", name: s.name, partOf: c.name, summary: `${s.description ?? ""} Part of ${c.name}.`.trim(), status: c.status, group: c.group, _s: score({ name: s.name, text: s.description ?? "" }) })
687
+ }
688
+ }
654
689
  if (!type || type === "provider")
655
690
  for (const p of PROVIDERS)
656
691
  pool.push({ kind: "provider", name: p.name, summary: p.summary, _s: score({ name: p.name, keywords: ["provider", ...(p.companions ?? [])], text: `${p.summary} ${p.placement ?? ""}` }) })
657
692
  if (!type || type === "pattern")
658
693
  for (const p of PATTERNS)
659
- pool.push({ kind: "pattern", name: p.name, summary: p.summary, _s: score({ name: p.name, text: `${p.summary} ${p.intent ?? ""}` }) })
694
+ pool.push({ kind: "pattern", name: p.name, summary: p.summary, _s: score({ name: p.name, keywords: p.keywords, text: `${p.summary} ${p.intent ?? ""}` }) })
660
695
  if (!type || type === "block")
661
696
  for (const b of BLOCKS)
662
697
  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 ?? ""}` }) })
@@ -716,7 +751,9 @@ function cmdSearch() {
716
751
  retrieve:
717
752
  h.kind === "example"
718
753
  ? `sable component ${h.component} --example "${h.label.replace(/"/g, '\\"')}"`
719
- : `sable ${h.kind === "doc" ? "docs" : h.kind} ${h.name}`,
754
+ : h.partOf
755
+ ? `sable component ${h.partOf} --props`
756
+ : `sable ${h.kind === "doc" ? "docs" : h.kind} ${h.name}`,
720
757
  }))
721
758
  const anyReference = results.some((h) => h.reference)
722
759
  ok("search", { query, count: results.length, results }, () =>
@@ -724,7 +761,7 @@ function cmdSearch() {
724
761
  ...results.map((h) =>
725
762
  DENSE
726
763
  ? `${h.kind}/${h.name}${h.reference ? " (reference)" : ""}`
727
- : `${h.kind.padEnd(9)} ${`${h.name}${h.reference ? " ◇" : ""}`.padEnd(24)} ${firstSentence(h.summary ?? "")}`,
764
+ : `${h.kind.padEnd(9)} ${`${h.name}${h.reference ? " ◇" : ""}`.padEnd(24)} ${h.partOf ? `(part of ${h.partOf}) ` : ""}${firstSentence(h.summary ?? "")}`,
728
765
  ),
729
766
  ...(anyReference && !DENSE
730
767
  ? ["", "◇ a reference layout: use it when it fits the brief, or compose your own from Sable's parts. Either is fine. Behaviour components (controls, overlays, fields) are always used, never rebuilt."]
@@ -801,6 +838,36 @@ function firstSentence(s) {
801
838
  return text
802
839
  }
803
840
 
841
+ /* ── icon ───────────────────────────────────────────────────────────────────── */
842
+
843
+ /**
844
+ * The names `<Icon name>` draws, and the other sets' names it accepts for them
845
+ * (registry → iconContract, generated from src/icon/names.ts). A work tracker
846
+ * asked for nine names that did not exist, got blank space, and wrote its own
847
+ * alias map, because nothing listed the names that do.
848
+ */
849
+ function cmdIcon() {
850
+ const names = R.iconContract?.names ?? []
851
+ const aliases = R.iconContract?.aliases ?? {}
852
+ const set = new Set(names)
853
+ const name = rest[0]
854
+ if (!name)
855
+ return ok("icon.list", { count: names.length, names, aliases, note: R.iconContract?.note }, () =>
856
+ [`${names.length} icon names (the bundled Reicon set). An app adds more with composeIconSets and lists them in .sable/icons.json.`, "", names.join(" ")].join("\n"),
857
+ )
858
+ if (set.has(name)) return ok("icon.detail", { name, known: true, usage: `<Icon name="${name}" />` }, () => `${name} — in the contract.\n<Icon name="${name}" />`)
859
+ if (aliases[name])
860
+ return ok("icon.detail", { name, known: false, alias: aliases[name], usage: `<Icon name="${aliases[name]}" />` }, () =>
861
+ `${name} is another set's name for ${aliases[name]}: <Icon> draws it, but write the contract name.\n<Icon name="${aliases[name]}" />`,
862
+ )
863
+ const use = nearest(name, names)
864
+ fail("ERR_UNKNOWN_ICON", `No icon named "${name}"; <Icon name="${name}"> renders nothing.`, [
865
+ ...use.map((n) => `Use "${n}"`),
866
+ "Or add the glyph to the app's set with composeIconSets and list it in .sable/icons.json",
867
+ "sable icon — every name",
868
+ ])
869
+ }
870
+
804
871
  /* ── component ──────────────────────────────────────────────────────────────── */
805
872
 
806
873
  function cmdComponent() {
@@ -833,12 +900,16 @@ function cmdComponent() {
833
900
  // A provider is asked for the way a component is. Answer with its entry rather
834
901
  // than ERR_UNKNOWN_COMPONENT, which is what `sable component SableThemeProvider`
835
902
  // used to say.
836
- if (!COMPONENTS.some((c) => c.name.toLowerCase() === rest[0].toLowerCase()) && findProvider(rest[0])) return cmdProvider()
903
+ if (!COMPONENTS.some((c) => c.name.toLowerCase() === rest[0].toLowerCase()) && !findPart(rest[0]) && findProvider(rest[0])) return cmdProvider()
837
904
  const c = findComponent(rest[0])
905
+ // Asked for a part: answer with its entry, and say so first, so the agent
906
+ // knows the part exists, what to import, and where the rest of it is documented.
907
+ const part = c.partOf ? { name: c.part.name, partOf: c.partOf, import: `import { ${c.partOf}, ${c.part.name} } from "@sableui/react"`, ...(c.part.type ? { type: c.part.type } : {}), description: c.part.description ?? "" } : null
908
+ const partLine = part ? `${part.name} is part of ${part.partOf}.${part.description ? ` ${part.description}` : ""}\n${part.import}\n` : ""
838
909
 
839
910
  if (flags.has("props"))
840
- return ok("component.props", { name: c.name, props: c.props ?? [], subcomponents: c.subcomponents ?? [] }, () =>
841
- renderProps(c),
911
+ return ok("component.props", { name: c.name, ...(part ? { part } : {}), props: c.props ?? [], subcomponents: c.subcomponents ?? [] }, () =>
912
+ `${partLine}${part ? "\n" : ""}${renderProps(c)}`,
842
913
  )
843
914
  if (flags.has("examples"))
844
915
  return ok("component.examples", { name: c.name, examples: c.examples ?? [] }, () => renderExamples(c))
@@ -869,6 +940,7 @@ function cmdComponent() {
869
940
  import: importLine(c.name),
870
941
  summary: c.summary,
871
942
  ...(c.composition ? { composition: c.composition } : {}),
943
+ ...(part ? { part } : {}),
872
944
  }
873
945
  if (level !== "brief") {
874
946
  data.props = c.props ?? []
@@ -886,6 +958,7 @@ function cmdComponent() {
886
958
 
887
959
  ok("component.detail", data, () =>
888
960
  [
961
+ partLine,
889
962
  `${c.name} [${c.status}] ${c.group} register:${c.register}`,
890
963
  c.summary,
891
964
  c.composition
@@ -1834,7 +1907,7 @@ const MANUAL_REVIEW = [
1834
1907
  { step: "Walk the journey", how: "sable docs reviewing-a-journey — operate the primary task and its recovery path end to end; loading, empty, error and success states; keyboard and touch." },
1835
1908
  { step: "Review the motion", how: "sable docs reviewing-motion — observe playback and interruption for anything that moves." },
1836
1909
  { step: "Check reduced motion", how: "Turn on prefers-reduced-motion (or MotionProvider \"reduced\"): state still changes, nothing essential depends on animation, nothing keeps moving." },
1837
- { step: "Write the reuse inventory", how: "List the Sable components, parts, utilities and token groups used, and every app-owned UI or CSS addition with the catalog search that justified it. Start from the app-owned surfaces sable check lists (sable grade --json → inventory) and note, for each, whether you used the Sable part named beside it or kept your own. Either is fine for layout and look. Behaviour (controls, overlays, keyboard handling) comes from Sable." },
1910
+ { step: "Write the reuse inventory", how: "List the Sable components, parts, utilities and token groups used, and every app-owned UI or CSS addition with the catalog search that justified it. Start from the app-owned surfaces and rebuilt parts sable check lists (sable grade --json → inventory, rebuiltParts) and note, for each, whether you used the Sable part named beside it or kept your own, and why. Either is fine for layout and look. Behaviour (controls, overlays, keyboard handling) comes from Sable." },
1838
1911
  { step: "Record what you learned", how: "sable docs learning-from-builds — keep conditional lessons with evidence (sable learn --record); never record human approval you did not get." },
1839
1912
  ]
1840
1913
 
@@ -1904,7 +1977,7 @@ function cmdCheck() {
1904
1977
  const LABELS = { hallucinatedComponent: "hallucinated components", hallucinatedProp: "hallucinated props", radixIdiom: "Radix idioms", hardcodedControl: "hardcoded control heights", offContractIcon: "off-contract icons" }
1905
1978
  const found = Object.entries(LABELS).filter(([k]) => grade.counts[k])
1906
1979
  const reviewMissing = review.length > 0 && grade.review?.status !== "review-recorded"
1907
- const details = found.flatMap(([k]) => grade.findings[k].map((f) => `${LABELS[k]} — ${f.file}:${f.line} → ${f.name ?? f.prop ?? f.what ?? f.match}${f.component ? ` on ${f.component}` : ""}${f.from ? ` from ${f.from}${f.reason ? ` (${f.reason})` : ""}` : ""}`))
1980
+ const details = found.flatMap(([k]) => grade.findings[k].map((f) => `${LABELS[k]} — ${f.file}:${f.line} → ${f.name ?? f.prop ?? f.what ?? f.match}${f.component ? ` on ${f.component}` : ""}${f.from ? ` from ${f.from}${f.reason ? ` (${f.reason})` : ""}` : ""}${f.via ? ` via ${f.via}` : ""}${f.suggest?.length ? `; the set has ${f.suggest.join(", ")} (sable icon)` : ""}`))
1908
1981
  if (reviewMissing) details.push(`review: ${grade.review?.status ?? "missing"}${grade.review?.problems?.length ? ` — ${grade.review.problems.join("; ")}` : ""}`)
1909
1982
  checks.push({
1910
1983
  name: "grade",
@@ -1917,7 +1990,13 @@ function cmdCheck() {
1917
1990
  .filter(Boolean)
1918
1991
  .join("; "),
1919
1992
  details,
1920
- data: { score: grade.score, contract: grade.contract, counts: grade.counts, review: grade.review, findings: grade.findings },
1993
+ // A half-themed palette passes the contract and breaks the moment
1994
+ // someone switches modes, so it is said on the grade line, as a warning.
1995
+ notes: [
1996
+ ...(grade.modes?.status === "warn" ? [`warn — colour modes: ${grade.modes.lightOnly.length} colour token(s) set for light with no dark value (${grade.modes.lightOnly.slice(0, 4).map((t) => `--${t.name}`).join(", ")}${grade.modes.lightOnly.length > 4 ? ", …" : ""}) → ${grade.modes.fix}`] : []),
1997
+ ...(grade.modes && !grade.modes.modeControl ? ["colour modes: nothing imports ThemeSwitcher, ThemeSelector or useSableTheme, so no one can switch modes to review both → mount ThemeSwitcher under SableThemeProvider, or pin the one mode the brief asks for"] : []),
1998
+ ],
1999
+ data: { score: grade.score, contract: grade.contract, counts: grade.counts, review: grade.review, findings: grade.findings, modes: grade.modes },
1921
2000
  })
1922
2001
  }
1923
2002
  /* App-owned surfaces, from grade. Never a failure: the input to the reuse
@@ -1926,6 +2005,9 @@ function cmdCheck() {
1926
2005
  /* Adoption, from grade. Never a failure either: wiring can hold while the app
1927
2006
  * composes almost none of Sable, and the move onto it is the user's call. */
1928
2007
  const adoption = grade && grade.type !== "error" ? (grade.adoption ?? null) : null
2008
+ /* Rebuilt parts, from grade: elements and app CSS standing in for a part
2009
+ * Sable ships, or reaching into one. Notes, answered in the reuse inventory. */
2010
+ const rebuilt = grade && grade.type !== "error" ? (grade.rebuiltParts ?? null) : null
1929
2011
  const lowAdoption = adoption && (adoption.level === "none" || adoption.level === "low")
1930
2012
 
1931
2013
  const failed = checks.filter((c) => c.status !== "pass").map((c) => c.name)
@@ -1936,7 +2018,7 @@ function cmdCheck() {
1936
2018
  ...MANUAL_REVIEW,
1937
2019
  ...(doctor.checks.some((c) => c.name === "css:size") ? [{ step: "Ship only the CSS you use", how: "sable sources --write src/sable-sources.css, then @import it in place of the whole-package @source (about 32 KB of CSS gzipped down to what the app imports). Re-run it whenever you import something new from Sable; sable check fails while it is stale." }] : []),
1938
2020
  ]
1939
- const data = { paths, passed: !failed.length, failed, checks, inventory, adoption, manualReview: manual }
2021
+ const data = { paths, passed: !failed.length, failed, checks, inventory, adoption, rebuiltParts: rebuilt, manualReview: manual }
1940
2022
  const MARK = { pass: "✓", fail: "✗", error: "✗" }
1941
2023
  const MAX_DETAILS = 12
1942
2024
  const summaryText = () =>
@@ -1946,7 +2028,7 @@ function cmdCheck() {
1946
2028
  ` ${MARK[c.status]} ${c.name.padEnd(7)}${c.summary}`,
1947
2029
  ...c.details.slice(0, MAX_DETAILS).map((d) => ` ${d}`),
1948
2030
  ...(c.details.length > MAX_DETAILS ? [` … ${c.details.length - MAX_DETAILS} more: sable ${c.name}${c.name === "doctor" ? "" : ` ${paths.join(" ")}`}`] : []),
1949
- ...(c.notes ?? []).map((n) => ` note — ${n}`),
2031
+ ...(c.notes ?? []).map((n) => (n.startsWith("warn — ") ? ` ${n}` : ` note — ${n}`)),
1950
2032
  ]),
1951
2033
  "",
1952
2034
  failed.length ? `[sable check] ✗ ${failed.join(", ")} failed. Fix these and run sable check again.` : "[sable check] ✓ doctor, lint and grade pass.",
@@ -1962,6 +2044,17 @@ function cmdCheck() {
1962
2044
  "",
1963
2045
  ].join("\n")
1964
2046
  }
2047
+ const rebuiltText = () => {
2048
+ const list = rebuilt?.items ?? []
2049
+ if (!list.length) return ""
2050
+ return [
2051
+ "",
2052
+ `Rebuilt parts — ${list.length}. Sable ships these, or the app styles one from outside. Use the part (or its variant or prop), or say in the reuse inventory why yours stays. A part that cannot fit your design is a missing variant to report, not a reason to rebuild it.`,
2053
+ ...list.slice(0, MAX_DETAILS).map((r) => ` ${r.file}:${r.line} ${r.what}${r.count ? ` ×${r.count}` : ""} → ${r.consider}`),
2054
+ ...(list.length > MAX_DETAILS ? [` … ${list.length - MAX_DETAILS} more: sable grade ${paths.join(" ")} --json → rebuiltParts.items`] : []),
2055
+ "",
2056
+ ].join("\n")
2057
+ }
1965
2058
  const adoptionText = () => {
1966
2059
  if (!adoption || adoption.level === "empty" || (adoption.level === "sable" && !adoption.swaps.length)) return ""
1967
2060
  return [
@@ -2001,7 +2094,7 @@ function cmdCheck() {
2001
2094
  }
2002
2095
 
2003
2096
  if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "check", data }, null, DENSE ? 0 : 1)}\n`)
2004
- else process.stdout.write(`${summaryText()}\n${adoptionText()}${inventoryText()}${manualText()}\n`)
2097
+ else process.stdout.write(`${summaryText()}\n${adoptionText()}${rebuiltText()}${inventoryText()}${manualText()}\n`)
2005
2098
  logInvocation(failed.length ? 1 : 0, failed.length ? "ERR_CHECK_FAILED" : undefined)
2006
2099
  process.exit(failed.length ? 1 : 0)
2007
2100
  }
@@ -2091,6 +2184,7 @@ const HANDLERS = {
2091
2184
  setup: cmdSetup,
2092
2185
  check: cmdCheck,
2093
2186
  provider: cmdProvider,
2187
+ icon: cmdIcon,
2094
2188
  manifest: cmdManifest,
2095
2189
  search: cmdSearch,
2096
2190
  component: cmdComponent,
@@ -46,7 +46,7 @@ type TypeScaleToken = (typeof TYPE_SCALE_TOKENS)[number];
46
46
  * Set them at a theme or region scope, then keep consuming whole `text-*` tiers.
47
47
  * Compact controls still follow TYPE_UI_TOKENS for the workspace register.
48
48
  */
49
- declare const TYPE_REFINEMENT_TOKENS: readonly ["type-display-4xl-size", "type-title-lg-size", "type-title-sm-size", "type-body-lg-size", "type-body-lg-weight", "type-body-lg-leading", "type-body-lg-tracking", "type-body-md-size", "type-body-md-weight", "type-body-md-leading", "type-body-md-tracking", "type-body-sm-size", "type-body-sm-weight", "type-body-sm-leading", "type-body-sm-tracking", "type-body-xs-size", "type-body-xs-weight", "type-body-xs-leading", "type-body-xs-tracking", "type-label-lg-size", "type-label-lg-weight", "type-label-lg-leading", "type-label-lg-tracking", "type-label-md-size", "type-label-md-weight", "type-label-md-leading", "type-label-md-tracking", "type-label-sm-size", "type-label-sm-weight", "type-label-sm-leading", "type-label-sm-tracking", "type-label-xs-size", "type-label-xs-weight", "type-label-xs-leading", "type-label-xs-tracking", "type-label-xs-transform", "type-caption-size", "type-caption-weight", "type-caption-leading", "type-caption-tracking", "type-mono-xl-size", "type-mono-xl-weight", "type-mono-xl-leading", "type-mono-xl-tracking", "type-mono-lg-size", "type-mono-lg-weight", "type-mono-lg-leading", "type-mono-lg-tracking", "type-mono-md-size", "type-mono-md-weight", "type-mono-md-leading", "type-mono-md-tracking", "type-mono-caption-size", "type-mono-caption-weight", "type-mono-caption-leading", "type-mono-caption-tracking", "type-mono-sm-size", "type-mono-sm-weight", "type-mono-sm-leading", "type-mono-sm-tracking"];
49
+ declare const TYPE_REFINEMENT_TOKENS: readonly ["type-base", "type-display-4xl-size", "type-title-lg-size", "type-title-sm-size", "type-body-lg-size", "type-body-lg-weight", "type-body-lg-leading", "type-body-lg-tracking", "type-body-md-size", "type-body-md-weight", "type-body-md-leading", "type-body-md-tracking", "type-body-sm-size", "type-body-sm-weight", "type-body-sm-leading", "type-body-sm-tracking", "type-body-xs-size", "type-body-xs-weight", "type-body-xs-leading", "type-body-xs-tracking", "type-label-lg-size", "type-label-lg-weight", "type-label-lg-leading", "type-label-lg-tracking", "type-label-md-size", "type-label-md-weight", "type-label-md-leading", "type-label-md-tracking", "type-label-sm-size", "type-label-sm-weight", "type-label-sm-leading", "type-label-sm-tracking", "type-label-xs-size", "type-label-xs-weight", "type-label-xs-leading", "type-label-xs-tracking", "type-label-xs-transform", "type-caption-size", "type-caption-weight", "type-caption-leading", "type-caption-tracking", "type-mono-xl-size", "type-mono-xl-weight", "type-mono-xl-leading", "type-mono-xl-tracking", "type-mono-lg-size", "type-mono-lg-weight", "type-mono-lg-leading", "type-mono-lg-tracking", "type-mono-md-size", "type-mono-md-weight", "type-mono-md-leading", "type-mono-md-tracking", "type-mono-caption-size", "type-mono-caption-weight", "type-mono-caption-leading", "type-mono-caption-tracking", "type-mono-sm-size", "type-mono-sm-weight", "type-mono-sm-leading", "type-mono-sm-tracking"];
50
50
  type TypeRefinementToken = (typeof TYPE_REFINEMENT_TOKENS)[number];
51
51
  /**
52
52
  * Overline/eyebrow/kicker type tokens (mode-independent). A theme MAY override
@@ -206,7 +206,7 @@ declare const MARK_TOKENS: readonly ["destructive-mark", "success-mark", "tone-w
206
206
  * an unrecognised `text-*` class falls into tailwind-merge's COLOUR group and
207
207
  * silently evicts the real colour class, which is a genuinely hard bug to see.
208
208
  */
209
- declare const TYPE_UI_TOKENS: readonly ["type-ui-md-size", "type-ui-md-weight", "type-ui-sm-size", "type-ui-sm-weight", "type-ui-leading", "type-ui-section-weight", "type-ui-micro-size", "type-ui-micro-weight"];
209
+ declare const TYPE_UI_TOKENS: readonly ["type-ui-md-size", "type-ui-md-weight", "type-ui-sm-size", "type-ui-sm-weight", "type-ui-leading", "type-ui-section-weight", "type-ui-micro-size", "type-ui-micro-weight", "type-ui-title-size", "type-ui-title-weight", "type-ui-title-leading"];
210
210
  type TypeUiToken = (typeof TYPE_UI_TOKENS)[number];
211
211
  type ThemeMode = "light" | "dark";
212
212
  /** Lightweight, typed description of a theme (the TS manifest each theme ships). */
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { B as BlockMeta } from '../meta-DX5r5wQL.js';
2
+ import { B as BlockMeta } from '../meta-Dfm9AExe.js';
3
3
 
4
4
  interface ApiKeysBlockProps {
5
5
  className?: string;
@@ -13,9 +13,10 @@ import {
13
13
  } from "react";
14
14
  import { cn } from "../../lib/cn.js";
15
15
  import { GradientArt } from "../gradient-art/index.js";
16
+ import { isPaletteColor } from "../../lib/palette.js";
16
17
  import { mergeRefs } from "../../lib/refs.js";
17
18
  import { useResizeObserver } from "../../lib/use-resize-observer.js";
18
- import { avatarGroupVariants, avatarVariants } from "./avatar.variants.js";
19
+ import { AVATAR_SQUARE_RADIUS, avatarGroupVariants, avatarVariants } from "./avatar.variants.js";
19
20
  import { jsx, jsxs } from "react/jsx-runtime";
20
21
  var AvatarGroupContext = createContext(null);
21
22
  var STATUS_SCALE = {
@@ -26,12 +27,14 @@ var IMAGE_FADE = {
26
27
  xl: "motion-fade",
27
28
  "2xl": "motion-fade"
28
29
  };
29
- function AvatarRoot({ size, className, ...props }) {
30
+ function AvatarRoot({ size, shape, tone, className, ...props }) {
30
31
  const group = useContext(AvatarGroupContext);
32
+ const palette = isPaletteColor(tone) ? tone : void 0;
31
33
  return /* @__PURE__ */ jsx(
32
34
  Base.Root,
33
35
  {
34
- className: cn(avatarVariants({ size: group?.size ?? size }), className),
36
+ "data-palette": palette,
37
+ className: cn(avatarVariants({ size: group?.size ?? size, shape }), palette && "bg-palette-wash", className),
35
38
  ...props
36
39
  }
37
40
  );
@@ -51,7 +54,7 @@ function AvatarFallback({ className, ...props }) {
51
54
  return /* @__PURE__ */ jsx(
52
55
  Base.Fallback,
53
56
  {
54
- className: cn("text-muted-foreground", className),
57
+ className: cn("text-muted-foreground [[data-palette]>&]:text-palette-text", className),
55
58
  ...props
56
59
  }
57
60
  );
@@ -63,6 +66,8 @@ function Avatar({
63
66
  fallback,
64
67
  fallbackDelay,
65
68
  size,
69
+ shape,
70
+ tone,
66
71
  status,
67
72
  variant = "neutral",
68
73
  seed,
@@ -70,7 +75,7 @@ function Avatar({
70
75
  ...props
71
76
  }) {
72
77
  const named = !src && alt ? { role: "img", "aria-label": alt } : null;
73
- const avatar = /* @__PURE__ */ jsxs(AvatarRoot, { size, className: status == null ? className : void 0, ...named, ...props, children: [
78
+ const avatar = /* @__PURE__ */ jsxs(AvatarRoot, { size, shape, tone: variant === "art" ? void 0 : tone, className: status == null ? className : void 0, ...named, ...props, children: [
74
79
  src ? /* @__PURE__ */ jsx(AvatarImage, { src, alt: alt ?? "", className: size ? IMAGE_FADE[size] : void 0 }) : null,
75
80
  variant === "art" ? /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, className: "size-full", children: /* @__PURE__ */ jsxs(GradientArt, { seed: seed ?? (typeof fallback === "string" && fallback || alt || void 0), className: "flex size-full items-center justify-center text-overlay-foreground", children: [
76
81
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-0 bg-overlay/30" }),
@@ -78,14 +83,16 @@ function Avatar({
78
83
  ] }) }) : /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, children: fallback })
79
84
  ] });
80
85
  if (status == null) return avatar;
81
- return /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-status-anchor", className: cn("relative inline-flex shrink-0 rounded-full", className), children: [
86
+ const anchorRadius = shape === "square" ? AVATAR_SQUARE_RADIUS[size ?? "md"] : "rounded-full";
87
+ return /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-status-anchor", className: cn("relative inline-flex shrink-0", anchorRadius, className), children: [
82
88
  avatar,
83
89
  /* @__PURE__ */ jsx(
84
90
  "span",
85
91
  {
86
92
  "data-slot": "avatar-status",
87
93
  className: cn(
88
- "absolute end-[14.6%] bottom-[14.6%] flex translate-x-1/2 translate-y-1/2 rounded-full rtl:-translate-x-1/2 *:ring-2 *:ring-[color:var(--avatar-group-ring,var(--background))]",
94
+ "absolute flex translate-x-1/2 translate-y-1/2 rounded-full rtl:-translate-x-1/2 *:ring-2 *:ring-[color:var(--avatar-group-ring,var(--background))]",
95
+ shape === "square" ? "end-[6%] bottom-[6%]" : "end-[14.6%] bottom-[14.6%]",
89
96
  size ? STATUS_SCALE[size] : void 0
90
97
  ),
91
98
  children: status
@@ -110,6 +117,8 @@ function AvatarGroupCount({
110
117
  }
111
118
  AvatarGroupCount.displayName = "AvatarGroupCount";
112
119
  var FIT_MIN_WIDTH = {
120
+ "2xs": "min-w-9",
121
+ xs: "min-w-11",
113
122
  sm: "min-w-15",
114
123
  md: "min-w-[--spacing(18.5)]",
115
124
  lg: "min-w-22"