@yahoo/uds-create-config 4.28.0 → 4.28.2
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.
|
@@ -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 {};
|
|
@@ -25,12 +25,38 @@ function iconDomainInvariants(config) {
|
|
|
25
25
|
* always one of these. Without the check the ref reaches the build as a package export the package
|
|
26
26
|
* row never lists, and fails there with the wrong cause.
|
|
27
27
|
*/
|
|
28
|
-
const unboundGlyphs =
|
|
29
|
-
|
|
28
|
+
const unboundGlyphs = (config) => {
|
|
29
|
+
const held = heldGlyphsOf(config);
|
|
30
|
+
if (held.length === 0) return [];
|
|
31
|
+
const problems = [];
|
|
32
|
+
const librariesByModule = librariesByModuleOf(config);
|
|
33
|
+
const leafSetOf = leafSetsOf(config);
|
|
34
|
+
for (const { path, key, glyph } of held) {
|
|
35
|
+
const holders = librariesByModule.get(glyph.specifier) ?? [];
|
|
36
|
+
const at = `Component "${path}" key "${key}"`;
|
|
37
|
+
if (holders.length === 0) {
|
|
38
|
+
problems.push(`${at} holds the glyph \`${glyph.name}\` of \`${glyph.specifier}\`, and no icon library draws from that package; declare the library, or hand its exports to the one that does (\`Icon.group({ glyphs })\`).`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const listing = holders.filter((library) => leafSetOf(`icon:${library}`).has(glyph.name));
|
|
42
|
+
if (listing.length === 0) {
|
|
43
|
+
const empty = holders.every((library) => leafSetOf(`icon:${library}`).size === 0);
|
|
44
|
+
problems.push(`${at} holds \`${glyph.name}\`, which is not a member of ${spellLibraries(holders)}${empty ? "; the library has no members, so the package may not be installed" : ""}.`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
problems.push(`${at} holds \`${glyph.name}\`, a glyph of ${spellLibraries(listing)}, which that key does not draw from; a glyph belongs on a prop whose domain names its library, or a forward onto one.`);
|
|
48
|
+
}
|
|
49
|
+
return problems;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Every `package:` export left in a component's bags, in component, bag and key order. Keyed on the
|
|
53
|
+
* component slice alone, so an icon write (the build bakes metadata into each of a library's members
|
|
54
|
+
* one patch at a time) re-reads none of the components.
|
|
55
|
+
*/
|
|
56
|
+
const heldGlyphsOf = createSliceMemo({
|
|
57
|
+
kinds: ["component"],
|
|
30
58
|
compute: (config) => {
|
|
31
|
-
const
|
|
32
|
-
const librariesByModule = librariesByModuleOf(config);
|
|
33
|
-
const leafSetOf = leafSetsOf(config);
|
|
59
|
+
const held = [];
|
|
34
60
|
for (const item of config.list("component")) {
|
|
35
61
|
const body = config.resolve("component", item.path)?.toJSON();
|
|
36
62
|
for (const site of glyphBagSites({
|
|
@@ -39,23 +65,14 @@ const unboundGlyphs = createSliceMemo({
|
|
|
39
65
|
body
|
|
40
66
|
})) for (const [key, value] of Object.entries(site.bag)) {
|
|
41
67
|
const glyph = packageExportOf(value);
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const listing = holders.filter((library) => leafSetOf(`icon:${library}`).has(glyph.name));
|
|
50
|
-
if (listing.length === 0) {
|
|
51
|
-
const empty = holders.every((library) => leafSetOf(`icon:${library}`).size === 0);
|
|
52
|
-
problems.push(`${at} holds \`${glyph.name}\`, which is not a member of ${spellLibraries(holders)}${empty ? "; the library has no members, so the package may not be installed" : ""}.`);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
problems.push(`${at} holds \`${glyph.name}\`, a glyph of ${spellLibraries(listing)}, which that key does not draw from; a glyph belongs on a prop whose domain names its library, or a forward onto one.`);
|
|
68
|
+
if (glyph) held.push({
|
|
69
|
+
path: item.path,
|
|
70
|
+
key,
|
|
71
|
+
glyph
|
|
72
|
+
});
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
|
-
return
|
|
75
|
+
return held;
|
|
59
76
|
}
|
|
60
77
|
});
|
|
61
78
|
/**
|
|
@@ -105,39 +122,52 @@ function spellLibraries(libraries) {
|
|
|
105
122
|
return libraries.length === 1 ? `the library ${named[0]}` : `the libraries ${named.join(", ")}`;
|
|
106
123
|
}
|
|
107
124
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
125
|
+
* The component half is keyed on the component slice and the leaf sets on the icon slice, so an icon
|
|
126
|
+
* write re-walks no component. Only a prop listing two or more libraries can collide, and when none
|
|
127
|
+
* does no leaf set is built at all.
|
|
111
128
|
*/
|
|
112
|
-
const collisions =
|
|
113
|
-
|
|
129
|
+
const collisions = (config) => {
|
|
130
|
+
const listing = multiLibraryPropsOf(config);
|
|
131
|
+
if (listing.length === 0) return [];
|
|
132
|
+
const problems = [];
|
|
133
|
+
const leafSetOf = leafSetsOf(config);
|
|
134
|
+
for (const { path, prop, refs } of listing) {
|
|
135
|
+
const heldBy = /* @__PURE__ */ new Map();
|
|
136
|
+
const shared = /* @__PURE__ */ new Map();
|
|
137
|
+
for (const target of refs) for (const leaf of leafSetOf(target)) {
|
|
138
|
+
const other = heldBy.get(leaf);
|
|
139
|
+
if (other === void 0) {
|
|
140
|
+
heldBy.set(leaf, target);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (other === target) continue;
|
|
144
|
+
const pair = `${other} + ${target}`;
|
|
145
|
+
shared.set(pair, [...shared.get(pair) ?? [], leaf]);
|
|
146
|
+
}
|
|
147
|
+
for (const [pair, leaves] of shared) {
|
|
148
|
+
const [first, second] = pair.split(" + ");
|
|
149
|
+
problems.push(`Component "${path}" prop "${prop}" binds icon libraries "${first}" and "${second}", which both ship ${spellLeaves(leaves)}; a leaf has to name one glyph.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return problems;
|
|
153
|
+
};
|
|
154
|
+
/** Each prop whose domain lists two or more distinct icon libraries, in component and prop order. */
|
|
155
|
+
const multiLibraryPropsOf = createSliceMemo({
|
|
156
|
+
kinds: ["component"],
|
|
114
157
|
compute: (config) => {
|
|
115
|
-
const
|
|
116
|
-
const leafSetOf = leafSetsOf(config);
|
|
158
|
+
const listing = [];
|
|
117
159
|
for (const item of config.list("component")) {
|
|
118
160
|
const body = config.resolve("component", item.path)?.toJSON();
|
|
119
161
|
for (const [prop, decl] of Object.entries(body?.props ?? {})) {
|
|
120
162
|
const refs = listedLibraryRefsOf(decl);
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (other === void 0) {
|
|
127
|
-
heldBy.set(leaf, target);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (other === target) continue;
|
|
131
|
-
const pair = `${other} + ${target}`;
|
|
132
|
-
shared.set(pair, [...shared.get(pair) ?? [], leaf]);
|
|
133
|
-
}
|
|
134
|
-
for (const [pair, leaves] of shared) {
|
|
135
|
-
const [first, second] = pair.split(" + ");
|
|
136
|
-
problems.push(`Component "${item.path}" prop "${prop}" binds icon libraries "${first}" and "${second}", which both ship ${spellLeaves(leaves)}; a leaf has to name one glyph.`);
|
|
137
|
-
}
|
|
163
|
+
if (refs && new Set(refs).size > 1) listing.push({
|
|
164
|
+
path: item.path,
|
|
165
|
+
prop,
|
|
166
|
+
refs
|
|
167
|
+
});
|
|
138
168
|
}
|
|
139
169
|
}
|
|
140
|
-
return
|
|
170
|
+
return listing;
|
|
141
171
|
}
|
|
142
172
|
});
|
|
143
173
|
/** One leaf set per referenced library, filled on demand and kept while the icon slice stands. */
|
|
@@ -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 };
|
|
@@ -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({
|