@yahoo/uds-create-config 4.27.1 → 4.28.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/dist/capture/capture-to-spec.js +228 -30
- package/dist/configs/system.js +2 -0
- package/dist/entities/system/CanvasRole.d.ts +6 -11
- package/dist/entities/system/CanvasRole.js +7 -11
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +1 -1
- package/dist/framework/projections.js +26 -2
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/validate-spec.js +11 -2
- package/dist/migrations/20260925143319_canvas_role_slot.d.ts +12 -0
- package/dist/migrations/20260925143319_canvas_role_slot.js +8 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRef, pathOf } from "../framework/utils/refs.js";
|
|
2
|
-
import { authoredValue } from "../framework/projections.js";
|
|
2
|
+
import { LAYER_PROPS, authoredValue } from "../framework/projections.js";
|
|
3
3
|
import { intoName } from "../framework/prop-surface.js";
|
|
4
4
|
import { Component } from "../entities/system/Component.js";
|
|
5
5
|
import { elementFromCaptured, isCapturedElement } from "./captured-tree.js";
|
|
@@ -205,7 +205,7 @@ var Converter = class {
|
|
|
205
205
|
const node = layer.childNodes[i];
|
|
206
206
|
if (!isElement(node)) continue;
|
|
207
207
|
if (node.getAttribute("data-uds-component") === path && node.getAttribute("data-uds-layer") !== "root") continue;
|
|
208
|
-
if (node.tagName.toLowerCase() === "svg" || node.hasAttribute("data-uds-component") && node.getAttribute("data-uds-layer") === "root") out.push(...this.convert(node));
|
|
208
|
+
if (node.tagName.toLowerCase() === "svg" || isImage(node) || node.hasAttribute("data-uds-component") && node.getAttribute("data-uds-layer") === "root") out.push(...this.convert(node));
|
|
209
209
|
else if (visible(node) && framesItself(node)) out.push(...this.frame(node));
|
|
210
210
|
else if (visible(node)) out.push(...this.pinFolded(node, this.convertOutsideLayers(node, path)));
|
|
211
211
|
}
|
|
@@ -219,6 +219,7 @@ var Converter = class {
|
|
|
219
219
|
return key ? [key] : [];
|
|
220
220
|
}
|
|
221
221
|
if (el.tagName.toLowerCase() === "svg") return this.icon(el);
|
|
222
|
+
if (isImage(el)) return this.image(el);
|
|
222
223
|
if (isField(el)) return this.field(el);
|
|
223
224
|
if (framesItself(el)) return this.frame(el);
|
|
224
225
|
return this.pinFolded(el, this.convertChildren(el));
|
|
@@ -294,20 +295,37 @@ var Converter = class {
|
|
|
294
295
|
* What `el`'s render placed outside its stamped slot layers and claimed layers, walking down to
|
|
295
296
|
* the first thing on each branch that is a piece of its own: a nested component, a glyph (unless
|
|
296
297
|
* `el` draws its own), a field it does not read, or an unmarked element with an inline style.
|
|
298
|
+
*
|
|
299
|
+
* With `takesText`, a run of text between them is placed through the text role, so a text
|
|
300
|
+
* component that mixes its own words with nested components keeps both, in order. Text with no
|
|
301
|
+
* component beside it is the component's own `children` value instead, and places nothing here.
|
|
297
302
|
*/
|
|
298
|
-
looseContent({ el, claimed, takesGlyphs, takesFields }) {
|
|
303
|
+
looseContent({ el, claimed, takesGlyphs, takesFields, takesText }) {
|
|
299
304
|
const out = [];
|
|
305
|
+
const runs = /* @__PURE__ */ new Set();
|
|
300
306
|
const walk = (node) => {
|
|
301
307
|
for (let i = 0; i < node.childNodes.length; i += 1) {
|
|
302
308
|
if (this.childCap > 0 && out.length >= this.childCap) return;
|
|
303
309
|
const child = node.childNodes[i];
|
|
304
|
-
if (!isElement(child)
|
|
310
|
+
if (!isElement(child)) {
|
|
311
|
+
const text = (child.textContent ?? "").replace(/\s+/g, " ");
|
|
312
|
+
if (takesText && text.trim()) for (const key of this.textRun(text)) {
|
|
313
|
+
runs.add(key);
|
|
314
|
+
out.push(key);
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (!visible(child) || claimed.has(child)) continue;
|
|
305
319
|
if (child.hasAttribute("data-uds-slot")) continue;
|
|
306
320
|
const tag = child.tagName.toLowerCase();
|
|
307
|
-
if (child.hasAttribute("data-uds-component") && child.getAttribute("data-uds-layer") === "root")
|
|
308
|
-
|
|
321
|
+
if (child.hasAttribute("data-uds-component") && child.getAttribute("data-uds-layer") === "root") {
|
|
322
|
+
const keys = this.convert(child);
|
|
323
|
+
if (takesText) for (const key of keys) this.flowInline(key, child);
|
|
324
|
+
out.push(...keys);
|
|
325
|
+
} else if (tag === "svg") {
|
|
309
326
|
if (takesGlyphs) out.push(...this.convert(child));
|
|
310
|
-
} else if (
|
|
327
|
+
} else if (isImage(child)) out.push(...this.image(child));
|
|
328
|
+
else if (isField(child)) {
|
|
311
329
|
if (takesFields) out.push(...this.field(child));
|
|
312
330
|
} else if (framesItself(child)) out.push(...this.frame(child));
|
|
313
331
|
else {
|
|
@@ -319,8 +337,78 @@ var Converter = class {
|
|
|
319
337
|
}
|
|
320
338
|
};
|
|
321
339
|
walk(el);
|
|
340
|
+
if (out.every((key) => runs.has(key))) {
|
|
341
|
+
for (const key of runs) delete this.elements[key];
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
322
344
|
return out;
|
|
323
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* A nested component in a text run keeps the page's inline flow. The page laid it out as an inline
|
|
348
|
+
* element (a breadcrumb's last crumb is a `Text as="span"`), and `as` is not a prop a spec carries,
|
|
349
|
+
* so the canvas would draw the component's block element and break the line. A component whose own
|
|
350
|
+
* root style declares a display keeps it.
|
|
351
|
+
*/
|
|
352
|
+
flowInline(key, el) {
|
|
353
|
+
if (!INLINE_TAGS.has(el.tagName.toLowerCase())) return;
|
|
354
|
+
const element = this.elements[key];
|
|
355
|
+
if (!element || element.props.display !== void 0) return;
|
|
356
|
+
const path = pathOf(element.type);
|
|
357
|
+
const grammar = this.grammar.component(path);
|
|
358
|
+
if (!grammar || rootDeclaresDisplay(this.options.config, path)) return;
|
|
359
|
+
this.propsFromDeclarations([["display", "inline"]], grammar, element.props);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* A run of a text component's own text, as the text role's component laid out inline so it flows
|
|
363
|
+
* with the components beside it and takes its type from the text around it.
|
|
364
|
+
*/
|
|
365
|
+
textRun(text) {
|
|
366
|
+
const role = canvasRole(this.options.config, "text");
|
|
367
|
+
if (!role) return [];
|
|
368
|
+
const props = {};
|
|
369
|
+
const grammar = this.grammar.component(role.component);
|
|
370
|
+
if (grammar) this.propsFromDeclarations([["display", "inline"]], grammar, props);
|
|
371
|
+
const key = this.key(role.component);
|
|
372
|
+
this.elements[key] = {
|
|
373
|
+
type: `component:${role.component}`,
|
|
374
|
+
props: {
|
|
375
|
+
...props,
|
|
376
|
+
children: text
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
return [key];
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* An unmarked `<img>` placed through the canvas's image role, with the source and the box it
|
|
383
|
+
* painted. One with no source shows nothing and places nothing.
|
|
384
|
+
*/
|
|
385
|
+
image(img) {
|
|
386
|
+
const src = img.getAttribute("src");
|
|
387
|
+
const role = canvasRole(this.options.config, "image");
|
|
388
|
+
if (!src || !role) return [];
|
|
389
|
+
const props = { ...role.defaultProps };
|
|
390
|
+
const grammar = this.grammar.component(role.component);
|
|
391
|
+
if (grammar) {
|
|
392
|
+
const width = numberAttr(img, "width");
|
|
393
|
+
const height = numberAttr(img, "height");
|
|
394
|
+
const sized = [];
|
|
395
|
+
if (width !== void 0) sized.push(["width", `${width}px`]);
|
|
396
|
+
if (height !== void 0) sized.push(["height", `${height}px`]);
|
|
397
|
+
this.propsFromDeclarations(sized, grammar, props);
|
|
398
|
+
this.propsFromInlineStyle(img, grammar, props);
|
|
399
|
+
}
|
|
400
|
+
const alt = img.getAttribute("alt");
|
|
401
|
+
const key = this.key(role.component);
|
|
402
|
+
this.elements[key] = {
|
|
403
|
+
type: `component:${role.component}`,
|
|
404
|
+
props: {
|
|
405
|
+
...props,
|
|
406
|
+
src,
|
|
407
|
+
...alt ? { alt } : {}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
return [key];
|
|
411
|
+
}
|
|
324
412
|
/** A form field no component reads, placed as the text it shows through the canvas's text role. */
|
|
325
413
|
field(field) {
|
|
326
414
|
const text = fieldText(field);
|
|
@@ -342,6 +430,11 @@ var Converter = class {
|
|
|
342
430
|
};
|
|
343
431
|
return [key];
|
|
344
432
|
}
|
|
433
|
+
/** The layers a component declares, which a stamped layer override may name. */
|
|
434
|
+
layerNames(path) {
|
|
435
|
+
const body = this.options.config.resolve("component", path);
|
|
436
|
+
return new Set(Object.keys(body?.layers ?? {}));
|
|
437
|
+
}
|
|
345
438
|
/** A bare `<svg>` placed as the component its library renders it with. */
|
|
346
439
|
icon(svg) {
|
|
347
440
|
const answer = this.options.icons?.resolve(svgOf(svg));
|
|
@@ -367,6 +460,11 @@ var Converter = class {
|
|
|
367
460
|
if (grammar) this.propsFromClasses(el, grammar, props);
|
|
368
461
|
Object.assign(props, stamped);
|
|
369
462
|
if (grammar) this.propsFromInlineStyle(el, grammar, props);
|
|
463
|
+
const layerProps = stampedLayerProps({
|
|
464
|
+
el,
|
|
465
|
+
layers: this.layerNames(path)
|
|
466
|
+
});
|
|
467
|
+
if (layerProps) props[LAYER_PROPS] = layerProps;
|
|
370
468
|
if (props.position === "fixed") props.position = "absolute";
|
|
371
469
|
const renders = this.options.icons?.renderers.has(path) ?? false;
|
|
372
470
|
if (renders) {
|
|
@@ -438,11 +536,14 @@ var Converter = class {
|
|
|
438
536
|
const field = fieldValue(el, declared.value);
|
|
439
537
|
if (field !== void 0) props.value = field;
|
|
440
538
|
}
|
|
539
|
+
const textSlot = declared.children;
|
|
540
|
+
const holdsText = textSlot?.type === "slot" && textSlot.text === true;
|
|
441
541
|
const loose = this.looseContent({
|
|
442
542
|
el,
|
|
443
543
|
claimed,
|
|
444
544
|
takesGlyphs: !renders,
|
|
445
|
-
takesFields: !("value" in declared)
|
|
545
|
+
takesFields: !("value" in declared),
|
|
546
|
+
takesText: holdsText && props.children === void 0
|
|
446
547
|
});
|
|
447
548
|
const filled = new Set(Object.keys(slots));
|
|
448
549
|
for (const nested of loose) {
|
|
@@ -459,8 +560,7 @@ var Converter = class {
|
|
|
459
560
|
if ("tooltip" in declared && props.tooltip === void 0) props.tooltip = aria;
|
|
460
561
|
props["aria-label"] = aria;
|
|
461
562
|
}
|
|
462
|
-
|
|
463
|
-
if (textSlot?.type === "slot" && textSlot.text && props.children === void 0 && !slots.children) {
|
|
563
|
+
if (holdsText && props.children === void 0 && !slots.children) {
|
|
464
564
|
const text = collapse(ownText(el, claimed));
|
|
465
565
|
if (text) props.children = text;
|
|
466
566
|
}
|
|
@@ -471,8 +571,9 @@ var Converter = class {
|
|
|
471
571
|
if ("checked" in declared) props.checked = true;
|
|
472
572
|
else if ("selected" in declared) props.selected = true;
|
|
473
573
|
}
|
|
474
|
-
if (grammar) {
|
|
475
|
-
|
|
574
|
+
if (grammar) for (const prop of Object.keys(props)) {
|
|
575
|
+
if (prop in declared || prop === "aria-label" || prop === "layerProps") continue;
|
|
576
|
+
delete props[prop];
|
|
476
577
|
}
|
|
477
578
|
this.elements[key] = element;
|
|
478
579
|
return key;
|
|
@@ -517,32 +618,82 @@ var Converter = class {
|
|
|
517
618
|
}
|
|
518
619
|
/** {@link propsFromInlineStyle} over declarations the caller supplies. */
|
|
519
620
|
propsFromDeclarations(declarations, grammar, props) {
|
|
520
|
-
for (const [property, raw] of declarations) {
|
|
621
|
+
for (const [property, raw] of declarations.flatMap(gridAreaLines)) {
|
|
521
622
|
let value = normalizeCssValue(raw);
|
|
522
623
|
if (!value || SKIP_INLINE_PROPERTY.has(property) || SKIP_INLINE_VALUE.has(value)) continue;
|
|
523
624
|
if (property === "position" && value === "fixed") value = "absolute";
|
|
524
625
|
if (VIEWPORT_VALUE.test(value) && !WHOLE_VIEWPORT.test(value)) value = frameUnits(value, this.options.width, this.options.height);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
626
|
+
if (this.propFromDeclaration({
|
|
627
|
+
property,
|
|
628
|
+
value,
|
|
629
|
+
grammar,
|
|
630
|
+
props
|
|
631
|
+
})) continue;
|
|
632
|
+
for (const [longhand, part] of gridLonghands({
|
|
633
|
+
property,
|
|
634
|
+
value
|
|
635
|
+
})) this.propFromDeclaration({
|
|
636
|
+
property: longhand,
|
|
637
|
+
value: part,
|
|
638
|
+
grammar,
|
|
639
|
+
props
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** Writes the prop one declaration becomes, answering whether any prop took it. */
|
|
644
|
+
propFromDeclaration({ property, value, grammar, props }) {
|
|
645
|
+
for (const option of this.grammar.stylePropsFor(property)) {
|
|
646
|
+
const prop = grammar.propForStyleProperty.get(option.path);
|
|
647
|
+
if (!prop) continue;
|
|
648
|
+
const css = option.negates ? negated(value) : value;
|
|
649
|
+
if (css === void 0) continue;
|
|
650
|
+
let next;
|
|
651
|
+
const leaf = equivalents({
|
|
652
|
+
property,
|
|
653
|
+
value: css
|
|
654
|
+
}).map((spelling) => option.leafFor(spelling)).find((found) => found !== void 0);
|
|
655
|
+
if (WHOLE_VIEWPORT.test(css)) next = option.fullLeaf;
|
|
656
|
+
else if (leaf !== void 0) next = leaf === "" ? true : leaf;
|
|
657
|
+
else if (option.arbitrary) next = `[${css.replaceAll(" ", "_")}]`;
|
|
658
|
+
if (next === void 0) continue;
|
|
659
|
+
props[prop] = next;
|
|
660
|
+
return true;
|
|
542
661
|
}
|
|
662
|
+
return false;
|
|
543
663
|
}
|
|
544
664
|
};
|
|
545
665
|
/**
|
|
666
|
+
* A `grid-area` as the `grid-row` and `grid-column` it sets: `row-start / column-start / row-end /
|
|
667
|
+
* column-end`, where an omitted or `auto` end leaves that end to the start. A table that places each
|
|
668
|
+
* cell by area otherwise auto-places them, and a row's full-width backdrop then takes the first
|
|
669
|
+
* column and pushes every cell one to the right. Any other declaration passes through.
|
|
670
|
+
*/
|
|
671
|
+
function gridAreaLines([property, value]) {
|
|
672
|
+
if (property !== "grid-area") return [[property, value]];
|
|
673
|
+
const [rowStart, columnStart, rowEnd, columnEnd] = value.split("/").map((line) => line.trim());
|
|
674
|
+
const lines = (start, end) => end && end !== "auto" ? `${start} / ${end}` : start;
|
|
675
|
+
const row = lines(rowStart, rowEnd);
|
|
676
|
+
const column = lines(columnStart, columnEnd);
|
|
677
|
+
const out = [];
|
|
678
|
+
if (row && row !== "auto") out.push(["grid-row", row]);
|
|
679
|
+
if (column && column !== "auto") out.push(["grid-column", column]);
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* A `grid-row` or `grid-column` shorthand as its start and end lines (`2 / 4` is start `2`, end `4`,
|
|
684
|
+
* and a lone `2` is the start), for a system that declares the lines rather than the shorthand. A
|
|
685
|
+
* page's rows often number past what a system declares, and a cell that keeps only its column still
|
|
686
|
+
* lands in its own row: auto-placement starts a new row when a cell's column comes before the last.
|
|
687
|
+
*/
|
|
688
|
+
function gridLonghands({ property, value }) {
|
|
689
|
+
if (property !== "grid-row" && property !== "grid-column") return [];
|
|
690
|
+
const [start, end] = value.split("/").map((line) => line.trim());
|
|
691
|
+
const out = [];
|
|
692
|
+
if (start) out.push([`${property}-start`, start]);
|
|
693
|
+
if (end) out.push([`${property}-end`, end]);
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
546
697
|
* The prop a nested component the render placed outside every stamped layer belongs to: `children`
|
|
547
698
|
* first, then the other slots in declaration order, taking the first that is not already filled and
|
|
548
699
|
* accepts the component's type. `children` also holds it when it forwards into a layer's children,
|
|
@@ -577,6 +728,9 @@ const ARTBOARD = [
|
|
|
577
728
|
];
|
|
578
729
|
/** How a single-line input lays out its text: centred across its height. */
|
|
579
730
|
const INPUT_LINE = [["display", "flex"], ["align-items", "center"]];
|
|
731
|
+
function isImage(el) {
|
|
732
|
+
return el.tagName.toLowerCase() === "img" && !el.hasAttribute("data-uds-component");
|
|
733
|
+
}
|
|
580
734
|
function isField(el) {
|
|
581
735
|
const tag = el.tagName.toLowerCase();
|
|
582
736
|
return tag === "input" || tag === "textarea";
|
|
@@ -589,6 +743,29 @@ function fieldText(field) {
|
|
|
589
743
|
if (NON_TEXT_INPUT.has(type)) return "";
|
|
590
744
|
return collapse(field.getAttribute("value") ?? "") || placeholder;
|
|
591
745
|
}
|
|
746
|
+
/** Elements the page lays out inline. */
|
|
747
|
+
const INLINE_TAGS = new Set([
|
|
748
|
+
"a",
|
|
749
|
+
"abbr",
|
|
750
|
+
"b",
|
|
751
|
+
"code",
|
|
752
|
+
"em",
|
|
753
|
+
"i",
|
|
754
|
+
"kbd",
|
|
755
|
+
"mark",
|
|
756
|
+
"s",
|
|
757
|
+
"small",
|
|
758
|
+
"span",
|
|
759
|
+
"strong",
|
|
760
|
+
"sub",
|
|
761
|
+
"sup",
|
|
762
|
+
"u"
|
|
763
|
+
]);
|
|
764
|
+
/** Whether the component's own unconditional style gives its root a display. */
|
|
765
|
+
function rootDeclaresDisplay(config, path) {
|
|
766
|
+
const styles = Component.get(config, path)?.styles ?? {};
|
|
767
|
+
return Object.values(styles).some((rule) => Object.keys(rule?.when ?? {}).length === 0 && rule.layers?.root?.display !== void 0);
|
|
768
|
+
}
|
|
592
769
|
/** The component a canvas role places with (`text`, `frame`), and the props it places it with. */
|
|
593
770
|
function canvasRole(config, name) {
|
|
594
771
|
const entries = (config.resolve("canvas-role", name)?.toJSON())?.entries ?? [];
|
|
@@ -650,6 +827,27 @@ function equivalents({ property, value }) {
|
|
|
650
827
|
if ((property === "grid-column" || property === "grid-row") && /^span \d+$/.test(value)) return [value, `${value} / ${value}`];
|
|
651
828
|
return [value];
|
|
652
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* The `layerProps` a call site passed, as the component stamped them in `data-uds-layer-props`: each
|
|
832
|
+
* layer the component declares, with its bag of props. Nothing when there is no stamp.
|
|
833
|
+
*/
|
|
834
|
+
function stampedLayerProps({ el, layers }) {
|
|
835
|
+
const raw = el.getAttribute("data-uds-layer-props");
|
|
836
|
+
if (!raw) return void 0;
|
|
837
|
+
let parsed;
|
|
838
|
+
try {
|
|
839
|
+
parsed = JSON.parse(raw);
|
|
840
|
+
} catch {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
844
|
+
const out = {};
|
|
845
|
+
for (const [layer, bag] of Object.entries(parsed)) {
|
|
846
|
+
if (!layers.has(layer) || !bag || typeof bag !== "object") continue;
|
|
847
|
+
out[layer] = bag;
|
|
848
|
+
}
|
|
849
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
850
|
+
}
|
|
653
851
|
function stampedProps(el) {
|
|
654
852
|
const raw = el.getAttribute("data-uds-props");
|
|
655
853
|
if (!raw) return {};
|
package/dist/configs/system.js
CHANGED
|
@@ -41,6 +41,7 @@ import { transitionTokenValueMigration } from "../migrations/20260923144958_tran
|
|
|
41
41
|
import { renderTokenRefsMigration } from "../migrations/20260923160425_render_token_refs.js";
|
|
42
42
|
import { globalStyleDerivedColorsMigration } from "../migrations/20260923163604_global_style_derived_colors.js";
|
|
43
43
|
import { slotCodeFormsMigration } from "../migrations/20260924044539_slot_code_forms.js";
|
|
44
|
+
import { canvasRoleSlotMigration } from "../migrations/20260925143319_canvas_role_slot.js";
|
|
44
45
|
//#region src/configs/system.ts
|
|
45
46
|
/**
|
|
46
47
|
* `System` — the design-system config TYPE. Declares the exact kinds a design system owns (tokens,
|
|
@@ -102,5 +103,6 @@ if (!registeredSchemaMigrations().has(20260923144958)) registerSchemaMigrations(
|
|
|
102
103
|
if (!registeredSchemaMigrations().has(20260923160425)) registerSchemaMigrations(renderTokenRefsMigration);
|
|
103
104
|
if (!registeredSchemaMigrations().has(20260923163604)) registerSchemaMigrations(globalStyleDerivedColorsMigration);
|
|
104
105
|
if (!registeredSchemaMigrations().has(20260924044539)) registerSchemaMigrations(slotCodeFormsMigration);
|
|
106
|
+
if (!registeredSchemaMigrations().has(20260925143319)) registerSchemaMigrations(canvasRoleSlotMigration);
|
|
105
107
|
//#endregion
|
|
106
108
|
export { System };
|
|
@@ -3,19 +3,14 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
//#region src/entities/system/CanvasRole.d.ts
|
|
5
5
|
/**
|
|
6
|
-
* The roles a canvas has a tool for
|
|
6
|
+
* The roles a canvas has a tool for, the vocabulary a design system fills in. A role a config leaves
|
|
7
|
+
* unregistered has no entries, and its tool is disabled rather than falling back to a guess; what
|
|
8
|
+
* fills a role is whichever component the config points at, never a name the editor chose.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* naming a role here offers the slot without asserting anything fills it, and what fills it is
|
|
11
|
-
* never a name the editor chose — it is whichever component the config pointed at.
|
|
12
|
-
*
|
|
13
|
-
* The names live here, with the entity, because the entity is what they are names OF. They were
|
|
14
|
-
* previously kept in `@yahoo/uds-create-platform-types` as a hand-maintained copy, annotated as
|
|
15
|
-
* one ("mirrors `@yahoo/uds-create-config`'s `CanvasRole` union without importing it") — a second
|
|
16
|
-
* source of truth wearing a comment admitting it.
|
|
10
|
+
* `slot` is the one role no toolbar draws with. Its entries are the host layers a component's new
|
|
11
|
+
* slot property can start as, and an entry's `defaultProps` are the props that layer starts with.
|
|
17
12
|
*/
|
|
18
|
-
declare const CANVAS_ROLES: readonly ["frame", "text", "image", "icon"];
|
|
13
|
+
declare const CANVAS_ROLES: readonly ["frame", "text", "image", "icon", "slot"];
|
|
19
14
|
type CanvasRoleName = (typeof CANVAS_ROLES)[number];
|
|
20
15
|
declare const CanvasRole: EntityClass<z.ZodObject<{
|
|
21
16
|
entries: z.ZodArray<z.ZodObject<{
|
|
@@ -15,23 +15,19 @@ import { z } from "zod";
|
|
|
15
15
|
* });
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
* The roles a canvas has a tool for
|
|
18
|
+
* The roles a canvas has a tool for, the vocabulary a design system fills in. A role a config leaves
|
|
19
|
+
* unregistered has no entries, and its tool is disabled rather than falling back to a guess; what
|
|
20
|
+
* fills a role is whichever component the config points at, never a name the editor chose.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* naming a role here offers the slot without asserting anything fills it, and what fills it is
|
|
23
|
-
* never a name the editor chose — it is whichever component the config pointed at.
|
|
24
|
-
*
|
|
25
|
-
* The names live here, with the entity, because the entity is what they are names OF. They were
|
|
26
|
-
* previously kept in `@yahoo/uds-create-platform-types` as a hand-maintained copy, annotated as
|
|
27
|
-
* one ("mirrors `@yahoo/uds-create-config`'s `CanvasRole` union without importing it") — a second
|
|
28
|
-
* source of truth wearing a comment admitting it.
|
|
22
|
+
* `slot` is the one role no toolbar draws with. Its entries are the host layers a component's new
|
|
23
|
+
* slot property can start as, and an entry's `defaultProps` are the props that layer starts with.
|
|
29
24
|
*/
|
|
30
25
|
const CANVAS_ROLES = [
|
|
31
26
|
"frame",
|
|
32
27
|
"text",
|
|
33
28
|
"image",
|
|
34
|
-
"icon"
|
|
29
|
+
"icon",
|
|
30
|
+
"slot"
|
|
35
31
|
];
|
|
36
32
|
const CanvasRole = defineEntity({
|
|
37
33
|
kind: "canvas-role",
|
|
@@ -72,7 +72,7 @@ interface RecordedSource {
|
|
|
72
72
|
* The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
|
|
73
73
|
* that replaced it (see `detectedWireVersion`).
|
|
74
74
|
*/
|
|
75
|
-
declare const SERIALIZED_CONFIG_VERSION =
|
|
75
|
+
declare const SERIALIZED_CONFIG_VERSION = 20260925143319;
|
|
76
76
|
interface SerializedConfig {
|
|
77
77
|
/** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
|
|
78
78
|
readonly version: number;
|
package/dist/framework/Config.js
CHANGED
|
@@ -4883,7 +4883,7 @@ var Config = class Config {
|
|
|
4883
4883
|
* links from the provided sources. A declared `linkedKind` with no provided source throws. */
|
|
4884
4884
|
hydrateFrom(stored, options) {
|
|
4885
4885
|
const claimed = schemaVersionOf(stored);
|
|
4886
|
-
if (claimed >
|
|
4886
|
+
if (claimed > 20260925143319) throw new SchemaVersionTooNew(claimed);
|
|
4887
4887
|
const wire = upgradeSerializedConfig(stored);
|
|
4888
4888
|
if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating, or port the repo once with \`uds migrate\`.`);
|
|
4889
4889
|
const leftover = wire.options;
|
|
@@ -1042,14 +1042,38 @@ function forwardIntoName(into, own) {
|
|
|
1042
1042
|
const member = memberOf(into) ?? into.__ref;
|
|
1043
1043
|
return member.slice(member.indexOf("/") + 1);
|
|
1044
1044
|
}
|
|
1045
|
+
/** The instance prop that carries per-layer overrides, spread onto each named layer at render. */
|
|
1046
|
+
const LAYER_PROPS = "layerProps";
|
|
1047
|
+
/**
|
|
1048
|
+
* What is wrong with an instance's `layerProps`: it is an object keyed by the component's layers,
|
|
1049
|
+
* each holding an object of props for that layer. Empty when it is absent or sound.
|
|
1050
|
+
*/
|
|
1051
|
+
function layerPropsErrors({ config, target, value, where }) {
|
|
1052
|
+
if (value === void 0) return [];
|
|
1053
|
+
if (!isBag(value)) return [`${where} layerProps must be an object of layers`];
|
|
1054
|
+
const layers = config.resolve("component", target)?.layers ?? {};
|
|
1055
|
+
const errors = [];
|
|
1056
|
+
for (const [layer, bag] of Object.entries(value)) if (!(layer in layers)) errors.push(`${where} has no layer "${layer}"`);
|
|
1057
|
+
else if (!isBag(bag)) errors.push(`${where} layerProps.${layer} must be an object of props`);
|
|
1058
|
+
return errors;
|
|
1059
|
+
}
|
|
1060
|
+
function isBag(value) {
|
|
1061
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1062
|
+
}
|
|
1045
1063
|
/** Validate a prop bag against a component's declared props — unknown props + per-value domain
|
|
1046
1064
|
* checks. Generic: it resolves each prop's domain via {@link propValueDomain} and checks membership;
|
|
1047
1065
|
* no per-prop-type logic here. Returns the error strings (empty = valid). Lives in config so the
|
|
1048
1066
|
* `component/validate` op's truth is config-defined and the pipeline only renders it. */
|
|
1049
1067
|
function validateComponentProps(config, componentPath, props) {
|
|
1050
1068
|
const declared = resolveComponentProps(config, componentPath);
|
|
1051
|
-
const errors =
|
|
1069
|
+
const errors = layerPropsErrors({
|
|
1070
|
+
config,
|
|
1071
|
+
target: componentPath,
|
|
1072
|
+
value: props[LAYER_PROPS],
|
|
1073
|
+
where: componentPath
|
|
1074
|
+
});
|
|
1052
1075
|
for (const [name, value] of Object.entries(props)) {
|
|
1076
|
+
if (name === "layerProps") continue;
|
|
1053
1077
|
const decl = declared[name];
|
|
1054
1078
|
if (!decl) {
|
|
1055
1079
|
errors.push(`unknown prop \`${name}\``);
|
|
@@ -1248,4 +1272,4 @@ function declaredPropDefaults(props) {
|
|
|
1248
1272
|
return out;
|
|
1249
1273
|
}
|
|
1250
1274
|
//#endregion
|
|
1251
|
-
export { authoredBag, authoredValue, className, cssPropValue, cssVar, cssVarRef, declaredPropDefaults, declaredPropRouting, glyphComponentOf, glyphLibrariesOf, glyphPropsOf, glyphPropsOfBody, holdingPreviewConditions, iconToolAxisOptions, iconToolOf, keyedPreviewConditions, memberFromLeaf, memberLeaves, negatedCssValue, opacityPercentage, previewConditionalProps, previewConditions, previewDefaults, previewMatrix, propOwner, propValueDomain, propValueFromAxis, refListOf, renderStyleValue, resolveComponentProps, resolveFieldValue, resolveTokenValue, resolveTokenValueUnder, routedBag, sourceVarPrefix, styleAliasesOf, styleDeclarations, stylePropEntries, stylePropLeafForToken, stylePropNegates, stylePropTokenGroup, stylePropTokenPath, stylePropTokenValues, stylePropValueFromLeaf, stylePropValueLeaves, stylePropValues, stylePropertiesWriting, stylePropertyAccepts, stylePropertyFor, stylePropertyPathFor, takenByForward, validateComponentProps };
|
|
1275
|
+
export { LAYER_PROPS, authoredBag, authoredValue, className, cssPropValue, cssVar, cssVarRef, declaredPropDefaults, declaredPropRouting, glyphComponentOf, glyphLibrariesOf, glyphPropsOf, glyphPropsOfBody, holdingPreviewConditions, iconToolAxisOptions, iconToolOf, keyedPreviewConditions, layerPropsErrors, memberFromLeaf, memberLeaves, negatedCssValue, opacityPercentage, previewConditionalProps, previewConditions, previewDefaults, previewMatrix, propOwner, propValueDomain, propValueFromAxis, refListOf, renderStyleValue, resolveComponentProps, resolveFieldValue, resolveTokenValue, resolveTokenValueUnder, routedBag, sourceVarPrefix, styleAliasesOf, styleDeclarations, stylePropEntries, stylePropLeafForToken, stylePropNegates, stylePropTokenGroup, stylePropTokenPath, stylePropTokenValues, stylePropValueFromLeaf, stylePropValueLeaves, stylePropValues, stylePropertiesWriting, stylePropertyAccepts, stylePropertyFor, stylePropertyPathFor, takenByForward, validateComponentProps };
|
|
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
|
|
|
15
15
|
* sets this constant to its own older timestamp; the merge ref then carries a registered migration
|
|
16
16
|
* newer than the constant, and the assertion fails until the author re-mints.
|
|
17
17
|
*/
|
|
18
|
-
declare const CURRENT_SCHEMA_VERSION =
|
|
18
|
+
declare const CURRENT_SCHEMA_VERSION = 20260925143319;
|
|
19
19
|
/** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
|
|
20
20
|
* and a raw old-shape record both flow in without casts. */
|
|
21
21
|
interface VersionStamped {
|
|
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
|
|
|
13
13
|
* sets this constant to its own older timestamp; the merge ref then carries a registered migration
|
|
14
14
|
* newer than the constant, and the assertion fails until the author re-mints.
|
|
15
15
|
*/
|
|
16
|
-
const CURRENT_SCHEMA_VERSION =
|
|
16
|
+
const CURRENT_SCHEMA_VERSION = 20260925143319;
|
|
17
17
|
/** The mint that moved `options` into the `settings` kind — restated here, rather than imported
|
|
18
18
|
* from the migration that owns it, because that migration imports this module. */
|
|
19
19
|
const SETTINGS_AS_KIND_VERSION = 20260908171448;
|
|
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
|
|
|
45
45
|
for (const migration of migrations) {
|
|
46
46
|
if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
|
|
47
47
|
if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
|
|
48
|
-
if (migration.version >
|
|
48
|
+
if (migration.version > 20260925143319) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
|
|
49
49
|
REGISTERED.set(migration.version, migration);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
|
|
|
102
102
|
* Already-current input passes through untouched — same reference, so hot paths pay nothing.
|
|
103
103
|
*/
|
|
104
104
|
function upgradeSerializedConfig(json, options) {
|
|
105
|
-
const target = options?.target ??
|
|
105
|
+
const target = options?.target ?? 20260925143319;
|
|
106
106
|
const version = detectedWireVersion(json);
|
|
107
107
|
if (version > target) throw new SchemaVersionTooNew(version, target);
|
|
108
108
|
const raw = json;
|
|
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
|
|
|
160
160
|
* meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
|
|
161
161
|
*/
|
|
162
162
|
function upgradePatch(patch, writtenAt, options) {
|
|
163
|
-
const target = options?.target ??
|
|
163
|
+
const target = options?.target ?? 20260925143319;
|
|
164
164
|
if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
|
|
165
165
|
let current = patch;
|
|
166
166
|
for (const migration of pendingFor(writtenAt, target, options)) {
|
|
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
|
|
|
179
179
|
* stamp would cause.
|
|
180
180
|
*/
|
|
181
181
|
function upgradeDraftEntries(draft, options) {
|
|
182
|
-
const target = options?.target ??
|
|
182
|
+
const target = options?.target ?? 20260925143319;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { kindOf, memberOf, pathOf, splitRef } from "./utils/refs.js";
|
|
2
2
|
import { childrenPolicy } from "../entities/system/element.js";
|
|
3
|
-
import { resolveComponentProps } from "./projections.js";
|
|
3
|
+
import { LAYER_PROPS, layerPropsErrors, resolveComponentProps } from "./projections.js";
|
|
4
4
|
import { slotOf } from "../spec/slot.js";
|
|
5
5
|
import { refResolves } from "./ref-integrity.js";
|
|
6
6
|
//#region src/framework/validate-spec.ts
|
|
@@ -28,7 +28,16 @@ function walk(config, node, path, errors) {
|
|
|
28
28
|
errors.push(`${path}: unknown ${kind} "${named}"`);
|
|
29
29
|
} else if (kind === "component") {
|
|
30
30
|
const props = resolveComponentProps(config, target);
|
|
31
|
-
for (const name of Object.keys(node.props ?? {}))
|
|
31
|
+
for (const name of Object.keys(node.props ?? {})) {
|
|
32
|
+
if (name === "layerProps") continue;
|
|
33
|
+
if (!(name in props)) errors.push(`${path}: unknown prop "${name}" on "${node.type}"`);
|
|
34
|
+
}
|
|
35
|
+
errors.push(...layerPropsErrors({
|
|
36
|
+
config,
|
|
37
|
+
target,
|
|
38
|
+
value: node.props?.[LAYER_PROPS],
|
|
39
|
+
where: `${path}: "${node.type}"`
|
|
40
|
+
}));
|
|
32
41
|
if (node.children?.length && childrenPolicy(config, target) === "none") errors.push(`${path}: "${node.type}" takes no children`);
|
|
33
42
|
for (const [name, fills] of Object.entries(node.slots ?? {})) {
|
|
34
43
|
const slot = slotOf({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An additive fence: `slot` joins the canvas roles, naming the host layer a new slot property starts
|
|
3
|
+
* as. No stored byte moves.
|
|
4
|
+
*
|
|
5
|
+
* Identity, so a stamp below this version means "may lack these fields" and one at it means they
|
|
6
|
+
* are understood, which is what lets the pull gate keep an older client from stripping them.
|
|
7
|
+
*
|
|
8
|
+
* Deletable once no stored config artifact is below this version.
|
|
9
|
+
*/
|
|
10
|
+
import type { SchemaMigration } from '../framework/schema-version';
|
|
11
|
+
export declare const CANVAS_ROLE_SLOT_VERSION = 20260925143319;
|
|
12
|
+
export declare const canvasRoleSlotMigration: SchemaMigration;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/migrations/20260925143319_canvas_role_slot.ts
|
|
2
|
+
const CANVAS_ROLE_SLOT_VERSION = 20260925143319;
|
|
3
|
+
const canvasRoleSlotMigration = {
|
|
4
|
+
version: CANVAS_ROLE_SLOT_VERSION,
|
|
5
|
+
up: (json) => json
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CANVAS_ROLE_SLOT_VERSION, canvasRoleSlotMigration };
|