@yschimke/compose-design-map 1.19.0 → 1.21.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 +23 -0
- package/design-map.mjs +89 -6
- package/emit-design-map.mjs +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,29 @@ Several modes with **no light among them** is the one case that stays unmapped.
|
|
|
137
137
|
guessing which of `Dark` and `Coral` the kit drew, so those components are reported
|
|
138
138
|
(`diagnostics.ambiguousMode`, and a `--strict` failure) rather than paired at random.
|
|
139
139
|
|
|
140
|
+
**A breakpoint fan-out is a size axis, not a mode.** A multipreview that draws one composable at
|
|
141
|
+
several screen sizes — the Wear round breakpoints are the live case — publishes several captures of
|
|
142
|
+
it, told apart by the *same* id segment a themed pair uses. Read as modes they are unresolvable
|
|
143
|
+
(`Light` is nowhere among `wearos_small_round` / `wearos_large_round`), so a full-screen component
|
|
144
|
+
used to drop out of the map entirely the moment it gained a second size.
|
|
145
|
+
|
|
146
|
+
They are told apart by a fact the id does not carry: each capture names a `device`, and the devices
|
|
147
|
+
have **different widths**. A palette does not change the frame's width, so captures whose modes map
|
|
148
|
+
one-to-one onto distinct device widths are a size axis and one of them can be picked on the merits:
|
|
149
|
+
|
|
150
|
+
- the **narrowest** is the base by default, because that is the size a kit draws — a kit publishes
|
|
151
|
+
its screen artwork at one size and leaves adaptation to the implementation, and the narrowest is
|
|
152
|
+
the one every larger screen is an adaptation *of*;
|
|
153
|
+
- `--base-breakpoint <dp>` moves it, for a kit that draws somewhere else. A named base a given
|
|
154
|
+
composable does not render falls back to the narrowest rather than dropping it — rendering a
|
|
155
|
+
subset of the catalog's breakpoints is a legitimate thing for one screen to do;
|
|
156
|
+
- the sizes the base did not take **fold under it as cells**, seeded `breakpoint=<dp>` and named
|
|
157
|
+
`<dp>dp`, so they are published rather than discarded.
|
|
158
|
+
|
|
159
|
+
Two captures of the *same* width are still a mode, whatever devices they name: nothing orders them,
|
|
160
|
+
so they stay `ambiguousMode`. An `@OverrideVariant` cell rides the base breakpoint only — the
|
|
161
|
+
product of both axes would multiply the sheet by every size, and the base carries the matrix.
|
|
162
|
+
|
|
140
163
|
**`overrides.props` beats `overrides.seeds` where both exist.** They are not the same list. `seeds`
|
|
141
164
|
holds only the values that differ from the composable's defaults; `props` — emitted for a
|
|
142
165
|
`@PreviewAxis` cross product — carries the full axis assignment, defaults included. A cell that
|
package/design-map.mjs
CHANGED
|
@@ -113,9 +113,51 @@ export function captureIdentity(preview) {
|
|
|
113
113
|
* guessing which of `Dark` and `Coral` the kit drew, and pairing the wrong one diffs a whole
|
|
114
114
|
* palette — so it is reported instead (`diagnostics.ambiguousMode`).
|
|
115
115
|
*/
|
|
116
|
-
function preferredMode(modes) {
|
|
116
|
+
function preferredMode(modes, widthByMode, baseBreakpointDp) {
|
|
117
117
|
if (modes.has(LIGHT_MODE)) return LIGHT_MODE;
|
|
118
|
-
|
|
118
|
+
if (modes.size === 1) return [...modes][0];
|
|
119
|
+
|
|
120
|
+
// A BREAKPOINT FAN-OUT is not an ambiguous mode, and telling them apart is what this arm is for.
|
|
121
|
+
//
|
|
122
|
+
// A multipreview that renders one function at several screen sizes produces several captures of
|
|
123
|
+
// one composable, exactly like a themed pair does — and the id segment they are told apart by is
|
|
124
|
+
// the same segment. Read as modes they are unresolvable (`Light` is nowhere among
|
|
125
|
+
// `wearos_small_round` / `wearos_large_round`), so every full-screen component of a Wear catalog
|
|
126
|
+
// dropped out of the map the moment it gained a second size, and `--strict` failed on it.
|
|
127
|
+
//
|
|
128
|
+
// They are distinguishable by a fact the id does not carry: each capture names a `device`, and
|
|
129
|
+
// the devices have DIFFERENT WIDTHS. A palette does not change the frame's width, so a set of
|
|
130
|
+
// captures whose modes map one-to-one onto distinct device widths is a size axis rather than a
|
|
131
|
+
// colour one, and one of them can be picked on the merits.
|
|
132
|
+
const sized = [...modes].map((mode) => [mode, widthByMode.get(mode)]);
|
|
133
|
+
const widths = sized.map(([, width]) => width);
|
|
134
|
+
if (!widths.every((width) => Number.isFinite(width))) return null;
|
|
135
|
+
if (new Set(widths).size !== widths.length) return null;
|
|
136
|
+
|
|
137
|
+
// NARROWEST by default, because that is the size a kit draws: a design kit publishes its screen
|
|
138
|
+
// artwork at one size and leaves adaptation to the implementation, and the narrowest is the one
|
|
139
|
+
// every larger screen is an adaptation OF. `baseBreakpointDp` overrides it for a kit that draws
|
|
140
|
+
// somewhere else. A named base this composable does not render falls back to the narrowest
|
|
141
|
+
// rather than dropping the component: rendering a subset of the catalog's breakpoints is a
|
|
142
|
+
// legitimate thing for one screen to do, and it is not a reason to publish no map row for it.
|
|
143
|
+
sized.sort((a, b) => a[1] - b[1]);
|
|
144
|
+
const named = Number.isFinite(baseBreakpointDp)
|
|
145
|
+
? sized.find(([, width]) => width === baseBreakpointDp)
|
|
146
|
+
: null;
|
|
147
|
+
return (named ?? sized[0])[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The device width a capture was drawn at, or `null` when it names no device.
|
|
152
|
+
*
|
|
153
|
+
* Read from `params.device` rather than `params.widthDp` alone: a device-less preview carries no
|
|
154
|
+
* width at all, and a `wrapSandbox` bound is not a screen size. Only a capture that names a device
|
|
155
|
+
* is claiming to be a picture of a screen.
|
|
156
|
+
*/
|
|
157
|
+
function deviceWidthDp(preview) {
|
|
158
|
+
const params = preview?.params;
|
|
159
|
+
if (!params?.device) return null;
|
|
160
|
+
return Number.isFinite(params.widthDp) ? params.widthDp : null;
|
|
119
161
|
}
|
|
120
162
|
|
|
121
163
|
/**
|
|
@@ -124,9 +166,10 @@ function preferredMode(modes) {
|
|
|
124
166
|
*
|
|
125
167
|
* @returns {{participates: (preview: object) => boolean, ambiguous: Array<object>}}
|
|
126
168
|
*/
|
|
127
|
-
export function selectCaptures(previews) {
|
|
169
|
+
export function selectCaptures(previews, { baseBreakpointDp } = {}) {
|
|
128
170
|
const modesBySubject = new Map();
|
|
129
171
|
const componentsBySubject = new Map();
|
|
172
|
+
const widthsBySubject = new Map();
|
|
130
173
|
for (const preview of previews) {
|
|
131
174
|
if (!preview?.catalog) continue;
|
|
132
175
|
const { subject, mode } = captureIdentity(preview);
|
|
@@ -136,12 +179,18 @@ export function selectCaptures(previews) {
|
|
|
136
179
|
const ids = componentsBySubject.get(subject) ?? new Set();
|
|
137
180
|
if (preview.catalog.componentId) ids.add(preview.catalog.componentId);
|
|
138
181
|
componentsBySubject.set(subject, ids);
|
|
182
|
+
const widths = widthsBySubject.get(subject) ?? new Map();
|
|
183
|
+
// A VARIANT capture rides the same device as its base, so it agrees rather than conflicts —
|
|
184
|
+
// but read the base's width first, since that is the one the fan-out is defined by.
|
|
185
|
+
if (!widths.has(mode)) widths.set(mode, deviceWidthDp(preview));
|
|
186
|
+
widthsBySubject.set(subject, widths);
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
const chosen = new Map();
|
|
142
190
|
const ambiguous = [];
|
|
143
191
|
for (const [subject, modes] of modesBySubject) {
|
|
144
|
-
const
|
|
192
|
+
const widths = widthsBySubject.get(subject) ?? new Map();
|
|
193
|
+
const mode = preferredMode(modes, widths, baseBreakpointDp);
|
|
145
194
|
if (mode === null) {
|
|
146
195
|
ambiguous.push({
|
|
147
196
|
subject,
|
|
@@ -160,6 +209,22 @@ export function selectCaptures(previews) {
|
|
|
160
209
|
const { subject, mode } = captureIdentity(preview);
|
|
161
210
|
return chosen.has(subject) && chosen.get(subject) === mode;
|
|
162
211
|
},
|
|
212
|
+
/**
|
|
213
|
+
* The device width of a capture that is a NON-BASE breakpoint of a fan-out, or `null`.
|
|
214
|
+
*
|
|
215
|
+
* This is what turns the sizes the base did not take into cells rather than into silence. A
|
|
216
|
+
* capture qualifies only when its subject resolved to some other mode and both that mode and
|
|
217
|
+
* this one name a device width — so a `Dark` capture standing beside a chosen `Light` one,
|
|
218
|
+
* which is a mode and not a size, is never mistaken for a breakpoint.
|
|
219
|
+
*/
|
|
220
|
+
breakpointOf(preview) {
|
|
221
|
+
const { subject, mode } = captureIdentity(preview);
|
|
222
|
+
if (!chosen.has(subject) || chosen.get(subject) === mode) return null;
|
|
223
|
+
const widths = widthsBySubject.get(subject);
|
|
224
|
+
const base = widths?.get(chosen.get(subject));
|
|
225
|
+
const here = widths?.get(mode);
|
|
226
|
+
return Number.isFinite(base) && Number.isFinite(here) ? here : null;
|
|
227
|
+
},
|
|
163
228
|
};
|
|
164
229
|
}
|
|
165
230
|
|
|
@@ -375,7 +440,25 @@ export function variantRendersByComponent(previews, selection = selectCaptures(p
|
|
|
375
440
|
// mode its component's base reference pairs with.
|
|
376
441
|
const isOverrideVariant = catalog.role === "COMPONENT" && isVariantCapture(preview);
|
|
377
442
|
const isCatalogVariant = catalog.role === "VARIANT";
|
|
378
|
-
|
|
443
|
+
|
|
444
|
+
// A BREAKPOINT capture is the third form, and it is not an annotation at all — it is the same
|
|
445
|
+
// composable drawn on a wider screen by a multipreview. It folds under its component like any
|
|
446
|
+
// other cell, seeded with the width it was drawn at, so the sizes the base did not take are
|
|
447
|
+
// published rather than discarded.
|
|
448
|
+
//
|
|
449
|
+
// Taken BEFORE the `participates` gate, because a non-base breakpoint is by definition the
|
|
450
|
+
// capture that did not participate. An `@OverrideVariant` cell of a non-base breakpoint is
|
|
451
|
+
// skipped, though — that is the product of two axes and would multiply the sheet by every
|
|
452
|
+
// size; the base breakpoint carries the component's matrix.
|
|
453
|
+
if (!isOverrideVariant && !isCatalogVariant) {
|
|
454
|
+
const widthDp = catalog.role === "COMPONENT" ? selection.breakpointOf(preview) : null;
|
|
455
|
+
if (widthDp === null) continue;
|
|
456
|
+
const seeds = [{ key: "breakpoint", raw: String(widthDp) }];
|
|
457
|
+
const list = byComponent.get(catalog.componentId) ?? [];
|
|
458
|
+
list.push({ previewId: preview.id, name: `${widthDp}dp`, seeds });
|
|
459
|
+
byComponent.set(catalog.componentId, list);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
379
462
|
if (!selection.participates(preview)) continue;
|
|
380
463
|
|
|
381
464
|
// A variant that names no axis says only "this is different", which is not enough to look
|
|
@@ -401,7 +484,7 @@ export function variantRendersByComponent(previews, selection = selectCaptures(p
|
|
|
401
484
|
* fact to report, not a failure.
|
|
402
485
|
*/
|
|
403
486
|
export function projectDesignMap(previews, opts = {}) {
|
|
404
|
-
const selection = selectCaptures(previews);
|
|
487
|
+
const selection = selectCaptures(previews, { baseBreakpointDp: opts.baseBreakpointDp });
|
|
405
488
|
const variantRenders = variantRendersByComponent(previews, selection);
|
|
406
489
|
|
|
407
490
|
const components = [];
|
package/emit-design-map.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* npx @yschimke/compose-design-map [--previews <path>] [--out design-map.json]
|
|
6
6
|
* [--variants design-map-variants.json] [--prefix catalog]
|
|
7
7
|
* [--check] [--strict] [--allow-stated-absence]
|
|
8
|
+
* [--base-breakpoint <dp>]
|
|
8
9
|
*
|
|
9
10
|
* Run `./gradlew :<module>:composePreviewDiscover` first so the manifest exists.
|
|
10
11
|
*
|
|
@@ -60,6 +61,15 @@ const PREFIX = arg("prefix", "catalog");
|
|
|
60
61
|
const CHECK = process.argv.includes("--check");
|
|
61
62
|
const STRICT = process.argv.includes("--strict");
|
|
62
63
|
const ALLOW_STATED_ABSENCE = process.argv.includes("--allow-stated-absence");
|
|
64
|
+
/**
|
|
65
|
+
* The screen width, in dp, whose capture is the BASE of a breakpoint fan-out.
|
|
66
|
+
*
|
|
67
|
+
* A multipreview that draws one composable at several screen sizes publishes several captures of
|
|
68
|
+
* it, and exactly one can carry the component's design reference — the rest fold under it as size
|
|
69
|
+
* cells. Absent this flag the narrowest wins, which is the size a kit usually draws. Pass it when
|
|
70
|
+
* the kit draws somewhere else, so the base capture and the base reference are the same width.
|
|
71
|
+
*/
|
|
72
|
+
const BASE_BREAKPOINT = Number(arg("base-breakpoint", ""));
|
|
63
73
|
|
|
64
74
|
if (!fs.existsSync(PREVIEWS)) {
|
|
65
75
|
console.error(
|
|
@@ -72,6 +82,9 @@ if (!fs.existsSync(PREVIEWS)) {
|
|
|
72
82
|
const manifest = JSON.parse(fs.readFileSync(PREVIEWS, "utf8"));
|
|
73
83
|
const { map, variants, diagnostics } = projectDesignMap(manifest.previews ?? [], {
|
|
74
84
|
prefix: PREFIX,
|
|
85
|
+
...(Number.isFinite(BASE_BREAKPOINT) && BASE_BREAKPOINT > 0
|
|
86
|
+
? { baseBreakpointDp: BASE_BREAKPOINT }
|
|
87
|
+
: {}),
|
|
75
88
|
});
|
|
76
89
|
|
|
77
90
|
// Gate BEFORE writing, not after. A run that fails should leave the committed map intact rather
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yschimke/compose-design-map",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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",
|