@yahoo/uds-create-config 3.1.0 → 3.3.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.
Files changed (36) hide show
  1. package/dist/configs/CanvasConfig.d.ts +66 -3
  2. package/dist/configs/react-native-system.d.ts +46 -2
  3. package/dist/configs/react-native-system.js +4 -0
  4. package/dist/configs/system.d.ts +46 -2
  5. package/dist/configs/system.js +4 -0
  6. package/dist/entities/system/Component.d.ts +40 -2
  7. package/dist/entities/system/Component.js +74 -9
  8. package/dist/entities/system/File.d.ts +1 -0
  9. package/dist/entities/system/File.js +27 -2
  10. package/dist/entities/system/style-bag.js +24 -5
  11. package/dist/framework/Config.d.ts +1 -1
  12. package/dist/framework/Config.js +1 -1
  13. package/dist/framework/anatomy-check.d.ts +99 -0
  14. package/dist/framework/anatomy-check.js +337 -0
  15. package/dist/framework/class-names.js +6 -2
  16. package/dist/framework/defineEntity.d.ts +1 -0
  17. package/dist/framework/ref-integrity.js +17 -3
  18. package/dist/framework/registry.d.ts +1 -0
  19. package/dist/framework/render-spec.d.ts +12 -1
  20. package/dist/framework/render-spec.js +122 -28
  21. package/dist/framework/schema-version.d.ts +1 -1
  22. package/dist/framework/schema-version.js +5 -5
  23. package/dist/index.d.ts +5 -3
  24. package/dist/index.js +5 -3
  25. package/dist/migrations/2.0.0/v1-artifact.d.ts +23 -1
  26. package/dist/migrations/20260914155048_component_create_blank_op.d.ts +13 -0
  27. package/dist/migrations/20260914155048_component_create_blank_op.js +8 -0
  28. package/dist/migrations/20260914181845_anatomy_harness.d.ts +23 -0
  29. package/dist/migrations/20260914181845_anatomy_harness.js +10 -0
  30. package/dist/renderer/assetEntries.js +2 -3
  31. package/dist/renderer/spec-content.js +1 -1
  32. package/dist/renderer/wrappers/inline-styles.js +21 -1
  33. package/dist/spec/index.d.ts +1 -1
  34. package/dist/spec/specToJsx.js +26 -2
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
@@ -8,7 +8,7 @@ import { derivedColor, gradient } from "./color.js";
8
8
  import { childrenPolicy } from "./element.js";
9
9
  import { listedIn } from "../../framework/utils/enumerated.js";
10
10
  import { valueSchemaOf } from "../../framework/value-domain.js";
11
- import { Style, addBagKeyIssues, bagNameEdges, bagRewriteName, bindBagTokens, collectBag, isPlainRecord, openBag, styleValue } from "./style-bag.js";
11
+ import { Style, addBagKeyIssues, anatomyPropValue, bagNameEdges, bagRewriteName, bindBagTokens, collectBag, isPlainRecord, openBag, styleValue } from "./style-bag.js";
12
12
  import { propValueDomain, resolveComponentProps, stylePropValueLeaves, validateComponentProps } from "../../framework/projections.js";
13
13
  import { intoName, routedPropIn, routesContent } from "../../framework/prop-surface.js";
14
14
  import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../framework/render-spec.js";
@@ -773,6 +773,13 @@ function sameElement(left, right) {
773
773
  function spellsLayer(element, key, layer) {
774
774
  return element === key || sameElement(element, layer);
775
775
  }
776
+ /** Whether a node keyed like a layer renders it: the layer's ref, or an element the prop chooses
777
+ * that names the layer it stands in for (`{ "$state": "/type", …, "layer": "root" }`), which wears
778
+ * the layer's markers and classes on whichever component the value picks. */
779
+ function rendersLayer(element, key) {
780
+ if (isRef(element)) return element.__ref === `layer:${key}`;
781
+ return typeof element === "object" && element !== null && "$state" in element && element.layer === key;
782
+ }
776
783
  /** The shape of a rule, for a bag written where the rule belongs or a rule with no `layers`. */
777
784
  const ruleShapeError = (issue) => issue.code === "invalid_type" || issue.code === "unrecognized_keys" ? "a style rule is `{ \"when\": null, \"layers\": { \"<layer>\": { \"<styleProp>\": \"<value>\" } } }` under a key of your own (`*` for the unconditional rule), or with `\"when\": { \"<prop>\": \"<value>\" }` for a variant; the layer bags sit under `layers`, never directly under the rule" : void 0;
778
785
  const StyleRule = defineSubEntity({
@@ -845,7 +852,20 @@ const ruleSchemas = /* @__PURE__ */ new WeakMap();
845
852
  const nodeElement = z.union([
846
853
  refSchema.describe("A ref. `layer:<name>` renders through one of this component's layers, which is the only way a node can be styled; `component:<path>`, `icon:<path>` or `package:<export>` renders that directly, unstyled."),
847
854
  z.string().describe("An intrinsic tag such as `div`, rendered unstyled, or `Slot`: the position where an untargeted slot prop's content lands, with the prop named in `props.name`, listed in the `children` of a layer that takes nodes. A text layer shows a prop through a `forward` declared on the prop instead."),
848
- z.strictObject({ $state: z.string() }).describe("An element the instance chooses: the value of the prop `$state` names, from that prop's own domain (an icon prop whose value is a glyph). It carries no per-value table; to show a different fixed glyph per variant value, give each glyph its own node with `visible: { \"$state\": \"<prop>\", \"eq\": \"<value>\" }`.")
855
+ z.strictObject({
856
+ $state: z.string(),
857
+ /** The render's own value → element table, for a root that mounts a different component per
858
+ * value of one prop (`if (mode === 'link') return <ChipLink/>`). Absent for a domain lookup,
859
+ * where the prop's declaration knows the values. */
860
+ map: z.record(z.string(), z.union([z.string(), refSchema])).optional(),
861
+ /** What mounts when no map entry matches — the render's final return. */
862
+ else: z.union([z.string(), refSchema]).optional(),
863
+ /** The layer the chosen element stands in for, so it keeps that layer's markers and classes. */
864
+ layer: z.string().optional()
865
+ }).refine((element) => element.map === void 0 || element.else !== void 0, {
866
+ message: "a `$state` element with a `map` needs an `else`: the element that mounts for a value the map does not name",
867
+ path: ["else"]
868
+ }).describe("An element the instance chooses: the value of the prop `$state` names. With no `map`, the value is resolved in that prop's own domain (an icon prop whose value is a glyph). With a `map` (value → `{ \"__ref\": \"component:<path>\" }`) and an `else`, it is the component the render mounts for that value — a root swapped by a variant — and `layer` names the layer it stands in for.")
849
869
  ], { error: "an element is `{ \"__ref\": \"layer:<name>\" }` for one of this component's layers, `{ \"__ref\": \"icon:<library>/<Name>\" }` for a glyph, `{ \"__ref\": \"component:<path>\" }` for a component rendered unstyled, an intrinsic tag such as `div`, `Slot` for where a slot prop's content lands, or `{ \"$state\": \"<prop>\" }` for an element the prop chooses" }).describe("What this node renders. Name a layer (`{ \"__ref\": \"layer:<name>\" }`) for anything a style rule should reach.");
850
870
  /** The operand each comparison accepts. Checked against the framework's operator set both ways, so
851
871
  * the schema can neither drop an operator the readers handle nor accept one they don't. */
@@ -1028,8 +1048,16 @@ const AnatomyNode = defineSubEntity({
1028
1048
  const body = parent;
1029
1049
  const shape = {
1030
1050
  element: nodeElement,
1051
+ /** A package factory the element renders THROUGH — `{ "__ref": "package:motion/react#motion" }`
1052
+ * for a layer the render wrapped as `motion.create(Box)`. The registry holds an entry for each
1053
+ * (wrapper, element) pair the anatomies name, so the wrapped layer paints as the module does,
1054
+ * reading the same motion props off this node. */
1055
+ wrapper: refSchema.optional().describe("A package factory this node renders through, such as `{ \"__ref\": \"package:motion/react#motion\" }` for a motion-wrapped layer. The node's props then include the wrapper's (`initial`, `animate`, `variants`, `transition`, …)."),
1031
1056
  visible: visibility.optional().describe("When the node renders: a condition on a prop value (`{ \"$state\": \"<prop>\", \"eq\": <value> }`), an `$or` of them, or a list that must all hold. Omit for a node that always renders."),
1032
- props: (config ? openBag(config, { cssBehind: false }) : z.record(z.string(), styleValue)).optional().describe("Props passed to what the node renders, keyed by prop or style-property name, as literal values. Content is not set here: a `slot` prop's content lands where a `{ \"element\": \"Slot\", \"props\": { \"name\": \"<prop>\" } }` node sits among a parent's `children` (on the root when there is none), and a `forward` prop (`{ \"type\": \"forward\", \"target\": \"label/children\" }`) routes it into that layer with no node entry. `$state` belongs to `visible` and to an `element` a prop chooses."),
1057
+ props: (config ? openBag(config, {
1058
+ cssBehind: false,
1059
+ values: anatomyPropValue
1060
+ }) : z.record(z.string(), anatomyPropValue)).optional().describe("Props passed to what the node renders, keyed by prop or style-property name, as literal values. Content is not set here: a `slot` prop's content lands where a `{ \"element\": \"Slot\", \"props\": { \"name\": \"<prop>\" } }` node sits among a parent's `children` (on the root when there is none), and a `forward` prop (`{ \"type\": \"forward\", \"target\": \"label/children\" }`) routes it into that layer with no node entry. `$state` belongs to `visible` and to an `element` a prop chooses."),
1033
1061
  text: z.string().optional().describe("Literal text content, for a node that renders a fixed string.")
1034
1062
  };
1035
1063
  if (!config || !body) return z.object({
@@ -1063,14 +1091,13 @@ const AnatomyNode = defineSubEntity({
1063
1091
  const current = key ? body.anatomy?.[key] : void 0;
1064
1092
  const element = node.element ?? current?.element;
1065
1093
  if (key !== void 0 && body.layers && key in body.layers) {
1066
- const rendersLayer = isRef(element) && element.__ref === `layer:${key}`;
1067
- if (element !== void 0 && !rendersLayer && !spellsLayer(element, key, body.layers[key])) {
1094
+ if (element !== void 0 && !rendersLayer(element, key) && !spellsLayer(element, key, body.layers[key])) {
1068
1095
  const spelled = isRef(element) ? `\`${element.__ref}\`` : JSON.stringify(element);
1069
1096
  ctx.issues.push({
1070
1097
  code: "custom",
1071
1098
  input: node,
1072
1099
  path: ["element"],
1073
- message: `node "${key}" is keyed like the layer "${key}", so it renders that layer: \`{ "__ref": "layer:${key}" }\`. Written as ${spelled} it would paint the element directly, unstyled, outside every style rule that reaches the layer.`
1100
+ message: `node "${key}" is keyed like the layer "${key}", so it renders that layer: \`{ "__ref": "layer:${key}" }\`, or an element the prop chooses with \`"layer": "${key}"\`. Written as ${spelled} it would paint the element directly, unstyled, outside every style rule that reaches the layer.`
1074
1101
  });
1075
1102
  }
1076
1103
  }
@@ -1249,6 +1276,22 @@ const componentFields = z.strictObject({
1249
1276
  }
1250
1277
  });
1251
1278
  /**
1279
+ * The body a blank component is created with: one intrinsic `div` layer and the anatomy node that
1280
+ * renders it, nothing else. Studio's "+" and the `create-blank` op both write exactly this.
1281
+ *
1282
+ * Both halves are needed. The kind requires a `root` layer, but layers alone project the component
1283
+ * rendering itself, a painting with nothing to compose into; the node is what puts a real `div`
1284
+ * carrying the editor's selection markers on the canvas. `div` is an intrinsic tag rather than a
1285
+ * system component, so no system is without it. Anything more (a `children` slot, preview content, a
1286
+ * style rule) makes the component authored rather than blank, and the editor's drop frame stands down.
1287
+ */
1288
+ function blankComponentBody() {
1289
+ return {
1290
+ layers: { root: "div" },
1291
+ anatomy: { root: { element: { __ref: "layer:root" } } }
1292
+ };
1293
+ }
1294
+ /**
1252
1295
  * Where a component keeps its bags: each style rule's per-layer bag, each anatomy node's props, and
1253
1296
  * the component's own defaults.
1254
1297
  *
@@ -1271,7 +1314,7 @@ const componentBags = (data) => {
1271
1314
  if (!isPlainRecord(node)) continue;
1272
1315
  collectBag(node.props, (next) => {
1273
1316
  node.props = next;
1274
- }, out);
1317
+ }, out, node.element);
1275
1318
  }
1276
1319
  collectBag(body?.defaultProps, (next) => {
1277
1320
  if (body) body.defaultProps = next;
@@ -1314,7 +1357,7 @@ slots: ({ config, path, member }) => {
1314
1357
  schemas: {
1315
1358
  create: {
1316
1359
  data: componentFields,
1317
- description: "Create a component at a new qualified `path`. When its complete body is known, include all layers, anatomy, props, defaults, and styles in this one operation. `data.layers` maps each stable layer name to the element it renders and must include `root` — a tag for a primitive (`{ \"layers\": { \"root\": \"div\" } }`), and for anything built on top, a ref to a primitive (`{ \"root\": { \"__ref\": \"component:Box\" }, \"label\": { \"__ref\": \"component:Text\" } }`), which is what lets the layer take that primitive's style props. A primitive exposes style props as `{ \"type\": \"styleProperty\", \"value\": { \"__ref\": \"style-property:bg\" } }` and declares `children: { \"type\": \"slot\", \"text\": true }` when it holds content — without the slot, nothing nested inside it renders. A composed component exposes a layer's props at its own top level through `forwards`, keyed by layer: `{ \"forwards\": { \"root\": \"*\" } }` passes every prop the root's element takes (so `<Button width=\"full\">` reaches the root's `width`), and a list of member refs (`[{ \"__ref\": \"component:Box#props/width\" }]`, each naming the component that declares the prop) passes only those. A prop a layer already exposes is forwarded, never redeclared in `props`; a `props` entry of `{ \"type\": \"forward\" }` is the reverse route, one of this component's props into a layer's. `anatomy` is the tree the build would read off a render: each node renders a layer (`{ \"__ref\": \"layer:<name>\" }`), an intrinsic tag, or `Slot` (`{ \"element\": \"Slot\", \"props\": { \"name\": \"children\" } }`, where a slot prop's content lands — but a declared slot fills `root` by default, so a primitive still declares its `children` slot prop yet needs no `Slot` node or `children` entry for it, and a `Slot` node is only for landing that content in a nested layer); `children` lists sibling node keys from the `root` node, never a prop name; `text` is a fixed string; `visible` is a condition on a prop (`{ \"$state\": \"<prop>\" }`, or with `eq`); content shown in a nested layer is a route, `children: { \"type\": \"forward\", \"target\": \"label/children\" }`. `$state` appears only in `visible` and in an `element` a prop chooses, never as a prop value. A glyph is a node of its own, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" } }`, listed in the `children` of the layer node that holds it, never a prop value. Text a prop supplies to a text layer is never a node either: declare the prop `{ \"type\": \"forward\", \"target\": \"<layer>/children\" }` and the layer shows it; a `Slot` node goes only under a layer that takes nodes. A style rule is `\"styles\": { \"*\": { \"when\": null, \"layers\": { \"root\": { \"bg\": \"surface\" } } } }`, keyed by a name, with each layer's bag under `layers`. A composed component carries its own styles: give it an unconditional (`\"*\"`) rule that lays out and surfaces it through the system's style properties — spacing, alignment and gap on the container, a background and color where it has one — rather than leaning on the bare primitives, which paint flat. Style a layer only with the style props the primitive it renders exposes, set to the values those props accept: `component/get` on the primitive lists both, so read it and choose from that rather than reaching for a CSS name or value the system has no style property for. Set `previewProps` (or `defaultProps`) so the component previews with content; a root that takes children renders empty otherwise. A style rule's layer bag is keyed by style-prop name (`fontWeight`, `color`), never by CSS property name, and each value is one of that prop's values.",
1360
+ description: "Create a component at a new qualified `path`. For a component with nothing in it yet, one to design on the canvas before it has props or variants, use `create-blank` with only a `path` instead of authoring a minimal body here. When its complete body is known, include all layers, anatomy, props, defaults, and styles in this one operation. `data.layers` maps each stable layer name to the element it renders and must include `root` — a tag for a primitive (`{ \"layers\": { \"root\": \"div\" } }`), and for anything built on top, a ref to a primitive (`{ \"root\": { \"__ref\": \"component:Box\" }, \"label\": { \"__ref\": \"component:Text\" } }`), which is what lets the layer take that primitive's style props. A primitive exposes style props as `{ \"type\": \"styleProperty\", \"value\": { \"__ref\": \"style-property:bg\" } }` and declares `children: { \"type\": \"slot\", \"text\": true }` when it holds content — without the slot, nothing nested inside it renders. A composed component exposes a layer's props at its own top level through `forwards`, keyed by layer: `{ \"forwards\": { \"root\": \"*\" } }` passes every prop the root's element takes (so `<Button width=\"full\">` reaches the root's `width`), and a list of member refs (`[{ \"__ref\": \"component:Box#props/width\" }]`, each naming the component that declares the prop) passes only those. A prop a layer already exposes is forwarded, never redeclared in `props`; a `props` entry of `{ \"type\": \"forward\" }` is the reverse route, one of this component's props into a layer's. `anatomy` is the tree the build would read off a render: each node renders a layer (`{ \"__ref\": \"layer:<name>\" }`), an intrinsic tag, or `Slot` (`{ \"element\": \"Slot\", \"props\": { \"name\": \"children\" } }`, where a slot prop's content lands — but a declared slot fills `root` by default, so a primitive still declares its `children` slot prop yet needs no `Slot` node or `children` entry for it, and a `Slot` node is only for landing that content in a nested layer); `children` lists sibling node keys from the `root` node, never a prop name; `text` is a fixed string; `visible` is a condition on a prop (`{ \"$state\": \"<prop>\" }`, or with `eq`); content shown in a nested layer is a route, `children: { \"type\": \"forward\", \"target\": \"label/children\" }`. `$state` appears only in `visible` and in an `element` a prop chooses, never as a prop value. A glyph is a node of its own, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" } }`, listed in the `children` of the layer node that holds it, never a prop value. Text a prop supplies to a text layer is never a node either: declare the prop `{ \"type\": \"forward\", \"target\": \"<layer>/children\" }` and the layer shows it; a `Slot` node goes only under a layer that takes nodes. A style rule is `\"styles\": { \"*\": { \"when\": null, \"layers\": { \"root\": { \"bg\": \"surface\" } } } }`, keyed by a name, with each layer's bag under `layers`. A composed component carries its own styles: give it an unconditional (`\"*\"`) rule that lays out and surfaces it through the system's style properties — spacing, alignment and gap on the container, a background and color where it has one — rather than leaning on the bare primitives, which paint flat. Style a layer only with the style props the primitive it renders exposes, set to the values those props accept: `component/get` on the primitive lists both, so read it and choose from that rather than reaching for a CSS name or value the system has no style property for. Set `previewProps` (or `defaultProps`) so the component previews with content; a root that takes children renders empty otherwise. A style rule's layer bag is keyed by style-prop name (`fontWeight`, `color`), never by CSS property name, and each value is one of that prop's values.",
1318
1361
  example: ({ config }) => {
1319
1362
  const path = freeExamplePath(config);
1320
1363
  const composed = composedCreateExample(config, path);
@@ -1355,6 +1398,28 @@ slots: ({ config, path, member }) => {
1355
1398
  };
1356
1399
  }
1357
1400
  },
1401
+ /**
1402
+ * A component with nothing in it yet, as Studio's "+" makes one. Its own op because `create`
1403
+ * teaches the full grammar, and a model asked for a blank component authors a `children` slot and
1404
+ * preview content from it, at which point the component is no longer blank. Decomposes into the one
1405
+ * `create` patch the rail writes, so the changes list and undo read both gestures alike.
1406
+ */
1407
+ "create-blank": {
1408
+ input: z.object({ path: pathInput("component") }),
1409
+ readOnly: false,
1410
+ label: "Create blank",
1411
+ description: "Create an empty component at a new qualified `path`, to design on the canvas first: one `div` root layer and the node that renders it, with no props, no variants, no slot, no styles and no preview content. This is exactly what Studio's \"+\" creates. Takes only `path`. Use it for \"a new component\", \"a blank component\" or \"start a component from nothing\"; `create` is for a component whose body is already known.",
1412
+ scope: "item",
1413
+ creates: true,
1414
+ title: (entity) => `Create blank ${entity}`,
1415
+ example: ({ config }) => ({ path: freeExamplePath(config) }),
1416
+ handler: (input, config) => config.apply({
1417
+ kind: "component",
1418
+ operation: "create",
1419
+ path: String(input.path),
1420
+ data: blankComponentBody()
1421
+ })
1422
+ },
1358
1423
  "sub-create/anatomy": { description: "Add one anatomy node (`key` + `data`) to a component. In a batch, create child nodes before a parent whose `children` or `slots` names them; each referenced node must already exist when this operation runs." },
1359
1424
  "sub-create/props": { description: "Declare a prop this component owns (`key` + `data`): a `variant`, a `slot`, a `styleProperty` or `composite` on a primitive, a `forward` routing this prop into a layer's (`{ \"type\": \"forward\", \"target\": \"<layer>/<prop>\" }`), or `null` to drop a forwarded prop from the surface. A prop one of its layers already exposes — `width` on a root that renders `Box` — is not declared here; `sub-create/forwards` exposes it at the top level." },
1360
1425
  "sub-create/forwards": {
@@ -1843,4 +1908,4 @@ function exampleProps(config, componentPath) {
1843
1908
  return {};
1844
1909
  }
1845
1910
  //#endregion
1846
- export { Component, canonicalWhen, forwardedNames, layerRef, ownValues, ownsItsValues, valueRef };
1911
+ export { Component, blankComponentBody, canonicalWhen, forwardedNames, layerRef, ownValues, ownsItsValues, valueRef };
@@ -53,6 +53,7 @@ declare const File: EntityClass<z.ZodObject<{
53
53
  target: z.ZodType<Ref, unknown, z.core.$ZodTypeInternals<Ref, unknown>>;
54
54
  names: z.ZodOptional<z.ZodArray<z.ZodString>>;
55
55
  }, z.core.$strip>>>;
56
+ exports: z.ZodOptional<SubEntityClass<z.ZodObject<{}, z.core.$strict>>>;
56
57
  }, z.core.$strict>, z.ZodObject<Record<never, never>, z.core.$strip>, "file", Record<never, never>, {
57
58
  /**
58
59
  * Bytes rather than text, so a restore knows how to write it — derived from the path, because
@@ -82,6 +82,24 @@ const FileImport = defineSubEntity({
82
82
  })
83
83
  });
84
84
  /**
85
+ * One export of a sealed source file that a render places in JSX — `file:components/pagination.tsx#PaginationProvider`.
86
+ *
87
+ * A component file exports more than its components: a context provider the composite wraps its
88
+ * root in, a plain function component two renders share. When a render puts one in the tree, the
89
+ * anatomy has to name it — the same way it names a package's export — and the registry has to
90
+ * import it from the module the file emits to. The member is that name, with the same empty body a
91
+ * `Package` export has: its identity is its name, and the module it lives in is the owning file.
92
+ *
93
+ * Only what a render placed in JSX, for the reason a `Package` lists only that: nobody browses a
94
+ * file's exports, and a hook or a helper no JSX mentions is not a render surface.
95
+ */
96
+ const FileExport = defineSubEntity({
97
+ name: "exports",
98
+ label: "Export",
99
+ labelPlural: "Exports",
100
+ fields: z.strictObject({})
101
+ });
102
+ /**
85
103
  * File (`file:components/card.tsx`) — one artifact the seal owns.
86
104
  *
87
105
  * A component is a definition plus the React that renders it, and `config.json` cannot hold JSX, so
@@ -110,13 +128,17 @@ const File = defineEntity({
110
128
  labelPlural: "Files",
111
129
  authoredRefs: false,
112
130
  linkable: false,
131
+ bareMember: "exports",
113
132
  fields: z.strictObject({
114
133
  /** SHA-256 of the exact bytes — what the seal hashed, so a restore verifies rather than trusts. */
115
134
  hash: z.string(),
116
135
  /** What this file imports — keyed by the text the source wrote, in the order it wrote it. Every
117
136
  * local edge, plus each bare specifier a `Package` group already names; one the config names
118
137
  * nowhere is left out rather than pointed at a group minted to receive it. */
119
- imports: FileImport.optional()
138
+ imports: FileImport.optional(),
139
+ /** The exports a render placed in JSX — a same-file provider or function component an anatomy
140
+ * names by `file:<path>#<export>`. Absent for a file no anatomy reaches into. */
141
+ exports: FileExport.optional()
120
142
  }),
121
143
  computed: {
122
144
  /**
@@ -130,7 +152,10 @@ const File = defineEntity({
130
152
  * boolean read as a tri-state at every call site.
131
153
  */
132
154
  binary: ({ path }) => isBinaryAsset(path) },
133
- subEntities: { imports: FileImport }
155
+ subEntities: {
156
+ imports: FileImport,
157
+ exports: FileExport
158
+ }
134
159
  });
135
160
  //#endregion
136
161
  export { File, SCRIPT_EXTENSIONS, isBinaryAsset };
@@ -17,6 +17,21 @@ const styleValue = z.lazy(() => z.union([
17
17
  gradient,
18
18
  z.record(z.string(), styleValue)
19
19
  ]));
20
+ const anatomyPropValue = z.lazy(() => z.union([
21
+ z.string(),
22
+ z.number(),
23
+ z.boolean(),
24
+ z.null(),
25
+ refSchema,
26
+ derivedColor,
27
+ gradient,
28
+ z.array(z.union([
29
+ z.string(),
30
+ z.number(),
31
+ z.boolean()
32
+ ])),
33
+ z.record(z.string(), anatomyPropValue)
34
+ ]));
20
35
  /**
21
36
  * A style bag as a keyed COLLECTION — style-prop key → {@link styleValue}, with each key a member
22
37
  * in its own right.
@@ -68,11 +83,15 @@ function isPlainRecord(value) {
68
83
  * written into its parent before that parent is rebuilt. A modifier block holds a bag of the same
69
84
  * shape, so this is the only recursion either hook needs.
70
85
  */
71
- function collectBag(bag, write, out) {
86
+ function collectBag(bag, write, out, element) {
72
87
  if (!isPlainRecord(bag)) return;
73
- out.push({
88
+ out.push(element === void 0 ? {
74
89
  bag,
75
90
  write
91
+ } : {
92
+ bag,
93
+ write,
94
+ element
76
95
  });
77
96
  for (const [key, value] of Object.entries(bag)) {
78
97
  if (!key.startsWith("_") || !isPlainRecord(value)) continue;
@@ -234,8 +253,8 @@ function addBagKeyIssues({ config, bag, ctx, nested, declared, cssBehind = true,
234
253
  }
235
254
  /** The open bag — every key a style value, with token names bound to their tokens. `cssBehind`
236
255
  * as in {@link bagKeyIssue}: off for a bag that may reach an intrinsic element's attributes. */
237
- function openBag(config, { cssBehind = true } = {}) {
238
- return z.record(z.string(), styleValue).superRefine((bag, ctx) => addBagKeyIssues({
256
+ function openBag(config, { cssBehind = true, values = styleValue } = {}) {
257
+ return z.record(z.string(), values).superRefine((bag, ctx) => addBagKeyIssues({
239
258
  config,
240
259
  bag,
241
260
  ctx,
@@ -244,4 +263,4 @@ function openBag(config, { cssBehind = true } = {}) {
244
263
  })).transform((bag) => bindBagTokens(config, bag));
245
264
  }
246
265
  //#endregion
247
- export { Style, addBagKeyIssues, bagNameEdges, bagRewriteName, bindBagTokens, camelCase, collectBag, isPlainRecord, openBag, styleValue };
266
+ export { Style, addBagKeyIssues, anatomyPropValue, bagNameEdges, bagRewriteName, bindBagTokens, camelCase, collectBag, isPlainRecord, openBag, styleValue };
@@ -72,7 +72,7 @@ interface RecordedSource {
72
72
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
73
73
  * that replaced it (see `detectedWireVersion`).
74
74
  */
75
- declare const SERIALIZED_CONFIG_VERSION = 20260912164927;
75
+ declare const SERIALIZED_CONFIG_VERSION = 20260914181845;
76
76
  interface SerializedConfig {
77
77
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
78
78
  readonly version: number;
@@ -4715,7 +4715,7 @@ var Config = class Config {
4715
4715
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
4716
4716
  hydrateFrom(stored, options) {
4717
4717
  const claimed = schemaVersionOf(stored);
4718
- if (claimed > 20260912164927) throw new SchemaVersionTooNew(claimed);
4718
+ if (claimed > 20260914181845) throw new SchemaVersionTooNew(claimed);
4719
4719
  const wire = upgradeSerializedConfig(stored);
4720
4720
  if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating, or port the repo once with \`uds migrate\`.`);
4721
4721
  const leftover = wire.options;
@@ -0,0 +1,99 @@
1
+ //#region src/framework/anatomy-check.d.ts
2
+ /**
3
+ * Anatomy check — does a component's ANATOMY, expanded by the config, paint what the component's
4
+ * MODULE paints?
5
+ *
6
+ * The two are one component seen two ways: the editor paints a matrix cell from the stored anatomy
7
+ * (`expandSpec`), a canvas paints a dropped instance from the module. Every place they differ is a
8
+ * bug in the anatomy reader, the expansion, or a render the anatomy grammar cannot express. This
9
+ * module is the comparison: both renderings reduced to the same flat shape ({@link RenderedNode}),
10
+ * aligned, and their differences reported by kind, so a check can fail on them and a person can read
11
+ * them.
12
+ *
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.
15
+ * A browser surface snapshots live DOM instead (the studio's `?anatomyCheck=true`), and adds computed
16
+ * style to each node; {@link diffRendered} compares whatever a node carries.
17
+ */
18
+ /** One rendered element, reduced to what anatomy cares about. */
19
+ interface RenderedNode {
20
+ /** The component whose layer this element is, when it is one (`data-uds-component`). */
21
+ readonly component?: string;
22
+ /** The layer (`data-uds-layer`), when the element is one. */
23
+ readonly layer?: string;
24
+ readonly tag: string;
25
+ /** The element's OWN text — its direct text nodes, whitespace collapsed — never its children's. */
26
+ readonly text: string;
27
+ readonly classes: readonly string[];
28
+ /**
29
+ * Style, as the snapshot could read it. From live DOM, the computed subset {@link STYLE_PROPERTIES},
30
+ * compared where both sides carry a property. From markup, the element's inline `style`
31
+ * declarations — the only style markup has — compared as a whole: a declaration one side writes
32
+ * and the other does not is a difference, since inline is what a render's literal becomes.
33
+ */
34
+ readonly style: Readonly<Record<string, string>>;
35
+ /** `true` when `style` holds inline declarations rather than a computed subset. */
36
+ readonly inlineStyle?: boolean;
37
+ /** What the alignment matches on — see {@link renderedSignatureOf}. */
38
+ readonly signature: string;
39
+ }
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'
41
+ /** Both render it, with different class sets. Classes are how a style arrives, so where a
42
+ * computed style is available this is informational; where none is (markup only) it is the
43
+ * paint difference itself — see `diffRendered`'s `classesFail`. */
44
+ | 'classes';
45
+ interface AnatomyIssue {
46
+ readonly kind: AnatomyIssueKind;
47
+ /** The element, by signature, plus its ordinal among same-signature elements (`Modal/divider#2`). */
48
+ readonly node: string;
49
+ readonly property?: string;
50
+ readonly anatomy?: string;
51
+ readonly instance?: string;
52
+ }
53
+ interface AnatomyReport {
54
+ /** `true` when nothing that counts as a paint difference was found. */
55
+ readonly ok: boolean;
56
+ readonly issues: readonly AnatomyIssue[];
57
+ /** How many elements each side rendered under the component root. */
58
+ readonly counts: {
59
+ readonly anatomy: number;
60
+ readonly instance: number;
61
+ };
62
+ }
63
+ /**
64
+ * The computed properties compared on aligned elements. Paint, box, type — what a designer would
65
+ * notice. `width`/`height`/`line-height` compare with a pixel tolerance; everything else exactly,
66
+ * after {@link normalizeStyleValue}.
67
+ */
68
+ declare const STYLE_PROPERTIES: readonly string[];
69
+ /** Pixel slack for the size comparisons — sub-pixel layout and font rounding. */
70
+ declare const SIZE_TOLERANCE_PX = 1.5;
71
+ /** The alignment key: a layer is `Component/layer`; anything else is its tag. */
72
+ declare function renderedSignatureOf(tag: string, component: string | undefined, layer: string | undefined): string;
73
+ /**
74
+ * Every element under the component root of `html`, in document order, as {@link RenderedNode}s
75
+ * without computed style. The root is the first element carrying `data-uds-component`; hosts and
76
+ * shells above it are scaffolding. `data-anatomy-check-ignore` subtrees are skipped.
77
+ */
78
+ declare function snapshotHtml(html: string): RenderedNode[];
79
+ /** `rgb(…)` spacing and a trailing `px` zero are the browser's spelling, not a difference. */
80
+ declare function normalizeStyleValue(value: string): string;
81
+ interface DiffOptions {
82
+ /**
83
+ * Count a class-set difference as a failure. Off where computed styles are compared (the class is
84
+ * only how the style arrived); on where only markup is (the class IS the paint).
85
+ */
86
+ readonly classesFail?: boolean;
87
+ }
88
+ /**
89
+ * Compare the anatomy's rendering with the module's.
90
+ *
91
+ * Elements align by signature in document order; what the module renders and the anatomy does not
92
+ * is `missing`, the reverse is `extra`. Aligned elements then compare own text, computed style (when
93
+ * both sides carry it) and class set.
94
+ */
95
+ declare function diffRendered(anatomy: readonly RenderedNode[], instance: readonly RenderedNode[], options?: DiffOptions): AnatomyReport;
96
+ /** The allowlist spelling of an issue: kind, node, and the property for a style difference. */
97
+ declare function anatomyIssueKey(issue: AnatomyIssue): string;
98
+ //#endregion
99
+ export { AnatomyIssue, AnatomyIssueKind, AnatomyReport, DiffOptions, RenderedNode, SIZE_TOLERANCE_PX, STYLE_PROPERTIES, anatomyIssueKey, diffRendered, normalizeStyleValue, renderedSignatureOf, snapshotHtml };