@yahoo/uds-create-config 4.26.4 → 4.26.6
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.d.ts +1 -1
- package/dist/capture/capture-to-spec.js +296 -38
- package/dist/capture/class-grammar.d.ts +3 -0
- package/dist/capture/class-grammar.js +20 -6
- package/dist/capture/icon-resolver.d.ts +4 -4
- package/dist/capture/icon-resolver.js +3 -6
- package/dist/capture/node-ops.js +3 -3
- package/dist/configs/react-native-system.d.ts +18 -18
- package/dist/configs/system.js +1 -1
- package/dist/entities/native/NativeSettings.d.ts +1 -1
- package/dist/entities/native/NativeStyleProperty.d.ts +15 -15
- package/dist/entities/system/Component.js +3 -3
- package/dist/entities/system/Settings.js +2 -2
- package/dist/entities/system/copy-plan.js +1 -1
- package/dist/entities/system/icon-library.js +1 -1
- package/dist/entities/system/link-plan.js +1 -1
- package/dist/framework/Config.js +1 -1
- package/dist/framework/bindings.js +1 -1
- package/dist/framework/defineEntity.js +1 -1
- package/dist/framework/inline-sources.js +1 -1
- package/dist/framework/plan.js +1 -1
- package/dist/framework/projections.js +2 -2
- package/dist/framework/render-spec.js +2 -2
- package/dist/index.js +9 -9
- package/dist/spec/cell.js +1 -1
- package/dist/spec/detach.js +1 -1
- package/dist/spec/prop-conditions.js +1 -1
- package/dist/spec/slot.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ interface CaptureToSpecOptions {
|
|
|
32
32
|
readonly config: Config;
|
|
33
33
|
/** CSS selector for the subtree to convert; the whole document by default. */
|
|
34
34
|
readonly selector?: string;
|
|
35
|
-
/** Wraps every top-level element under this root;
|
|
35
|
+
/** Wraps every top-level element under this root; several are otherwise wrapped in the canvas frame. */
|
|
36
36
|
readonly root?: {
|
|
37
37
|
type: string;
|
|
38
38
|
props?: Record<string, unknown>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { isRef, pathOf } from "../framework/utils/refs.js";
|
|
2
|
+
import { authoredValue } from "../framework/projections.js";
|
|
3
|
+
import { intoName } from "../framework/prop-surface.js";
|
|
4
|
+
import { Component } from "../entities/system/Component.js";
|
|
1
5
|
import { elementFromCaptured, isCapturedElement } from "./captured-tree.js";
|
|
2
6
|
import { classGrammar, normalizeCssValue } from "./class-grammar.js";
|
|
3
7
|
//#region src/capture/capture-to-spec.ts
|
|
4
8
|
/** Inline declarations that are rendering plumbing rather than a prop. */
|
|
5
9
|
const SKIP_INLINE_PROPERTY = new Set([
|
|
6
|
-
"display",
|
|
7
10
|
"visibility",
|
|
8
11
|
"box-sizing",
|
|
9
12
|
"touch-action",
|
|
@@ -72,18 +75,23 @@ const ROOT_LAYER = "[data-uds-component][data-uds-layer=\"root\"]";
|
|
|
72
75
|
const DEFAULT_CHILD_CAP = 12;
|
|
73
76
|
const isElement = isCapturedElement;
|
|
74
77
|
function specFromCapture(options) {
|
|
75
|
-
const tree = elementFromCaptured(options.tree);
|
|
78
|
+
const tree = elementFromCaptured(markOrphanLayers(options.tree));
|
|
76
79
|
let scope = tree;
|
|
77
80
|
if (options.selector) scope = tree.closest(options.selector) ?? tree.querySelector(options.selector);
|
|
78
81
|
if (!scope) throw new Error(`Nothing matches "${options.selector}" in the capture.`);
|
|
79
82
|
const converter = new Converter(options, classGrammar(options.config));
|
|
80
83
|
const top = scope.closest(ROOT_LAYER) === scope || scope.tagName === "svg" ? converter.convertTop(scope) : converter.convertChildren(scope);
|
|
84
|
+
const frameRole = canvasRole(options.config, "frame");
|
|
85
|
+
const wrapper = options.root ?? (top.length > 1 && frameRole ? {
|
|
86
|
+
type: `component:${frameRole.component}`,
|
|
87
|
+
props: converter.artboardProps(frameRole.component)
|
|
88
|
+
} : void 0);
|
|
81
89
|
let rootKey;
|
|
82
|
-
if (
|
|
90
|
+
if (wrapper) {
|
|
83
91
|
rootKey = "root";
|
|
84
92
|
converter.elements.root = {
|
|
85
|
-
type:
|
|
86
|
-
props: { ...
|
|
93
|
+
type: wrapper.type,
|
|
94
|
+
props: { ...wrapper.props ?? {} },
|
|
87
95
|
children: top
|
|
88
96
|
};
|
|
89
97
|
} else if (top.length === 1) rootKey = top[0];
|
|
@@ -99,6 +107,39 @@ function specFromCapture(options) {
|
|
|
99
107
|
unresolvedIcons: converter.unresolvedIcons
|
|
100
108
|
};
|
|
101
109
|
}
|
|
110
|
+
/** Marks a layer whose component's root is not in the DOM with `<component>#<layer>`. */
|
|
111
|
+
const ORPHAN_LAYER = "data-uds-orphan-layer";
|
|
112
|
+
/**
|
|
113
|
+
* The tree with each layer of a component whose root is not in the DOM unmarked, and named in
|
|
114
|
+
* {@link ORPHAN_LAYER} instead. A tooltip renders only its anchor around the trigger and portals the
|
|
115
|
+
* bubble; the anchor is placed as a frame carrying the anchor's own styles, since there is no root for
|
|
116
|
+
* an instance of the component to paint.
|
|
117
|
+
*/
|
|
118
|
+
function markOrphanLayers(tree) {
|
|
119
|
+
const visit = (node, open) => {
|
|
120
|
+
const path = node.attrs?.["data-uds-component"];
|
|
121
|
+
const layer = node.attrs?.["data-uds-layer"];
|
|
122
|
+
let marked = node;
|
|
123
|
+
let inside = open;
|
|
124
|
+
if (path && layer && !open.has(path)) if (layer === "root") inside = new Set([...open, path]);
|
|
125
|
+
else {
|
|
126
|
+
const { "data-uds-component": _path, "data-uds-layer": _layer, ...attrs } = node.attrs ?? {};
|
|
127
|
+
marked = {
|
|
128
|
+
...node,
|
|
129
|
+
attrs: {
|
|
130
|
+
...attrs,
|
|
131
|
+
[ORPHAN_LAYER]: `${path}#${layer}`
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!marked.children?.length) return marked;
|
|
136
|
+
return {
|
|
137
|
+
...marked,
|
|
138
|
+
children: marked.children.map((child) => typeof child === "string" ? child : visit(child, inside))
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
return visit(tree, /* @__PURE__ */ new Set());
|
|
142
|
+
}
|
|
102
143
|
/**
|
|
103
144
|
* A capture lands on a design file, where an element's content is its structural `children` and
|
|
104
145
|
* `slots` holds named slots only: that is what a hand-built container has, and what the chrome, the
|
|
@@ -160,6 +201,7 @@ var Converter = class {
|
|
|
160
201
|
if (!isElement(node)) continue;
|
|
161
202
|
if (node.getAttribute("data-uds-component") === path && node.getAttribute("data-uds-layer") !== "root") continue;
|
|
162
203
|
if (node.tagName.toLowerCase() === "svg" || node.hasAttribute("data-uds-component") && node.getAttribute("data-uds-layer") === "root") out.push(...this.convert(node));
|
|
204
|
+
else if (visible(node) && framesItself(node)) out.push(...this.frame(node));
|
|
163
205
|
else if (visible(node)) out.push(...this.convertOutsideLayers(node, path));
|
|
164
206
|
}
|
|
165
207
|
return out;
|
|
@@ -172,8 +214,111 @@ var Converter = class {
|
|
|
172
214
|
return key ? [key] : [];
|
|
173
215
|
}
|
|
174
216
|
if (el.tagName.toLowerCase() === "svg") return this.icon(el);
|
|
217
|
+
if (isField(el)) return this.field(el);
|
|
218
|
+
if (framesItself(el)) return this.frame(el);
|
|
175
219
|
return this.convertChildren(el);
|
|
176
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* An unmarked element with an inline style, which is what the page drew it with, as the canvas's
|
|
223
|
+
* frame component carrying that style. One with nothing inline only groups its children and is
|
|
224
|
+
* folded away, and an empty one that paints nothing of its own (a portal's container) places
|
|
225
|
+
* nothing.
|
|
226
|
+
*/
|
|
227
|
+
frame(el) {
|
|
228
|
+
const children = this.convertChildren(el);
|
|
229
|
+
if (children.length === 0 && !paintsItself(el)) return [];
|
|
230
|
+
const role = canvasRole(this.options.config, "frame");
|
|
231
|
+
const grammar = role ? this.grammar.component(role.component) : void 0;
|
|
232
|
+
if (!role || !grammar) return children;
|
|
233
|
+
const props = this.orphanLayerProps(el, grammar);
|
|
234
|
+
this.propsFromInlineStyle(el, grammar, props);
|
|
235
|
+
if (Object.keys(props).length === 0) return children;
|
|
236
|
+
const key = this.key(role.component);
|
|
237
|
+
this.elements[key] = {
|
|
238
|
+
type: `component:${role.component}`,
|
|
239
|
+
props,
|
|
240
|
+
...children.length > 0 ? { children } : {}
|
|
241
|
+
};
|
|
242
|
+
return [key];
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The styles the config gives an orphaned layer, as the frame's props: the rules that always apply
|
|
246
|
+
* and the ones its classes say are on, in authored order. Only values the frame component declares,
|
|
247
|
+
* spelled as an author writes them, are kept.
|
|
248
|
+
*/
|
|
249
|
+
orphanLayerProps(el, frame) {
|
|
250
|
+
const [path, layer] = (el.getAttribute(ORPHAN_LAYER) ?? "").split("#");
|
|
251
|
+
if (!path || !layer) return {};
|
|
252
|
+
const active = {};
|
|
253
|
+
const own = this.grammar.component(path);
|
|
254
|
+
for (const cls of classesOf(el)) Object.assign(active, own?.conditions.get(cls));
|
|
255
|
+
const props = {};
|
|
256
|
+
const styles = Component.get(this.options.config, path)?.styles ?? {};
|
|
257
|
+
for (const rule of Object.values(styles)) {
|
|
258
|
+
const when = rule?.when ?? {};
|
|
259
|
+
if (!Object.entries(when).every(([key, value]) => active[key] === value)) continue;
|
|
260
|
+
for (const [key, value] of Object.entries(rule.layers?.[layer] ?? {})) {
|
|
261
|
+
if (!(key in frame.declared)) continue;
|
|
262
|
+
const authored = authoredValue(this.options.config, key, value);
|
|
263
|
+
if (typeof authored === "string" || typeof authored === "boolean") props[key] = authored;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return props;
|
|
267
|
+
}
|
|
268
|
+
/** The props that make `component` an artboard: what its content is placed against and clipped to. */
|
|
269
|
+
artboardProps(component) {
|
|
270
|
+
const props = {};
|
|
271
|
+
const grammar = this.grammar.component(component);
|
|
272
|
+
if (grammar) this.propsFromDeclarations(ARTBOARD, grammar, props);
|
|
273
|
+
return props;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* What `el`'s render placed outside its stamped slot layers and claimed layers, walking down to
|
|
277
|
+
* the first thing on each branch that is a piece of its own: a nested component, a glyph (unless
|
|
278
|
+
* `el` draws its own), a field it does not read, or an unmarked element with an inline style.
|
|
279
|
+
*/
|
|
280
|
+
looseContent({ el, claimed, takesGlyphs, takesFields }) {
|
|
281
|
+
const out = [];
|
|
282
|
+
const walk = (node) => {
|
|
283
|
+
for (let i = 0; i < node.childNodes.length; i += 1) {
|
|
284
|
+
if (this.childCap > 0 && out.length >= this.childCap) return;
|
|
285
|
+
const child = node.childNodes[i];
|
|
286
|
+
if (!isElement(child) || !visible(child) || claimed.has(child)) continue;
|
|
287
|
+
if (child.hasAttribute("data-uds-slot")) continue;
|
|
288
|
+
const tag = child.tagName.toLowerCase();
|
|
289
|
+
if (child.hasAttribute("data-uds-component") && child.getAttribute("data-uds-layer") === "root") out.push(...this.convert(child));
|
|
290
|
+
else if (tag === "svg") {
|
|
291
|
+
if (takesGlyphs) out.push(...this.convert(child));
|
|
292
|
+
} else if (isField(child)) {
|
|
293
|
+
if (takesFields) out.push(...this.field(child));
|
|
294
|
+
} else if (framesItself(child)) out.push(...this.frame(child));
|
|
295
|
+
else walk(child);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
walk(el);
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
/** A form field no component reads, placed as the text it shows through the canvas's text role. */
|
|
302
|
+
field(field) {
|
|
303
|
+
const text = fieldText(field);
|
|
304
|
+
const role = canvasRole(this.options.config, "text");
|
|
305
|
+
if (!text || !role) return [];
|
|
306
|
+
const props = { ...role.defaultProps };
|
|
307
|
+
const grammar = this.grammar.component(role.component);
|
|
308
|
+
if (grammar) {
|
|
309
|
+
if (field.tagName.toLowerCase() === "input") this.propsFromDeclarations(INPUT_LINE, grammar, props);
|
|
310
|
+
this.propsFromInlineStyle(field, grammar, props);
|
|
311
|
+
}
|
|
312
|
+
const key = this.key(role.component);
|
|
313
|
+
this.elements[key] = {
|
|
314
|
+
type: `component:${role.component}`,
|
|
315
|
+
props: {
|
|
316
|
+
...props,
|
|
317
|
+
children: text
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
return [key];
|
|
321
|
+
}
|
|
177
322
|
/** A bare `<svg>` placed as the component its library renders it with. */
|
|
178
323
|
icon(svg) {
|
|
179
324
|
const answer = this.options.icons?.resolve(svgOf(svg));
|
|
@@ -251,22 +396,40 @@ var Converter = class {
|
|
|
251
396
|
if (text) props[prop] = text;
|
|
252
397
|
}
|
|
253
398
|
}
|
|
399
|
+
for (const [prop, declaration] of Object.entries(declared)) {
|
|
400
|
+
if (props[prop] !== void 0 || slots[prop]) continue;
|
|
401
|
+
const route = declaration;
|
|
402
|
+
if (route?.type !== "forward" || !isRef(route.target)) continue;
|
|
403
|
+
const into = intoName(route.into);
|
|
404
|
+
if (into === "children") continue;
|
|
405
|
+
const layer = layerOf(el, path, pathOf(route.target.__ref));
|
|
406
|
+
if (!layer || !visible(layer) || hiddenBetween(layer, el)) continue;
|
|
407
|
+
const svg = layer.tagName.toLowerCase() === "svg" ? layer : layer.querySelector("svg");
|
|
408
|
+
const drawn = asArray(layer.querySelectorAll(ROOT_LAYER)).find((inner) => nearestRoot(inner) === el);
|
|
409
|
+
const glyph = svg ? this.options.icons?.resolve(svgOf(svg))?.props[into] : drawn && stampedProps(drawn)[into];
|
|
410
|
+
if (glyph === void 0) continue;
|
|
411
|
+
props[prop] = glyph;
|
|
412
|
+
claimed.add(layer);
|
|
413
|
+
}
|
|
254
414
|
if ("value" in declared && props.value === void 0) {
|
|
255
415
|
const field = fieldValue(el, declared.value);
|
|
256
416
|
if (field !== void 0) props.value = field;
|
|
257
417
|
}
|
|
258
|
-
const loose =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
418
|
+
const loose = this.looseContent({
|
|
419
|
+
el,
|
|
420
|
+
claimed,
|
|
421
|
+
takesGlyphs: !renders,
|
|
422
|
+
takesFields: !("value" in declared)
|
|
423
|
+
});
|
|
424
|
+
const filled = new Set(Object.keys(slots));
|
|
425
|
+
for (const nested of loose) {
|
|
426
|
+
const slot = grammar ? looseSlot({
|
|
427
|
+
grammar,
|
|
428
|
+
filled,
|
|
429
|
+
type: this.elements[nested].type
|
|
430
|
+
}) : "children";
|
|
431
|
+
if (slot) slots[slot] = [...slots[slot] ?? [], nested];
|
|
268
432
|
}
|
|
269
|
-
if (loose.length > 0 && !slots.children) slots.children = loose;
|
|
270
433
|
if (Object.keys(slots).length > 0) element.slots = slots;
|
|
271
434
|
const aria = el.getAttribute("aria-label");
|
|
272
435
|
if (aria) {
|
|
@@ -323,28 +486,32 @@ var Converter = class {
|
|
|
323
486
|
* The element's inline styles as props: a component's own `style` is the one place a size the
|
|
324
487
|
* classes do not carry lives, so `width: 300px` becomes the declared width prop with `[300px]`,
|
|
325
488
|
* and `column-gap: var(--uds-spacing-2)` becomes the gap leaf `2`. A value with no leaf lands as
|
|
326
|
-
* an off-scale `[value]` only on a property that takes one. Rendering plumbing (
|
|
327
|
-
*
|
|
489
|
+
* an off-scale `[value]` only on a property that takes one. Rendering plumbing (box sizing,
|
|
490
|
+
* transitions, browser defaults) is not a prop and is skipped.
|
|
328
491
|
*/
|
|
329
492
|
propsFromInlineStyle(el, grammar, props) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
let value = normalizeCssValue(declaration.slice(at + 1));
|
|
493
|
+
this.propsFromDeclarations(inlineDeclarations(el), grammar, props);
|
|
494
|
+
}
|
|
495
|
+
/** {@link propsFromInlineStyle} over declarations the caller supplies. */
|
|
496
|
+
propsFromDeclarations(declarations, grammar, props) {
|
|
497
|
+
for (const [property, raw] of declarations) {
|
|
498
|
+
let value = normalizeCssValue(raw);
|
|
337
499
|
if (!value || SKIP_INLINE_PROPERTY.has(property) || SKIP_INLINE_VALUE.has(value)) continue;
|
|
338
500
|
if (property === "position" && value === "fixed") value = "absolute";
|
|
339
501
|
if (VIEWPORT_VALUE.test(value) && !WHOLE_VIEWPORT.test(value)) value = frameUnits(value, this.options.width, this.options.height);
|
|
340
502
|
for (const option of this.grammar.stylePropsFor(property)) {
|
|
341
503
|
const prop = grammar.propForStyleProperty.get(option.path);
|
|
342
504
|
if (!prop) continue;
|
|
505
|
+
const css = option.negates ? negated(value) : value;
|
|
506
|
+
if (css === void 0) continue;
|
|
343
507
|
let next;
|
|
344
|
-
const leaf =
|
|
345
|
-
|
|
508
|
+
const leaf = equivalents({
|
|
509
|
+
property,
|
|
510
|
+
value: css
|
|
511
|
+
}).map((spelling) => option.leafFor(spelling)).find((found) => found !== void 0);
|
|
512
|
+
if (WHOLE_VIEWPORT.test(css)) next = option.fullLeaf;
|
|
346
513
|
else if (leaf !== void 0) next = leaf === "" ? true : leaf;
|
|
347
|
-
else if (option.arbitrary) next = `[${
|
|
514
|
+
else if (option.arbitrary) next = `[${css.replaceAll(" ", "_")}]`;
|
|
348
515
|
if (next === void 0) continue;
|
|
349
516
|
props[prop] = next;
|
|
350
517
|
break;
|
|
@@ -352,6 +519,107 @@ var Converter = class {
|
|
|
352
519
|
}
|
|
353
520
|
}
|
|
354
521
|
};
|
|
522
|
+
/**
|
|
523
|
+
* The prop a nested component the render placed outside every stamped layer belongs to: `children`
|
|
524
|
+
* first, then the other slots in declaration order, taking the first that is not already filled and
|
|
525
|
+
* accepts the component's type. `children` also holds it when it forwards into a layer's children,
|
|
526
|
+
* unless that route only takes text.
|
|
527
|
+
*/
|
|
528
|
+
function looseSlot({ grammar, filled, type }) {
|
|
529
|
+
const names = Object.keys(grammar.declared);
|
|
530
|
+
const ordered = names.includes("children") ? ["children", ...names.filter((name) => name !== "children")] : names;
|
|
531
|
+
const path = pathOf(type);
|
|
532
|
+
for (const name of ordered) {
|
|
533
|
+
if (filled.has(name)) continue;
|
|
534
|
+
const declaration = grammar.declared[name];
|
|
535
|
+
const route = grammar.layerProps.get(name);
|
|
536
|
+
if (!(declaration?.type === "slot" || name === "children" && route?.content === true && route.text !== true && (route.into ?? "children") === "children")) continue;
|
|
537
|
+
const accepts = Array.isArray(declaration?.accepts) ? declaration.accepts : [];
|
|
538
|
+
if (accepts.length > 0 && !accepts.some((ref) => refPath(ref) === path)) continue;
|
|
539
|
+
return name;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function refPath(ref) {
|
|
543
|
+
if (typeof ref === "string") return pathOf(ref);
|
|
544
|
+
if (ref && typeof ref === "object" && "__ref" in ref) return pathOf(ref.__ref);
|
|
545
|
+
}
|
|
546
|
+
/** How an artboard holds a page: the containing block for its positioned chrome, clipped to its box. */
|
|
547
|
+
const ARTBOARD = [["position", "relative"], ["overflow", "hidden"]];
|
|
548
|
+
/** How a single-line input lays out its text: centred across its height. */
|
|
549
|
+
const INPUT_LINE = [["display", "flex"], ["align-items", "center"]];
|
|
550
|
+
function isField(el) {
|
|
551
|
+
const tag = el.tagName.toLowerCase();
|
|
552
|
+
return tag === "input" || tag === "textarea";
|
|
553
|
+
}
|
|
554
|
+
/** The text a field shows: a text input's `value`, a textarea's content, else its placeholder. */
|
|
555
|
+
function fieldText(field) {
|
|
556
|
+
const placeholder = collapse(field.getAttribute("placeholder") ?? "");
|
|
557
|
+
if (field.tagName.toLowerCase() === "textarea") return collapse(field.textContent ?? "") || placeholder;
|
|
558
|
+
const type = (field.getAttribute("type") ?? "text").toLowerCase();
|
|
559
|
+
if (NON_TEXT_INPUT.has(type)) return "";
|
|
560
|
+
return collapse(field.getAttribute("value") ?? "") || placeholder;
|
|
561
|
+
}
|
|
562
|
+
/** The component a canvas role places with (`text`, `frame`), and the props it places it with. */
|
|
563
|
+
function canvasRole(config, name) {
|
|
564
|
+
const entries = (config.resolve("canvas-role", name)?.toJSON())?.entries ?? [];
|
|
565
|
+
const entry = entries.find((candidate) => candidate.default) ?? entries[0];
|
|
566
|
+
if (!entry || !isRef(entry.component)) return void 0;
|
|
567
|
+
return {
|
|
568
|
+
component: pathOf(entry.component.__ref),
|
|
569
|
+
defaultProps: entry.defaultProps ?? {}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/** An unmarked element the page gave an inline style, or an orphaned layer: kept as a frame. */
|
|
573
|
+
function framesItself(el) {
|
|
574
|
+
if (el.hasAttribute("data-uds-component")) return false;
|
|
575
|
+
if (inlineDeclarations(el).some(([property, value]) => property === "display" && value.trim() === "contents")) return false;
|
|
576
|
+
if (el.hasAttribute(ORPHAN_LAYER)) return true;
|
|
577
|
+
return /\S/.test((el.getAttribute("style") ?? "").replace(/;/g, ""));
|
|
578
|
+
}
|
|
579
|
+
const NOTHING = new Set([
|
|
580
|
+
"none",
|
|
581
|
+
"transparent",
|
|
582
|
+
"0",
|
|
583
|
+
"0px",
|
|
584
|
+
"initial"
|
|
585
|
+
]);
|
|
586
|
+
/** Whether an element's inline style draws something of its own: a fill, a border or a shadow. */
|
|
587
|
+
function paintsItself(el) {
|
|
588
|
+
return inlineDeclarations(el).some(([property, value]) => {
|
|
589
|
+
const drawn = !NOTHING.has(value.trim().toLowerCase());
|
|
590
|
+
if (property === "background-color" || property === "background-image") return drawn;
|
|
591
|
+
return drawn && (property === "box-shadow" || property === "border" || /^border(-[a-z]+)?-width$/.test(property));
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/** An element's inline declarations as `[property, value]` pairs. */
|
|
595
|
+
function inlineDeclarations(el) {
|
|
596
|
+
const out = [];
|
|
597
|
+
for (const declaration of (el.getAttribute("style") ?? "").split(";")) {
|
|
598
|
+
const at = declaration.indexOf(":");
|
|
599
|
+
if (at < 0) continue;
|
|
600
|
+
const property = declaration.slice(0, at).trim().toLowerCase();
|
|
601
|
+
if (property === "transform") continue;
|
|
602
|
+
const value = declaration.slice(at + 1).trim();
|
|
603
|
+
if (property === "background") {
|
|
604
|
+
if (!/url\(|gradient\(/.test(value)) out.push(["background-color", value]);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
out.push([property, value]);
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
/** A single negative length or number without its sign; `undefined` for anything else. */
|
|
612
|
+
function negated(value) {
|
|
613
|
+
return /^-\d*\.?\d+[a-z%]*$/.test(value) ? value.slice(1) : void 0;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A value with the other spellings that paint the same, for matching against a leaf: a grid line
|
|
617
|
+
* that only spans (`span 6`) is the same six tracks as `span 6 / span 6`.
|
|
618
|
+
*/
|
|
619
|
+
function equivalents({ property, value }) {
|
|
620
|
+
if ((property === "grid-column" || property === "grid-row") && /^span \d+$/.test(value)) return [value, `${value} / ${value}`];
|
|
621
|
+
return [value];
|
|
622
|
+
}
|
|
355
623
|
function stampedProps(el) {
|
|
356
624
|
const raw = el.getAttribute("data-uds-props");
|
|
357
625
|
if (!raw) return {};
|
|
@@ -386,7 +654,6 @@ function visible(el) {
|
|
|
386
654
|
if (/display:\s*none/.test(style) || /visibility:\s*hidden/.test(style)) return false;
|
|
387
655
|
if (/(^|;)\s*opacity:\s*0+(\.0+)?\s*(;|$)/.test(style)) return false;
|
|
388
656
|
if (el.hasAttribute("hidden")) return false;
|
|
389
|
-
if (el.getAttribute("aria-hidden") === "true" && el.tagName.toLowerCase() !== "svg") return false;
|
|
390
657
|
if (el.getAttribute("role") === "tooltip") return false;
|
|
391
658
|
if (el.getAttribute("data-uds-props")?.includes("\"hidden\":true")) return false;
|
|
392
659
|
return true;
|
|
@@ -419,15 +686,6 @@ function hiddenBetween(el, root) {
|
|
|
419
686
|
}
|
|
420
687
|
return false;
|
|
421
688
|
}
|
|
422
|
-
/** Whether `el` sits inside one of `layers`, below `root`. */
|
|
423
|
-
function withinAny(el, layers, root) {
|
|
424
|
-
let node = el.parentElement;
|
|
425
|
-
while (node && node !== root) {
|
|
426
|
-
if (layers.has(node)) return true;
|
|
427
|
-
node = node.parentElement;
|
|
428
|
-
}
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
431
689
|
/** The component's own layer named `layer`, when the DOM marks one under this root. */
|
|
432
690
|
function layerOf(root, path, layer) {
|
|
433
691
|
for (const candidate of asArray(root.querySelectorAll(`[data-uds-component="${path}"][data-uds-layer="${layer}"]`))) if (nearestRoot(candidate) === root) return candidate;
|
|
@@ -35,6 +35,7 @@ interface ComponentGrammar {
|
|
|
35
35
|
layer: string;
|
|
36
36
|
content: boolean;
|
|
37
37
|
into?: string;
|
|
38
|
+
text?: boolean;
|
|
38
39
|
}>;
|
|
39
40
|
/** Layer classes → layer name. */
|
|
40
41
|
readonly layers: ReadonlyMap<string, string>;
|
|
@@ -44,6 +45,8 @@ interface CssPropertyMatch {
|
|
|
44
45
|
readonly path: string;
|
|
45
46
|
/** Whether the property takes an off-scale `[value]`. */
|
|
46
47
|
readonly arbitrary: boolean;
|
|
48
|
+
/** The property writes the negation of the prop's value (`offsetLeft` → `margin-left: -<value>`). */
|
|
49
|
+
readonly negates: boolean;
|
|
47
50
|
/** The leaf whose value is this CSS value, as `var(--…)` or as the literal it resolves to. */
|
|
48
51
|
leafFor(cssValue: string): string | undefined;
|
|
49
52
|
/** The leaf that fills the container (`100%`), which is what a viewport unit means inside a frame. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRef, pathOf } from "../framework/utils/refs.js";
|
|
2
2
|
import { className, cssVarRef, resolveComponentProps, resolveTokenValue, stylePropEntries } from "../framework/projections.js";
|
|
3
|
+
import { intoName, routesContent, routesIntoText } from "../framework/prop-surface.js";
|
|
3
4
|
import { componentLayerClass, componentRuleClasses, stylePropClasses } from "../framework/class-names.js";
|
|
4
|
-
import { intoName, routesContent } from "../framework/prop-surface.js";
|
|
5
5
|
//#region src/capture/class-grammar.ts
|
|
6
6
|
const VIEWPORT_UNIT = /\d(vh|vw|dvh|dvw|svh|svw|lvh|lvw|vmin|vmax)\b/;
|
|
7
7
|
/** `0px` → `0`, collapsed spaces, lowercase keywords; what two spellings of one value share. */
|
|
@@ -36,9 +36,10 @@ function classGrammar(config) {
|
|
|
36
36
|
leafValues.set(path, known);
|
|
37
37
|
return known;
|
|
38
38
|
};
|
|
39
|
-
const matchFor = (path, arbitrary) => ({
|
|
39
|
+
const matchFor = ({ path, arbitrary, negates }) => ({
|
|
40
40
|
path,
|
|
41
41
|
arbitrary,
|
|
42
|
+
negates,
|
|
42
43
|
leafFor(cssValue) {
|
|
43
44
|
const wanted = normalizeCssValue(cssValue);
|
|
44
45
|
for (const [leaf, spellings] of valuesOf(path)) if (spellings.includes(wanted)) return leaf;
|
|
@@ -52,13 +53,25 @@ function classGrammar(config) {
|
|
|
52
53
|
const properties = body.properties ?? [];
|
|
53
54
|
for (const property of properties) {
|
|
54
55
|
const list = byCss.get(property) ?? [];
|
|
55
|
-
list.push(matchFor(
|
|
56
|
+
list.push(matchFor({
|
|
57
|
+
path: entity.path,
|
|
58
|
+
arbitrary: body.arbitrary === true,
|
|
59
|
+
negates: body.negates === true
|
|
60
|
+
}));
|
|
56
61
|
byCss.set(property, list);
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
const propertyCount = /* @__PURE__ */ new Map();
|
|
60
|
-
|
|
61
|
-
for (const
|
|
65
|
+
const classPrefix = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const entity of config.list("style-property")) {
|
|
67
|
+
const body = entity.toJSON();
|
|
68
|
+
propertyCount.set(entity.path, (body.properties ?? []).length);
|
|
69
|
+
classPrefix.set(entity.path, body.classPrefix);
|
|
70
|
+
}
|
|
71
|
+
for (const [property, list] of byCss) {
|
|
72
|
+
const whole = (path) => classPrefix.get(path) === property ? 0 : 1;
|
|
73
|
+
list.sort((a, b) => (propertyCount.get(a.path) ?? 0) - (propertyCount.get(b.path) ?? 0) || whole(a.path) - whole(b.path));
|
|
74
|
+
}
|
|
62
75
|
const components = /* @__PURE__ */ new Map();
|
|
63
76
|
const componentGrammar = (path) => {
|
|
64
77
|
const component = config.resolve("component", path);
|
|
@@ -96,7 +109,8 @@ function classGrammar(config) {
|
|
|
96
109
|
if (isRef(target)) layerProps.set(prop, {
|
|
97
110
|
layer: pathOf(target.__ref),
|
|
98
111
|
content: true,
|
|
99
|
-
into: intoName(declaration.into)
|
|
112
|
+
into: intoName(declaration.into),
|
|
113
|
+
text: routesIntoText(config, body.layers, declaration)
|
|
100
114
|
});
|
|
101
115
|
} else if (declaration.type === "string" && body.layers?.[prop] !== void 0 && prop !== "root") layerProps.set(prop, {
|
|
102
116
|
layer: prop,
|
|
@@ -9,10 +9,10 @@ import { IconLibrary } from "../entities/system/icon-library.js";
|
|
|
9
9
|
*
|
|
10
10
|
* Everything about a library is read off its group node: the package its glyphs come from
|
|
11
11
|
* (`metadata.from`), the named sizes (`sizes`) and the weights it ships (`variants`). The component
|
|
12
|
-
* that renders them
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* that renders them, and the prop that picks the glyph, are the ones `glyphComponentOf` answers; the
|
|
13
|
+
* other props are found by what they reference: the one bound to `#variants` is the weight, and the
|
|
14
|
+
* one whose values are the library's size names is the size. The host reads the glyph files, since a
|
|
15
|
+
* package's layout on disk is the host's to know.
|
|
16
16
|
*/
|
|
17
17
|
/** Where a library's glyph files come from: the catalogue package its metadata names, and its format. */
|
|
18
18
|
interface GlyphSource {
|
|
@@ -33,18 +33,15 @@ function indexLibrary(config, library, glyphs) {
|
|
|
33
33
|
if (!drawer) return void 0;
|
|
34
34
|
const component = drawer.path;
|
|
35
35
|
const declared = resolveComponentProps(config, component);
|
|
36
|
-
let nameProp;
|
|
37
36
|
let variantProp;
|
|
38
37
|
let sizeProp;
|
|
39
38
|
const sizeNames = Object.keys(library.sizes);
|
|
39
|
+
const nameProp = drawer.prop;
|
|
40
40
|
for (const [prop, decl] of Object.entries(declared)) {
|
|
41
|
-
if (decl?.type !== "variant") continue;
|
|
42
|
-
|
|
43
|
-
if (ref === `icon:${library.path}`) nameProp = prop;
|
|
44
|
-
else if (ref === `icon:${library.path}#variants`) variantProp = prop;
|
|
41
|
+
if (prop === nameProp || decl?.type !== "variant") continue;
|
|
42
|
+
if (refOf(decl) === `icon:${library.path}#variants`) variantProp = prop;
|
|
45
43
|
else if (sizeNames.length > 0 && sizeNames.every((size) => listedValues(decl).includes(size))) sizeProp = prop;
|
|
46
44
|
}
|
|
47
|
-
if (!nameProp) return void 0;
|
|
48
45
|
const defaultVariant = library.defaultVariant ?? library.variants[0];
|
|
49
46
|
const ordered = [...glyphs].sort((a, b) => {
|
|
50
47
|
const rank = (g) => g.variant === defaultVariant ? 0 : 1;
|
package/dist/capture/node-ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { CapturedNode } from "./captured-tree.js";
|
|
2
|
-
import { pathOf } from "../framework/utils/refs.js";
|
|
3
1
|
import { rejectData } from "../framework/rejection.js";
|
|
2
|
+
import { pathOf } from "../framework/utils/refs.js";
|
|
4
3
|
import { pathInput } from "../framework/schemas.js";
|
|
4
|
+
import { CapturedNode } from "./captured-tree.js";
|
|
5
5
|
import { specFromCapture } from "./capture-to-spec.js";
|
|
6
6
|
import { specToNodePatches } from "./node-patches.js";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -29,7 +29,7 @@ const fromHtmlData = z.object({
|
|
|
29
29
|
width: z.number().min(100).max(4e3).optional(),
|
|
30
30
|
/** The captured viewport's height, what `100vh` measures inside the frame. */
|
|
31
31
|
height: z.number().min(100).max(8e3).optional(),
|
|
32
|
-
/** Wraps the capture's top-level components;
|
|
32
|
+
/** Wraps the capture's top-level components; several are otherwise wrapped in the canvas frame. */
|
|
33
33
|
frame: frameSchema.optional(),
|
|
34
34
|
/** Convert only the subtree this selector finds. */
|
|
35
35
|
selector: z.string().optional(),
|