@yschimke/compose-design-map 1.45.0 → 1.46.0

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.
Files changed (2) hide show
  1. package/design-map.mjs +61 -5
  2. package/package.json +1 -1
package/design-map.mjs CHANGED
@@ -68,9 +68,37 @@ const LIGHT_MODE = "Light";
68
68
  /** The tag discovery puts in the id of an `@OverrideVariant` reseed: `…_VARIANT_<name>`. */
69
69
  const VARIANT_TAG = "_VARIANT_";
70
70
 
71
+ /**
72
+ * The head of a capture's id with the `_VARIANT_<name>` reseed suffix removed, and whether there
73
+ * was one — `{ head, reseed }`.
74
+ *
75
+ * `_VARIANT_` is a legal substring of a Kotlin function name, so a plain `includes()` read the BASE
76
+ * capture of a composable called `Icon_VARIANT_Only` as a generated reseed. Every `continue` guarded
77
+ * by that test then dropped the composable outright, including its explicit `noReference` — a
78
+ * record whose whole purpose is to survive into the diagnostics.
79
+ *
80
+ * Discovery appends the tag to the id it has already built (`base.id + "_VARIANT_<name>"`), so the
81
+ * marker of an actual reseed is the LAST one and the function's own name is still in front of it.
82
+ * Splitting there and requiring the `.<functionName>` marker to survive in the head distinguishes
83
+ * the two: `…FooKt.Icon_VARIANT_Only_Light` splits to `…FooKt.Icon`, which no longer contains
84
+ * `.Icon_VARIANT_Only`, so it is a base capture; a real reseed of `Icon`,
85
+ * `…FooKt.Icon_Light_VARIANT_pressed`, splits to `…FooKt.Icon_Light`, which still contains `.Icon`.
86
+ *
87
+ * A capture that names no function cannot be told apart this way, so it keeps the old reading.
88
+ */
89
+ function splitVariantTag(preview) {
90
+ const id = String(preview.id ?? "");
91
+ const at = id.lastIndexOf(VARIANT_TAG);
92
+ if (at < 0) return { head: id, reseed: false };
93
+ const head = id.slice(0, at);
94
+ const marker = preview.functionName ? `.${preview.functionName}` : null;
95
+ if (marker && !head.includes(marker)) return { head: id, reseed: false };
96
+ return { head, reseed: true };
97
+ }
98
+
71
99
  /** Whether a capture is an `@OverrideVariant` reseed rather than a base capture. */
72
100
  function isVariantCapture(preview) {
73
- return String(preview.id ?? "").includes(VARIANT_TAG);
101
+ return splitVariantTag(preview).reseed;
74
102
  }
75
103
 
76
104
  /**
@@ -80,13 +108,14 @@ function isVariantCapture(preview) {
80
108
  * is the `@Preview` name a multipreview gives the capture — `Light` / `Dark` for a themed pair, and
81
109
  * EMPTY for an unnamed single capture. Splitting on the function name rather than pattern-matching
82
110
  * the tail is what lets a dark-first catalog be recognised at all: its ids carry no mode segment to
83
- * match against.
111
+ * match against. The reseed suffix comes off first, via [splitVariantTag] rather than a bare
112
+ * substring split, so a function whose own name contains `_VARIANT_` keeps its whole identity.
84
113
  *
85
114
  * A capture whose id does not contain its own function name is left as its own subject with an
86
115
  * empty mode — it cannot be grouped with anything, so it selects itself.
87
116
  */
88
117
  export function captureIdentity(preview) {
89
- const head = String(preview.id ?? "").split(VARIANT_TAG)[0];
118
+ const { head } = splitVariantTag(preview);
90
119
  const marker = preview.functionName ? `.${preview.functionName}` : null;
91
120
  const at = marker ? head.lastIndexOf(marker) : -1;
92
121
  if (at < 0) return { subject: head, mode: "" };
@@ -519,8 +548,28 @@ export function variantRendersByComponent(previews, selection = selectCaptures(p
519
548
  const seeds = variantSeeds(preview);
520
549
  if (!seeds.length) continue;
521
550
 
551
+ // A `@CatalogVariant` may state its OWN kit correspondence, and either spelling changes what a
552
+ // resolver should do with this render. Without reading them here a variant's declaration is
553
+ // emitted under the parent's `reference` regardless — which is precisely the mispairing the two
554
+ // fields were added to prevent.
555
+ //
556
+ // `noReference` says the kit exports no cell this render could honestly pair with. Handing it
557
+ // to the resolver anyway scores it against the PARENT's picture, so it is dropped: the absence
558
+ // is already reported once, as a stated absence, and a render that says "there is nothing to
559
+ // compare me to" must not then be compared.
560
+ if (isCatalogVariant && catalog.noReference) continue;
561
+
522
562
  const list = byComponent.get(catalog.componentId) ?? [];
523
- list.push({ previewId: preview.id, name: variantName(preview, seeds), seeds });
563
+ list.push({
564
+ previewId: preview.id,
565
+ name: variantName(preview, seeds),
566
+ seeds,
567
+ // `reference` names the variant's own kit cell. Carried onto the render so a resolver pairs
568
+ // that handle instead of deriving one from the parent's by seed. Additive to the sidecar's
569
+ // shape — a resolver that does not read it sees exactly what it saw before, which is why the
570
+ // schema string does not move.
571
+ ...(isCatalogVariant && catalog.reference ? { reference: catalog.reference } : {}),
572
+ });
524
573
  byComponent.set(catalog.componentId, list);
525
574
  }
526
575
  return byComponent;
@@ -608,7 +657,14 @@ export function projectDesignMap(previews, opts = {}) {
608
657
  if (catalog.noReference) {
609
658
  statedAbsentIds.set(`component:${id}`, { label: id, reason: catalog.noReference });
610
659
  statedAbsentComponentIds.add(id);
611
- } else if (!statedAbsentIds.has(id)) unmappedIds.set(id, true);
660
+ // Unconditional: [unmapped] below filters this set through [statedAbsentComponentIds], which
661
+ // is the only correct place for it — a component's stated absence may be read from a LATER
662
+ // capture than the one that first reports it unmapped, and a guard here can only see what has
663
+ // been read so far. The guard that used to stand here tested a bare `id` against a map now
664
+ // keyed `component:<id>` / `subject:<id>`, so for an ordinary id it never matched, and for a
665
+ // component legally named `component:X` it matched the wrong entry and dropped it from
666
+ // `unmapped` — letting `--strict` pass over a component with no reference and no reason.
667
+ } else unmappedIds.set(id, true);
612
668
  }
613
669
  /** Components carrying neither a reference nor a stated reason for its absence. */
614
670
  const unmapped = [...unmappedIds.keys()].filter((id) => !statedAbsentComponentIds.has(id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yschimke/compose-design-map",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "Project a compose-preview discovery manifest into design-parity's design-map.json, plus a sidecar of unresolved variant declarations. Dependency-free.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",