@yahoo/uds-create-config 5.25.2 → 5.26.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 (44) hide show
  1. package/dist/configs/system.js +2 -0
  2. package/dist/entities/system/Component.d.ts +35 -2
  3. package/dist/entities/system/Component.js +224 -9
  4. package/dist/entities/system/Settings.js +1 -1
  5. package/dist/entities/system/StyleProperty.js +1 -1
  6. package/dist/entities/system/component-module.d.ts +17 -1
  7. package/dist/entities/system/component-module.js +54 -2
  8. package/dist/entities/system/defineComponent.d.ts +53 -13
  9. package/dist/entities/system/defineComponent.js +1 -0
  10. package/dist/entities/system/forward-route-invariants.js +1 -1
  11. package/dist/entities/system/icon-domain-invariants.js +1 -1
  12. package/dist/entities/system/root-layer-invariants.js +1 -1
  13. package/dist/entities/system/token-values.js +1 -1
  14. package/dist/framework/Config.d.ts +1 -1
  15. package/dist/framework/Config.js +1 -1
  16. package/dist/framework/class-names.d.ts +27 -1
  17. package/dist/framework/class-names.js +56 -3
  18. package/dist/framework/component-states.d.ts +40 -0
  19. package/dist/framework/component-states.js +53 -0
  20. package/dist/framework/interaction-conditions.d.ts +48 -0
  21. package/dist/framework/interaction-conditions.js +104 -0
  22. package/dist/framework/layer-styles.d.ts +5 -1
  23. package/dist/framework/layer-styles.js +18 -4
  24. package/dist/framework/projections.js +44 -2
  25. package/dist/framework/render-spec.js +14 -4
  26. package/dist/framework/schema-version.d.ts +1 -1
  27. package/dist/framework/schema-version.js +5 -5
  28. package/dist/framework/validate-spec.js +14 -0
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.js +6 -4
  31. package/dist/migrations/20261013154037_component_states.d.ts +19 -0
  32. package/dist/migrations/20261013154037_component_states.js +74 -0
  33. package/dist/renderer/componentEntry.d.ts +7 -1
  34. package/dist/renderer/componentEntry.js +30 -1
  35. package/dist/renderer/forced-stamp.d.ts +26 -0
  36. package/dist/renderer/forced-stamp.js +50 -0
  37. package/dist/renderer/index.d.ts +3 -1
  38. package/dist/renderer/index.js +3 -1
  39. package/dist/renderer/use-forced-stamp.d.ts +11 -0
  40. package/dist/renderer/use-forced-stamp.js +50 -0
  41. package/dist/spec/specToJsx.d.ts +6 -0
  42. package/dist/spec/specToJsx.js +5 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -2
@@ -66,6 +66,7 @@ import { startingStyleModifierMigration } from "../migrations/20261009022722_sta
66
66
  import { cssMotionStrictMigration } from "../migrations/20261009181635_css_motion_strict.js";
67
67
  import { linkOverrideRefMigration } from "../migrations/20261009193004_link_override_ref.js";
68
68
  import { linkedComponentModulesMigration } from "../migrations/20261012200606_linked_component_modules.js";
69
+ import { componentStatesMigration } from "../migrations/20261013154037_component_states.js";
69
70
  const System = defineConfig({
70
71
  name: WEB_CONFIG_NAME,
71
72
  description: "A design system: the values, primitives and compositions an app is built from. A ref is written `{ \"__ref\": \"<kind>:<path>\" }`, so a token is `{ \"__ref\": \"token:<group>/<name>\" }` and a token group is `{ \"__ref\": \"token:<group>\" }`. Build it in this order. Tokens first, in groups by purpose (colors, spacing, radii, type sizes), since everything else names them. Then style properties, each with `values` holding one ref to the token group whose members it accepts (`{ \"__ref\": \"token:color\" }`), so a prop takes token names rather than raw CSS; add a string keyword only for a value no token holds. A style rule's bag holds a prop's values (`\"padding\": \"4\"`, `\"bg\": \"brand\"`), never raw CSS. Then a few primitive components, one layer each, whose props are style properties (a box for layout and surface, a text for type, a pressable for interaction). Then every other component composed from those primitives: its layers name a primitive or an element, its anatomy places them (a node renders a layer, a tag or `Slot` where a slot prop's content lands; `children` is the edge list; `visible` is `{ \"$state\": \"<prop>\" }`; a prop shown in a nested layer is a `forward` route, and `$state` never sits in a node's props), and its style rules set style-property values per layer, keyed by prop values when a variant changes them. Modes (a dark mode is `{ type: \"selector\", value: \".dark\" }`) change token values through a token's `overrides`, so a component never restyles for a mode; modifiers (hover, focus, disabled) name a state, and a style rule applies under one as a `_hover` block inside a layer bag. Composites bundle style values that recur. Last, register canvas roles: the canvas draws with them, one entity per tool (`frame` for the layout primitive, `text` for the text primitive, `image`) whose entries name the components that tool inserts, and a tool whose role is unregistered is disabled. Give each component a `preview` entry under `*` so it shows content, and never write a CSS property name where a style-property name belongs.",
@@ -143,5 +144,6 @@ if (!registeredSchemaMigrations().has(20261009022722)) registerSchemaMigrations(
143
144
  if (!registeredSchemaMigrations().has(20261009181635)) registerSchemaMigrations(cssMotionStrictMigration);
144
145
  if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(linkOverrideRefMigration);
145
146
  if (!registeredSchemaMigrations().has(20261012200606)) registerSchemaMigrations(linkedComponentModulesMigration);
147
+ if (!registeredSchemaMigrations().has(20261013154037)) registerSchemaMigrations(componentStatesMigration);
146
148
  //#endregion
147
149
  export { System };
@@ -11,7 +11,7 @@ import { StyleClass } from "./style-bag.js";
11
11
  import { TokenClass } from "./Token.js";
12
12
  import { StoredPreviewCondition } from "../../framework/preview-store.js";
13
13
  import { RepeatSpec } from "../../framework/render-spec.js";
14
- import { AsInput, DefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StyleRuleInput } from "./defineComponent.js";
14
+ import { AsInput, DefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StateMap, StyleRuleInput } from "./defineComponent.js";
15
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";
@@ -645,6 +645,13 @@ interface AnatomyNodeClass extends Named<typeof AnatomyNodeDefinition> {}
645
645
  declare const componentFields: z.ZodObject<{
646
646
  layers: LayerClass;
647
647
  props: z.ZodOptional<PropClass>;
648
+ states: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
649
+ type: z.ZodLiteral<"boolean">;
650
+ }, z.core.$strict>, z.ZodObject<{
651
+ type: z.ZodLiteral<"variant">;
652
+ value: z.ZodArray<z.ZodString>;
653
+ default: z.ZodOptional<z.ZodString>;
654
+ }, z.core.$strict>], "type">>>;
648
655
  forwards: z.ZodOptional<ForwardClass>;
649
656
  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>>>]>>;
650
657
  renderProps: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -679,6 +686,13 @@ declare function blankComponentBody(): ComponentBody;
679
686
  declare const ComponentEntity: EntityClass<z.ZodObject<{
680
687
  layers: LayerClass;
681
688
  props: z.ZodOptional<PropClass>;
689
+ states: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
690
+ type: z.ZodLiteral<"boolean">;
691
+ }, z.core.$strict>, z.ZodObject<{
692
+ type: z.ZodLiteral<"variant">;
693
+ value: z.ZodArray<z.ZodString>;
694
+ default: z.ZodOptional<z.ZodString>;
695
+ }, z.core.$strict>], "type">>>;
682
696
  forwards: z.ZodOptional<ForwardClass>;
683
697
  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>>>]>>;
684
698
  renderProps: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -780,6 +794,19 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
780
794
  }, z.core.$strip>>;
781
795
  }, z.core.$strip>;
782
796
  };
797
+ readonly 'element-wrap': {
798
+ readonly input: z.ZodObject<{
799
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
800
+ key: z.ZodOptional<z.ZodString>;
801
+ data: z.ZodObject<{
802
+ elements: z.ZodArray<z.ZodString>;
803
+ element: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
804
+ __ref: z.ZodString;
805
+ }, z.core.$strip>]>>;
806
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
807
+ }, z.core.$strip>;
808
+ }, z.core.$strip>;
809
+ };
783
810
  readonly validate: {
784
811
  readonly input: z.ZodObject<{
785
812
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
@@ -841,6 +868,12 @@ type ResolvedForwards<Contract, Delta> = Delta extends {
841
868
  } ? Forwards : Contract extends {
842
869
  forwards: infer Forwards;
843
870
  } ? Forwards : Record<never, never>;
871
+ /** The states an extension ends up with: its delta's, else the linked contract's. */
872
+ type ResolvedStates<Contract, Delta> = Delta extends {
873
+ readonly states: infer States extends StateMap;
874
+ } ? States : Contract extends {
875
+ states: infer States extends StateMap;
876
+ } ? States : Record<never, never>;
844
877
  type ForwardedLayerNames<Contract, Delta> = ResolvedForwards<Contract, Delta> extends Record<string, unknown> ? keyof ResolvedForwards<Contract, Delta> & string : never;
845
878
  /** The source-only half of a linked component extension. The callback is deliberately absent from
846
879
  * {@link ExtendedRef}: it is code, so ts-morph reads it from the authoring file while Config stores
@@ -852,7 +885,7 @@ type ExtendedComponentRef<Contract = ComponentContractOf<string>, Delta extends
852
885
  * read signature for `ComponentProps`. */
853
886
  <As extends AsInput = never>(props: PhantomCallProps<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>, ResolvedDefaults<Contract, Delta>, Extra, ForwardedLayerNames<Contract, Delta>, As>): null;
854
887
  <As extends AsInput = never>(props: PhantomReadProps<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>, ResolvedDefaults<Contract, Delta>, Extra, ForwardedLayerNames<Contract, Delta>, As>): null;
855
- render<E = Extra>(fn: (args: RenderArgs<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>> & E) => unknown): ExtendedComponentRef<Contract, Delta, E>;
888
+ render<E = Extra>(fn: (args: RenderArgs<ResolvedLayers<Contract, Delta>, ResolvedProps<Contract, Delta>, ResolvedStates<Contract, Delta>> & E) => unknown): ExtendedComponentRef<Contract, Delta, E>;
856
889
  /**
857
890
  * Style rules for the extension, typed against the layers it ends up with — the linked contract's,
858
891
  * with the delta's replacing them when it declares its own.
@@ -7,6 +7,7 @@ import { shadowValue } from "./shadow.js";
7
7
  import { defineEntity } from "../../framework/defineEntity.js";
8
8
  import { defineSubEntity, orList } from "../../framework/defineSubEntity.js";
9
9
  import { childrenPolicy, forwardedPropName } from "./element.js";
10
+ import { interactionKey, interactionModifiers } from "../../framework/interaction-conditions.js";
10
11
  import { listedIn } from "../../framework/utils/enumerated.js";
11
12
  import { valueSchemaOf } from "../../framework/value-domain.js";
12
13
  import { Package } from "./Package.js";
@@ -712,25 +713,38 @@ function bagFor(config, layerElement) {
712
713
  bags.set(key, built);
713
714
  return built;
714
715
  }
716
+ /**
717
+ * A content prop's value in a rule, or `false` for none: a state that drops a Button's start icon
718
+ * says so with `startIcon: false`, since leaving the key out keeps whatever the rule beneath gives.
719
+ * Not for a required prop, whose content is the component's to have.
720
+ */
721
+ function withNone(value, declaration) {
722
+ if (declaration?.required === true) return value;
723
+ return z.union([value, z.literal(false)]);
724
+ }
715
725
  /** One element's bag, built. Reached only through the memo above. */
716
726
  function buildBagFor(config, layerElement) {
717
727
  const shape = { ...stylePropShape(config) };
718
728
  const component = isRef(layerElement) && config.resolveRef(layerElement.__ref) ? pathOf(layerElement.__ref) : void 0;
719
729
  const glyphProps = component ? glyphPropsOf(config, component) : {};
720
- for (const [name, decl] of Object.entries(component ? resolveComponentProps(config, component) : {})) shape[name] = styleBagValue({
721
- config,
722
- declaration: decl,
723
- name,
724
- glyphLibraries: decl?.type === "forward" ? glyphProps[name] : void 0
725
- }).optional();
730
+ for (const [name, decl] of Object.entries(component ? resolveComponentProps(config, component) : {})) {
731
+ const forwarded = decl?.type === "forward";
732
+ const value = styleBagValue({
733
+ config,
734
+ declaration: decl,
735
+ name,
736
+ glyphLibraries: forwarded ? glyphProps[name] : void 0
737
+ });
738
+ shape[name] = (forwarded ? withNone(value, decl) : value).optional();
739
+ }
726
740
  const declared = component ? (config.resolve("component", component)?.toJSON())?.props ?? {} : {};
727
741
  for (const [name, decl] of Object.entries(declared)) {
728
742
  if (decl?.type !== "slot" || name in shape) continue;
729
- shape[name] = styleBagValue({
743
+ shape[name] = withNone(styleBagValue({
730
744
  config,
731
745
  declaration: decl,
732
746
  name
733
- }).optional();
747
+ }), decl).optional();
734
748
  }
735
749
  shape.as ??= z.string().optional();
736
750
  for (const [key, owned] of config.layerKeys()) shape[key] = owned.schema(shape[key]).optional();
@@ -808,6 +822,18 @@ function conditionValue(prop, declaration, config) {
808
822
  if (!ownsItsValues(declaration)) return domain;
809
823
  return z.union([domain, refSchema]).transform((value) => isRef(value) ? value : valueRef(prop, String(value)));
810
824
  }
825
+ /** The condition on a STATE: a boolean's `true`/`false`, or one of a variant state's names. A state
826
+ * has no value members to ref, so a variant's condition is the name itself. */
827
+ function stateConditionValue(declaration) {
828
+ const decl = declaration;
829
+ if (decl?.type === "variant" && Array.isArray(decl.value)) return z.enum(decl.value);
830
+ return scalarConditionValue({ type: "boolean" }) ?? z.boolean();
831
+ }
832
+ /** An interaction condition's value: `true` (the root), or the layer whose interaction it is. */
833
+ function interactionConditionValue(layers) {
834
+ const truthy = z.union([z.literal(true), z.literal("true").transform(() => true)]);
835
+ return layers.length > 0 ? z.union([truthy, z.enum(layers)], { error: `an interaction condition is \`true\` (the component's root) or one of its layers: ${layers.join(", ")}` }) : truthy;
836
+ }
811
837
  /**
812
838
  * The condition on a boolean or number prop, in the prop's type. A preview axis spells every value
813
839
  * as a string and a rule's key stringifies it, so a writer reading either hands back `'true'` where
@@ -1022,6 +1048,13 @@ const StyleRule = defineSubEntity({
1022
1048
  build: () => {
1023
1049
  const when = {};
1024
1050
  for (const [name, decl] of Object.entries(body.props ?? {})) when[name] = conditionValue(name, decl, config).optional();
1051
+ for (const [name, decl] of Object.entries(body.states ?? {})) when[name] = stateConditionValue(decl).optional();
1052
+ const layerNames = Object.keys(body.layers ?? {});
1053
+ for (const modifier of interactionModifiers(config)) {
1054
+ const key = interactionKey(modifier);
1055
+ if (key in when) continue;
1056
+ when[key] = interactionConditionValue(layerNames).optional();
1057
+ }
1025
1058
  const layers = {};
1026
1059
  for (const [name, el] of Object.entries(body.layers ?? {})) layers[name] = bagFor(config, el).optional();
1027
1060
  return z.strictObject({
@@ -1049,10 +1082,11 @@ function perOwner({ cache, config, owner, build }) {
1049
1082
  cache.set(config, perConfig);
1050
1083
  }
1051
1084
  const hit = perConfig.get(owner);
1052
- if (hit && hit.props === owner.props && hit.layers === owner.layers) return hit.schema;
1085
+ if (hit && hit.props === owner.props && hit.states === owner.states && hit.layers === owner.layers) return hit.schema;
1053
1086
  const schema = build();
1054
1087
  perConfig.set(owner, {
1055
1088
  props: owner.props,
1089
+ states: owner.states,
1056
1090
  layers: owner.layers,
1057
1091
  schema
1058
1092
  });
@@ -1089,6 +1123,7 @@ const Preview = defineSubEntity({
1089
1123
  build: () => {
1090
1124
  const when = {};
1091
1125
  for (const [name, decl] of Object.entries(body.props ?? {})) when[name] = conditionValue(name, decl, config).optional();
1126
+ for (const [name, decl] of Object.entries(body.states ?? {})) when[name] = stateConditionValue(decl).optional();
1092
1127
  return z.strictObject({
1093
1128
  when: z.strictObject(when, { error: previewShapeError }).nullable().default(null),
1094
1129
  props: z.record(z.string(), z.unknown()).transform((props) => bindPreviewGlyphs({
@@ -1347,6 +1382,83 @@ function anatomySubtreeContains(anatomy, ancestor, key) {
1347
1382
  function resolveMoveBody(config, path) {
1348
1383
  return config.resolve("component", path)?.toJSON();
1349
1384
  }
1385
+ /**
1386
+ * Wrap a component's root: the container becomes `root`, and what the root was — its element, its
1387
+ * children and slots, its props, the style rules and forwards that name it — moves onto `inner`, the
1388
+ * container's one child. Props the config doesn't model (`id`, an `onClick`) keep landing on `root`,
1389
+ * which is now the container.
1390
+ */
1391
+ function wrapRoot({ config, path, inner, element, props }) {
1392
+ const body = config.resolve("component", path)?.toJSON();
1393
+ const root = body?.anatomy?.root;
1394
+ if (!body || !root) reject("NOT_FOUND", `"${path}" has no root to wrap.`);
1395
+ const { element: _element, ...rootRest } = root;
1396
+ let next = config.apply({
1397
+ kind: "component",
1398
+ operation: "sub-create",
1399
+ path,
1400
+ field: "layers",
1401
+ key: inner,
1402
+ data: body?.layers?.root
1403
+ }).apply({
1404
+ kind: "component",
1405
+ operation: "sub-create",
1406
+ path,
1407
+ field: "anatomy",
1408
+ key: inner,
1409
+ data: {
1410
+ ...rootRest,
1411
+ element: layerRef(inner)
1412
+ }
1413
+ }).apply({
1414
+ kind: "component",
1415
+ operation: "sub-upsert",
1416
+ path,
1417
+ field: "layers",
1418
+ key: "root",
1419
+ data: asElementRef(config, element)
1420
+ }).apply({
1421
+ kind: "component",
1422
+ operation: "sub-upsert",
1423
+ path,
1424
+ field: "anatomy",
1425
+ key: "root",
1426
+ data: {
1427
+ element: layerRef("root"),
1428
+ children: [inner],
1429
+ ...props ? { props } : {}
1430
+ }
1431
+ });
1432
+ for (const [key, rule] of Object.entries(body?.styles ?? {})) {
1433
+ const bag = rule.layers?.root;
1434
+ if (bag === void 0) continue;
1435
+ next = next.apply({
1436
+ kind: "component",
1437
+ operation: "sub-update",
1438
+ path,
1439
+ field: "styles",
1440
+ key,
1441
+ data: {
1442
+ when: rule.when ?? null,
1443
+ [`layers.${inner}`]: bag,
1444
+ "layers.root": null
1445
+ }
1446
+ });
1447
+ }
1448
+ for (const [name, decl] of Object.entries(body?.props ?? {})) {
1449
+ const target = decl.target;
1450
+ if (!(isRef(target) ? kindOf(target) === "layer" && pathOf(target) === "root" : typeof target === "string" && target.startsWith("root/"))) continue;
1451
+ next = next.apply({
1452
+ kind: "component",
1453
+ operation: "sub-update",
1454
+ path,
1455
+ field: "props",
1456
+ key: name,
1457
+ data: { target: isRef(target) ? layerRef(inner) : `${inner}/${String(target).slice(5)}` }
1458
+ });
1459
+ }
1460
+ return next;
1461
+ }
1350
1462
  const AnatomyNode = defineSubEntity({
1351
1463
  name: "element",
1352
1464
  label: "Element",
@@ -1538,6 +1650,18 @@ const componentFields = z.strictObject({
1538
1650
  }),
1539
1651
  props: Prop.optional(),
1540
1652
  /**
1653
+ * What the RENDER knows and a style rule keys on — the field has a value, the image loaded, the
1654
+ * indicator is sliding. Not props: a caller never passes one, so none is part of the component's
1655
+ * API. The render passes them to `states(props, { … })` and spreads the layers it hands back;
1656
+ * the rules naming one follow the values given, and the root carries each as a `data-<name>`
1657
+ * attribute. Studio and tests force one through the module's internal channel, never a prop.
1658
+ */
1659
+ states: z.record(z.string(), z.discriminatedUnion("type", [z.strictObject({ type: z.literal("boolean") }), z.strictObject({
1660
+ type: z.literal("variant"),
1661
+ value: z.array(z.string()).min(1),
1662
+ default: z.string().optional()
1663
+ })])).optional().describe("What the render knows and a style rule keys on, by name: `{ \"filled\": { \"type\": \"boolean\" } }`, or `{ \"phase\": { \"type\": \"variant\", \"value\": [\"idle\", \"sliding\"], \"default\": \"idle\" } }`. Not props, so not part of the component's API: the render passes its `props` and the values it knows to `const layers = states(props, { filled: value !== \"\" })` and spreads those layers (`{...layers.root}`) instead of `props`; a rule naming a state in `when` (`{ \"filled\": true }`, `{ \"phase\": \"sliding\" }`) is tested against the values given, and the root carries each as a `data-<name>` attribute. A value a caller controls is a prop instead, which the render reports the state from."),
1664
+ /**
1541
1665
  * Which layers' element props this component also exposes — see {@link Forward}.
1542
1666
  *
1543
1667
  * Replaced `inherits`, which named one layer and meant `{ [layer]: '*' }`. That field and its
@@ -1598,6 +1722,34 @@ const componentFields = z.strictObject({
1598
1722
  message: `\`${name}\` conditions on itself. \`when\` names a sibling prop and one of its values.`
1599
1723
  });
1600
1724
  }
1725
+ if (isPlainObject(body.states) && Object.keys(body.states).length > 0) {
1726
+ const props = isPlainObject(body.props) ? body.props : {};
1727
+ if (Object.hasOwn(props, "states")) ctx.issues.push({
1728
+ code: "custom",
1729
+ input: props.states,
1730
+ path: ["props", "states"],
1731
+ message: "A component with states hands its render a `states` helper, so no prop of its own can be named `states`."
1732
+ });
1733
+ for (const [name, decl] of Object.entries(body.states)) {
1734
+ if (Object.hasOwn(props, name)) ctx.issues.push({
1735
+ code: "custom",
1736
+ input: decl,
1737
+ path: ["states", name],
1738
+ message: `\`${name}\` is both a prop and a state. A state is what the render knows, never a value a caller passes; a caller-controlled value is a prop the render reports the state from.`
1739
+ });
1740
+ const variant = decl;
1741
+ if (variant?.type === "variant" && variant.default !== void 0 && Array.isArray(variant.value) && !variant.value.includes(variant.default)) ctx.issues.push({
1742
+ code: "custom",
1743
+ input: variant.default,
1744
+ path: [
1745
+ "states",
1746
+ name,
1747
+ "default"
1748
+ ],
1749
+ message: `\`${name}\`'s default \`${String(variant.default)}\` is not one of its values (${variant.value.join(", ")}).`
1750
+ });
1751
+ }
1752
+ }
1601
1753
  if (body.forwards || body.file || isRef(body.props)) return;
1602
1754
  const declared = body.props ?? {};
1603
1755
  if (Object.hasOwn(declared, "children") && declared.children !== null) return;
@@ -2328,6 +2480,69 @@ subtree: z.boolean().default(true) }).optional()
2328
2480
  });
2329
2481
  }
2330
2482
  },
2483
+ "element-wrap": {
2484
+ input: z.object({
2485
+ path: pathInput("component"),
2486
+ /** The new layer's name: the wrapper's, or — wrapping the root — the old root's new name. The
2487
+ * entity mints a free one when the caller doesn't care. */
2488
+ key: z.string().optional(),
2489
+ data: z.object({
2490
+ /** The layers to wrap: siblings under one parent, wrapped in their order, or `['root']`. */
2491
+ elements: z.array(z.string()).min(1),
2492
+ /** What the wrapper renders — a frame component or a tag. */
2493
+ element: z.union([z.string(), refSchema]).default("div"),
2494
+ /** The wrapper's own props. */
2495
+ props: z.record(z.string(), z.unknown()).optional()
2496
+ })
2497
+ }),
2498
+ readOnly: false,
2499
+ label: "Wrap elements",
2500
+ description: "Wrap a component's sibling layers in a new container. Wrapping `root` makes the container the root and moves the old root — its element, children, slots, styles and forwards — onto a new layer inside it.",
2501
+ scope: "item",
2502
+ creates: true,
2503
+ title: (entity) => `Wrap ${entity} elements`,
2504
+ handler: (input, config) => {
2505
+ const payload = input.data;
2506
+ const path = String(input.path);
2507
+ const keys = payload.elements;
2508
+ const anatomy = resolveMoveBody(config, path)?.anatomy ?? {};
2509
+ for (const k of keys) if (!anatomy[k]) reject("NOT_FOUND", `"${path}" has no element "${k}" to wrap.`);
2510
+ if (keys.includes("root")) {
2511
+ if (keys.length > 1) rejectData("The root wraps on its own: it has no siblings to wrap with it.", ["elements"]);
2512
+ return wrapRoot({
2513
+ config,
2514
+ path,
2515
+ inner: input.key ? String(input.key) : freeLayerKey(config, path, "content"),
2516
+ element: payload.element,
2517
+ props: payload.props
2518
+ });
2519
+ }
2520
+ const parent = Object.entries(anatomy).find(([, node]) => keys.every((k) => node.children?.includes(k)));
2521
+ if (!parent) rejectData("Only layers that share a parent wrap together — wrap each parent's layers on their own.", ["elements"]);
2522
+ const [parentKey, parentNode] = parent;
2523
+ const order = (parentNode.children ?? []).filter((c) => keys.includes(c));
2524
+ const index = (parentNode.children ?? []).indexOf(order[0]);
2525
+ const wrapper = input.key ? String(input.key) : freeLayerKey(config, path, "wrap");
2526
+ return config.run([{
2527
+ kind: "component",
2528
+ operation: "element-insert",
2529
+ path,
2530
+ key: wrapper,
2531
+ data: {
2532
+ element: payload.element,
2533
+ parent: parentKey,
2534
+ index,
2535
+ ...payload.props ? { props: payload.props } : {}
2536
+ }
2537
+ }, ...order.map((k) => ({
2538
+ kind: "component",
2539
+ operation: "element-move",
2540
+ path,
2541
+ key: k,
2542
+ data: { parent: wrapper }
2543
+ }))]);
2544
+ }
2545
+ },
2331
2546
  validate: {
2332
2547
  input: z.object({
2333
2548
  path: pathInput("component"),
@@ -1,9 +1,9 @@
1
1
  import { splitRef } from "../../framework/utils/refs.js";
2
2
  import { defineEntity } from "../../framework/defineEntity.js";
3
3
  import { defineSubEntity } from "../../framework/defineSubEntity.js";
4
+ import { createSliceMemo } from "../../framework/memo.js";
4
5
  import { buildFields } from "../../configs/build-options.js";
5
6
  import { parseRootFontSize } from "../../css/root-font-size.js";
6
- import { createSliceMemo } from "../../framework/memo.js";
7
7
  import { resolveInputDir } from "../../framework/registry-dir.js";
8
8
  import { safelistSchema } from "./safelist.js";
9
9
  import { z } from "zod";
@@ -1,8 +1,8 @@
1
1
  import { declarationText, isRef, kindOf, pathOf, ref, refOfKey, refSchema, replaceRefMarkers, sourceOf, splitRef } from "../../framework/utils/refs.js";
2
2
  import { defineEntity } from "../../framework/defineEntity.js";
3
+ import { createSliceMemo } from "../../framework/memo.js";
3
4
  import { cssValueIssue, isCssProperty } from "../../css/values.js";
4
5
  import { camelCase } from "./style-bag.js";
5
- import { createSliceMemo } from "../../framework/memo.js";
6
6
  import { isIdentifier } from "../../framework/utils/identifier.js";
7
7
  import { cssVar, declaresCssVariable } from "../../framework/projections.js";
8
8
  import { z } from "zod";
@@ -26,6 +26,22 @@ interface ComponentModuleSource {
26
26
  readonly version: string;
27
27
  };
28
28
  }
29
+ /** The directory, under the system's input root, a component made in Create is built from. No file
30
+ * is ever written there: the server writes the source `uds pull` would, and builds it in place. */
31
+ declare const CREATED_COMPONENT_DIRECTORY = "__created__";
32
+ /** The virtual source path a component made in Create builds from. */
33
+ declare function createdComponentFilePath(config: Config, path: string): string;
34
+ /** The component a virtual created path names, when it names one this config made in Create. */
35
+ declare function createdComponentPathOf(config: Config, filePath: string): string | undefined;
36
+ /** Whether a component was made in Create: a web component no file holds, whose structure is its
37
+ * anatomy. */
38
+ declare function isCreatedComponent(config: Config, path: string): boolean;
39
+ /**
40
+ * A component made in Create's module key: the runtime every module embeds, the component's own
41
+ * build body, and the bodies of the components its layers place — whose props decide how it routes
42
+ * and emits. Computed from the config alone, so the browser and the server spell it the same.
43
+ */
44
+ declare function createdModuleKey(config: Config, path: string): string | undefined;
29
45
  /**
30
46
  * The static an emitted module stamps a component's registry-entry facts on: what
31
47
  * `makeComponentEntry` takes beyond the component (`childless`, `computed`). A static and not an
@@ -110,4 +126,4 @@ declare function linkedComponentModule(config: Config, componentPathOrRef: strin
110
126
  readonly view: Config;
111
127
  } | undefined;
112
128
  //#endregion
113
- export { COMPONENT_ENTRY_STATIC, ComponentModuleSource, ComponentRegistryImport, ComponentRegistryImportBinding, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, linkedComponentModule, linkedModuleView };
129
+ export { COMPONENT_ENTRY_STATIC, CREATED_COMPONENT_DIRECTORY, ComponentModuleSource, ComponentRegistryImport, ComponentRegistryImportBinding, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, createdComponentFilePath, createdComponentPathOf, createdModuleKey, isCreatedComponent, linkedComponentModule, linkedModuleView };
@@ -1,4 +1,4 @@
1
- import { ref, routedSource, sourceHops, splitRef } from "../../framework/utils/refs.js";
1
+ import { isRef, kindOf, pathOf, ref, routedSource, sourceHops, splitRef } from "../../framework/utils/refs.js";
2
2
  import { createSliceMemo } from "../../framework/memo.js";
3
3
  import { Settings, classPrefixOf, prefixed, varPrefixOf } from "./Settings.js";
4
4
  import { File } from "./File.js";
@@ -28,6 +28,58 @@ const WEB_RUNTIME_KINDS = [
28
28
  "motion",
29
29
  "composite"
30
30
  ];
31
+ /** The directory, under the system's input root, a component made in Create is built from. No file
32
+ * is ever written there: the server writes the source `uds pull` would, and builds it in place. */
33
+ const CREATED_COMPONENT_DIRECTORY = "__created__";
34
+ /** The virtual source path a component made in Create builds from. */
35
+ function createdComponentFilePath(config, path) {
36
+ return `${createdComponentDirectory(config)}${path}.tsx`;
37
+ }
38
+ /** The directory, with its trailing slash, a config's created paths start with. An empty input
39
+ * root puts it at the top, since a sealed path never starts with a slash. */
40
+ function createdComponentDirectory(config) {
41
+ const root = (Settings.get(config)?.inputDir ?? "src").replace(/\/+$/, "");
42
+ return root ? `${root}/${CREATED_COMPONENT_DIRECTORY}/` : `${CREATED_COMPONENT_DIRECTORY}/`;
43
+ }
44
+ /** The component a virtual created path names, when it names one this config made in Create. */
45
+ function createdComponentPathOf(config, filePath) {
46
+ const directory = createdComponentDirectory(config);
47
+ if (!filePath.startsWith(directory) || !filePath.endsWith(".tsx")) return;
48
+ const path = filePath.slice(directory.length, -4);
49
+ return isCreatedComponent(config, path) ? path : void 0;
50
+ }
51
+ /** Whether a component was made in Create: a web component no file holds, whose structure is its
52
+ * anatomy. */
53
+ function isCreatedComponent(config, path) {
54
+ if (isNativeConfig(config)) return false;
55
+ const component = Component.get(config, path);
56
+ if (!component || component.source !== void 0) return false;
57
+ const { file, export: exportName, anatomy } = component.body;
58
+ return !file && !exportName && Object.keys(anatomy ?? {}).length > 0;
59
+ }
60
+ /**
61
+ * A component made in Create's module key: the runtime every module embeds, the component's own
62
+ * build body, and the bodies of the components its layers place — whose props decide how it routes
63
+ * and emits. Computed from the config alone, so the browser and the server spell it the same.
64
+ */
65
+ function createdModuleKey(config, path) {
66
+ const component = Component.get(config, path);
67
+ if (!component) return void 0;
68
+ const body = component.toJSON();
69
+ const placed = {};
70
+ for (const element of Object.values(body.layers ?? {})) {
71
+ if (!isRef(element) || kindOf(element) !== "component") continue;
72
+ const target = Component.get(config, pathOf(element.__ref));
73
+ if (target) placed[target.path] = buildEntityBody("component", target.toJSON());
74
+ }
75
+ return contentKey({
76
+ version: COMPONENT_MODULE_BUILD_VERSION,
77
+ runtimeKey: componentRuntimeKey(config),
78
+ created: path,
79
+ body: buildEntityBody("component", body),
80
+ placed
81
+ });
82
+ }
31
83
  /**
32
84
  * The static an emitted module stamps a component's registry-entry facts on: what
33
85
  * `makeComponentEntry` takes beyond the component (`childless`, `computed`). A static and not an
@@ -393,4 +445,4 @@ function publishedPin(config, slug) {
393
445
  }
394
446
  }
395
447
  //#endregion
396
- export { COMPONENT_ENTRY_STATIC, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, linkedComponentModule, linkedModuleView };
448
+ export { COMPONENT_ENTRY_STATIC, CREATED_COMPONENT_DIRECTORY, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports, createdComponentFilePath, createdComponentPathOf, createdModuleKey, isCreatedComponent, linkedComponentModule, linkedModuleView };