@yahoo/uds-create-config 4.25.1 → 4.26.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.
@@ -8,7 +8,7 @@ import { defineSubEntity } from "../../framework/defineSubEntity.js";
8
8
  import { derivedColor, gradient } from "./color.js";
9
9
  import { listedIn } from "../../framework/utils/enumerated.js";
10
10
  import { valueSchemaOf } from "../../framework/value-domain.js";
11
- import { glyphLeafOf } from "./glyph-identity.js";
11
+ import { bindPreviewGlyphs, glyphLeafOf } from "./glyph-identity.js";
12
12
  import { Style, addBagKeyIssues, anatomyPropValue, bagNameEdges, bagRewriteName, bindBagTokens, collectBag, isPlainRecord, openBag, styleValue } from "./style-bag.js";
13
13
  import { storedPreview } from "../../framework/preview-store.js";
14
14
  import { glyphPropsOf, propValueDomain, refListOf, resolveComponentProps, stylePropValueLeaves, validateComponentProps } from "../../framework/projections.js";
@@ -21,7 +21,7 @@ import { File } from "./File.js";
21
21
  import { iconDomainInvariants } from "./icon-domain-invariants.js";
22
22
  import { rootLayerInvariants } from "./root-layer-invariants.js";
23
23
  import { Token } from "./Token.js";
24
- import { carriesContent, intoName, isValueSlot, routedPropIn, routesContent } from "../../framework/prop-surface.js";
24
+ import { carriesContent, intoName, isValueSlot, renderBoundProp, routedPropIn, routesContent } from "../../framework/prop-surface.js";
25
25
  import { z } from "zod";
26
26
  //#region src/entities/system/Component.ts
27
27
  /**
@@ -985,7 +985,10 @@ const Preview = defineSubEntity({
985
985
  for (const [name, decl] of Object.entries(body.props ?? {})) when[name] = conditionValue(name, decl, config).optional();
986
986
  return z.strictObject({
987
987
  when: z.strictObject(when, { error: previewShapeError }).nullable().default(null),
988
- props: z.record(z.string(), z.unknown())
988
+ props: z.record(z.string(), z.unknown()).transform((props) => bindPreviewGlyphs({
989
+ config,
990
+ props
991
+ }))
989
992
  }, { error: previewShapeError });
990
993
  }
991
994
  });
@@ -1329,11 +1332,13 @@ const AnatomyNode = defineSubEntity({
1329
1332
  continue;
1330
1333
  }
1331
1334
  if (!isPlainObject(value) || !("$state" in value)) continue;
1335
+ const read = renderBoundProp(value);
1336
+ if (read !== void 0 && body.props && read in body.props) continue;
1332
1337
  ctx.issues.push({
1333
1338
  code: "custom",
1334
1339
  input: value,
1335
1340
  path: ["props", prop],
1336
- message: `\`${prop}\` is bound with \`$state\`, which anatomy uses for \`visible\` and for an \`element\` a prop chooses, never for a prop value: the object would reach the element as-is and paint nothing. Content reaches a node through the prop's declaration — \`{ "type": "forward", "target": "<layer>/children" }\` lands it in that layer, and a \`{ "element": "Slot", "props": { "name": "<prop>" } }\` node among a parent's \`children\` marks where a slot's content goes. To vary content by a prop, give each state its own node and choose between them with \`visible\`.`
1341
+ message: `\`${prop}\` is bound with \`$state\`, which anatomy uses for \`visible\` and for an \`element\` a prop chooses, and for a bare read of one of this component's own props that the render passes down (\`{ "$state": "/<prop>" }\`); any other binding would reach the element as-is and paint nothing. Content reaches a node through the prop's declaration — \`{ "type": "forward", "target": "<layer>/children" }\` lands it in that layer, and a \`{ "element": "Slot", "props": { "name": "<prop>" } }\` node among a parent's \`children\` marks where a slot's content goes. To vary content by a prop, give each state its own node and choose between them with \`visible\`.`
1337
1342
  });
1338
1343
  }
1339
1344
  const slotAnatomyProp = slotAnatomyPropOf({
@@ -1,6 +1,6 @@
1
1
  import { isRef, pathOf } from "../../framework/utils/refs.js";
2
2
  import { defineEntity } from "../../framework/defineEntity.js";
3
- import { glyphOfLibrary, recordGlyphIdentities } from "./glyph-identity.js";
3
+ import { glyphOfLibrary, libraryGlyphFacts, recordGlyphIdentities } from "./glyph-identity.js";
4
4
  import { Package } from "./Package.js";
5
5
  import { iconDomainInvariants } from "./icon-domain-invariants.js";
6
6
  import { iconMemberMetadata, iconMetadataDeclaration } from "./icon-metadata.js";
@@ -93,15 +93,11 @@ glyphs: ({ input, body }) => {
93
93
  * open-bag pass turned it into. Either is answered as the member name when the export is one of
94
94
  * this library's and comes from the module its `package` names.
95
95
  */
96
- authoredValue: ({ config, target, leaves }) => {
97
- const meta = config.configFor(target)?.groupMeta("icon", pathOf(target));
98
- const module = isRef(meta?.package) ? pathOf(meta.package) : void 0;
99
- return glyphOfLibrary({
100
- library: pathOf(target),
101
- module,
102
- leaves
103
- });
104
- },
96
+ authoredValue: ({ config, target, leaves }) => glyphOfLibrary(libraryGlyphFacts({
97
+ config,
98
+ target,
99
+ leaves
100
+ })),
105
101
  invariants: (config) => iconDomainInvariants(config)
106
102
  });
107
103
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { canonicalCondition, isRef, kindOf, memberRef, pathOf } from "../../framework/utils/refs.js";
2
- import { isComponentType } from "./glyph-identity.js";
2
+ import { glyphIdentityOf, glyphPackageRefOf, isComponentType } from "./glyph-identity.js";
3
3
  import "../../framework/preview-store.js";
4
4
  import { Component, canonicalWhen, layerRef } from "./Component.js";
5
5
  //#region src/entities/system/defineComponent.ts
@@ -46,13 +46,10 @@ function build(body) {
46
46
  * this runs when a config is registered, and a reader can't tell which made the config it holds.
47
47
  *
48
48
  * A component handed as a value (`icon: PlusIcon`) stores as the bare ref the registry keys it under,
49
- * so the receiving component gets the component back rather than a node.
50
- *
51
- * OPEN: a foreign component (a Phosphor glyph) is dropped. Its export is known once its library has
52
- * handed `register` its namespace (`glyphIdentityOf`), but a preview entry's props are stored open
53
- * (`Preview.fields`) and never re-bound to a member, so a ref written here would reach the build as
54
- * a package export the package row never lists. An absent default renders nothing; the build fills
55
- * the prop from the import specifier (`extractPreviewProps`).
49
+ * so the receiving component gets the component back rather than a node. A glyph of a registered
50
+ * library stores as its package export, and the owner-bound pass (`Preview.fields`) binds that to
51
+ * the library member, the spelling the build writes. A component neither side can name is dropped:
52
+ * an absent default renders nothing, where a guessed ref would render the wrong thing.
56
53
  */
57
54
  function storableProps(props) {
58
55
  if (props === void 0) return void 0;
@@ -68,9 +65,13 @@ function storableProps(props) {
68
65
  function isReactElement(value) {
69
66
  return typeof value === "object" && value !== null && "$$typeof" in value && "props" in value;
70
67
  }
71
- /** A component handed as a value, as the ref that names it — `undefined` for one this path can't name. */
68
+ /** A component handed as a value, as the ref that names it: a component of this system under its
69
+ * path, a glyph a library handed `register` as its package export. `undefined` for one neither
70
+ * names. */
72
71
  function storableComponentRef(value) {
73
- return isComponentElement(value) && value.path ? Component.ref(value.path) : void 0;
72
+ if (isComponentElement(value) && value.path) return Component.ref(value.path);
73
+ const identity = glyphIdentityOf(value);
74
+ return identity ? glyphPackageRefOf(identity) : void 0;
74
75
  }
75
76
  const FRAGMENT = Symbol.for("react.fragment");
76
77
  /**
@@ -99,6 +99,25 @@ const glyphPackageRef = z.custom(isComponentType).transform((value, ctx) => {
99
99
  });
100
100
  glyphPackageRef._zod.toJSONSchema = () => ({ not: {} });
101
101
  /**
102
+ * The facts for the library a target names. `target` is the library's ref (`icon:phosphor`, with a
103
+ * source qualifier for a borrowed set), whose config answers for its `package`. `leaves` may be
104
+ * handed in by a domain that has already expanded them.
105
+ */
106
+ function libraryGlyphFacts({ config, target, leaves = memberLeaves(config, target) }) {
107
+ const library = pathOf(target);
108
+ const named = (config.configFor(target)?.groupMeta("icon", library))?.package;
109
+ return {
110
+ library,
111
+ module: isRef(named) ? pathOf(named) : void 0,
112
+ leaves
113
+ };
114
+ }
115
+ /** Whether a library ships a glyph: the identity's specifier is the library's package and its name
116
+ * is one of the members. */
117
+ function libraryClaims(facts, identity) {
118
+ return facts.module === identity.specifier && facts.leaves.includes(identity.name);
119
+ }
120
+ /**
102
121
  * The owner-bound form of a glyph for one library: a component or its `package:` ref, answered as
103
122
  * the member name so the domain binds it like a written leaf. Refused, naming the reason, when the
104
123
  * export belongs to another module than the library draws from or is not one of its members.
@@ -138,16 +157,42 @@ function glyphOfLibrary({ library, module, leaves }) {
138
157
  * so a glyph of a library the key does not reach says so.
139
158
  */
140
159
  function glyphLeafOf({ config, libraries }) {
141
- const arms = libraries.map((library) => {
142
- const named = config.groupMeta("icon", library)?.package;
143
- return glyphOfLibrary({
144
- library,
145
- module: isRef(named) ? pathOf(named) : void 0,
146
- leaves: memberLeaves(config, `icon:${library}`)
147
- });
148
- });
160
+ const arms = libraries.map((library) => glyphOfLibrary(libraryGlyphFacts({
161
+ config,
162
+ target: `icon:${library}`
163
+ })));
149
164
  if (arms.length === 1) return arms[0];
150
165
  return z.union(arms);
151
166
  }
167
+ /** `icon:<library>/<name>` for a glyph identity a library of this config claims; `undefined` when
168
+ * none does. */
169
+ function glyphMemberRefOf(config, identity) {
170
+ for (const { path } of config.groups("icon")) if (libraryClaims(libraryGlyphFacts({
171
+ config,
172
+ target: `icon:${path}`
173
+ }), identity)) return { __ref: `icon:${path}/${identity.name}` };
174
+ }
175
+ /**
176
+ * Preview props with every glyph bound to its library member.
177
+ *
178
+ * A glyph handed under `.preview()` is stored open as the package export it is
179
+ * (`package:<specifier>#<name>`), since the props there are not each bound to a declared prop the
180
+ * way `defaultProps` are. Once an owner is known, this pass binds each such ref to
181
+ * `icon:<library>/<name>` through the library whose package ships it, at the top level and inside a
182
+ * stored element's props, which is the spelling the build writes when it reads `.preview()` from
183
+ * source. A `package:` ref no library claims is another package's export and stays as written.
184
+ */
185
+ function bindPreviewGlyphs({ config, props }) {
186
+ const bind = (value) => {
187
+ if (typeof value !== "object" || value === null) return value;
188
+ if (Array.isArray(value)) return value.map(bind);
189
+ const identity = glyphIdentityOfRef(value);
190
+ if (identity) return glyphMemberRefOf(config, identity) ?? value;
191
+ const out = {};
192
+ for (const [key, member] of Object.entries(value)) out[key] = bind(member);
193
+ return out;
194
+ };
195
+ return bind(props);
196
+ }
152
197
  //#endregion
153
- export { glyphLeafOf, glyphOfLibrary, glyphPackageRef, isComponentType, recordGlyphIdentities };
198
+ export { bindPreviewGlyphs, glyphIdentityOf, glyphLeafOf, glyphOfLibrary, glyphPackageRef, glyphPackageRefOf, isComponentType, libraryGlyphFacts, recordGlyphIdentities };
@@ -0,0 +1,97 @@
1
+ import { Config } from "./Config.js";
2
+ import { BindingKind } from "./prop-surface.js";
3
+
4
+ //#region src/framework/bindings.d.ts
5
+ /** Which marker a value is, by the key that makes it one. */
6
+ type MarkerKind = 'state' | 'bindState' | 'cond' | 'item' | 'index' | 'computed' | 'template' | 'other';
7
+ /**
8
+ * The marker a value is, or `undefined` for a literal. A record with any other `$`-prefixed key is
9
+ * `other`: a directive this module can name but not settle.
10
+ */
11
+ declare function markerKind(value: unknown): MarkerKind | undefined;
12
+ /** Whether a value reads state anywhere, however deep: a `$state` or `$bindState`, in a prop value or
13
+ * in a condition. */
14
+ declare function readsState(value: unknown): boolean;
15
+ /** The state path a `$state` or `$bindState` reads, as written, or `undefined` for any other value. */
16
+ declare function bindingPath(value: unknown): string | undefined;
17
+ /** A `$cond` value's parts, or `undefined` for any other value. */
18
+ declare function condArms(value: unknown): {
19
+ when: unknown;
20
+ ifTrue: unknown;
21
+ ifFalse: unknown;
22
+ } | undefined;
23
+ /** Whether a marker sits anywhere in a value, however deep. */
24
+ declare function containsMarker(value: unknown): boolean;
25
+ /** A value settled against a prop state, or the fact that it could not be. */
26
+ type ResolvedBinding = {
27
+ readonly resolved: true;
28
+ readonly value: unknown;
29
+ } | {
30
+ readonly resolved: false;
31
+ };
32
+ /**
33
+ * A value settled against `state`, the whole prop state of the instance being read: its defaults,
34
+ * overrides and cell coordinate.
35
+ *
36
+ * A `$state` or `$bindState` reads the state, and a prop the state lacks reads `undefined`. A
37
+ * `$cond` settles only when its condition reads props the state holds, since a capture can key a
38
+ * condition on a seed no prop state carries, and reading that seed as `undefined` would pick a
39
+ * branch the render never paints. Only the branch taken has to settle. Any other directive, and
40
+ * `$template` in particular, which interpolates an unread path to `''`, stays unresolved.
41
+ */
42
+ declare function resolveBinding(value: unknown, state: Record<string, unknown>): ResolvedBinding;
43
+ /** A read of one prop: `{ $state: '/tone' }`. */
44
+ declare function stateRef(prop: string): {
45
+ $state: string;
46
+ };
47
+ /** A two-way read of one state key, for a control that writes it back: `{ $bindState: '/open' }`. */
48
+ declare function bindStateRef(key: string): {
49
+ $bindState: string;
50
+ };
51
+ /**
52
+ * A test of one prop, in the grammar `visible` and `$cond` share: truthy, or equal to a value, and
53
+ * negated with `not`. Naming `eq` makes it a comparison even when its value is `undefined`.
54
+ */
55
+ declare function stateTest(test: {
56
+ prop: string;
57
+ eq?: unknown;
58
+ not?: boolean;
59
+ }): Record<string, unknown>;
60
+ /** A value chosen by a condition: `{ $cond, $then, $else }`. */
61
+ declare function condValue({
62
+ when,
63
+ ifTrue,
64
+ ifFalse
65
+ }: {
66
+ when: unknown;
67
+ ifTrue: unknown;
68
+ ifFalse: unknown;
69
+ }): {
70
+ $cond: unknown;
71
+ $then: unknown;
72
+ $else: unknown;
73
+ };
74
+ /** The component prop that drives one of a layer's props, and how it gets there. */
75
+ interface LayerPropDriver {
76
+ /** The component prop, without a pointer slash. */
77
+ readonly prop: string;
78
+ /** A declared forward, or a binding the render recorded. */
79
+ readonly kind: BindingKind;
80
+ /** For a render binding, the anatomy node it sits on, which is what clearing it edits. */
81
+ readonly node?: string;
82
+ }
83
+ /**
84
+ * Every prop of `layer` a component prop drives, keyed by the layer prop. A declared forward wins
85
+ * where a render binding names the same layer prop.
86
+ */
87
+ declare function layerPropDrivers({
88
+ config,
89
+ path,
90
+ layer
91
+ }: {
92
+ config: Config;
93
+ path: string;
94
+ layer: string;
95
+ }): Record<string, LayerPropDriver>;
96
+ //#endregion
97
+ export { LayerPropDriver, MarkerKind, ResolvedBinding, bindStateRef, bindingPath, condArms, condValue, containsMarker, layerPropDrivers, markerKind, readsState, resolveBinding, stateRef, stateTest };
@@ -0,0 +1,173 @@
1
+ import { isExpression, readStatePointer, resolveVisibility, slotTargetsOf, valueForwardsOf } from "./render-spec.js";
2
+ import { renderBindingsOf } from "./prop-surface.js";
3
+ //#region src/framework/bindings.ts
4
+ const KIND_BY_KEY = [
5
+ ["$bindState", "bindState"],
6
+ ["$cond", "cond"],
7
+ ["$computed", "computed"],
8
+ ["$template", "template"],
9
+ ["$item", "item"],
10
+ ["$index", "index"],
11
+ ["$state", "state"]
12
+ ];
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ /**
17
+ * The marker a value is, or `undefined` for a literal. A record with any other `$`-prefixed key is
18
+ * `other`: a directive this module can name but not settle.
19
+ */
20
+ function markerKind(value) {
21
+ if (!isRecord(value)) return void 0;
22
+ for (const [key, kind] of KIND_BY_KEY) if (key in value) return kind;
23
+ return Object.keys(value).some((key) => key.startsWith("$")) ? "other" : void 0;
24
+ }
25
+ /** Whether a value reads state anywhere, however deep: a `$state` or `$bindState`, in a prop value or
26
+ * in a condition. */
27
+ function readsState(value) {
28
+ if (Array.isArray(value)) return value.some(readsState);
29
+ if (!isRecord(value)) return false;
30
+ if ("$state" in value || "$bindState" in value) return true;
31
+ return Object.values(value).some(readsState);
32
+ }
33
+ /** The state path a `$state` or `$bindState` reads, as written, or `undefined` for any other value. */
34
+ function bindingPath(value) {
35
+ if (!isRecord(value)) return void 0;
36
+ const kind = markerKind(value);
37
+ const path = kind === "state" ? value.$state : kind === "bindState" ? value.$bindState : void 0;
38
+ return typeof path === "string" ? path : void 0;
39
+ }
40
+ /** A `$cond` value's parts, or `undefined` for any other value. */
41
+ function condArms(value) {
42
+ if (!isRecord(value) || markerKind(value) !== "cond") return void 0;
43
+ if (!Object.hasOwn(value, "$then") || !Object.hasOwn(value, "$else")) return;
44
+ return {
45
+ when: value.$cond,
46
+ ifTrue: value.$then,
47
+ ifFalse: value.$else
48
+ };
49
+ }
50
+ /** Whether a marker sits anywhere in a value, however deep. */
51
+ function containsMarker(value) {
52
+ if (Array.isArray(value)) return value.some(containsMarker);
53
+ if (!isRecord(value)) return false;
54
+ if (isExpression(value) || markerKind(value) !== void 0) return true;
55
+ return Object.values(value).some(containsMarker);
56
+ }
57
+ const UNRESOLVED = { resolved: false };
58
+ /**
59
+ * A value settled against `state`, the whole prop state of the instance being read: its defaults,
60
+ * overrides and cell coordinate.
61
+ *
62
+ * A `$state` or `$bindState` reads the state, and a prop the state lacks reads `undefined`. A
63
+ * `$cond` settles only when its condition reads props the state holds, since a capture can key a
64
+ * condition on a seed no prop state carries, and reading that seed as `undefined` would pick a
65
+ * branch the render never paints. Only the branch taken has to settle. Any other directive, and
66
+ * `$template` in particular, which interpolates an unread path to `''`, stays unresolved.
67
+ */
68
+ function resolveBinding(value, state) {
69
+ if (Array.isArray(value)) {
70
+ const out = [];
71
+ for (const entry of value) {
72
+ const inner = resolveBinding(entry, state);
73
+ if (!inner.resolved) return UNRESOLVED;
74
+ out.push(inner.value);
75
+ }
76
+ return {
77
+ resolved: true,
78
+ value: out
79
+ };
80
+ }
81
+ if (!isRecord(value)) return {
82
+ resolved: true,
83
+ value
84
+ };
85
+ const kind = markerKind(value);
86
+ const directives = Object.keys(value).filter((key) => key.startsWith("$"));
87
+ if (kind === void 0) {
88
+ const out = {};
89
+ for (const [key, member] of Object.entries(value)) {
90
+ const inner = resolveBinding(member, state);
91
+ if (!inner.resolved) return UNRESOLVED;
92
+ out[key] = inner.value;
93
+ }
94
+ return {
95
+ resolved: true,
96
+ value: out
97
+ };
98
+ }
99
+ if (kind === "cond") {
100
+ if (directives.some((key) => !COND_KEYS.has(key))) return UNRESOLVED;
101
+ const holds = resolveVisibility(value.$cond, state, { seededOnly: true });
102
+ if (holds === void 0) return UNRESOLVED;
103
+ return resolveBinding(holds ? value.$then : value.$else, state);
104
+ }
105
+ if (directives.length !== 1) return UNRESOLVED;
106
+ const pointer = kind === "state" ? value.$state : value.$bindState;
107
+ if (kind !== "state" && kind !== "bindState" || typeof pointer !== "string") return UNRESOLVED;
108
+ return {
109
+ resolved: true,
110
+ value: readStatePointer(state, pointer)
111
+ };
112
+ }
113
+ const COND_KEYS = new Set([
114
+ "$cond",
115
+ "$then",
116
+ "$else"
117
+ ]);
118
+ /** The JSON pointer json-render reads a prop by: `/tone`. */
119
+ function pointerTo(prop) {
120
+ return prop.startsWith("/") ? prop : `/${prop}`;
121
+ }
122
+ /** A read of one prop: `{ $state: '/tone' }`. */
123
+ function stateRef(prop) {
124
+ return { $state: pointerTo(prop) };
125
+ }
126
+ /** A two-way read of one state key, for a control that writes it back: `{ $bindState: '/open' }`. */
127
+ function bindStateRef(key) {
128
+ return { $bindState: pointerTo(key) };
129
+ }
130
+ /**
131
+ * A test of one prop, in the grammar `visible` and `$cond` share: truthy, or equal to a value, and
132
+ * negated with `not`. Naming `eq` makes it a comparison even when its value is `undefined`.
133
+ */
134
+ function stateTest(test) {
135
+ return {
136
+ ...stateRef(test.prop),
137
+ ...Object.hasOwn(test, "eq") ? { eq: test.eq } : {},
138
+ ...test.not ? { not: true } : {}
139
+ };
140
+ }
141
+ /** A value chosen by a condition: `{ $cond, $then, $else }`. */
142
+ function condValue({ when, ifTrue, ifFalse }) {
143
+ return {
144
+ $cond: when,
145
+ $then: ifTrue,
146
+ $else: ifFalse
147
+ };
148
+ }
149
+ /**
150
+ * Every prop of `layer` a component prop drives, keyed by the layer prop. A declared forward wins
151
+ * where a render binding names the same layer prop.
152
+ */
153
+ function layerPropDrivers({ config, path, layer }) {
154
+ const out = {};
155
+ for (const route of [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)]) {
156
+ if (route.layer !== layer || route.into in out) continue;
157
+ out[route.into] = {
158
+ prop: route.from,
159
+ kind: "forward"
160
+ };
161
+ }
162
+ for (const route of renderBindingsOf(config, path)) {
163
+ if (route.layer !== layer || route.into in out) continue;
164
+ out[route.into] = {
165
+ prop: route.from,
166
+ kind: "render",
167
+ ...route.node !== void 0 ? { node: route.node } : {}
168
+ };
169
+ }
170
+ return out;
171
+ }
172
+ //#endregion
173
+ export { bindStateRef, bindingPath, condArms, condValue, containsMarker, layerPropDrivers, markerKind, readsState, resolveBinding, stateRef, stateTest };
@@ -191,5 +191,27 @@ declare function forwardClaims(config: Config, componentPath: string): ForwardCl
191
191
  * surface editing the layer's values has nothing to write for it.
192
192
  */
193
193
  declare function layerPropRoutes(config: Config, componentPath: string, layer: string): ReadonlyMap<string, string>;
194
+ /** How a component prop reaches a layer prop: a `forward` the config declares, or a read the render
195
+ * records (`<Pill color={color} />`). */
196
+ type BindingKind = 'forward' | 'render';
197
+ /** A component prop delivered to one prop of one layer. */
198
+ interface PropRoute {
199
+ readonly layer: string;
200
+ /** The component prop the value arrives as. */
201
+ readonly from: string;
202
+ /** The layer prop it lands on. */
203
+ readonly into: string;
204
+ /** The anatomy node the render binding sits on, for a route the render records; absent for a
205
+ * declared forward. */
206
+ readonly node?: string;
207
+ }
208
+ /**
209
+ * The layer props a render binds to a component prop by name, as routes. `<ChipButton color={color} />`
210
+ * in a render is recorded on the layer's node as `color: { $state: '/color' }`, the same relationship a
211
+ * `forward` declares, so a surface routing a layer prop back to the instance prop that delivers it
212
+ * reads both. Only a bare, single-segment `$state` naming a declared prop counts: a nested pointer or
213
+ * a conditional is a value a surface shows, not a prop it edits.
214
+ */
215
+ declare function renderBindingsOf(config: Config, componentPath: string): PropRoute[];
194
216
  //#endregion
195
- export { CodeSlot, ForwardClaims, SurfaceCompositeProp, SurfaceProp, SurfacePropCondition, SurfacePropKind, SurfaceScalarProp, SurfaceSlotProp, SurfaceStylePropertyProp, SurfaceVariantProp, codeSlotsOf, forwardClaims, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, routedProp, routedPropIn, routesContent, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
217
+ export { BindingKind, CodeSlot, ForwardClaims, PropRoute, SurfaceCompositeProp, SurfaceProp, SurfacePropCondition, SurfacePropKind, SurfaceScalarProp, SurfaceSlotProp, SurfaceStylePropertyProp, SurfaceVariantProp, codeSlotsOf, forwardClaims, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, renderBindingsOf, routedProp, routedPropIn, routesContent, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
@@ -1,7 +1,7 @@
1
1
  import { isRef, kindOf, memberOf, pathOf, refLeaf } from "./utils/refs.js";
2
2
  import { forwardsOf, rootLayerOf } from "../entities/system/element.js";
3
3
  import { propValueDomain, refListOf, resolveComponentProps, takenByForward } from "./projections.js";
4
- import { slotHostLayer } from "./slot-host.js";
4
+ import { layerOfElement, slotHostLayer } from "./slot-host.js";
5
5
  import { Component } from "../entities/system/Component.js";
6
6
  //#region src/framework/prop-surface.ts
7
7
  /**
@@ -461,7 +461,45 @@ function layerPropRoutes(config, componentPath, layer) {
461
461
  }
462
462
  const { forwarded } = computeForwardClaims(config, componentPath);
463
463
  for (const [name, claimedBy] of forwarded) if (claimedBy === layer && !routes.has(name)) routes.set(name, name);
464
+ for (const route of renderBindingsOf(config, componentPath)) if (route.layer === layer && !routes.has(route.into)) routes.set(route.into, route.from);
464
465
  return routes;
465
466
  }
467
+ /**
468
+ * The layer props a render binds to a component prop by name, as routes. `<ChipButton color={color} />`
469
+ * in a render is recorded on the layer's node as `color: { $state: '/color' }`, the same relationship a
470
+ * `forward` declares, so a surface routing a layer prop back to the instance prop that delivers it
471
+ * reads both. Only a bare, single-segment `$state` naming a declared prop counts: a nested pointer or
472
+ * a conditional is a value a surface shows, not a prop it edits.
473
+ */
474
+ function renderBindingsOf(config, componentPath) {
475
+ const body = Component.get(config, componentPath)?.toJSON();
476
+ if (!body?.anatomy) return [];
477
+ const declared = resolveComponentProps(config, componentPath);
478
+ const routes = [];
479
+ for (const [key, node] of Object.entries(body.anatomy)) {
480
+ const layer = layerOfElement(node.element);
481
+ if (layer === void 0 || !node.props) continue;
482
+ for (const [into, value] of Object.entries(node.props)) {
483
+ const from = renderBoundProp(value);
484
+ if (from !== void 0 && from in declared) routes.push({
485
+ layer,
486
+ from,
487
+ into,
488
+ node: key
489
+ });
490
+ }
491
+ }
492
+ return routes;
493
+ }
494
+ /** The prop a bare `{ $state: '/name' }` reads, or `undefined` for any other value. */
495
+ function renderBoundProp(value) {
496
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
497
+ const keys = Object.keys(value);
498
+ if (keys.length !== 1 || keys[0] !== "$state") return void 0;
499
+ const pointer = value.$state;
500
+ if (typeof pointer !== "string" || !pointer.startsWith("/")) return void 0;
501
+ const name = pointer.slice(1);
502
+ return name.length > 0 && !name.includes("/") ? name : void 0;
503
+ }
466
504
  //#endregion
467
- export { carriesContent, codeSlotsOf, forwardClaims, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, routedProp, routedPropIn, routesContent, routesIntoText, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
505
+ export { carriesContent, codeSlotsOf, forwardClaims, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, renderBindingsOf, renderBoundProp, routedProp, routedPropIn, routesContent, routesIntoText, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
@@ -67,6 +67,10 @@ interface ResolveContext {
67
67
  * left for the renderer (and where fill routing reads it); on inside a `$computed`'s arguments
68
68
  * and a repeat's source, which have to be values before the call. */
69
69
  readonly settle?: boolean;
70
+ /** Read a `$state` only when the bag holds its prop. A condition a capture keyed on a seed no prop
71
+ * state carries would otherwise read that seed as `undefined` and pick a branch the render never
72
+ * takes. */
73
+ readonly seededOnly?: boolean;
70
74
  }
71
75
  /**
72
76
  * The registry key of an element rendered THROUGH a package factory — `motion.create(Box)`, spelled
@@ -6,7 +6,7 @@ import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, refLi
6
6
  import { componentLayerClass, componentPropClasses, componentRuleClasses, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
7
7
  import { graftPreviewElements } from "../spec/preview-elements.js";
8
8
  import { slotHostLayer } from "./slot-host.js";
9
- import { forwardedLayers, intoName, isValueSlot, routesContent } from "./prop-surface.js";
9
+ import { forwardedLayers, intoName, isValueSlot, renderBindingsOf, routesContent } from "./prop-surface.js";
10
10
  //#region src/framework/render-spec.ts
11
11
  const refOf = (value) => typeof value?.__ref === "string" ? value.__ref : void 0;
12
12
  const layerOf = (element) => {
@@ -578,7 +578,10 @@ const UNREADABLE = Symbol("unreadable");
578
578
  * `$index` read the row of a repeated node; outside one they read nothing.
579
579
  */
580
580
  function readBinding(test, state, context) {
581
- if (typeof test.$state === "string") return readStatePointer(state, test.$state);
581
+ if (typeof test.$state === "string") {
582
+ if (context.seededOnly && !Object.hasOwn(state, statePointerProp(test.$state))) return UNREADABLE;
583
+ return readStatePointer(state, test.$state);
584
+ }
582
585
  if (typeof test.$item === "string") {
583
586
  if (!context.row) return UNREADABLE;
584
587
  if (test.$item === "") return context.row.item;
@@ -989,17 +992,26 @@ function valueForwardsOf(config, path) {
989
992
  * instance prop's preview value, not a style.
990
993
  */
991
994
  function propInto({ config, path, layer, into }) {
992
- return [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)].find((route) => route.layer === layer && route.into === into)?.from;
995
+ return [
996
+ ...slotTargetsOf(config, path),
997
+ ...valueForwardsOf(config, path),
998
+ ...renderBindingsOf(config, path)
999
+ ].find((route) => route.layer === layer && route.into === into)?.from;
993
1000
  }
994
1001
  /** The route a prop's content or value takes into a layer, or `undefined` for a prop that reaches
995
1002
  * no layer. */
996
1003
  function routeOf({ config, path, prop }) {
997
- return [...slotTargetsOf(config, path), ...valueForwardsOf(config, path)].find((route) => route.from === prop);
1004
+ return [
1005
+ ...slotTargetsOf(config, path),
1006
+ ...valueForwardsOf(config, path),
1007
+ ...renderBindingsOf(config, path)
1008
+ ].find((route) => route.from === prop);
998
1009
  }
999
1010
  function propsIntoLayer({ config, path, layer }) {
1000
1011
  const names = /* @__PURE__ */ new Set();
1001
1012
  for (const route of slotTargetsOf(config, path)) if (route.layer === layer) names.add(route.from);
1002
1013
  for (const route of valueForwardsOf(config, path)) if (route.layer === layer) names.add(route.from);
1014
+ for (const route of renderBindingsOf(config, path)) if (route.layer === layer) names.add(route.from);
1003
1015
  for (const name of forwardedLayers(config, path).get(layer) ?? []) names.add(name);
1004
1016
  return [...names];
1005
1017
  }
@@ -1413,4 +1425,4 @@ function expandComponent(config, path, props = {}, options = {}) {
1413
1425
  return expandSpec(config, instanceSpec(path, props), options);
1414
1426
  }
1415
1427
  //#endregion
1416
- export { ANATOMY_MARKER, anatomyElementComponent, anatomyTextProp, andListOf, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, spelled, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
1428
+ export { ANATOMY_MARKER, anatomyElementComponent, anatomyTextProp, andListOf, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, isExpression, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, spelled, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
@@ -5,6 +5,8 @@ interface AnatomyNode {
5
5
  readonly children?: readonly string[];
6
6
  readonly slots?: Record<string, string[]>;
7
7
  }
8
+ /** The layer a node's element renders as: a `layer:<name>` ref, or the layer a prop-chosen element stands in for. */
9
+ export declare function layerOfElement(element: unknown): string | undefined;
8
10
  /**
9
11
  * The layer holding a slot prop's content-position node: the nearest layer above the `Slot` node
10
12
  * named for the prop, itself included. `undefined` when the anatomy records no position for the
@@ -38,4 +38,4 @@ function slotHostLayer(anatomy, from) {
38
38
  }
39
39
  }
40
40
  //#endregion
41
- export { slotHostLayer };
41
+ export { layerOfElement, slotHostLayer };