@yahoo/uds-create-config 4.36.1 → 4.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,8 @@ import { renderTokenRefsMigration } from "../migrations/20260923160425_render_to
42
42
  import { globalStyleDerivedColorsMigration } from "../migrations/20260923163604_global_style_derived_colors.js";
43
43
  import { slotCodeFormsMigration } from "../migrations/20260924044539_slot_code_forms.js";
44
44
  import { canvasRoleSlotMigration } from "../migrations/20260925143319_canvas_role_slot.js";
45
+ import { linkOnLinkedMigration } from "../migrations/20260928180801_link_on_linked.js";
46
+ import { compositePropLayerMigration } from "../migrations/20260928215509_composite_prop_layer.js";
45
47
  const System = defineConfig({
46
48
  name: WEB_CONFIG_NAME,
47
49
  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.",
@@ -95,5 +97,7 @@ if (!registeredSchemaMigrations().has(20260923160425)) registerSchemaMigrations(
95
97
  if (!registeredSchemaMigrations().has(20260923163604)) registerSchemaMigrations(globalStyleDerivedColorsMigration);
96
98
  if (!registeredSchemaMigrations().has(20260924044539)) registerSchemaMigrations(slotCodeFormsMigration);
97
99
  if (!registeredSchemaMigrations().has(20260925143319)) registerSchemaMigrations(canvasRoleSlotMigration);
100
+ if (!registeredSchemaMigrations().has(20260928180801)) registerSchemaMigrations(linkOnLinkedMigration);
101
+ if (!registeredSchemaMigrations().has(20260928215509)) registerSchemaMigrations(compositePropLayerMigration);
98
102
  //#endregion
99
103
  export { System };
@@ -176,17 +176,46 @@ declare const propShapeDefinition: z.ZodDiscriminatedUnion<[z.ZodObject<{
176
176
  }, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodObject<{
177
177
  __ref: z.ZodString;
178
178
  }, z.core.$strip>]>>]>>>;
179
- }, z.core.$strict>, z.ZodObject<{
179
+ }, z.core.$strict>, z.ZodPipe<z.ZodObject<{
180
180
  type: z.ZodLiteral<"composite">;
181
181
  value: z.ZodObject<{
182
182
  __ref: z.ZodString;
183
183
  }, z.core.$strip>;
184
+ layer: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
185
+ __ref: z.ZodString;
186
+ }, z.core.$strip>]>>;
184
187
  when: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodObject<{
185
188
  __ref: z.ZodString;
186
189
  }, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodObject<{
187
190
  __ref: z.ZodString;
188
191
  }, z.core.$strip>]>>]>>>;
189
- }, z.core.$strict>, z.ZodObject<{
192
+ }, z.core.$strict>, z.ZodTransform<{
193
+ type: "composite";
194
+ value: {
195
+ __ref: string;
196
+ };
197
+ layer?: string | {
198
+ __ref: string;
199
+ } | undefined;
200
+ when?: Record<string, string | number | boolean | {
201
+ __ref: string;
202
+ } | (string | number | boolean | {
203
+ __ref: string;
204
+ })[]> | undefined;
205
+ }, {
206
+ type: "composite";
207
+ value: {
208
+ __ref: string;
209
+ };
210
+ layer?: string | {
211
+ __ref: string;
212
+ } | undefined;
213
+ when?: Record<string, string | number | boolean | {
214
+ __ref: string;
215
+ } | (string | number | boolean | {
216
+ __ref: string;
217
+ })[]> | undefined;
218
+ }>>, z.ZodObject<{
190
219
  type: z.ZodLiteral<"motion">;
191
220
  value: z.ZodDefault<z.ZodObject<{
192
221
  __ref: z.ZodString;
@@ -360,6 +360,14 @@ function routedTarget(decl) {
360
360
  into: decl.target.slice(separator + 1)
361
361
  };
362
362
  }
363
+ /** A composite prop's authored `layer` name as the layer ref it is stored as — see {@link routedTarget}
364
+ * for why the coercion lives in the schema. */
365
+ function layerAsRef(decl) {
366
+ return typeof decl.layer === "string" ? {
367
+ ...decl,
368
+ layer: layerRef(decl.layer)
369
+ } : decl;
370
+ }
363
371
  /** A prop that applies only under a condition on its siblings — the record a style rule's `when`
364
372
  * holds. Open here, where no owner is known; `propSchemaFor` narrows it to the owner's props. */
365
373
  const conditionLeaf = z.union([
@@ -415,8 +423,9 @@ function propShapeWith(when) {
415
423
  z.strictObject({
416
424
  type: z.literal("composite"),
417
425
  value: refSchema,
426
+ layer: z.union([z.string().min(1), refSchema]).optional(),
418
427
  when
419
- }),
428
+ }).transform(layerAsRef),
420
429
  z.strictObject({
421
430
  type: z.literal("motion"),
422
431
  value: refSchema.default({ __ref: "motion:" }),
@@ -107,12 +107,20 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
107
107
  readonly input: z.ZodObject<{
108
108
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
109
109
  data: z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodArray<z.ZodString>]>>]>;
110
+ onLinked: z.ZodOptional<z.ZodEnum<{
111
+ keep: "keep";
112
+ take: "take";
113
+ }>>;
110
114
  }, z.core.$strip>;
111
115
  };
112
116
  readonly 'link-plan': {
113
117
  readonly input: z.ZodObject<{
114
118
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
115
119
  data: z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodArray<z.ZodString>]>>]>;
120
+ onLinked: z.ZodOptional<z.ZodEnum<{
121
+ keep: "keep";
122
+ take: "take";
123
+ }>>;
116
124
  }, z.core.$strip>;
117
125
  };
118
126
  readonly unlink: {
@@ -146,7 +146,10 @@ const linkInput = z.object({
146
146
  path: pathInput("linked-system"),
147
147
  /** What to draw, keyed by accessor the way the pin's `link` is, or `'*'` for everything the source
148
148
  * offers. A path that names a folder in the source covers its members. */
149
- data: z.union([z.literal("*"), z.record(z.string(), copyPaths)])
149
+ data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]),
150
+ /** What to do at a path another linked system provides: `'take'` makes it follow this source,
151
+ * `'keep'` leaves it where it comes from. Required when there is one; `link-plan` lists them. */
152
+ onLinked: z.enum(["keep", "take"]).optional()
150
153
  });
151
154
  const unlinkInput = z.object({
152
155
  path: pathInput("linked-system"),
@@ -173,6 +176,14 @@ function copyInputOf(input) {
173
176
  ...input.onConflict === void 0 ? {} : { onConflict: input.onConflict }
174
177
  };
175
178
  }
179
+ /** The parsed `link` input as the plan takes it. */
180
+ function linkPlanInputOf(input) {
181
+ return {
182
+ path: String(input.path),
183
+ data: input.data,
184
+ ...input.onLinked === void 0 ? {} : { onLinked: input.onLinked }
185
+ };
186
+ }
176
187
  /** A `link` or `unlink` example: one entity the source offers, under the accessor this config files it by. */
177
188
  function linkExample({ config, path }) {
178
189
  const target = exampleBorrowable(config, path);
@@ -243,7 +254,8 @@ const LinkedSystem = defineEntity({
243
254
  * Draw entities from a linked system: the pin's `link` declaration grows to cover them and the
244
255
  * build mints a pointer row per entity, in this one op. A row of this config's own at a covered
245
256
  * path is deleted so theirs shows through; a row held through another pin becomes an extension
246
- * through this one. `link-plan` says what that replaces before anything is written.
257
+ * through this one when `onLinked` is `'take'`. `link-plan` says what that replaces before anything
258
+ * is written.
247
259
  */
248
260
  link: {
249
261
  input: linkInput,
@@ -255,13 +267,14 @@ const LinkedSystem = defineEntity({
255
267
  title: (entity) => `Link something from ${entity}`,
256
268
  example: linkExample,
257
269
  handler: (input, config) => {
258
- const { patches } = planLink({
270
+ const { linked, patches } = planLink({
259
271
  config,
260
- input: {
261
- path: String(input.path),
262
- data: input.data
263
- }
272
+ input: linkPlanInputOf(input)
264
273
  });
274
+ if (linked.length > 0 && input.onLinked === void 0) {
275
+ const named = linked.map((at) => `${at.path} (from ${at.from})`);
276
+ rejectData(`Linking through "${String(input.path)}" reaches what another linked system provides: ${named.join(", ")}. Say whether to take them (\`onLinked: 'take'\`) or keep them where they come from (\`onLinked: 'keep'\`).`, ["onLinked"]);
277
+ }
265
278
  if (patches.length === 0) return config;
266
279
  return config.applyAll(patches).withDerivedRows();
267
280
  }
@@ -270,17 +283,14 @@ const LinkedSystem = defineEntity({
270
283
  input: linkInput,
271
284
  readOnly: true,
272
285
  label: "Plan a link",
273
- description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, rows held through another pin turned into extensions, and members left alone because they are already here from the same origin. Writes nothing.",
286
+ description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), and members left alone because they are already here from the same origin. Writes nothing.",
274
287
  scope: "item",
275
288
  title: (entity) => `Plan a link from ${entity}`,
276
289
  example: linkExample,
277
290
  handler: (input, config) => {
278
291
  const { patches: _patches, ...plan } = planLink({
279
292
  config,
280
- input: {
281
- path: String(input.path),
282
- data: input.data
283
- }
293
+ input: linkPlanInputOf(input)
284
294
  });
285
295
  return plan;
286
296
  }
@@ -413,8 +413,7 @@ function planCopy({ config, input }) {
413
413
  const removal = /* @__PURE__ */ new Map();
414
414
  for (const { kind, at, named } of taking) {
415
415
  if (!named) continue;
416
- const stored = config.storedBody(kind, at);
417
- if (!stored || !isMintedRow(stored)) continue;
416
+ if (config.storedBody(kind, at) === void 0) continue;
418
417
  if (config.resolve(kind, at)?.borrowedFrom !== slug) continue;
419
418
  const accessor = config.accessorOf(kind) ?? kind;
420
419
  if (!covers({
@@ -189,6 +189,9 @@ type PropDecl<Path extends string = string> = SlotDecl | ForwardDecl<Path> | {
189
189
  } | {
190
190
  type: 'composite';
191
191
  value: Ref$1;
192
+ /** The layer the chosen member styles; the root by default. Authored as its name, stored as a
193
+ * layer ref. */
194
+ layer?: string | Ref$1;
192
195
  required?: boolean;
193
196
  when?: PropCondition;
194
197
  } | {
@@ -6,10 +6,21 @@ import { Config, Patch, TreeNode } from "../../framework/Config.js";
6
6
  /** What one op names, keyed by accessor the way the pin's `link` is: every entry of the kind, or the
7
7
  * source's paths, where a folder names its members. `'*'` alone is every kind the source offers. */
8
8
  type LinkPaths = '*' | Readonly<Record<string, '*' | readonly string[]>>;
9
+ /** What a link does at a path held through another linked system: `'take'` makes the row follow this
10
+ * source, `'keep'` leaves it with the system it comes from. */
11
+ type OnLinked = 'keep' | 'take';
9
12
  interface LinkPlanInput {
10
13
  /** The pin's slug. */
11
14
  readonly path: string;
12
15
  readonly data: LinkPaths;
16
+ /** Planned as `'take'` when omitted; the `link` op refuses without it while `linked` is not empty. */
17
+ readonly onLinked?: OnLinked;
18
+ }
19
+ /** A path the link reaches that another linked system provides, or declares under a row of this
20
+ * config's own. */
21
+ interface LinkedElsewhere extends EntityAddress {
22
+ /** The slug of the pin it comes through. */
23
+ readonly from: string;
13
24
  }
14
25
  /** One row a link mints. `named` when a path the call named covers it; otherwise it comes along
15
26
  * because something linked points at it. */
@@ -25,6 +36,8 @@ interface LinkPlan {
25
36
  readonly replaces: number;
26
37
  /** Rows of this config's own deleted so the source's row shows through at the path. */
27
38
  readonly addresses: readonly EntityAddress[];
39
+ /** Every path the link reaches that another linked system provides, whatever `onLinked` says. */
40
+ readonly linked: readonly LinkedElsewhere[];
28
41
  /** Rows held through another pin, turned into extensions through this one. */
29
42
  readonly extended: readonly EntityAddress[];
30
43
  /** Members already here from the source's own origin, left alone and kept out of the declaration. */
@@ -85,6 +98,11 @@ interface SourceRowState {
85
98
  readonly linked: boolean;
86
99
  /** Rows of this config's own a `link` would write over. */
87
100
  readonly replaces: number;
101
+ /** Members another linked system provides or also declares, which a `link` asks about through
102
+ * `onLinked`. */
103
+ readonly linkedElsewhere: number;
104
+ /** The slugs of the pins those come through. */
105
+ readonly linkedFrom: readonly string[];
88
106
  /** Members already here from the source's own origin, through another pin. */
89
107
  readonly shared: number;
90
108
  /** That origin's name, when `shared` is above zero. */
@@ -97,10 +115,11 @@ interface SourceRowState {
97
115
  * The declaration goes first. A row of this config's own that other rows point at can only be deleted
98
116
  * once the declaration covers the path, since the config refuses a delete that leaves a referrer
99
117
  * dangling. A row held through another pin is not deleted — that pin's declaration would mint it
100
- * again, and declaring one path on two pins is a collision nothing derives — so it becomes an
101
- * extension through this pin: a row of this config's own that shadows the other pin's and follows this
102
- * source. A member already here from the source's own origin is left alone and kept out of the
103
- * declaration, so a folder that holds one is declared by its other members.
118
+ * again, and declaring one path on two pins is a collision nothing derives. With `onLinked: 'take'` it
119
+ * becomes an extension through this pin: a row of this config's own that shadows the other pin's and
120
+ * follows this source. With `'keep'` it stays with the other pin and out of the declaration. A member
121
+ * already here from the source's own origin is left alone and kept out of the declaration, so a
122
+ * folder that holds one is declared by its other members.
104
123
  */
105
124
  declare function planLink({
106
125
  config,
@@ -159,4 +178,4 @@ declare function sourceRowStates({
159
178
  readonly slug: string;
160
179
  }): SourceRowState[];
161
180
  //#endregion
162
- export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
181
+ export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
@@ -42,6 +42,20 @@ function sharedWithOrigin({ config, source, slug, kind, path }) {
42
42
  if (!theirs || !mine || theirs.key !== mine.key) return void 0;
43
43
  return theirs.pin.displayName ?? theirs.key;
44
44
  }
45
+ /** Another pin whose declaration covers the path and whose source has it: the pin the path would
46
+ * derive from if this config held nothing there. */
47
+ function declaredElsewhere({ config, slug, kind, path }) {
48
+ const accessor = config.accessorOf(kind) ?? kind;
49
+ for (const pin of config.list(PIN_KIND)) {
50
+ if (pin.path === slug) continue;
51
+ if (!covers({
52
+ declared: declarationOf(config, pin.path),
53
+ accessor,
54
+ path
55
+ })) continue;
56
+ if (config.sourceFor(pin.path)?.resolve(kind, path)) return pin.path;
57
+ }
58
+ }
45
59
  /**
46
60
  * Link paths of a source: the declaration that mints them, then the edits that let them show through
47
61
  * where this config already holds something.
@@ -49,10 +63,11 @@ function sharedWithOrigin({ config, source, slug, kind, path }) {
49
63
  * The declaration goes first. A row of this config's own that other rows point at can only be deleted
50
64
  * once the declaration covers the path, since the config refuses a delete that leaves a referrer
51
65
  * dangling. A row held through another pin is not deleted — that pin's declaration would mint it
52
- * again, and declaring one path on two pins is a collision nothing derives — so it becomes an
53
- * extension through this pin: a row of this config's own that shadows the other pin's and follows this
54
- * source. A member already here from the source's own origin is left alone and kept out of the
55
- * declaration, so a folder that holds one is declared by its other members.
66
+ * again, and declaring one path on two pins is a collision nothing derives. With `onLinked: 'take'` it
67
+ * becomes an extension through this pin: a row of this config's own that shadows the other pin's and
68
+ * follows this source. With `'keep'` it stays with the other pin and out of the declaration. A member
69
+ * already here from the source's own origin is left alone and kept out of the declaration, so a
70
+ * folder that holds one is declared by its other members.
56
71
  */
57
72
  function planLink({ config, input }) {
58
73
  const { path: slug } = input;
@@ -66,12 +81,13 @@ function planLink({ config, input }) {
66
81
  verb: "link"
67
82
  });
68
83
  const addresses = [];
84
+ const linked = [];
69
85
  const extended = [];
70
86
  const shared = [];
71
87
  const shadows = [];
72
88
  /** Per accessor, what the declaration gains: the kind whole, or paths. */
73
89
  const gains = /* @__PURE__ */ new Map();
74
- let sharedAnywhere = false;
90
+ let leftAnywhere = false;
75
91
  const shadow = (kind, path) => {
76
92
  const held = config.resolve(kind, path);
77
93
  if (!held || held.borrowedFrom === slug) return true;
@@ -86,10 +102,16 @@ function planLink({ config, input }) {
86
102
  kind,
87
103
  path
88
104
  });
89
- sharedAnywhere = true;
105
+ leftAnywhere = true;
90
106
  return false;
91
107
  }
92
- if (held.borrowedFrom === void 0) {
108
+ const from = held.borrowedFrom ?? declaredElsewhere({
109
+ config,
110
+ slug,
111
+ kind,
112
+ path
113
+ });
114
+ if (from === void 0) {
93
115
  addresses.push({
94
116
  kind,
95
117
  path
@@ -101,6 +123,20 @@ function planLink({ config, input }) {
101
123
  });
102
124
  return true;
103
125
  }
126
+ linked.push({
127
+ kind,
128
+ path,
129
+ from
130
+ });
131
+ if (input.onLinked === "keep") {
132
+ if (held.borrowedFrom === void 0) shadows.push({
133
+ kind,
134
+ operation: "delete",
135
+ path
136
+ });
137
+ leftAnywhere = true;
138
+ return false;
139
+ }
104
140
  const stored = config.storedBody(kind, path);
105
141
  const minted = stored !== void 0 && isMintedRow(stored);
106
142
  extended.push({
@@ -145,7 +181,7 @@ function planLink({ config, input }) {
145
181
  }
146
182
  const declares = [];
147
183
  let declared = current;
148
- if (current !== "*") if (input.data === "*" && !sharedAnywhere) {
184
+ if (current !== "*") if (input.data === "*" && !leftAnywhere) {
149
185
  declared = "*";
150
186
  declares.push({
151
187
  kind: PIN_KIND,
@@ -192,6 +228,7 @@ function planLink({ config, input }) {
192
228
  }),
193
229
  replaces: addresses.length,
194
230
  addresses,
231
+ linked,
195
232
  extended,
196
233
  shared,
197
234
  patches
@@ -485,7 +522,7 @@ function planBreak({ config, input }) {
485
522
  const pointing = /* @__PURE__ */ new Map();
486
523
  /** Folders drawn through the pin by a pointer of this config's own, released once their members are copied. */
487
524
  const pointedFolders = [];
488
- const reach = ({ kind, path, reach }) => {
525
+ const reach = ({ kind, path, reach: how }) => {
489
526
  const held = config.resolve(kind, path);
490
527
  if (held?.borrowedFrom === slug) {
491
528
  copying.path(kind, path);
@@ -495,7 +532,7 @@ function planBreak({ config, input }) {
495
532
  folder: false,
496
533
  refs: [ref(kind, path, slug).__ref],
497
534
  outcome: "copied",
498
- reach
535
+ reach: how
499
536
  });
500
537
  return;
501
538
  }
@@ -516,9 +553,15 @@ function planBreak({ config, input }) {
516
553
  folder: true,
517
554
  refs: [ref(kind, path, slug).__ref],
518
555
  outcome: "copied",
519
- reach
556
+ reach: how
520
557
  });
558
+ return;
521
559
  }
560
+ for (const member of config.members(kind, path)) if (member.borrowedFrom === slug) reach({
561
+ kind,
562
+ path: member.path,
563
+ reach: "taken"
564
+ });
522
565
  return;
523
566
  }
524
567
  const refs = drawnThroughPin({
@@ -534,7 +577,7 @@ function planBreak({ config, input }) {
534
577
  }
535
578
  pointing.set(`${kind}:${path}`, {
536
579
  refs,
537
- reach
580
+ reach: how
538
581
  });
539
582
  };
540
583
  if (scoped) for (const { kind, accessor, paths } of wantedKinds({
@@ -730,6 +773,8 @@ function sourceRowStates({ config, slug }) {
730
773
  if (members.length === 0) continue;
731
774
  let linked = 0;
732
775
  let local = 0;
776
+ let elsewhere = 0;
777
+ const linkedFrom = /* @__PURE__ */ new Set();
733
778
  let shared = 0;
734
779
  let sharedOrigin;
735
780
  for (const leaf of members) {
@@ -739,7 +784,6 @@ function sourceRowStates({ config, slug }) {
739
784
  linked += 1;
740
785
  continue;
741
786
  }
742
- local += 1;
743
787
  const origin = sharedWithOrigin({
744
788
  config: derived,
745
789
  source,
@@ -747,10 +791,19 @@ function sourceRowStates({ config, slug }) {
747
791
  kind,
748
792
  path: leaf.path
749
793
  });
794
+ const from = origin === void 0 ? held.borrowedFrom ?? declaredElsewhere({
795
+ config: derived,
796
+ slug,
797
+ kind,
798
+ path: leaf.path
799
+ }) : void 0;
750
800
  if (origin !== void 0) {
751
801
  shared += 1;
752
802
  sharedOrigin ??= origin;
753
- }
803
+ } else if (from !== void 0) {
804
+ elsewhere += 1;
805
+ linkedFrom.add(from);
806
+ } else local += 1;
754
807
  }
755
808
  rows.push({
756
809
  kind,
@@ -760,7 +813,9 @@ function sourceRowStates({ config, slug }) {
760
813
  members: members.length,
761
814
  held: linked,
762
815
  linked: linked === members.length,
763
- replaces: local - shared,
816
+ replaces: local,
817
+ linkedElsewhere: elsewhere,
818
+ linkedFrom: [...linkedFrom],
764
819
  shared,
765
820
  ...sharedOrigin === void 0 ? {} : { sharedOrigin }
766
821
  });
@@ -72,7 +72,7 @@ interface RecordedSource {
72
72
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
73
73
  * that replaced it (see `detectedWireVersion`).
74
74
  */
75
- declare const SERIALIZED_CONFIG_VERSION = 20260925143319;
75
+ declare const SERIALIZED_CONFIG_VERSION = 20260928215509;
76
76
  interface SerializedConfig {
77
77
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
78
78
  readonly version: number;
@@ -5135,7 +5135,7 @@ var Config = class Config {
5135
5135
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5136
5136
  hydrateFrom(stored, options) {
5137
5137
  const claimed = schemaVersionOf(stored);
5138
- if (claimed > 20260925143319) throw new SchemaVersionTooNew(claimed);
5138
+ if (claimed > 20260928215509) throw new SchemaVersionTooNew(claimed);
5139
5139
  const wire = upgradeSerializedConfig(stored);
5140
5140
  if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating, or port the repo once with \`uds migrate\`.`);
5141
5141
  const leftover = wire.options;
@@ -84,6 +84,9 @@ interface ComponentRuleClass {
84
84
  * emitted module tests it with `=== true`. */
85
85
  readonly when: Record<string, unknown>;
86
86
  readonly className: string;
87
+ /** Set when the class comes from a composite PROP rather than a style rule: the member whose bag
88
+ * the class paints. See {@link compositePropRuleClasses}. */
89
+ readonly composite?: string;
87
90
  }
88
91
  /**
89
92
  * Every conditional class a component's style rules produce.
@@ -137,23 +140,59 @@ declare function componentMotionClasses(config: Config, path: string): Component
137
140
  * defaults, because a value the caller didn't set has to read the default — the same rule
138
141
  * {@link layerStyles} follows.
139
142
  *
140
- * Keyed by layer even though today it only ever names the ROOT one, so a caller needs no opinion
141
- * about which layer that is: an instance's own props land where its own props land.
143
+ * Keyed by layer: a prop lands on the layer it names (the root by default), or on the layer that
144
+ * forwards it. Each member contributes two classes, the composite's own and the component's rule
145
+ * class for it; {@link componentRuleClasses} lists the second too, for the module and the sheet, so
146
+ * a projection reading both skips its `composite` entries.
142
147
  */
143
148
  declare function componentPropClasses(config: Config, path: string, state?: Record<string, unknown>): Record<string, string[]>;
149
+ /** One composite an instance's props pick, and where it lands. */
150
+ interface PickedComposite {
151
+ readonly prop: string;
152
+ readonly layer: string;
153
+ /** The member's path (`textVariant/title1/emphasized`). */
154
+ readonly member: string;
155
+ /** The value as a caller writes it, the member's path under its group (`title1/emphasized`). */
156
+ readonly value: string;
157
+ }
158
+ /**
159
+ * The composites an instance's props pick, resolved over the component's defaults: each one beats
160
+ * the component's own rules on its layer (see {@link compositePropRuleClasses}).
161
+ *
162
+ * A projection that paints a component's rule declarations as props, as the component editor's cells
163
+ * do, needs this to hold that precedence: a rule's `fontSize` painted as a prop would otherwise beat
164
+ * the member a caller picked, where the stylesheet has the member win.
165
+ */
166
+ declare function componentPropComposites(config: Config, path: string, state?: Record<string, unknown>): PickedComposite[];
167
+ /**
168
+ * A style rule's bag with each composite it names written out in place: the member's styles first,
169
+ * the bag's own keys over them, and the composite's marker (`--<prefix>-composite-<group>`), so
170
+ * `_on<Group><Member>` still finds it.
171
+ *
172
+ * A composite a component's OWN rule names is part of that layer's defaults, the same as a
173
+ * `fontSize` beside it: the layer's element beats it with its variants, and a key the bag spells
174
+ * next to it overrides it (`{ textVariant: 'ui6', lineHeight: 'tight' }`). Forwarded to the element
175
+ * as a prop instead, it arrived as a caller's pick, and a caller's pick beats the element's
176
+ * variants (see {@link compositePropRuleClasses}), so FormInput's own `ui6` beat the `size="xs"`
177
+ * it renders its Input at. Only a composite passed as a prop at a call site takes that route.
178
+ *
179
+ * `element` is the layer's element. A key is a composite when the element declares it as one, so a
180
+ * plain-tag layer (which declares nothing) and a key naming no member are left as they are.
181
+ */
182
+ declare function inlineRuleComposites(config: Config, element: unknown, bag: Record<string, unknown>): Record<string, unknown>;
144
183
  /**
145
184
  * The props a style rule's bag sends to each layer's ELEMENT rather than to the stylesheet, for one
146
185
  * instance. The same split the emitted module makes when it builds a layer's routed bundle.
147
186
  *
148
187
  * What makes a bag key CSS is that a style property claims it: that value lives in the stylesheet
149
188
  * under the rule's condition class. Everything else in the bag is a prop of the layer's element and
150
- * travels to it as one, whether or not the element declares it: a composite (`variant` on a
151
- * `Text`-backed layer, which the receiver resolves to its own class), the receiver's own variant or
189
+ * travels to it as one, whether or not the element declares it: the receiver's own variant or
152
190
  * scalar prop (`size: 'sm'` on an Icon layer), or an attribute of a plain element (`as: 'div'`,
153
191
  * `type: 'button'`, `aria-hidden`), or a value the rule hands a receiver's forward or slot
154
192
  * (`children: 'AI'` on a badge layer, `icon: 'XIcon'` on a button layer), which the emitted module
155
- * routes the same way. A style property under another name and motion stay out, since the
156
- * stylesheet and the motion classes already carry them.
193
+ * routes the same way. A style property under another name, a composite (written into the rule's
194
+ * declarations, {@link inlineRuleComposites}) and motion stay out, since the stylesheet and the
195
+ * motion classes already carry them.
157
196
  *
158
197
  * Forwarding rather than resolving is what makes a layer's styling live-editable at all: a class only
159
198
  * exists once a build has emitted the stylesheet, so an edit that makes a NEW style rule paints
@@ -258,4 +297,4 @@ declare function stylePropClasses(config: Config, path: string): Map<string, str
258
297
  */
259
298
  declare function modifierUtilitiesUsed(config: Config): Set<string>;
260
299
  //#endregion
261
- export { ComponentRuleClass, SplitStyleProps, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, stylePropClassBase, stylePropClassName, stylePropClasses, styleRuleMotionClasses };
300
+ export { ComponentRuleClass, PickedComposite, SplitStyleProps, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, stylePropClassBase, stylePropClassName, stylePropClasses, styleRuleMotionClasses };