synthesisui 0.16.104 → 0.16.105

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.
@@ -467,6 +467,14 @@ async function resolveAtNodes(root, entries) {
467
467
  * this sends the evidence rather than a second implementation of it.
468
468
  */
469
469
  ...(look?.rootTag ? { rootTag: look.rootTag } : {}),
470
+ /**
471
+ * HOW LONG THE SKETCH IS, so the far side can tell an `at` that names a
472
+ * node from an `at` that names nothing. A reading points at the sketch by
473
+ * INDEX - that is what keeps a class string from being retyped - and an
474
+ * index past the end resolves to no classes at all, which is the silent
475
+ * version of sending none (dono, 02/08).
476
+ */
477
+ ...(sketch ? { sketchNodes: sketch.length } : {}),
470
478
  }
471
479
  : recipe0;
472
480
  entry = { ...entry, recipe: withLook };
@@ -354,6 +354,9 @@ const STATE_SPELLING = {
354
354
  "read-only": "readOnly",
355
355
  required: "required",
356
356
  placeholder: "placeholder",
357
+ /** Pseudo-ELEMENTS, which the contract spells like any other selector key. */
358
+ after: "after",
359
+ before: "before",
357
360
  highlighted: "highlighted",
358
361
  even: "even",
359
362
  odd: "odd",
@@ -1004,6 +1007,78 @@ function composeGradient(utilities, declared) {
1004
1007
  broken,
1005
1008
  };
1006
1009
  }
1010
+ /**
1011
+ * WHICH HALF OF THE TRANSCRIPTION A MODIFIER LIST BELONGS TO.
1012
+ *
1013
+ * Extracted because a GRADIENT needs the same answer: it is three utilities that
1014
+ * only mean something together, so it is composed per modifier group and then has
1015
+ * to land in the slot that group shares. Two copies of this routing would be two
1016
+ * answers to "where does `dark:after:` go", and the day they differ the reader is
1017
+ * told a decision arrived somewhere it did not.
1018
+ *
1019
+ * MUTATES `out`: a layer or a state block is created on demand, so only call it
1020
+ * once there is something to write.
1021
+ */
1022
+ function slotFor(out, modifiers) {
1023
+ const isDark = modifiers.includes("dark");
1024
+ const rest = modifiers.filter((m) => m !== "dark");
1025
+ if (rest.length === 0) {
1026
+ return { target: isDark ? out.dark : out.base, unslotted: false };
1027
+ }
1028
+ /**
1029
+ * EVERY MODIFIER GETS A SLOT, and the contract has had one all along.
1030
+ *
1031
+ * This used to route a bare state and drop everything else, on a comment
1032
+ * that said "a recipe has no slot for it" - true when it was written and
1033
+ * false since the transcription grew `layers`, which hold `scheme`,
1034
+ * `state`, `within` and `at` TOGETHER. Measured on the owner's library:
1035
+ * 29 real decisions dropped on that stale line, most of them a `dark:`
1036
+ * over a state (not-expressed.md, dono, 01/08).
1037
+ *
1038
+ * A recipe that reconstructs the component must carry them, so the router
1039
+ * reads the whole modifier list and only gives up on a modifier that
1040
+ * genuinely has no home - and then it SAYS so.
1041
+ */
1042
+ const when = {};
1043
+ let at;
1044
+ let unslotted = false;
1045
+ for (const mod of rest) {
1046
+ /** `group-hover:`, `group-data-[checked]:` - the PARENT's state. */
1047
+ const group = /^group-(.+)$/.exec(mod);
1048
+ const bare = group ? group[1] : mod;
1049
+ const raw = STATE_SPELLING[bare] != null
1050
+ ? bare
1051
+ : (DATA_STATE.exec(bare)?.[1] ?? null);
1052
+ const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
1053
+ if (state) {
1054
+ if (group)
1055
+ when.within = state;
1056
+ else
1057
+ when.state = state;
1058
+ continue;
1059
+ }
1060
+ /** `md:`, `lg:` - a breakpoint of theirs, by their own name. */
1061
+ if (BREAKPOINT.test(mod) && !at) {
1062
+ at = mod;
1063
+ continue;
1064
+ }
1065
+ unslotted = true;
1066
+ }
1067
+ if (unslotted)
1068
+ return { target: null, unslotted: true };
1069
+ if (isDark)
1070
+ when.scheme = "dark";
1071
+ // A bare single state with no scheme keeps its own slot: `states` is what
1072
+ // the compiler and every lens already read, and moving it to a layer
1073
+ // would be a shape change nothing asked for.
1074
+ if (when.state && !when.within && !when.scheme && !at && rest.length === 1) {
1075
+ return {
1076
+ target: out.states[when.state] ?? (out.states[when.state] = {}),
1077
+ unslotted: false,
1078
+ };
1079
+ }
1080
+ return { target: layerFor(out, when, at).style, unslotted: false };
1081
+ }
1007
1082
  export function transcribe(classes, declared) {
1008
1083
  const out = {
1009
1084
  base: {},
@@ -1016,92 +1091,64 @@ export function transcribe(classes, declared) {
1016
1091
  fromLiteral: 0,
1017
1092
  };
1018
1093
  /**
1019
- * The composed gradient first: it is one decision spread across four classes, and
1020
- * only the whole list sees all the pieces. Base-level only - a per-scheme stop
1021
- * (`dark:from-x`) is a condition this pass does not attempt, and it lands in
1022
- * `unreadable` below rather than half-composing.
1094
+ * A GRADIENT IS THREE UTILITIES, AND IT BELONGS TO THE SLOT THEY SHARE.
1095
+ *
1096
+ * `composeGradient` ran on the BARE utilities only, so a gradient under any
1097
+ * modifier was invisible - and the one gradient in a real 36-component library
1098
+ * is under two of them: a Card draws its dark border as a masked `::after`
1099
+ * filled with `dark:after:bg-gradient-to-b dark:after:from-electric-indigo-500
1100
+ * dark:after:to-vivid-pink-500`. With the mask read and the paint missing, that
1101
+ * edge is a shape with no colour, which is the same invisible as before
1102
+ * (not-expressed.md, dono, 02/08).
1103
+ *
1104
+ * Grouped by the modifier list because that is what a gradient's three parts
1105
+ * have in common, and routed through the same `slotFor` the loop below uses.
1023
1106
  */
1024
- const bareUtilities = classes
1025
- .filter((cls) => parseClass(cls).modifiers.length === 0)
1026
- .map((cls) => parseClass(cls).utility);
1027
- const gradient = composeGradient(bareUtilities, declared);
1028
- if (gradient.value)
1029
- out.base.backgroundImage = gradient.value;
1030
- for (const broke of gradient.broken)
1031
- out.unreadable.push(broke);
1107
+ const consumedByGroup = new Set();
1108
+ const groups = new Map();
1032
1109
  for (const cls of classes) {
1033
1110
  const { modifiers, utility } = parseClass(cls);
1034
- if (gradient.consumed.has(utility) && modifiers.length === 0)
1111
+ const key = modifiers.join(":");
1112
+ const entry = groups.get(key) ?? { modifiers, utilities: [] };
1113
+ entry.utilities.push(utility);
1114
+ groups.set(key, entry);
1115
+ }
1116
+ for (const group of groups.values()) {
1117
+ const gradient = composeGradient(group.utilities, declared);
1118
+ if (gradient.consumed.size === 0)
1119
+ continue;
1120
+ const prefix = group.modifiers.length
1121
+ ? `${group.modifiers.join(":")}:`
1122
+ : "";
1123
+ for (const utility of gradient.consumed)
1124
+ consumedByGroup.add(`${prefix}${utility}`);
1125
+ if (gradient.value) {
1126
+ // Slotted only now: `slotFor` creates the layer it answers with, and an
1127
+ // empty layer is a shape the lint rejects on the way out.
1128
+ const { target, unslotted } = slotFor(out, group.modifiers);
1129
+ if (unslotted || !target) {
1130
+ for (const utility of gradient.consumed)
1131
+ out.skipped.push(`${prefix}${utility}`);
1132
+ continue;
1133
+ }
1134
+ target.backgroundImage = gradient.value;
1135
+ continue;
1136
+ }
1137
+ for (const broke of gradient.broken) {
1138
+ const at = `${prefix}${broke}`;
1139
+ if (!out.unreadable.includes(at))
1140
+ out.unreadable.push(at);
1141
+ }
1142
+ }
1143
+ for (const cls of classes) {
1144
+ const { modifiers, utility } = parseClass(cls);
1145
+ if (consumedByGroup.has(cls))
1035
1146
  continue;
1036
- const isDark = modifiers.includes("dark");
1037
- const rest = modifiers.filter((m) => m !== "dark");
1038
1147
  // THE SLOT IS DECIDED BEFORE THE VALUE IS READ. Resolving first meant a
1039
1148
  // `dark:hover:` whose colour happened to be undeclared vanished in silence
1040
1149
  // rather than being reported as a look we have nowhere to put - two
1041
1150
  // different problems, and only one of them is theirs to hear about.
1042
- let target = null;
1043
- let unslotted = false;
1044
- if (rest.length === 0) {
1045
- target = isDark ? out.dark : out.base;
1046
- }
1047
- else {
1048
- /**
1049
- * EVERY MODIFIER GETS A SLOT, and the contract has had one all along.
1050
- *
1051
- * This used to route a bare state and drop everything else, on a comment
1052
- * that said "a recipe has no slot for it" - true when it was written and
1053
- * false since the transcription grew `layers`, which hold `scheme`,
1054
- * `state`, `within` and `at` TOGETHER. Measured on the owner's library:
1055
- * 29 real decisions dropped on that stale line, most of them a `dark:`
1056
- * over a state (not-expressed.md, dono, 01/08).
1057
- *
1058
- * A recipe that reconstructs the component must carry them, so the router
1059
- * reads the whole modifier list and only gives up on a modifier that
1060
- * genuinely has no home - and then it SAYS so.
1061
- */
1062
- const when = {};
1063
- let at;
1064
- for (const mod of rest) {
1065
- /** `group-hover:`, `group-data-[checked]:` - the PARENT's state. */
1066
- const group = /^group-(.+)$/.exec(mod);
1067
- const bare = group ? group[1] : mod;
1068
- const raw = STATE_SPELLING[bare] != null
1069
- ? bare
1070
- : (DATA_STATE.exec(bare)?.[1] ?? null);
1071
- const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
1072
- if (state) {
1073
- if (group)
1074
- when.within = state;
1075
- else
1076
- when.state = state;
1077
- continue;
1078
- }
1079
- /** `md:`, `lg:` - a breakpoint of theirs, by their own name. */
1080
- if (BREAKPOINT.test(mod) && !at) {
1081
- at = mod;
1082
- continue;
1083
- }
1084
- unslotted = true;
1085
- }
1086
- if (!unslotted) {
1087
- if (isDark)
1088
- when.scheme = "dark";
1089
- // A bare single state with no scheme keeps its own slot: `states` is what
1090
- // the compiler and every lens already read, and moving it to a layer
1091
- // would be a shape change nothing asked for.
1092
- if (when.state &&
1093
- !when.within &&
1094
- !when.scheme &&
1095
- !at &&
1096
- rest.length === 1) {
1097
- target = out.states[when.state] ?? (out.states[when.state] = {});
1098
- }
1099
- else {
1100
- const layer = layerFor(out, when, at);
1101
- target = layer.style;
1102
- }
1103
- }
1104
- }
1151
+ const { target, unslotted } = slotFor(out, modifiers);
1105
1152
  const decl = readUtility(utility, declared);
1106
1153
  if (unslotted) {
1107
1154
  // Only worth saying when it IS a design value; an unslotted `sm:flex` is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.104",
3
+ "version": "0.16.105",
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": {