synthesisui 0.8.0 → 0.8.1

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.
@@ -308,8 +308,13 @@ export async function doctor(opts) {
308
308
  console.log(body(file));
309
309
  for (const o of opts.all ? list : list.slice(0, 3)) {
310
310
  console.log(` ${String(o.line).padStart(4)} ds-${o.component} · ${o.prop}: ${o.wrote}`);
311
+ // Always say what is being overruled. A finding that cannot name it is
312
+ // indistinguishable from a bug in the reader's eyes, and one of these
313
+ // (`.ds-card:hover { transform: none }`) was a real, deliberate call.
311
314
  if (o.recipe)
312
315
  console.log(` the recipe binds ${o.recipe}`);
316
+ else if (o.where)
317
+ console.log(` the recipe binds it ${o.where}`);
313
318
  }
314
319
  if (!opts.all && list.length > 3) {
315
320
  console.log(` +${list.length - 3} more`);
@@ -46,10 +46,37 @@ export function bindingsOf(recipe) {
46
46
  collect(r.variants, false);
47
47
  collect(r.states, false);
48
48
  collect(r.parts, false);
49
+ // Where a non-base property lives, so the report can name it. Base wins: if
50
+ // base binds it too, that value is the one worth showing.
51
+ const boundIn = new Map();
52
+ const note = (block, label) => {
53
+ if (!block || typeof block !== "object")
54
+ return;
55
+ for (const [k, v] of Object.entries(block)) {
56
+ if (typeof v === "string" || typeof v === "number") {
57
+ const p = kebab(k);
58
+ if (!baseValues.has(p) && !boundIn.has(p))
59
+ boundIn.set(p, `${label}: ${v}`);
60
+ }
61
+ else if (v && typeof v === "object") {
62
+ note(v, label);
63
+ }
64
+ }
65
+ };
66
+ for (const [state, block] of Object.entries((r.states ?? {}))) {
67
+ note(block, `on ${state}`);
68
+ }
69
+ for (const [axis, opts] of Object.entries((r.variants ?? {}))) {
70
+ if (!opts || typeof opts !== "object")
71
+ continue;
72
+ for (const [opt, block] of Object.entries(opts)) {
73
+ note(block, `on ${axis}="${opt}"`);
74
+ }
75
+ }
49
76
  const usage = Array.isArray(r.usage)
50
77
  ? r.usage.filter((x) => typeof x === "string")
51
78
  : [];
52
- return { props, baseValues, usage };
79
+ return { props, baseValues, boundIn, usage };
53
80
  }
54
81
  /**
55
82
  * How many places each known component is used. An inventory nobody has today:
@@ -117,6 +144,55 @@ function enclosingTag(src, at) {
117
144
  return null;
118
145
  }
119
146
  const lineAt = (src, index) => src.slice(0, index).split("\n").length;
147
+ /**
148
+ * Whatever the `style` prop was given, with where it starts. The expression
149
+ * container, not `{{` specifically: a real component writes
150
+ * `style={showImg ? undefined : { color: "#fff" }}`, and requiring the double
151
+ * brace dropped that finding on the floor. Brace matching rather than a regex,
152
+ * because the value nests.
153
+ */
154
+ function styleRegions(tag) {
155
+ const out = [];
156
+ const open = /style\s*=\s*\{/g;
157
+ let m = open.exec(tag);
158
+ while (m !== null) {
159
+ const start = m.index + m[0].length;
160
+ let depth = 1; // the container brace just consumed
161
+ let end = start;
162
+ for (; end < tag.length; end++) {
163
+ if (tag[end] === "{")
164
+ depth++;
165
+ else if (tag[end] === "}" && --depth === 0)
166
+ break;
167
+ }
168
+ // Only the OBJECT LITERALS inside the container. Scanning the container
169
+ // whole let a one-line ternary eat the declaration: in
170
+ // `style={on ? undefined : { padding: "7px" }}` the colon after `undefined`
171
+ // reads as a property separator and swallows the padding behind it.
172
+ const container = tag.slice(start, end);
173
+ let d = 0;
174
+ let objStart = -1;
175
+ for (let i = 0; i < container.length; i++) {
176
+ if (container[i] === "{") {
177
+ if (d === 0)
178
+ objStart = i + 1;
179
+ d++;
180
+ }
181
+ else if (container[i] === "}" && d > 0 && --d === 0) {
182
+ out.push({
183
+ text: container.slice(objStart, i),
184
+ offset: start + objStart,
185
+ });
186
+ }
187
+ }
188
+ // `style={{ … }}` - the container IS the object
189
+ if (objStart === -1)
190
+ out.push({ text: container, offset: start });
191
+ open.lastIndex = end;
192
+ m = open.exec(tag);
193
+ }
194
+ return out;
195
+ }
120
196
  /**
121
197
  * The literal a style property was given, or null when it was given an
122
198
  * expression instead.
@@ -159,22 +235,29 @@ export function findOverrides(source, recipes) {
159
235
  const tag = enclosingTag(source, m.index ?? 0);
160
236
  if (!tag)
161
237
  continue;
162
- // inline style={{ borderRadius: 4 }}
163
- for (const s of tag.text.matchAll(/([a-zA-Z-]+)\s*:\s*("[^"]*"|'[^']*'|[^,}\n]+)/g)) {
164
- const prop = kebab(s[1]);
165
- if (!bind.props.has(prop))
166
- continue;
167
- const wrote = literalValue(s[2]);
168
- if (wrote === null)
169
- continue;
170
- record({
171
- component: name,
172
- prop,
173
- wrote,
174
- recipe: bind.baseValues.get(prop) ?? null,
175
- line: lineAt(source, tag.start + (s.index ?? 0)),
176
- excerpt: clip(lines[lineAt(source, tag.start + (s.index ?? 0)) - 1]?.trim() ?? ""),
177
- });
238
+ // inline style={{ borderRadius: 4 }} - and ONLY that. Reading the whole tag
239
+ // meant reading every object prop on it, so Framer Motion's
240
+ // `animate={{ opacity: 1 }}` came back as "ds-card overrules opacity"
241
+ // (investidorez, 25/07). An animation is not a design decision.
242
+ for (const region of styleRegions(tag.text)) {
243
+ for (const s of region.text.matchAll(/([a-zA-Z-]+)\s*:\s*("[^"]*"|'[^']*'|[^,}\n]+)/g)) {
244
+ const prop = kebab(s[1]);
245
+ if (!bind.props.has(prop))
246
+ continue;
247
+ const wrote = literalValue(s[2]);
248
+ if (wrote === null)
249
+ continue;
250
+ const at = tag.start + region.offset + (s.index ?? 0);
251
+ record({
252
+ component: name,
253
+ prop,
254
+ wrote,
255
+ recipe: bind.baseValues.get(prop) ?? null,
256
+ where: bind.boundIn.get(prop) ?? null,
257
+ line: lineAt(source, at),
258
+ excerpt: clip(lines[lineAt(source, at) - 1]?.trim() ?? ""),
259
+ });
260
+ }
178
261
  }
179
262
  // utility classes sitting beside the ds-* class
180
263
  for (const cls of tag.text.matchAll(/[\w:[\]#().%/-]+/g)) {
@@ -186,6 +269,7 @@ export function findOverrides(source, recipes) {
186
269
  prop,
187
270
  wrote: cls[0],
188
271
  recipe: bind.baseValues.get(prop) ?? null,
272
+ where: bind.boundIn.get(prop) ?? null,
189
273
  line: lineAt(source, tag.start + (cls.index ?? 0)),
190
274
  excerpt: clip(lines[lineAt(source, tag.start + (cls.index ?? 0)) - 1]?.trim() ?? ""),
191
275
  });
@@ -193,7 +277,10 @@ export function findOverrides(source, recipes) {
193
277
  }
194
278
  // ── CSS: a rule whose selector reaches a ds-* component ───────────────────
195
279
  for (const rule of source.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
196
- const selector = rule[1];
280
+ // A CSS comment is not a selector. `/* botões (.ds-button) … */` above an
281
+ // unrelated rule made the doctor attribute that rule to ds-button - it was
282
+ // reading prose as structure (investidorez, 25/07).
283
+ const selector = rule[1].replace(/\/\*[\s\S]*?\*\//g, "");
197
284
  const hit = /\.ds-([a-z][a-z0-9-]*)\b/.exec(selector);
198
285
  const bind = hit ? recipes.get(hit[1]) : undefined;
199
286
  if (!hit || !bind)
@@ -211,6 +298,7 @@ export function findOverrides(source, recipes) {
211
298
  prop,
212
299
  wrote: decl[2].trim(),
213
300
  recipe: bind.baseValues.get(prop) ?? null,
301
+ where: bind.boundIn.get(prop) ?? null,
214
302
  line: lineAt(source, at),
215
303
  excerpt: clip(lines[lineAt(source, at) - 1]?.trim() ?? ""),
216
304
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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": {