@yahoo/uds-create-config 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/AssetGroup.d.ts +77 -0
  2. package/dist/AssetGroup.js +125 -0
  3. package/dist/Component.d.ts +308 -0
  4. package/dist/Component.js +896 -0
  5. package/dist/ComponentGroup.d.ts +20 -0
  6. package/dist/ComponentGroup.js +46 -0
  7. package/dist/CompositeStyle.d.ts +27 -0
  8. package/dist/CompositeStyle.js +52 -0
  9. package/dist/Config.d.ts +423 -0
  10. package/dist/Config.js +1429 -0
  11. package/dist/Mode.d.ts +41 -0
  12. package/dist/Mode.js +81 -0
  13. package/dist/Modifier.d.ts +51 -0
  14. package/dist/Modifier.js +97 -0
  15. package/dist/MotionDef.d.ts +49 -0
  16. package/dist/MotionDef.js +97 -0
  17. package/dist/Props.d.ts +317 -0
  18. package/dist/Props.js +35 -0
  19. package/dist/Provider.d.ts +20 -0
  20. package/dist/Provider.js +14 -0
  21. package/dist/StyleProp.d.ts +112 -0
  22. package/dist/StyleProp.js +197 -0
  23. package/dist/Token.d.ts +55 -0
  24. package/dist/Token.js +112 -0
  25. package/dist/TokenGroup.d.ts +32 -0
  26. package/dist/TokenGroup.js +68 -0
  27. package/dist/asset-kind.d.ts +55 -0
  28. package/dist/asset-kind.js +29 -0
  29. package/dist/brands.d.ts +30 -0
  30. package/dist/brands.js +20 -0
  31. package/dist/captureCallerPath.d.ts +48 -0
  32. package/dist/captureCallerPath.js +95 -0
  33. package/dist/colorExpressions.d.ts +131 -0
  34. package/dist/colorExpressions.js +148 -0
  35. package/dist/defineAssetGroup.d.ts +166 -0
  36. package/dist/defineAssetGroup.js +264 -0
  37. package/dist/defineProvider.d.ts +29 -0
  38. package/dist/defineProvider.js +60 -0
  39. package/dist/element-marker.d.ts +54 -0
  40. package/dist/element-marker.js +113 -0
  41. package/dist/entity-utils.d.ts +56 -0
  42. package/dist/entity-utils.js +105 -0
  43. package/dist/factories.d.ts +707 -0
  44. package/dist/factories.js +393 -0
  45. package/dist/foreign-component-name.d.ts +21 -0
  46. package/dist/foreign-component-name.js +42 -0
  47. package/dist/index.d.ts +32 -0
  48. package/dist/index.js +28 -0
  49. package/dist/interpolate.d.ts +20 -0
  50. package/dist/interpolate.js +10 -0
  51. package/dist/jsx/__fixtures__/cross-component-preview.d.ts +3 -0
  52. package/dist/jsx/__fixtures__/cross-component-preview.js +15 -0
  53. package/dist/jsx/jsx-dev-runtime.d.ts +15 -0
  54. package/dist/jsx/jsx-dev-runtime.js +11 -0
  55. package/dist/jsx/jsx-runtime.d.ts +48 -0
  56. package/dist/jsx/jsx-runtime.js +305 -0
  57. package/dist/markers.d.ts +217 -0
  58. package/dist/markers.js +67 -0
  59. package/dist/refs.d.ts +158 -0
  60. package/dist/refs.js +104 -0
  61. package/dist/renderer/RendererErrorBoundary.d.ts +26 -0
  62. package/dist/renderer/RendererErrorBoundary.js +30 -0
  63. package/dist/renderer/UdsRenderer.d.ts +80 -0
  64. package/dist/renderer/UdsRenderer.js +33 -0
  65. package/dist/renderer/assetRenderable.d.ts +13 -0
  66. package/dist/renderer/assetRenderable.js +13 -0
  67. package/dist/renderer/index.d.ts +14 -0
  68. package/dist/renderer/index.js +14 -0
  69. package/dist/renderer/makeRegistry.d.ts +34 -0
  70. package/dist/renderer/makeRegistry.js +52 -0
  71. package/dist/renderer/makeUdsRenderer.d.ts +29 -0
  72. package/dist/renderer/makeUdsRenderer.js +35 -0
  73. package/dist/renderer/primitives/FragmentRenderer.d.ts +12 -0
  74. package/dist/renderer/primitives/FragmentRenderer.js +11 -0
  75. package/dist/renderer/primitives/SlotRenderer.d.ts +21 -0
  76. package/dist/renderer/primitives/SlotRenderer.js +20 -0
  77. package/dist/renderer/wrapRegistry.d.ts +64 -0
  78. package/dist/renderer/wrapRegistry.js +39 -0
  79. package/dist/renderer/wrappers/component-slots.d.ts +41 -0
  80. package/dist/renderer/wrappers/component-slots.js +66 -0
  81. package/dist/renderer/wrappers/event-bridge.d.ts +20 -0
  82. package/dist/renderer/wrappers/event-bridge.js +78 -0
  83. package/dist/renderer/wrappers/hex-normalize.d.ts +14 -0
  84. package/dist/renderer/wrappers/hex-normalize.js +35 -0
  85. package/dist/renderer/wrappers/html-aliases.d.ts +24 -0
  86. package/dist/renderer/wrappers/html-aliases.js +62 -0
  87. package/dist/renderer/wrappers/inline-styles.d.ts +16 -0
  88. package/dist/renderer/wrappers/inline-styles.js +104 -0
  89. package/dist/renderer/wrappers/slot-resolution.d.ts +25 -0
  90. package/dist/renderer/wrappers/slot-resolution.js +68 -0
  91. package/dist/renderer/wrappers/void-elements.d.ts +23 -0
  92. package/dist/renderer/wrappers/void-elements.js +28 -0
  93. package/dist/spec/apply-forced-modifiers.d.ts +19 -0
  94. package/dist/spec/apply-forced-modifiers.js +49 -0
  95. package/dist/spec/asset-jsx.d.ts +48 -0
  96. package/dist/spec/asset-jsx.js +48 -0
  97. package/dist/spec/collapse-text-labels.d.ts +44 -0
  98. package/dist/spec/collapse-text-labels.js +107 -0
  99. package/dist/spec/empty-node-slots.d.ts +51 -0
  100. package/dist/spec/empty-node-slots.js +141 -0
  101. package/dist/spec/index.d.ts +10 -0
  102. package/dist/spec/index.js +10 -0
  103. package/dist/spec/jsxToSpec.d.ts +48 -0
  104. package/dist/spec/jsxToSpec.js +506 -0
  105. package/dist/spec/layer-props.d.ts +52 -0
  106. package/dist/spec/layer-props.js +149 -0
  107. package/dist/spec/preview-controls.d.ts +44 -0
  108. package/dist/spec/preview-controls.js +139 -0
  109. package/dist/spec/slot-refs.d.ts +39 -0
  110. package/dist/spec/slot-refs.js +56 -0
  111. package/dist/spec/specToJsx.d.ts +35 -0
  112. package/dist/spec/specToJsx.js +127 -0
  113. package/dist/token-override-rows.d.ts +66 -0
  114. package/dist/token-override-rows.js +223 -0
  115. package/dist/tokenValueType.d.ts +34 -0
  116. package/dist/tokenValueType.js +138 -0
  117. package/dist/tsconfig.tsbuildinfo +1 -0
  118. package/dist/types/css-properties.d.ts +232 -0
  119. package/dist/types/css-properties.js +14 -0
  120. package/dist/types/css-property-keywords.d.ts +156 -0
  121. package/dist/types/css-property-keywords.js +616 -0
  122. package/dist/types/css-values.d.ts +63 -0
  123. package/dist/types/css-values.js +16 -0
  124. package/dist/types.d.ts +708 -0
  125. package/dist/types.js +12 -0
  126. package/dist/units.d.ts +14 -0
  127. package/dist/units.js +16 -0
  128. package/dist/utils/index.d.ts +4 -0
  129. package/dist/utils/index.js +4 -0
  130. package/package.json +81 -0
@@ -0,0 +1,68 @@
1
+ import { isSlotRef } from "../../spec/slot-refs.js";
2
+ import React from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/renderer/wrappers/slot-resolution.tsx
5
+ /**
6
+ * Resolve `$slot` prop references to rendered React children.
7
+ *
8
+ * Components like `Card` declare named slots in their authoring spec:
9
+ *
10
+ * props: {
11
+ * title: slot('header/text'),
12
+ * footer: slot('footer'),
13
+ * }
14
+ *
15
+ * Codegen lowers each `slot(...)` to a `{ $slot: 'key' }` marker that
16
+ * surfaces in `element.props`. At render time this wrapper pairs each
17
+ * slot key against the keyed React children passed in by `Renderer`
18
+ * (json-render assigns each rendered child a key derived from its
19
+ * spec element id), pulls the matching child out of the regular
20
+ * children stream, and feeds it through as the resolved prop value.
21
+ * Children whose keys don't match any slot pass through to the
22
+ * component as regular `children`.
23
+ */
24
+ function wrapSlotResolution(reg) {
25
+ const wrapped = {};
26
+ for (const [name, Comp] of Object.entries(reg)) wrapped[name] = function SlotWrapper(renderProps) {
27
+ const { element, children } = renderProps;
28
+ const props = element.props;
29
+ if (!props) return /* @__PURE__ */ jsx(Comp, { ...renderProps });
30
+ const slotEntries = [];
31
+ for (const [propName, value] of Object.entries(props)) if (isSlotRef(value)) slotEntries.push([propName, value.$slot]);
32
+ if (slotEntries.length === 0) return /* @__PURE__ */ jsx(Comp, { ...renderProps });
33
+ const slotKeySet = /* @__PURE__ */ new Set();
34
+ for (const [, ref] of slotEntries) {
35
+ const refs = Array.isArray(ref) ? ref : [ref];
36
+ for (const r of refs) slotKeySet.add(r);
37
+ }
38
+ const childArray = React.Children.toArray(children);
39
+ const slotChildMap = /* @__PURE__ */ new Map();
40
+ const regularChildren = [];
41
+ for (const child of childArray) {
42
+ if (React.isValidElement(child) && typeof child.key === "string") {
43
+ const elementKey = child.key.startsWith(".$") ? child.key.slice(2) : child.key;
44
+ if (slotKeySet.has(elementKey)) {
45
+ slotChildMap.set(elementKey, child);
46
+ continue;
47
+ }
48
+ }
49
+ regularChildren.push(child);
50
+ }
51
+ const resolvedProps = { ...props };
52
+ for (const [propName, ref] of slotEntries) if (Array.isArray(ref)) {
53
+ const resolved = ref.map((key) => slotChildMap.get(key)).filter(Boolean);
54
+ resolvedProps[propName] = resolved.length > 0 ? resolved : void 0;
55
+ } else resolvedProps[propName] = slotChildMap.get(ref) ?? void 0;
56
+ return /* @__PURE__ */ jsx(Comp, {
57
+ ...renderProps,
58
+ element: {
59
+ ...element,
60
+ props: resolvedProps
61
+ },
62
+ children: regularChildren.length > 0 ? regularChildren : void 0
63
+ });
64
+ };
65
+ return wrapped;
66
+ }
67
+ //#endregion
68
+ export { wrapSlotResolution };
@@ -0,0 +1,23 @@
1
+ import { ComponentRegistry } from "@json-render/react";
2
+
3
+ //#region src/renderer/wrappers/void-elements.d.ts
4
+ /**
5
+ * Strip `children` from void HTML elements (e.g. `<img>`-backed UDS
6
+ * components) that React refuses to render with children. Source
7
+ * Specs sometimes include a `children` prop on these — silently drop
8
+ * it rather than letting React throw.
9
+ *
10
+ * `voidComponents` is config-derived (component names whose root renders a
11
+ * void HTML tag), so this stays config-agnostic. Matches on the
12
+ * namespace-stripped local name, so a namespaced registry (`yahoo-os:Image`)
13
+ * is recognized, not just the bare `Image` key. No void set ⇒ no-op.
14
+ */
15
+ interface VoidElementsConfig {
16
+ /** Component names whose root renders a void HTML tag (`img`, `input`, …). */
17
+ voidComponents?: readonly string[];
18
+ }
19
+ declare function wrapVoidElements(reg: ComponentRegistry, {
20
+ voidComponents
21
+ }?: VoidElementsConfig): ComponentRegistry;
22
+ //#endregion
23
+ export { wrapVoidElements };
@@ -0,0 +1,28 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ //#region src/renderer/wrappers/void-elements.tsx
3
+ /** Local component name of a registry key: `yahoo-os:Image` → `Image`,
4
+ * `Image` → `Image`. Registry keys are `<namespace>:<Name>`; component names
5
+ * never contain `:`, so split on the first one. */
6
+ function localName(key) {
7
+ const colon = key.indexOf(":");
8
+ return colon === -1 ? key : key.slice(colon + 1);
9
+ }
10
+ function wrapVoidElements(reg, { voidComponents } = {}) {
11
+ if (!voidComponents || voidComponents.length === 0) return reg;
12
+ const voidSet = new Set(voidComponents);
13
+ const wrapped = { ...reg };
14
+ for (const key of Object.keys(reg)) {
15
+ if (!voidSet.has(localName(key))) continue;
16
+ const Orig = wrapped[key];
17
+ if (!Orig) continue;
18
+ wrapped[key] = function VoidElementWrapper(p) {
19
+ return /* @__PURE__ */ jsx(Orig, {
20
+ ...p,
21
+ children: void 0
22
+ });
23
+ };
24
+ }
25
+ return wrapped;
26
+ }
27
+ //#endregion
28
+ export { wrapVoidElements };
@@ -0,0 +1,19 @@
1
+ //#region src/spec/apply-forced-modifiers.d.ts
2
+ type Props = Record<string, unknown>;
3
+ interface SpecShape {
4
+ root: string;
5
+ elements: Record<string, {
6
+ props?: Props;
7
+ }>;
8
+ }
9
+ /**
10
+ * Return a copy of `spec` with each active state modifier folded into concrete
11
+ * style props on every element that declares it. `activeModifiers` are the
12
+ * forced states for this cell (e.g. `['_hover']`); `state` is the seeded
13
+ * discriminator model (`{ variant, size, __previewSeedRow, … }`) the modifier
14
+ * blocks' `$cond`s key off. Non-mutating: only changed elements are rebuilt,
15
+ * and an empty `activeModifiers` returns the input spec unchanged.
16
+ */
17
+ declare function applyForcedModifiers<S extends SpecShape>(spec: S, activeModifiers: readonly string[], state: Props): S;
18
+ //#endregion
19
+ export { applyForcedModifiers };
@@ -0,0 +1,49 @@
1
+ import { evaluateVisibility } from "@json-render/core";
2
+ //#region src/spec/apply-forced-modifiers.ts
3
+ function isCondValue(value) {
4
+ return typeof value === "object" && value !== null && "$cond" in value;
5
+ }
6
+ function resolveBlock(value, state) {
7
+ if (value === null || typeof value !== "object") return void 0;
8
+ if (isCondValue(value)) return resolveBlock(evaluateVisibility(value.$cond, { stateModel: state }) ? value.$then : value.$else, state);
9
+ return value;
10
+ }
11
+ /**
12
+ * Return a copy of `spec` with each active state modifier folded into concrete
13
+ * style props on every element that declares it. `activeModifiers` are the
14
+ * forced states for this cell (e.g. `['_hover']`); `state` is the seeded
15
+ * discriminator model (`{ variant, size, __previewSeedRow, … }`) the modifier
16
+ * blocks' `$cond`s key off. Non-mutating: only changed elements are rebuilt,
17
+ * and an empty `activeModifiers` returns the input spec unchanged.
18
+ */
19
+ function applyForcedModifiers(spec, activeModifiers, state) {
20
+ if (activeModifiers.length === 0) return spec;
21
+ let changed = false;
22
+ const nextElements = {};
23
+ for (const [key, element] of Object.entries(spec.elements)) {
24
+ const props = element.props;
25
+ const present = props ? activeModifiers.filter((mod) => mod in props) : [];
26
+ if (!props || present.length === 0) {
27
+ nextElements[key] = element;
28
+ continue;
29
+ }
30
+ const nextProps = { ...props };
31
+ for (const mod of present) {
32
+ const block = resolveBlock(nextProps[mod], state);
33
+ if (block) Object.assign(nextProps, block);
34
+ delete nextProps[mod];
35
+ }
36
+ nextElements[key] = {
37
+ ...element,
38
+ props: nextProps
39
+ };
40
+ changed = true;
41
+ }
42
+ if (!changed) return spec;
43
+ return {
44
+ ...spec,
45
+ elements: nextElements
46
+ };
47
+ }
48
+ //#endregion
49
+ export { applyForcedModifiers };
@@ -0,0 +1,48 @@
1
+ //#region src/spec/asset-jsx.d.ts
2
+ /** Pull the slug + member out of an asset id, or `null` for a normal type. */
3
+ declare function parseAssetType(type: string): {
4
+ slug: string;
5
+ member: string;
6
+ } | null;
7
+ /**
8
+ * `slug → authoring-component name` (e.g. `{ icons: 'Icon' }`). Derived from
9
+ * the asset groups that declare a `component`; a slug missing from the map has
10
+ * no authoring component and falls back to the bare member tag.
11
+ */
12
+ type AssetComponentMap = Readonly<Record<string, string>>;
13
+ /** How a placed asset node should appear in exported JSX. */
14
+ interface AssetJsxForm {
15
+ /** The JSX tag — the authoring component (`Icon`) or the bare member. */
16
+ tag: string;
17
+ /**
18
+ * The `name` attribute value when the authoring component carries the
19
+ * member identity (`name="Trophy"`). Absent on the bare-member fallback,
20
+ * which IS the member.
21
+ */
22
+ nameAttr?: string;
23
+ /** Identifier to import — the component when mapped, else the member. */
24
+ importName: string;
25
+ }
26
+ /**
27
+ * Resolve a spec element `type` to its exported-JSX form, or `null` when the
28
+ * type is not an asset node (a normal component — caller serializes as usual).
29
+ *
30
+ * - mapped slug → `<Icon name="Member" … />` (component-typed export);
31
+ * - unmapped slug → `<Member … />` (component-less fallback — a self-contained
32
+ * member renders itself; a bare-path glyph would need raw `<svg>`, which the
33
+ * string-only serializers can't synthesize, so the member tag is the best
34
+ * they can emit).
35
+ */
36
+ declare function assetJsxForm(type: string, assetComponents?: AssetComponentMap): AssetJsxForm | null;
37
+ /**
38
+ * Inverse of `assetJsxForm` for the editor round-trip: given a parsed JSX tag
39
+ * and its props, reconstruct the asset-node `type` string — or `null` if the
40
+ * tag is a genuine component, not an authoring component placing a known member.
41
+ * The caller closes over its live config to find the group whose `component`
42
+ * matches `tag` and whose members include the `name` value, then returns
43
+ * `group.assetType(name)`. When it resolves, the parser drops the `name` prop
44
+ * (the member identity lives in the type) and keeps the rest (variant, size).
45
+ */
46
+ type AssetNodeResolver = (tag: string, props: Readonly<Record<string, unknown>>) => string | null;
47
+ //#endregion
48
+ export { AssetComponentMap, AssetJsxForm, AssetNodeResolver, assetJsxForm, parseAssetType };
@@ -0,0 +1,48 @@
1
+ //#region src/spec/asset-jsx.ts
2
+ const ASSET_MARKER = "asset:";
3
+ const NAMESPACED_ASSET_MARKER = ":asset:";
4
+ /** Pull the slug + member out of an asset id, or `null` for a normal type. */
5
+ function parseAssetType(type) {
6
+ let bodyStart;
7
+ if (type.startsWith(ASSET_MARKER)) bodyStart = 6;
8
+ else {
9
+ const markerIndex = type.indexOf(NAMESPACED_ASSET_MARKER);
10
+ if (markerIndex === -1) return null;
11
+ bodyStart = markerIndex + 7;
12
+ }
13
+ const slashIndex = type.indexOf("/", bodyStart);
14
+ if (slashIndex === -1) return null;
15
+ const slug = type.slice(bodyStart, slashIndex);
16
+ const member = type.slice(slashIndex + 1);
17
+ if (!slug || !member) return null;
18
+ return {
19
+ slug,
20
+ member
21
+ };
22
+ }
23
+ /**
24
+ * Resolve a spec element `type` to its exported-JSX form, or `null` when the
25
+ * type is not an asset node (a normal component — caller serializes as usual).
26
+ *
27
+ * - mapped slug → `<Icon name="Member" … />` (component-typed export);
28
+ * - unmapped slug → `<Member … />` (component-less fallback — a self-contained
29
+ * member renders itself; a bare-path glyph would need raw `<svg>`, which the
30
+ * string-only serializers can't synthesize, so the member tag is the best
31
+ * they can emit).
32
+ */
33
+ function assetJsxForm(type, assetComponents) {
34
+ const parsed = parseAssetType(type);
35
+ if (!parsed) return null;
36
+ const component = assetComponents?.[parsed.slug];
37
+ if (component) return {
38
+ tag: component,
39
+ nameAttr: parsed.member,
40
+ importName: component
41
+ };
42
+ return {
43
+ tag: parsed.member,
44
+ importName: parsed.member
45
+ };
46
+ }
47
+ //#endregion
48
+ export { assetJsxForm, parseAssetType };
@@ -0,0 +1,44 @@
1
+ //#region src/spec/collapse-text-labels.d.ts
2
+ type Props = Record<string, unknown>;
3
+ interface ElementShape {
4
+ type: string;
5
+ props?: Props;
6
+ children?: unknown;
7
+ }
8
+ interface SpecShape {
9
+ root: string;
10
+ elements: Record<string, ElementShape>;
11
+ }
12
+ interface ComponentClassification {
13
+ derived: {
14
+ acceptsChildren: boolean;
15
+ acceptsChildNodes: boolean;
16
+ };
17
+ }
18
+ interface ConfigLike {
19
+ getComponent(type: string): ComponentClassification | undefined;
20
+ }
21
+ /** One collapse the transform performed — surfaced for lint/reporting. */
22
+ interface CollapsedTextLabel {
23
+ /** The string-label component whose label was inlined. */
24
+ parentKey: string;
25
+ /** The string-label component's spec `type` (e.g. `yahoo-os:Badge`). */
26
+ parentType: string;
27
+ /** The wrapped Text element that was removed (when it wasn't shared). */
28
+ childKey: string;
29
+ /** The label text hoisted onto the parent's `props.children`. */
30
+ text: string;
31
+ }
32
+ interface CollapseTextLabelsResult<S extends SpecShape> {
33
+ spec: S;
34
+ collapsed: CollapsedTextLabel[];
35
+ }
36
+ /**
37
+ * Return a copy of `spec` with every redundant Text-wrapped label folded into a
38
+ * string `children` on the string-label component that wraps it (see file
39
+ * header). Non-mutating: unchanged elements are reused, and a spec with no such
40
+ * wrappers is returned as-is with an empty `collapsed` report.
41
+ */
42
+ declare function collapseWrappedTextLabels<S extends SpecShape>(spec: S, config: ConfigLike): CollapseTextLabelsResult<S>;
43
+ //#endregion
44
+ export { CollapseTextLabelsResult, CollapsedTextLabel, collapseWrappedTextLabels };
@@ -0,0 +1,107 @@
1
+ //#region src/spec/collapse-text-labels.ts
2
+ /** Child element keys an element references through its `children` array. */
3
+ function childKeysOf(element) {
4
+ if (!Array.isArray(element.children)) return [];
5
+ return element.children.filter((c) => typeof c === "string");
6
+ }
7
+ /**
8
+ * Count every parent→child edge in the spec so a wrapped Text node is only
9
+ * deleted when nothing *else* still points at it. Covers `children` arrays and
10
+ * top-level `$slot` prop refs (the two ways the renderer reaches an element).
11
+ */
12
+ function countReferences(elements) {
13
+ const counts = /* @__PURE__ */ new Map();
14
+ const bump = (key) => counts.set(key, (counts.get(key) ?? 0) + 1);
15
+ for (const element of Object.values(elements)) {
16
+ for (const child of childKeysOf(element)) bump(child);
17
+ if (!element.props) continue;
18
+ for (const value of Object.values(element.props)) {
19
+ if (typeof value !== "object" || value === null || !("$slot" in value)) continue;
20
+ const slot = value.$slot;
21
+ const refs = Array.isArray(slot) ? slot : [slot];
22
+ for (const ref of refs) if (typeof ref === "string") bump(ref);
23
+ }
24
+ }
25
+ return counts;
26
+ }
27
+ /**
28
+ * A *bare string leaf* is an element whose only rendered content is a string
29
+ * `children` prop: no element children, and no prop other than `children`. Such
30
+ * a node contributes exactly its text, so flattening it onto a string-label
31
+ * parent is lossless. Returns the string, or `undefined` if the element carries
32
+ * anything more (own styling, icons, nested children) and must be preserved.
33
+ */
34
+ function bareLabelText(element) {
35
+ if (childKeysOf(element).length > 0) return void 0;
36
+ const props = element.props;
37
+ if (!props) return void 0;
38
+ const text = props.children;
39
+ if (typeof text !== "string") return void 0;
40
+ if (Object.keys(props).filter((k) => k !== "children").length > 0) return void 0;
41
+ return text;
42
+ }
43
+ /**
44
+ * Return a copy of `spec` with every redundant Text-wrapped label folded into a
45
+ * string `children` on the string-label component that wraps it (see file
46
+ * header). Non-mutating: unchanged elements are reused, and a spec with no such
47
+ * wrappers is returned as-is with an empty `collapsed` report.
48
+ */
49
+ function collapseWrappedTextLabels(spec, config) {
50
+ const collapsed = [];
51
+ const refCounts = countReferences(spec.elements);
52
+ const rewrites = /* @__PURE__ */ new Map();
53
+ const toDelete = /* @__PURE__ */ new Set();
54
+ for (const [key, element] of Object.entries(spec.elements)) {
55
+ const component = config.getComponent(element.type);
56
+ if (!component) continue;
57
+ const { acceptsChildren, acceptsChildNodes } = component.derived;
58
+ if (!acceptsChildren || acceptsChildNodes) continue;
59
+ const childKeys = childKeysOf(element);
60
+ if (childKeys.length !== 1) continue;
61
+ const existingLabel = element.props?.children;
62
+ if (typeof existingLabel === "string" && existingLabel !== "") continue;
63
+ const childKey = childKeys[0];
64
+ const child = spec.elements[childKey];
65
+ if (!child) continue;
66
+ const text = bareLabelText(child);
67
+ if (text === void 0) continue;
68
+ rewrites.set(key, text);
69
+ if ((refCounts.get(childKey) ?? 0) <= 1) toDelete.add(childKey);
70
+ collapsed.push({
71
+ parentKey: key,
72
+ parentType: element.type,
73
+ childKey,
74
+ text
75
+ });
76
+ }
77
+ if (collapsed.length === 0) return {
78
+ spec,
79
+ collapsed
80
+ };
81
+ const nextElements = {};
82
+ for (const [key, element] of Object.entries(spec.elements)) {
83
+ if (toDelete.has(key)) continue;
84
+ const label = rewrites.get(key);
85
+ if (label === void 0) {
86
+ nextElements[key] = element;
87
+ continue;
88
+ }
89
+ const { children: _dropped, ...rest } = element;
90
+ nextElements[key] = {
91
+ ...rest,
92
+ props: {
93
+ ...element.props,
94
+ children: label
95
+ }
96
+ };
97
+ }
98
+ return {
99
+ spec: {
100
+ ...spec,
101
+ elements: nextElements
102
+ },
103
+ collapsed
104
+ };
105
+ }
106
+ //#endregion
107
+ export { collapseWrappedTextLabels };
@@ -0,0 +1,51 @@
1
+ import { Component } from "../Component.js";
2
+ import { Spec } from "@json-render/core";
3
+
4
+ //#region src/spec/empty-node-slots.d.ts
5
+ /**
6
+ * Materialize every EMPTY layer-bound node slot of `spec`'s root as a `Slot`
7
+ * element wired through a `$slot` ref, so an unfilled slot is a stable element
8
+ * in the spec rather than an absent prop — the studio canvas needs a drop
9
+ * target, the layers panel a row, and the inspector an "Empty" state. Slots the
10
+ * spec already fills are left as-is. Returns `spec` unchanged when there's
11
+ * nothing to inject (no node slots, or all of them filled).
12
+ */
13
+ declare function withEmptyNodeSlots(spec: Spec, component: Pick<Component, 'derived'>): Spec;
14
+ /**
15
+ * Pin a persistent trailing `Slot` as the last child of every container element
16
+ * in `spec` (tree-wide), so a container always exposes an insertion drop target
17
+ * at the bottom of its children — the same `Slot` model used for node slots,
18
+ * but appended to `children` rather than replacing the element when filled.
19
+ * Idempotent: a container whose children already end in a `Slot` is left as-is,
20
+ * so re-running (and filling, which inserts *before* the trailing slot) never
21
+ * stacks duplicates. Returns `spec` unchanged when there's nothing to add.
22
+ */
23
+ declare function withContainerChildSlots(spec: Spec): Spec;
24
+ /**
25
+ * Compose both slot injections into the canvas's render view: layer-bound node
26
+ * slots on a component instance's root (`withEmptyNodeSlots`, needs the
27
+ * component definition) plus a trailing insertion `Slot` in every container,
28
+ * tree-wide (`withContainerChildSlots`). This is the spec the studio canvas
29
+ * renders so empty slots show as drop targets — it is NEVER persisted. Stored
30
+ * specs stay slot-free; `stripInjectedSlots` is the inverse, applied at every
31
+ * serialization boundary (DB, clipboard, AI prompt). Both helpers return the
32
+ * same reference when there's nothing to add, so this is cheap and idempotent —
33
+ * re-running on an already-slotted spec adds nothing.
34
+ */
35
+ declare function withRenderSlots(spec: Spec, component: Pick<Component, 'derived'> | null | undefined): Spec;
36
+ /**
37
+ * Inverse of `withEmptyNodeSlots` + `withContainerChildSlots`: drop every
38
+ * injected `Slot` element and scrub its references, so a spec carries no
39
+ * editing scaffolding. A `Slot`-typed element is ALWAYS an injected content
40
+ * hole (json-render's placeholder), never authored content — both injectors
41
+ * emit `type: 'Slot'` — so removing all of them and their references is the
42
+ * exact reversal: a trailing `children` Slot drops out of its parent's
43
+ * `children`, and an empty node slot drops both its `Slot` element and the
44
+ * `{ $slot }` prop that pointed at it (a FILLED slot points at a real element,
45
+ * which is not `Slot`-typed, so it survives). Use at export/serialization
46
+ * boundaries (e.g. clipboard); the canvas re-injects fresh drop targets on
47
+ * load. Idempotent: returns `spec` unchanged when there are no `Slot` elements.
48
+ */
49
+ declare function stripInjectedSlots(spec: Spec): Spec;
50
+ //#endregion
51
+ export { stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
@@ -0,0 +1,141 @@
1
+ //#region src/spec/empty-node-slots.ts
2
+ const SLOT_TYPE = "Slot";
3
+ /**
4
+ * Layer-bound single-node slots — Button `startIcon`/`endIcon`, Item `media`:
5
+ * a `slot()` bound to a named layer whose value is a renderable node. Open
6
+ * `children` slots (no layer) are authored as child layers, and text/scalar
7
+ * slots as inputs, so neither is materialized here.
8
+ */
9
+ function nodeSlotNames(component) {
10
+ return component.derived.props.filter((p) => p.kind === "slot" && p.layer !== void 0 && p.valueType === "ReactNode" && p.authoredType !== "string").map((p) => p.name);
11
+ }
12
+ /**
13
+ * Materialize every EMPTY layer-bound node slot of `spec`'s root as a `Slot`
14
+ * element wired through a `$slot` ref, so an unfilled slot is a stable element
15
+ * in the spec rather than an absent prop — the studio canvas needs a drop
16
+ * target, the layers panel a row, and the inspector an "Empty" state. Slots the
17
+ * spec already fills are left as-is. Returns `spec` unchanged when there's
18
+ * nothing to inject (no node slots, or all of them filled).
19
+ */
20
+ function withEmptyNodeSlots(spec, component) {
21
+ const names = nodeSlotNames(component);
22
+ if (names.length === 0) return spec;
23
+ const rootKey = spec.root;
24
+ const next = structuredClone(spec);
25
+ const elements = next.elements;
26
+ const root = elements[rootKey];
27
+ if (!root) return spec;
28
+ root.props = root.props ?? {};
29
+ let injected = false;
30
+ for (const name of names) {
31
+ if (root.props[name] !== void 0) continue;
32
+ const key = `${rootKey}__${name}__slot`;
33
+ elements[key] = {
34
+ type: SLOT_TYPE,
35
+ props: { name }
36
+ };
37
+ root.props[name] = { $slot: key };
38
+ injected = true;
39
+ }
40
+ return injected ? next : spec;
41
+ }
42
+ const CONTAINER_TYPES = new Set([
43
+ "VStack",
44
+ "HStack",
45
+ "Box",
46
+ "ZStack",
47
+ "Flex",
48
+ "Grid",
49
+ "ScrollView"
50
+ ]);
51
+ /**
52
+ * Pin a persistent trailing `Slot` as the last child of every container element
53
+ * in `spec` (tree-wide), so a container always exposes an insertion drop target
54
+ * at the bottom of its children — the same `Slot` model used for node slots,
55
+ * but appended to `children` rather than replacing the element when filled.
56
+ * Idempotent: a container whose children already end in a `Slot` is left as-is,
57
+ * so re-running (and filling, which inserts *before* the trailing slot) never
58
+ * stacks duplicates. Returns `spec` unchanged when there's nothing to add.
59
+ */
60
+ function withContainerChildSlots(spec) {
61
+ const elements = spec.elements;
62
+ const targets = Object.keys(elements).filter((key) => {
63
+ const el = elements[key];
64
+ if (!el || !CONTAINER_TYPES.has(el.type ?? "")) return false;
65
+ const children = el.children ?? [];
66
+ return elements[children[children.length - 1] ?? ""]?.type !== SLOT_TYPE;
67
+ });
68
+ if (targets.length === 0) return spec;
69
+ const next = structuredClone(spec);
70
+ const nextEls = next.elements;
71
+ const used = new Set(Object.keys(nextEls));
72
+ for (const key of targets) {
73
+ let slotKey = `${key}__children__slot`;
74
+ let n = 0;
75
+ while (used.has(slotKey)) slotKey = `${key}__children__slot__${++n}`;
76
+ used.add(slotKey);
77
+ nextEls[slotKey] = {
78
+ type: SLOT_TYPE,
79
+ props: { name: "children" }
80
+ };
81
+ const el = nextEls[key];
82
+ el.children = [...el.children ?? [], slotKey];
83
+ }
84
+ return next;
85
+ }
86
+ /**
87
+ * Compose both slot injections into the canvas's render view: layer-bound node
88
+ * slots on a component instance's root (`withEmptyNodeSlots`, needs the
89
+ * component definition) plus a trailing insertion `Slot` in every container,
90
+ * tree-wide (`withContainerChildSlots`). This is the spec the studio canvas
91
+ * renders so empty slots show as drop targets — it is NEVER persisted. Stored
92
+ * specs stay slot-free; `stripInjectedSlots` is the inverse, applied at every
93
+ * serialization boundary (DB, clipboard, AI prompt). Both helpers return the
94
+ * same reference when there's nothing to add, so this is cheap and idempotent —
95
+ * re-running on an already-slotted spec adds nothing.
96
+ */
97
+ function withRenderSlots(spec, component) {
98
+ return withContainerChildSlots(component ? withEmptyNodeSlots(spec, component) : spec);
99
+ }
100
+ function slotRefKeys(value) {
101
+ if (typeof value !== "object" || value === null || !("$slot" in value)) return;
102
+ const ref = value.$slot;
103
+ if (typeof ref === "string") return [ref];
104
+ return Array.isArray(ref) ? ref : void 0;
105
+ }
106
+ /**
107
+ * Inverse of `withEmptyNodeSlots` + `withContainerChildSlots`: drop every
108
+ * injected `Slot` element and scrub its references, so a spec carries no
109
+ * editing scaffolding. A `Slot`-typed element is ALWAYS an injected content
110
+ * hole (json-render's placeholder), never authored content — both injectors
111
+ * emit `type: 'Slot'` — so removing all of them and their references is the
112
+ * exact reversal: a trailing `children` Slot drops out of its parent's
113
+ * `children`, and an empty node slot drops both its `Slot` element and the
114
+ * `{ $slot }` prop that pointed at it (a FILLED slot points at a real element,
115
+ * which is not `Slot`-typed, so it survives). Use at export/serialization
116
+ * boundaries (e.g. clipboard); the canvas re-injects fresh drop targets on
117
+ * load. Idempotent: returns `spec` unchanged when there are no `Slot` elements.
118
+ */
119
+ function stripInjectedSlots(spec) {
120
+ const elements = spec.elements;
121
+ const slotKeys = new Set(Object.keys(elements).filter((k) => elements[k]?.type === SLOT_TYPE));
122
+ if (slotKeys.size === 0) return spec;
123
+ const next = structuredClone(spec);
124
+ const nextEls = next.elements;
125
+ for (const key of slotKeys) delete nextEls[key];
126
+ for (const el of Object.values(nextEls)) {
127
+ if (!el) continue;
128
+ if (Array.isArray(el.children)) el.children = el.children.filter((c) => !slotKeys.has(c));
129
+ if (!el.props) continue;
130
+ for (const [propName, value] of Object.entries(el.props)) {
131
+ const refs = slotRefKeys(value);
132
+ if (!refs) continue;
133
+ const remaining = refs.filter((r) => !slotKeys.has(r));
134
+ if (remaining.length === 0) delete el.props[propName];
135
+ else if (remaining.length !== refs.length) el.props[propName] = { $slot: remaining };
136
+ }
137
+ }
138
+ return next;
139
+ }
140
+ //#endregion
141
+ export { stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
@@ -0,0 +1,10 @@
1
+ import { applyForcedModifiers } from "./apply-forced-modifiers.js";
2
+ import { AssetComponentMap, AssetJsxForm, AssetNodeResolver, assetJsxForm, parseAssetType } from "./asset-jsx.js";
3
+ import { CollapseTextLabelsResult, CollapsedTextLabel, collapseWrappedTextLabels } from "./collapse-text-labels.js";
4
+ import { stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots } from "./empty-node-slots.js";
5
+ import { JsxToSpecOptions, UNRESOLVED, hasUnresolvedProps, jsxToSpec, stripUnresolvedProps } from "./jsxToSpec.js";
6
+ import { ResolvableComponent, resolveAuthoredLayerProps, resolveRenderedLayerProps } from "./layer-props.js";
7
+ import { PreviewControl, PreviewControlKind, derivePreviewControls, propOptions } from "./preview-controls.js";
8
+ import { collectSlotRefs, isSlotRef } from "./slot-refs.js";
9
+ import { SpecToJsxOptions, SpecToJsxResult, specToJsx } from "./specToJsx.js";
10
+ export { type AssetComponentMap, type AssetJsxForm, type AssetNodeResolver, type CollapseTextLabelsResult, type CollapsedTextLabel, type JsxToSpecOptions, type PreviewControl, type PreviewControlKind, type ResolvableComponent, type SpecToJsxOptions, type SpecToJsxResult, UNRESOLVED, applyForcedModifiers, assetJsxForm, collapseWrappedTextLabels, collectSlotRefs, derivePreviewControls, hasUnresolvedProps, isSlotRef, jsxToSpec, parseAssetType, propOptions, resolveAuthoredLayerProps, resolveRenderedLayerProps, specToJsx, stripInjectedSlots, stripUnresolvedProps, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
@@ -0,0 +1,10 @@
1
+ import { collectSlotRefs, isSlotRef } from "./slot-refs.js";
2
+ import { applyForcedModifiers } from "./apply-forced-modifiers.js";
3
+ import { assetJsxForm, parseAssetType } from "./asset-jsx.js";
4
+ import { collapseWrappedTextLabels } from "./collapse-text-labels.js";
5
+ import { stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots } from "./empty-node-slots.js";
6
+ import { UNRESOLVED, hasUnresolvedProps, jsxToSpec, stripUnresolvedProps } from "./jsxToSpec.js";
7
+ import { resolveAuthoredLayerProps, resolveRenderedLayerProps } from "./layer-props.js";
8
+ import { derivePreviewControls, propOptions } from "./preview-controls.js";
9
+ import { specToJsx } from "./specToJsx.js";
10
+ export { UNRESOLVED, applyForcedModifiers, assetJsxForm, collapseWrappedTextLabels, collectSlotRefs, derivePreviewControls, hasUnresolvedProps, isSlotRef, jsxToSpec, parseAssetType, propOptions, resolveAuthoredLayerProps, resolveRenderedLayerProps, specToJsx, stripInjectedSlots, stripUnresolvedProps, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };