@sableui/react 0.17.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.
- package/AGENTS.md +10 -3
- package/CHANGELOG.md +235 -0
- package/README.md +11 -4
- package/bin/sable.mjs +266 -30
- package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/avatar/avatar.js +16 -7
- package/dist/components/avatar/avatar.meta.js +43 -10
- package/dist/components/avatar/avatar.variants.js +26 -2
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/collapsible/collapsible.js +6 -5
- package/dist/components/collapsible/collapsible.meta.js +14 -5
- package/dist/components/data-table/data-table.js +132 -45
- package/dist/components/data-table/data-table.meta.js +63 -4
- package/dist/components/date-picker/date-picker.js +13 -6
- package/dist/components/date-picker/date-picker.meta.js +22 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/menu/menu.js +2 -0
- package/dist/components/menu/menu.meta.js +2 -2
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/panel-group/panel-group.js +17 -1
- package/dist/components/panel-group/panel-group.meta.js +43 -3
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/components/side-nav/side-nav.variants.js +8 -3
- package/dist/components/sidebar/index.js +4 -0
- package/dist/components/sidebar/sidebar.js +90 -11
- package/dist/components/sidebar/sidebar.meta.js +38 -4
- package/dist/components/table/table.js +7 -3
- package/dist/components/tabs/tabs.js +10 -2
- package/dist/components/tabs/tabs.meta.js +4 -0
- package/dist/components/workspace/workspace.js +13 -3
- package/dist/components/workspace/workspace.meta.js +109 -1
- package/dist/file-hashes.json +94 -80
- package/dist/foundations/typography.js +13 -0
- package/dist/icon/Icon.js +21 -2
- package/dist/icon/index.d.ts +36 -1
- package/dist/icon/index.js +5 -0
- package/dist/icon/names.js +240 -0
- package/dist/icon-sets/reicon.js +41 -1
- package/dist/index.d.ts +540 -75
- package/dist/index.js +18 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/cn.js +1 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +1 -0
- package/dist/lib/use-media-query.js +41 -0
- package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
- package/dist/patterns/dashboard-page.js +23 -17
- package/dist/patterns/index.d.ts +7 -2
- package/dist/patterns/index.js +5 -1
- package/dist/patterns/tracker-page.js +392 -0
- package/dist/patterns/tracker-page.meta.js +40 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/styles.css +301 -188
- package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
- package/dist/themes/_contract.js +9 -1
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/avatar.md +32 -9
- package/llms/components/code-block.md +1 -0
- package/llms/components/collapsible.md +12 -4
- package/llms/components/data-table.md +40 -3
- package/llms/components/date-picker.md +15 -1
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/page-header.md +16 -1
- package/llms/components/panel-group.md +34 -2
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/sheet.md +2 -2
- package/llms/components/sidebar.md +17 -4
- package/llms/components/tabs.md +1 -0
- package/llms/components/workspace.md +81 -1
- package/llms/foundations/typography.md +4 -1
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/patterns/dashboard-page.md +18 -15
- package/llms/patterns/tracker-page.md +450 -0
- package/llms/reference/token-intent.md +3 -1
- package/llms/reference/utilities.md +4 -1
- package/llms/tasks/auditing-an-app.md +35 -1
- package/llms/tasks/building-a-page.md +3 -2
- package/llms/tasks/building-a-tool.md +101 -5
- package/llms/tasks/building-with-sable.md +11 -3
- package/llms/tasks/creating-a-preset.md +9 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +1215 -89
- package/llms.txt +11 -9
- package/package.json +7 -1
- package/registry.json +1021 -70
- package/scripts/adoption.mjs +252 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/app-css.mjs +129 -0
- package/scripts/grade.mjs +178 -2
- package/scripts/lint-rules.mjs +126 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +64 -6
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +9 -0
package/bin/sable.mjs
CHANGED
|
@@ -36,13 +36,15 @@
|
|
|
36
36
|
|
|
37
37
|
import { spawnSync } from "node:child_process"
|
|
38
38
|
import { appendFileSync, existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
39
|
+
import { createRequire } from "node:module"
|
|
39
40
|
import { dirname, join, relative, resolve } from "node:path"
|
|
40
41
|
import { fileURLToPath } from "node:url"
|
|
41
42
|
import { skeletonize } from "../scripts/skeleton.mjs"
|
|
42
43
|
import { claudeStopHook, installAgentSkill, installClaudeStopHook } from "../scripts/agent-setup.mjs"
|
|
44
|
+
import { adoptionReport, swapLines } from "../scripts/adoption.mjs"
|
|
43
45
|
import { patchChecks } from "../scripts/patches.mjs"
|
|
44
46
|
import { sourcesCheck } from "../scripts/sources.mjs"
|
|
45
|
-
import { detectFramework, detectPackageManager, fixWiring, fontsCheck, providerSnippet, tailwindBuildCheck } from "../scripts/wiring.mjs"
|
|
47
|
+
import { detectFramework, detectPackageManager, fixWiring, fontsCheck, installCommand, providerSnippet, tailwindBuildCheck } from "../scripts/wiring.mjs"
|
|
46
48
|
|
|
47
49
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
48
50
|
const PKG_ROOT = join(HERE, "..")
|
|
@@ -119,7 +121,7 @@ const COMMANDS = [
|
|
|
119
121
|
},
|
|
120
122
|
{
|
|
121
123
|
name: "setup",
|
|
122
|
-
summary: "Install package-backed skill entry files for coding agents, preserving local edits.",
|
|
124
|
+
summary: "Install package-backed skill entry files for coding agents, plus a short managed block in AGENTS.md / CLAUDE.md that sends every UI request through the skill, preserving local edits.",
|
|
123
125
|
arg: null,
|
|
124
126
|
flags: [
|
|
125
127
|
{ name: "--agent", value: null, describe: "Install skill entry files. Required." },
|
|
@@ -128,6 +130,7 @@ const COMMANDS = [
|
|
|
128
130
|
{ name: "--update", value: null, describe: "Refresh unchanged managed entries after a package upgrade." },
|
|
129
131
|
{ name: "--dry-run", value: null, describe: "Check and report the destination without writing files." },
|
|
130
132
|
{ name: "--hook", value: null, describe: "Also add a Claude Code Stop hook that runs `sable check` to .claude/settings.json (Claude targets only). Without it, setup prints the hook and writes nothing there." },
|
|
133
|
+
{ name: "--no-instructions", value: null, describe: "Skip the managed Sable block in AGENTS.md / CLAUDE.md. Without that block an agent uses the skill only when a request names it." },
|
|
131
134
|
],
|
|
132
135
|
returns: ["setup.agent"],
|
|
133
136
|
},
|
|
@@ -220,6 +223,13 @@ const COMMANDS = [
|
|
|
220
223
|
],
|
|
221
224
|
returns: ["provider.list", "provider.detail", "provider.props", "provider.examples"],
|
|
222
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
|
+
},
|
|
223
233
|
{
|
|
224
234
|
name: "token",
|
|
225
235
|
summary: "The token contract, with intent — what each token means and what it is NOT.",
|
|
@@ -249,7 +259,7 @@ const COMMANDS = [
|
|
|
249
259
|
},
|
|
250
260
|
{
|
|
251
261
|
name: "doctor",
|
|
252
|
-
summary: "Check this project's Sable wiring. Exits 1 on any failure.",
|
|
262
|
+
summary: "Check this project's Sable wiring, and warn when its UI barely uses Sable (adoption). Exits 1 on any wiring failure.",
|
|
253
263
|
arg: null,
|
|
254
264
|
flags: [
|
|
255
265
|
{ name: "--fix", value: null, describe: "Apply the fixes that need no judgment (imports in order, @source, missing packages, Vite dedupe and Tailwind plugin, fonts, IconProvider in a starter entry), print the rest, then check again." },
|
|
@@ -265,10 +275,26 @@ const COMMANDS = [
|
|
|
265
275
|
flags: [
|
|
266
276
|
{ name: "--dry-run", value: null, describe: "Report every change without writing or installing anything." },
|
|
267
277
|
{ name: "--no-agent", value: null, describe: "Skip the agent skill (sable setup --agent)." },
|
|
278
|
+
{ name: "--no-instructions", value: null, describe: "Install the skill but skip its managed block in AGENTS.md / CLAUDE.md." },
|
|
279
|
+
{ name: "--hook", value: null, describe: "Also add the Claude Code Stop hook that runs `sable check` before a task can end. In a terminal, init asks; elsewhere it only prints the hook." },
|
|
280
|
+
{ name: "--no-hook", value: null, describe: "Don't ask about the Stop hook." },
|
|
268
281
|
{ name: "--no-install", value: null, describe: "Print install commands instead of running them." },
|
|
269
282
|
],
|
|
270
283
|
returns: ["init"],
|
|
271
284
|
},
|
|
285
|
+
{
|
|
286
|
+
name: "upgrade",
|
|
287
|
+
summary: "Update @sableui/react to the latest release, then run the new version's init: skill entries and the AGENTS.md / CLAUDE.md block refreshed, wiring checked, adoption reported. Safe on an app that is already set up.",
|
|
288
|
+
arg: null,
|
|
289
|
+
flags: [
|
|
290
|
+
{ name: "--dry-run", value: null, describe: "Print the update command and preview init's changes without installing or writing anything." },
|
|
291
|
+
{ name: "--no-install", value: null, describe: "Skip the package update (you updated it yourself); just run the installed version's init." },
|
|
292
|
+
{ name: "--hook", value: null, describe: "Passed to init: add the Claude Code Stop hook." },
|
|
293
|
+
{ name: "--no-hook", value: null, describe: "Passed to init: don't ask about the Stop hook." },
|
|
294
|
+
{ name: "--no-instructions", value: null, describe: "Passed to init: skip the AGENTS.md / CLAUDE.md block." },
|
|
295
|
+
],
|
|
296
|
+
returns: ["init"],
|
|
297
|
+
},
|
|
272
298
|
{
|
|
273
299
|
name: "grade",
|
|
274
300
|
summary: "Static contract checks and evidence review. Original compositions carry no penalty; no automatic taste score.",
|
|
@@ -358,6 +384,7 @@ const ERROR_CODES = [
|
|
|
358
384
|
{ code: "ERR_PRESET_CONTRAST", when: "A label on a brand or status fill (primary, sidebar-primary, highlight, destructive, success, tone-warning, tone-info) misses 4.5:1 or APCA Lc 60. Apply the suggested pair, or record an `exceptions` entry with a reason." },
|
|
359
385
|
{ code: "ERR_AGENT_SETUP", when: "The packaged skill is missing, the destination is linked, or existing local edits would be overwritten." },
|
|
360
386
|
{ code: "ERR_INIT", when: "sable init ran outside an app root (no package.json)." },
|
|
387
|
+
{ code: "ERR_UPGRADE", when: "sable upgrade ran where @sableui/react is not a dependency, or the package manager's update failed." },
|
|
361
388
|
{ code: "ERR_UNKNOWN_COMMAND", when: "The first argument is not a command." },
|
|
362
389
|
{ code: "ERR_MISSING_ARGUMENT", when: "A command that requires an argument got none." },
|
|
363
390
|
{ code: "ERR_UNKNOWN_COMPONENT", when: "No component by that name. `suggestions` carries near matches." },
|
|
@@ -368,6 +395,7 @@ const ERROR_CODES = [
|
|
|
368
395
|
{ code: "ERR_UNKNOWN_UTILITY", when: "No utility or variant by that name. `suggestions` carries near matches." },
|
|
369
396
|
{ code: "ERR_UNKNOWN_DOC", when: "No package guide or doc mirror by that slug. `suggestions` carries near matches and the package guides." },
|
|
370
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." },
|
|
371
399
|
{ code: "ERR_UNKNOWN_EXAMPLE", when: "The component has no example with that label. `suggestions` carries near matches as commands." },
|
|
372
400
|
{ code: "ERR_LINT_INPUT", when: "sable lint --json: a path to lint does not exist." },
|
|
373
401
|
{ code: "ERR_CHECK_FAILED", when: "sable check: doctor, lint, or grade failed. `data.checks` names which." },
|
|
@@ -551,14 +579,32 @@ const importLine = (name) => `import { ${name} } from "@sableui/react"`
|
|
|
551
579
|
const PROVIDERS = R.providers?.items ?? []
|
|
552
580
|
const findProvider = (name) => PROVIDERS.find((p) => p.name.toLowerCase() === String(name).toLowerCase())
|
|
553
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
|
+
|
|
554
598
|
function findComponent(name) {
|
|
555
599
|
const hit = COMPONENTS.find((c) => c.name.toLowerCase() === name.toLowerCase())
|
|
556
600
|
if (hit) return hit
|
|
601
|
+
const part = findPart(name)
|
|
602
|
+
if (part) return { ...part.parent, partOf: part.parent.name, part: part.part }
|
|
557
603
|
fail(
|
|
558
604
|
"ERR_UNKNOWN_COMPONENT",
|
|
559
605
|
`No component named "${name}".`,
|
|
560
606
|
[
|
|
561
|
-
...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}?`),
|
|
562
608
|
`sable search "${name}" — searches summaries and keywords too`,
|
|
563
609
|
"sable component --list",
|
|
564
610
|
],
|
|
@@ -629,14 +675,23 @@ function cmdSearch() {
|
|
|
629
675
|
|
|
630
676
|
const pool = []
|
|
631
677
|
if (!type || type === "component")
|
|
632
|
-
for (const c of COMPONENTS)
|
|
678
|
+
for (const c of COMPONENTS) {
|
|
633
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
|
+
}
|
|
634
689
|
if (!type || type === "provider")
|
|
635
690
|
for (const p of PROVIDERS)
|
|
636
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 ?? ""}` }) })
|
|
637
692
|
if (!type || type === "pattern")
|
|
638
693
|
for (const p of PATTERNS)
|
|
639
|
-
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 ?? ""}` }) })
|
|
640
695
|
if (!type || type === "block")
|
|
641
696
|
for (const b of BLOCKS)
|
|
642
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 ?? ""}` }) })
|
|
@@ -696,7 +751,9 @@ function cmdSearch() {
|
|
|
696
751
|
retrieve:
|
|
697
752
|
h.kind === "example"
|
|
698
753
|
? `sable component ${h.component} --example "${h.label.replace(/"/g, '\\"')}"`
|
|
699
|
-
:
|
|
754
|
+
: h.partOf
|
|
755
|
+
? `sable component ${h.partOf} --props`
|
|
756
|
+
: `sable ${h.kind === "doc" ? "docs" : h.kind} ${h.name}`,
|
|
700
757
|
}))
|
|
701
758
|
const anyReference = results.some((h) => h.reference)
|
|
702
759
|
ok("search", { query, count: results.length, results }, () =>
|
|
@@ -704,7 +761,7 @@ function cmdSearch() {
|
|
|
704
761
|
...results.map((h) =>
|
|
705
762
|
DENSE
|
|
706
763
|
? `${h.kind}/${h.name}${h.reference ? " (reference)" : ""}`
|
|
707
|
-
: `${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 ?? "")}`,
|
|
708
765
|
),
|
|
709
766
|
...(anyReference && !DENSE
|
|
710
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."]
|
|
@@ -781,6 +838,36 @@ function firstSentence(s) {
|
|
|
781
838
|
return text
|
|
782
839
|
}
|
|
783
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
|
+
|
|
784
871
|
/* ── component ──────────────────────────────────────────────────────────────── */
|
|
785
872
|
|
|
786
873
|
function cmdComponent() {
|
|
@@ -813,12 +900,16 @@ function cmdComponent() {
|
|
|
813
900
|
// A provider is asked for the way a component is. Answer with its entry rather
|
|
814
901
|
// than ERR_UNKNOWN_COMPONENT, which is what `sable component SableThemeProvider`
|
|
815
902
|
// used to say.
|
|
816
|
-
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()
|
|
817
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` : ""
|
|
818
909
|
|
|
819
910
|
if (flags.has("props"))
|
|
820
|
-
return ok("component.props", { name: c.name, props: c.props ?? [], subcomponents: c.subcomponents ?? [] }, () =>
|
|
821
|
-
renderProps(c)
|
|
911
|
+
return ok("component.props", { name: c.name, ...(part ? { part } : {}), props: c.props ?? [], subcomponents: c.subcomponents ?? [] }, () =>
|
|
912
|
+
`${partLine}${part ? "\n" : ""}${renderProps(c)}`,
|
|
822
913
|
)
|
|
823
914
|
if (flags.has("examples"))
|
|
824
915
|
return ok("component.examples", { name: c.name, examples: c.examples ?? [] }, () => renderExamples(c))
|
|
@@ -849,6 +940,7 @@ function cmdComponent() {
|
|
|
849
940
|
import: importLine(c.name),
|
|
850
941
|
summary: c.summary,
|
|
851
942
|
...(c.composition ? { composition: c.composition } : {}),
|
|
943
|
+
...(part ? { part } : {}),
|
|
852
944
|
}
|
|
853
945
|
if (level !== "brief") {
|
|
854
946
|
data.props = c.props ?? []
|
|
@@ -866,6 +958,7 @@ function cmdComponent() {
|
|
|
866
958
|
|
|
867
959
|
ok("component.detail", data, () =>
|
|
868
960
|
[
|
|
961
|
+
partLine,
|
|
869
962
|
`${c.name} [${c.status}] ${c.group} register:${c.register}`,
|
|
870
963
|
c.summary,
|
|
871
964
|
c.composition
|
|
@@ -1471,6 +1564,21 @@ function doctorReport() {
|
|
|
1471
1564
|
)
|
|
1472
1565
|
}
|
|
1473
1566
|
|
|
1567
|
+
/* Adoption: wiring that holds says nothing about whether the app composes
|
|
1568
|
+
* Sable. An agent that never loaded the skill wires the stylesheet, uses the
|
|
1569
|
+
* tokens, and writes its own Button and Modal. A warning, never a failure:
|
|
1570
|
+
* check lists the swaps, and moving working screens is the user's call. */
|
|
1571
|
+
if (pkg && { ...pkg.dependencies, ...pkg.devDependencies }["@sableui/react"]) {
|
|
1572
|
+
const files = src
|
|
1573
|
+
.filter((f) => /\.(tsx|jsx)$/.test(f.path) && !/\.(test|spec|stories)\./.test(f.path))
|
|
1574
|
+
.map((f) => ({ rel: relative(CWD, f.path).replaceAll("\\", "/"), text: f.text }))
|
|
1575
|
+
const adoption = adoptionReport({ files, components: COMPONENTS, deps: { ...pkg.dependencies, ...pkg.devDependencies } })
|
|
1576
|
+
if (adoption.level === "none" || adoption.level === "low")
|
|
1577
|
+
add("warn", "adoption", `${adoption.summary} ${adoption.recommendation}`, "sable check src lists each swap; then ask your agent to /sable audit (or $sable audit) the app's use of Sable.")
|
|
1578
|
+
else if (adoption.level === "sable") add("pass", "adoption", adoption.summary)
|
|
1579
|
+
else if (adoption.level === "fresh") add("info", "adoption", adoption.summary)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1474
1582
|
/* Tailwind in the build, and the theme's fonts: both fail as a page that
|
|
1475
1583
|
* renders, just not as Sable — unstyled, or in system fonts. */
|
|
1476
1584
|
if (pkg) {
|
|
@@ -1567,7 +1675,7 @@ function cmdDoctorFix() {
|
|
|
1567
1675
|
* app still has to mount. Each step is the code its own command runs, so init
|
|
1568
1676
|
* can never wire an app differently from what doctor checks.
|
|
1569
1677
|
*/
|
|
1570
|
-
function cmdInit() {
|
|
1678
|
+
async function cmdInit() {
|
|
1571
1679
|
if (!existsSync(join(CWD, "package.json")))
|
|
1572
1680
|
fail("ERR_INIT", `No package.json in ${CWD}.`, ["Run sable init from your app's root.", "Starting fresh: pnpm create vite@latest my-app --template react-ts, then pnpm add @sableui/react && pnpm exec sable init"])
|
|
1573
1681
|
const dryRun = flags.has("dry-run")
|
|
@@ -1582,26 +1690,57 @@ function cmdInit() {
|
|
|
1582
1690
|
try {
|
|
1583
1691
|
// update: an unchanged managed entry from an older Sable is refreshed;
|
|
1584
1692
|
// one with local edits still refuses, and is reported for the user.
|
|
1585
|
-
const setup = installAgentSkill({ root: CWD, project: CWD, packageRoot: PKG_ROOT, target: "all", update: true, dryRun })
|
|
1586
|
-
agent = { status: dryRun ? "would-apply" : "applied", destinations: setup.destinations, next: setup.next }
|
|
1693
|
+
const setup = installAgentSkill({ root: CWD, project: CWD, packageRoot: PKG_ROOT, target: "all", update: true, dryRun, instructions: !flags.has("no-instructions") })
|
|
1694
|
+
agent = { status: dryRun ? "would-apply" : "applied", destinations: setup.destinations, instructions: setup.instructions, instructionsChanged: setup.instructionsChanged, next: setup.next }
|
|
1587
1695
|
} catch (error) {
|
|
1588
1696
|
agent = { status: "manual", detail: error.message, fix: "sable setup --agent --dry-run" }
|
|
1589
1697
|
}
|
|
1590
1698
|
|
|
1699
|
+
/* The Stop hook changes how Claude Code behaves, so init never adds it on its
|
|
1700
|
+
* own. A person at a terminal is asked; an agent running init (no TTY) gets
|
|
1701
|
+
* the one-line offer in the output, and --hook adds it either way. */
|
|
1702
|
+
let hook = null
|
|
1703
|
+
if (agent.status === "applied" || agent.status === "would-apply") {
|
|
1704
|
+
let want = flags.has("hook")
|
|
1705
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !JSON_OUT && !dryRun
|
|
1706
|
+
if (!want && !flags.has("no-hook") && interactive) {
|
|
1707
|
+
const { createInterface } = await import("node:readline/promises")
|
|
1708
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
1709
|
+
const answer = await rl.question("Add a Claude Code Stop hook, so Claude can't end a task while `sable check` fails? [Y/n] ")
|
|
1710
|
+
rl.close()
|
|
1711
|
+
want = !/^\s*n/i.test(answer)
|
|
1712
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
hook = want ? installClaudeStopHook({ root: CWD, project: CWD, dryRun }) : claudeStopHook({ root: CWD, project: CWD })
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
hook = { installed: false, error: error.message }
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1591
1720
|
const after = dryRun ? before : doctorReport()
|
|
1592
1721
|
verifyFixes(fixes, after)
|
|
1593
1722
|
const mounted = after.checks.find((c) => c.name === "icons")?.status === "pass"
|
|
1594
1723
|
const providers = { ...providerSnippet(framework), mounted }
|
|
1595
|
-
|
|
1724
|
+
// An app that is wired but built mostly without Sable needs the audit path
|
|
1725
|
+
// next, not a new build: `build` would start the product over.
|
|
1726
|
+
const lowAdoption = after.checks.some((c) => c.name === "adoption" && c.status === "warn")
|
|
1727
|
+
const data = { framework: { id: framework.id, label: framework.label }, packageManager, dryRun, fixes, agent, stopHook: hook, providers, doctor: after, next: lowAdoption ? EXISTING_APP_NEXT : (agent.next ?? null) }
|
|
1596
1728
|
|
|
1597
1729
|
if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "init", data }, null, DENSE ? 0 : 1)}\n`)
|
|
1598
1730
|
else {
|
|
1599
1731
|
const agentText = {
|
|
1600
|
-
applied: () => `Agent skill: installed at ${agent.destinations.join(", ")}
|
|
1601
|
-
"would-apply": () => `Agent skill: would install at ${agent.destinations.join(", ")}
|
|
1732
|
+
applied: () => `Agent skill: installed at ${agent.destinations.join(", ")}.\n${instructionsText({ ...agent, dryRun })}`,
|
|
1733
|
+
"would-apply": () => `Agent skill: would install at ${agent.destinations.join(", ")}.\n${instructionsText({ ...agent, dryRun })}`,
|
|
1602
1734
|
skipped: () => "Agent skill: skipped (--no-agent).",
|
|
1603
1735
|
manual: () => `Agent skill: not installed — ${agent.detail}\n → ${agent.fix}`,
|
|
1604
1736
|
}[agent.status]()
|
|
1737
|
+
const hookText = !hook
|
|
1738
|
+
? ""
|
|
1739
|
+
: hook.error
|
|
1740
|
+
? `Stop hook: not added — ${hook.error}`
|
|
1741
|
+
: hook.installed
|
|
1742
|
+
? `Stop hook: ${dryRun ? "would add to" : hook.alreadyPresent ? "already in" : "added to"} ${hook.settingsFile}. Claude can't end a task while sable check fails.`
|
|
1743
|
+
: `Stop hook (optional): sable init --hook, or sable setup --agent --hook, makes Claude Code run sable check before it can end a task.`
|
|
1605
1744
|
const iconsFix = fixes.find((f) => f.check === "icons")
|
|
1606
1745
|
const providerText = mounted
|
|
1607
1746
|
? "Providers: <IconProvider> is mounted. MotionProvider and SableThemeProvider are optional: sable provider --list."
|
|
@@ -1617,13 +1756,14 @@ function cmdInit() {
|
|
|
1617
1756
|
"",
|
|
1618
1757
|
fixes.length ? `${dryRun ? "Would fix:" : "Fixes:"}\n${fixLines(fixes)}` : "Nothing to fix.\n",
|
|
1619
1758
|
agentText,
|
|
1759
|
+
...(hookText ? [hookText] : []),
|
|
1620
1760
|
"",
|
|
1621
1761
|
providerText,
|
|
1622
1762
|
"",
|
|
1623
1763
|
`Doctor:\n${doctorLines(after.checks)}`,
|
|
1624
1764
|
after.failed
|
|
1625
1765
|
? `[sable init] ✗ ${after.failed} failure(s) ${dryRun ? "now" : "left; the steps marked ! need you"}. Re-check with sable doctor.`
|
|
1626
|
-
: `[sable init] ✓ wiring holds. Next: ${agent.next ?? "sable docs building-with-sable"}`,
|
|
1766
|
+
: `[sable init] ✓ wiring holds. Next: ${lowAdoption ? EXISTING_APP_NEXT : (agent.next ?? "sable docs building-with-sable")}`,
|
|
1627
1767
|
"",
|
|
1628
1768
|
].join("\n"),
|
|
1629
1769
|
)
|
|
@@ -1632,6 +1772,54 @@ function cmdInit() {
|
|
|
1632
1772
|
process.exit(after.failed ? 1 : 0)
|
|
1633
1773
|
}
|
|
1634
1774
|
|
|
1775
|
+
const EXISTING_APP_NEXT = "most of this app's UI isn't built from Sable yet. Tell your agent \"move this app onto Sable\": it lists what to swap and asks before changing anything."
|
|
1776
|
+
|
|
1777
|
+
/* ── upgrade ────────────────────────────────────────────────────────────────── */
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* The whole update in one command: the package, then the NEW version's init.
|
|
1781
|
+
* This process is the old CLI, so after installing it hands over to the
|
|
1782
|
+
* freshly installed bin rather than running its own init: the skill entries,
|
|
1783
|
+
* the instructions block and the checks must be the new release's.
|
|
1784
|
+
*/
|
|
1785
|
+
function cmdUpgrade() {
|
|
1786
|
+
let pkg
|
|
1787
|
+
try {
|
|
1788
|
+
pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf8"))
|
|
1789
|
+
} catch {
|
|
1790
|
+
fail("ERR_UPGRADE", `No package.json in ${CWD}.`, ["Run sable upgrade from your app's root."])
|
|
1791
|
+
}
|
|
1792
|
+
const spec = pkg.dependencies?.["@sableui/react"] ?? pkg.devDependencies?.["@sableui/react"]
|
|
1793
|
+
if (!spec) fail("ERR_UPGRADE", "@sableui/react is not a dependency of this project.", ["pnpm add @sableui/react, then pnpm exec sable init"])
|
|
1794
|
+
const pm = detectPackageManager(CWD)
|
|
1795
|
+
const dryRun = flags.has("dry-run")
|
|
1796
|
+
// A tarball, link or workspace copy is not something "latest" can replace.
|
|
1797
|
+
const local = /^(?:file:|link:|workspace:|portal:|\.{0,2}\/)/.test(String(spec))
|
|
1798
|
+
const install = installCommand(pm, ["@sableui/react@latest"], { dev: !pkg.dependencies?.["@sableui/react"] })
|
|
1799
|
+
const say = (line) => { if (!JSON_OUT) process.stdout.write(`${line}\n`) }
|
|
1800
|
+
|
|
1801
|
+
if (flags.has("no-install")) say("sable upgrade — skipping the package update (--no-install).")
|
|
1802
|
+
else if (local) say(`sable upgrade — @sableui/react comes from ${spec}, which "latest" can't replace. Update that copy yourself; running the installed version's init.`)
|
|
1803
|
+
else if (dryRun) say(`sable upgrade — would run: ${install.join(" ")}`)
|
|
1804
|
+
else {
|
|
1805
|
+
say(`sable upgrade — ${install.join(" ")}`)
|
|
1806
|
+
const r = spawnSync(install[0], install.slice(1), { cwd: CWD, stdio: JSON_OUT ? "pipe" : "inherit", shell: process.platform === "win32" })
|
|
1807
|
+
if (r.status !== 0) fail("ERR_UPGRADE", `${install.join(" ")} exited ${r.status}.`, ["Fix the install, then run sable upgrade --no-install."])
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// The installed package's own CLI, resolved the way the skill resolves it.
|
|
1811
|
+
let bin = join(PKG_ROOT, "bin", "sable.mjs")
|
|
1812
|
+
try {
|
|
1813
|
+
const registry = createRequire(join(CWD, "package.json")).resolve("@sableui/react/registry.json")
|
|
1814
|
+
const installed = join(dirname(registry), "bin", "sable.mjs")
|
|
1815
|
+
if (existsSync(installed)) bin = installed
|
|
1816
|
+
} catch { /* not resolvable from here (a workspace copy): this CLI's init is the installed one */ }
|
|
1817
|
+
const passed = ["dry-run", "hook", "no-hook", "no-instructions", "json"].filter((f) => flags.has(f)).map((f) => `--${f}`)
|
|
1818
|
+
const r = spawnSync(process.execPath, [bin, "init", ...passed], { cwd: CWD, stdio: "inherit" })
|
|
1819
|
+
logInvocation(r.status ?? 1)
|
|
1820
|
+
process.exit(r.status ?? 1)
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1635
1823
|
function doctorLines(checks, indent = " ") {
|
|
1636
1824
|
const mark = { pass: "✓", warn: "!", fail: "✗", info: "·" }
|
|
1637
1825
|
return checks
|
|
@@ -1719,7 +1907,7 @@ const MANUAL_REVIEW = [
|
|
|
1719
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." },
|
|
1720
1908
|
{ step: "Review the motion", how: "sable docs reviewing-motion — observe playback and interruption for anything that moves." },
|
|
1721
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." },
|
|
1722
|
-
{ 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." },
|
|
1723
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." },
|
|
1724
1912
|
]
|
|
1725
1913
|
|
|
@@ -1751,7 +1939,8 @@ function cmdCheck() {
|
|
|
1751
1939
|
details: doctorFailures.map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
|
|
1752
1940
|
// Warnings and notes don't fail the check, but they are things to act on,
|
|
1753
1941
|
// and a passing check that printed nothing else hid them from the agent.
|
|
1754
|
-
|
|
1942
|
+
// Adoption is reported once, from grade, with its swaps.
|
|
1943
|
+
notes: doctor.checks.filter((c) => (c.status === "warn" || c.status === "info") && c.name !== "adoption").map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
|
|
1755
1944
|
data: doctor,
|
|
1756
1945
|
})
|
|
1757
1946
|
|
|
@@ -1788,7 +1977,7 @@ function cmdCheck() {
|
|
|
1788
1977
|
const LABELS = { hallucinatedComponent: "hallucinated components", hallucinatedProp: "hallucinated props", radixIdiom: "Radix idioms", hardcodedControl: "hardcoded control heights", offContractIcon: "off-contract icons" }
|
|
1789
1978
|
const found = Object.entries(LABELS).filter(([k]) => grade.counts[k])
|
|
1790
1979
|
const reviewMissing = review.length > 0 && grade.review?.status !== "review-recorded"
|
|
1791
|
-
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)` : ""}`))
|
|
1792
1981
|
if (reviewMissing) details.push(`review: ${grade.review?.status ?? "missing"}${grade.review?.problems?.length ? ` — ${grade.review.problems.join("; ")}` : ""}`)
|
|
1793
1982
|
checks.push({
|
|
1794
1983
|
name: "grade",
|
|
@@ -1801,20 +1990,35 @@ function cmdCheck() {
|
|
|
1801
1990
|
.filter(Boolean)
|
|
1802
1991
|
.join("; "),
|
|
1803
1992
|
details,
|
|
1804
|
-
|
|
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 },
|
|
1805
2000
|
})
|
|
1806
2001
|
}
|
|
1807
2002
|
/* App-owned surfaces, from grade. Never a failure: the input to the reuse
|
|
1808
2003
|
* inventory (finish step 6), which otherwise gets written from memory. */
|
|
1809
2004
|
const inventory = grade && grade.type !== "error" ? (grade.inventory ?? null) : null
|
|
2005
|
+
/* Adoption, from grade. Never a failure either: wiring can hold while the app
|
|
2006
|
+
* composes almost none of Sable, and the move onto it is the user's call. */
|
|
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
|
|
2011
|
+
const lowAdoption = adoption && (adoption.level === "none" || adoption.level === "low")
|
|
1810
2012
|
|
|
1811
2013
|
const failed = checks.filter((c) => c.status !== "pass").map((c) => c.name)
|
|
1812
2014
|
// A build that still scans all of Sable ships every component's classes;
|
|
1813
2015
|
// trimming it is a finish step, because sources are only final once the app is.
|
|
1814
|
-
const manual =
|
|
1815
|
-
? [
|
|
1816
|
-
|
|
1817
|
-
|
|
2016
|
+
const manual = [
|
|
2017
|
+
...(lowAdoption ? [{ step: "Ask about moving onto Sable", how: `${adoption.recommendation} The swaps are listed under Adoption above; sable docs auditing-an-app ("Adoption") covers how to put the choice to the user.` }] : []),
|
|
2018
|
+
...MANUAL_REVIEW,
|
|
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." }] : []),
|
|
2020
|
+
]
|
|
2021
|
+
const data = { paths, passed: !failed.length, failed, checks, inventory, adoption, rebuiltParts: rebuilt, manualReview: manual }
|
|
1818
2022
|
const MARK = { pass: "✓", fail: "✗", error: "✗" }
|
|
1819
2023
|
const MAX_DETAILS = 12
|
|
1820
2024
|
const summaryText = () =>
|
|
@@ -1824,7 +2028,7 @@ function cmdCheck() {
|
|
|
1824
2028
|
` ${MARK[c.status]} ${c.name.padEnd(7)}${c.summary}`,
|
|
1825
2029
|
...c.details.slice(0, MAX_DETAILS).map((d) => ` ${d}`),
|
|
1826
2030
|
...(c.details.length > MAX_DETAILS ? [` … ${c.details.length - MAX_DETAILS} more: sable ${c.name}${c.name === "doctor" ? "" : ` ${paths.join(" ")}`}`] : []),
|
|
1827
|
-
...(c.notes ?? []).map((n) => ` note — ${n}`),
|
|
2031
|
+
...(c.notes ?? []).map((n) => (n.startsWith("warn — ") ? ` ${n}` : ` note — ${n}`)),
|
|
1828
2032
|
]),
|
|
1829
2033
|
"",
|
|
1830
2034
|
failed.length ? `[sable check] ✗ ${failed.join(", ")} failed. Fix these and run sable check again.` : "[sable check] ✓ doctor, lint and grade pass.",
|
|
@@ -1840,6 +2044,27 @@ function cmdCheck() {
|
|
|
1840
2044
|
"",
|
|
1841
2045
|
].join("\n")
|
|
1842
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
|
+
}
|
|
2058
|
+
const adoptionText = () => {
|
|
2059
|
+
if (!adoption || adoption.level === "empty" || (adoption.level === "sable" && !adoption.swaps.length)) return ""
|
|
2060
|
+
return [
|
|
2061
|
+
"",
|
|
2062
|
+
`Adoption — ${adoption.level}. ${adoption.summary}`,
|
|
2063
|
+
` ${adoption.recommendation}`,
|
|
2064
|
+
...swapLines(adoption, MAX_DETAILS),
|
|
2065
|
+
"",
|
|
2066
|
+
].join("\n")
|
|
2067
|
+
}
|
|
1843
2068
|
const manualText = () =>
|
|
1844
2069
|
[
|
|
1845
2070
|
"",
|
|
@@ -1869,7 +2094,7 @@ function cmdCheck() {
|
|
|
1869
2094
|
}
|
|
1870
2095
|
|
|
1871
2096
|
if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "check", data }, null, DENSE ? 0 : 1)}\n`)
|
|
1872
|
-
else process.stdout.write(`${summaryText()}\n${inventoryText()}${manualText()}\n`)
|
|
2097
|
+
else process.stdout.write(`${summaryText()}\n${adoptionText()}${rebuiltText()}${inventoryText()}${manualText()}\n`)
|
|
1873
2098
|
logInvocation(failed.length ? 1 : 0, failed.length ? "ERR_CHECK_FAILED" : undefined)
|
|
1874
2099
|
process.exit(failed.length ? 1 : 0)
|
|
1875
2100
|
}
|
|
@@ -1899,6 +2124,15 @@ function cmdDesignTool(script) {
|
|
|
1899
2124
|
passThrough(runScript(join(PKG_ROOT, "scripts", script)))
|
|
1900
2125
|
}
|
|
1901
2126
|
|
|
2127
|
+
/** What setup did to the project instructions, in one line. */
|
|
2128
|
+
function instructionsText(data) {
|
|
2129
|
+
if (!data.instructions.length) return "Project instructions: skipped (--no-instructions). Agents use the skill only when a request names it."
|
|
2130
|
+
const changed = data.instructionsChanged
|
|
2131
|
+
const files = data.instructions.join(", ")
|
|
2132
|
+
if (!changed.length) return `Project instructions: ${files} already carry the Sable block.`
|
|
2133
|
+
return `Project instructions: ${data.dryRun ? "would add" : "added"} the Sable block to ${changed.join(", ")}, so every UI request goes through the skill. Edit outside its markers; --no-instructions skips it.`
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1902
2136
|
function cmdSetup() {
|
|
1903
2137
|
if (!flags.has("agent") || rest.length) fail("ERR_BAD_ARGUMENT", "Use sable setup --agent from your project root.", ["sable setup --agent --dry-run"])
|
|
1904
2138
|
const root = flags.get("root")
|
|
@@ -1908,7 +2142,7 @@ function cmdSetup() {
|
|
|
1908
2142
|
if (flags.has("hook") && !forClaude)
|
|
1909
2143
|
fail("ERR_BAD_ARGUMENT", "--hook installs a Claude Code Stop hook; use it with --target claude or all.", ["sable setup --agent --target claude --hook"])
|
|
1910
2144
|
try {
|
|
1911
|
-
const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run") }
|
|
2145
|
+
const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run"), instructions: !flags.has("no-instructions") }
|
|
1912
2146
|
// Preflight the settings file too, so a refusal there writes nothing anywhere.
|
|
1913
2147
|
if (flags.has("hook")) installClaudeStopHook({ ...options, dryRun: true })
|
|
1914
2148
|
const data = installAgentSkill(options)
|
|
@@ -1922,7 +2156,7 @@ function cmdSetup() {
|
|
|
1922
2156
|
: hook.installed
|
|
1923
2157
|
? `\n${data.dryRun ? "Would add" : hook.alreadyPresent ? "Already present:" : "Added"} a Claude Code Stop hook to ${hook.settingsFile}: Claude cannot end a task while \`sable check\` fails.`
|
|
1924
2158
|
: `\nOptional: block Claude Code from ending a task while \`sable check\` fails. Add this to ${hook.settingsFile} (merge into an existing "hooks" key), or rerun with --hook to add it:\n\n${JSON.stringify(hook.settings, null, 2)}\n\nThe hook runs: ${hook.command}`
|
|
1925
|
-
ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\nNext: ${data.next}${hookText}`)
|
|
2159
|
+
ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\n${instructionsText(data)}\nNext: ${data.next}${hookText}`)
|
|
1926
2160
|
} catch (error) {
|
|
1927
2161
|
fail("ERR_AGENT_SETUP", error.message, ["sable setup --agent --dry-run"])
|
|
1928
2162
|
}
|
|
@@ -1950,6 +2184,7 @@ const HANDLERS = {
|
|
|
1950
2184
|
setup: cmdSetup,
|
|
1951
2185
|
check: cmdCheck,
|
|
1952
2186
|
provider: cmdProvider,
|
|
2187
|
+
icon: cmdIcon,
|
|
1953
2188
|
manifest: cmdManifest,
|
|
1954
2189
|
search: cmdSearch,
|
|
1955
2190
|
component: cmdComponent,
|
|
@@ -1961,6 +2196,7 @@ const HANDLERS = {
|
|
|
1961
2196
|
docs: cmdDocs,
|
|
1962
2197
|
doctor: cmdDoctor,
|
|
1963
2198
|
init: cmdInit,
|
|
2199
|
+
upgrade: cmdUpgrade,
|
|
1964
2200
|
grade: cmdGrade,
|
|
1965
2201
|
learn: cmdLearn,
|
|
1966
2202
|
atlas: () => cmdDesignTool("design-atlas.mjs"),
|
|
@@ -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). */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** One day of activity. Only `date` and `value` are required. */
|
|
2
|
+
interface ActivityDay {
|
|
3
|
+
/** The calendar day, "YYYY-MM-DD". Read as a day, never as an instant. */
|
|
4
|
+
date: string;
|
|
5
|
+
/** The measure for that day. A day missing from the data counts as 0. */
|
|
6
|
+
value: number;
|
|
7
|
+
/** 0…levels-1. Overrides the computed level — GitHub's API sends its own. */
|
|
8
|
+
level?: number;
|
|
9
|
+
/** A key into the heatmap's `categories`, for multi-source cells. */
|
|
10
|
+
category?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Days as a list, or as a map of date to value (GitLab's and Mantine's shape). */
|
|
13
|
+
type ActivityData = ActivityDay[] | Record<string, number>;
|
|
14
|
+
/**
|
|
15
|
+
* Several sources' days merged into one list with categories: a day active in
|
|
16
|
+
* one source takes that source's key, a day active in more than one takes
|
|
17
|
+
* `both`. Values sum. For "GitHub, Codex, or both on that day".
|
|
18
|
+
*
|
|
19
|
+
* @example mergeActivity({ github: githubDays, codex: codexDays })
|
|
20
|
+
*/
|
|
21
|
+
declare function mergeActivity(sources: Record<string, ActivityData>, { both }?: {
|
|
22
|
+
both?: string;
|
|
23
|
+
}): ActivityDay[];
|
|
24
|
+
|
|
25
|
+
export { type ActivityDay as A, type ActivityData as a, mergeActivity as m };
|