@yahoo/uds-create-config 4.51.3 → 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 (36) 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 +6 -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 +47 -32
  9. package/dist/entities/system/Component.js +181 -30
  10. package/dist/entities/system/LinkedSystem.d.ts +10 -3
  11. package/dist/entities/system/Motion.d.ts +5 -1
  12. package/dist/entities/system/Motion.js +7 -2
  13. package/dist/entities/system/component-forwards.d.ts +46 -0
  14. package/dist/entities/system/component-forwards.js +131 -0
  15. package/dist/entities/system/token-values.d.ts +9 -1
  16. package/dist/entities/system/token-values.js +4 -3
  17. package/dist/framework/Config.d.ts +1 -1
  18. package/dist/framework/Config.js +1 -1
  19. package/dist/framework/defineSubEntity.d.ts +10 -1
  20. package/dist/framework/defineSubEntity.js +19 -1
  21. package/dist/framework/rejection.js +16 -4
  22. package/dist/framework/schema-version.d.ts +1 -1
  23. package/dist/framework/schema-version.js +5 -5
  24. package/dist/framework/schemas.js +14 -3
  25. package/dist/index.d.ts +7 -6
  26. package/dist/index.js +5 -5
  27. package/dist/migrations/20261001112308_style_rules_as_list.d.ts +12 -0
  28. package/dist/migrations/20261001112308_style_rules_as_list.js +8 -0
  29. package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.d.ts +9 -0
  30. package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.js +10 -0
  31. package/dist/migrations/20261001131000_layer_style_and_forward_ops.d.ts +11 -0
  32. package/dist/migrations/20261001131000_layer_style_and_forward_ops.js +10 -0
  33. package/dist/spec/slot.d.ts +19 -1
  34. package/dist/spec/slot.js +49 -11
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
@@ -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
 
@@ -320,30 +321,6 @@ interface ComponentStyleBody {
320
321
  * exposes), which no single static type can state — so the bag stays open here. */
321
322
  layers?: Record<string, Record<string, unknown> | undefined>;
322
323
  }
323
- declare const StyleRuleDefinition: SubEntityClass<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>;
324
- interface StyleRuleClass extends Named<typeof StyleRuleDefinition> {}
325
- /**
326
- * The props a cell previews with, keyed by the condition they hold under the way a style rule is:
327
- * `*` for every cell, a condition on the component's own props for some. `when` names those props,
328
- * so a prop rename moves it and a value rename moves the ref it stores; `props` is keyed by prop
329
- * too, which `Prop.keyedByNameIn` and `VariantValue.namedValuesIn` reach through `preview/*\/props`.
330
- * Deleting a prop or a value drops the entries conditioned on it.
331
- */
332
- declare const PreviewDefinition: SubEntityClass<z.ZodType<StoredPreviewCondition, unknown, z.core.$ZodTypeInternals<StoredPreviewCondition, unknown>>>;
333
- interface PreviewClass extends Named<typeof PreviewDefinition> {}
334
- /**
335
- * One node of an inserted subtree, in the shape the anatomy already stores.
336
- *
337
- * An insert takes a whole subtree because inserting one is a single user action: "restore this slot's
338
- * default" puts back a stack holding a text, and splitting that into one row per node would make one
339
- * gesture several rows in the changes panel and several things to undo. It is the same reason the op
340
- * already writes the layer and the node together.
341
- *
342
- * Flat, keyed, with `children` as an edge list of keys — because that is what anatomy is, and what a
343
- * json-render spec is. A nested form would need converting at both ends: the caller holds a flat spec
344
- * subtree, and the store keeps a flat edge list. Every descendant gets its own layer, for the reason
345
- * the root does: a thing a person added is added in order to be styled, and `styles` keys on layers.
346
- */
347
324
  declare const visibilityDefinition: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodPipe<z.ZodObject<{
348
325
  not: z.ZodOptional<z.ZodLiteral<true>>;
349
326
  eq: z.ZodOptional<z.ZodUnknown>;
@@ -669,11 +646,11 @@ declare const componentFields: z.ZodObject<{
669
646
  layers: LayerClass;
670
647
  props: z.ZodOptional<PropClass>;
671
648
  forwards: z.ZodOptional<ForwardClass>;
672
- styles: z.ZodOptional<StyleRuleClass>;
649
+ styles: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>, z.ZodTransform<Record<string, ComponentStyleBody>, ComponentStyleBody[]>>, SubEntityClass<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>]>>;
673
650
  renderProps: z.ZodOptional<z.ZodArray<z.ZodString>>;
674
651
  anatomy: z.ZodOptional<AnatomyNodeClass>;
675
652
  defaultProps: z.ZodOptional<StyleClass>;
676
- preview: z.ZodOptional<PreviewClass>;
653
+ preview: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodType<StoredPreviewCondition, unknown, z.core.$ZodTypeInternals<StoredPreviewCondition, unknown>>>, z.ZodTransform<Record<string, StoredPreviewCondition>, StoredPreviewCondition[]>>, SubEntityClass<z.ZodType<StoredPreviewCondition, unknown, z.core.$ZodTypeInternals<StoredPreviewCondition, unknown>>>]>>;
677
654
  file: z.ZodOptional<z.ZodObject<{
678
655
  __ref: z.ZodString;
679
656
  }, z.core.$strip>>;
@@ -703,11 +680,11 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
703
680
  layers: LayerClass;
704
681
  props: z.ZodOptional<PropClass>;
705
682
  forwards: z.ZodOptional<ForwardClass>;
706
- styles: z.ZodOptional<StyleRuleClass>;
683
+ styles: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>, z.ZodTransform<Record<string, ComponentStyleBody>, ComponentStyleBody[]>>, SubEntityClass<z.ZodType<ComponentStyleBody, unknown, z.core.$ZodTypeInternals<ComponentStyleBody, unknown>>>]>>;
707
684
  renderProps: z.ZodOptional<z.ZodArray<z.ZodString>>;
708
685
  anatomy: z.ZodOptional<AnatomyNodeClass>;
709
686
  defaultProps: z.ZodOptional<StyleClass>;
710
- preview: z.ZodOptional<PreviewClass>;
687
+ preview: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodType<StoredPreviewCondition, unknown, z.core.$ZodTypeInternals<StoredPreviewCondition, unknown>>>, z.ZodTransform<Record<string, StoredPreviewCondition>, StoredPreviewCondition[]>>, SubEntityClass<z.ZodType<StoredPreviewCondition, unknown, z.core.$ZodTypeInternals<StoredPreviewCondition, unknown>>>]>>;
711
688
  file: z.ZodOptional<z.ZodObject<{
712
689
  __ref: z.ZodString;
713
690
  }, z.core.$strip>>;
@@ -763,6 +740,26 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
763
740
  }, z.core.$strip>>;
764
741
  }, z.core.$strip>;
765
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
+ };
766
763
  readonly 'element-delete': {
767
764
  readonly input: z.ZodObject<{
768
765
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
@@ -792,6 +789,17 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
792
789
  }, z.core.$strip>;
793
790
  };
794
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>;
795
803
  /**
796
804
  * The prop names an instance may fill through a spec element's `slots` field — every prop that
797
805
  * routes content: a `slot`, or a `forward` whose far end takes content. Resolved over the
@@ -872,9 +880,14 @@ type ExtensionDefaults<Contract, Delta> = {
872
880
  type ComponentCreateInput = CreateInput<typeof componentFields>;
873
881
  /** `Component.item`'s body: the stored fields, with `styles` also taking the rule list
874
882
  * `defineComponent.styles` takes, checked against this item's own layers. */
875
- type ComponentItemInput<L extends LayerMap> = Omit<ComponentCreateInput, 'styles'> & {
883
+ type ComponentItemInput<L extends LayerMap> = Omit<ComponentRecordInput, 'styles'> & {
876
884
  readonly layers: L;
877
- readonly styles?: ComponentCreateInput['styles'] | readonly StyleRuleInput<L>[];
885
+ readonly styles?: ComponentRecordInput['styles'] | readonly StyleRuleInput<L>[];
886
+ };
887
+ /** A component body with its `styles` as the keyed record, so a rule list is checked by
888
+ * {@link ComponentItemInput} against the item's own layers rather than taken unchecked. */
889
+ type ComponentRecordInput = Omit<ComponentCreateInput, 'styles'> & {
890
+ readonly styles?: Exclude<ComponentCreateInput['styles'], readonly unknown[]>;
878
891
  };
879
892
  /**
880
893
  * Component keeps the generic entity surface, plus the questions only a component answers: its
@@ -887,12 +900,14 @@ interface ComponentClass extends Omit<typeof ComponentEntity, 'ref' | 'item'> {
887
900
  /** One component. `styles` takes the stored record or the rule list `defineComponent.styles`
888
901
  * takes, and a list is stored as the record. */
889
902
  item<const L extends LayerMap>(def: ComponentItemInput<L>): ComponentCreateInput;
890
- item(def: ComponentCreateInput | string | number | Ref$1): ComponentCreateInput | string | number | Ref$1;
903
+ item(def: ComponentRecordInput | string | number | Ref$1): ComponentCreateInput | string | number | Ref$1;
891
904
  ref<const Path extends PathOf<'component'>>(path: Path): ComponentReference<ComponentContractOf<Path>, Path>;
892
905
  slots: typeof listSlots;
893
906
  slot: typeof slotOf;
894
907
  slotHostedBy: typeof slotHostedBy;
895
908
  namedSlots: typeof namedSlots;
909
+ slotsFor: typeof slotsFor;
910
+ forwardOptions: typeof forwardOptions;
896
911
  cell: typeof cellAt;
897
912
  instance: typeof instanceAt;
898
913
  }
@@ -5,7 +5,7 @@ import { pathInput } from "../../framework/schemas.js";
5
5
  import { derivedColor, gradient } from "./color.js";
6
6
  import { shadowValue } from "./shadow.js";
7
7
  import { defineEntity } from "../../framework/defineEntity.js";
8
- import { defineSubEntity } from "../../framework/defineSubEntity.js";
8
+ import { defineSubEntity, orList } from "../../framework/defineSubEntity.js";
9
9
  import { childrenPolicy, forwardedPropName } from "./element.js";
10
10
  import { listedIn } from "../../framework/utils/enumerated.js";
11
11
  import { valueSchemaOf } from "../../framework/value-domain.js";
@@ -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;
@@ -934,18 +940,18 @@ function composedCreateExample(config, path) {
934
940
  props: { name: "children" }
935
941
  }
936
942
  },
937
- ...Object.keys(styleLayers).length ? { styles: { "*": {
943
+ ...Object.keys(styleLayers).length ? { styles: [{
938
944
  when: null,
939
945
  layers: styleLayers
940
- } } } : {},
941
- preview: { ["*"]: {
946
+ }] } : {},
947
+ preview: [{
942
948
  when: null,
943
949
  props: {
944
950
  icon: true,
945
951
  label: "Example",
946
952
  children: "Body text"
947
953
  }
948
- } }
954
+ }]
949
955
  }
950
956
  };
951
957
  }
@@ -983,7 +989,7 @@ function rendersLayer(element, key) {
983
989
  return typeof element === "object" && element !== null && "$state" in element && element.layer === key;
984
990
  }
985
991
  /** The shape of a rule, for a bag written where the rule belongs or a rule with no `layers`. */
986
- const ruleShapeError = (issue) => issue.code === "invalid_type" || issue.code === "unrecognized_keys" ? "a style rule is `{ \"when\": null, \"layers\": { \"<layer>\": { \"<styleProp>\": \"<value>\" } } }` under a key of your own (`*` for the unconditional rule), or with `\"when\": { \"<prop>\": \"<value>\" }` for a variant; the layer bags sit under `layers`, never directly under the rule" : void 0;
992
+ const ruleShapeError = (issue) => issue.code === "invalid_type" || issue.code === "unrecognized_keys" ? "a style rule is `{ \"when\": null, \"layers\": { \"<layer>\": { \"<styleProp>\": \"<value>\" } } }` for the unconditional rule, or with `\"when\": { \"<prop>\": \"<value>\" }` for a variant, and `styles` is a list of them; the layer bags sit under `layers`, never directly under the rule" : void 0;
987
993
  /** Whether a rule declares nothing: no `layers`, or every bag empty. A rule is its declarations, so
988
994
  * such a rule emits no CSS and styles no layer. */
989
995
  function ruleStylesNothing(rule) {
@@ -1048,7 +1054,7 @@ function perOwner({ cache, config, owner, build }) {
1048
1054
  return schema;
1049
1055
  }
1050
1056
  const ruleSchemas = /* @__PURE__ */ new WeakMap();
1051
- const PREVIEW_DESCRIPTION = "What the catalogue tile and the matrix render with, keyed by condition the way `styles` is. The `*` entry, `{ \"when\": null, \"props\": { ... } }`, is what every cell starts from; an entry with a condition, `{ \"when\": { \"type\": \"image\" }, \"props\": { \"src\": \"…\" } }`, adds its props to the cells whose values match, in the grammar a style rule uses. A slot prop previews with a stored element, `{ \"element\": { \"__ref\": \"component:<Name>\" } }`. A condition on a prop no rule styles makes that prop a preview axis with the value named. A component whose root takes children renders empty until an entry or `defaultProps` carries some.";
1057
+ const PREVIEW_DESCRIPTION = "What the catalogue tile and the matrix render with: a list of entries, each identified by its condition the way a style rule is. The entry `{ \"when\": null, \"props\": { ... } }` is what every cell starts from; an entry with a condition, `{ \"when\": { \"type\": \"image\" }, \"props\": { \"src\": \"…\" } }`, adds its props to the cells whose values match, in the grammar a style rule uses. A slot prop previews with a stored element, `{ \"element\": { \"__ref\": \"component:<Name>\" } }`. A condition on a prop no rule styles makes that prop a preview axis with the value named. A component whose root takes children renders empty until an entry or `defaultProps` carries some.";
1052
1058
  const previewShapeError = "a preview entry is `{ \"when\": null | { \"<prop>\": \"<value>\" }, \"props\": { \"<prop>\": <value> } }`: `when` names this component's own props at one of their values (`null` for the entry every cell reads) and `props` the props the matching cells preview with.";
1053
1059
  const previewBase = z.object({
1054
1060
  when: z.record(z.string(), z.unknown()).nullable().default(null),
@@ -1534,7 +1540,7 @@ const componentFields = z.strictObject({
1534
1540
  * forwards nothing rather than being quietly translated.
1535
1541
  */
1536
1542
  forwards: Forward.optional(),
1537
- styles: StyleRule.optional(),
1543
+ styles: orList(StyleRule).optional(),
1538
1544
  /** The props the render reads that the component neither declares nor forwards — its render
1539
1545
  * extras (`.render<{ swatchColor?: string }>()`). Read off the render at build like `anatomy`,
1540
1546
  * never authored as data. The module consumes them; a component painted out of its anatomy has
@@ -1544,7 +1550,7 @@ const componentFields = z.strictObject({
1544
1550
  defaultProps: Style.optional(),
1545
1551
  /** What the tile and the matrix render with — see `preview({ defaultProps, when })`. Read off
1546
1552
  * `.preview()` at build, and written per cell from the matrix. */
1547
- preview: Preview.optional().describe(PREVIEW_DESCRIPTION),
1553
+ preview: orList(Preview).optional().describe(PREVIEW_DESCRIPTION),
1548
1554
  /**
1549
1555
  * The source that implements this component — a ref, because the file is an entity the seal owns.
1550
1556
  *
@@ -1671,31 +1677,52 @@ 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),
1695
1722
  schemas: {
1696
1723
  create: {
1697
1724
  data: componentFields,
1698
- description: "Create a component at a new qualified `path`. For a component with nothing in it yet, one to design on the canvas before it has props or variants, use `create-blank` with only a `path` instead of authoring a minimal body here. When its complete body is known, include all layers, anatomy, props, defaults, and styles in this one operation. `data.layers` maps each stable layer name to the element it renders and must include `root` — a tag for a primitive (`{ \"layers\": { \"root\": \"div\" } }`), and for anything built on top, a ref to a primitive (`{ \"root\": { \"__ref\": \"component:Box\" }, \"label\": { \"__ref\": \"component:Text\" } }`), which is what lets the layer take that primitive's style props. A primitive exposes style props as `{ \"type\": \"styleProperty\", \"value\": { \"__ref\": \"style-property:bg\" } }` and declares `children: { \"type\": \"slot\", \"text\": true }` when it holds content — without the slot, nothing nested inside it renders. A composed component exposes a layer's props at its own top level through `forwards`, keyed by layer: `{ \"forwards\": { \"root\": \"*\" } }` passes every prop the root's element takes (so `<Button width=\"full\">` reaches the root's `width`), and a list of member refs (`[{ \"__ref\": \"component:Box#props/width\" }]`, each naming the component that declares the prop) passes only those. A prop a layer already exposes is forwarded, never redeclared in `props`; a `props` entry of `{ \"type\": \"forward\" }` is the reverse route, one of this component's props into a layer's. `anatomy` is the tree the build would read off a render: each node renders a layer (`{ \"__ref\": \"layer:<name>\" }`), an intrinsic tag, or `Slot` (`{ \"element\": \"Slot\", \"props\": { \"name\": \"children\" } }`, where a slot prop's content lands — but a declared slot fills `root` by default, so a primitive still declares its `children` slot prop yet needs no `Slot` node or `children` entry for it, and a `Slot` node is only for landing that content in a nested layer); `children` lists sibling node keys from the `root` node, never a prop name; `text` is a fixed string; `visible` is a condition on a prop (`{ \"$state\": \"<prop>\" }`, or with `eq`); content shown in a nested layer is a route, `children: { \"type\": \"forward\", \"target\": \"label/children\" }`. `$state` appears only in `visible` and in an `element` a prop chooses, never as a prop value. A glyph is a node of its own, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" } }`, listed in the `children` of the layer node that holds it, never a prop value. Text a prop supplies to a text layer is never a node either: declare the prop `{ \"type\": \"forward\", \"target\": \"<layer>/children\" }` and the layer shows it; a `Slot` node goes only under a layer that takes nodes. A style rule is `\"styles\": { \"*\": { \"when\": null, \"layers\": { \"root\": { \"bg\": \"surface\" } } } }`, keyed by a name, with each layer's bag under `layers`. A composed component carries its own styles: give it an unconditional (`\"*\"`) rule that lays out and surfaces it through the system's style properties — spacing, alignment and gap on the container, a background and color where it has one — rather than leaning on the bare primitives, which paint flat. Style a layer only with the style props the primitive it renders exposes, set to the values those props accept: `component/get` on the primitive lists both, so read it and choose from that rather than reaching for a CSS name or value the system has no style property for. Set `preview` (`{ \"*\": { \"when\": null, \"props\": { \"children\": \"Example\" } } }`) or `defaultProps` so the component previews with content; a root that takes children renders empty otherwise. A style rule's layer bag is keyed by style-prop name (`fontWeight`, `color`), never by CSS property name, and each value is one of that prop's values.",
1725
+ description: "Create a component at a new qualified `path`. For a component with nothing in it yet, one to design on the canvas before it has props or variants, use `create-blank` with only a `path` instead of authoring a minimal body here. When its complete body is known, include all layers, anatomy, props, defaults, and styles in this one operation. `data.layers` maps each stable layer name to the element it renders and must include `root` — a tag for a primitive (`{ \"layers\": { \"root\": \"div\" } }`), and for anything built on top, a ref to a primitive (`{ \"root\": { \"__ref\": \"component:Box\" }, \"label\": { \"__ref\": \"component:Text\" } }`), which is what lets the layer take that primitive's style props. A primitive exposes style props as `{ \"type\": \"styleProperty\", \"value\": { \"__ref\": \"style-property:bg\" } }` and declares `children: { \"type\": \"slot\", \"text\": true }` when it holds content — without the slot, nothing nested inside it renders. A composed component exposes a layer's props at its own top level through `forwards`, keyed by layer: `{ \"forwards\": { \"root\": \"*\" } }` passes every prop the root's element takes (so `<Button width=\"full\">` reaches the root's `width`), and a list of member refs (`[{ \"__ref\": \"component:Box#props/width\" }]`, each naming the component that declares the prop) passes only those. A prop a layer already exposes is forwarded, never redeclared in `props`; a `props` entry of `{ \"type\": \"forward\" }` is the reverse route, one of this component's props into a layer's. `anatomy` is the tree the build would read off a render: each node renders a layer (`{ \"__ref\": \"layer:<name>\" }`), an intrinsic tag, or `Slot` (`{ \"element\": \"Slot\", \"props\": { \"name\": \"children\" } }`, where a slot prop's content lands — but a declared slot fills `root` by default, so a primitive still declares its `children` slot prop yet needs no `Slot` node or `children` entry for it, and a `Slot` node is only for landing that content in a nested layer); `children` lists sibling node keys from the `root` node, never a prop name; `text` is a fixed string; `visible` is a condition on a prop (`{ \"$state\": \"<prop>\" }`, or with `eq`); content shown in a nested layer is a route, `children: { \"type\": \"forward\", \"target\": \"label/children\" }`. `$state` appears only in `visible` and in an `element` a prop chooses, never as a prop value. A glyph is a node of its own, `{ \"element\": { \"__ref\": \"icon:<library>/<Name>\" } }`, listed in the `children` of the layer node that holds it, never a prop value. Text a prop supplies to a text layer is never a node either: declare the prop `{ \"type\": \"forward\", \"target\": \"<layer>/children\" }` and the layer shows it; a `Slot` node goes only under a layer that takes nodes. `styles` is a list of rules, `\"styles\": [{ \"when\": null, \"layers\": { \"root\": { \"bg\": \"surface\" } } }]`, each identified by its `when`, with each layer's bag under `layers`. A composed component carries its own styles: give it an unconditional (`\"when\": null`) rule that lays out and surfaces it through the system's style properties — spacing, alignment and gap on the container, a background and color where it has one — rather than leaning on the bare primitives, which paint flat. Style a layer only with the style props the primitive it renders exposes, set to the values those props accept: `component/get` on the primitive lists both, so read it and choose from that rather than reaching for a CSS name or value the system has no style property for. Set `preview` (`[{ \"when\": null, \"props\": { \"children\": \"Example\" } }]`) or `defaultProps` so the component previews with content; a root that takes children renders empty otherwise. A style rule's layer bag is keyed by style-prop name (`fontWeight`, `color`), never by CSS property name, and each value is one of that prop's values.",
1699
1726
  example: ({ config }) => {
1700
1727
  const path = freeExamplePath(config);
1701
1728
  const composed = composedCreateExample(config, path);
@@ -1731,10 +1758,10 @@ slots: ({ config, path, member }) => {
1731
1758
  children: ["icon", "content"]
1732
1759
  }
1733
1760
  },
1734
- preview: { ["*"]: {
1761
+ preview: [{
1735
1762
  when: null,
1736
1763
  props: { children: "Example" }
1737
- } }
1764
+ }]
1738
1765
  }
1739
1766
  };
1740
1767
  }
@@ -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. */
@@ -248,9 +248,11 @@ type PinComponentAdjustment<E, P extends string> = {
248
248
  type PinComponentBody<E, P extends string> = Omit<PartialBodyOf<E>, 'styles'> & {
249
249
  styles?: StoredStylesOf<PartialBodyOf<E>> | PinStyleRules<P>;
250
250
  };
251
+ /** The keyed record a body's `styles` is stored as, so a rule list is checked by
252
+ * {@link PinStyleRules} against the linked component's layers rather than taken unchecked. */
251
253
  type StoredStylesOf<Body> = Body extends {
252
254
  styles?: infer Styles;
253
- } ? Styles : never;
255
+ } ? Exclude<Styles, readonly unknown[]> : never;
254
256
  /** The linked contracts registered at `path`, from whichever source. */
255
257
  type LinkedContractKey<P extends string> = Extract<keyof RegisteredComponentContracts, `${P}@${string}`>;
256
258
  type PinStyleRules<P extends string> = [LinkedContractKey<P>] extends [never] ? readonly StyleRuleInput<LayerMap>[] : ContractStyleRules<RegisteredComponentContracts[LinkedContractKey<P>]>;
@@ -259,10 +261,15 @@ type LinkDeclaration = '*' | Record<string, '*' | readonly string[]>;
259
261
  /** One `options` entry, checked against the kind its accessor addresses. The runtime floor — that an
260
262
  * entry names at least one of the three — is the schema's `refine`, which reports the path. */
261
263
  type PinAdjustment<E> = {
262
- override?: PartialBodyOf<E>;
263
- extend?: PartialBodyOf<E>;
264
+ override?: StoredBodyOf<PartialBodyOf<E>>;
265
+ extend?: StoredBodyOf<PartialBodyOf<E>>;
264
266
  hidden?: boolean;
265
267
  };
268
+ /** A body with its `styles` as the keyed record it is stored as. A rule list is taken only where
269
+ * {@link PinOptionsInput} checks it against the linked component's layers. */
270
+ type StoredBodyOf<Body> = 'styles' extends keyof Body ? Omit<Body, 'styles'> & {
271
+ styles?: StoredStylesOf<Body>;
272
+ } : Body;
266
273
  /**
267
274
  * One `options` entry as stored — what this config does to that path, with the two bodies left open.
268
275
  *
@@ -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 };