@yschimke/compose-design-map 1.14.1 → 1.16.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.
- package/README.md +13 -2
- package/design-map.mjs +115 -22
- package/emit-design-map.mjs +28 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,10 +122,21 @@ written when nothing declares an axis.
|
|
|
122
122
|
|
|
123
123
|
## Two things worth knowing
|
|
124
124
|
|
|
125
|
-
**
|
|
126
|
-
|
|
125
|
+
**One capture per component is mapped, not one per rendered mode** — a component maps to a single
|
|
126
|
+
design node. Where a composable publishes a themed pair, the **light** capture is the one that
|
|
127
|
+
pairs, because that is the mode design kits draw their frames in: diffing a dark render against a
|
|
127
128
|
light reference reports the whole palette as a finding.
|
|
128
129
|
|
|
130
|
+
Where it publishes exactly **one** mode, that one pairs, whatever it is. A dark-first catalog — a
|
|
131
|
+
Wear watch face is a black screen, so its component multipreview is a single dark capture — names no
|
|
132
|
+
`Light` capture anywhere, and demanding one used to project the whole catalog to an empty map: a
|
|
133
|
+
file reading as "nothing here corresponds to the kit" rather than "the projector could not see
|
|
134
|
+
these", which `--strict` could not fire on either.
|
|
135
|
+
|
|
136
|
+
Several modes with **no light among them** is the one case that stays unmapped. Picking one would be
|
|
137
|
+
guessing which of `Dark` and `Coral` the kit drew, so those components are reported
|
|
138
|
+
(`diagnostics.ambiguousMode`, and a `--strict` failure) rather than paired at random.
|
|
139
|
+
|
|
129
140
|
**`overrides.props` beats `overrides.seeds` where both exist.** They are not the same list. `seeds`
|
|
130
141
|
holds only the values that differ from the composable's defaults; `props` — emitted for a
|
|
131
142
|
`@PreviewAxis` cross product — carries the full axis assignment, defaults included. A cell that
|
package/design-map.mjs
CHANGED
|
@@ -59,16 +59,109 @@
|
|
|
59
59
|
export const DESIGN_MAP_VARIANTS_SCHEMA = "compose-preview-design-map-variants/v1";
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* The capture a
|
|
62
|
+
* The mode a capture is drawn in, when a design map should prefer one: the mode design kits draw
|
|
63
|
+
* their frames in. Diffing a dark render against a light reference reports the whole palette as a
|
|
64
|
+
* finding, so where a light capture exists it is the one that pairs with the reference.
|
|
65
|
+
*/
|
|
66
|
+
const LIGHT_MODE = "Light";
|
|
67
|
+
|
|
68
|
+
/** The tag discovery puts in the id of an `@OverrideVariant` reseed: `…_VARIANT_<name>`. */
|
|
69
|
+
const VARIANT_TAG = "_VARIANT_";
|
|
70
|
+
|
|
71
|
+
/** Whether a capture is an `@OverrideVariant` reseed rather than a base capture. */
|
|
72
|
+
function isVariantCapture(preview) {
|
|
73
|
+
return String(preview.id ?? "").includes(VARIANT_TAG);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A capture's id split into the composable it captures and the mode it was drawn in.
|
|
63
78
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
79
|
+
* Discovery builds an id as `<class>.<function>[_<mode>][_VARIANT_<name>]`, where the mode segment
|
|
80
|
+
* is the `@Preview` name a multipreview gives the capture — `Light` / `Dark` for a themed pair, and
|
|
81
|
+
* EMPTY for an unnamed single capture. Splitting on the function name rather than pattern-matching
|
|
82
|
+
* the tail is what lets a dark-first catalog be recognised at all: its ids carry no mode segment to
|
|
83
|
+
* match against.
|
|
84
|
+
*
|
|
85
|
+
* A capture whose id does not contain its own function name is left as its own subject with an
|
|
86
|
+
* empty mode — it cannot be grouped with anything, so it selects itself.
|
|
67
87
|
*/
|
|
68
|
-
|
|
88
|
+
export function captureIdentity(preview) {
|
|
89
|
+
const head = String(preview.id ?? "").split(VARIANT_TAG)[0];
|
|
90
|
+
const marker = preview.functionName ? `.${preview.functionName}` : null;
|
|
91
|
+
const at = marker ? head.lastIndexOf(marker) : -1;
|
|
92
|
+
if (at < 0) return { subject: head, mode: "" };
|
|
93
|
+
const cut = at + marker.length;
|
|
94
|
+
return { subject: head.slice(0, cut), mode: head.slice(cut).replace(/^_/, "") };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The one mode of a composable's captures that pairs with its design reference, or `null` when the
|
|
99
|
+
* captures do not say which that would be.
|
|
100
|
+
*
|
|
101
|
+
* LIGHT wins whenever it is published, which is every catalog that renders a themed pair — design
|
|
102
|
+
* kits draw their frames in light mode, so a dark render diffed against a light reference reports
|
|
103
|
+
* the whole palette as a finding.
|
|
104
|
+
*
|
|
105
|
+
* A composable that publishes exactly ONE mode pairs with that one, whatever it is. A dark-first
|
|
106
|
+
* catalog — a Wear watch face is a black screen, so its component multipreview is a single dark
|
|
107
|
+
* capture — names no `Light` capture anywhere, and demanding one projected it to an empty map: a
|
|
108
|
+
* file that reads as "nothing here corresponds to the kit" rather than "the projector could not see
|
|
109
|
+
* these", and that `--strict` cannot fire on either, since there is nothing to be strict about
|
|
110
|
+
* (compose-ai-tools#4192).
|
|
111
|
+
*
|
|
112
|
+
* Several modes with no light among them is the case that stays unselected. Picking one would be
|
|
113
|
+
* guessing which of `Dark` and `Coral` the kit drew, and pairing the wrong one diffs a whole
|
|
114
|
+
* palette — so it is reported instead (`diagnostics.ambiguousMode`).
|
|
115
|
+
*/
|
|
116
|
+
function preferredMode(modes) {
|
|
117
|
+
if (modes.has(LIGHT_MODE)) return LIGHT_MODE;
|
|
118
|
+
return modes.size === 1 ? [...modes][0] : null;
|
|
119
|
+
}
|
|
69
120
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Which capture of each composable participates in the projection — one per composable, never one
|
|
123
|
+
* per rendered mode, since a component maps to a single design node.
|
|
124
|
+
*
|
|
125
|
+
* @returns {{participates: (preview: object) => boolean, ambiguous: Array<object>}}
|
|
126
|
+
*/
|
|
127
|
+
export function selectCaptures(previews) {
|
|
128
|
+
const modesBySubject = new Map();
|
|
129
|
+
const componentsBySubject = new Map();
|
|
130
|
+
for (const preview of previews) {
|
|
131
|
+
if (!preview?.catalog) continue;
|
|
132
|
+
const { subject, mode } = captureIdentity(preview);
|
|
133
|
+
const modes = modesBySubject.get(subject) ?? new Set();
|
|
134
|
+
modes.add(mode);
|
|
135
|
+
modesBySubject.set(subject, modes);
|
|
136
|
+
const ids = componentsBySubject.get(subject) ?? new Set();
|
|
137
|
+
if (preview.catalog.componentId) ids.add(preview.catalog.componentId);
|
|
138
|
+
componentsBySubject.set(subject, ids);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const chosen = new Map();
|
|
142
|
+
const ambiguous = [];
|
|
143
|
+
for (const [subject, modes] of modesBySubject) {
|
|
144
|
+
const mode = preferredMode(modes);
|
|
145
|
+
if (mode === null) {
|
|
146
|
+
ambiguous.push({
|
|
147
|
+
subject,
|
|
148
|
+
componentIds: [...(componentsBySubject.get(subject) ?? [])].sort(),
|
|
149
|
+
modes: [...modes].sort(),
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
chosen.set(subject, mode);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
ambiguous.sort((a, b) => a.subject.localeCompare(b.subject));
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
ambiguous,
|
|
159
|
+
participates(preview) {
|
|
160
|
+
const { subject, mode } = captureIdentity(preview);
|
|
161
|
+
return chosen.has(subject) && chosen.get(subject) === mode;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
72
165
|
|
|
73
166
|
/** design-parity addresses a code subject as `<path>#<function>`. */
|
|
74
167
|
export function codeHandle(preview, { prefix = "catalog" } = {}) {
|
|
@@ -263,7 +356,7 @@ export function declarationMisses(preview) {
|
|
|
263
356
|
* this projection, which is why a FAB size axis read as unauthored while `FabSmall`/`FabMedium`/
|
|
264
357
|
* `FabLarge` sat in the catalog all along.
|
|
265
358
|
*/
|
|
266
|
-
export function variantRendersByComponent(previews) {
|
|
359
|
+
export function variantRendersByComponent(previews, selection = selectCaptures(previews)) {
|
|
267
360
|
const byComponent = new Map();
|
|
268
361
|
for (const preview of previews) {
|
|
269
362
|
const catalog = preview.catalog;
|
|
@@ -272,19 +365,18 @@ export function variantRendersByComponent(previews) {
|
|
|
272
365
|
// An `@OverrideVariant` render is a reseed of the SAME composable, so it keeps the parent's
|
|
273
366
|
// COMPONENT role and is distinguished only by the `_VARIANT_` tag discovery puts in its id.
|
|
274
367
|
// A `@CatalogVariant` render is its own composable, so it carries the VARIANT role and an
|
|
275
|
-
// ordinary
|
|
368
|
+
// ordinary base-capture id.
|
|
276
369
|
//
|
|
277
370
|
// A VARIANT role with a `_VARIANT_` id is the third case, and it used to fall through both
|
|
278
371
|
// tests into the `continue` below: a folded component carrying a matrix of its own. Discovery
|
|
279
372
|
// emitted those renders all along — they were simply never projected, so the kit nodes they
|
|
280
373
|
// sit on went uncompared, and a component could not be folded without deleting its cells.
|
|
281
|
-
// Either way only the
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const isCatalogVariant =
|
|
285
|
-
catalog.role === "VARIANT" &&
|
|
286
|
-
(LIGHT_CAPTURE.test(preview.id) || LIGHT_VARIANT_CAPTURE.test(preview.id));
|
|
374
|
+
// Either way only the selected capture participates — one declaration per variant, in the same
|
|
375
|
+
// mode its component's base reference pairs with.
|
|
376
|
+
const isOverrideVariant = catalog.role === "COMPONENT" && isVariantCapture(preview);
|
|
377
|
+
const isCatalogVariant = catalog.role === "VARIANT";
|
|
287
378
|
if (!isOverrideVariant && !isCatalogVariant) continue;
|
|
379
|
+
if (!selection.participates(preview)) continue;
|
|
288
380
|
|
|
289
381
|
// A variant that names no axis says only "this is different", which is not enough to look
|
|
290
382
|
// anything up in a kit. Dropped rather than guessed at from the function name.
|
|
@@ -309,7 +401,8 @@ export function variantRendersByComponent(previews) {
|
|
|
309
401
|
* fact to report, not a failure.
|
|
310
402
|
*/
|
|
311
403
|
export function projectDesignMap(previews, opts = {}) {
|
|
312
|
-
const
|
|
404
|
+
const selection = selectCaptures(previews);
|
|
405
|
+
const variantRenders = variantRendersByComponent(previews, selection);
|
|
313
406
|
|
|
314
407
|
const components = [];
|
|
315
408
|
const declarations = [];
|
|
@@ -326,7 +419,7 @@ export function projectDesignMap(previews, opts = {}) {
|
|
|
326
419
|
for (const preview of previews) {
|
|
327
420
|
const catalog = preview.catalog;
|
|
328
421
|
if (!catalog || catalog.role !== "COMPONENT") continue;
|
|
329
|
-
if (!
|
|
422
|
+
if (isVariantCapture(preview) || !selection.participates(preview)) continue;
|
|
330
423
|
|
|
331
424
|
if (!catalog.reference) {
|
|
332
425
|
if (catalog.noReference) {
|
|
@@ -372,11 +465,7 @@ export function projectDesignMap(previews, opts = {}) {
|
|
|
372
465
|
// Only the captures that participate: a variant declares once, and reporting its dark capture
|
|
373
466
|
// beside its light one would double every line of a list that exists to be acted on.
|
|
374
467
|
const unplacedDeclarations = previews
|
|
375
|
-
.filter(
|
|
376
|
-
(preview) =>
|
|
377
|
-
preview.catalog &&
|
|
378
|
-
(LIGHT_CAPTURE.test(preview.id) || LIGHT_VARIANT_CAPTURE.test(preview.id)),
|
|
379
|
-
)
|
|
468
|
+
.filter((preview) => preview.catalog && selection.participates(preview))
|
|
380
469
|
.flatMap(declarationMisses);
|
|
381
470
|
|
|
382
471
|
return {
|
|
@@ -386,6 +475,10 @@ export function projectDesignMap(previews, opts = {}) {
|
|
|
386
475
|
unmapped,
|
|
387
476
|
statedAbsent,
|
|
388
477
|
unplacedDeclarations,
|
|
478
|
+
// Composables whose captures name no mode a reference could pair with — several modes, none
|
|
479
|
+
// of them light. Reported rather than guessed at: pairing `Dark` when the kit drew `Coral`
|
|
480
|
+
// diffs a whole palette.
|
|
481
|
+
ambiguousMode: selection.ambiguous,
|
|
389
482
|
variantRenders: declarations.reduce((n, d) => n + d.renders.length, 0),
|
|
390
483
|
withSet: components.filter((c) => c.refSet).length,
|
|
391
484
|
},
|
package/emit-design-map.mjs
CHANGED
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
* `--strict` is the opposite posture, for a catalog whose whole purpose is to reproduce a kit —
|
|
29
29
|
* there, a component with no kit node to compare against does not belong in the published
|
|
30
30
|
* inventory at all, and publishing it means shipping a sticker that can never be checked. It gates
|
|
31
|
-
* on
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* on EVERY kind of absence: a missing `reference`, one explained by `noReference`, and a component
|
|
32
|
+
* whose captures name no mode the reference could pair with. The annotation still earns its keep in
|
|
33
|
+
* the default mode, where the three are reported apart so a retired pattern does not read as
|
|
34
|
+
* neglect; `--strict` simply says there are no exceptions.
|
|
34
35
|
*/
|
|
35
36
|
import fs from "node:fs";
|
|
36
37
|
import path from "node:path";
|
|
@@ -71,11 +72,20 @@ if (STRICT) {
|
|
|
71
72
|
const missing = [
|
|
72
73
|
...diagnostics.unmapped.map((id) => `${id} — no reference, and no reason given`),
|
|
73
74
|
...diagnostics.statedAbsent.map((s) => `${s.componentId} — ${s.reason}`),
|
|
75
|
+
// An ambiguous mode is the third way a component ends up outside the map, and the quietest:
|
|
76
|
+
// the reference is there, but nothing says which capture it pairs with, so the component is
|
|
77
|
+
// simply absent. Under --strict that is as much a gap as a missing reference.
|
|
78
|
+
...diagnostics.ambiguousMode.map(
|
|
79
|
+
(a) =>
|
|
80
|
+
`${a.componentIds.join(", ") || a.subject} — captures ${a.modes
|
|
81
|
+
.map((m) => m || "(unnamed)")
|
|
82
|
+
.join(", ")}, none of them Light, so none pairs with the reference`,
|
|
83
|
+
),
|
|
74
84
|
];
|
|
75
85
|
if (missing.length) {
|
|
76
86
|
console.error(
|
|
77
|
-
`::error::--strict: ${missing.length} component(s)
|
|
78
|
-
`@CatalogComponent(reference = …):`,
|
|
87
|
+
`::error::--strict: ${missing.length} component(s) reach no design reference — no ` +
|
|
88
|
+
`@CatalogComponent(reference = …), or none their captures can pair with:`,
|
|
79
89
|
);
|
|
80
90
|
for (const line of missing) console.error(` - ${line}`);
|
|
81
91
|
console.error(
|
|
@@ -162,6 +172,19 @@ if (diagnostics.unplacedDeclarations?.length) {
|
|
|
162
172
|
}
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
if (diagnostics.ambiguousMode?.length) {
|
|
176
|
+
console.log(
|
|
177
|
+
`\n${diagnostics.ambiguousMode.length} composable(s) publish several capture modes with no ` +
|
|
178
|
+
`Light among them, so which one the reference pairs with is undeclared, and they were ` +
|
|
179
|
+
`skipped. Rendering a single mode makes it the one that pairs; naming one of them "Light" ` +
|
|
180
|
+
`picks it explicitly:`,
|
|
181
|
+
);
|
|
182
|
+
for (const a of diagnostics.ambiguousMode) {
|
|
183
|
+
const who = a.componentIds.join(", ") || a.subject;
|
|
184
|
+
console.log(` - ${who} — ${a.modes.map((m) => m || "(unnamed)").join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
if (diagnostics.unmapped.length) {
|
|
166
189
|
console.log(
|
|
167
190
|
`\n${diagnostics.unmapped.length} component(s) carry neither ` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yschimke/compose-design-map",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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",
|