synthesisui 0.16.93 → 0.16.94

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.
@@ -396,7 +396,30 @@ export async function takeCensus(root, opts) {
396
396
  // component's own look, and everything below it is a part this slice does
397
397
  // not attempt.
398
398
  if (found.length > 0) {
399
- const t = transcribe(rootClasses(src), declaredValues);
399
+ /**
400
+ * EVERY COMPONENT THE FILE DEFINES, not just the first.
401
+ *
402
+ * One file in a real library defines two - `RadioCard` and
403
+ * `RadioCardGroup` - and the second arrived with an empty base while its
404
+ * `flex flex-col gap-3` sat six lines below the first component's return
405
+ * (audit, dono, 01/08). The file-level readings (the CSS module, the
406
+ * variant tables) belong to the PRIMARY definition; the root classes and
407
+ * the root tag are per component, scoped by name.
408
+ */
409
+ for (const extra of found.slice(1)) {
410
+ const et = transcribe(rootClasses(src, extra.name), declaredValues);
411
+ const etag = rootTag(src, extra.name);
412
+ const esize = Object.keys(et.base).length +
413
+ Object.keys(et.dark).length +
414
+ Object.keys(et.states).length;
415
+ if (esize > 0 || etag) {
416
+ looks[extra.name] = {
417
+ ...et,
418
+ ...(etag ? { rootTag: etag } : {}),
419
+ };
420
+ }
421
+ }
422
+ const t = transcribe(rootClasses(src, found[0].name), declaredValues);
400
423
  /**
401
424
  * THE VARIANT STYLES, FOLDED IN.
402
425
  *
@@ -480,7 +503,7 @@ export async function takeCensus(root, opts) {
480
503
  Object.keys(states).length +
481
504
  Object.keys(parts).length +
482
505
  layers.length;
483
- const tag = rootTag(src);
506
+ const tag = rootTag(src, found[0].name);
484
507
  /**
485
508
  * THE SKETCH travels with the look, so the reading phase never opens this
486
509
  * file again: naming parts is a sentence over data the census already holds,
@@ -23,6 +23,56 @@ import { scanTags } from "./css-modules.js";
23
23
  const MAX_NODES = 60;
24
24
  /** `className="..."` or className={"..."} / {`...`} with no interpolation. */
25
25
  const CLASS_ATTR = /className=\{?["'`]([^"'`{}$]*)["'`]\}?/;
26
+ /**
27
+ * `className={cn("a b", "c", cond && "d", record[x])}` - the STATIC strings.
28
+ *
29
+ * The plain-attr regex above sees a call expression and reads nothing, and
30
+ * 19 of 36 components in a real library style exactly this way - so their
31
+ * elements arrived in the sketch with no classes at all, and the Tooltip's
32
+ * popup carried none when the base had to be transcribed by hand
33
+ * (not-expressed.md, dono, 01/08). The always-on string arguments ARE the
34
+ * element's resting classes; the conditional halves are variants, which the
35
+ * variant readers already read - so only TOP-LEVEL string literals count.
36
+ * Template literals with `${}` and computed values stay out: a class we
37
+ * cannot see in full is a class we would be guessing at.
38
+ *
39
+ * THE NAMESPACE COUNTS. Measured on the real library: 64 calls are a bare
40
+ * `cn(`, 11 are `twUtils.cn(` and 2 are `clsx(` - and the first version of this
41
+ * regex read only the bare form, so a Modal's whole surface (its fill, border,
42
+ * radius and padding) stayed invisible while its siblings read fine
43
+ * (e2e, dono, 01/08).
44
+ */
45
+ const CLASS_CALL = /className=\{\s*(?:[A-Za-z_$][\w$]*\.)?(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(/;
46
+ function staticCallClasses(body) {
47
+ const call = CLASS_CALL.exec(body);
48
+ if (!call)
49
+ return null;
50
+ const start = call.index + call[0].length;
51
+ // Walk the call's arguments at depth 0, collecting bare string literals.
52
+ let depth = 1;
53
+ let argStart = start;
54
+ const args = [];
55
+ for (let i = start; i < body.length && depth > 0; i++) {
56
+ const ch = body[i];
57
+ if (ch === "(" || ch === "{" || ch === "[")
58
+ depth += 1;
59
+ else if (ch === ")" || ch === "}" || ch === "]") {
60
+ depth -= 1;
61
+ if (depth === 0)
62
+ args.push(body.slice(argStart, i));
63
+ }
64
+ else if (ch === "," && depth === 1) {
65
+ args.push(body.slice(argStart, i));
66
+ argStart = i + 1;
67
+ }
68
+ }
69
+ const classes = args
70
+ .map((a) => a.trim())
71
+ .filter((a) => /^(["'`]).*\1$/.test(a) && !a.includes("${"))
72
+ .map((a) => a.slice(1, -1).trim())
73
+ .filter(Boolean);
74
+ return classes.length > 0 ? classes.join(" ") : null;
75
+ }
26
76
  /**
27
77
  * The sketch of one component file.
28
78
  *
@@ -47,6 +97,11 @@ export function sketchOf(source) {
47
97
  const cls = CLASS_ATTR.exec(event.body);
48
98
  if (cls?.[1].trim())
49
99
  node.classes = cls[1].trim();
100
+ else {
101
+ const called = staticCallClasses(event.body);
102
+ if (called)
103
+ node.classes = called;
104
+ }
50
105
  // Literal text: the slice between this open and the next event, when it is prose.
51
106
  const next = events[i + 1];
52
107
  if (next) {
@@ -1090,15 +1090,40 @@ export function transcribe(classes, declared) {
1090
1090
  * which is still a signal, just a weaker one, so it travels as written and the
1091
1091
  * platform decides what to do with it.
1092
1092
  */
1093
- export function rootTag(source) {
1094
- const ret = source.search(/return\s*\(/);
1093
+ /**
1094
+ * WHERE *THIS* COMPONENT'S `return (` IS.
1095
+ *
1096
+ * Both readers below took the FIRST `return (` in the file, which is right for
1097
+ * the 34 files that define one component and wrong for the one that defines
1098
+ * two: `RadioCard.tsx` exports `RadioCard` and `RadioCardGroup`, and the group
1099
+ * - a real `flex flex-col gap-3` - arrived with an empty base and the screen
1100
+ * said "Not written yet" over it (audit, dono, 01/08).
1101
+ *
1102
+ * Scoped by NAME when a name is given: find that declaration, then its return.
1103
+ * Absent or unfound, the old behaviour stands exactly - which keeps every
1104
+ * single-component file reading byte-identically.
1105
+ */
1106
+ function returnAt(source, name) {
1107
+ if (name) {
1108
+ const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
1109
+ if (decl) {
1110
+ const from = source.slice(decl.index);
1111
+ const ret = from.search(/return\s*\(/);
1112
+ if (ret !== -1)
1113
+ return decl.index + ret;
1114
+ }
1115
+ }
1116
+ return source.search(/return\s*\(/);
1117
+ }
1118
+ export function rootTag(source, name) {
1119
+ const ret = returnAt(source, name);
1095
1120
  if (ret === -1)
1096
1121
  return null;
1097
1122
  const m = /<([A-Za-z][A-Za-z0-9.]*)/.exec(source.slice(ret));
1098
1123
  return m ? m[1] : null;
1099
1124
  }
1100
- export function rootClasses(source) {
1101
- const ret = source.search(/return\s*\(/);
1125
+ export function rootClasses(source, name) {
1126
+ const ret = returnAt(source, name);
1102
1127
  if (ret === -1)
1103
1128
  return [];
1104
1129
  const open = source.indexOf("<", ret);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.93",
3
+ "version": "0.16.94",
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": {