@smallpen/core 0.1.0-alpha.1
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/package.json +18 -0
- package/src/canonical.mjs +64 -0
- package/src/capabilities.mjs +495 -0
- package/src/catalog.mjs +362 -0
- package/src/component-samples.mjs +84 -0
- package/src/components-domain.mjs +335 -0
- package/src/contexts.mjs +248 -0
- package/src/design-projection.mjs +657 -0
- package/src/design-read.mjs +825 -0
- package/src/design-system-authoring.mjs +102 -0
- package/src/design-system-canvas.mjs +862 -0
- package/src/design-system.mjs +437 -0
- package/src/design-validation.mjs +324 -0
- package/src/draft.mjs +784 -0
- package/src/effective-tokens.mjs +377 -0
- package/src/errors.mjs +12 -0
- package/src/index.mjs +112 -0
- package/src/initialization.mjs +310 -0
- package/src/package.mjs +5935 -0
- package/src/projection-values.mjs +138 -0
- package/src/requirements-domain.mjs +222 -0
- package/src/scenarios-domain.mjs +268 -0
- package/src/token-advice.mjs +272 -0
- package/src/token-import.mjs +607 -0
- package/src/tokens-domain.mjs +410 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { findComponentVariant } from "./components-domain.mjs";
|
|
2
|
+
import { resolveContext } from "./contexts.mjs";
|
|
3
|
+
import { fail } from "./errors.mjs";
|
|
4
|
+
import { applyEffectiveTokenBindings } from "./projection-values.mjs";
|
|
5
|
+
|
|
6
|
+
function screenById(snapshot, screenId) {
|
|
7
|
+
const entry = snapshot.manifest.entries.screens.find(
|
|
8
|
+
(candidate) => snapshot.entries[candidate].id === screenId,
|
|
9
|
+
);
|
|
10
|
+
if (!entry) fail("missing_screen", `Screen not found: ${screenId}`, { screenId });
|
|
11
|
+
return { entry, screen: snapshot.entries[entry] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function replacementFor(product, reference) {
|
|
15
|
+
const replacements = [...product.domain.componentSets.values()].filter(
|
|
16
|
+
(componentSet) =>
|
|
17
|
+
componentSet.replaces?.packageId === reference.packageId &&
|
|
18
|
+
componentSet.replaces.assetId === reference.assetId,
|
|
19
|
+
);
|
|
20
|
+
if (replacements.length > 1) {
|
|
21
|
+
fail(
|
|
22
|
+
"duplicate_product_component_replacement",
|
|
23
|
+
`More than one Product Component replaces ${reference.assetId}`,
|
|
24
|
+
{ componentIds: replacements.map(({ id }) => id), reference },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return replacements[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function componentTarget(product, foundation, libraries, reference) {
|
|
31
|
+
const replacement = replacementFor(product, reference);
|
|
32
|
+
if (replacement) return { componentSet: replacement, owner: product };
|
|
33
|
+
if (reference.packageId === product.manifest.packageId) {
|
|
34
|
+
const componentSet = product.domain.componentSets.get(reference.assetId);
|
|
35
|
+
return componentSet ? { componentSet, owner: product } : null;
|
|
36
|
+
}
|
|
37
|
+
if (reference.packageId === foundation?.manifest.packageId) {
|
|
38
|
+
const componentSet = foundation.domain.componentSets.get(reference.assetId);
|
|
39
|
+
if (componentSet?.visibility === "public") {
|
|
40
|
+
return { componentSet, owner: foundation };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const library = libraries.find(
|
|
44
|
+
(candidate) => candidate.manifest.packageId === reference.packageId,
|
|
45
|
+
);
|
|
46
|
+
const componentSet = library?.domain.componentSets.get(reference.assetId);
|
|
47
|
+
if (componentSet?.visibility === "public") {
|
|
48
|
+
return { componentSet, owner: library };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function qualifyReference(reference, owner, product) {
|
|
54
|
+
if (
|
|
55
|
+
typeof reference !== "string" ||
|
|
56
|
+
owner.manifest.packageId === product.manifest.packageId
|
|
57
|
+
) {
|
|
58
|
+
return reference;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
assetId: reference,
|
|
62
|
+
packageId: owner.manifest.packageId,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function qualifyPaintReferences(paints, owner, product) {
|
|
67
|
+
return paints?.map((paint) => ({
|
|
68
|
+
...paint,
|
|
69
|
+
...(paint.colorRef
|
|
70
|
+
? { colorRef: qualifyReference(paint.colorRef, owner, product) }
|
|
71
|
+
: {}),
|
|
72
|
+
...(paint.mediaRef
|
|
73
|
+
? { mediaRef: qualifyReference(paint.mediaRef, owner, product) }
|
|
74
|
+
: {}),
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function qualifyTextStyle(style, owner, product) {
|
|
79
|
+
if (!style?.typographyRef) return style;
|
|
80
|
+
return {
|
|
81
|
+
...style,
|
|
82
|
+
typographyRef: qualifyReference(style.typographyRef, owner, product),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function qualifyComponentAssetReferences(node, owner, product) {
|
|
87
|
+
if (owner.manifest.packageId === product.manifest.packageId) return node;
|
|
88
|
+
return {
|
|
89
|
+
...node,
|
|
90
|
+
...(node.mediaRef
|
|
91
|
+
? { mediaRef: qualifyReference(node.mediaRef, owner, product) }
|
|
92
|
+
: {}),
|
|
93
|
+
...(node.fills
|
|
94
|
+
? { fills: qualifyPaintReferences(node.fills, owner, product) }
|
|
95
|
+
: {}),
|
|
96
|
+
...(node.strokes
|
|
97
|
+
? { strokes: qualifyPaintReferences(node.strokes, owner, product) }
|
|
98
|
+
: {}),
|
|
99
|
+
...(node.textStyle
|
|
100
|
+
? { textStyle: qualifyTextStyle(node.textStyle, owner, product) }
|
|
101
|
+
: {}),
|
|
102
|
+
...(node.textBlocks
|
|
103
|
+
? {
|
|
104
|
+
textBlocks: node.textBlocks.map((block) => ({
|
|
105
|
+
...block,
|
|
106
|
+
...(block.fills
|
|
107
|
+
? { fills: qualifyPaintReferences(block.fills, owner, product) }
|
|
108
|
+
: {}),
|
|
109
|
+
...(block.textStyle
|
|
110
|
+
? { textStyle: qualifyTextStyle(block.textStyle, owner, product) }
|
|
111
|
+
: {}),
|
|
112
|
+
runs: (block.runs ?? []).map((run) => ({
|
|
113
|
+
...run,
|
|
114
|
+
...(run.fills
|
|
115
|
+
? { fills: qualifyPaintReferences(run.fills, owner, product) }
|
|
116
|
+
: {}),
|
|
117
|
+
...(run.textStyle
|
|
118
|
+
? { textStyle: qualifyTextStyle(run.textStyle, owner, product) }
|
|
119
|
+
: {}),
|
|
120
|
+
})),
|
|
121
|
+
})),
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function prefixedNodeId(instanceId, sourceNodeId) {
|
|
128
|
+
return `${instanceId}__${sourceNodeId}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyInstanceOverrides(nodes, instance) {
|
|
132
|
+
for (const [overridePath, value] of Object.entries(
|
|
133
|
+
instance.instance.overrides ?? {},
|
|
134
|
+
)) {
|
|
135
|
+
const separator = overridePath.indexOf(":");
|
|
136
|
+
if (separator <= 0 || separator === overridePath.length - 1) {
|
|
137
|
+
fail(
|
|
138
|
+
"invalid_component_override_path",
|
|
139
|
+
`Component override path is invalid: ${overridePath}`,
|
|
140
|
+
{ instanceId: instance.id, overridePath },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const sourceNodeId = overridePath.slice(0, separator);
|
|
144
|
+
const field = overridePath.slice(separator + 1);
|
|
145
|
+
const nodeId =
|
|
146
|
+
sourceNodeId === instance.sourceNodeId
|
|
147
|
+
? instance.id
|
|
148
|
+
: prefixedNodeId(instance.id, sourceNodeId);
|
|
149
|
+
const node = nodes[nodeId];
|
|
150
|
+
if (!node) {
|
|
151
|
+
fail(
|
|
152
|
+
"missing_component_override_target",
|
|
153
|
+
`Component override target is missing: ${overridePath}`,
|
|
154
|
+
{ instanceId: instance.id, overridePath },
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (!new Set(["fills", "name", "opacity", "text", "visible"]).has(field)) {
|
|
158
|
+
fail(
|
|
159
|
+
"unsupported_component_override",
|
|
160
|
+
`Component override field is unsupported: ${field}`,
|
|
161
|
+
{ field, instanceId: instance.id, overridePath },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (field === "text" && node.type !== "TEXT") {
|
|
165
|
+
fail(
|
|
166
|
+
"component_override_type_mismatch",
|
|
167
|
+
`Text override target is not TEXT: ${sourceNodeId}`,
|
|
168
|
+
{ instanceId: instance.id, overridePath },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
node[field] = structuredClone(value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function instantiateComponent(instanceValue, product, options, stack) {
|
|
176
|
+
const instance = structuredClone(instanceValue);
|
|
177
|
+
const target = componentTarget(
|
|
178
|
+
product,
|
|
179
|
+
options.foundation,
|
|
180
|
+
options.libraries ?? [],
|
|
181
|
+
instance.instance.component,
|
|
182
|
+
);
|
|
183
|
+
if (!target) {
|
|
184
|
+
fail(
|
|
185
|
+
"missing_component",
|
|
186
|
+
`Component not found: ${instance.instance.component.assetId}`,
|
|
187
|
+
{ instanceId: instance.id, reference: instance.instance.component },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (stack.has(target.componentSet.id)) {
|
|
191
|
+
fail(
|
|
192
|
+
"component_instance_cycle",
|
|
193
|
+
`Component instance cycle includes ${target.componentSet.id}`,
|
|
194
|
+
{ componentId: target.componentSet.id },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const match = findComponentVariant(
|
|
198
|
+
target.componentSet,
|
|
199
|
+
instance.instance.variant,
|
|
200
|
+
{ allowPreviewFallback: options.allowPreviewFallback },
|
|
201
|
+
);
|
|
202
|
+
if (!match.variant) {
|
|
203
|
+
fail(
|
|
204
|
+
"missing_variant",
|
|
205
|
+
`No exact variant exists for ${target.componentSet.id}`,
|
|
206
|
+
{
|
|
207
|
+
componentId: target.componentSet.id,
|
|
208
|
+
instanceId: instance.id,
|
|
209
|
+
selection: instance.instance.variant,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
stack.add(target.componentSet.id);
|
|
214
|
+
const result = {};
|
|
215
|
+
const sourceNodes = match.variant.nodes;
|
|
216
|
+
let nestedFallbackUsed = false;
|
|
217
|
+
const visit = (sourceId) => {
|
|
218
|
+
const source = qualifyComponentAssetReferences(
|
|
219
|
+
applyEffectiveTokenBindings(sourceNodes[sourceId], product, {
|
|
220
|
+
context: options.context,
|
|
221
|
+
foundation: options.foundation,
|
|
222
|
+
libraries: options.libraries,
|
|
223
|
+
}),
|
|
224
|
+
target.owner,
|
|
225
|
+
product,
|
|
226
|
+
);
|
|
227
|
+
const derivedId = sourceId === match.variant.rootId
|
|
228
|
+
? instance.id
|
|
229
|
+
: prefixedNodeId(instance.id, sourceId);
|
|
230
|
+
if (source.instance) {
|
|
231
|
+
const nested = instantiateComponent(
|
|
232
|
+
{ ...source, id: derivedId },
|
|
233
|
+
product,
|
|
234
|
+
options,
|
|
235
|
+
stack,
|
|
236
|
+
);
|
|
237
|
+
Object.assign(result, nested.nodes);
|
|
238
|
+
nestedFallbackUsed ||= nested.fallbackUsed;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const children = source.children.map((childId) =>
|
|
242
|
+
childId === match.variant.rootId
|
|
243
|
+
? instance.id
|
|
244
|
+
: prefixedNodeId(instance.id, childId),
|
|
245
|
+
);
|
|
246
|
+
result[derivedId] = {
|
|
247
|
+
...source,
|
|
248
|
+
...(sourceId === match.variant.rootId ? instance : {}),
|
|
249
|
+
children,
|
|
250
|
+
componentRef: structuredClone(instance.instance.component),
|
|
251
|
+
id: derivedId,
|
|
252
|
+
sourceNodeId: sourceId,
|
|
253
|
+
variantId: match.variant.id,
|
|
254
|
+
variantSelection: structuredClone(match.variant.selection),
|
|
255
|
+
...(sourceId === match.variant.rootId
|
|
256
|
+
? {
|
|
257
|
+
height: instance.height,
|
|
258
|
+
name: instance.name,
|
|
259
|
+
type: "INSTANCE",
|
|
260
|
+
width: instance.width,
|
|
261
|
+
x: instance.x,
|
|
262
|
+
y: instance.y,
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
};
|
|
266
|
+
delete result[derivedId].instance;
|
|
267
|
+
for (const childId of source.children) visit(childId);
|
|
268
|
+
};
|
|
269
|
+
visit(match.variant.rootId);
|
|
270
|
+
const root = result[instance.id];
|
|
271
|
+
root.sourceNodeId = match.variant.rootId;
|
|
272
|
+
root.componentId = target.componentSet.id;
|
|
273
|
+
root.componentOwnerPackageId = target.owner.manifest.packageId;
|
|
274
|
+
applyInstanceOverrides(result, {
|
|
275
|
+
...instance,
|
|
276
|
+
sourceNodeId: match.variant.rootId,
|
|
277
|
+
});
|
|
278
|
+
stack.delete(target.componentSet.id);
|
|
279
|
+
return {
|
|
280
|
+
fallbackUsed: match.fallbackUsed || nestedFallbackUsed,
|
|
281
|
+
nodes: result,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function projectNodes(sourceNodes, product, options) {
|
|
286
|
+
const nodes = {};
|
|
287
|
+
let fallbackUsed = false;
|
|
288
|
+
for (const node of Object.values(sourceNodes)) {
|
|
289
|
+
if (node.instance) {
|
|
290
|
+
const instance = instantiateComponent(node, product, options, new Set());
|
|
291
|
+
Object.assign(nodes, instance.nodes);
|
|
292
|
+
fallbackUsed ||= instance.fallbackUsed;
|
|
293
|
+
} else {
|
|
294
|
+
nodes[node.id] = applyEffectiveTokenBindings(node, product, {
|
|
295
|
+
context: options.context,
|
|
296
|
+
foundation: options.foundation,
|
|
297
|
+
libraries: options.libraries,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const node of Object.values(nodes)) {
|
|
302
|
+
node.children = (node.children ?? []).flatMap((childId) =>
|
|
303
|
+
sourceNodes[childId]?.instance ? [childId] : [childId],
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
applyLocatedCopyInheritance(nodes, product, options);
|
|
307
|
+
return { fallbackUsed, nodes };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const COPY_TOUCHED_FIELDS = new Map([
|
|
311
|
+
["blur-group", ["backgroundBlur", "blur"]],
|
|
312
|
+
["content-group", ["text", "textBlocks"]],
|
|
313
|
+
["fill-group", ["fills"]],
|
|
314
|
+
["geometry-group", ["height", "rotation", "width"]],
|
|
315
|
+
["mask-group", ["masked-group", "show-content"]],
|
|
316
|
+
["radius-group", ["cornerRadius"]],
|
|
317
|
+
["shadow-group", ["shadow"]],
|
|
318
|
+
["stroke-group", ["strokes"]],
|
|
319
|
+
["text-display-group", ["textStyle"]],
|
|
320
|
+
["text-font-group", ["textStyle"]],
|
|
321
|
+
["visibility-group", ["visible"]],
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
// Untouched located component copies re-read the master's shared fields on
|
|
325
|
+
// every projection so later master edits propagate (CMP-005). Fields in the
|
|
326
|
+
// copy's `touched` groups keep the copy's own values.
|
|
327
|
+
function applyLocatedCopyInheritance(nodes, product, options) {
|
|
328
|
+
const mastersBySourceNodeId = new Map();
|
|
329
|
+
const resolveSource = (node) =>
|
|
330
|
+
// Masters inherit from their token-evaluated values, not raw Canonical
|
|
331
|
+
// fields, so a copy follows the same resolved color as its master.
|
|
332
|
+
applyEffectiveTokenBindings(node, product, {
|
|
333
|
+
context: options?.context,
|
|
334
|
+
foundation: options?.foundation,
|
|
335
|
+
libraries: options?.libraries,
|
|
336
|
+
});
|
|
337
|
+
for (const component of product.domain.locatedComponents.values()) {
|
|
338
|
+
const entry = product.manifest.entries.screens.find(
|
|
339
|
+
(candidate) => product.entries[candidate].id === component.screenId,
|
|
340
|
+
);
|
|
341
|
+
const presentation = entry
|
|
342
|
+
? product.entries[entry].presentations.find(
|
|
343
|
+
({ id }) => id === component.presentationId,
|
|
344
|
+
)
|
|
345
|
+
: undefined;
|
|
346
|
+
if (!presentation) continue;
|
|
347
|
+
const masterNode = presentation.nodes[component.mainNodeId];
|
|
348
|
+
if (!masterNode) continue;
|
|
349
|
+
mastersBySourceNodeId.set(component.mainNodeId, resolveSource(masterNode));
|
|
350
|
+
const visit = (nodeId) => {
|
|
351
|
+
const node = presentation.nodes[nodeId];
|
|
352
|
+
if (!node) return;
|
|
353
|
+
if (nodeId !== component.mainNodeId) {
|
|
354
|
+
mastersBySourceNodeId.set(nodeId, resolveSource(node));
|
|
355
|
+
}
|
|
356
|
+
for (const childId of node.children ?? []) visit(childId);
|
|
357
|
+
};
|
|
358
|
+
visit(component.mainNodeId);
|
|
359
|
+
}
|
|
360
|
+
if (mastersBySourceNodeId.size === 0) return;
|
|
361
|
+
for (const node of Object.values(nodes)) {
|
|
362
|
+
if (node.sourceNodeId === undefined) continue;
|
|
363
|
+
// Instance expansions also carry sourceNodeId; only located copies inherit.
|
|
364
|
+
if (node.componentRef !== undefined || node.variantSelection !== undefined) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const master = mastersBySourceNodeId.get(node.sourceNodeId);
|
|
368
|
+
if (!master) continue;
|
|
369
|
+
const touchedGroups = new Set(node.touched ?? []);
|
|
370
|
+
// Identity fields never inherit: a located copy stays its own INSTANCE
|
|
371
|
+
// node with its own placement and master link (RV-001-A).
|
|
372
|
+
const ownFields = new Set([
|
|
373
|
+
"children",
|
|
374
|
+
"componentId",
|
|
375
|
+
"id",
|
|
376
|
+
"name",
|
|
377
|
+
"sourceNodeId",
|
|
378
|
+
"touched",
|
|
379
|
+
"type",
|
|
380
|
+
"x",
|
|
381
|
+
"y",
|
|
382
|
+
]);
|
|
383
|
+
for (const [group, fields] of COPY_TOUCHED_FIELDS) {
|
|
384
|
+
if (touchedGroups.has(group)) {
|
|
385
|
+
for (const field of fields) ownFields.add(field);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (touchedGroups.has("modifiable-group")) ownFields.add("name");
|
|
389
|
+
for (const field of Object.keys(master)) {
|
|
390
|
+
if (ownFields.has(field)) continue;
|
|
391
|
+
if (master[field] === undefined) continue;
|
|
392
|
+
node[field] = structuredClone(master[field]);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function layoutPaddingOf(node) {
|
|
398
|
+
const padding = node["layout-padding"];
|
|
399
|
+
if (!padding || typeof padding !== "object") {
|
|
400
|
+
return { bottom: 0, left: 0, right: 0, top: 0 };
|
|
401
|
+
}
|
|
402
|
+
const number = (value) => (Number.isFinite(Number(value)) ? Number(value) : 0);
|
|
403
|
+
return {
|
|
404
|
+
bottom: number(padding.p3),
|
|
405
|
+
left: number(padding.p4),
|
|
406
|
+
right: number(padding.p2),
|
|
407
|
+
top: number(padding.p1),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function layoutMainGapOf(node) {
|
|
412
|
+
const gap = node["layout-gap"];
|
|
413
|
+
if (!gap || typeof gap !== "object") return 0;
|
|
414
|
+
const rowGap = Number.isFinite(Number(gap.rowGap)) ? Number(gap.rowGap) : 0;
|
|
415
|
+
const columnGap = Number.isFinite(Number(gap.columnGap)) ? Number(gap.columnGap) : 0;
|
|
416
|
+
return node["layout-flex-dir"] === "column" ? rowGap : columnGap;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Deterministic flex reflow for projected nodes: gap/padding/direction drive
|
|
420
|
+
// child positions (relative to the parent origin) so layout changes are
|
|
421
|
+
// visible in pixels and semantic bounds.
|
|
422
|
+
function reflowFlexLayout(nodes, nodeId) {
|
|
423
|
+
const node = nodes[nodeId];
|
|
424
|
+
if (!node) return;
|
|
425
|
+
for (const childId of node.children ?? []) reflowFlexLayout(nodes, childId);
|
|
426
|
+
if (node.layout !== "flex") return;
|
|
427
|
+
const items = (node.children ?? [])
|
|
428
|
+
.map((childId) => nodes[childId])
|
|
429
|
+
.filter((child) => child && child["layout-item-absolute"] !== true);
|
|
430
|
+
if (items.length === 0) return;
|
|
431
|
+
const column = node["layout-flex-dir"] === "column";
|
|
432
|
+
const padding = layoutPaddingOf(node);
|
|
433
|
+
const gap = layoutMainGapOf(node);
|
|
434
|
+
const innerWidth = Math.max(0, node.width - padding.left - padding.right);
|
|
435
|
+
const innerHeight = Math.max(0, node.height - padding.top - padding.bottom);
|
|
436
|
+
const mainSize = (child) => (column ? child.height : child.width);
|
|
437
|
+
const crossSize = (child) => (column ? child.width : child.height);
|
|
438
|
+
const fillMainField = column ? "layout-item-v-sizing" : "layout-item-h-sizing";
|
|
439
|
+
const fillCrossField = column ? "layout-item-h-sizing" : "layout-item-v-sizing";
|
|
440
|
+
const fillItems = items.filter((child) => child[fillMainField] === "fill");
|
|
441
|
+
if (fillItems.length > 0) {
|
|
442
|
+
const fixedTotal = items
|
|
443
|
+
.filter((child) => child[fillMainField] !== "fill")
|
|
444
|
+
.reduce((sum, child) => sum + mainSize(child), 0);
|
|
445
|
+
const availableMain =
|
|
446
|
+
(column ? innerHeight : innerWidth) - gap * (items.length - 1);
|
|
447
|
+
const fillEach = Math.max(
|
|
448
|
+
0,
|
|
449
|
+
(availableMain - fixedTotal) / fillItems.length,
|
|
450
|
+
);
|
|
451
|
+
for (const child of fillItems) {
|
|
452
|
+
if (column) child.height = fillEach;
|
|
453
|
+
else child.width = fillEach;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const child of items) {
|
|
457
|
+
if (child[fillCrossField] !== "fill") continue;
|
|
458
|
+
if (column) child.width = innerWidth;
|
|
459
|
+
else child.height = innerHeight;
|
|
460
|
+
}
|
|
461
|
+
const contentMain =
|
|
462
|
+
items.reduce((sum, child) => sum + mainSize(child), 0) +
|
|
463
|
+
gap * (items.length - 1);
|
|
464
|
+
const justify = node["layout-justify-content"];
|
|
465
|
+
const free = Math.max(0, (column ? innerHeight : innerWidth) - contentMain);
|
|
466
|
+
const spaceBetween =
|
|
467
|
+
justify === "space-between" && items.length > 1 ? free / (items.length - 1) : 0;
|
|
468
|
+
let cursor = column ? padding.top : padding.left;
|
|
469
|
+
if (justify === "center") cursor += free / 2;
|
|
470
|
+
else if (justify === "end") cursor += free;
|
|
471
|
+
for (const child of items) {
|
|
472
|
+
const align = child["layout-item-align-self"] ?? node["layout-align-items"];
|
|
473
|
+
const crossFree = column ? innerWidth : innerHeight;
|
|
474
|
+
let crossOffset = column ? padding.left : padding.top;
|
|
475
|
+
if (align === "center") crossOffset += (crossFree - crossSize(child)) / 2;
|
|
476
|
+
else if (align === "end") crossOffset += crossFree - crossSize(child);
|
|
477
|
+
if (column) {
|
|
478
|
+
child.x = crossOffset;
|
|
479
|
+
child.y = cursor;
|
|
480
|
+
} else {
|
|
481
|
+
child.x = cursor;
|
|
482
|
+
child.y = crossOffset;
|
|
483
|
+
}
|
|
484
|
+
cursor += mainSize(child) + gap + spaceBetween;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function reflowProjection(nodes, rootId) {
|
|
489
|
+
if (nodes[rootId]) reflowFlexLayout(nodes, rootId);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function projectComponentVariant(product, variant, options = {}) {
|
|
493
|
+
const projected = projectNodes(variant.nodes, product, {
|
|
494
|
+
...options,
|
|
495
|
+
context: resolveContext(product, options.foundation, options.context ?? {}),
|
|
496
|
+
allowPreviewFallback: false,
|
|
497
|
+
});
|
|
498
|
+
reflowProjection(projected.nodes, variant.rootId);
|
|
499
|
+
return projected.nodes;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function projectScreen(product, screenId, options = {}) {
|
|
503
|
+
const { screen } = screenById(product, screenId);
|
|
504
|
+
const presentationId = options.presentationId ?? screen.basePresentationId;
|
|
505
|
+
const presentation = screen.presentations.find(({ id }) => id === presentationId);
|
|
506
|
+
if (!presentation) {
|
|
507
|
+
fail("missing_presentation", `Presentation not found: ${presentationId}`, {
|
|
508
|
+
presentationId,
|
|
509
|
+
screenId,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const context = resolveContext(
|
|
513
|
+
product,
|
|
514
|
+
options.foundation,
|
|
515
|
+
options.context ?? {},
|
|
516
|
+
);
|
|
517
|
+
const projected = projectNodes(presentation.nodes, product, {
|
|
518
|
+
allowPreviewFallback: options.allowPreviewFallback === true,
|
|
519
|
+
context,
|
|
520
|
+
foundation: options.foundation,
|
|
521
|
+
libraries: options.libraries,
|
|
522
|
+
});
|
|
523
|
+
reflowProjection(projected.nodes, presentation.rootId);
|
|
524
|
+
return {
|
|
525
|
+
context,
|
|
526
|
+
fallbackUsed: projected.fallbackUsed,
|
|
527
|
+
nodes: projected.nodes,
|
|
528
|
+
presentation: { ...structuredClone(presentation), nodes: projected.nodes },
|
|
529
|
+
presentationId,
|
|
530
|
+
rootId: presentation.rootId,
|
|
531
|
+
screen: structuredClone(screen),
|
|
532
|
+
screenId,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function projectComponentScenario(product, scenario, options) {
|
|
537
|
+
const target = componentTarget(
|
|
538
|
+
product,
|
|
539
|
+
options.foundation,
|
|
540
|
+
options.libraries ?? [],
|
|
541
|
+
scenario.target.component,
|
|
542
|
+
);
|
|
543
|
+
if (!target) {
|
|
544
|
+
fail(
|
|
545
|
+
"missing_component",
|
|
546
|
+
`Component not found: ${scenario.target.component.assetId}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const selection = structuredClone(scenario.target.variant);
|
|
550
|
+
for (const action of scenario.actions) {
|
|
551
|
+
if (action.type === "set-state") selection[action.axisId] = action.value;
|
|
552
|
+
}
|
|
553
|
+
const match = findComponentVariant(target.componentSet, selection, {
|
|
554
|
+
allowPreviewFallback: options.allowPreviewFallback,
|
|
555
|
+
});
|
|
556
|
+
if (!match.variant) {
|
|
557
|
+
fail(
|
|
558
|
+
"missing_variant",
|
|
559
|
+
`No exact variant exists for ${target.componentSet.id}`,
|
|
560
|
+
{ componentId: target.componentSet.id, selection },
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
const projected = projectNodes(match.variant.nodes, product, {
|
|
564
|
+
allowPreviewFallback: options.allowPreviewFallback === true,
|
|
565
|
+
context: options.context,
|
|
566
|
+
foundation: options.foundation,
|
|
567
|
+
libraries: options.libraries,
|
|
568
|
+
});
|
|
569
|
+
reflowProjection(projected.nodes, match.variant.rootId);
|
|
570
|
+
const nodes = projected.nodes;
|
|
571
|
+
for (const action of scenario.actions) {
|
|
572
|
+
if (action.type !== "set-override") continue;
|
|
573
|
+
const separator = action.overridePath.indexOf(":");
|
|
574
|
+
const nodeId = action.overridePath.slice(0, separator);
|
|
575
|
+
const field = action.overridePath.slice(separator + 1);
|
|
576
|
+
if (separator <= 0 || !nodes[nodeId] || !field) {
|
|
577
|
+
fail(
|
|
578
|
+
"missing_component_override_target",
|
|
579
|
+
`Scenario override target is missing: ${action.overridePath}`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
nodes[nodeId][field] = structuredClone(action.value);
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
context: options.context,
|
|
586
|
+
fallbackUsed: match.fallbackUsed || projected.fallbackUsed,
|
|
587
|
+
nodes,
|
|
588
|
+
presentationId: null,
|
|
589
|
+
rootId: match.variant.rootId,
|
|
590
|
+
screenId: null,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function applyScenarioActions(projection, scenario) {
|
|
595
|
+
for (const action of scenario.actions) {
|
|
596
|
+
if (action.type === "set-state" || action.type === "set-override") continue;
|
|
597
|
+
const node = projection.nodes[action.nodeId];
|
|
598
|
+
if (!node) {
|
|
599
|
+
fail("missing_scenario_node", `Scenario Node is missing: ${action.nodeId}`);
|
|
600
|
+
}
|
|
601
|
+
if (action.type === "set-text") {
|
|
602
|
+
if (node.type !== "TEXT") {
|
|
603
|
+
fail(
|
|
604
|
+
"scenario_node_type_mismatch",
|
|
605
|
+
`set-text target is not TEXT: ${action.nodeId}`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
node.text = action.value;
|
|
609
|
+
} else if (action.type === "set-visibility") {
|
|
610
|
+
node.visible = action.visible;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function projectScenario(product, scenarioId, options = {}) {
|
|
616
|
+
const scenario = product.domain.scenarios.get(scenarioId);
|
|
617
|
+
if (!scenario) {
|
|
618
|
+
fail("missing_scenario", `Scenario not found: ${scenarioId}`, { scenarioId });
|
|
619
|
+
}
|
|
620
|
+
const context = resolveContext(product, options.foundation, {
|
|
621
|
+
...scenario.context,
|
|
622
|
+
...(options.context ?? {}),
|
|
623
|
+
});
|
|
624
|
+
const projection =
|
|
625
|
+
scenario.target.kind === "screen"
|
|
626
|
+
? projectScreen(product, scenario.target.screen.assetId, {
|
|
627
|
+
...options,
|
|
628
|
+
context,
|
|
629
|
+
presentationId: scenario.target.presentationId,
|
|
630
|
+
})
|
|
631
|
+
: projectComponentScenario(product, scenario, {
|
|
632
|
+
...options,
|
|
633
|
+
context,
|
|
634
|
+
});
|
|
635
|
+
applyScenarioActions(projection, scenario);
|
|
636
|
+
const visibleNodeIds = Object.values(projection.nodes)
|
|
637
|
+
.filter((node) => node.visible !== false)
|
|
638
|
+
.map(({ id }) => id)
|
|
639
|
+
.sort();
|
|
640
|
+
const expectedVisibleNodeIds = [...scenario.expectedVisibleNodeIds].sort();
|
|
641
|
+
return {
|
|
642
|
+
...projection,
|
|
643
|
+
diagnostics:
|
|
644
|
+
JSON.stringify(visibleNodeIds) === JSON.stringify(expectedVisibleNodeIds)
|
|
645
|
+
? []
|
|
646
|
+
: [
|
|
647
|
+
{
|
|
648
|
+
code: "scenario_visibility_mismatch",
|
|
649
|
+
expectedVisibleNodeIds,
|
|
650
|
+
message: "Projected visibility differs from Scenario expectation",
|
|
651
|
+
visibleNodeIds,
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
scenario: structuredClone(scenario),
|
|
655
|
+
scenarioId,
|
|
656
|
+
};
|
|
657
|
+
}
|