synthesisui 0.16.219 → 0.16.220

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.
@@ -6,7 +6,7 @@ import { generateComponentFiles } from "../component-codegen.js";
6
6
  import { readProjectConfig, resolveRegistry } from "../config.js";
7
7
  import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
8
8
  import { body, section, snippet } from "../output.js";
9
- import { findCollision, reactMajorOf, readInstalledConvention, } from "../project-facts.js";
9
+ import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
10
10
  import { fetchComponent, RegistryError } from "../registry.js";
11
11
  /**
12
12
  * Writes the shared `cn.ts` next to the components, built from THIS project's
@@ -157,7 +157,7 @@ export async function component(slug, name, opts) {
157
157
  * it, so the TSX and the stylesheet in the same folder agree by
158
158
  * construction. Falling back to disk keeps an older registry working.
159
159
  */
160
- res.classNames ?? (await readInstalledConvention(root, slug)));
160
+ res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
161
161
  for (const file of files) {
162
162
  await writeFile(join(compDir, file.filename), file.code, "utf8");
163
163
  }
@@ -2,7 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
- import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
5
+ import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
6
6
  import { postGenerate, RegistryError } from "../registry.js";
7
7
  /** PascalCase para o hint de import (course-card → CourseCard). */
8
8
  function pascalName(name) {
@@ -74,7 +74,7 @@ export async function generate(description, opts) {
74
74
  const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, config.styles, await reactMajorOf(root),
75
75
  // Read off the installed document: a generated component lands in the same
76
76
  // project as the stylesheet it has to match.
77
- await readInstalledConvention(root, slug));
77
+ await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
78
78
  for (const file of files) {
79
79
  await writeFile(join(compDir, file.filename), file.code, "utf8");
80
80
  }
@@ -3,7 +3,7 @@ import { basename, join } from "node:path";
3
3
  import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
5
  import { body, section, snippet } from "../output.js";
6
- import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
6
+ import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
7
7
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
8
8
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
9
9
  * a folder holding only refit artifacts doesn't count). */
@@ -133,7 +133,7 @@ export async function refit(file, opts) {
133
133
  if (config.target === "next") {
134
134
  const compDir = join(root, config.componentsDir, res.name);
135
135
  await mkdir(compDir, { recursive: true });
136
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug));
136
+ const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
137
137
  for (const f of files) {
138
138
  await writeFile(join(compDir, f.filename), f.code, "utf8");
139
139
  }
@@ -7,7 +7,7 @@ import { readProjectConfig, resolveRegistry } from "../config.js";
7
7
  import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
8
8
  import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
9
9
  import { body, section, snippet } from "../output.js";
10
- import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
10
+ import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
11
11
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
12
  import { add } from "./add.js";
13
13
  import { reportWhatIsLeft } from "./align.js";
@@ -300,7 +300,7 @@ export async function upgrade(asked, opts) {
300
300
  // Both were missing here, and `upgrade` is the command that REWRITES
301
301
  // components somebody already has: without the convention it would have
302
302
  // taken a working component and stripped its styles.
303
- await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)));
303
+ await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
304
304
  for (const file of files) {
305
305
  await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
306
306
  }
@@ -192,12 +192,50 @@ function layerAxes(part, variants) {
192
192
  }
193
193
  return out;
194
194
  }
195
- /** The axes of a part: the ones it declares, plus the ones its layers read. */
195
+ /**
196
+ * The axes of a NODE - root or part: the ones it declares, plus the ones its layers
197
+ * read. Both callers use this; a node that reads only `variants` is the reader that
198
+ * dropped 15 of his 19 components.
199
+ */
196
200
  function partAxesOf(part, variants) {
197
201
  const own = axesOf(part.variants ?? {});
198
202
  const keys = new Set(own.map((a) => a.key));
199
203
  return [...own, ...layerAxes(part, variants).filter((a) => !keys.has(a.key))];
200
204
  }
205
+ /**
206
+ * THE AXES OF THE COMPONENT ITSELF - and an axis nothing styles is still his API.
207
+ *
208
+ * `axesOf` drops an axis whose every option block is empty, which is right for a
209
+ * recipe we generated: there the option IS the styles, so an empty one is noise we
210
+ * invented. On a recipe we READ it is the opposite. His `Card` declares
211
+ * `variant: default | full-border | left-border` and this document carries no
212
+ * declarations for any of the three - the axis is a fact about HIS component that
213
+ * we failed to read the look for, and dropping the prop would silently narrow the
214
+ * API his own call sites already use.
215
+ *
216
+ * So the prop and the `data-variant` ship. The missing look is a reading gap, and
217
+ * it is already counted as one - `gaps` and the ledger say so with a number, which
218
+ * is the honest place for it. Ten of his components sat in exactly this state.
219
+ */
220
+ function rootAxes(recipe) {
221
+ const fromLayers = partAxesOf(recipe, recipe.variants);
222
+ const seen = new Set(fromLayers.map((a) => a.key));
223
+ const declared = [];
224
+ for (const [axis, options] of Object.entries(recipe.variants ?? {})) {
225
+ const keys = Object.keys(options);
226
+ if (keys.length === 0 || seen.has(axis))
227
+ continue;
228
+ declared.push({
229
+ key: axis,
230
+ prop: camel(axis),
231
+ attr: kebab(axis),
232
+ boolean: keys.every((k) => k === "true" || k === "false"),
233
+ styledFalse: keys.includes("false"),
234
+ options: keys,
235
+ });
236
+ }
237
+ return [...fromLayers, ...declared];
238
+ }
201
239
  function axesOf(variants) {
202
240
  const axes = [];
203
241
  for (const [axis, options] of Object.entries(variants ?? {})) {
@@ -489,17 +527,196 @@ function declToTailwind(prop, value) {
489
527
  }
490
528
  return [arbitrary(prop, value)];
491
529
  }
492
- const STATE_PREFIX = {
493
- hover: "hover:",
494
- focus: "focus:",
495
- focusVisible: "focus-visible:",
496
- active: "active:",
497
- disabled: "disabled:",
530
+ /**
531
+ * TAILWIND'S OWN WORD FOR A STATE, WHEN IT HAS ONE - and a mechanical rule when it
532
+ * does not, because a hand-written table would be a table of LIBRARIES.
533
+ *
534
+ * The contract's `STATE_SELECTORS` is a closed vocabulary of about thirty states,
535
+ * and most of them are attribute-shaped on purpose: `pressed` is how @base-ui
536
+ * spells it, `highlighted` is the option the arrow keys are on, `startingStyle` is
537
+ * an enter animation. Naming each one here by hand would rot the moment the
538
+ * contract grows a thirty-first, and the CLI cannot import the contract (it ships
539
+ * standalone), so a mirror would be a second copy to keep in step.
540
+ *
541
+ * The rule instead: a state Tailwind already has a variant for uses that word;
542
+ * everything else becomes `data-[<kebab>]:`, which is exactly the selector the
543
+ * compiler emits for it. New state in the contract, no edit here.
544
+ */
545
+ const TW_NATIVE = new Set([
546
+ "hover",
547
+ "active",
548
+ "checked",
549
+ "disabled",
550
+ "empty",
551
+ "even",
552
+ "first",
553
+ "indeterminate",
554
+ "invalid",
555
+ "last",
556
+ "odd",
557
+ "open",
558
+ "placeholder",
559
+ "read-only",
560
+ "required",
561
+ "target",
562
+ "visited",
563
+ "after",
564
+ "before",
565
+ ]);
566
+ /**
567
+ * WHERE ONE STATE HAS MORE THAN ONE SPELLING, ALL OF THEM SHIP.
568
+ *
569
+ * `disabled` is `:disabled` on a real control, `[aria-disabled]` on an accessible
570
+ * div and `[data-disabled]` in every headless kit - the compiler matches all three
571
+ * with one comma-joined rule, and picking one here would be picking a library. In
572
+ * utilities that costs one extra class per declaration, on the handful of states
573
+ * that actually have two grammars.
574
+ */
575
+ const TW_ALSO = {
576
+ disabled: ["aria-disabled:", "data-[disabled]:"],
577
+ checked: ["aria-checked:", "data-[checked]:"],
578
+ open: ["aria-expanded:", "data-[open]:"],
579
+ selected: ["aria-selected:", "data-[selected]:"],
498
580
  };
581
+ /** A state name → every Tailwind variant that expresses it. */
582
+ function twStateVariants(state) {
583
+ // The compiler maps both to `:focus-visible` - a focus ring that also shows on
584
+ // a mouse click is the one thing nobody wants back.
585
+ if (state === "focus" || state === "focusVisible")
586
+ return ["focus-visible:"];
587
+ const word = kebab(state);
588
+ const native = TW_NATIVE.has(word) ? [`${word}:`] : [`data-[${word}]:`];
589
+ return [...native, ...(TW_ALSO[state] ?? [])];
590
+ }
499
591
  function blockToTailwind(block, prefix = "") {
500
592
  return Object.entries(block).flatMap(([prop, value]) => declToTailwind(prop, value).map((cls) => `${prefix}${cls}`));
501
593
  }
594
+ /** Every prefix combination one layer needs. A condition that cannot be
595
+ * expressed returns no combination, and the layer is dropped rather than
596
+ * emitted wrong - the same choice the compiler makes for an unknown state. */
597
+ function twPrefixes(when, at, scheme,
598
+ /** On the ROOT, `within` IS its own state - the compiler says so explicitly. */
599
+ asRoot) {
600
+ const groups = [];
601
+ /**
602
+ * THE SCHEME THE DOCUMENT OPENS IN IS THE RESTING LOOK, and getting this
603
+ * backwards is the one mistake here that inverts light and dark. His document
604
+ * opens DARK, so its 79 `scheme: dark` layers are what the component looks like
605
+ * at rest and they take no prefix at all; `light` is the one that needs the
606
+ * ancestor. `compile-css.ts` decides it the same way, off `docScheme`.
607
+ */
608
+ if (when.scheme && when.scheme !== scheme) {
609
+ // Both attributes, because that is what the compiled stylesheet matches and a
610
+ // real project puts one or the other on <html>.
611
+ groups.push([
612
+ `[[data-scheme=${when.scheme}]_&]:`,
613
+ `[[data-theme=${when.scheme}]_&]:`,
614
+ ]);
615
+ }
616
+ if (when.within) {
617
+ const own = twStateVariants(when.within);
618
+ if (own.length === 0)
619
+ return [];
620
+ groups.push(asRoot ? own : own.map((v) => `group-${v}`));
621
+ }
622
+ if (when.state) {
623
+ const own = twStateVariants(when.state);
624
+ if (own.length === 0)
625
+ return [];
626
+ groups.push(own);
627
+ }
628
+ for (const [axis, option] of Object.entries(when.variant ?? {})) {
629
+ // `!option` is every OTHER option - `not-data-[…]` is the utility for the
630
+ // `:not()` the compiler writes.
631
+ groups.push(option.startsWith("!")
632
+ ? [`not-data-[${kebab(axis)}=${option.slice(1)}]:`]
633
+ : [`data-[${kebab(axis)}=${option}]:`]);
634
+ }
635
+ if (at)
636
+ groups.push([`${at}:`]);
637
+ return groups.reduce((acc, group) => acc.flatMap((prefix) => group.map((g) => `${prefix}${g}`)), [""]);
638
+ }
639
+ function resolveNode(node, scheme, asRoot) {
640
+ const variants = {};
641
+ for (const [axis, options] of Object.entries(node.variants ?? {})) {
642
+ variants[axis] = {};
643
+ for (const [option, block] of Object.entries(options))
644
+ variants[axis][option] = { ...block };
645
+ }
646
+ const states = {};
647
+ for (const [state, block] of Object.entries(node.states ?? {}))
648
+ states[state] = { ...block };
649
+ const conditional = [];
650
+ for (const layer of node.layers ?? []) {
651
+ if (!layer.style || Object.keys(layer.style).length === 0)
652
+ continue;
653
+ const when = { ...(layer.when ?? {}) };
654
+ /**
655
+ * A LAYER FOR THE DOCUMENT'S OWN SCHEME CARRIES NO CONDITION - dropping the key
656
+ * here is what lets `{scheme: dark, variant: ocean}` land in the variant map as
657
+ * plain `ocean`, instead of being pushed into a compound prefix nobody needs.
658
+ */
659
+ if (when.scheme === scheme)
660
+ delete when.scheme;
661
+ const axes = Object.entries(when.variant ?? {});
662
+ const simpleVariant = !layer.at &&
663
+ !when.state &&
664
+ !when.within &&
665
+ !when.scheme &&
666
+ axes.length === 1 &&
667
+ !axes[0][1].startsWith("!");
668
+ const simpleState = !layer.at &&
669
+ !when.within &&
670
+ !when.scheme &&
671
+ axes.length === 0 &&
672
+ Boolean(when.state);
673
+ if (simpleVariant) {
674
+ const [axis, option] = axes[0];
675
+ variants[axis] = variants[axis] ?? {};
676
+ variants[axis][option] = { ...variants[axis][option], ...layer.style };
677
+ continue;
678
+ }
679
+ if (simpleState && when.state) {
680
+ states[when.state] = { ...states[when.state], ...layer.style };
681
+ continue;
682
+ }
683
+ for (const prefix of twPrefixes(when, layer.at, scheme, asRoot))
684
+ conditional.push({ prefix, style: layer.style });
685
+ }
686
+ /**
687
+ * AND THE TOGGLE HAS TO BE ABLE TO UNDO THE RESTING LOOK.
688
+ *
689
+ * A part reads as `base: bg-white` + a `dark` layer; on a dark document that
690
+ * layer is correct bare, but bare it also wins when somebody flips to light. So
691
+ * every property a resting-scheme layer overrides gets its BASE value back under
692
+ * the other scheme's ancestor - the same rule `compile-css.ts` applies, and
693
+ * without it the flip is one-way.
694
+ */
695
+ const alt = scheme === "dark" ? "light" : "dark";
696
+ for (const layer of node.layers ?? []) {
697
+ if (layer.when?.scheme !== scheme)
698
+ continue;
699
+ if (layer.when?.state || layer.when?.within)
700
+ continue;
701
+ const restore = {};
702
+ for (const prop of Object.keys(layer.style ?? {}))
703
+ if (node.base?.[prop] != null)
704
+ restore[prop] = node.base[prop];
705
+ if (Object.keys(restore).length === 0)
706
+ continue;
707
+ for (const prefix of [
708
+ `[[data-scheme=${alt}]_&]:`,
709
+ `[[data-theme=${alt}]_&]:`,
710
+ ])
711
+ conditional.push({ prefix, style: restore });
712
+ }
713
+ return { variants, states, conditional };
714
+ }
715
+ /** The classes every compound condition contributes, in layer order. */
716
+ const conditionalClasses = (resolved) => resolved.conditional.flatMap((c) => blockToTailwind(c.style, c.prefix));
502
717
  function tailwindClassList(recipe,
718
+ /** The node's states and compound conditions, already read off its layers. */
719
+ resolved,
503
720
  /** CSS properties a variant axis owns - see `variantOwnedProps`. */
504
721
  exclude) {
505
722
  const base = exclude
@@ -509,7 +726,10 @@ exclude) {
509
726
  ...blockToTailwind(base),
510
727
  // States keep everything: `hover:` and `disabled:` cannot collide with an
511
728
  // unprefixed variant class, so there is nothing to resolve.
512
- ...Object.entries(recipe.states ?? {}).flatMap(([state, block]) => STATE_PREFIX[state] ? blockToTailwind(block, STATE_PREFIX[state]) : []),
729
+ ...Object.entries(resolved.states).flatMap(([state, block]) => twStateVariants(state).flatMap((prefix) => blockToTailwind(block, prefix))),
730
+ // Compound conditions last: they are the most specific thing the recipe says,
731
+ // and in utilities the later class is the one a reader expects to win.
732
+ ...conditionalClasses(resolved),
513
733
  ];
514
734
  return classes.join(" ");
515
735
  }
@@ -771,7 +991,16 @@ function emitCssMode(slug, name, recipe, version, props, convention,
771
991
  localName = name) {
772
992
  const { tag, attrs, voidEl } = elementFor(name, recipe);
773
993
  const el = asElement(tag, attrs, voidEl);
774
- const axes = axesOf(recipe.variants);
994
+ /**
995
+ * THE ROOT READS ITS AXES THE SAME WAY ITS PARTS DO, which is the whole fix.
996
+ *
997
+ * `axesOf` alone keeps an axis only when the option's own block carries
998
+ * declarations - true for a recipe we generated, and false for every recipe we
999
+ * READ, where the block is empty and the look is in `layers`. The parts learned
1000
+ * this on 04/08; the root was left on the old reader and 15 of his 19 components
1001
+ * with an axis shipped with no prop for it.
1002
+ */
1003
+ const axes = rootAxes(recipe);
775
1004
  const comp = pascal(localName);
776
1005
  const propNames = axes.map((a) => a.prop);
777
1006
  const tree = recipe.preview?.parts ?? [];
@@ -857,28 +1086,56 @@ ${parts.filter(Boolean).join("\n")}`;
857
1086
  }
858
1087
  function emitTailwindMode(slug, name, recipe, version, props,
859
1088
  /** The name it takes in THEIR project. The utilities stay the system's. */
860
- localName = name) {
1089
+ localName = name,
1090
+ /** The scheme the document opens in - see `generateComponentFiles`. */
1091
+ scheme = "dark") {
861
1092
  const { tag, attrs, voidEl } = elementFor(name, recipe);
862
1093
  const el = asElement(tag, attrs, voidEl);
863
- const axes = axesOf(recipe.variants);
1094
+ const axes = rootAxes(recipe);
864
1095
  const comp = pascal(localName);
1096
+ const resolved = resolveNode(recipe, scheme, true);
865
1097
  const variantConsts = axes
866
1098
  .filter((a) => !a.boolean)
867
1099
  .map((a) => {
868
1100
  const entries = a.options
869
- .map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(recipe.variants[a.key]?.[o] ?? {}).join(" "))},`)
1101
+ .map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.[o] ?? {}).join(" "))},`)
870
1102
  .join("\n");
871
1103
  return `const ${a.prop.toUpperCase()}: Record<string, string> = {\n${entries}\n};`;
872
1104
  });
873
1105
  const booleanConsts = axes
874
1106
  .filter((a) => a.boolean)
875
- .map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(recipe.variants[a.key]?.true ?? {}).join(" "))};`);
1107
+ .map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.true ?? {}).join(" "))};`);
876
1108
  // Every property some axis controls leaves BASE, and the base value becomes
877
1109
  // that axis's default - so exactly one class ever sets it and the prop
878
1110
  // actually wins.
879
- const owned = new Map(axes.map((a) => [a.prop, variantOwnedProps(recipe.variants, a)]));
1111
+ const owned = new Map(axes.map((a) => [a.prop, variantOwnedProps(resolved.variants, a)]));
880
1112
  const excluded = new Set([...owned.values()].flat());
881
- const fallbackFor = (a) => JSON.stringify(blockToTailwind(Object.fromEntries(Object.entries(recipe.base).filter(([prop]) => (owned.get(a.prop) ?? []).includes(prop)))).join(" "));
1113
+ /**
1114
+ * `group-hover:` NEEDS A GROUP, and the root is the only node that can carry it.
1115
+ *
1116
+ * `within` is a state of the ROOT read from a PART - the idiom a real table row
1117
+ * is built on, and the contract names the utility outright. Emitting
1118
+ * `group-hover:` on the part while nothing marks the group is a class that
1119
+ * matches nothing, which is the same silent nothing this whole file is about.
1120
+ */
1121
+ const needsGroup = Object.values(recipe.parts ?? {}).some((part) => (part.layers ?? []).some((layer) => layer.when?.within));
1122
+ /**
1123
+ * THE DEFAULT OPTION IS THE RESTING LOOK - which the destructuring already says.
1124
+ *
1125
+ * `{ variant }` undefined means the component IS its default, and on a READ
1126
+ * recipe the base has no value for the property at all (his Button declares no
1127
+ * `backgroundColor` in base - every one of the nine lives in a layer). Falling
1128
+ * back to base alone therefore rendered the default variant as nothing. The
1129
+ * compiler emits the default's layer bare for exactly this reason.
1130
+ */
1131
+ const fallbackFor = (a) => {
1132
+ const fromBase = Object.fromEntries(Object.entries(recipe.base).filter(([prop]) => (owned.get(a.prop) ?? []).includes(prop)));
1133
+ const preset = recipe.defaults?.[a.key];
1134
+ const fromDefault = preset
1135
+ ? (resolved.variants[a.key]?.[preset] ?? {})
1136
+ : {};
1137
+ return JSON.stringify(blockToTailwind({ ...fromBase, ...fromDefault }).join(" "));
1138
+ };
882
1139
  const clsParts = [
883
1140
  "BASE",
884
1141
  ...axes.map((a) => a.boolean
@@ -922,7 +1179,7 @@ ${dataAttrLines(axes)}${axes.length ? "\n" : ""} `;
922
1179
  import type { ${needsElementType ? `ElementType, ${props}` : props} } from "react";
923
1180
  import { cn } from "../cn";
924
1181
 
925
- const BASE = ${JSON.stringify(tailwindClassList(recipe, excluded))};
1182
+ const BASE = ${JSON.stringify([needsGroup ? "group" : "", tailwindClassList(recipe, resolved, excluded)].filter(Boolean).join(" "))};
926
1183
  ${[...variantConsts, ...booleanConsts].join("\n")}
927
1184
 
928
1185
  type ${comp}Props = ${propsType(axes, tag, props, el.offersAs)};
@@ -965,8 +1222,19 @@ ${orderedParts
965
1222
  * toggles data-active from its own state, which is how the GUIDE
966
1223
  * documents the contract.
967
1224
  */
968
- const partVariantClasses = partAxes.flatMap((a) => a.options.flatMap((o) => blockToTailwind(part.variants?.[a.key]?.[o] ?? {}, `data-[${a.attr}=${o}]:`)));
969
- const partCls = [tailwindClassList(part), ...partVariantClasses]
1225
+ /**
1226
+ * AND THE OPTION'S LOOK COMES OFF THE LAYERS TOO - the half of 04/08 that was
1227
+ * left behind. The part got its prop and its `data-status` attribute, and the
1228
+ * classes were read from `part.variants[axis][option]`, which on a read recipe
1229
+ * is the empty block: the attribute shipped, nothing answered it, and the
1230
+ * status colours of his card were still nowhere.
1231
+ */
1232
+ const partResolved = resolveNode(part, scheme, false);
1233
+ const partVariantClasses = partAxes.flatMap((a) => a.options.flatMap((o) => blockToTailwind(partResolved.variants[a.key]?.[o] ?? {}, `data-[${a.attr}=${o}]:`)));
1234
+ const partCls = [
1235
+ tailwindClassList(part, partResolved),
1236
+ ...partVariantClasses,
1237
+ ]
970
1238
  .filter(Boolean)
971
1239
  .join(" ");
972
1240
  /**
@@ -1020,7 +1288,24 @@ convention = DEFAULT_CONVENTION,
1020
1288
  *
1021
1289
  * Absent means the component keeps its own name, which is every caller today.
1022
1290
  */
1023
- localName = name) {
1291
+ localName = name,
1292
+ /**
1293
+ * WHICH SCHEME THE DOCUMENT OPENS IN - `meta.scheme`, and the one input here that
1294
+ * cannot be guessed without inverting light and dark.
1295
+ *
1296
+ * A layer for the document's own scheme is the RESTING look and takes no prefix;
1297
+ * the other one hangs off a `[data-scheme]` ancestor. Read it backwards on a
1298
+ * dark-native system and every dark value moves behind an attribute nothing sets,
1299
+ * so the component arrives unpainted. `readInstalledScheme` answers it off the
1300
+ * document on disk - the same one the stylesheet next to it came from, so the two
1301
+ * can never disagree.
1302
+ *
1303
+ * REQUIRED, with no default, deliberately. `compile-css.ts` defaults to `"dark"`
1304
+ * and a default here would agree with it - but it would also let a new caller ship
1305
+ * without ever deciding, and an optional argument that can be forgotten is the
1306
+ * shape this file has already been bitten by. `tsc` refuses the half-call instead.
1307
+ */
1308
+ scheme) {
1024
1309
  const files = [];
1025
1310
  const props = propsTypeName(reactMajor);
1026
1311
  if (styles === "css") {
@@ -1033,7 +1318,7 @@ localName = name) {
1033
1318
  else {
1034
1319
  files.push({
1035
1320
  filename: `${localName}.tsx`,
1036
- code: `${emitTailwindMode(slug, name, recipe, version, props, localName)}\n`,
1321
+ code: `${emitTailwindMode(slug, name, recipe, version, props, localName, scheme)}\n`,
1037
1322
  });
1038
1323
  }
1039
1324
  files.push({
@@ -64,8 +64,14 @@
64
64
  * carrega - as regras de elemento da folha global dele. Elas viajavam no `design-system.json` desde
65
65
  * sempre e o arquivo que o agente é mandado ler não as citava, então seis regras adotadas no sistema
66
66
  * real eram invisíveis para quem ia escrever a próxima tela.
67
+ *
68
+ * 0.16.217 -> 0.16.220 em 13/08, e o SIM mais forte que esta marca já teve: o COMPONENTE que cai na
69
+ * pasta dele mudou. O codegen lia o estilo condicional só onde uma receita NOSSA o guarda, e uma
70
+ * receita LIDA guarda em `layers` - então 15 dos 19 componentes dele com eixo eram materializados sem
71
+ * a prop, e 104 das 207 condições sem expressão nenhuma. Um `upgrade` anterior a esta versão reescreve
72
+ * os componentes dele com a perda intacta.
67
73
  */
68
- export const MATERIALISER_SINCE = "0.16.217";
74
+ export const MATERIALISER_SINCE = "0.16.220";
69
75
  /**
70
76
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
71
77
  *
@@ -143,3 +143,31 @@ export async function readInstalledConvention(root, slug) {
143
143
  return DEFAULT_CONVENTION;
144
144
  }
145
145
  }
146
+ /**
147
+ * WHICH SCHEME THE INSTALLED SYSTEM OPENS IN, read off the same document on disk.
148
+ *
149
+ * The twin of `readInstalledConvention`, and for the same reason: the document is
150
+ * the one the compiled stylesheet came from, so this can never disagree with the
151
+ * CSS sitting next to it.
152
+ *
153
+ * It decides whether a `scheme: dark` layer is the RESTING look or the alternative
154
+ * one - the whole difference between a component that arrives painted and one whose
155
+ * every dark value hides behind an attribute nothing sets. `"dark"` when the
156
+ * document cannot be read, which is what the compiler assumes when nobody says.
157
+ */
158
+ export async function readInstalledScheme(root, slug) {
159
+ const dir = join(root, "_synthesisui", "ds", slug);
160
+ const version = await pinnedVersion(dir);
161
+ const raw = version
162
+ ? await readFile(join(dir, `v${version}`, "design-system.json"), "utf8").catch(() => "")
163
+ : "";
164
+ if (!raw)
165
+ return "dark";
166
+ try {
167
+ const doc = JSON.parse(raw);
168
+ return doc.meta?.scheme === "light" ? "light" : "dark";
169
+ }
170
+ catch {
171
+ return "dark";
172
+ }
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.219",
3
+ "version": "0.16.220",
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": {