synthesisui 0.16.2 → 0.16.5

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 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
- function catalogLines(recipes) {
43
- return Object.entries(recipes)
44
- .sort(([a], [b]) => a.localeCompare(b))
45
- .map(([name, recipe]) => {
46
- const r = recipe;
47
- const axes = Object.entries(r.variants ?? {})
48
- .map(([axis, options]) => {
49
- const keys = Object.keys(options);
50
- return keys.length <= 4 ? `${axis}: ${keys.join("|")}` : axis;
51
- })
52
- .join("; ");
53
- const desc = summarize(r.description);
54
- return {
55
- name,
56
- line: ` - \`ds-${name}\`${desc ? ` - ${desc}` : ""}${axes ? ` [${axes}]` : ""}`,
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 = catalogLines(doc.components ?? {});
71
- const blocks = catalogLines(doc.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}) - USE these before creating new ones:`);
77
- lines.push(...components.map((c) => c.line));
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(...blocks.map((c) => c.line));
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 {
@@ -292,7 +292,14 @@ export async function doctor(opts) {
292
292
  *
293
293
  * Nobody debugs from 0%. They conclude the product does not work.
294
294
  */
295
- const wiring = { imported: false, scoped: false };
295
+ const wiring = {
296
+ imported: false,
297
+ scoped: false,
298
+ /** `init` wrote a fonts file for this project (Next targets only). */
299
+ fontsWritten: false,
300
+ /** …and the stylesheet actually maps it onto the system's type tokens. */
301
+ fontsMapped: false,
302
+ };
296
303
  for await (const file of scopes.length > 0 ? walkAll(scopes) : walk(root)) {
297
304
  const src = await readFile(file, "utf8").catch(() => "");
298
305
  if (!src)
@@ -305,6 +312,15 @@ export async function doctor(opts) {
305
312
  wiring.imported = true;
306
313
  if (src.includes(`data-ds="${table.slug}"`))
307
314
  wiring.scoped = true;
315
+ // The third requirement, and the one that stayed invisible. `init`
316
+ // writes a fonts file and asks for two more edits; an agent told to
317
+ // check only the first two did exactly that, stopped, and left the
318
+ // project rendering in the framework's default face (my-test2, 27/07).
319
+ // What the checker checks is what gets done.
320
+ if (src.includes("--font-ds-") && /next\/font/.test(src))
321
+ wiring.fontsWritten = true;
322
+ if (/--ds-typography-families-\w+\s*:\s*var\(\s*--font-ds-/.test(src))
323
+ wiring.fontsMapped = true;
308
324
  }
309
325
  reports.push(scanSource(rel, src, table));
310
326
  if (recipes.size > 0) {
@@ -378,7 +394,11 @@ export async function doctor(opts) {
378
394
  // Before the number, because the number is the thing that misleads. An
379
395
  // installed-but-unwired system reads 0%, and 0% reads as "broken product"
380
396
  // rather than "one import missing".
381
- const unwired = table.source === "installed" && (!wiring.imported || !wiring.scoped);
397
+ // Fonts only count as missing when `init` actually wrote the file - a
398
+ // non-Next project has no fonts.ts and must not be nagged about one.
399
+ const fontsPending = wiring.fontsWritten && !wiring.fontsMapped;
400
+ const unwired = table.source === "installed" &&
401
+ (!wiring.imported || !wiring.scoped || fontsPending);
382
402
  if (unwired) {
383
403
  console.log("");
384
404
  console.log(body(`${table.name ?? table.slug} is installed - but not wired up yet.`));
@@ -389,9 +409,28 @@ export async function doctor(opts) {
389
409
  console.log(body(wiring.scoped
390
410
  ? ` ✓ data-ds="${table.slug}" found`
391
411
  : ` ✗ no element carries data-ds="${table.slug}"`));
412
+ if (wiring.fontsWritten) {
413
+ console.log(body(wiring.fontsMapped
414
+ ? " ✓ the type from fonts.ts is mapped onto the system"
415
+ : " ✗ fonts.ts exists but nothing maps it - the system's type is not being used"));
416
+ }
392
417
  console.log("");
393
- console.log(body(`Until both are true, none of the ${table.byName.size} tokens reach the browser`));
394
- console.log(body("and the number below cannot mean anything."));
418
+ // Three requirements now, and they fail differently. Missing the import or
419
+ // the scope means NOTHING reaches the browser and the number below is
420
+ // noise. Missing only the type mapping means colour and spacing are
421
+ // working and the faces are not - saying "nothing reaches the browser"
422
+ // there would be false, and a report that overstates is one nobody trusts
423
+ // the next time.
424
+ const blocked = !wiring.imported || !wiring.scoped;
425
+ if (blocked) {
426
+ console.log(body(`Until the first two are true, none of the ${table.byName.size} tokens reach the`));
427
+ console.log(body("browser and the number below cannot mean anything."));
428
+ }
429
+ else {
430
+ console.log(body("Colour and spacing are working. Type is not: the system's faces"));
431
+ console.log(body("are declared and nothing points at them, so the page renders in"));
432
+ console.log(body("whatever the framework picked."));
433
+ }
395
434
  console.log(body("The exact snippets are in the output of `init`."));
396
435
  }
397
436
  if (hasSystem && measurable) {
@@ -189,24 +189,69 @@ export function parseTokens(css) {
189
189
  */
190
190
  export function parseRootTokens(css) {
191
191
  const out = new Map();
192
- for (const block of css.matchAll(/([^{}]*)\{([^{}]*)\}/g)) {
193
- const selector = block[1].trim();
194
- const isRoot = /^@theme\b/i.test(selector) ||
195
- /(^|[\s,>+~])(:root|html|:host)\b/i.test(selector);
196
- if (!isRoot)
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
- for (const m of block[2].matchAll(/(--[a-z0-9_-]+)\s*:\s*([^;}]+)/gi)) {
199
- const name = m[1].toLowerCase();
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 = m[2].trim();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.2",
3
+ "version": "0.16.5",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {