@yahoo/uds-create-config 5.75.0 → 5.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,10 @@
1
- import { isRef, pathOf } from "../framework/utils/refs.js";
1
+ import { isRef, kindOf, pathOf } from "../framework/utils/refs.js";
2
2
  import { StyleProperty } from "../entities/system/StyleProperty.js";
3
- import { LAYER_PROPS, authoredValue } from "../framework/projections.js";
3
+ import { LAYER_PROPS, authoredValue, glyphComponentOf } from "../framework/projections.js";
4
4
  import { intoName } from "../framework/prop-surface.js";
5
+ import { arbitraryClassName, stylePropClassName } from "../framework/class-names.js";
5
6
  import { Component } from "../entities/system/Component.js";
7
+ import { iconLibraries } from "../entities/system/icon-library.js";
6
8
  import { elementFromCaptured, isCapturedElement } from "./captured-tree.js";
7
9
  import { classGrammar, normalizeCssValue } from "./class-grammar.js";
8
10
  //#region src/capture/capture-to-spec.ts
@@ -114,6 +116,15 @@ function specFromCapture(options) {
114
116
  * value, `<width>x<height>`, is the layout size its content keeps.
115
117
  */
116
118
  const FOLD = "data-uds-fold";
119
+ /** The page capture's mark on the empty box it left where an embed was. */
120
+ const EMBED_SPACE_ATTR = "data-uds-embed";
121
+ /**
122
+ * The styles the page's stylesheets gave an unmarked element, as the browser computed them, which the
123
+ * capture writes beside its own `style`. They are what a wrapper styled by anything the system does not
124
+ * name (an app's Tailwind, a CSS module) drew with, and rank below both the element's own inline style
125
+ * and the system's classes on it, which name the value rather than the pixels it came to.
126
+ */
127
+ const COMPUTED_STYLE_ATTR = "data-uds-computed";
117
128
  /** Marks a layer whose component's root is not in the DOM with `<component>#<layer>`. */
118
129
  const ORPHAN_LAYER = "data-uds-orphan-layer";
119
130
  /**
@@ -183,10 +194,38 @@ var Converter = class {
183
194
  counter = 0;
184
195
  childCap;
185
196
  ruled = /* @__PURE__ */ new Map();
197
+ /** Each component that draws a glyph by name → the prop that names it. */
198
+ glyphDrawers;
199
+ /** Each breakpoint's class prefix (`lg`) → whether it applied at the capture's width, and its rank. */
200
+ breakpoints;
201
+ /** Each modifier's class prefix (`md`) → the prop key its bag is written under (`_md`). */
202
+ modifierKeys;
186
203
  constructor(options, grammar) {
187
204
  this.options = options;
188
205
  this.grammar = grammar;
189
206
  this.childCap = options.childCap ?? DEFAULT_CHILD_CAP;
207
+ const drawers = /* @__PURE__ */ new Map();
208
+ for (const library of iconLibraries(options.config)) {
209
+ const drawer = glyphComponentOf(options.config, library.path);
210
+ if (drawer) drawers.set(drawer.path, drawer.prop);
211
+ }
212
+ this.glyphDrawers = drawers;
213
+ const width = options.width ?? DEFAULT_CAPTURE_WIDTH;
214
+ const breakpoints = /* @__PURE__ */ new Map();
215
+ for (const modifier of options.config.list("modifier")) {
216
+ const { type, value } = modifier.toJSON();
217
+ if (type !== "media-query" || typeof value !== "string") continue;
218
+ const min = /min-width:\s*(\d+(?:\.\d+)?)px/.exec(value);
219
+ const max = /max-width:\s*(\d+(?:\.\d+)?)px/.exec(value);
220
+ if (!min && !max) continue;
221
+ const active = (!min || width >= Number(min[1])) && (!max || width <= Number(max[1]));
222
+ breakpoints.set(modifier.leaf, {
223
+ active,
224
+ rank: min ? Number(min[1]) : 0
225
+ });
226
+ }
227
+ this.breakpoints = breakpoints;
228
+ this.modifierKeys = new Map(options.config.list("modifier").map((modifier) => [modifier.leaf, `_${modifier.leaf}`]));
190
229
  }
191
230
  convertTop(el) {
192
231
  return this.convert(el);
@@ -200,15 +239,25 @@ var Converter = class {
200
239
  }
201
240
  return out;
202
241
  }
203
- /** The components under `layer`, skipping every other layer `path` marks inside it. */
204
- convertOutsideLayers(layer, path) {
242
+ /**
243
+ * The components under `layer`, skipping every other layer `path` marks inside it. With
244
+ * `callerMarkup`, the layer is one a slot of elements lands on, so the unmarked markup in it is
245
+ * the caller's, kept as the plain tags it is, rather than the render's own.
246
+ */
247
+ convertOutsideLayers(layer, path, callerMarkup = false) {
205
248
  const out = [];
249
+ const mixed = callerMarkup && Array.from(layer.childNodes).some((node) => isElement(node) && visible(node));
206
250
  for (let i = 0; i < layer.childNodes.length; i += 1) {
207
251
  if (this.childCap > 0 && out.length >= this.childCap) break;
208
252
  const node = layer.childNodes[i];
209
- if (!isElement(node)) continue;
253
+ if (!isElement(node)) {
254
+ const text = (node.textContent ?? "").replace(/\s+/g, " ");
255
+ if (mixed && text.trim()) out.push(...this.plainTextRun(text, layer));
256
+ continue;
257
+ }
210
258
  if (node.getAttribute("data-uds-component") === path && node.getAttribute("data-uds-layer") !== "root") continue;
211
259
  if (node.tagName.toLowerCase() === "svg" || isImage(node) || node.hasAttribute("data-uds-component") && node.getAttribute("data-uds-layer") === "root") out.push(...this.convert(node));
260
+ else if (visible(node) && callerMarkup) out.push(...this.plain(node));
212
261
  else if (visible(node) && framesItself(node)) out.push(...this.frame(node));
213
262
  else if (visible(node)) out.push(...this.pinFolded(node, this.convertOutsideLayers(node, path)));
214
263
  }
@@ -222,10 +271,270 @@ var Converter = class {
222
271
  return key ? [key] : [];
223
272
  }
224
273
  if (el.tagName.toLowerCase() === "svg") return this.icon(el);
225
- if (isImage(el)) return this.image(el);
226
- if (isField(el)) return this.field(el);
227
- if (framesItself(el)) return this.frame(el);
228
- return this.pinFolded(el, this.convertChildren(el));
274
+ if (el.hasAttribute(ORPHAN_LAYER)) return this.frame(el);
275
+ return this.plain(el);
276
+ }
277
+ /**
278
+ * What an element inside a component holds, by the rule for a component's render: a nested
279
+ * component is itself, and an unmarked element folds into what it holds, or is a frame when it
280
+ * places itself, since it may be the render's own markup rather than the page's.
281
+ */
282
+ convertInside(el) {
283
+ const out = [];
284
+ for (let i = 0; i < el.childNodes.length; i += 1) {
285
+ if (this.childCap > 0 && out.length >= this.childCap) break;
286
+ const node = el.childNodes[i];
287
+ if (!isElement(node) || !visible(node)) continue;
288
+ if (node.tagName.toLowerCase() === "svg" || node.hasAttribute("data-uds-component") && node.getAttribute("data-uds-layer") === "root") out.push(...this.convert(node));
289
+ else if (isImage(node)) out.push(...this.image(node));
290
+ else if (isField(node)) out.push(...this.field(node));
291
+ else if (framesItself(node)) out.push(...this.frame(node));
292
+ else out.push(...this.pinFolded(node, this.convertInside(node)));
293
+ }
294
+ return out;
295
+ }
296
+ /**
297
+ * An unmarked element as the plain tag it is: an `img` keeps its source, a link its `href`, a field
298
+ * its type and placeholder, and its text stays inside it. Its styling is stored as classes, the way
299
+ * the canvas paints and the design panel edits a plain element: the classes it wears that the system
300
+ * names, then its own inline style and the styles its stylesheets gave it written as the system's
301
+ * classes (`16px` of gap is `gap-4`, an off-scale `13px` is `gap-[13px]`). What no style prop can
302
+ * hold stays as an inline style. A `div` or `span` that carries nothing only groups its children and
303
+ * folds away, as a wrapper the capture marked foldable does.
304
+ */
305
+ plain(el, convertKids = () => this.plainChildren(el)) {
306
+ const tag = el.tagName.toLowerCase();
307
+ if (FORBIDDEN_TAGS.has(tag)) return [];
308
+ if (el.hasAttribute(FOLD)) return this.pinFolded(el, convertKids());
309
+ if ([...inlineDeclarations(el), ...declarationsOf(el.getAttribute("data-uds-computed") ?? "")].find(([property]) => property === "display")?.[1].trim() === "contents") return convertKids();
310
+ const { className, style, paints } = this.plainStyle(el);
311
+ const attrs = plainAttributes(el);
312
+ if (tag === "img" && !attrs.src) return [];
313
+ const children = VOID_TAGS.has(tag) ? [] : convertKids();
314
+ const text = children.length === 0 ? plainText(el) : "";
315
+ if (children.length === 0 && !text && !VOID_TAGS.has(tag) && !paints && !el.hasAttribute(EMBED_SPACE_ATTR)) return [];
316
+ if (!(className !== "" || Object.keys(style).length > 0 || Object.keys(attrs).length > 0) && GROUPING_TAGS.has(tag)) {
317
+ if (text) return this.plainTextRun(text);
318
+ return children;
319
+ }
320
+ const props = { ...attrs };
321
+ if (className) props.className = className;
322
+ if (Object.keys(style).length > 0) props.style = style;
323
+ if (text) props.children = text;
324
+ const key = this.key(tag);
325
+ this.elements[key] = {
326
+ type: tag,
327
+ props,
328
+ ...children.length > 0 ? { children } : {}
329
+ };
330
+ return [key];
331
+ }
332
+ /**
333
+ * An inline SVG as itself: the `svg` and every shape in it, each with its attributes under the
334
+ * names React takes them by. Definitions it references (a clip path, a gradient) are left out with
335
+ * the references to them, since an id the capture copied may name nothing on the canvas.
336
+ */
337
+ plainSvg(svg) {
338
+ if (holdsMoreThan(svg, SVG_SHAPE_LIMIT)) {
339
+ const covered = /* @__PURE__ */ new Set();
340
+ const size = (attr) => {
341
+ const value = svg.getAttribute(attr);
342
+ return value && /^\d+(?:\.\d+)?$/.test(value) ? this.classForDeclaration(attr, `${value}px`, covered) : void 0;
343
+ };
344
+ const className = [size("width"), size("height")].filter((cls) => !!cls && cls !== "covered").join(" ");
345
+ const key = this.key("div");
346
+ this.elements[key] = {
347
+ type: "div",
348
+ props: className ? { className } : {}
349
+ };
350
+ return key;
351
+ }
352
+ const build = (el) => {
353
+ const tag = el.tagName.toLowerCase();
354
+ const props = { [VERBATIM_ATTRIBUTES]: true };
355
+ const styled = [];
356
+ for (const { name, value } of el.attributes) {
357
+ if (SVG_SKIPPED_ATTRIBUTES.test(name)) continue;
358
+ if (name === "class" || name === "style") continue;
359
+ if (name === "width" || name === "height") continue;
360
+ const prop = svgAttributeName(name);
361
+ if (this.options.config.resolve("style-property", prop)) {
362
+ styled.push([name, value]);
363
+ continue;
364
+ }
365
+ props[prop] = value;
366
+ }
367
+ const { className, style } = this.plainStyle(el, styled);
368
+ if (className) props.className = className;
369
+ if (Object.keys(style).length > 0) props.style = style;
370
+ const children = [];
371
+ let text = "";
372
+ for (const node of el.childNodes) {
373
+ if (!isElement(node)) {
374
+ text += node.textContent ?? "";
375
+ continue;
376
+ }
377
+ if (SVG_SKIPPED_TAGS.has(node.tagName.toLowerCase())) continue;
378
+ children.push(build(node));
379
+ }
380
+ if (children.length === 0 && text.trim()) props.children = text.trim();
381
+ const key = this.key(SVG_TAGS.get(tag) ?? tag);
382
+ this.elements[key] = {
383
+ type: SVG_TAGS.get(tag) ?? tag,
384
+ props,
385
+ ...children.length > 0 ? { children } : {}
386
+ };
387
+ return key;
388
+ };
389
+ return build(svg);
390
+ }
391
+ /** A plain element's content: its elements, and each run of text between them as a `span`. */
392
+ plainChildren(el) {
393
+ if (!Array.from(el.childNodes).some((node) => isElement(node) && visible(node))) return [];
394
+ const out = [];
395
+ for (let i = 0; i < el.childNodes.length; i += 1) {
396
+ if (this.childCap > 0 && out.length >= this.childCap) break;
397
+ const node = el.childNodes[i];
398
+ if (isElement(node)) {
399
+ out.push(...this.convert(node));
400
+ continue;
401
+ }
402
+ const text = (node.textContent ?? "").replace(/\s+/g, " ");
403
+ if (text.trim()) out.push(...this.plainTextRun(text));
404
+ }
405
+ return out;
406
+ }
407
+ /**
408
+ * A run of text as a `span`. It keeps how its holder wraps (`white-space: nowrap` on a nav item),
409
+ * which the holder, a component that takes no such prop, may have no way to say itself.
410
+ */
411
+ plainTextRun(text, holder) {
412
+ const props = { children: text };
413
+ const wraps = declarationsOf(holder?.getAttribute("data-uds-computed") ?? "").find(([property]) => property === "white-space");
414
+ if (wraps) {
415
+ const cls = this.classForDeclaration("white-space", normalizeCssValue(wraps[1]), /* @__PURE__ */ new Set());
416
+ if (cls && cls !== "covered") props.className = cls;
417
+ else props.style = { whiteSpace: wraps[1] };
418
+ }
419
+ const key = this.key("span");
420
+ this.elements[key] = {
421
+ type: "span",
422
+ props
423
+ };
424
+ return [key];
425
+ }
426
+ /**
427
+ * A plain element's styling as the system's classes, and what none can hold as inline style. The
428
+ * page's own inline style painted over its classes, so it is read first and a class for a property it
429
+ * sets is dropped; the computed styles fill only what neither says.
430
+ */
431
+ plainStyle(el, attributes = []) {
432
+ const classes = [];
433
+ const style = {};
434
+ const covered = /* @__PURE__ */ new Set();
435
+ let paints = false;
436
+ const notePaint = (property) => {
437
+ if (PAINT_PROPERTY.test(property)) paints = true;
438
+ };
439
+ const written = /* @__PURE__ */ new Set();
440
+ const fromDeclarations = (declarations) => {
441
+ for (const [property, raw] of declarations.flatMap(gridAreaLines)) {
442
+ if (written.has(property) || written.has(LOGICAL_SIDES[property] ?? property)) continue;
443
+ let value = normalizeCssValue(raw);
444
+ const paintNone = value === "none" && (property === "fill" || property === "stroke");
445
+ if (!value || SKIP_INLINE_PROPERTY.has(property) || SKIP_INLINE_VALUE.has(value) && !paintNone) continue;
446
+ if (property === "position" && value === "fixed") value = "absolute";
447
+ if (VIEWPORT_VALUE.test(value) && !WHOLE_VIEWPORT.test(value)) value = frameUnits(value, this.options.width, this.options.height);
448
+ const cls = this.classForDeclaration(property, value, covered);
449
+ if (cls === "covered") continue;
450
+ notePaint(property);
451
+ if (cls) {
452
+ classes.push(cls);
453
+ continue;
454
+ }
455
+ let placed = false;
456
+ for (const [longhand, part] of gridLonghands({
457
+ property,
458
+ value
459
+ })) {
460
+ const longClass = this.classForDeclaration(longhand, part, covered);
461
+ if (longClass && longClass !== "covered") classes.push(longClass);
462
+ placed = placed || longClass !== void 0;
463
+ }
464
+ if (!placed && !(camelCase(property) in style)) style[camelCase(property)] = raw.trim();
465
+ }
466
+ };
467
+ fromDeclarations(inlineDeclarations(el));
468
+ fromDeclarations(attributes);
469
+ fromDeclarations(["width", "height"].flatMap((attr) => {
470
+ const value = el.getAttribute(attr);
471
+ return value && /^\d+(?:\.\d+)?$/.test(value) ? [[attr, `${value}px`]] : [];
472
+ }));
473
+ const read = classesOf(el).flatMap((cls) => {
474
+ const each = this.readModifiedClass(cls);
475
+ if (!each) return [];
476
+ if (each.modifiers.length === 0 && covered.has(each.style.path)) return [];
477
+ const { properties = [] } = this.options.config.resolve("style-property", each.style.path)?.toJSON() ?? {};
478
+ const atoms = atomsOf(properties);
479
+ if (atoms.size === 0) atoms.add(`prop:${each.style.path}`);
480
+ return [{
481
+ ...each,
482
+ properties,
483
+ atoms
484
+ }];
485
+ });
486
+ const winners = /* @__PURE__ */ new Map();
487
+ for (const each of read) for (const atom of each.atoms) {
488
+ const slot = `${each.modifiers.join(".")}|${atom}`;
489
+ const prior = winners.get(slot);
490
+ if (!prior || each.rank > prior.rank || each.rank === prior.rank && each.atoms.size < prior.atoms.size) winners.set(slot, each);
491
+ }
492
+ const painted = new Set(winners.values());
493
+ for (const each of read) {
494
+ if (!painted.has(each)) continue;
495
+ if (each.modifiers.length === 0) covered.add(each.style.path);
496
+ for (const property of each.properties) {
497
+ notePaint(property);
498
+ if (each.modifiers.length === 0) for (const longhand of longhandsOf(property)) written.add(longhand);
499
+ }
500
+ const owned = [...each.atoms].filter((atom) => winners.get(`${each.modifiers.join(".")}|${atom}`) === each);
501
+ classes.push(...owned.length < each.atoms.size ? this.narrowed(each, new Set(owned)) : [each.bare]);
502
+ }
503
+ fromDeclarations(declarationsOf(el.getAttribute("data-uds-computed") ?? ""));
504
+ return {
505
+ className: [...new Set(classes)].join(" "),
506
+ style,
507
+ paints
508
+ };
509
+ }
510
+ /**
511
+ * The system's class for one declaration: the class of the leaf whose value it is, or the bracket
512
+ * class of an off-scale value on a prop that takes one. `covered` when a prop for the property is
513
+ * already set, so the first source to set it wins; nothing when no prop writes the property.
514
+ */
515
+ classForDeclaration(property, value, covered) {
516
+ const logical = LOGICAL_SIDES[property];
517
+ const options = [...this.grammar.stylePropsFor(property), ...logical ? this.grammar.stylePropsFor(logical) : []];
518
+ for (const option of options) {
519
+ if (covered.has(option.path)) return "covered";
520
+ const css = option.negates ? negated(value) : value;
521
+ if (css === void 0) continue;
522
+ const leaf = equivalents({
523
+ property,
524
+ value: css
525
+ }).map((spelling) => option.leafFor(spelling)).find((found) => found !== void 0);
526
+ let cls;
527
+ if (WHOLE_VIEWPORT.test(css) && option.fullLeaf !== void 0) cls = stylePropClassName(this.options.config, option.path, option.fullLeaf);
528
+ else if (leaf !== void 0) cls = stylePropClassName(this.options.config, option.path, leaf);
529
+ else if (option.arbitrary) cls = arbitraryClassName({
530
+ config: this.options.config,
531
+ path: option.path,
532
+ value: `[${css.replaceAll(" ", "_")}]`
533
+ });
534
+ if (cls === void 0) continue;
535
+ covered.add(option.path);
536
+ return cls;
537
+ }
229
538
  }
230
539
  /**
231
540
  * The content of an unmarked wrapper the capture folded, with the wrapper's measured box written
@@ -236,6 +545,18 @@ var Converter = class {
236
545
  const size = el.getAttribute(FOLD)?.match(/^(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)$/);
237
546
  if (!size || keys.length !== 1 || el.hasAttribute("data-uds-component")) return keys;
238
547
  const element = this.elements[keys[0]];
548
+ if (kindOf(element.type) !== "component") {
549
+ const covered = /* @__PURE__ */ new Set();
550
+ const sized = [this.classForDeclaration("width", `${size[1]}px`, covered), this.classForDeclaration("height", `${size[2]}px`, covered)].filter((cls) => !!cls && cls !== "covered");
551
+ const own = String(element.props.className ?? "").split(/\s+/).filter((cls) => {
552
+ const read = cls ? this.readModifiedClass(cls) : void 0;
553
+ if (!read || read.modifiers.length > 0) return Boolean(cls);
554
+ const atoms = this.styleAtoms().get(read.style.path);
555
+ return !(atoms?.has("width") || atoms?.has("height"));
556
+ });
557
+ element.props.className = [...own, ...sized].join(" ");
558
+ return keys;
559
+ }
239
560
  const grammar = this.grammar.component(element.type.replace(/^component:/, ""));
240
561
  if (grammar) this.propsFromDeclarations([["width", `${size[1]}px`], ["height", `${size[2]}px`]], grammar, element.props);
241
562
  return keys;
@@ -247,7 +568,7 @@ var Converter = class {
247
568
  * nothing.
248
569
  */
249
570
  frame(el) {
250
- const children = this.convertChildren(el);
571
+ const children = this.convertInside(el);
251
572
  if (children.length === 0 && !paintsItself(el)) return [];
252
573
  const role = canvasRole(this.options.config, "frame");
253
574
  const grammar = role ? this.grammar.component(role.component) : void 0;
@@ -264,6 +585,102 @@ var Converter = class {
264
585
  return [key];
265
586
  }
266
587
  /**
588
+ * The style props `classes` spell for a component with `grammar`, each class under a modifier
589
+ * prefix written into that modifier's bag (`md:hover:flex` → `_md._hover.display`). A class whose
590
+ * prefix names no modifier the config has, or whose prop the component does not take, is not read.
591
+ * A value already set is kept: the first class for a prop wins, as a stamped value does.
592
+ */
593
+ stylePropsFromClasses(classes, grammar, props) {
594
+ const ranks = /* @__PURE__ */ new Map();
595
+ for (const cls of classes) {
596
+ const read = this.readModifiedClass(cls);
597
+ if (!read) continue;
598
+ const prop = grammar.propForStyleProperty.get(read.style.path);
599
+ if (!prop) continue;
600
+ const slot = `${read.modifiers.join(".")}|${prop}`;
601
+ const before = ranks.get(slot);
602
+ if (before !== void 0 && before >= read.rank) continue;
603
+ ranks.set(slot, read.rank);
604
+ let bag = props;
605
+ for (const key of read.modifiers) {
606
+ const inner = bag[key];
607
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) bag = inner;
608
+ else if (inner === void 0) {
609
+ const next = {};
610
+ bag[key] = next;
611
+ bag = next;
612
+ } else bag = {};
613
+ }
614
+ bag[prop] = this.frameValue(read.style.path, read.style.leaf);
615
+ }
616
+ }
617
+ atomsByStyle;
618
+ /** Each style property → the physical longhands it writes. */
619
+ styleAtoms() {
620
+ if (!this.atomsByStyle) this.atomsByStyle = new Map(this.options.config.list("style-property").map((prop) => {
621
+ const { properties = [] } = prop.toJSON();
622
+ return [prop.path, atomsOf(properties)];
623
+ }));
624
+ return this.atomsByStyle;
625
+ }
626
+ /**
627
+ * A shorthand class that a narrower one overrode on some sides (`p-5` under `px-16`), as the
628
+ * classes for the sides it still painted (`py-5`), so the canvas's stylesheet order cannot let it
629
+ * win the others back. The class as written when the system has no narrower class at its value.
630
+ */
631
+ narrowed(shorthand, owned) {
632
+ const config = this.options.config;
633
+ const options = /* @__PURE__ */ new Map();
634
+ for (const [path, atoms] of this.styleAtoms()) {
635
+ if (path === shorthand.style.path || atoms.size === 0) continue;
636
+ if ([...atoms].every((each) => owned.has(each))) options.set(path, atoms);
637
+ }
638
+ const left = new Set(owned);
639
+ const out = [];
640
+ for (const [path, atoms] of [...options].sort((a, b) => b[1].size - a[1].size)) {
641
+ if (![...atoms].every((each) => left.has(each))) continue;
642
+ const cls = stylePropClassName(config, path, shorthand.style.leaf);
643
+ if (this.grammar.styleProp(cls)?.path !== path) continue;
644
+ out.push(`${shorthand.prefix}${cls}`);
645
+ for (const each of atoms) left.delete(each);
646
+ }
647
+ return left.size === 0 ? out : [shorthand.bare];
648
+ }
649
+ /** A class as the modifier keys over it and the style prop it names, or nothing for one it doesn't. */
650
+ readModifiedClass(cls) {
651
+ const bracket = cls.indexOf("[");
652
+ const head = bracket === -1 ? cls : cls.slice(0, bracket);
653
+ const at = head.lastIndexOf(":");
654
+ const prefixes = at === -1 ? [] : head.slice(0, at).split(":");
655
+ const modifiers = [];
656
+ const kept = [];
657
+ let rank = 0;
658
+ for (const prefix of prefixes) {
659
+ const breakpoint = this.breakpoints.get(prefix);
660
+ if (breakpoint) {
661
+ if (!breakpoint.active) return void 0;
662
+ rank = Math.max(rank, breakpoint.rank);
663
+ continue;
664
+ }
665
+ const key = this.modifierKeys.get(prefix);
666
+ if (!key) return void 0;
667
+ modifiers.push(key);
668
+ kept.push(prefix);
669
+ }
670
+ let leaf = at === -1 ? cls : cls.slice(at + 1);
671
+ const important = /^!|!$/.test(leaf);
672
+ if (important) leaf = leaf.replace(/^!|!$/, "");
673
+ if (important) rank += IMPORTANT_RANK;
674
+ const style = this.grammar.styleProp(leaf);
675
+ return style ? {
676
+ modifiers,
677
+ style,
678
+ rank,
679
+ bare: [...kept, leaf].join(":"),
680
+ prefix: kept.map((each) => `${each}:`).join("")
681
+ } : void 0;
682
+ }
683
+ /**
267
684
  * The styles the config gives an orphaned layer, as the frame's props: the rules that always apply
268
685
  * and the ones its classes say are on, in authored order. Only values the frame component declares,
269
686
  * spelled as an author writes them, are kept.
@@ -388,7 +805,8 @@ var Converter = class {
388
805
  image(img) {
389
806
  const src = img.getAttribute("src");
390
807
  const role = canvasRole(this.options.config, "image");
391
- if (!src || !role) return [];
808
+ if (!src) return [];
809
+ if (!role) return this.plain(img);
392
810
  const props = { ...role.defaultProps };
393
811
  const grammar = this.grammar.component(role.component);
394
812
  if (grammar) {
@@ -443,7 +861,7 @@ var Converter = class {
443
861
  const answer = this.options.icons?.resolve(svgOf(svg));
444
862
  if (!answer) {
445
863
  this.unresolvedIcons += 1;
446
- return [];
864
+ return [this.plainSvg(svg)];
447
865
  }
448
866
  const key = this.key(answer.component);
449
867
  this.elements[key] = {
@@ -461,8 +879,16 @@ var Converter = class {
461
879
  const props = {};
462
880
  const stamped = stampedProps(el);
463
881
  if (grammar) this.propsFromClasses(el, grammar, props);
882
+ if (grammar) for (const [prop, raw] of Object.entries(grammar.declared)) {
883
+ if (stamped[prop] !== void 0) continue;
884
+ const declared = raw;
885
+ const attr = declared?.type === "forward" && declared.target?.__ref === "layer:root" && typeof declared.into === "string" ? declared.into : declared?.type === "string" && ROOT_ATTRIBUTE_PROPS.includes(prop) ? prop : void 0;
886
+ const value = attr ? el.getAttribute(attr) : null;
887
+ if (value) props[prop] = value;
888
+ }
464
889
  Object.assign(props, stamped);
465
890
  if (grammar) this.propsFromInlineStyle(el, grammar, props, path);
891
+ if (grammar) this.propsFromComputed(el, grammar, props, path);
466
892
  const layerProps = stampedLayerProps({
467
893
  el,
468
894
  layers: this.layerNames(path)
@@ -476,9 +902,14 @@ var Converter = class {
476
902
  if (answer) for (const [prop, value] of Object.entries(answer.props)) props[prop] ??= value;
477
903
  else if (Object.keys(stamped).length === 0) {
478
904
  this.unresolvedIcons += 1;
479
- return;
905
+ return svg ? this.plainSvg(svg) : void 0;
480
906
  }
481
907
  }
908
+ const glyphProp = this.glyphDrawers.get(path);
909
+ if (glyphProp && props[glyphProp] === void 0) {
910
+ const svg = el.tagName.toLowerCase() === "svg" ? el : el.querySelector("svg");
911
+ if (svg) return this.plainSvg(svg);
912
+ }
482
913
  const key = this.key(path);
483
914
  const element = {
484
915
  type: `component:${path}`,
@@ -486,8 +917,10 @@ var Converter = class {
486
917
  };
487
918
  const declared = grammar?.declared ?? {};
488
919
  const slots = {};
489
- for (const layer of asArray(el.querySelectorAll("[data-uds-slot]"))) {
490
- if (nearestRoot(layer) !== el || hiddenBetween(layer, el)) continue;
920
+ let rootSlotted = false;
921
+ const marked = [...el.hasAttribute("data-uds-slot") ? [el] : [], ...asArray(el.querySelectorAll("[data-uds-slot]"))];
922
+ for (const layer of marked) {
923
+ if (layer !== el && layer.getAttribute("data-uds-layer") === "root" || layer !== el && nearestRoot(layer) !== el || hiddenBetween(layer, el)) continue;
491
924
  const names = slotNames(layer).filter((slot) => !grammar || slot in declared);
492
925
  const intoOf = (slot) => grammar?.layerProps.get(slot)?.into ?? "children";
493
926
  const ownChildren = names.filter((slot) => intoOf(slot) === "children");
@@ -496,10 +929,13 @@ var Converter = class {
496
929
  if (into === "children" && slot !== ownChildren.at(-1)) continue;
497
930
  const holder = into === "children" ? layer : slotLayerNamed(layer, into);
498
931
  if (!holder) continue;
499
- const fills = holder === layer ? this.convertOutsideLayers(layer, path) : this.convertChildren(holder);
932
+ const declaration = declared[slot];
933
+ const callerMarkup = declaration?.type === "slot" && declaration.text !== true;
934
+ const fills = holder === layer ? this.convertOutsideLayers(layer, path, callerMarkup) : this.convertChildren(holder);
935
+ if (layer === el && holder === layer) rootSlotted = true;
500
936
  if (fills.length > 0) slots[slot] = fills;
501
- else if (holder !== layer) {
502
- const text = collapse(ownText(holder));
937
+ else {
938
+ const text = collapse(holder === layer ? ownText(layer, /* @__PURE__ */ new Set()) : ownText(holder));
503
939
  if (text) props[slot] = text;
504
940
  }
505
941
  }
@@ -541,7 +977,7 @@ var Converter = class {
541
977
  }
542
978
  const textSlot = declared.children;
543
979
  const holdsText = textSlot?.type === "slot" && textSlot.text === true;
544
- const loose = this.looseContent({
980
+ const loose = rootSlotted ? [] : this.looseContent({
545
981
  el,
546
982
  claimed,
547
983
  takesGlyphs: !renders,
@@ -574,9 +1010,24 @@ var Converter = class {
574
1010
  if ("checked" in declared) props.checked = true;
575
1011
  else if ("selected" in declared) props.selected = true;
576
1012
  }
577
- if (grammar) for (const prop of Object.keys(props)) {
578
- if (prop in declared || prop === "aria-label" || prop === "layerProps") continue;
579
- delete props[prop];
1013
+ if (grammar) {
1014
+ if (props.style !== void 0 && !("style" in declared)) {
1015
+ const bags = props["layerProps"] ?? {};
1016
+ const root = bags.root ?? {};
1017
+ bags.root = {
1018
+ ...root,
1019
+ style: {
1020
+ ...Object.fromEntries(Object.entries(props.style).map(([property, value]) => [property, inlineCss(value)])),
1021
+ ...root.style ?? {}
1022
+ }
1023
+ };
1024
+ props[LAYER_PROPS] = bags;
1025
+ delete props.style;
1026
+ }
1027
+ for (const prop of Object.keys(props)) {
1028
+ if (prop in declared || prop === "aria-label" || prop === "layerProps") continue;
1029
+ delete props[prop];
1030
+ }
580
1031
  }
581
1032
  this.elements[key] = element;
582
1033
  return key;
@@ -593,11 +1044,7 @@ var Converter = class {
593
1044
  props[composite.prop] = composite.value;
594
1045
  continue;
595
1046
  }
596
- const style = this.grammar.styleProp(cls);
597
- if (style) {
598
- const prop = grammar.propForStyleProperty.get(style.path);
599
- if (prop && props[prop] === void 0) props[prop] = this.frameValue(style.path, style.leaf);
600
- }
1047
+ this.stylePropsFromClasses([cls], grammar, props);
601
1048
  }
602
1049
  }
603
1050
  /**
@@ -619,6 +1066,46 @@ var Converter = class {
619
1066
  propsFromInlineStyle(el, grammar, props, path) {
620
1067
  this.propsFromDeclarations(inlineDeclarations(el), grammar, props, path === void 0 ? void 0 : this.ruledProperties(path));
621
1068
  }
1069
+ /**
1070
+ * The styles the page's own classes gave a component's root, from what the browser computed, for
1071
+ * the CSS properties the component does not set itself: a stack's `display` and direction are its
1072
+ * own, while the width and flex share an app's Tailwind gave it are the page's, and no prop said
1073
+ * them. A property a rule, a default or a prop already read sets is left to that.
1074
+ */
1075
+ propsFromComputed(el, grammar, props, path) {
1076
+ const computed = el.getAttribute(COMPUTED_STYLE_ATTR);
1077
+ if (!computed) return;
1078
+ const own = new Set(this.ruledProperties(path));
1079
+ const { defaultProps = {} } = Component.get(this.options.config, path) ?? {};
1080
+ const named = new Set(Object.keys(props));
1081
+ for (const [stylePath, prop] of grammar.propForStyleProperty) {
1082
+ if (!named.has(prop)) continue;
1083
+ for (const property of StyleProperty.get(this.options.config, stylePath)?.properties ?? []) for (const each of longhandsOf(property)) own.add(each);
1084
+ }
1085
+ const fill = {};
1086
+ const unheld = {};
1087
+ for (const declaration of declarationsOf(computed)) {
1088
+ const [property, value] = declaration;
1089
+ if (own.has(property) || own.has(LOGICAL_SIDES[property] ?? property)) continue;
1090
+ const before = Object.keys(fill).length;
1091
+ this.propsFromDeclarations([declaration], grammar, fill);
1092
+ if (Object.keys(fill).length === before && COMPONENT_INLINE_LAYOUT.has(property)) {
1093
+ const arbitrary = this.grammar.stylePropsFor(property).some((option) => option.arbitrary);
1094
+ unheld[camelCase(property)] = arbitrary ? `[${normalizeCssValue(value).replaceAll(" ", "_")}]` : value;
1095
+ }
1096
+ }
1097
+ for (const [prop, value] of Object.entries(fill)) {
1098
+ if (props[prop] !== void 0 || defaultProps[prop] === value) continue;
1099
+ props[prop] = value;
1100
+ }
1101
+ if (Object.keys(unheld).length > 0) {
1102
+ const own = props.style ?? {};
1103
+ props.style = {
1104
+ ...unheld,
1105
+ ...own
1106
+ };
1107
+ }
1108
+ }
622
1109
  /** The CSS properties a component's own style rules write on its root layer. */
623
1110
  ruledProperties(path) {
624
1111
  const known = this.ruled.get(path);
@@ -744,6 +1231,226 @@ const ARTBOARD = [
744
1231
  ];
745
1232
  /** How a single-line input lays out its text: centred across its height. */
746
1233
  const INPUT_LINE = [["display", "flex"], ["align-items", "center"]];
1234
+ /** Tags the canvas refuses to render, which a capture leaves out. */
1235
+ /** A value in class form (`[304px_auto]`) as the CSS it stands for (`304px auto`); others as they are. */
1236
+ function inlineCss(value) {
1237
+ if (typeof value !== "string" || !/^\[.*\]$/.test(value)) return value;
1238
+ return value.slice(1, -1).replace(/url\([^)]*\)|_/g, (span) => span === "_" ? " " : span);
1239
+ }
1240
+ /** Whether `el` holds more than `limit` elements, counted until the limit is passed. */
1241
+ function holdsMoreThan(el, limit) {
1242
+ let count = 0;
1243
+ const visit = (node) => {
1244
+ for (const child of node.children) {
1245
+ count += 1;
1246
+ if (count > limit || visit(child)) return true;
1247
+ }
1248
+ return false;
1249
+ };
1250
+ return visit(el);
1251
+ }
1252
+ /** The most shapes a drawing the capture keeps as itself may hold. */
1253
+ const SVG_SHAPE_LIMIT = 300;
1254
+ const FORBIDDEN_TAGS = new Set([
1255
+ "script",
1256
+ "style",
1257
+ "noscript",
1258
+ "template",
1259
+ "iframe",
1260
+ "object",
1261
+ "embed",
1262
+ "base",
1263
+ "link",
1264
+ "meta"
1265
+ ]);
1266
+ const SIDES = [
1267
+ "top",
1268
+ "right",
1269
+ "bottom",
1270
+ "left"
1271
+ ];
1272
+ const CORNERS = [
1273
+ "top-left",
1274
+ "top-right",
1275
+ "bottom-right",
1276
+ "bottom-left"
1277
+ ];
1278
+ /** A property with the longhands a shorthand of it sets, physical and logical. */
1279
+ /** Outranks any breakpoint's rank, which is its min-width in pixels. */
1280
+ const IMPORTANT_RANK = 1e6;
1281
+ /** The physical longhands a set of CSS properties writes: `margin-inline` → margin-left, margin-right. */
1282
+ function atomsOf(properties) {
1283
+ const physical = new Map(Object.entries(LOGICAL_SIDES).map(([side, logical]) => [logical, side]));
1284
+ const atoms = /* @__PURE__ */ new Set();
1285
+ for (const property of properties) for (const each of longhandsOf(property)) {
1286
+ const side = physical.get(each) ?? each;
1287
+ if (longhandsOf(side).length <= 1) atoms.add(side);
1288
+ }
1289
+ return atoms;
1290
+ }
1291
+ function longhandsOf(property) {
1292
+ const boxed = /^(padding|margin)(-inline|-block)?$/.exec(property);
1293
+ if (boxed) {
1294
+ const [, box, axis] = boxed;
1295
+ return [property, ...(axis === "-inline" ? ["left", "right"] : axis === "-block" ? ["top", "bottom"] : SIDES).flatMap((side) => [`${box}-${side}`, LOGICAL_SIDES[`${box}-${side}`] ?? ""])].filter(Boolean);
1296
+ }
1297
+ switch (property) {
1298
+ case "inset": return [property, ...SIDES];
1299
+ case "gap": return [
1300
+ property,
1301
+ "row-gap",
1302
+ "column-gap"
1303
+ ];
1304
+ case "overflow": return [
1305
+ property,
1306
+ "overflow-x",
1307
+ "overflow-y"
1308
+ ];
1309
+ case "border-radius": return [property, ...CORNERS.map((corner) => `border-${corner}-radius`)];
1310
+ case "border-width":
1311
+ case "border-color":
1312
+ case "border-style": {
1313
+ const part = property.slice(7);
1314
+ return [property, ...SIDES.map((side) => `border-${side}-${part}`)];
1315
+ }
1316
+ default: return [property];
1317
+ }
1318
+ }
1319
+ /** Each physical side's logical name in a left-to-right page. */
1320
+ const LOGICAL_SIDES = {
1321
+ "padding-left": "padding-inline-start",
1322
+ "padding-right": "padding-inline-end",
1323
+ "margin-left": "margin-inline-start",
1324
+ "margin-right": "margin-inline-end",
1325
+ "border-left-width": "border-inline-start-width",
1326
+ "border-right-width": "border-inline-end-width",
1327
+ "border-left-color": "border-inline-start-color",
1328
+ "border-right-color": "border-inline-end-color",
1329
+ left: "inset-inline-start",
1330
+ right: "inset-inline-end",
1331
+ "border-top-left-radius": "border-start-start-radius",
1332
+ "border-top-right-radius": "border-start-end-radius",
1333
+ "border-bottom-left-radius": "border-end-start-radius",
1334
+ "border-bottom-right-radius": "border-end-end-radius"
1335
+ };
1336
+ /** The renderer's `VERBATIM_ATTRIBUTES` key (`renderer/wrappers/html-aliases`), spelled here so the
1337
+ * capture does not load the React renderer. */
1338
+ const VERBATIM_ATTRIBUTES = "__verbatimAttributes";
1339
+ /** SVG attributes a copied drawing leaves out: script handlers, ids and references to definitions. */
1340
+ const SVG_SKIPPED_ATTRIBUTES = /^(on|data-|id$|clip-path$|mask$|filter$|xlink:href$|href$)/;
1341
+ /** SVG elements a copied drawing leaves out: its definitions, and what holds only them. */
1342
+ const SVG_SKIPPED_TAGS = new Set([
1343
+ "defs",
1344
+ "clippath",
1345
+ "mask",
1346
+ "lineargradient",
1347
+ "radialgradient",
1348
+ "filter",
1349
+ "pattern",
1350
+ "symbol",
1351
+ "script",
1352
+ "style",
1353
+ "title",
1354
+ "desc",
1355
+ "use"
1356
+ ]);
1357
+ /** SVG tags whose case the captured tree lost. */
1358
+ const SVG_TAGS = new Map([["foreignobject", "foreignObject"]]);
1359
+ /** An SVG attribute under the name React takes it by: `fill-rule` is `fillRule`, `viewBox` stays. */
1360
+ function svgAttributeName(name) {
1361
+ if (name === "viewbox") return "viewBox";
1362
+ if (name.startsWith("aria-") || name === "role") return name;
1363
+ if (name === "xmlns" || name === "xmlns:xlink") return name === "xmlns" ? "xmlns" : "xmlnsXlink";
1364
+ return name.replace(/[-:]([a-z])/g, (_, letter) => letter.toUpperCase());
1365
+ }
1366
+ /** The layout a component's root may need inline when none of its props can say it. */
1367
+ const COMPONENT_INLINE_LAYOUT = new Set([
1368
+ "grid-template-columns",
1369
+ "grid-template-rows",
1370
+ "grid-column",
1371
+ "grid-row",
1372
+ "white-space",
1373
+ "aspect-ratio",
1374
+ "flex-basis",
1375
+ "flex-grow",
1376
+ "flex-shrink",
1377
+ "max-width",
1378
+ "min-width"
1379
+ ]);
1380
+ /** The width a capture is read at when it states none, which settles its breakpoints. */
1381
+ const DEFAULT_CAPTURE_WIDTH = 1600;
1382
+ /** The attributes a component's root renders a string prop of the same name as. */
1383
+ const ROOT_ATTRIBUTE_PROPS = [
1384
+ "src",
1385
+ "alt",
1386
+ "href",
1387
+ "title",
1388
+ "placeholder"
1389
+ ];
1390
+ /** The CSS properties that draw something of an element's own. */
1391
+ const PAINT_PROPERTY = /^(background|border-(top-|right-|bottom-|left-)?(width|color)|box-shadow|outline)/;
1392
+ /** Tags that only group what they hold, and fold away when they carry nothing of their own. */
1393
+ const GROUPING_TAGS = new Set(["div", "span"]);
1394
+ /** Tags with no content. */
1395
+ const VOID_TAGS = new Set([
1396
+ "img",
1397
+ "input",
1398
+ "br",
1399
+ "hr",
1400
+ "wbr",
1401
+ "area",
1402
+ "col",
1403
+ "source",
1404
+ "track"
1405
+ ]);
1406
+ /** The attributes a plain element keeps, as the props React takes them under. */
1407
+ const PLAIN_ATTRIBUTES = {
1408
+ href: "href",
1409
+ src: "src",
1410
+ alt: "alt",
1411
+ title: "title",
1412
+ role: "role",
1413
+ type: "type",
1414
+ placeholder: "placeholder",
1415
+ name: "name",
1416
+ target: "target",
1417
+ rel: "rel",
1418
+ colspan: "colSpan",
1419
+ rowspan: "rowSpan",
1420
+ for: "htmlFor"
1421
+ };
1422
+ function plainAttributes(el) {
1423
+ const out = {};
1424
+ const tag = el.tagName.toLowerCase();
1425
+ for (const [attr, prop] of Object.entries(PLAIN_ATTRIBUTES)) {
1426
+ const value = el.getAttribute(attr);
1427
+ if (value === null || value === void 0) continue;
1428
+ if ((attr === "href" || attr === "src") && /^\s*javascript:/i.test(value)) continue;
1429
+ out[prop] = value;
1430
+ }
1431
+ for (const { name, value } of el.attributes) if (name.startsWith("aria-")) out[name] = value;
1432
+ if (tag === "input" || tag === "textarea") {
1433
+ const value = tag === "textarea" ? el.textContent ?? "" : el.getAttribute("value");
1434
+ if (value) out.defaultValue = value;
1435
+ }
1436
+ return out;
1437
+ }
1438
+ /** The text an element holds directly, its whitespace collapsed; empty for a field. */
1439
+ /** An element's text as it showed: what a hidden descendant (a tooltip, `hidden`) holds is left out. */
1440
+ function plainText(el) {
1441
+ if (isField(el)) return "";
1442
+ let text = "";
1443
+ const read = (node) => {
1444
+ for (const child of node.childNodes) if (child.nodeType === 3) text += child.textContent ?? "";
1445
+ else if (child.nodeType === 1 && visible(child)) read(child);
1446
+ };
1447
+ read(el);
1448
+ return text.replace(/\s+/g, " ").trim();
1449
+ }
1450
+ function camelCase(property) {
1451
+ if (property.startsWith("--")) return property;
1452
+ return property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1453
+ }
747
1454
  function isImage(el) {
748
1455
  return el.tagName.toLowerCase() === "img" && !el.hasAttribute("data-uds-component");
749
1456
  }
@@ -816,8 +1523,12 @@ function paintsItself(el) {
816
1523
  }
817
1524
  /** An element's inline declarations as `[property, value]` pairs. */
818
1525
  function inlineDeclarations(el) {
1526
+ return declarationsOf(el.getAttribute("style") ?? "");
1527
+ }
1528
+ /** A declaration list (`display: flex; gap: 8px`) as property and value pairs. */
1529
+ function declarationsOf(text) {
819
1530
  const out = [];
820
- for (const declaration of (el.getAttribute("style") ?? "").split(";")) {
1531
+ for (const declaration of text.split(";")) {
821
1532
  const at = declaration.indexOf(":");
822
1533
  if (at < 0) continue;
823
1534
  const property = declaration.slice(0, at).trim().toLowerCase();
@@ -841,6 +1552,8 @@ function negated(value) {
841
1552
  */
842
1553
  function equivalents({ property, value }) {
843
1554
  if ((property === "grid-column" || property === "grid-row") && /^span \d+$/.test(value)) return [value, `${value} / ${value}`];
1555
+ const px = /^(-?(?:\d+(?:\.\d+)?|\.\d+))px$/.exec(value);
1556
+ if (px) return [value, `${Number(px[1]) / 16}rem`];
844
1557
  return [value];
845
1558
  }
846
1559
  /**