@yahoo/uds-create-config 4.5.2 → 4.7.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 (72) hide show
  1. package/dist/configs/CanvasConfig.d.ts +900 -80
  2. package/dist/configs/react-native-system.d.ts +603 -55
  3. package/dist/configs/system.d.ts +603 -55
  4. package/dist/configs/system.js +7 -1
  5. package/dist/entities/system/Component.d.ts +480 -39
  6. package/dist/entities/system/Component.js +264 -93
  7. package/dist/entities/system/File.d.ts +1 -1
  8. package/dist/entities/system/LinkedSystem.d.ts +2 -2
  9. package/dist/entities/system/Operation.d.ts +1 -1
  10. package/dist/entities/system/StyleProperty.d.ts +8 -0
  11. package/dist/entities/system/StyleProperty.js +12 -1
  12. package/dist/entities/system/Tool.d.ts +1 -1
  13. package/dist/entities/system/component-module.js +1 -1
  14. package/dist/entities/system/defineComponent.d.ts +45 -7
  15. package/dist/entities/system/defineComponent.js +52 -25
  16. package/dist/framework/Config.d.ts +8 -2
  17. package/dist/framework/Config.js +97 -23
  18. package/dist/framework/class-names.d.ts +4 -2
  19. package/dist/framework/class-names.js +56 -17
  20. package/dist/framework/defineDerivedEntity.d.ts +1 -1
  21. package/dist/framework/defineEntity.d.ts +2 -2
  22. package/dist/framework/defineSubEntity.d.ts +3 -2
  23. package/dist/framework/preview-store.d.ts +19 -0
  24. package/dist/framework/preview-store.js +18 -0
  25. package/dist/framework/projections.d.ts +28 -8
  26. package/dist/framework/projections.js +50 -16
  27. package/dist/framework/prop-surface.d.ts +21 -1
  28. package/dist/framework/prop-surface.js +99 -21
  29. package/dist/framework/render-spec.d.ts +103 -3
  30. package/dist/framework/render-spec.js +285 -37
  31. package/dist/framework/schema-version.d.ts +1 -1
  32. package/dist/framework/schema-version.js +5 -5
  33. package/dist/framework/source-integrity.d.ts +14 -1
  34. package/dist/framework/utils/refs.js +10 -1
  35. package/dist/framework/validate-spec.d.ts +2 -0
  36. package/dist/framework/validate-spec.js +13 -0
  37. package/dist/index.d.ts +15 -12
  38. package/dist/index.js +7 -6
  39. package/dist/migrations/2.0.0/v1-artifact.d.ts +305 -29
  40. package/dist/migrations/20260919051027_prop_conditions_and_anatomy_positions_at_rest.d.ts +22 -0
  41. package/dist/migrations/20260919051027_prop_conditions_and_anatomy_positions_at_rest.js +10 -0
  42. package/dist/migrations/20260920004019_preview_conditions_keyed.d.ts +27 -0
  43. package/dist/migrations/20260920004019_preview_conditions_keyed.js +148 -0
  44. package/dist/migrations/20260920161309_prefixed_token_group.d.ts +12 -0
  45. package/dist/migrations/20260920161309_prefixed_token_group.js +10 -0
  46. package/dist/renderer/wrapRegistry.js +8 -2
  47. package/dist/renderer/wrappers/component-refs.d.ts +14 -0
  48. package/dist/renderer/wrappers/component-refs.js +79 -0
  49. package/dist/renderer/wrappers/hex-normalize.d.ts +5 -7
  50. package/dist/renderer/wrappers/hex-normalize.js +11 -10
  51. package/dist/spec/cell.d.ts +171 -0
  52. package/dist/spec/cell.js +361 -0
  53. package/dist/spec/detach.d.ts +125 -0
  54. package/dist/spec/detach.js +511 -0
  55. package/dist/spec/empty-node-slots.d.ts +11 -23
  56. package/dist/spec/empty-node-slots.js +12 -39
  57. package/dist/spec/index.d.ts +5 -3
  58. package/dist/spec/index.js +3 -3
  59. package/dist/spec/instance.d.ts +77 -0
  60. package/dist/spec/instance.js +80 -0
  61. package/dist/spec/preview-content.d.ts +95 -0
  62. package/dist/spec/preview-content.js +149 -0
  63. package/dist/spec/preview-elements.d.ts +54 -15
  64. package/dist/spec/preview-elements.js +109 -29
  65. package/dist/spec/prop-conditions.d.ts +28 -0
  66. package/dist/spec/prop-conditions.js +39 -0
  67. package/dist/spec/slot.d.ts +206 -0
  68. package/dist/spec/slot.js +542 -0
  69. package/dist/spec/visibility.d.ts +82 -0
  70. package/dist/spec/visibility.js +109 -0
  71. package/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +18 -2
@@ -1,7 +1,9 @@
1
- import { kindOf, memberOf, pathOf, ref, splitRef } from "./utils/refs.js";
1
+ import { isRef, kindOf, memberOf, pathOf, ref, splitRef } from "./utils/refs.js";
2
+ import { renderedElement } from "../entities/system/element.js";
2
3
  import { createSliceMemo } from "./memo.js";
3
- import { authoredBag, memberFromLeaf, resolveComponentProps } from "./projections.js";
4
- import { intoName, routesContent } from "./prop-surface.js";
4
+ import { unconditionalPreviewProps } from "./preview-store.js";
5
+ import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, resolveComponentProps, stylePropertyPathFor } from "./projections.js";
6
+ import { forwardedLayers, intoName, routesContent } from "./prop-surface.js";
5
7
  import { graftPreviewElements } from "../spec/preview-elements.js";
6
8
  import { componentLayerClass, componentPropClasses, componentRuleClasses, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
7
9
  //#region src/framework/render-spec.ts
@@ -181,6 +183,27 @@ function authoredProps(config, props) {
181
183
  * and that was always stated rather than run.
182
184
  */
183
185
  function deriveSpec(config, path) {
186
+ const projected = componentSpec(config, path);
187
+ return projected.kind === "composition" ? projected.spec : void 0;
188
+ }
189
+ /**
190
+ * Per slice, the projected spec of each component asked for so far. The matrix, the layers rail and
191
+ * the designer panel each ask per cell or per row, and a projection walks the anatomy and reads every
192
+ * node prop's leaf back, so one answer serves them all until the config they read from changes.
193
+ *
194
+ * Keyed on the kinds a projection reads: the component's body, and the style properties and the
195
+ * token and composite members its node props' refs are read back through.
196
+ */
197
+ const componentSpecCache = createSliceMemo({
198
+ kinds: [
199
+ "component",
200
+ "style-property",
201
+ "token",
202
+ "composite"
203
+ ],
204
+ compute: (_config) => /* @__PURE__ */ new Map()
205
+ });
206
+ function projectSpec(config, path) {
184
207
  const body = bodyOf(config, path) ?? {};
185
208
  const layers = body.layers ?? {};
186
209
  const anatomy = body.anatomy;
@@ -210,11 +233,13 @@ function deriveSpec(config, path) {
210
233
  const props = {
211
234
  ...authoredProps(config, node.props),
212
235
  ...node.text !== void 0 ? { children: node.text } : {},
236
+ ...layer === void 0 ? { [ANATOMY_MARKER]: key } : {},
213
237
  ...layer !== void 0 ? {
214
238
  "data-uds-layer": layer,
215
239
  "data-uds-component": path
216
240
  } : {}
217
241
  };
242
+ markReadOnlyField(config, type, props);
218
243
  const children = node.children?.length ? [...node.children] : void 0;
219
244
  elements[key] = {
220
245
  type,
@@ -291,7 +316,15 @@ function instanceSpec(path, props = {}) {
291
316
  * surface reads `kind` to decide whether to offer composition tools, and `reason` to explain why not.
292
317
  */
293
318
  function componentSpec(config, path) {
294
- const derived = deriveSpec(config, path);
319
+ const cache = componentSpecCache(config);
320
+ const hit = cache.get(path);
321
+ if (hit) return hit;
322
+ const built = buildComponentSpec(config, path);
323
+ cache.set(path, built);
324
+ return built;
325
+ }
326
+ function buildComponentSpec(config, path) {
327
+ const derived = projectSpec(config, path);
295
328
  if (derived) return {
296
329
  spec: derived,
297
330
  kind: "composition"
@@ -334,7 +367,7 @@ function componentSpec(config, path) {
334
367
  function previewSpec(config, path) {
335
368
  const body = bodyOf(config, path);
336
369
  if (!body) return void 0;
337
- const values = body.previewProps ?? {};
370
+ const values = unconditionalPreviewProps(body) ?? {};
338
371
  return graftPreviewElements(instanceSpec(path, values), values);
339
372
  }
340
373
  /** A key no emitted element is using yet. Anatomy keys are namespaced under the instance they belong
@@ -439,6 +472,63 @@ function readStatePointer(state, pointer) {
439
472
  function statePointerProp(pointer) {
440
473
  return pointer.replace(/^\//, "").split("/")[0] ?? "";
441
474
  }
475
+ /**
476
+ * The marker on an element a render placed inline, naming its anatomy node. A layer is addressed by
477
+ * `data-uds-layer`; this is for the node between layers that has no bag of its own. Stamped by the
478
+ * emitted module and by both projections, so a rendered element and a projected one read alike.
479
+ */
480
+ const ANATOMY_MARKER = "data-uds-anatomy";
481
+ /** The string or number prop `node` binds into `into` (`{ $state: '/<prop>' }`), when it does. */
482
+ function boundValueProp({ config, path, node, into }) {
483
+ const pointer = (node.props?.[into])?.$state;
484
+ if (typeof pointer !== "string") return void 0;
485
+ const prop = statePointerProp(pointer);
486
+ const decl = resolveComponentProps(config, path)[prop];
487
+ return decl?.type === "string" || decl?.type === "number" ? prop : void 0;
488
+ }
489
+ /**
490
+ * The value prop a component paints as a layer's text: the anatomy node rendering `layer` binds its
491
+ * `children` to `{ $state: '/<prop>' }`, and that prop is declared a string or number. A forward
492
+ * surfaces its layer on the prop itself; this answers for a render that reads a value prop and
493
+ * writes it into a layer by hand, the way a `title` string reaches a heading.
494
+ */
495
+ function layerTextProp({ config, path, layer }) {
496
+ const body = bodyOf(config, path);
497
+ if (!body) return void 0;
498
+ for (const node of Object.values(body.anatomy ?? {})) {
499
+ if (layerOf(node.element) !== layer) continue;
500
+ const prop = boundValueProp({
501
+ config,
502
+ path,
503
+ node,
504
+ into: "children"
505
+ });
506
+ if (prop) return prop;
507
+ }
508
+ }
509
+ /**
510
+ * The value prop a component hands an element it placed inline: the anatomy node `element` binds
511
+ * `into` to `{ $state: '/<prop>' }`, and that prop is a string or number. `into` is `children` for
512
+ * text the element shows itself, or the far component's prop the render passed it to
513
+ * (`<CommentHeader title={title} />` binds `title`).
514
+ */
515
+ function anatomyTextProp({ config, path, element, into }) {
516
+ const node = bodyOf(config, path)?.anatomy?.[element];
517
+ if (!node) return void 0;
518
+ return boundValueProp({
519
+ config,
520
+ path,
521
+ node,
522
+ into
523
+ });
524
+ }
525
+ /** The component an inline anatomy element renders, when it is one (`component:<path>`). */
526
+ function anatomyElementComponent({ config, path, element }) {
527
+ const target = refOf(bodyOf(config, path)?.anatomy?.[element]?.element);
528
+ if (target === void 0) return void 0;
529
+ const { kind, qualifiedPath } = splitRef(target);
530
+ return kind === "component" ? qualifiedPath : void 0;
531
+ }
442
532
  /** A value in json-render's `visible` grammar, resolved against a prop bag. `undefined` when this
443
533
  * reader can't settle it, which leaves the condition on the element for the renderer to judge. */
444
534
  function resolveVisibility(condition, state) {
@@ -593,18 +683,28 @@ function collectTerms({ condition, junction, group, sink }) {
593
683
  * spellings are the same test.
594
684
  */
595
685
  function withoutTruthyTerm({ condition, prop }) {
596
- if (spentBy({
597
- condition,
598
- prop
599
- })) return null;
600
- if (!Array.isArray(condition)) return void 0;
601
- const kept = condition.filter((entry) => !spentBy({
686
+ const list = andListOf(condition);
687
+ const kept = list.filter((entry) => !spentBy({
602
688
  condition: entry,
603
689
  prop
604
690
  }));
605
- if (kept.length === condition.length) return void 0;
606
- if (kept.length === 0) return null;
607
- return kept.length === 1 ? kept[0] : kept;
691
+ if (kept.length === list.length) return void 0;
692
+ return spelled(kept);
693
+ }
694
+ /** A condition as the and-list it is: a bare clause is a list of one, nothing (or a projection's
695
+ * baked `true`) is empty. `false` is a position nothing at rest shows, which no clause can add to
696
+ * or take from, so it reads as a list holding itself. */
697
+ function andListOf(condition) {
698
+ if (condition === void 0 || condition === true) return [];
699
+ if (Array.isArray(condition)) return [...condition];
700
+ return [condition];
701
+ }
702
+ /** The stored spelling of an and-list: nothing clears the field, one clause is bare, several stay
703
+ * the list. */
704
+ function spelled(list) {
705
+ if (list.length === 0) return null;
706
+ if (list.length === 1) return list[0];
707
+ return list;
608
708
  }
609
709
  /** Whether a single term of an and-list is one `prop` being present makes true: the bare test, an
610
710
  * `eq: true` test (content is never the boolean `true`, so presence is the only reading that can
@@ -672,9 +772,31 @@ function computeConsumedProps(config, path, body) {
672
772
  const consumed = new Set(["className", "layerProps"]);
673
773
  const declared = body.props ?? {};
674
774
  for (const [name, decl] of Object.entries(declared)) if (decl && decl.type !== "styleProperty") consumed.add(name);
775
+ for (const name of body.renderProps ?? []) consumed.add(name);
675
776
  for (const rule of componentRuleClasses(config, path)) for (const prop of Object.keys(rule.when)) if (declared[prop]) consumed.add(prop);
676
777
  return consumed;
677
778
  }
779
+ /** The elements React treats as controlled once given a `value`. HTML's list, not the config's. */
780
+ const FORM_FIELDS = new Set([
781
+ "input",
782
+ "textarea",
783
+ "select"
784
+ ]);
785
+ /**
786
+ * A projected field has no handler: the render's `onChange` does not survive into the anatomy, so a
787
+ * controlled `value` is read-only here, and the element says so, which is what React requires of a
788
+ * value it cannot change. The element counts as a field when it renders as one of
789
+ * {@link FORM_FIELDS}, through a component or as the tag.
790
+ */
791
+ function markReadOnlyField(config, type, props) {
792
+ if (props.value === void 0 || props.readOnly !== void 0) return;
793
+ const tag = kindOf(type) === "component" ? renderedElement(config, pathOf(type)) : type;
794
+ if (FORM_FIELDS.has(tag)) props.readOnly = true;
795
+ }
796
+ /** A slot value that is text rather than elements. */
797
+ function isText(value) {
798
+ return typeof value === "string" || typeof value === "number";
799
+ }
678
800
  /** The prop JSX children arrive on — json-render's content channel, and the same name
679
801
  * `childrenPolicy` reads to answer whether a component accepts any. Not a system-defined name: a
680
802
  * design system names its components and tokens, and this one names the channel. */
@@ -731,8 +853,9 @@ function computeSlotTargets(config, body) {
731
853
  * the layer meant to hold it renders empty. Every one of Studio's own components shaped that way
732
854
  * (`Badge`, `StudioButton`, `Toggle`, …) painted as an empty shell in the component editor.
733
855
  *
734
- * Exported so the answer has ONE owner. An untargeted slot still resolves to the root here — that is
735
- * `slotTargets`' rule, not a caller's assumption, and it is the same rule the expansion renders by.
856
+ * Exported so the answer has ONE owner. An untargeted slot is hosted by the layer the anatomy
857
+ * places its content-position node in — a row's `leading` slot sits in its `leading` Box — and by the
858
+ * root when the anatomy records no position for it, which is where the expansion renders it.
736
859
  */
737
860
  function slotTargetsOf(config, path) {
738
861
  const body = bodyOf(config, path);
@@ -743,11 +866,85 @@ function slotTargetsOf(config, path) {
743
866
  layer,
744
867
  from: decl.from,
745
868
  into: intoName(decl.into)
746
- })), ...root ? untargeted.map((decl) => ({
747
- layer: root,
748
- from: decl.from,
749
- into: intoName(decl.into)
750
- })) : []];
869
+ })), ...untargeted.flatMap((decl) => {
870
+ const layer = slotHostLayer(body.anatomy, decl.from) ?? root;
871
+ return layer ? [{
872
+ layer,
873
+ from: decl.from,
874
+ into: intoName(decl.into)
875
+ }] : [];
876
+ })];
877
+ }
878
+ /**
879
+ * The layer holding a slot prop's content-position node: the nearest layer above the `Slot` node
880
+ * named for the prop, itself included. `undefined` when the anatomy records no position for the
881
+ * prop, or the node hangs under no layer.
882
+ */
883
+ function slotHostLayer(anatomy, from) {
884
+ if (!anatomy) return void 0;
885
+ const parents = /* @__PURE__ */ new Map();
886
+ for (const [key, node] of Object.entries(anatomy)) for (const child of heldKeys(node)) parents.set(child, key);
887
+ let key = Object.keys(anatomy).find((candidate) => slotAnatomyPropOf(anatomy[candidate]) === from);
888
+ const seen = /* @__PURE__ */ new Set();
889
+ while (key !== void 0 && !seen.has(key)) {
890
+ seen.add(key);
891
+ const layer = layerOf(anatomy[key]?.element);
892
+ if (layer !== void 0) return layer;
893
+ key = parents.get(key);
894
+ }
895
+ }
896
+ /**
897
+ * Layer → the forwards that hand a VALUE to one of its props.
898
+ *
899
+ * `startIcon: { type: 'forward', target: 'startIcon/name' }` and `swatch: { type: 'forward', target:
900
+ * 'swatch/swatchColor' }` are this shape: the instance's value lands on the layer's element as the
901
+ * prop the forward names, the way the emitted module spreads it into that layer's bundle. A forward
902
+ * whose target prop is a slot is content instead, and {@link slotTargetsOf} answers for it.
903
+ */
904
+ function valueForwardsOf(config, path) {
905
+ const body = bodyOf(config, path);
906
+ return body ? valueForwards(config, body) : [];
907
+ }
908
+ /**
909
+ * The instance props whose values or content reach `layer`: content routed into it, values
910
+ * forwarded onto one of its props, and the props the layer exposes through `forwards`. A file
911
+ * editing a placed instance can change exactly these about the layer; the layer's own styles are
912
+ * the component's.
913
+ */
914
+ /**
915
+ * The instance prop whose content or value lands on `into` of `layer`, or `undefined` when the
916
+ * layer owns that prop itself. A change to the layer's prop in a cell is then a change to the
917
+ * instance prop's preview value, not a style.
918
+ */
919
+ function propInto({ config, path, layer, into }) {
920
+ return [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)].find((route) => route.layer === layer && route.into === into)?.from;
921
+ }
922
+ /** The route a prop's content or value takes into a layer, or `undefined` for a prop that reaches
923
+ * no layer. */
924
+ function routeOf({ config, path, prop }) {
925
+ return [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)].find((route) => route.from === prop);
926
+ }
927
+ function propsIntoLayer({ config, path, layer }) {
928
+ const names = /* @__PURE__ */ new Set();
929
+ for (const route of slotTargetsOf(config, path)) if (route.layer === layer) names.add(route.from);
930
+ for (const route of valueForwardsOf(config, path)) if (route.layer === layer) names.add(route.from);
931
+ for (const name of forwardedLayers(config, path).get(layer) ?? []) names.add(name);
932
+ return [...names];
933
+ }
934
+ function valueForwards(config, body) {
935
+ const routes = [];
936
+ for (const [from, decl] of Object.entries(body.props ?? {})) {
937
+ if (decl?.type !== "forward") continue;
938
+ if (routesContent(config, body.layers, decl)) continue;
939
+ const target = refOf(decl.target);
940
+ if (!target) continue;
941
+ routes.push({
942
+ layer: pathOf(target),
943
+ from,
944
+ into: intoName(decl.into)
945
+ });
946
+ }
947
+ return routes;
751
948
  }
752
949
  /** The className a layer wears for one instance: its own class, every style rule whose condition
753
950
  * holds, and — on the root only — what the call site passed. */
@@ -779,12 +976,53 @@ function place(sink, type, props, children, hint, slots) {
779
976
  const key = freeKey(sink, hint);
780
977
  sink.elements[key] = {
781
978
  type: tag,
782
- props: own,
979
+ props: moduleProps(sink.config, tag, own),
783
980
  ...children.length ? { children: [...children] } : {},
784
981
  ...slots && Object.keys(slots).length ? { slots } : {}
785
982
  };
786
983
  return key;
787
984
  }
985
+ /**
986
+ * An element's props as the component behind it reads them.
987
+ *
988
+ * A style prop's token arrives as the stored ref when an instance's defaults come from the config
989
+ * (`bg` as `token:bg/primary`). The expansion reads such a ref through the prop's domain and paints
990
+ * the class; a compiled module reads a value string and paints nothing for an object. So an element
991
+ * left to its registry entry gets the same leaf the expansion would have named, resolved through
992
+ * the component's own prop routing (`gap` on a `VStack` writes `gapY`). A modifier block nests a bag
993
+ * of the same shape. Only a token ref on a key that writes a style property is touched: a component
994
+ * ref handed as a prop value stays the ref it is.
995
+ */
996
+ function moduleProps(config, type, props) {
997
+ const through = type.includes(":") && kindOf(type) === "component" ? [pathOf(type)] : [];
998
+ const modifiers = new Set(config.list("modifier").map((modifier) => `_${modifier.leaf}`));
999
+ const walk = (bag) => {
1000
+ let changed = false;
1001
+ const out = {};
1002
+ for (const [key, value] of Object.entries(bag)) {
1003
+ if (modifiers.has(key) && value && typeof value === "object" && !Array.isArray(value) && !isRef(value)) {
1004
+ const inner = walk(value);
1005
+ out[key] = inner;
1006
+ changed ||= inner !== value;
1007
+ continue;
1008
+ }
1009
+ if (isRef(value) && kindOf(value.__ref) === "token") {
1010
+ const property = stylePropertyPathFor({
1011
+ config,
1012
+ through,
1013
+ key
1014
+ });
1015
+ const leaf = property === void 0 ? value : authoredValue(config, property, value);
1016
+ out[key] = leaf;
1017
+ changed ||= leaf !== value;
1018
+ continue;
1019
+ }
1020
+ out[key] = value;
1021
+ }
1022
+ return changed ? out : bag;
1023
+ };
1024
+ return walk(props);
1025
+ }
788
1026
  /** Whether any node under `key` (itself excluded) binds `prop` as its content. */
789
1027
  function subtreeBinds(anatomy, key, prop) {
790
1028
  const seen = /* @__PURE__ */ new Set();
@@ -812,10 +1050,13 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
812
1050
  const { config } = sink;
813
1051
  const body = bodyOf(config, path) ?? {};
814
1052
  const layers = body.layers ?? {};
815
- const anatomy = body.anatomy && Object.keys(body.anatomy).length > 0 ? body.anatomy : { root: { element: { __ref: `layer:${rootKeyOf(layers)}` } } };
1053
+ const stored = body.anatomy && Object.keys(body.anatomy).length > 0;
1054
+ const anatomy = stored ? body.anatomy : { root: { element: { __ref: `layer:${rootKeyOf(layers)}` } } };
816
1055
  const filled = {};
817
1056
  for (const [from, fills] of Object.entries(instanceSlots ?? {})) if (fills.length > 0 && instanceProps[from] === void 0) filled[from] = true;
1057
+ const declaredDefaults = declaredPropDefaults(body.props);
818
1058
  const state = {
1059
+ ...declaredDefaults,
819
1060
  ...body.defaultProps,
820
1061
  ...filled,
821
1062
  ...instanceProps
@@ -823,19 +1064,13 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
823
1064
  const consumed = consumedProps(config, path, body);
824
1065
  const { byLayer: targets } = slotTargets(config, path, body);
825
1066
  const valueRoutes = /* @__PURE__ */ new Map();
826
- for (const [from, decl] of Object.entries(body.props ?? {})) {
827
- if (decl?.type !== "forward") continue;
828
- if (routesContent(config, layers, decl)) continue;
829
- const target = refOf(decl.target);
830
- if (!target) continue;
831
- const routes = valueRoutes.get(pathOf(target)) ?? [];
832
- routes.push({
833
- from,
834
- into: intoName(decl.into)
835
- });
836
- valueRoutes.set(pathOf(target), routes);
1067
+ for (const route of valueForwards(config, body)) {
1068
+ const routes = valueRoutes.get(route.layer) ?? [];
1069
+ routes.push(route);
1070
+ valueRoutes.set(route.layer, routes);
837
1071
  }
838
1072
  const supplied = {
1073
+ ...declaredDefaults,
839
1074
  ...body.defaultProps ?? {},
840
1075
  ...instanceProps
841
1076
  };
@@ -887,6 +1122,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
887
1122
  const layer = layerOf(node.element);
888
1123
  const isRoot = key === hostNode;
889
1124
  let type = typeOf(layers, node.element);
1125
+ if (!stored && isRoot && type !== void 0 && kindOf(type) === "component") type = renderedElement(config, path);
890
1126
  const bound = boundPropOf(node.element);
891
1127
  if (bound) type = boundChoice(config, path, node.element, state[bound]);
892
1128
  type = wrappedTypeOf(node, type);
@@ -904,7 +1140,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
904
1140
  return key;
905
1141
  }
906
1142
  const slot = layer ? targets.get(layer) : void 0;
907
- const slotValue = slot && !subtreeBinds(anatomy, key, slot.from) ? instanceProps[slot.from] : void 0;
1143
+ const slotValue = slot && !subtreeBinds(anatomy, key, slot.from) && !(positioned.has(slot.from) && isText(instanceProps[slot.from])) ? instanceProps[slot.from] : void 0;
908
1144
  const props = layer ? {
909
1145
  ...authoredProps(config, node.props),
910
1146
  "data-uds-component": path,
@@ -919,12 +1155,16 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
919
1155
  isRoot ? instanceProps.className : void 0,
920
1156
  layerProps[layer]?.className
921
1157
  ]) || void 0
922
- } : authoredProps(config, node.props);
1158
+ } : {
1159
+ ...authoredProps(config, node.props),
1160
+ [ANATOMY_MARKER]: key
1161
+ };
923
1162
  if (node.text !== void 0) props.children = node.text;
924
1163
  for (const [name, value] of Object.entries(props)) {
925
1164
  const settled = resolveChosenValue(value, state);
926
1165
  if (settled !== value) props[name] = settled;
927
1166
  }
1167
+ markReadOnlyField(config, type, props);
928
1168
  const children = [];
929
1169
  const boundFills = {};
930
1170
  for (const [name, value] of Object.entries(props)) {
@@ -935,9 +1175,16 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
935
1175
  if (name === "children") children.push(...fills);
936
1176
  else boundFills[name] = [...fills];
937
1177
  }
1178
+ let positionedText;
938
1179
  for (const child of node.children ?? []) {
939
1180
  const childSlot = slotAnatomyPropOf(anatomy[child]);
940
1181
  if (childSlot !== void 0) {
1182
+ if (resolveVisibility(anatomy[child]?.visible, state) === false) continue;
1183
+ const given = state[childSlot];
1184
+ if (isText(given)) {
1185
+ positionedText = String(given);
1186
+ contentPlaced = contentPlaced || childSlot === untargetedSlot;
1187
+ }
941
1188
  if (childSlot === untargetedSlot) {
942
1189
  const fills = instanceSlots?.[childSlot] ?? [];
943
1190
  if (allContent.length > 0 || fills.length > 0) {
@@ -951,6 +1198,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
951
1198
  if (placed) children.push(placed);
952
1199
  }
953
1200
  if (isRoot && allContent.length > 0 && !contentPlaced) children.push(...allContent);
1201
+ if (positionedText !== void 0 && props.children === void 0) props.children = positionedText;
954
1202
  const slots = { ...boundFills };
955
1203
  const routed = slot ? instanceSlots?.[slot.from] : void 0;
956
1204
  const overridden = routed?.length && slot && !slot.untargeted ? intoName(slot.into) : void 0;
@@ -1041,4 +1289,4 @@ function expandComponent(config, path, props = {}, options = {}) {
1041
1289
  return expandSpec(config, instanceSpec(path, props), options);
1042
1290
  }
1043
1291
  //#endregion
1044
- export { boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, liveStateProps, previewSpec, readStatePointer, resolveVisibility, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, statePointerProp, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
1292
+ export { ANATOMY_MARKER, anatomyElementComponent, anatomyTextProp, andListOf, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveVisibility, routeOf, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, spelled, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
15
15
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
16
16
  * newer than the constant, and the assertion fails until the author re-mints.
17
17
  */
18
- declare const CURRENT_SCHEMA_VERSION = 20260918161049;
18
+ declare const CURRENT_SCHEMA_VERSION = 20260920161309;
19
19
  /** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
20
20
  * and a raw old-shape record both flow in without casts. */
21
21
  interface VersionStamped {
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
13
13
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
14
14
  * newer than the constant, and the assertion fails until the author re-mints.
15
15
  */
16
- const CURRENT_SCHEMA_VERSION = 20260918161049;
16
+ const CURRENT_SCHEMA_VERSION = 20260920161309;
17
17
  /** The mint that moved `options` into the `settings` kind — restated here, rather than imported
18
18
  * from the migration that owns it, because that migration imports this module. */
19
19
  const SETTINGS_AS_KIND_VERSION = 20260908171448;
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
45
45
  for (const migration of migrations) {
46
46
  if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
47
47
  if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
48
- if (migration.version > 20260918161049) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
48
+ if (migration.version > 20260920161309) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
49
49
  REGISTERED.set(migration.version, migration);
50
50
  }
51
51
  }
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
102
102
  * Already-current input passes through untouched — same reference, so hot paths pay nothing.
103
103
  */
104
104
  function upgradeSerializedConfig(json, options) {
105
- const target = options?.target ?? 20260918161049;
105
+ const target = options?.target ?? 20260920161309;
106
106
  const version = detectedWireVersion(json);
107
107
  if (version > target) throw new SchemaVersionTooNew(version, target);
108
108
  const raw = json;
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
160
160
  * meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
161
161
  */
162
162
  function upgradePatch(patch, writtenAt, options) {
163
- const target = options?.target ?? 20260918161049;
163
+ const target = options?.target ?? 20260920161309;
164
164
  if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
165
165
  let current = patch;
166
166
  for (const migration of pendingFor(writtenAt, target, options)) {
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
179
179
  * stamp would cause.
180
180
  */
181
181
  function upgradeDraftEntries(draft, options) {
182
- const target = options?.target ?? 20260918161049;
182
+ const target = options?.target ?? 20260920161309;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -162,6 +162,19 @@ declare function overlayVerb(overlay: LocalOverlay, field: LeafAddress): Overlay
162
162
  * have to call it one or the other.
163
163
  */
164
164
  declare function misdeclaredOverlays(config: Config): MisdeclaredOverlay[];
165
+ /**
166
+ * Split a merged body into `override`/`extend` by judging each leaf against the source — the forward
167
+ * direction of the walk {@link misdeclaredOverlays} runs to check one. A leaf the source defines
168
+ * differently is an override; one it lacks is an extension; one it spells the same is neither, and is
169
+ * left out.
170
+ *
171
+ * What a directive written this way can never do is trip the check that classified it: every leaf
172
+ * lands on the side the live source agrees it belongs on, by construction.
173
+ */
174
+ declare function classifyOverlay(local: Record<string, unknown>, theirs: unknown): {
175
+ override?: Record<string, unknown>;
176
+ extend?: Record<string, unknown>;
177
+ };
165
178
  /** A directive aimed at a path whose kind the pin never adopted. */
166
179
  interface UnadoptedDirective {
167
180
  slug: string;
@@ -223,4 +236,4 @@ interface UnstatableCondition {
223
236
  */
224
237
  declare function unstatableConditions(config: Config): UnstatableCondition[];
225
238
  //#endregion
226
- export { DanglingDirective, DanglingRef, LeafAddress, MisdeclaredOverlay, OverlayVerb, RedundantQualifier, UnadoptedDirective, UnreadableBorrow, UnstatableCondition, danglingDirectives, danglingSourcedRefs, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, unadoptedDirectives, unreadableBorrows, unstatableConditions, valueAt };
239
+ export { DanglingDirective, DanglingRef, LeafAddress, MisdeclaredOverlay, OverlayVerb, RedundantQualifier, UnadoptedDirective, UnreadableBorrow, UnstatableCondition, classifyOverlay, danglingDirectives, danglingSourcedRefs, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, unadoptedDirectives, unreadableBorrows, unstatableConditions, valueAt };
@@ -399,5 +399,14 @@ function rewriteRefSource(value, from, to) {
399
399
  }
400
400
  return value;
401
401
  }
402
+ /**
403
+ * A condition's identity: its entries sorted by prop, each value read to its leaf, so a bare name
404
+ * and the ref it is stored as spell the same key. `*` for no condition. A style rule and a preview
405
+ * condition are both keyed by this.
406
+ */
407
+ function canonicalCondition(when) {
408
+ const entries = Object.entries(when ?? {}).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
409
+ return entries.length === 0 ? "*" : entries.map(([k, v]) => `${k}=${String(refLeaf(v))}`).join("&");
410
+ }
402
411
  //#endregion
403
- export { ENTITY_PATH_MESSAGE, MINTED, collectRefs, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, qualifyRefs, ref, refLeaf, refSchema, rewriteRefNamespace, rewriteRefSource, rewriteRefs, singletonRef, sourceOf, splitRef };
412
+ export { ENTITY_PATH_MESSAGE, MINTED, canonicalCondition, collectRefs, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, qualifyRefs, ref, refLeaf, refSchema, rewriteRefNamespace, rewriteRefSource, rewriteRefs, singletonRef, sourceOf, splitRef };
@@ -18,6 +18,8 @@ interface SpecNode {
18
18
  readonly type: string;
19
19
  readonly props?: Record<string, unknown>;
20
20
  readonly children?: readonly SpecNode[];
21
+ /** Named slot fills, each a list of nodes under the slot prop they fill. */
22
+ readonly slots?: Readonly<Record<string, readonly SpecNode[]>>;
21
23
  readonly text?: string;
22
24
  }
23
25
  /** Validate a design-file spec against the config — every node's `type` a real component and its
@@ -1,6 +1,7 @@
1
1
  import { kindOf, memberOf, pathOf, splitRef } from "./utils/refs.js";
2
2
  import { childrenPolicy } from "../entities/system/element.js";
3
3
  import { resolveComponentProps } from "./projections.js";
4
+ import { slotOf } from "../spec/slot.js";
4
5
  import { refResolves } from "./ref-integrity.js";
5
6
  //#region src/framework/validate-spec.ts
6
7
  /**
@@ -29,6 +30,18 @@ function walk(config, node, path, errors) {
29
30
  const props = resolveComponentProps(config, target);
30
31
  for (const name of Object.keys(node.props ?? {})) if (!(name in props)) errors.push(`${path}: unknown prop "${name}" on "${node.type}"`);
31
32
  if (node.children?.length && childrenPolicy(config, target) === "none") errors.push(`${path}: "${node.type}" takes no children`);
33
+ for (const [name, fills] of Object.entries(node.slots ?? {})) {
34
+ const slot = slotOf({
35
+ config,
36
+ path: target,
37
+ slot: name
38
+ });
39
+ if (!slot) errors.push(`${path}: "${node.type}" has no slot "${name}"`);
40
+ fills.forEach((fill, i) => {
41
+ if (slot && !slot.takes(fill.type)) errors.push(`${path}: slot "${name}" of "${node.type}" takes ${slot.accepts.join(", ")}, not "${fill.type}"`);
42
+ walk(config, fill, `${path} > ${node.type}.${name}[${i}]`, errors);
43
+ });
44
+ }
32
45
  }
33
46
  (node.children ?? []).forEach((child, i) => {
34
47
  walk(config, child, `${path} > ${node.type}[${i}]`, errors);