@yahoo/uds-create-config 5.38.0 → 5.39.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.
@@ -72,6 +72,7 @@ import { linkExcludeOpMigration } from "../migrations/20261013182150_link_exclud
72
72
  import { fontFacesMigration } from "../migrations/20261013201548_font_faces.js";
73
73
  import { maxAndMinMigration } from "../migrations/20261015171036_max_and_min.js";
74
74
  import { componentStyleSetMigration } from "../migrations/20261015192819_component_style_set.js";
75
+ import { componentElementSwapMigration } from "../migrations/20261015202345_component_element_swap.js";
75
76
  const System = defineConfig({
76
77
  name: WEB_CONFIG_NAME,
77
78
  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.",
@@ -155,5 +156,6 @@ if (!registeredSchemaMigrations().has(20261013182150)) registerSchemaMigrations(
155
156
  if (!registeredSchemaMigrations().has(20261013201548)) registerSchemaMigrations(fontFacesMigration);
156
157
  if (!registeredSchemaMigrations().has(20261015171036)) registerSchemaMigrations(maxAndMinMigration);
157
158
  if (!registeredSchemaMigrations().has(20261015192819)) registerSchemaMigrations(componentStyleSetMigration);
159
+ if (!registeredSchemaMigrations().has(20261015202345)) registerSchemaMigrations(componentElementSwapMigration);
158
160
  //#endregion
159
161
  export { System };
@@ -822,6 +822,17 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
822
822
  }, z.core.$strip>;
823
823
  }, z.core.$strip>;
824
824
  };
825
+ readonly 'element-swap': {
826
+ readonly input: z.ZodObject<{
827
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
828
+ key: z.ZodString;
829
+ data: z.ZodObject<{
830
+ element: z.ZodObject<{
831
+ __ref: z.ZodString;
832
+ }, z.core.$strip>;
833
+ }, z.core.$strip>;
834
+ }, z.core.$strip>;
835
+ };
825
836
  readonly validate: {
826
837
  readonly input: z.ZodObject<{
827
838
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
@@ -16,7 +16,7 @@ import { bindPreviewGlyphs, glyphMemberRefIn } from "./glyph-identity.js";
16
16
  import { Style, addBagKeyIssues, anatomyPropValue, bagNameEdges, bagRewriteName, bindBagTokens, collectBag, isPlainRecord, openBag, styleValue } from "./style-bag.js";
17
17
  import { storedPreview } from "../../framework/preview-store.js";
18
18
  import { glyphPropsOf, glyphPropsOfBody, propOwner, propValueDomain, refListOf, resolveComponentProps, stylePropNumberUnit, stylePropValueLeaves, validateComponentProps } from "../../framework/projections.js";
19
- import { carriesContent, intoName, isValueSlot, renderBoundProp, routedPropIn, routesContent } from "../../framework/prop-surface.js";
19
+ import { carriesContent, intoName, isValueSlot, renderBoundProp, routedPropIn, routesContent, surfaceProps } from "../../framework/prop-surface.js";
20
20
  import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../framework/render-spec.js";
21
21
  import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
22
22
  import { cellAt } from "../../spec/cell.js";
@@ -1407,6 +1407,20 @@ function anatomySubtreeContains(anatomy, ancestor, key) {
1407
1407
  }
1408
1408
  return false;
1409
1409
  }
1410
+ /** Whether `from`, or a component its layers render, is `to`. */
1411
+ function componentReaches({ config, from, to }) {
1412
+ const seen = /* @__PURE__ */ new Set();
1413
+ const queue = [from];
1414
+ while (queue.length > 0) {
1415
+ const current = queue.shift();
1416
+ if (current === void 0 || seen.has(current)) continue;
1417
+ if (current === to) return true;
1418
+ seen.add(current);
1419
+ const layers = (config.resolve("component", current)?.toJSON())?.layers;
1420
+ for (const layer of Object.values(layers ?? {})) if (isRef(layer) && kindOf(layer) === "component") queue.push(pathOf(layer.__ref));
1421
+ }
1422
+ return false;
1423
+ }
1410
1424
  /** The component body as `element-move` reads it — re-resolved between writes so each step builds
1411
1425
  * on the previous one's result rather than the pre-move snapshot. */
1412
1426
  function resolveMoveBody(config, path) {
@@ -2646,6 +2660,93 @@ subtree: z.boolean().default(true) }).optional()
2646
2660
  }))]);
2647
2661
  }
2648
2662
  },
2663
+ /**
2664
+ * Swap a layer for another component, the way `node/element-swap` swaps an element on a design
2665
+ * file: the layer renders the new component and keeps its place and the styles the new one takes.
2666
+ * Its children stay when the new component takes child nodes. When it takes none they go, except
2667
+ * a `Slot` node, which is where a slot prop's content lands and which the component still
2668
+ * declares.
2669
+ *
2670
+ * The removals run first, so the style rules they empty are gone before the layer's own bags are
2671
+ * pruned. A bag keeps the props the new component takes, so a `color` carries over to another
2672
+ * text component and nothing the new one can't apply is left stored.
2673
+ */
2674
+ "element-swap": {
2675
+ input: z.object({
2676
+ path: pathInput("component"),
2677
+ /** The layer to swap. */
2678
+ key: z.string(),
2679
+ data: z.object({
2680
+ /** The component the layer renders instead. */
2681
+ element: refSchema })
2682
+ }),
2683
+ readOnly: false,
2684
+ label: "Swap element",
2685
+ description: "Swap a component's layer for another component: the layer renders the new one and keeps the styles the new one takes. Its children stay when the new component takes child nodes; otherwise they go with their layers and styles, and a `Slot` node stays.",
2686
+ scope: "item",
2687
+ approval: "destructive",
2688
+ title: (entity) => `Swap ${entity} element`,
2689
+ handler: (input, config) => {
2690
+ const path = String(input.path);
2691
+ const key = String(input.key);
2692
+ const element = input.data.element;
2693
+ const body = resolveMoveBody(config, path);
2694
+ if (!body?.layers || !(key in body.layers)) reject("NOT_FOUND", `"${path}" has no layer "${key}" to swap.`);
2695
+ if (kindOf(element) !== "component") rejectData("A layer swaps for a component.", ["element"]);
2696
+ const target = pathOf(element.__ref);
2697
+ if (!config.resolve("component", target)) reject("NOT_FOUND", `There is no component "${target}" to swap to.`);
2698
+ if (componentReaches({
2699
+ config,
2700
+ from: target,
2701
+ to: path
2702
+ })) rejectData(`"${target}" renders "${path}", so swapping it in would make "${path}" render itself.`, ["element"]);
2703
+ const anatomy = body?.anatomy ?? {};
2704
+ const keepsChildren = childrenPolicy(config, target) === "nodes";
2705
+ let next = config.run((keepsChildren ? [] : anatomy[key]?.children ?? []).filter((child) => anatomy[child]?.element !== "Slot").map((child) => ({
2706
+ kind: "component",
2707
+ operation: "element-delete",
2708
+ path,
2709
+ key: child
2710
+ })));
2711
+ const taken = new Set(surfaceProps(next, target).map((prop) => prop.name));
2712
+ const styles = (next.resolve("component", path)?.toJSON())?.styles;
2713
+ for (const [condition, rule] of Object.entries(styles ?? {})) {
2714
+ const bag = rule.layers?.[key];
2715
+ if (!bag) continue;
2716
+ const dropped = Object.keys(bag).filter((prop) => !taken.has(prop));
2717
+ if (dropped.length === 0) continue;
2718
+ const kept = Object.fromEntries(Object.entries(bag).filter(([prop]) => taken.has(prop)));
2719
+ next = ruleStylesNothing({
2720
+ ...rule,
2721
+ layers: {
2722
+ ...rule.layers,
2723
+ [key]: kept
2724
+ }
2725
+ }) ? next.apply({
2726
+ kind: "component",
2727
+ operation: "sub-delete",
2728
+ path,
2729
+ field: "styles",
2730
+ key: condition
2731
+ }) : next.apply({
2732
+ kind: "component",
2733
+ operation: "sub-update",
2734
+ path,
2735
+ field: "styles",
2736
+ key: condition,
2737
+ data: Object.fromEntries(dropped.map((prop) => [`layers.${key}.${prop}`, null]))
2738
+ });
2739
+ }
2740
+ return next.apply({
2741
+ kind: "component",
2742
+ operation: "sub-upsert",
2743
+ path,
2744
+ field: "layers",
2745
+ key,
2746
+ data: element
2747
+ });
2748
+ }
2749
+ },
2649
2750
  validate: {
2650
2751
  input: z.object({
2651
2752
  path: pathInput("component"),
@@ -73,7 +73,7 @@ interface RecordedSource {
73
73
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
74
74
  * that replaced it (see `detectedWireVersion`).
75
75
  */
76
- declare const SERIALIZED_CONFIG_VERSION = 20261015192819;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261015202345;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -5862,7 +5862,7 @@ var Config = class Config {
5862
5862
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5863
5863
  hydrateFrom(stored, options) {
5864
5864
  const claimed = schemaVersionOf(stored);
5865
- if (claimed > 20261015192819) throw new SchemaVersionTooNew(claimed);
5865
+ if (claimed > 20261015202345) throw new SchemaVersionTooNew(claimed);
5866
5866
  const wire = upgradeSerializedConfig(stored);
5867
5867
  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.`);
5868
5868
  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 = 20261015192819;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261015202345;
19
19
  /** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
20
20
  * and a raw old-shape record both flow in without casts. */
21
21
  interface VersionStamped {
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
13
13
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
14
14
  * newer than the constant, and the assertion fails until the author re-mints.
15
15
  */
16
- const CURRENT_SCHEMA_VERSION = 20261015192819;
16
+ const CURRENT_SCHEMA_VERSION = 20261015202345;
17
17
  /** The mint that moved `options` into the `settings` kind — restated here, rather than imported
18
18
  * from the migration that owns it, because that migration imports this module. */
19
19
  const SETTINGS_AS_KIND_VERSION = 20260908171448;
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
45
45
  for (const migration of migrations) {
46
46
  if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
47
47
  if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
48
- if (migration.version > 20261015192819) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
48
+ if (migration.version > 20261015202345) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
49
49
  REGISTERED.set(migration.version, migration);
50
50
  }
51
51
  }
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
102
102
  * Already-current input passes through untouched — same reference, so hot paths pay nothing.
103
103
  */
104
104
  function upgradeSerializedConfig(json, options) {
105
- const target = options?.target ?? 20261015192819;
105
+ const target = options?.target ?? 20261015202345;
106
106
  const version = detectedWireVersion(json);
107
107
  if (version > target) throw new SchemaVersionTooNew(version, target);
108
108
  const raw = json;
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
160
160
  * meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
161
161
  */
162
162
  function upgradePatch(patch, writtenAt, options) {
163
- const target = options?.target ?? 20261015192819;
163
+ const target = options?.target ?? 20261015202345;
164
164
  if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
165
165
  let current = patch;
166
166
  for (const migration of pendingFor(writtenAt, target, options)) {
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
179
179
  * stamp would cause.
180
180
  */
181
181
  function upgradeDraftEntries(draft, options) {
182
- const target = options?.target ?? 20261015192819;
182
+ const target = options?.target ?? 20261015202345;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Additions only, so the envelope passes through as it is:
3
+ *
4
+ * - `component/element-swap` swaps a layer for another component, keeping its children when the
5
+ * new component takes child nodes and the styles it takes.
6
+ *
7
+ * Deletable once no reader older than this version is in use: identity hops cost nothing to keep.
8
+ */
9
+ import type { SchemaMigration } from '../framework/schema-version';
10
+ export declare const COMPONENT_ELEMENT_SWAP_VERSION = 20261015202345;
11
+ export declare const componentElementSwapMigration: SchemaMigration;
@@ -0,0 +1,10 @@
1
+ //#region src/migrations/20261015202345_component_element_swap.ts
2
+ const COMPONENT_ELEMENT_SWAP_VERSION = 20261015202345;
3
+ const componentElementSwapMigration = {
4
+ version: COMPONENT_ELEMENT_SWAP_VERSION,
5
+ up(json) {
6
+ return json;
7
+ }
8
+ };
9
+ //#endregion
10
+ export { COMPONENT_ELEMENT_SWAP_VERSION, componentElementSwapMigration };