@sableui/react 0.3.1 → 0.3.2

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 (36) hide show
  1. package/AGENTS.md +5 -1
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +5 -1
  4. package/bin/sable.mjs +6 -2
  5. package/dist/{_contract-o3tUc8Ig.d.ts → _contract-BlKMR4eX.d.ts} +6 -1
  6. package/dist/blocks/index.js +2 -2
  7. package/dist/{chunk-CKNM7KSB.js → chunk-CCUDFLU3.js} +23 -6
  8. package/dist/{chunk-6LCIBMMG.js → chunk-D2ETQBHV.js} +1 -1
  9. package/dist/{chunk-WCFLQVK5.js → chunk-FBSL2LZQ.js} +1 -1
  10. package/dist/{chunk-X276MF2W.js → chunk-OQACYT2D.js} +2 -0
  11. package/dist/index.d.ts +7 -7
  12. package/dist/index.js +8 -8
  13. package/dist/patterns/index.js +2 -2
  14. package/dist/presets/index.d.ts +1 -1
  15. package/dist/presets/index.js +1 -1
  16. package/dist/styles.css +9 -0
  17. package/dist/{theme-provider-BE-KqUG-.d.ts → theme-provider-CuYYxF0o.d.ts} +1 -1
  18. package/dist/themes/atelier/index.d.ts +1 -1
  19. package/dist/themes/base/index.d.ts +1 -1
  20. package/dist/themes/ember/index.d.ts +1 -1
  21. package/dist/themes/index.d.ts +2 -2
  22. package/dist/themes/meridian/index.d.ts +1 -1
  23. package/dist/themes/poppy/index.d.ts +1 -1
  24. package/dist/themes/prism/index.d.ts +1 -1
  25. package/dist/themes/verdant/index.d.ts +1 -1
  26. package/llms/components/button.md +15 -3
  27. package/llms/components/input.md +1 -1
  28. package/llms/components/scrub-field.md +1 -1
  29. package/llms/components/select.md +1 -1
  30. package/llms/reference/token-intent.md +5 -1
  31. package/llms/tasks/getting-started.md +4 -0
  32. package/llms-full.txt +27 -7
  33. package/package.json +1 -1
  34. package/registry.json +2446 -8
  35. package/scripts/agent-setup.mjs +34 -7
  36. package/scripts/grade.mjs +33 -7
@@ -1,14 +1,40 @@
1
1
  import { createHash } from "node:crypto"
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
- import { dirname, join } from "node:path"
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
4
4
 
5
5
  const hash = content => createHash("sha256").update(content).digest("hex")
6
6
 
7
- /** Install discoverable loaders; the installed package owns the actual workflow. */
8
- export function installAgentSkill({ root, packageRoot, target = "all", update = false, dryRun = false }) {
7
+ const RESOLVE = `node -p "require.resolve('@sableui/react/registry.json')"`
8
+ const CONTEXT = "From the target project's dependency context, run:"
9
+
10
+ /**
11
+ * The entry as written at `root`. When Sable is a dependency of a workspace
12
+ * package rather than of the root, a bare `require.resolve` from the root fails
13
+ * (pnpm does not hoist it), so the entry names the package and resolves through it.
14
+ */
15
+ function entryContent(source, projectPath) {
16
+ if (!projectPath) return source
17
+ if (!source.includes(RESOLVE) || !source.includes(CONTEXT)) throw new Error("The installed skill entry has an unexpected shape. Reinstall Sable.")
18
+ return source
19
+ .replace(CONTEXT, `Sable is installed in this repository's \`${projectPath}\` package. From the repository root, run:`)
20
+ .replace(RESOLVE, `node -p "require.resolve('@sableui/react/registry.json', { paths: ['${projectPath}'] })"`)
21
+ }
22
+
23
+ /**
24
+ * Install discoverable loaders; the installed package owns the actual workflow.
25
+ *
26
+ * `project` is where Sable is a dependency (the directory setup runs in); `root`
27
+ * is where the agent entries go. They differ in a monorepo, where agents start at
28
+ * the repository root but Sable belongs to one workspace package.
29
+ */
30
+ export function installAgentSkill({ root, project = root, packageRoot, target = "all", update = false, dryRun = false }) {
9
31
  const reject = (message) => { throw new Error(message) }
10
32
  if (!["all", "codex", "claude", "cursor"].includes(target)) reject("Choose --target all, codex, claude, or cursor.")
11
- if (!existsSync(join(root, "package.json"))) reject("Run setup from a project with a package.json.")
33
+ if (!existsSync(join(project, "package.json"))) reject("Run setup from a project with a package.json.")
34
+ if (!existsSync(root) || !statSync(root).isDirectory()) reject(`--root is not a directory: ${root}`)
35
+ const projectPath = relative(resolve(root), resolve(project)).replaceAll("\\", "/")
36
+ if (projectPath.startsWith("..") || isAbsolute(projectPath)) reject("--root must be the project directory or one of its ancestors, such as the repository root.")
37
+ if (/['"\\`]/.test(projectPath)) reject(`Unsupported characters in the project path: ${projectPath}`)
12
38
  const canonical = join(packageRoot, "skills/sable/SKILL.md")
13
39
  const entry = join(packageRoot, "skills/entry/SKILL.md")
14
40
  if (!existsSync(canonical) || !existsSync(entry)) reject("The installed package is missing its skill. Reinstall Sable.")
@@ -29,8 +55,9 @@ export function installAgentSkill({ root, packageRoot, target = "all", update =
29
55
  receipt = JSON.parse(readFileSync(join(root, receiptPath), "utf8"))
30
56
  if (receipt.schemaVersion !== 1 || !receipt.files || typeof receipt.files !== "object") reject("Invalid Sable setup receipt.")
31
57
  }
58
+ const skill = entryContent(readFileSync(entry, "utf8"), projectPath)
32
59
  const files = destinations.flatMap(destination => [
33
- { path: `${destination}/SKILL.md`, content: readFileSync(entry, "utf8") },
60
+ { path: `${destination}/SKILL.md`, content: skill },
34
61
  ...(destination.startsWith(".agents/") ? [{ path: `${destination}/agents/openai.yaml`, content: readFileSync(join(packageRoot, "skills/sable/agents/openai.yaml"), "utf8") }] : []),
35
62
  ])
36
63
  // Preflight every host and the receipt before any writes; preserve local edits.
@@ -50,5 +77,5 @@ export function installAgentSkill({ root, packageRoot, target = "all", update =
50
77
  mkdirSync(dirname(join(root, receiptPath)), { recursive: true })
51
78
  writeFileSync(join(root, receiptPath), JSON.stringify(receipt, null, 2) + "\n")
52
79
  }
53
- return { destination: destinations[0], destinations, files: files.map(file => file.path), dryRun, update, next: "Use $sable build <your brief> in Codex or /sable build <your brief> in Claude Code or Cursor." }
80
+ return { destination: destinations[0], destinations, root, project: projectPath || ".", files: files.map(file => file.path), dryRun, update, next: "Use $sable build <your brief> in Codex or /sable build <your brief> in Claude Code or Cursor." }
54
81
  }
package/scripts/grade.mjs CHANGED
@@ -45,7 +45,7 @@ const argv = process.argv.slice(2)
45
45
  const flags = new Map()
46
46
  const targets = []
47
47
  const BOOLEAN_FLAGS = new Set(["json", "help", "template", "strict"])
48
- const VALUE_FLAGS = new Set(["cli-log", "review"])
48
+ const VALUE_FLAGS = new Set(["cli-log", "review", "icons"])
49
49
  const JSON_OUT = argv.includes("--json")
50
50
  function abort(message) {
51
51
  const error = { type: "error", code: "ERR_GRADE_INPUT", message, suggestions: ["sable grade --help", "sable grade --template"] }
@@ -72,12 +72,13 @@ if (flags.has("help") || !targets.length) {
72
72
  [
73
73
  "grade — check the static contract and recorded review evidence. No model calls.",
74
74
  "",
75
- " sable grade <dir...> [--json] [--review <review.json>] [--strict]",
75
+ " sable grade <dir...> [--json] [--review <review.json>] [--icons <icons.json>] [--strict]",
76
76
  "",
77
77
  " --json machine-readable scorecard",
78
78
  " --cli-log a SABLE_CLI_LOG jsonl, to report whether the CLI was used at all",
79
79
  " --template print a review JSON template; fill after inspecting the rendered build",
80
80
  " --review validate evidence files and observations; paths are relative to this JSON",
81
+ ' --icons icon names the app adds with composeIconSets: { "names": [...] }. Default: .sable/icons.json',
81
82
  " --strict exit 1 for static findings or a missing/incomplete/unresolved review",
82
83
  "",
83
84
  "Checks: design-contract violations · hallucinated components and props ·",
@@ -102,7 +103,27 @@ const motionTokens = new Set(
102
103
  ].filter(Boolean),
103
104
  )
104
105
  const RULES = makeRules({ semanticTokens, motionTokens }).filter((r) => r.scope !== "package")
106
+ const PROP_SURFACE = REGISTRY.propSurface?.items ?? {}
105
107
  const ICONS = new Set(REGISTRY.iconContract?.names ?? [])
108
+ /**
109
+ * The names an app adds with `composeIconSets(appExtras, reiconIconSet)`. That is
110
+ * the sanctioned way to extend the set, and the grade cannot read a set's names
111
+ * out of a render function, so the app declares them once. Without this every
112
+ * domain icon read as off-contract.
113
+ */
114
+ const APP_ICONS = (() => {
115
+ const explicit = flags.get("icons")
116
+ const path = resolve(CWD, explicit ?? ".sable/icons.json")
117
+ if (!existsSync(path)) {
118
+ if (explicit) abort(`Not found: ${explicit}`)
119
+ return null
120
+ }
121
+ let names
122
+ try { names = JSON.parse(readFileSync(path, "utf8")).names }
123
+ catch (error) { abort(`Cannot read ${relative(CWD, path)}: ${error.message}`) }
124
+ if (!Array.isArray(names) || !names.every((n) => typeof n === "string" && n)) abort(`${relative(CWD, path)} must be { "names": ["icon-name", …] }.`)
125
+ return { file: relative(CWD, path).replaceAll("\\", "/"), names: new Set(names) }
126
+ })()
106
127
  const PATTERNS = REGISTRY.patterns?.items ?? []
107
128
 
108
129
  /**
@@ -275,11 +296,14 @@ for (const file of files) {
275
296
  }
276
297
  }
277
298
 
278
- /* 3. Props set on a Sable component that its contract does not declare. */
299
+ /* 3. Props set on a Sable component that its contract does not declare.
300
+ The contract is the component's real type (registry → propSurface),
301
+ which includes what it forwards to Base UI — Toggle's `pressed`,
302
+ Combobox's `filter` — plus its documented table. */
279
303
  for (const m of text.matchAll(/<([A-Z][\w]*)[\s/>]/g)) {
280
304
  const comp = REGISTRY.components?.find((c) => c.name === m[1])
281
305
  if (!comp?.props?.length) continue
282
- const declared = new Set(comp.props.map((p) => p.name))
306
+ const declared = new Set([...comp.props.map((p) => p.name), ...(PROP_SURFACE[m[1]] ?? [])])
283
307
  for (const sub of comp.subcomponents ?? []) {
284
308
  if (typeof sub !== "string") for (const p of sub.props ?? []) declared.add(p.name)
285
309
  }
@@ -307,9 +331,10 @@ for (const file of files) {
307
331
  if (px >= 20 && px <= 56) findings.hardcodedControl.push({ file: rel, match: m[0], line: lineOf(text, m.index) })
308
332
  }
309
333
 
310
- /* 6. Icon names outside the contract render nothing at all. */
334
+ /* 6. Icon names outside the contract render nothing at all — unless the app
335
+ supplies them itself and has declared them (APP_ICONS). */
311
336
  for (const m of text.matchAll(/<Icon\s+[^>]*name=["']([^"']+)["']/g)) {
312
- if (!ICONS.has(m[1])) findings.offContractIcon.push({ file: rel, name: m[1], line: lineOf(text, m.index) })
337
+ if (!ICONS.has(m[1]) && !APP_ICONS?.names.has(m[1])) findings.offContractIcon.push({ file: rel, name: m[1], line: lineOf(text, m.index) })
313
338
  }
314
339
 
315
340
  /* 7. Composition. Anything exporting a component that returns a big tree is
@@ -447,6 +472,7 @@ const result = {
447
472
  per100loc: Object.fromEntries(Object.entries(counts).map(([k, v]) => [k, per100(v)])),
448
473
  conformance: { informationalOnly: true, mean: meanConformance, routes: conformance },
449
474
  cliUsage,
475
+ appIcons: APP_ICONS ? { file: APP_ICONS.file, names: APP_ICONS.names.size } : null,
450
476
  findings,
451
477
  }
452
478
 
@@ -468,7 +494,7 @@ row("hallucinated components", counts.hallucinatedComponent)
468
494
  row("hallucinated props", counts.hallucinatedProp)
469
495
  row("Radix idioms", counts.radixIdiom, counts.radixIdiom ? "asChild / direct primitive imports" : "")
470
496
  row("hardcoded control heights", counts.hardcodedControl)
471
- row("off-contract icon names", counts.offContractIcon)
497
+ row("off-contract icon names", counts.offContractIcon, APP_ICONS ? `+${APP_ICONS.names.size} app icons from ${APP_ICONS.file}` : counts.offContractIcon ? "app-supplied? list them in .sable/icons.json" : "")
472
498
  L()
473
499
  if (conformance.length) {
474
500
  L(` pattern similarity (informational) — ${conformance.length} route-like file(s), mean ${meanConformance}`)