@yahoo/uds-create-config 4.52.0 → 4.53.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 (30) hide show
  1. package/dist/capture/node-ops.js +1 -11
  2. package/dist/configs/CanvasConfig.js +5 -1
  3. package/dist/configs/system.js +4 -0
  4. package/dist/entities/canvas/node-gestures.d.ts +2 -0
  5. package/dist/entities/canvas/node-gestures.js +616 -0
  6. package/dist/{capture → entities/canvas}/node-patches.d.ts +6 -4
  7. package/dist/{capture → entities/canvas}/node-patches.js +16 -3
  8. package/dist/entities/system/Component.d.ts +35 -1
  9. package/dist/entities/system/Component.js +169 -18
  10. package/dist/entities/system/Motion.d.ts +5 -1
  11. package/dist/entities/system/Motion.js +7 -2
  12. package/dist/entities/system/component-forwards.d.ts +46 -0
  13. package/dist/entities/system/component-forwards.js +131 -0
  14. package/dist/entities/system/token-values.d.ts +9 -1
  15. package/dist/entities/system/token-values.js +4 -3
  16. package/dist/framework/Config.d.ts +1 -1
  17. package/dist/framework/Config.js +1 -1
  18. package/dist/framework/schema-version.d.ts +1 -1
  19. package/dist/framework/schema-version.js +5 -5
  20. package/dist/framework/schemas.js +14 -3
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +4 -4
  23. package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.d.ts +9 -0
  24. package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.js +10 -0
  25. package/dist/migrations/20261001131000_layer_style_and_forward_ops.d.ts +11 -0
  26. package/dist/migrations/20261001131000_layer_style_and_forward_ops.js +10 -0
  27. package/dist/spec/slot.d.ts +19 -1
  28. package/dist/spec/slot.js +49 -11
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +2 -2
@@ -1,7 +1,9 @@
1
- import { Config, Patch } from "../framework/Config.js";
2
- import { Spec } from "./capture-to-spec.js";
1
+ import { Config, Patch } from "../../framework/Config.js";
2
+ import { Spec } from "../../capture/capture-to-spec.js";
3
3
 
4
- //#region src/capture/node-patches.d.ts
4
+ //#region src/entities/canvas/node-patches.d.ts
5
+ /** The key a placed node's own element takes when its rows read back as a spec. */
6
+ declare const PLACED_NODE_KEY = "root";
5
7
  interface Placement {
6
8
  readonly page: string;
7
9
  readonly x?: number;
@@ -29,4 +31,4 @@ declare function specToNodePatches({
29
31
  readonly placement: Placement;
30
32
  }): Patch[];
31
33
  //#endregion
32
- export { specToNodePatches };
34
+ export { PLACED_NODE_KEY, specToNodePatches };
@@ -1,10 +1,23 @@
1
- import { ref } from "../framework/utils/refs.js";
2
- //#region src/capture/node-patches.ts
1
+ import { rejectData } from "../../framework/rejection.js";
2
+ import { ref } from "../../framework/utils/refs.js";
3
+ //#region src/entities/canvas/node-patches.ts
3
4
  /**
4
5
  * A spec as the node rows of one new placed node. A placed node's content is its subtree rows, one
5
6
  * per element, at `<node>/<element key>`, each pointing at its children by ref; the root element's
6
7
  * content lives on the placed node itself beside its placement.
7
8
  */
9
+ /** The system this canvas draws on, with the slug node types are qualified by. */
10
+ function attachedSystem(config) {
11
+ const [source] = config.list("system-source");
12
+ const system = source ? config.sourceFor(source.path) : void 0;
13
+ if (!source || !system) return rejectData("This canvas has no system attached, so there are no components to place on it.");
14
+ return {
15
+ slug: source.path,
16
+ system
17
+ };
18
+ }
19
+ /** The key a placed node's own element takes when its rows read back as a spec. */
20
+ const PLACED_NODE_KEY = "root";
8
21
  const INTRINSIC_TAG = /^[a-z][a-zA-Z0-9-]*$/;
9
22
  const SPEC_PRIMITIVES = new Set(["Slot", "Fragment"]);
10
23
  /** A spec element's `type` as the ref a node row stores, qualified with the system's slug. */
@@ -96,4 +109,4 @@ function specToNodePatches({ config, nodeId, spec, slug, placement }) {
96
109
  return patches;
97
110
  }
98
111
  //#endregion
99
- export { specToNodePatches };
112
+ export { PLACED_NODE_KEY, attachedSystem, elementTypeRef, postOrder, specToNodePatches };
@@ -12,9 +12,10 @@ import { TokenClass } from "./Token.js";
12
12
  import { StoredPreviewCondition } from "../../framework/preview-store.js";
13
13
  import { RepeatSpec } from "../../framework/render-spec.js";
14
14
  import { AsInput, DefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StyleRuleInput } from "./defineComponent.js";
15
- import { listSlots, namedSlots, slotHostedBy, slotOf } from "../../spec/slot.js";
15
+ import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
16
16
  import { cellAt } from "../../spec/cell.js";
17
17
  import { instanceAt } from "../../spec/instance.js";
18
+ import { ForwardOptions, forwardOptions } from "./component-forwards.js";
18
19
  import { Config } from "../../framework/Config.js";
19
20
  import { z } from "zod";
20
21
 
@@ -739,6 +740,26 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
739
740
  }, z.core.$strip>>;
740
741
  }, z.core.$strip>;
741
742
  };
743
+ readonly 'element-duplicate': {
744
+ readonly input: z.ZodObject<{
745
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
746
+ key: z.ZodString;
747
+ data: z.ZodPrefault<z.ZodObject<{
748
+ count: z.ZodOptional<z.ZodNumber>;
749
+ }, z.core.$strip>>;
750
+ }, z.core.$strip>;
751
+ };
752
+ readonly 'layer-forward': {
753
+ readonly input: z.ZodObject<{
754
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
755
+ key: z.ZodString;
756
+ data: z.ZodObject<{
757
+ props: z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodArray<z.ZodString>]>;
758
+ except: z.ZodOptional<z.ZodArray<z.ZodString>>;
759
+ replace: z.ZodOptional<z.ZodBoolean>;
760
+ }, z.core.$strip>;
761
+ }, z.core.$strip>;
762
+ };
742
763
  readonly 'element-delete': {
743
764
  readonly input: z.ZodObject<{
744
765
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
@@ -768,6 +789,17 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
768
789
  }, z.core.$strip>;
769
790
  };
770
791
  }, {
792
+ /**
793
+ * What each layer can forward, for the layers that render a component with props: every prop its
794
+ * element exposes, whether the layer forwards it now, and why one is blocked (the component
795
+ * declares it, or a sibling layer forwards it). Read before writing `forwards`, so "everything
796
+ * but icon" is a list the caller can spell.
797
+ */
798
+ readonly forwardable: ({
799
+ config,
800
+ path,
801
+ member
802
+ }: ComputedInput) => Record<string, ForwardOptions>;
771
803
  /**
772
804
  * The prop names an instance may fill through a spec element's `slots` field — every prop that
773
805
  * routes content: a `slot`, or a `forward` whose far end takes content. Resolved over the
@@ -874,6 +906,8 @@ interface ComponentClass extends Omit<typeof ComponentEntity, 'ref' | 'item'> {
874
906
  slot: typeof slotOf;
875
907
  slotHostedBy: typeof slotHostedBy;
876
908
  namedSlots: typeof namedSlots;
909
+ slotsFor: typeof slotsFor;
910
+ forwardOptions: typeof forwardOptions;
877
911
  cell: typeof cellAt;
878
912
  instance: typeof instanceAt;
879
913
  }
@@ -16,9 +16,10 @@ import { storedPreview } from "../../framework/preview-store.js";
16
16
  import { glyphPropsOf, propOwner, propValueDomain, refListOf, resolveComponentProps, stylePropValueLeaves, validateComponentProps } from "../../framework/projections.js";
17
17
  import { carriesContent, intoName, isValueSlot, renderBoundProp, routedPropIn, routesContent } from "../../framework/prop-surface.js";
18
18
  import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../framework/render-spec.js";
19
- import { listSlots, namedSlots, slotHostedBy, slotOf } from "../../spec/slot.js";
19
+ import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
20
20
  import { cellAt } from "../../spec/cell.js";
21
21
  import { instanceAt } from "../../spec/instance.js";
22
+ import { forwardOptions, forwardsWrite } from "./component-forwards.js";
22
23
  import { File } from "./File.js";
23
24
  import { forwardRouteInvariants, refusedRoutes } from "./forward-route-invariants.js";
24
25
  import { iconDomainInvariants } from "./icon-domain-invariants.js";
@@ -275,6 +276,11 @@ const ForwardDefinition = defineSubEntity({
275
276
  input: entry,
276
277
  message: `"${name}" is not a prop of layer "${key}"'s element (${pathOf(element.__ref)}).`
277
278
  });
279
+ if (name && body.props?.[name] != null) ctx.issues.push({
280
+ code: "custom",
281
+ input: entry,
282
+ message: `"${name}" is declared by the component itself, which wins over a forward.`
283
+ });
278
284
  }
279
285
  const mine = forwardedNames(config, element, ctx.value);
280
286
  if (!mine) return;
@@ -1671,24 +1677,45 @@ const ComponentEntity = defineEntity({
1671
1677
  anatomy: AnatomyNode,
1672
1678
  defaultProps: Style
1673
1679
  },
1674
- computed: {
1675
- /**
1676
- * The prop names an instance may fill through a spec element's `slots` field — every prop that
1677
- * routes content: a `slot`, or a `forward` whose far end takes content. Resolved over the
1678
- * inheritance chain, since an inherited slot is as fillable as a declared one. This is the
1679
- * `slots: string[]` json-render's catalog metadata declares per component, computed here so the
1680
- * emitted catalog, the validator, and every picker read one answer.
1681
- */
1682
- slots: ({ config, path, member }) => {
1683
- const layers = member.layers;
1684
- const names = [];
1685
- for (const [name, decl] of Object.entries(resolveComponentProps(config, path))) {
1686
- const body = decl;
1687
- if (!body) continue;
1688
- if (carriesContent(config, layers, body)) names.push(name);
1680
+ computed: {
1681
+ /**
1682
+ * What each layer can forward, for the layers that render a component with props: every prop its
1683
+ * element exposes, whether the layer forwards it now, and why one is blocked (the component
1684
+ * declares it, or a sibling layer forwards it). Read before writing `forwards`, so "everything
1685
+ * but icon" is a list the caller can spell.
1686
+ */
1687
+ forwardable: ({ config, path, member }) => {
1688
+ const out = {};
1689
+ const body = member;
1690
+ for (const layer of Object.keys(body.layers ?? {})) {
1691
+ const options = forwardOptions({
1692
+ config,
1693
+ path,
1694
+ layer,
1695
+ member: body
1696
+ });
1697
+ if (options) out[layer] = options;
1698
+ }
1699
+ return out;
1700
+ },
1701
+ /**
1702
+ * The prop names an instance may fill through a spec element's `slots` field — every prop that
1703
+ * routes content: a `slot`, or a `forward` whose far end takes content. Resolved over the
1704
+ * inheritance chain, since an inherited slot is as fillable as a declared one. This is the
1705
+ * `slots: string[]` json-render's catalog metadata declares per component, computed here so the
1706
+ * emitted catalog, the validator, and every picker read one answer.
1707
+ */
1708
+ slots: ({ config, path, member }) => {
1709
+ const layers = member.layers;
1710
+ const names = [];
1711
+ for (const [name, decl] of Object.entries(resolveComponentProps(config, path))) {
1712
+ const body = decl;
1713
+ if (!body) continue;
1714
+ if (carriesContent(config, layers, body)) names.push(name);
1715
+ }
1716
+ return names;
1689
1717
  }
1690
- return names;
1691
- } },
1718
+ },
1692
1719
  nameEdges: bagNameEdges(componentBags),
1693
1720
  styleBags: componentBags,
1694
1721
  rewriteName: bagRewriteName(componentBags),
@@ -1956,6 +1983,126 @@ slots: ({ config, path, member }) => {
1956
1983
  }
1957
1984
  },
1958
1985
  /**
1986
+ * Copy an element of a composition, with everything beneath it, right after it: each copy gets
1987
+ * layers of its own, and every style rule that styles a copied layer styles its copy the same.
1988
+ * One op for the reason `element-insert` is one: the layers, the nodes, the parent's edge and the
1989
+ * rule bags are one gesture, and any of them alone is a copy that renders wrong.
1990
+ */
1991
+ "element-duplicate": {
1992
+ input: z.object({
1993
+ path: pathInput("component"),
1994
+ key: z.string(),
1995
+ data: z.object({
1996
+ /** How many copies, each after the last. */
1997
+ count: z.number().int().min(1).max(50).optional() }).prefault({})
1998
+ }),
1999
+ readOnly: false,
2000
+ label: "Duplicate element",
2001
+ description: "Copy an element of a component, with its subtree and its styles, right after it: each copy gets its own layers, and every style rule styling a copied layer styles the copy the same.",
2002
+ scope: "item",
2003
+ creates: true,
2004
+ title: (entity) => `Duplicate ${entity} element`,
2005
+ handler: (input, config) => {
2006
+ const path = String(input.path);
2007
+ const source = String(input.key);
2008
+ const count = input.data?.count ?? 1;
2009
+ const body = config.resolve("component", path)?.toJSON();
2010
+ const anatomy = body?.anatomy ?? {};
2011
+ if (!anatomy[source]) return rejectData(`"${source}" is not an element of ${path}.`, ["key"]);
2012
+ const parentEntry = Object.entries(anatomy).find(([, node]) => [...node.children ?? [], ...Object.values(node.slots ?? {}).flat()].includes(source));
2013
+ if (!parentEntry) return rejectData(`"${source}" is the root of ${path}; it has no place to copy into.`, ["key"]);
2014
+ const [parent, parentNode] = parentEntry;
2015
+ const slotProp = Object.entries(parentNode.slots ?? {}).find(([, fills]) => fills.includes(source))?.[0];
2016
+ const subtree = [];
2017
+ const visit = (at) => {
2018
+ subtree.push(at);
2019
+ const node = anatomy[at];
2020
+ for (const kid of [...node?.children ?? [], ...Object.values(node?.slots ?? {}).flat()]) visit(kid);
2021
+ };
2022
+ visit(source);
2023
+ const elements = Object.fromEntries(subtree.map((at) => {
2024
+ const node = anatomy[at] ?? {};
2025
+ return [at, {
2026
+ element: body?.layers?.[at] ?? "div",
2027
+ ...node.props ? { props: node.props } : {},
2028
+ ...node.children?.length ? { children: node.children } : {},
2029
+ ...node.slots ? { slots: node.slots } : {},
2030
+ ...node.visible !== void 0 ? { visible: node.visible } : {},
2031
+ ...node.text !== void 0 ? { text: node.text } : {}
2032
+ }];
2033
+ }));
2034
+ let next = config;
2035
+ let previous = source;
2036
+ for (let n = 0; n < count; n++) {
2037
+ const key = freeLayerKey(next, path, source);
2038
+ const siblings = (next.resolve("component", path)?.toJSON())?.anatomy?.[parent]?.children ?? [];
2039
+ const index = siblings.indexOf(previous) + 1;
2040
+ next = next.run([{
2041
+ kind: "component",
2042
+ operation: "element-insert",
2043
+ path,
2044
+ key,
2045
+ data: {
2046
+ parent,
2047
+ ...slotProp ? { slotProp } : { index: index > 0 ? index : siblings.length },
2048
+ subtree: {
2049
+ root: source,
2050
+ elements
2051
+ }
2052
+ }
2053
+ }]);
2054
+ const writes = {};
2055
+ for (const [rule, entry] of Object.entries(body?.styles ?? {})) for (const at of subtree) {
2056
+ const bag = entry.layers?.[at];
2057
+ if (bag === void 0) continue;
2058
+ const copy = at === source ? key : graftKey(key, at);
2059
+ writes[`styles.${rule}.layers.${copy}`] = bag;
2060
+ }
2061
+ if (Object.keys(writes).length > 0) next = next.apply({
2062
+ kind: "component",
2063
+ operation: "update",
2064
+ path,
2065
+ data: writes
2066
+ });
2067
+ previous = key;
2068
+ }
2069
+ return next;
2070
+ }
2071
+ },
2072
+ /**
2073
+ * Make a layer forward the props a request names. The stored write is the generic `forwards`
2074
+ * `sub-upsert` or `sub-delete`; this op works out which, from what the layer forwards now.
2075
+ */
2076
+ "layer-forward": {
2077
+ input: z.object({
2078
+ path: pathInput("component"),
2079
+ /** The layer whose element's props are forwarded. */
2080
+ key: z.string(),
2081
+ data: z.object({
2082
+ /** Every prop the layer can forward, or the ones to add to what it forwards now. */
2083
+ props: z.union([z.literal("*"), z.array(z.string().min(1))]),
2084
+ /** Props to leave out of either. */
2085
+ except: z.array(z.string().min(1)).optional(),
2086
+ /** Forward exactly `props`, dropping what the layer forwards now. */
2087
+ replace: z.boolean().optional()
2088
+ })
2089
+ }),
2090
+ readOnly: false,
2091
+ label: "Forward layer props",
2092
+ description: "Expose a layer's props at the component's top level (\"forward everything from the root but icon\", \"also forward width\"): `props` is `\"*\"` for every prop the layer's element takes, or a list added to what the layer forwards now, or that replaces it with `replace`; `except` leaves props out of either. A prop the element lacks, one the component declares itself, and one a sibling layer already forwards are refused. Read `component/get` for what a layer forwards now.",
2093
+ scope: "item",
2094
+ title: (entity) => `Forward ${entity} layer props`,
2095
+ handler: (input, config) => {
2096
+ const patch = forwardsWrite({
2097
+ config,
2098
+ path: String(input.path),
2099
+ layer: String(input.key),
2100
+ request: input.data
2101
+ });
2102
+ return patch ? config.apply(patch) : config;
2103
+ }
2104
+ },
2105
+ /**
1959
2106
  * Remove an element from a composition — the node, its layer, and everything that named it.
1960
2107
  *
1961
2108
  * A custom op for the same reason `element-insert` is: it writes more than one thing and either
@@ -2271,6 +2418,10 @@ const Component = Object.assign(ComponentEntity, {
2271
2418
  slotHostedBy,
2272
2419
  /** The named node slots alone, without a positioned `children`. */
2273
2420
  namedSlots,
2421
+ /** Every component slot whose `accepts` names an element. */
2422
+ slotsFor,
2423
+ /** What one layer can forward, and why a prop is blocked. */
2424
+ forwardOptions,
2274
2425
  /** The component under one coordinate of its matrix. */
2275
2426
  cell: cellAt,
2276
2427
  /** The component a design file's spec element places, with what the file put in its slots. */
@@ -3,6 +3,9 @@ import { Named } from "../../framework/named.js";
3
3
  import { z } from "zod";
4
4
 
5
5
  //#region src/entities/system/Motion.d.ts
6
+ /** The fields of a JS preset that `motion.create` reads as props, in schema order. The emitter
7
+ * reads this rather than restating the schema, so a field added here reaches every emitted preset. */
8
+ declare const jsMotionPropFields: readonly string[];
6
9
  declare const MotionDefinition: EntityClass<z.ZodDiscriminatedUnion<[z.ZodObject<{
7
10
  runtime: z.ZodLiteral<"css">;
8
11
  keyframes: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -16,6 +19,7 @@ declare const MotionDefinition: EntityClass<z.ZodDiscriminatedUnion<[z.ZodObject
16
19
  runtime: z.ZodLiteral<"js">;
17
20
  initial: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
18
21
  animate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
22
+ exit: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
19
23
  transition: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
20
24
  whileHover: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
21
25
  whileTap: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -28,4 +32,4 @@ declare const MotionDefinition: EntityClass<z.ZodDiscriminatedUnion<[z.ZodObject
28
32
  interface MotionClass extends Named<typeof MotionDefinition> {}
29
33
  declare const Motion: MotionClass;
30
34
  //#endregion
31
- export { Motion, MotionClass };
35
+ export { Motion, MotionClass, jsMotionPropFields };
@@ -6,7 +6,8 @@ import { z } from "zod";
6
6
  * coupled to how it animates, not loosely optional:
7
7
  * - `css` → `keyframes` REQUIRED (codegen emits `@keyframes` from them); a css preset with
8
8
  * no keyframes — which used to validate and silently emit nothing — is now rejected.
9
- * - `js` → no keyframes; carries the interaction state the client drives.
9
+ * - `js` → no keyframes; carries the interaction state the client drives, `exit` included,
10
+ * which runs under an `AnimatePresence` ancestor when the element unmounts.
10
11
  * Illegal cross-runtime mixes (a css preset with `animate`, a js preset with `keyframes`)
11
12
  * can't be represented — each member is strict.
12
13
  *
@@ -30,6 +31,7 @@ const JsMotion = z.strictObject({
30
31
  runtime: z.literal("js").describe(RUNTIME),
31
32
  initial: gesture.describe("The starting values, as `motion/react` reads them."),
32
33
  animate: gesture.describe("The values to animate to."),
34
+ exit: gesture.describe("The values to animate to as the element leaves."),
33
35
  transition: gesture.describe("How the animation moves, such as a spring."),
34
36
  whileHover: gesture.describe("Values held while the element is hovered."),
35
37
  whileTap: gesture.describe("Values held while the element is pressed."),
@@ -39,6 +41,9 @@ const JsMotion = z.strictObject({
39
41
  dragConstraints: z.record(z.string(), z.number()).optional().describe("The box a drag stays within."),
40
42
  dragElastic: z.number().optional().describe("How far past its constraints a drag can stretch.")
41
43
  });
44
+ /** The fields of a JS preset that `motion.create` reads as props, in schema order. The emitter
45
+ * reads this rather than restating the schema, so a field added here reaches every emitted preset. */
46
+ const jsMotionPropFields = Object.keys(JsMotion.shape).filter((field) => field !== "runtime");
42
47
  const Motion = defineEntity({
43
48
  kind: "motion",
44
49
  label: "Motion",
@@ -46,4 +51,4 @@ const Motion = defineEntity({
46
51
  fields: z.discriminatedUnion("runtime", [CssMotion, JsMotion])
47
52
  });
48
53
  //#endregion
49
- export { Motion };
54
+ export { Motion, jsMotionPropFields };
@@ -0,0 +1,46 @@
1
+ import { ComponentBody } from "./Component.js";
2
+ import { Config } from "../../framework/Config.js";
3
+
4
+ //#region src/entities/system/component-forwards.d.ts
5
+ /** Why a prop can't be forwarded from a layer: the component declares it itself, which always wins
6
+ * over a forward, or a sibling layer's entry already delivers it. */
7
+ type ForwardBlockedReason = {
8
+ readonly by: 'own';
9
+ } | {
10
+ readonly by: 'layer';
11
+ readonly layer: string;
12
+ };
13
+ /** One prop a layer's element exposes, as a forward from that layer sees it. */
14
+ interface ForwardOption {
15
+ readonly name: string;
16
+ /** Whether the layer's `forwards` entry covers it now. */
17
+ readonly forwarded: boolean;
18
+ readonly blocked?: ForwardBlockedReason;
19
+ }
20
+ /** What one layer of a component can forward, and what it forwards now. */
21
+ interface ForwardOptions {
22
+ /** Every prop the layer's element exposes, in name order. */
23
+ readonly options: readonly ForwardOption[];
24
+ /** Whether the layer has a `forwards` entry at all. */
25
+ readonly forwarded: boolean;
26
+ }
27
+ /**
28
+ * What `layer` of the component at `path` can forward. An option the component declares itself, or a
29
+ * sibling layer already delivers, is listed with the reason it's blocked rather than left out. Null
30
+ * when the layer renders a tag, or its element declares no props.
31
+ */
32
+ declare function forwardOptions(args: {
33
+ config: Config;
34
+ path: string;
35
+ layer: string;
36
+ member?: ComponentBody;
37
+ }): ForwardOptions | null;
38
+ /** What a forward asks for: every prop, or a list that joins what the layer forwards now unless it
39
+ * replaces it, less `except`. */
40
+ interface ForwardRequest {
41
+ readonly props: '*' | readonly string[];
42
+ readonly except?: readonly string[];
43
+ readonly replace?: boolean;
44
+ }
45
+ //#endregion
46
+ export { ForwardBlockedReason, ForwardOption, ForwardOptions, ForwardRequest, forwardOptions };
@@ -0,0 +1,131 @@
1
+ import { reject } from "../../framework/rejection.js";
2
+ import { isRef, kindOf, memberRef, pathOf } from "../../framework/utils/refs.js";
3
+ import { forwardedPropName } from "./element.js";
4
+ import { propOwner, resolveComponentProps } from "../../framework/projections.js";
5
+ import { forwardClaims } from "../../framework/prop-surface.js";
6
+ //#region src/entities/system/component-forwards.ts
7
+ function siteOf({ config, path, layer, member }) {
8
+ const body = member ?? config.resolve("component", path)?.toJSON();
9
+ const element = body?.layers?.[layer];
10
+ if (!body || !isRef(element) || kindOf(element) !== "component") return null;
11
+ const elementPath = pathOf(element);
12
+ const exposes = Object.keys(resolveComponentProps(config, elementPath));
13
+ if (exposes.length === 0) return null;
14
+ const own = body.forwards?.[layer];
15
+ let stored;
16
+ if (own === "*") stored = exposes;
17
+ else if (Array.isArray(own)) stored = own.map(forwardedPropName).filter((name) => !!name);
18
+ else stored = [];
19
+ const claims = forwardClaims(config, path);
20
+ return {
21
+ body,
22
+ elementPath,
23
+ options: [...exposes].sort((a, b) => a.localeCompare(b)).map((name) => {
24
+ let blocked;
25
+ const claimer = claims.forwarded.get(name);
26
+ if (claims.own.has(name)) blocked = { by: "own" };
27
+ else if (claimer !== void 0 && claimer !== layer) blocked = {
28
+ by: "layer",
29
+ layer: claimer
30
+ };
31
+ return {
32
+ name,
33
+ forwarded: stored.includes(name),
34
+ ...blocked ? { blocked } : {}
35
+ };
36
+ }),
37
+ forwarded: own !== void 0
38
+ };
39
+ }
40
+ /**
41
+ * What `layer` of the component at `path` can forward. An option the component declares itself, or a
42
+ * sibling layer already delivers, is listed with the reason it's blocked rather than left out. Null
43
+ * when the layer renders a tag, or its element declares no props.
44
+ */
45
+ function forwardOptions(args) {
46
+ const site = siteOf(args);
47
+ return site && {
48
+ options: site.options,
49
+ forwarded: site.forwarded
50
+ };
51
+ }
52
+ /** The names a forward ends up exposing: `'*'` is every unblocked prop, and a list joins what the
53
+ * layer forwards now unless it replaces it. `except` comes off either. */
54
+ function selectionOf({ site, data }) {
55
+ const names = new Set(site.options.map((option) => option.name));
56
+ const asked = data.props === "*" ? [] : data.props;
57
+ for (const name of [...asked, ...data.except ?? []]) if (!names.has(name)) reject("NOT_FOUND", `"${name}" is not a prop of the layer's element (${[...names].join(", ")}).`);
58
+ for (const name of asked) {
59
+ const blocked = site.options.find((option) => option.name === name)?.blocked;
60
+ if (!blocked) continue;
61
+ reject("ALREADY_EXISTS", blocked.by === "own" ? `"${name}" is declared by the component itself, which wins over a forward.` : `"${name}" is already forwarded from the "${blocked.layer}" layer.`);
62
+ }
63
+ const selectable = site.options.filter((option) => !option.blocked);
64
+ const kept = /* @__PURE__ */ new Set();
65
+ if (data.props === "*") for (const option of selectable) kept.add(option.name);
66
+ else {
67
+ if (!data.replace) {
68
+ for (const option of selectable) if (option.forwarded) kept.add(option.name);
69
+ }
70
+ for (const name of asked) kept.add(name);
71
+ }
72
+ for (const name of data.except ?? []) kept.delete(name);
73
+ return selectable.map((option) => option.name).filter((name) => kept.has(name));
74
+ }
75
+ /**
76
+ * The `forwards` write that makes `next` the layer's forwarded set, or null when it already is. An
77
+ * empty set deletes the entry, the full surface is `'*'` when no sibling layer has an entry of its
78
+ * own, and anything else is a list of member refs.
79
+ */
80
+ function forwardPatch({ config, path, layer, site, next }) {
81
+ const current = site.options.filter((option) => option.forwarded && !option.blocked).map((option) => option.name);
82
+ if (next.length === current.length && next.every((name) => current.includes(name))) return null;
83
+ const at = {
84
+ kind: "component",
85
+ path,
86
+ field: "forwards",
87
+ key: layer
88
+ };
89
+ if (next.length === 0) return site.forwarded ? {
90
+ ...at,
91
+ operation: "sub-delete"
92
+ } : null;
93
+ const hasSiblings = Object.keys(site.body.forwards ?? {}).some((sibling) => sibling !== layer);
94
+ const unowned = site.options.filter((option) => option.blocked?.by !== "own");
95
+ const data = !hasSiblings && unowned.length > 0 && unowned.every((option) => next.includes(option.name)) ? "*" : next.map((name) => memberRef("component", propOwner(config, site.elementPath, name) ?? site.elementPath, "props", name));
96
+ return {
97
+ ...at,
98
+ operation: "sub-upsert",
99
+ data
100
+ };
101
+ }
102
+ /**
103
+ * The generic `forwards` write that makes `layer` forward what `request` asks for: a `sub-upsert`
104
+ * with the refs minted (compacted to `'*'` where no sibling entry could collide), a `sub-delete`
105
+ * when nothing is left, or null when the layer already forwards exactly that. Refuses a prop the
106
+ * layer's element lacks, one the component declares itself or a sibling layer forwards, and a layer
107
+ * with nothing to forward.
108
+ */
109
+ function forwardsWrite({ config, path, layer, request }) {
110
+ const body = config.resolve("component", path)?.toJSON();
111
+ if (!body) reject("NOT_FOUND", `No component at "${path}".`);
112
+ if (!(layer in (body.layers ?? {}))) reject("NOT_FOUND", `"${layer}" is not one of this component's layers (${Object.keys(body.layers ?? {}).join(", ")}).`);
113
+ const site = siteOf({
114
+ config,
115
+ path,
116
+ layer
117
+ });
118
+ if (!site) reject("UNSUPPORTED", `The "${layer}" layer doesn't render a component with props, so it has nothing to forward.`);
119
+ return forwardPatch({
120
+ config,
121
+ path,
122
+ layer,
123
+ site,
124
+ next: selectionOf({
125
+ site,
126
+ data: request
127
+ })
128
+ });
129
+ }
130
+ //#endregion
131
+ export { forwardOptions, forwardsWrite };
@@ -13,6 +13,14 @@ interface TokenValueEntry {
13
13
  readonly px?: number;
14
14
  readonly ms?: number;
15
15
  }
16
+ /** A resolved token value as its CSS string and the numbers a JS caller reads off it. */
17
+ declare function parseTokenValue({
18
+ value,
19
+ rem
20
+ }: {
21
+ value: string | number; /** The pixels one `rem` is on the page, or `undefined` when the root is fluid and fixes none. */
22
+ rem: number | undefined;
23
+ }): TokenValueEntry;
16
24
  /** What a token resolved to, or why it did not. */
17
25
  type TokenValueState = /** One value, the same under every mode and state. */{
18
26
  readonly kind: 'value';
@@ -144,4 +152,4 @@ declare function spellTokenRead({
144
152
  reader: TokenReader;
145
153
  }): string;
146
154
  //#endregion
147
- export { RegisteredTokenValues, TOKEN_READERS, TokenGroupReader, TokenReader, TokenReaders, TokenValueEntry, TokenValueRow, TokenValueState, isTokenReader, rootFontSize, spellTokenRead, tokenExists, tokenValueRow, tokenValueRows };
155
+ export { RegisteredTokenValues, TOKEN_READERS, TokenGroupReader, TokenReader, TokenReaders, TokenValueEntry, TokenValueRow, TokenValueState, isTokenReader, parseTokenValue, rootFontSize, spellTokenRead, tokenExists, tokenValueRow, tokenValueRows };
@@ -19,7 +19,8 @@ import { cssVar, resolveTokenValue } from "../../framework/projections.js";
19
19
  * whatever the viewer's mode or state says on the page, so it is only ever reachable as its `var()`.
20
20
  */
21
21
  const NUMERIC = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem|ms|s)?$/;
22
- function tokenValueEntry({ value, rem }) {
22
+ /** A resolved token value as its CSS string and the numbers a JS caller reads off it. */
23
+ function parseTokenValue({ value, rem }) {
23
24
  if (typeof value === "number") return {
24
25
  css: String(value),
25
26
  px: value
@@ -93,7 +94,7 @@ function stateOf({ config, path, rem }) {
93
94
  if (resolved === void 0) return { kind: "unresolved" };
94
95
  return {
95
96
  kind: "value",
96
- value: tokenValueEntry({
97
+ value: parseTokenValue({
97
98
  value: resolved,
98
99
  rem
99
100
  })
@@ -158,4 +159,4 @@ function spellTokenRead({ path, reader }) {
158
159
  return `Token.ref(${JSON.stringify(path)}).${reader}()`;
159
160
  }
160
161
  //#endregion
161
- export { TOKEN_READERS, isTokenReader, rootFontSize, spellTokenRead, tokenExists, tokenValueRow, tokenValueRows };
162
+ export { TOKEN_READERS, isTokenReader, parseTokenValue, rootFontSize, spellTokenRead, tokenExists, tokenValueRow, tokenValueRows };
@@ -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 = 20261001112308;
75
+ declare const SERIALIZED_CONFIG_VERSION = 20261001131000;
76
76
  interface SerializedConfig {
77
77
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
78
78
  readonly version: number;
@@ -5250,7 +5250,7 @@ var Config = class Config {
5250
5250
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5251
5251
  hydrateFrom(stored, options) {
5252
5252
  const claimed = schemaVersionOf(stored);
5253
- if (claimed > 20261001112308) throw new SchemaVersionTooNew(claimed);
5253
+ if (claimed > 20261001131e3) throw new SchemaVersionTooNew(claimed);
5254
5254
  const wire = upgradeSerializedConfig(stored);
5255
5255
  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\`.`);
5256
5256
  const leftover = wire.options;
@@ -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 = 20261001112308;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261001131000;
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 {