synthesisui 0.16.7 → 0.16.8
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/commands/doctor.js +6 -1
- package/dist/component-codegen.js +81 -15
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -409,7 +409,12 @@ export async function doctor(opts) {
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
else if (asideTotal > 0) {
|
|
412
|
-
|
|
412
|
+
// "a token could never hold" was written when every aside was an image
|
|
413
|
+
// renderer or an SVG paint. A literal sitting in a token's OWN fallback -
|
|
414
|
+
// `var(--ds-color-semantic-knob, #ffffff)` - is set aside for the opposite
|
|
415
|
+
// reason: it is already tokenized. The summary said the false half out
|
|
416
|
+
// loud and hid the true half behind a flag.
|
|
417
|
+
console.log(body(`set aside ${plural(asideTotal, "value")} that are not drift (--verbose for why)`));
|
|
413
418
|
}
|
|
414
419
|
// 0 of 0 is not a perfect score, it is an empty measurement - printing a
|
|
415
420
|
// full bar there would be the report's first lie.
|
|
@@ -32,11 +32,36 @@ function elementFor(name, recipe) {
|
|
|
32
32
|
? { tag: "button", attrs: ' type="button"' }
|
|
33
33
|
: undefined);
|
|
34
34
|
const tag = hit?.tag ?? "div";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const attrs = hit?.attrs ?? "";
|
|
36
|
+
const isVoid = tag === "input" || tag === "hr";
|
|
37
|
+
/**
|
|
38
|
+
* A VOID ELEMENT CANNOT HOST THE PARTS WE TELL PEOPLE TO PUT INSIDE IT.
|
|
39
|
+
*
|
|
40
|
+
* `switch` resolved to `<input type="checkbox">`, and the recipe gives it a
|
|
41
|
+
* `thumb` part - so the generated file documented `<Switch><SwitchThumb/>
|
|
42
|
+
* </Switch>`, which React refuses at runtime: input is void and must not
|
|
43
|
+
* have children. Found by an agent building a settings page against it
|
|
44
|
+
* (my-test4, 27/07). The documented usage was impossible.
|
|
45
|
+
*
|
|
46
|
+
* The recipe was right and the tag was wrong. Its own CSS styles `.ds-switch`
|
|
47
|
+
* as an `inline-flex` track with a 16px knob inside a 24px rail - that is a
|
|
48
|
+
* container, described as one, and only the element choice disagreed.
|
|
49
|
+
*
|
|
50
|
+
* `<button role="switch">` is the accessible pattern for exactly this: it
|
|
51
|
+
* takes children, it is focusable and operable by keyboard for free, and the
|
|
52
|
+
* caller supplies `aria-checked`. Promotion happens ONLY when parts exist, so
|
|
53
|
+
* a plain input stays an input.
|
|
54
|
+
*/
|
|
55
|
+
if (isVoid && Object.keys(recipe.parts ?? {}).length > 0) {
|
|
56
|
+
// Carry the ARIA role across - it is the part of the input's meaning that
|
|
57
|
+
// survives the tag change, and dropping it would trade a runtime error for
|
|
58
|
+
// a silent accessibility regression.
|
|
59
|
+
const role = / role="[a-z]+"/.exec(attrs)?.[0] ?? "";
|
|
60
|
+
return tag === "input"
|
|
61
|
+
? { tag: "button", attrs: ` type="button"${role}`, voidEl: false }
|
|
62
|
+
: { tag: "div", attrs: role || ' role="separator"', voidEl: false };
|
|
63
|
+
}
|
|
64
|
+
return { tag, attrs, voidEl: isVoid };
|
|
40
65
|
}
|
|
41
66
|
/** Variant axes → typed props. An axis whose options ⊆ {true,false} is a
|
|
42
67
|
* boolean prop; empty axes (no visual effect) are skipped. */
|
|
@@ -272,13 +297,44 @@ const STATE_PREFIX = {
|
|
|
272
297
|
function blockToTailwind(block, prefix = "") {
|
|
273
298
|
return Object.entries(block).flatMap(([prop, value]) => declToTailwind(prop, value).map((cls) => `${prefix}${cls}`));
|
|
274
299
|
}
|
|
275
|
-
function tailwindClassList(recipe
|
|
300
|
+
function tailwindClassList(recipe,
|
|
301
|
+
/** CSS properties a variant axis owns - see `variantOwnedProps`. */
|
|
302
|
+
exclude) {
|
|
303
|
+
const base = exclude
|
|
304
|
+
? Object.fromEntries(Object.entries(recipe.base).filter(([prop]) => !exclude.has(prop)))
|
|
305
|
+
: recipe.base;
|
|
276
306
|
const classes = [
|
|
277
|
-
...blockToTailwind(
|
|
307
|
+
...blockToTailwind(base),
|
|
308
|
+
// States keep everything: `hover:` and `disabled:` cannot collide with an
|
|
309
|
+
// unprefixed variant class, so there is nothing to resolve.
|
|
278
310
|
...Object.entries(recipe.states ?? {}).flatMap(([state, block]) => STATE_PREFIX[state] ? blockToTailwind(block, STATE_PREFIX[state]) : []),
|
|
279
311
|
];
|
|
280
312
|
return classes.join(" ");
|
|
281
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* A VARIANT CANNOT OVERRIDE THE BASE WHEN BOTH ARE PLAIN UTILITIES.
|
|
316
|
+
*
|
|
317
|
+
* `Card` emitted `p-md` in BASE and `[padding:0]` for `padding="none"`. Same
|
|
318
|
+
* specificity, so which one wins is decided by the order Tailwind emits them
|
|
319
|
+
* in the stylesheet - not by the order of the class names, which is what the
|
|
320
|
+
* code looks like it controls. The prop silently did nothing (found by an
|
|
321
|
+
* agent that worked around it in a comment rather than reporting it,
|
|
322
|
+
* my-test4, 27/07).
|
|
323
|
+
*
|
|
324
|
+
* CSS mode never had this: it selects on `[data-padding="none"]`, which
|
|
325
|
+
* outranks the base class honestly. Tailwind mode has no such ladder, so the
|
|
326
|
+
* conflict has to be resolved where it is created - at generation.
|
|
327
|
+
*
|
|
328
|
+
* The property leaves BASE and the base value becomes the axis's default, so
|
|
329
|
+
* exactly one class ever sets it.
|
|
330
|
+
*/
|
|
331
|
+
function variantOwnedProps(variants, axis) {
|
|
332
|
+
const props = new Set();
|
|
333
|
+
for (const option of axis.options)
|
|
334
|
+
for (const prop of Object.keys(variants?.[axis.key]?.[option] ?? {}))
|
|
335
|
+
props.add(prop);
|
|
336
|
+
return [...props];
|
|
337
|
+
}
|
|
282
338
|
// ── Emission ─────────────────────────────────────────────────────────────────
|
|
283
339
|
function header(slug, name, version, mode) {
|
|
284
340
|
const setup = mode === "tailwind"
|
|
@@ -295,8 +351,14 @@ const joinCls = (parts) => `[${parts.join(", ")}].filter(Boolean).join(" ")`;
|
|
|
295
351
|
/** JSDoc showing how to compose the component with its parts + content, so the
|
|
296
352
|
* materialized code doesn't read as "a bare shell renders nothing" (dogfood
|
|
297
353
|
* #5). Built from the recipe's parts. */
|
|
298
|
-
function compositionHint(comp, name, recipe) {
|
|
354
|
+
function compositionHint(comp, name, recipe, voidEl = false) {
|
|
299
355
|
const partNames = Object.keys(recipe.parts ?? {});
|
|
356
|
+
// An `<input>` or `<hr>` takes no children, and telling somebody to put
|
|
357
|
+
// content inside one is an instruction that throws. With parts, the element
|
|
358
|
+
// was promoted above and this branch never runs for a void tag.
|
|
359
|
+
if (voidEl) {
|
|
360
|
+
return `/** Wears the "${name}" recipe. Takes no children - it renders a single void element. */`;
|
|
361
|
+
}
|
|
300
362
|
if (partNames.length === 0) {
|
|
301
363
|
return `/** Wears the "${name}" recipe. Put your content inside: <${comp}>…</${comp}>. */`;
|
|
302
364
|
}
|
|
@@ -317,7 +379,6 @@ function emitCssMode(slug, name, recipe, version) {
|
|
|
317
379
|
const comp = pascal(name);
|
|
318
380
|
const propNames = axes.map((a) => a.prop);
|
|
319
381
|
const destructure = [...propNames, "className", "...props"].join(", ");
|
|
320
|
-
void voidEl; // both void and container elements self-close ({...props} carries children)
|
|
321
382
|
const rootJsx = ` <${tag}${attrs}\n className={${joinCls([`"ds-${name}"`, "className"])}}\n${dataAttrLines(axes)}${axes.length ? "\n" : ""} {...props}\n />`;
|
|
322
383
|
const parts = Object.entries(recipe.parts ?? {}).map(([partName, part]) => {
|
|
323
384
|
const partAxes = axesOf(part.variants ?? {});
|
|
@@ -345,7 +406,7 @@ import type { ComponentPropsWithoutRef } from "react";
|
|
|
345
406
|
|
|
346
407
|
type ${comp}Props = ${propsType(axes, tag)};
|
|
347
408
|
|
|
348
|
-
${compositionHint(comp, name, recipe)}
|
|
409
|
+
${compositionHint(comp, name, recipe, voidEl)}
|
|
349
410
|
export function ${comp}({ ${destructure} }: ${comp}Props) {
|
|
350
411
|
return (
|
|
351
412
|
${rootJsx}
|
|
@@ -368,11 +429,17 @@ function emitTailwindMode(slug, name, recipe, version) {
|
|
|
368
429
|
const booleanConsts = axes
|
|
369
430
|
.filter((a) => a.boolean)
|
|
370
431
|
.map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(recipe.variants[a.key]?.true ?? {}).join(" "))};`);
|
|
432
|
+
// Every property some axis controls leaves BASE, and the base value becomes
|
|
433
|
+
// that axis's default - so exactly one class ever sets it and the prop
|
|
434
|
+
// actually wins.
|
|
435
|
+
const owned = new Map(axes.map((a) => [a.prop, variantOwnedProps(recipe.variants, a)]));
|
|
436
|
+
const excluded = new Set([...owned.values()].flat());
|
|
437
|
+
const fallbackFor = (a) => JSON.stringify(blockToTailwind(Object.fromEntries(Object.entries(recipe.base).filter(([prop]) => (owned.get(a.prop) ?? []).includes(prop)))).join(" "));
|
|
371
438
|
const clsParts = [
|
|
372
439
|
"BASE",
|
|
373
440
|
...axes.map((a) => a.boolean
|
|
374
|
-
? `${a.prop} ? ${a.prop.toUpperCase()} :
|
|
375
|
-
: `${a.prop} ? ${a.prop.toUpperCase()}[${a.prop}] :
|
|
441
|
+
? `${a.prop} ? ${a.prop.toUpperCase()} : ${fallbackFor(a)}`
|
|
442
|
+
: `${a.prop} ? ${a.prop.toUpperCase()}[${a.prop}] : ${fallbackFor(a)}`),
|
|
376
443
|
"className",
|
|
377
444
|
];
|
|
378
445
|
const destructure = [
|
|
@@ -380,17 +447,16 @@ function emitTailwindMode(slug, name, recipe, version) {
|
|
|
380
447
|
"className",
|
|
381
448
|
"...props",
|
|
382
449
|
].join(", ");
|
|
383
|
-
void voidEl;
|
|
384
450
|
return `${header(slug, name, version, "tailwind")}
|
|
385
451
|
|
|
386
452
|
import type { ComponentPropsWithoutRef } from "react";
|
|
387
453
|
|
|
388
|
-
const BASE = ${JSON.stringify(tailwindClassList(recipe))};
|
|
454
|
+
const BASE = ${JSON.stringify(tailwindClassList(recipe, excluded))};
|
|
389
455
|
${[...variantConsts, ...booleanConsts].join("\n")}
|
|
390
456
|
|
|
391
457
|
type ${comp}Props = ${propsType(axes, tag)};
|
|
392
458
|
|
|
393
|
-
${compositionHint(comp, name, recipe)}
|
|
459
|
+
${compositionHint(comp, name, recipe, voidEl)}
|
|
394
460
|
export function ${comp}({ ${destructure} }: ${comp}Props) {
|
|
395
461
|
return (
|
|
396
462
|
<${tag}${attrs}
|
package/package.json
CHANGED