@yahoo/uds-create-config 5.68.0 → 5.68.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.
@@ -907,15 +907,24 @@ type AuthoredComponent = ComponentBuilder<LayerMap, PropMap, unknown, unknown>;
907
907
  * The builder is read off `metadata()`'s return type, so a handle with statics on it (the Bridge's
908
908
  * `Menu.Item`) is matched as the bare builder and keeps its statics. Matched whole, a handle whose
909
909
  * statics are builders too passes TypeScript's complexity limit (TS2859) once a system's style props
910
- * are registered.
911
- *
912
- * OPEN: a static that is the component itself (`Menu.Content`) keeps the source's types.
910
+ * are registered. A static that is the component itself (`Menu.Content`) is the rebuilt handle too.
913
911
  */
914
912
  type LinkedHandle<Handle, Props extends PropMap = Record<never, never>, Layers extends Record<string, LayerElement> = Record<never, never>> = Handle extends {
915
913
  metadata(meta: never): infer Builder;
916
- } ? Builder extends ComponentBuilder<infer L extends LayerMap, infer P extends PropMap, infer E, infer D, infer F extends string, infer S extends StateMap> ? WithStatics<ComponentBuilder<Without<L, keyof Layers> & Layers extends infer Swapped extends LayerMap ? Swapped : L, CompactProps<Without<P, keyof Props> & Props>, E, D, F, S>, Omit<Handle, keyof Builder>> : Handle : Handle;
917
- /** A rebuilt handle with the source handle's statics beside it, when it has any. */
918
- type WithStatics<Rebuilt, Statics> = [keyof Statics] extends [never] ? Rebuilt : Rebuilt & Statics;
914
+ } ? Builder extends ComponentBuilder<infer L extends LayerMap, infer P extends PropMap, infer E, infer D, infer F extends string, infer S extends StateMap> ? WithStatics<ComponentBuilder<Without<L, keyof Layers> & Layers extends infer Swapped extends LayerMap ? Swapped : L, CompactProps<Without<P, keyof Props> & Props>, E, D, F, S>, Builder, Omit<Handle, keyof Builder>> : Handle : Handle;
915
+ /**
916
+ * A rebuilt handle with the source handle's statics beside it, when it has any. A static whose
917
+ * builder is the handle's own is the rebuilt handle, with that static's own statics beside it.
918
+ */
919
+ type WithStatics<Rebuilt, Builder, Statics> = [keyof Statics] extends [never] ? Rebuilt : Rebuilt & { [K in keyof Statics]: IsBuilder<Statics[K], Builder> extends true ? Rebuilt & Omit<Statics[K], keyof Builder> : Statics[K] };
920
+ /**
921
+ * Whether a static's builder is the handle's own. An identity check, which TypeScript answers at
922
+ * once when both are the same type: an assignability check would compare the two builders whole,
923
+ * as the TS2859 above did.
924
+ */
925
+ type IsBuilder<Static, Builder> = Static extends {
926
+ metadata(meta: never): infer Own;
927
+ } ? (<T>() => T extends Own ? 1 : 2) extends (<T>() => T extends Builder ? 1 : 2) ? true : false : false;
919
928
  interface DefineComponent {
920
929
  /** The one-layer form: the element IS the component. */
921
930
  <const El extends LayerElement>(element: El): ComponentBuilder<{
@@ -11,7 +11,8 @@
11
11
  * them.
12
12
  *
13
13
  * Two ways in. {@link snapshotHtml} reads the markup `react-dom/server` produces, which is what
14
- * `uds anatomy` compares in a Bun process — structure, text and classes, since no layout runs there.
14
+ * `uds anatomy` compares in a Bun process — structure, text, classes, inline style and a plain tag's
15
+ * attributes, since no layout runs there.
15
16
  * A browser surface snapshots live DOM instead (the studio's `?anatomyCheck=true`), and adds computed
16
17
  * style to each node; {@link diffRendered} compares whatever a node carries.
17
18
  */
@@ -34,10 +35,17 @@ interface RenderedNode {
34
35
  readonly style: Readonly<Record<string, string>>;
35
36
  /** `true` when `style` holds inline declarations rather than a computed subset. */
36
37
  readonly inlineStyle?: boolean;
38
+ /**
39
+ * A plain tag's attributes — an element that is no component's layer, such as an svg `<path>`,
40
+ * whose attributes are often its paint (`fill-opacity`, `stroke-width`, `d`). Left out for a layer,
41
+ * whose attributes come from its component, and where the snapshot can't read them. See
42
+ * {@link paintAttributesOf} for what is skipped.
43
+ */
44
+ readonly attributes?: Readonly<Record<string, string>>;
37
45
  /** What the alignment matches on — see {@link renderedSignatureOf}. */
38
46
  readonly signature: string;
39
47
  }
40
- type AnatomyIssueKind = /** The module renders this element and the anatomy does not. */'missing' /** The anatomy renders this element and the module does not. */ | 'extra' /** Both render it — aligned as the same layer — as different elements (`div` against `span`). */ | 'tag' /** Both render it, with different own text. */ | 'text' /** Both render it, with a computed style that differs beyond tolerance. */ | 'style'
48
+ type AnatomyIssueKind = /** The module renders this element and the anatomy does not. */'missing' /** The anatomy renders this element and the module does not. */ | 'extra' /** Both render it — aligned as the same layer — as different elements (`div` against `span`). */ | 'tag' /** Both render it, with different own text. */ | 'text' /** Both render it as a plain tag, with an attribute that differs or that one side lacks. */ | 'attribute' /** Both render it, with a computed style that differs beyond tolerance. */ | 'style'
41
49
  /** Both render it, with different class sets. Classes are how a style arrives, so where a
42
50
  * computed style is available this is informational; where none is (markup only) it is the
43
51
  * paint difference itself — see `diffRendered`'s `classesFail`. */
@@ -90,7 +98,7 @@ interface DiffOptions {
90
98
  *
91
99
  * Elements align by signature in document order; what the module renders and the anatomy does not
92
100
  * is `missing`, the reverse is `extra`. Aligned elements then compare own text, computed style (when
93
- * both sides carry it) and class set.
101
+ * both sides carry it), a plain tag's attributes (when both sides carry them) and class set.
94
102
  */
95
103
  declare function diffRendered(anatomy: readonly RenderedNode[], instance: readonly RenderedNode[], options?: DiffOptions): AnatomyReport;
96
104
  /** The allowlist spelling of an issue: kind, node, and the property for a style difference. */
@@ -161,6 +161,31 @@ function ownTextOf(element) {
161
161
  for (const child of element.children) if ("text" in child) text += child.text;
162
162
  return text.replace(/\s+/g, " ").trim();
163
163
  }
164
+ /** Attributes that name an id rather than paint: React mints ids per render, so the two sides differ. */
165
+ const ID_ATTRIBUTES = new Set([
166
+ "id",
167
+ "for",
168
+ "aria-activedescendant",
169
+ "aria-controls",
170
+ "aria-describedby",
171
+ "aria-labelledby",
172
+ "aria-owns"
173
+ ]);
174
+ /**
175
+ * The attributes of a plain tag that the two renderings should agree on: every one but `class` and
176
+ * `style` (compared on their own), `data-*` (markers), and what names an id — an id attribute, or a
177
+ * value pointing at one (`url(#…)`, `#…`).
178
+ */
179
+ function paintAttributesOf(attributes) {
180
+ const out = {};
181
+ for (const [name, value] of Object.entries(attributes)) {
182
+ if (name === "class" || name === "style" || name.startsWith("data-")) continue;
183
+ if (ID_ATTRIBUTES.has(name)) continue;
184
+ if (value.includes("url(#") || value.startsWith("#")) continue;
185
+ out[name] = value;
186
+ }
187
+ return out;
188
+ }
164
189
  function firstComponentRoot(node) {
165
190
  for (const child of node.children) {
166
191
  if ("text" in child) continue;
@@ -190,6 +215,7 @@ function snapshotHtml(html) {
190
215
  classes: (element.attributes.class ?? "").split(/\s+/).filter(Boolean).sort(),
191
216
  style: inlineStyleOf(element.attributes.style),
192
217
  inlineStyle: true,
218
+ ...component === void 0 ? { attributes: paintAttributesOf(element.attributes) } : {},
193
219
  signature: renderedSignatureOf(element.tag, component, layer)
194
220
  });
195
221
  for (const child of element.children) if (!("text" in child)) visit(child);
@@ -255,7 +281,7 @@ function nodeLabel(node, ordinal) {
255
281
  *
256
282
  * Elements align by signature in document order; what the module renders and the anatomy does not
257
283
  * is `missing`, the reverse is `extra`. Aligned elements then compare own text, computed style (when
258
- * both sides carry it) and class set.
284
+ * both sides carry it), a plain tag's attributes (when both sides carry them) and class set.
259
285
  */
260
286
  function diffRendered(anatomy, instance, options = {}) {
261
287
  const issues = [];
@@ -310,6 +336,21 @@ function diffRendered(anatomy, instance, options = {}) {
310
336
  instance: vb === void 0 ? void 0 : normalizeStyleValue(vb)
311
337
  });
312
338
  }
339
+ if (a.attributes && b.attributes) {
340
+ const names = [...new Set([...Object.keys(a.attributes), ...Object.keys(b.attributes)])].sort();
341
+ for (const name of names) {
342
+ const va = a.attributes[name];
343
+ const vb = b.attributes[name];
344
+ if (va === vb) continue;
345
+ issues.push({
346
+ kind: "attribute",
347
+ node: label,
348
+ property: name,
349
+ anatomy: va,
350
+ instance: vb
351
+ });
352
+ }
353
+ }
313
354
  const onlyA = a.classes.filter((c) => !b.classes.includes(c));
314
355
  const onlyB = b.classes.filter((c) => !a.classes.includes(c));
315
356
  if (onlyA.length || onlyB.length) issues.push({
@@ -975,6 +975,7 @@ function slotTargets(config, path, body) {
975
975
  }
976
976
  function computeSlotTargets(config, body) {
977
977
  const byLayer = /* @__PURE__ */ new Map();
978
+ const targeted = /* @__PURE__ */ new Map();
978
979
  const untargeted = [];
979
980
  for (const [from, decl] of Object.entries(body.props ?? {})) {
980
981
  if (decl?.type === "slot") {
@@ -988,11 +989,15 @@ function computeSlotTargets(config, body) {
988
989
  if (decl?.type !== "forward") continue;
989
990
  if (!routesContent(config, body.layers, decl)) continue;
990
991
  const target = refOf(decl.target);
991
- if (target) byLayer.set(pathOf(target), {
992
- ...decl,
993
- from
994
- });
995
- else untargeted.push({
992
+ if (target) {
993
+ const layer = pathOf(target);
994
+ const route = {
995
+ ...decl,
996
+ from
997
+ };
998
+ byLayer.set(layer, route);
999
+ targeted.set(layer, [...(targeted.get(layer) ?? []).filter((other) => intoName(other.into) !== intoName(route.into)), route]);
1000
+ } else untargeted.push({
996
1001
  ...decl,
997
1002
  from,
998
1003
  untargeted: true
@@ -1003,6 +1008,7 @@ function computeSlotTargets(config, body) {
1003
1008
  if (root && content && !byLayer.has(root)) byLayer.set(root, content);
1004
1009
  return {
1005
1010
  byLayer,
1011
+ targeted,
1006
1012
  untargeted
1007
1013
  };
1008
1014
  }
@@ -1024,9 +1030,9 @@ function computeSlotTargets(config, body) {
1024
1030
  function slotTargetsOf(config, path) {
1025
1031
  const body = bodyOf(config, path);
1026
1032
  if (!body) return [];
1027
- const { byLayer, untargeted } = slotTargets(config, path, body);
1033
+ const { targeted: targetedByLayer, untargeted } = slotTargets(config, path, body);
1028
1034
  const root = rootKeyOf(body.layers ?? {});
1029
- return [...[...byLayer].filter(([, decl]) => !decl.untargeted).map(([layer, decl]) => ({
1035
+ return [...[...targetedByLayer].flatMap(([layer, routes]) => routes.map((decl) => [layer, decl])).map(([layer, decl]) => ({
1030
1036
  layer,
1031
1037
  from: decl.from,
1032
1038
  into: intoName(decl.into)
@@ -1288,7 +1294,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1288
1294
  ...Object.fromEntries(Object.entries(pins).map(([key, value]) => key.startsWith("_") ? [key, value === true || value === "true"] : [key, value]))
1289
1295
  };
1290
1296
  const consumed = consumedProps(config, path, body);
1291
- const { byLayer: targets } = slotTargets(config, path, body);
1297
+ const { byLayer: targets, targeted: targetedRoutes } = slotTargets(config, path, body);
1292
1298
  const valueRoutes = /* @__PURE__ */ new Map();
1293
1299
  for (const route of valueForwards(config, body)) {
1294
1300
  const routes = valueRoutes.get(route.layer) ?? [];
@@ -1331,6 +1337,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1331
1337
  let contentPlaced = false;
1332
1338
  const routedFroms = /* @__PURE__ */ new Set();
1333
1339
  for (const target of targets.values()) routedFroms.add(target.from);
1340
+ for (const routes of targetedRoutes.values()) for (const route of routes) routedFroms.add(route.from);
1334
1341
  const positioned = /* @__PURE__ */ new Set();
1335
1342
  for (const node of Object.values(anatomy)) {
1336
1343
  const prop = slotAnatomyPropOf(node);
@@ -1428,6 +1435,8 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1428
1435
  }
1429
1436
  const slot = layer ? targets.get(layer) : void 0;
1430
1437
  const slotValue = slot && !subtreeBinds(anatomy, key, slot.from) && !(positioned.has(slot.from) && isText(instanceProps[slot.from])) ? instanceProps[slot.from] : void 0;
1438
+ const siblingRoutes = layer ? (targetedRoutes.get(layer) ?? []).filter((route) => route !== slot) : [];
1439
+ const siblingValues = Object.fromEntries(siblingRoutes.filter((route) => instanceProps[route.from] !== void 0 && !subtreeBinds(anatomy, key, route.from)).map((route) => [intoName(route.into), instanceProps[route.from]]));
1431
1440
  const props = layer ? {
1432
1441
  ...withoutKeys(authoredProps(config, node.props), ruleKeysFor(config, path, layer, state, pins)),
1433
1442
  "data-uds-component": path,
@@ -1438,6 +1447,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1438
1447
  ...isRoot ? componentStateAttributes(componentStates(config, path), state) : {},
1439
1448
  ...isRoot || type === void 0 || kindOf(type) !== "component" ? forcedModifierAttributes(pins, path) : {},
1440
1449
  ...isRoot ? rest : {},
1450
+ ...siblingValues,
1441
1451
  ...slot && slotValue !== void 0 ? { [intoName(slot.into)]: slotValue } : {},
1442
1452
  ...layerProps[layer],
1443
1453
  className: classesFor(config, path, layer, state, [
@@ -1513,6 +1523,10 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1513
1523
  if (placedFills.length) slots[name] = placedFills;
1514
1524
  }
1515
1525
  if (overridden && routed) slots[overridden] = [...routed];
1526
+ for (const route of siblingRoutes) {
1527
+ const fills = instanceSlots?.[route.from];
1528
+ if (fills?.length) slots[intoName(route.into)] = [...fills];
1529
+ }
1516
1530
  if (routed?.length && !overridden && !(slot && positioned.has(slot.from))) children.push(...routed);
1517
1531
  const placed = place(nested, type, props, children, keyHint, Object.keys(slots).length ? slots : void 0);
1518
1532
  if (visible === void 0) withCondition(nested, placed, { visible: node.visible });
@@ -1,5 +1,6 @@
1
1
  import { FragmentRenderer } from "./primitives/FragmentRenderer.js";
2
2
  import { SlotRenderer } from "./primitives/SlotRenderer.js";
3
+ import { glyphMemberFor } from "../spec/glyph-members.js";
3
4
  import { wrapComponentRefs } from "./wrappers/component-refs.js";
4
5
  import { wrapEventBridge } from "./wrappers/event-bridge.js";
5
6
  import { wrapGlyphProps } from "./wrappers/glyph-props.js";
@@ -28,7 +29,10 @@ function wrapRegistry(reg, descriptor, iconMembers) {
28
29
  };
29
30
  const stylePropNames = descriptor?.stylePropClasses ? new Set([...Object.keys(descriptor.stylePropClasses), ...Object.values(descriptor.cssPropertyToStyleProp ?? {})]) : void 0;
30
31
  const holder = {};
31
- const final = aliasHtmlTypes(wrapInlineStyleProps(wrapNormalizeHex(wrapEventBridge(wrapGlyphProps(wrapComponentRefs(base, (type) => holder.registry?.[type]), descriptor?.glyphProps, iconMembers)), stylePropNames), {
32
+ const final = aliasHtmlTypes(wrapInlineStyleProps(wrapNormalizeHex(wrapEventBridge(wrapGlyphProps(wrapComponentRefs(base, (type) => holder.registry?.[type], iconMembers ? (ref) => glyphMemberFor({
33
+ ref,
34
+ members: iconMembers
35
+ }) : void 0), descriptor?.glyphProps, iconMembers)), stylePropNames), {
32
36
  cssPropertyToStyleProp: descriptor?.cssPropertyToStyleProp,
33
37
  layoutComponents: descriptor?.layoutComponents,
34
38
  textComponents: descriptor?.textComponents
@@ -1,4 +1,5 @@
1
1
  import type { ComponentRegistry, ComponentRenderer } from '@json-render/react';
2
+ import { type PreviewRef } from '../../spec/preview-elements';
2
3
  /**
3
4
  * A component handed as a prop VALUE (`icon={DotsThreeIcon}`) crosses the config as a bare ref,
4
5
  * `{ __ref: 'icon:phosphor/DotsThreeIcon' }`, keyed the way the registry keys the component. This
@@ -11,4 +12,6 @@ import type { ComponentRegistry, ComponentRenderer } from '@json-render/react';
11
12
  * like any spec element. One component per ref, so the receiving component's element type is stable
12
13
  * across renders and React keeps the glyph mounted.
13
14
  */
14
- export declare function wrapComponentRefs(reg: ComponentRegistry, resolve: (type: string) => ComponentRenderer | undefined): ComponentRegistry;
15
+ export declare function wrapComponentRefs(reg: ComponentRegistry, resolve: (type: string) => ComponentRenderer | undefined,
16
+ /** The member an `icon:` ref names, for a glyph nested inside a prop's data. */
17
+ glyphOf?: (ref: PreviewRef) => unknown): ComponentRegistry;
@@ -20,14 +20,24 @@ function hasRefDeep(value) {
20
20
  if (isComponentRef(value)) return true;
21
21
  return (Array.isArray(value) ? value : Object.values(value)).some(hasRefDeep);
22
22
  }
23
- function reviveDeep(value, revive) {
23
+ function reviveDeep({ value, revive, glyphOf, nested = false }) {
24
24
  if (typeof value !== "object" || value === null) return value;
25
25
  if ("$$typeof" in value) return value;
26
26
  if (isPreviewElement(value)) return value;
27
- if (isComponentRef(value)) return revive(value);
28
- if (Array.isArray(value)) return value.map((item) => reviveDeep(item, revive));
27
+ if (isComponentRef(value)) return (nested ? glyphOf?.(value) : void 0) ?? revive(value);
28
+ if (Array.isArray(value)) return value.map((item) => reviveDeep({
29
+ value: item,
30
+ revive,
31
+ glyphOf,
32
+ nested: true
33
+ }));
29
34
  const out = {};
30
- for (const [key, member] of Object.entries(value)) out[key] = reviveDeep(member, revive);
35
+ for (const [key, member] of Object.entries(value)) out[key] = reviveDeep({
36
+ value: member,
37
+ revive,
38
+ glyphOf,
39
+ nested: true
40
+ });
31
41
  return out;
32
42
  }
33
43
  /**
@@ -42,7 +52,7 @@ function reviveDeep(value, revive) {
42
52
  * like any spec element. One component per ref, so the receiving component's element type is stable
43
53
  * across renders and React keeps the glyph mounted.
44
54
  */
45
- function wrapComponentRefs(reg, resolve) {
55
+ function wrapComponentRefs(reg, resolve, glyphOf) {
46
56
  const revived = /* @__PURE__ */ new Map();
47
57
  const revive = (ref) => {
48
58
  const type = ref.__ref;
@@ -72,7 +82,11 @@ function wrapComponentRefs(reg, resolve) {
72
82
  ...renderProps,
73
83
  element: {
74
84
  ...renderProps.element,
75
- props: reviveDeep(props, revive)
85
+ props: Object.fromEntries(Object.entries(props).map(([name, value]) => [name, reviveDeep({
86
+ value,
87
+ revive,
88
+ glyphOf
89
+ })]))
76
90
  }
77
91
  });
78
92
  };
@@ -5,6 +5,10 @@ import { ComponentRegistry } from "@json-render/react";
5
5
  * Set to `true` on an intrinsic element whose props are the tag's own attributes. A style-prop key
6
6
  * whose value names no class is then kept as the attribute rather than dropped, so a captured
7
7
  * `<svg width={16}>` keeps its `width`. The key never reaches the DOM.
8
+ *
9
+ * An element a component's render placed inline (the anatomy marker, `data-uds-anatomy`) is one
10
+ * without the flag: its props are the JSX attributes the module renders as attributes, so a
11
+ * `<path fillOpacity={0}>` keeps its `fill-opacity` as the module does.
8
12
  */
9
13
  declare const VERBATIM_ATTRIBUTES = "__verbatimAttributes";
10
14
  /**
@@ -1,3 +1,4 @@
1
+ import "../../framework/render-spec.js";
1
2
  import { specContent } from "../spec-content.js";
2
3
  import React from "react";
3
4
  //#region src/renderer/wrappers/html-aliases.tsx
@@ -16,6 +17,10 @@ const FORBIDDEN_INTRINSIC_PROPS = new Set(["dangerouslySetInnerHTML"]);
16
17
  * Set to `true` on an intrinsic element whose props are the tag's own attributes. A style-prop key
17
18
  * whose value names no class is then kept as the attribute rather than dropped, so a captured
18
19
  * `<svg width={16}>` keeps its `width`. The key never reaches the DOM.
20
+ *
21
+ * An element a component's render placed inline (the anatomy marker, `data-uds-anatomy`) is one
22
+ * without the flag: its props are the JSX attributes the module renders as attributes, so a
23
+ * `<path fillOpacity={0}>` keeps its `fill-opacity` as the module does.
19
24
  */
20
25
  const VERBATIM_ATTRIBUTES = "__verbatimAttributes";
21
26
  const NullEl = () => null;
@@ -43,7 +48,7 @@ function aliasHtmlTypes(reg, stylePropClasses) {
43
48
  if (typeof prop === "string" && INTRINSIC_TAG_RE.test(prop)) {
44
49
  const IntrinsicEl = ({ element, children, slots }) => {
45
50
  const props = element.props;
46
- const verbatim = props?.[VERBATIM_ATTRIBUTES] === true;
51
+ const verbatim = props?.["__verbatimAttributes"] === true || typeof props?.["data-uds-anatomy"] === "string";
47
52
  const intrinsicProps = {};
48
53
  const styleClasses = [];
49
54
  for (const [key, value] of Object.entries(props ?? {})) {