synthesisui 0.16.1 → 0.16.4
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/dist/claude-md.js +25 -29
- package/dist/commands/adopt.js +37 -1
- package/dist/doctor/tokens.js +53 -8
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -31,31 +31,26 @@ async function readInstalled(projectRoot) {
|
|
|
31
31
|
}
|
|
32
32
|
return locks;
|
|
33
33
|
}
|
|
34
|
-
/** First sentence of a description, capped - the manifest must stay lean. */
|
|
35
|
-
function summarize(desc) {
|
|
36
|
-
if (typeof desc !== "string" || !desc.trim())
|
|
37
|
-
return "";
|
|
38
|
-
const first = desc.trim().split(/(?<=\.)\s/)[0] ?? desc.trim();
|
|
39
|
-
return first.length > 90 ? `${first.slice(0, 87)}…` : first;
|
|
40
|
-
}
|
|
41
34
|
/** One manifest line per recipe: name, what it is, and its variant axes. */
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
/**
|
|
36
|
+
* NAMES ONLY, and the 87% that buys.
|
|
37
|
+
*
|
|
38
|
+
* The manifest was name + description + variant axes for every component -
|
|
39
|
+
* 4059 of this block's 5837 bytes on a 48-component system, carried into every
|
|
40
|
+
* session including the ones that never touch UI, and growing linearly with
|
|
41
|
+
* each system installed.
|
|
42
|
+
*
|
|
43
|
+
* But the question the agent asks here is binary: "is there already something
|
|
44
|
+
* for this?" A name answers it. Description and variants only matter AFTER
|
|
45
|
+
* that decision, and by then the agent is opening the GUIDE or running
|
|
46
|
+
* `component <slug> <name>`, which hands it the real typed props anyway.
|
|
47
|
+
*
|
|
48
|
+
* Names stay INLINE rather than moving to a file, deliberately. A lookup that
|
|
49
|
+
* costs a file read is a lookup an agent skips when it is in a hurry, and then
|
|
50
|
+
* writes the fourth button. Cheap to consult is the whole point.
|
|
51
|
+
*/
|
|
52
|
+
function catalogNames(recipes) {
|
|
53
|
+
return Object.keys(recipes).sort((a, b) => a.localeCompare(b));
|
|
59
54
|
}
|
|
60
55
|
/**
|
|
61
56
|
* The COMPONENT MANIFEST for one installed system, read from its versioned
|
|
@@ -67,19 +62,20 @@ async function readManifest(projectRoot, ds) {
|
|
|
67
62
|
try {
|
|
68
63
|
const raw = await readFile(join(projectRoot, "_synthesisui", "ds", ds.slug, `v${ds.version}`, "design-system.json"), "utf8");
|
|
69
64
|
const doc = JSON.parse(raw);
|
|
70
|
-
const components =
|
|
71
|
-
const blocks =
|
|
65
|
+
const components = catalogNames(doc.components ?? {});
|
|
66
|
+
const blocks = catalogNames(doc.blocks ?? {});
|
|
72
67
|
if (components.length === 0 && blocks.length === 0)
|
|
73
68
|
return null;
|
|
74
69
|
const lines = [];
|
|
75
70
|
if (components.length > 0) {
|
|
76
|
-
lines.push(` Components (${components.length}) -
|
|
77
|
-
lines.push(
|
|
71
|
+
lines.push(` Components (${components.length}) - look here BEFORE writing anything new:`);
|
|
72
|
+
lines.push(` ${components.map((n) => `ds-${n}`).join(" ")}`);
|
|
78
73
|
}
|
|
79
74
|
if (blocks.length > 0) {
|
|
80
75
|
lines.push(` Engagement blocks (${blocks.length}):`);
|
|
81
|
-
lines.push(
|
|
76
|
+
lines.push(` ${blocks.map((n) => `ds-${n}`).join(" ")}`);
|
|
82
77
|
}
|
|
78
|
+
lines.push(" What each one does, its variants and states: the GUIDE above.");
|
|
83
79
|
return lines.join("\n");
|
|
84
80
|
}
|
|
85
81
|
catch {
|
package/dist/commands/adopt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
4
|
import { parseRootTokens } from "../doctor/tokens.js";
|
|
@@ -75,6 +75,34 @@ function guessSlug(tokens, pkgName) {
|
|
|
75
75
|
.replace(/^-|-$/g, "")
|
|
76
76
|
.toLowerCase() || "my-system");
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* A system WE installed, if there is one.
|
|
80
|
+
*
|
|
81
|
+
* Run inside a repo already carrying Moments - 158 tokens, 93% coverage - adopt
|
|
82
|
+
* announced "found no custom properties … if you do not have a system yet,
|
|
83
|
+
* start from one" (27/07). Telling somebody with a design system that they have
|
|
84
|
+
* none is the exact dead end this command was written to avoid: our own tokens
|
|
85
|
+
* live under `_synthesisui/`, which the walk skips, so it looked and found
|
|
86
|
+
* nothing and drew the wrong conclusion from it.
|
|
87
|
+
*/
|
|
88
|
+
async function installedSystems(root) {
|
|
89
|
+
const dir = join(root, "_synthesisui", "ds");
|
|
90
|
+
const slugs = [];
|
|
91
|
+
for (const e of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
92
|
+
if (!e.isDirectory())
|
|
93
|
+
continue;
|
|
94
|
+
const raw = await readFile(join(dir, e.name, ".lock"), "utf8").catch(() => "");
|
|
95
|
+
try {
|
|
96
|
+
const lock = JSON.parse(raw);
|
|
97
|
+
if (!lock.adopted)
|
|
98
|
+
slugs.push(lock.name ?? e.name);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// no readable lock - not something we put there
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return slugs;
|
|
105
|
+
}
|
|
78
106
|
async function findTokens(root, only) {
|
|
79
107
|
let files = 0;
|
|
80
108
|
const from = [];
|
|
@@ -146,6 +174,14 @@ export async function adopt(opts) {
|
|
|
146
174
|
const found = await findTokens(root, opts.tokens);
|
|
147
175
|
console.log(section("Adopt"));
|
|
148
176
|
if (found.tokens.size === 0) {
|
|
177
|
+
const already = await installedSystems(root);
|
|
178
|
+
if (already.length > 0) {
|
|
179
|
+
console.log(body(`${already.join(", ")} ${already.length === 1 ? "is" : "are"} already installed here, and adopt is for a system we did not install.`));
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log(body("Nothing to do. To see how the code is holding up:"));
|
|
182
|
+
console.log(snippet(["npx synthesisui@latest doctor"]));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
149
185
|
// Never "failed to adopt". Always: what was looked for, where, and the
|
|
150
186
|
// next thing to type. Being stuck with no move is the worst outcome.
|
|
151
187
|
console.log(body(`Looked in ${SCOPE_HINT} across ${found.files} stylesheet${found.files === 1 ? "" : "s"} and found no custom properties.`));
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -189,24 +189,69 @@ export function parseTokens(css) {
|
|
|
189
189
|
*/
|
|
190
190
|
export function parseRootTokens(css) {
|
|
191
191
|
const out = new Map();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
const isRoot = (sel) => /^@theme\b/i.test(sel) || /(^|[\s,>+~])(:root|html|:host)\b/i.test(sel);
|
|
193
|
+
// Every `<selector> {` in the file. Nested ones show up too and are filtered
|
|
194
|
+
// by isRoot, so an @keyframes inside @theme is skipped rather than mined.
|
|
195
|
+
const opens = /([^{}]*)\{/g;
|
|
196
|
+
let m = opens.exec(css);
|
|
197
|
+
while (m !== null) {
|
|
198
|
+
// The capture runs back to the previous brace, so it carries imports and
|
|
199
|
+
// comments with it. The SELECTOR is only what follows the last `;` or
|
|
200
|
+
// comment - and `@theme` is anchored to the start, so without this it
|
|
201
|
+
// matched only when the block happened to be the first thing in the file.
|
|
202
|
+
// Every fixture had it first. This repo's own globals.css does not, and
|
|
203
|
+
// reported 0 of its 48 tokens (27/07).
|
|
204
|
+
const sel = (m[1]
|
|
205
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
206
|
+
.split(";")
|
|
207
|
+
.pop() ?? "").trim();
|
|
208
|
+
if (!isRoot(sel)) {
|
|
209
|
+
m = opens.exec(css);
|
|
197
210
|
continue;
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
}
|
|
212
|
+
// Walk to the MATCHING close, counting depth. The flat regex this replaces
|
|
213
|
+
// required a body with no braces at all, so it silently skipped any
|
|
214
|
+
// `@theme` containing `@keyframes` - which is the documented Tailwind v4
|
|
215
|
+
// layout. Measured against this repo's own globals.css: 48 tokens present,
|
|
216
|
+
// 2 found, and the 2 came from unrelated test fixtures (27/07).
|
|
217
|
+
const start = m.index + m[0].length;
|
|
218
|
+
let depth = 1;
|
|
219
|
+
let i = start;
|
|
220
|
+
while (i < css.length && depth > 0) {
|
|
221
|
+
const c = css[i];
|
|
222
|
+
if (c === "{")
|
|
223
|
+
depth++;
|
|
224
|
+
else if (c === "}")
|
|
225
|
+
depth--;
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
// Only declarations that are DIRECT children count. A custom property
|
|
229
|
+
// inside a keyframe step is animation state, not a design token.
|
|
230
|
+
let flat = "";
|
|
231
|
+
let d = 0;
|
|
232
|
+
for (let j = start; j < i - 1; j++) {
|
|
233
|
+
const c = css[j];
|
|
234
|
+
if (c === "{")
|
|
235
|
+
d++;
|
|
236
|
+
else if (c === "}")
|
|
237
|
+
d--;
|
|
238
|
+
else if (d === 0)
|
|
239
|
+
flat += c;
|
|
240
|
+
}
|
|
241
|
+
for (const t of flat.matchAll(/(--[a-z0-9_-]+)\s*:\s*([^;}]+)/gi)) {
|
|
242
|
+
const name = t[1].toLowerCase();
|
|
200
243
|
// Belt and braces: v4 emits some `--tw-*` bookkeeping into @theme, and
|
|
201
244
|
// it is machinery, not somebody's design vocabulary.
|
|
202
245
|
if (name.startsWith("--tw-"))
|
|
203
246
|
continue;
|
|
204
|
-
const value =
|
|
247
|
+
const value = t[2].trim();
|
|
205
248
|
if (!value || value.startsWith("var("))
|
|
206
249
|
continue;
|
|
207
250
|
if (!out.has(name))
|
|
208
251
|
out.set(name, value);
|
|
209
252
|
}
|
|
253
|
+
opens.lastIndex = i;
|
|
254
|
+
m = opens.exec(css);
|
|
210
255
|
}
|
|
211
256
|
return out;
|
|
212
257
|
}
|
package/package.json
CHANGED