synthesisui 0.16.82 → 0.16.84

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.
@@ -525,6 +525,116 @@ export function readInlineConditions(source) {
525
525
  }
526
526
  return layers;
527
527
  }
528
+ /**
529
+ * A LOOKUP RECORD - the shape 16 of 35 components in a real library use, against 2 for
530
+ * `cva`.
531
+ *
532
+ * const colorStyles: Record<TagColor, string> = {
533
+ * primary: "bg-ocean-500 text-white",
534
+ * danger: "bg-danger-400 text-white",
535
+ * };
536
+ * className={cn("inline-flex px-2.5 rounded-full", colorStyles[color], className)}
537
+ *
538
+ * As structured as `cva` and more common by five to one. The first version of this file
539
+ * read `cva` and the conditional expression - the two shapes I assumed - and covered 6%
540
+ * of the library. Enumerating first is the rule that produced this function (dono, 01/08).
541
+ *
542
+ * THE PROP COMES FROM THE INDEX, not from the record's name. `colorStyles[color]` says
543
+ * the axis is `color`; the variable could be called anything. When the index is not a
544
+ * bare identifier - `styles[key]`, `map[a ?? b]` - the axis is unknowable and the record
545
+ * is skipped rather than attached to a guess.
546
+ */
547
+ const LOOKUP_RECORD = /(?:const|let)\s+([A-Za-z_$][\w$]*)\s*(?::\s*(?:Record|Partial<Record)<[^>]*>+)?\s*=\s*\{/g;
548
+ /** `colorStyles[color]` - the record, and the prop that indexes it. */
549
+ const RECORD_INDEX = /([A-Za-z_$][\w$]*)\s*\[\s*([A-Za-z_$][\w$]*)\s*\]/g;
550
+ export function readLookupRecords(source,
551
+ /** Filled with axis → options, because the axis NAMES have to travel too: a first pass
552
+ * sent the styles and not the names, so a Tag with eight colours reported no axis at
553
+ * all (probe, 01/08). */
554
+ axesOut) {
555
+ const clean = stripComments(source);
556
+ // Which prop indexes which record. Read first, because a record nobody indexes is a
557
+ // constant rather than a variant axis.
558
+ const axisOf = new Map();
559
+ RECORD_INDEX.lastIndex = 0;
560
+ for (const m of clean.matchAll(RECORD_INDEX)) {
561
+ if (!axisOf.has(m[1]))
562
+ axisOf.set(m[1], m[2]);
563
+ }
564
+ const layers = [];
565
+ LOOKUP_RECORD.lastIndex = 0;
566
+ for (const m of clean.matchAll(LOOKUP_RECORD)) {
567
+ const symbol = m[1];
568
+ const axis = axisOf.get(symbol);
569
+ if (!axis)
570
+ continue;
571
+ const open = (m.index ?? 0) + m[0].length - 1;
572
+ const end = endOf(clean, open);
573
+ if (end === -1)
574
+ continue;
575
+ const entries = topLevelEntries(clean.slice(open + 1, end - 1));
576
+ // Two options is what makes a closed set somebody chose from - the same rule the
577
+ // contract writer and the doctor already follow.
578
+ const options = entries.filter((e) => literalClasses(e.value).length > 0);
579
+ if (options.length < 2)
580
+ continue;
581
+ if (axesOut) {
582
+ const names = options.map((o) => o.key);
583
+ axesOut[axis] = [...new Set([...(axesOut[axis] ?? []), ...names])];
584
+ }
585
+ for (const option of options) {
586
+ const per = splitByCondition(literalClasses(option.value), {
587
+ [axis]: option.key,
588
+ });
589
+ layers.push(...per.layers);
590
+ }
591
+ }
592
+ return layers;
593
+ }
594
+ /**
595
+ * A TERNARY BETWEEN TWO CLASS LITERALS - 8 of 35 in the same library.
596
+ *
597
+ * className={cn("base", isActive ? "bg-ocean-500" : "bg-transparent")}
598
+ *
599
+ * Both branches are a look, so both are a layer: the truthy one under the prop being
600
+ * `true` and the falsy one under `false`. Reading only the truthy half would say the
601
+ * component has one conditional look when it has two.
602
+ */
603
+ const CLASS_TERNARY = /([!A-Za-z_$][\w$.!]*)\s*(?:===\s*["']([^"']+)["']\s*)?\?\s*["']([^"']*)["']\s*:\s*["']([^"']*)["']/g;
604
+ export function readClassTernaries(source) {
605
+ const layers = [];
606
+ CLASS_TERNARY.lastIndex = 0;
607
+ for (const m of stripComments(source).matchAll(CLASS_TERNARY)) {
608
+ const negated = m[1].startsWith("!");
609
+ const prop = m[1].replace(/^!/, "").split(".").pop() ?? "";
610
+ if (!prop)
611
+ continue;
612
+ // `x === "solid" ? a : b` names the option; a bare `x ? a : b` is the boolean, and a
613
+ // negated condition swaps which branch means true.
614
+ const whenTrue = m[2] ?? (negated ? "false" : "true");
615
+ const whenFalse = m[2] ? null : negated ? "true" : "false";
616
+ for (const [option, raw] of [
617
+ [whenTrue, m[3]],
618
+ [whenFalse, m[4]],
619
+ ]) {
620
+ if (!option)
621
+ continue;
622
+ const classes = raw.split(/\s+/).filter(Boolean);
623
+ if (classes.length === 0)
624
+ continue;
625
+ const per = splitByCondition(classes, { [prop]: option });
626
+ layers.push(...per.layers);
627
+ }
628
+ }
629
+ return layers;
630
+ }
631
+ /**
632
+ * A LOOKUP INDEXED BY A TERNARY, which is how the same record serves a boolean.
633
+ *
634
+ * Left out on purpose, and said so rather than half-read: `styles[selected ? "on" : "off"]`
635
+ * would need both the record and the condition resolved together, and the enumeration
636
+ * found exactly four of them in 530 files. Four is not worth a wrong reading.
637
+ */
528
638
  /**
529
639
  * The whole reading of one file's variant styling, transcribed - ready for a recipe.
530
640
  *
@@ -533,7 +643,13 @@ export function readInlineConditions(source) {
533
643
  */
534
644
  export function transcribeVariants(source, declared) {
535
645
  const reads = readVariants(source);
536
- const inline = readInlineConditions(source);
646
+ const fromRecords = {};
647
+ const inline = [
648
+ ...readInlineConditions(source),
649
+ // The two shapes the enumeration found to be five times more common than `cva`.
650
+ ...readLookupRecords(source, fromRecords),
651
+ ...readClassTernaries(source),
652
+ ];
537
653
  const axes = {};
538
654
  const defaults = {};
539
655
  const allLayers = [];
@@ -553,6 +669,10 @@ export function transcribeVariants(source, declared) {
553
669
  }
554
670
  }
555
671
  allLayers.push(...inline);
672
+ for (const [axis, options] of Object.entries(fromRecords)) {
673
+ if (options.length >= 2)
674
+ axes[axis] = options;
675
+ }
556
676
  const layers = allLayers
557
677
  .map((layer) => {
558
678
  /**
@@ -537,6 +537,49 @@ extensions are configured at construction, not toggled later
537
537
  of their manifest and attaches it (\`"^2.1.0 in packages/ui"\`), so the rule does not go stale the
538
538
  day they upgrade and the information is not lost either.
539
539
 
540
+ ### Ask what a recipe can hold, BEFORE you write one
541
+
542
+ \`\`\`
543
+ recipe_vocabulary what a recipe CAN hold - states, forms, the floor per kind
544
+ validate_recipe(name, r) would this survive? what would NOT, by name
545
+ \`\`\`
546
+
547
+ Both are MCP tools on the \`synthesisui\` server. **Call \`recipe_vocabulary\` once, at the
548
+ start**, and work from what it says rather than from what you remember - it is served from
549
+ the live contract, so it is right about a state that was added last week and this file is
550
+ not.
551
+
552
+ Then **validate every recipe before you send it.** This is not a formality. Nothing checked
553
+ a recipe before these tools existed, and four separate readings were lost in silence: a
554
+ \`dark:\` inside a variant, a state the compiler cannot spell, a part name with a dot in it,
555
+ a layer with an empty style. Each of those was correct work that arrived as nothing.
556
+
557
+ \`validate_recipe\` never answers yes or no. It answers with what would not survive:
558
+
559
+ \`\`\`
560
+ REFUSED by the contract fix it or the WHOLE recipe is dropped
561
+ HELD BY THE SCHEMA AND the silent half - it validates, it compiles to nothing.
562
+ DROPPED BY THE COMPILER This is the one worth fixing before sending.
563
+ BELOW THE FLOOR it would arrive and not be drawable
564
+ \`\`\`
565
+
566
+ ### Write down what we could not hold
567
+
568
+ Everything \`validate_recipe\` reports goes into \`_synthesisui/not-expressed.md\`, appended
569
+ per component:
570
+
571
+ \`\`\`
572
+ ## MetricCard
573
+ - layers[2].when.scheme … (what the validator said, VERBATIM)
574
+ - below the floor: a fill, so it reads as a surface rather than as bare text
575
+ - the source you read, quoted, so the gap can be checked without opening their repo
576
+ \`\`\`
577
+
578
+ **Verbatim, and with the source beside it.** This file is how a gap on OUR side gets fixed
579
+ instead of being worked around: said only in chat it evaporates, and paraphrased it becomes
580
+ unfindable. If the file ends up empty, say so - that is the good outcome and it is worth
581
+ one line.
582
+
540
583
  ### The floor: what a preview needs before it can draw anything
541
584
 
542
585
  The owner asked this directly - "to render the button, what do I need to have?" - and the
@@ -599,6 +642,9 @@ found them wastes a turn and risks contradicting the measurement:
599
642
  - **whether a component forwards the rest of its props**, and which it names
600
643
  - **which components appear inside which** at their call sites, as pairs
601
644
  - **the folder architecture**, and it will ask you which one has priority
645
+ - **which of your components are ours**, matched against the LIVE catalogue rather than a
646
+ list baked into the CLI. If a run says it fell back to the built-in names, the match is a
647
+ smaller answer than it should be and the reason is on screen
602
648
 
603
649
  What is still yours: the anatomy tree, the part names, the frontiers, the roles, the
604
650
  concept, and the rules that are not about a class name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.82",
3
+ "version": "0.16.84",
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": {