@sableui/react 0.3.0 → 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.
- package/AGENTS.md +5 -1
- package/CHANGELOG.md +52 -0
- package/README.md +5 -1
- package/bin/sable.mjs +6 -2
- package/dist/{_contract-o3tUc8Ig.d.ts → _contract-BlKMR4eX.d.ts} +6 -1
- package/dist/blocks/index.js +2 -2
- package/dist/{chunk-CKNM7KSB.js → chunk-CCUDFLU3.js} +23 -6
- package/dist/{chunk-6LCIBMMG.js → chunk-D2ETQBHV.js} +1 -1
- package/dist/{chunk-K6LII3OE.js → chunk-FBSL2LZQ.js} +27 -23
- package/dist/{chunk-X276MF2W.js → chunk-OQACYT2D.js} +2 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +8 -8
- package/dist/ios/styles.css +8 -0
- package/dist/patterns/index.js +2 -2
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +11 -2
- package/dist/styles.css +9 -0
- package/dist/{theme-provider-BE-KqUG-.d.ts → theme-provider-CuYYxF0o.d.ts} +1 -1
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/base/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/verdant/index.d.ts +1 -1
- package/llms/components/button.md +15 -3
- package/llms/components/input.md +1 -1
- package/llms/components/scrub-field.md +1 -1
- package/llms/components/select.md +1 -1
- package/llms/reference/token-intent.md +5 -1
- package/llms/tasks/getting-started.md +4 -0
- package/llms-full.txt +27 -7
- package/package.json +1 -1
- package/registry.json +2446 -8
- package/scripts/agent-setup.mjs +34 -7
- package/scripts/grade.mjs +33 -7
package/scripts/agent-setup.mjs
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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(
|
|
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:
|
|
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}`)
|