@yahoo/uds-create-config 4.37.0 → 4.37.2

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.
@@ -43,6 +43,7 @@ import { globalStyleDerivedColorsMigration } from "../migrations/20260923163604_
43
43
  import { slotCodeFormsMigration } from "../migrations/20260924044539_slot_code_forms.js";
44
44
  import { canvasRoleSlotMigration } from "../migrations/20260925143319_canvas_role_slot.js";
45
45
  import { linkOnLinkedMigration } from "../migrations/20260928180801_link_on_linked.js";
46
+ import { compositePropLayerMigration } from "../migrations/20260928215509_composite_prop_layer.js";
46
47
  const System = defineConfig({
47
48
  name: WEB_CONFIG_NAME,
48
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.",
@@ -97,5 +98,6 @@ if (!registeredSchemaMigrations().has(20260923163604)) registerSchemaMigrations(
97
98
  if (!registeredSchemaMigrations().has(20260924044539)) registerSchemaMigrations(slotCodeFormsMigration);
98
99
  if (!registeredSchemaMigrations().has(20260925143319)) registerSchemaMigrations(canvasRoleSlotMigration);
99
100
  if (!registeredSchemaMigrations().has(20260928180801)) registerSchemaMigrations(linkOnLinkedMigration);
101
+ if (!registeredSchemaMigrations().has(20260928215509)) registerSchemaMigrations(compositePropLayerMigration);
100
102
  //#endregion
101
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:" }),
@@ -15,9 +15,14 @@ interface ComponentModuleSource {
15
15
  * authored name is `Component.export`, and the two differ for a grouped registration under
16
16
  * another leaf and for a draft rename the source file has not caught up with. */
17
17
  readonly exportName: string;
18
- /** The ctx-shaped registry export emitted beside the component, `<leaf>Entry`. */
19
- readonly entryExportName: string;
20
18
  }
19
+ /**
20
+ * The static an emitted module stamps a component's registry-entry facts on: what
21
+ * `makeComponentEntry` takes beyond the component (`childless`, `computed`). A static and not an
22
+ * export, because React Fast Refresh treats a module exporting anything but components as a
23
+ * full-reload boundary — an `__udsEntries` export made every component edit reload the page.
24
+ */
25
+ declare const COMPONENT_ENTRY_STATIC = "__udsEntry";
21
26
  /** One component export an authored import can resolve through the runtime registry. */
22
27
  interface ComponentRegistryImportBinding {
23
28
  readonly componentRef: string;
@@ -63,4 +68,4 @@ declare function componentModuleKey(config: Config, filePath: string): string |
63
68
  */
64
69
  declare function componentModuleSource(config: Config, componentPathOrRef: string | Ref): ComponentModuleSource | undefined;
65
70
  //#endregion
66
- export { ComponentModuleSource, ComponentRegistryImport, ComponentRegistryImportBinding, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports };
71
+ export { COMPONENT_ENTRY_STATIC, ComponentModuleSource, ComponentRegistryImport, ComponentRegistryImportBinding, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports };
@@ -28,9 +28,14 @@ const WEB_RUNTIME_KINDS = [
28
28
  "motion",
29
29
  "composite"
30
30
  ];
31
- /** The name an emitted module exports a component under: its path's leaf. Codegen's
32
- * `componentEntryExportName` is this leaf plus `Entry`; the config package cannot import codegen, so
33
- * the leaf is computed here as well. */
31
+ /**
32
+ * The static an emitted module stamps a component's registry-entry facts on: what
33
+ * `makeComponentEntry` takes beyond the component (`childless`, `computed`). A static and not an
34
+ * export, because React Fast Refresh treats a module exporting anything but components as a
35
+ * full-reload boundary — an `__udsEntries` export made every component edit reload the page.
36
+ */
37
+ const COMPONENT_ENTRY_STATIC = "__udsEntry";
38
+ /** The name an emitted module exports a component under: its path's leaf. */
34
39
  function componentModuleExportName(path) {
35
40
  return path.split("/").pop() ?? path;
36
41
  }
@@ -258,9 +263,8 @@ function componentModuleSource(config, componentPathOrRef) {
258
263
  componentRef: Component.ref(component.path).__ref,
259
264
  filePath: ref.path,
260
265
  moduleKey,
261
- exportName: forward,
262
- entryExportName: `${forward}Entry`
266
+ exportName: forward
263
267
  };
264
268
  }
265
269
  //#endregion
266
- export { componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports };
270
+ export { COMPONENT_ENTRY_STATIC, componentModuleFilePaths, componentModuleKey, componentModuleSource, componentRegistryImports };
@@ -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
  } | {
@@ -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 = 20260928180801;
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 > 20260928180801) 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 };
@@ -1,7 +1,7 @@
1
1
  import { isRef, kindOf, memberOf, pathOf, refLeaf } from "./utils/refs.js";
2
2
  import { forwardsOf, rootLayerOf } from "../entities/system/element.js";
3
3
  import { createSliceMemo } from "./memo.js";
4
- import { classPrefixOf } from "../entities/system/Settings.js";
4
+ import { classPrefixOf, varPrefixOf } from "../entities/system/Settings.js";
5
5
  import { storedPreview } from "./preview-store.js";
6
6
  import { authoredValue, className, negatedCssValue, renderStyleValue, resolveComponentProps, stylePropLeafForToken, stylePropValueLeaves, stylePropertyPathFor } from "./projections.js";
7
7
  import { ruleApplies } from "./layer-styles.js";
@@ -156,7 +156,11 @@ function componentRuleClasses(config, path) {
156
156
  * that slice is part of the key.
157
157
  */
158
158
  const ruleClassCache = createSliceMemo({
159
- kinds: ["component", "settings"],
159
+ kinds: [
160
+ "component",
161
+ "composite",
162
+ "settings"
163
+ ],
160
164
  compute: (_config) => /* @__PURE__ */ new Map()
161
165
  });
162
166
  function computeRuleClasses(config, path) {
@@ -178,6 +182,44 @@ function computeRuleClasses(config, path) {
178
182
  });
179
183
  }
180
184
  }
185
+ out.push(...compositePropRuleClasses(config, path));
186
+ return out;
187
+ }
188
+ /**
189
+ * The classes a component's composite PROPS apply: one per member of the bound group, conditional on
190
+ * the prop naming that member — `variant: 'body1'` → `uds-text-root-variant-body1` — on the layer the
191
+ * prop names (the root by default).
192
+ *
193
+ * A composite a caller picks is an explicit choice, not a default the layer starts from, so it has to
194
+ * win over the component's own type. The shared composite class can't: it is emitted before every
195
+ * component rule at the same zero specificity, so a layer that sets `fontSize` itself kept its size
196
+ * whatever preset was asked for (UDS-4610). As a rule class, the member's bag is emitted after the
197
+ * component's `when` rules at their specificity, so it beats the layer's base rules and its variants,
198
+ * and a style-prop utility — a later cascade layer — still beats it.
199
+ *
200
+ * Both kinds {@link compositePropsOf} reads: a prop the component declares (an extension a linking
201
+ * config adds is part of the resolved body), and one it exposes from a component layer through
202
+ * `forwards` — its module takes that value out of the bag, so the receiver never sees it, and the
203
+ * class has to be this component's.
204
+ */
205
+ function compositePropRuleClasses(config, path) {
206
+ const declared = compositePropsOf(config, path);
207
+ if (!declared) return [];
208
+ const base = componentClassBase(config, path);
209
+ const out = [];
210
+ for (const [prop, groupRef, layer] of declared.groups) {
211
+ const group = pathOf(groupRef);
212
+ for (const composite of config.list("composite")) {
213
+ if (!composite.path.startsWith(`${group}/`)) continue;
214
+ const when = { [prop]: composite.path.slice(group.length + 1) };
215
+ out.push({
216
+ layer,
217
+ when,
218
+ className: `${base}-${layer}-${ruleCondition(when)}`,
219
+ composite: composite.path
220
+ });
221
+ }
222
+ }
181
223
  return out;
182
224
  }
183
225
  /** The MEMBER a bag value names, under the group its prop binds — the value is either the qualified
@@ -290,27 +332,46 @@ function componentMotionClasses(config, path) {
290
332
  * defaults, because a value the caller didn't set has to read the default — the same rule
291
333
  * {@link layerStyles} follows.
292
334
  *
293
- * Keyed by layer even though today it only ever names the ROOT one, so a caller needs no opinion
294
- * about which layer that is: an instance's own props land where its own props land.
335
+ * Keyed by layer: a prop lands on the layer it names (the root by default), or on the layer that
336
+ * forwards it. Each member contributes two classes, the composite's own and the component's rule
337
+ * class for it; {@link componentRuleClasses} lists the second too, for the module and the sheet, so
338
+ * a projection reading both skips its `composite` entries.
295
339
  */
296
340
  function componentPropClasses(config, path, state = {}) {
341
+ const out = {};
342
+ for (const { prop, layer, member, value } of componentPropComposites(config, path, state)) {
343
+ const classes = out[layer] ?? [];
344
+ classes.push(className(config, "composite", member), `${componentClassBase(config, path)}-${layer}-${ruleCondition({ [prop]: value })}`);
345
+ out[layer] = classes;
346
+ }
347
+ return out;
348
+ }
349
+ /**
350
+ * The composites an instance's props pick, resolved over the component's defaults: each one beats
351
+ * the component's own rules on its layer (see {@link compositePropRuleClasses}).
352
+ *
353
+ * A projection that paints a component's rule declarations as props, as the component editor's cells
354
+ * do, needs this to hold that precedence: a rule's `fontSize` painted as a prop would otherwise beat
355
+ * the member a caller picked, where the stylesheet has the member win.
356
+ */
357
+ function componentPropComposites(config, path, state = {}) {
297
358
  const declared = compositePropsOf(config, path);
298
- if (!declared) return {};
299
- const { groups, defaultProps } = declared;
300
- if (groups.length === 0) return {};
359
+ if (!declared || declared.groups.length === 0) return [];
301
360
  const resolved = {
302
- ...defaultProps,
361
+ ...declared.defaultProps,
303
362
  ...state
304
363
  };
305
- const out = {};
306
- for (const [prop, group, layer] of groups) {
307
- const value = resolved[prop];
308
- const target = memberNamed(group, value);
309
- if (target === void 0) continue;
310
- if (!config.resolve("composite", target)) continue;
311
- const classes = out[layer] ?? [];
312
- classes.push(className(config, "composite", target));
313
- out[layer] = classes;
364
+ const out = [];
365
+ for (const [prop, group, layer] of declared.groups) {
366
+ const member = memberNamed(group, resolved[prop]);
367
+ if (member === void 0) continue;
368
+ if (!config.resolve("composite", member)) continue;
369
+ out.push({
370
+ prop,
371
+ layer,
372
+ member,
373
+ value: member.slice(pathOf(group).length + 1)
374
+ });
314
375
  }
315
376
  return out;
316
377
  }
@@ -329,11 +390,13 @@ function compositePropsOf(config, path, seen = /* @__PURE__ */ new Set()) {
329
390
  let defaultProps = {};
330
391
  for (const [prop, decl] of Object.entries(props)) {
331
392
  if (decl?.type !== "composite") continue;
393
+ const named = isRef(decl.layer) ? pathOf(decl.layer.__ref) : decl.layer;
394
+ const layer = typeof named === "string" && named in layers ? named : root;
332
395
  const group = isRef(decl.value) ? decl.value.__ref : void 0;
333
396
  if (group) groups.push([
334
397
  prop,
335
398
  group,
336
- root
399
+ layer
337
400
  ]);
338
401
  }
339
402
  const inner = new Set([...seen, path]);
@@ -378,18 +441,68 @@ const compositePropsCache = createSliceMemo({
378
441
  compute: (_config) => /* @__PURE__ */ new Map()
379
442
  });
380
443
  /**
444
+ * A style rule's bag with each composite it names written out in place: the member's styles first,
445
+ * the bag's own keys over them, and the composite's marker (`--<prefix>-composite-<group>`), so
446
+ * `_on<Group><Member>` still finds it.
447
+ *
448
+ * A composite a component's OWN rule names is part of that layer's defaults, the same as a
449
+ * `fontSize` beside it: the layer's element beats it with its variants, and a key the bag spells
450
+ * next to it overrides it (`{ textVariant: 'ui6', lineHeight: 'tight' }`). Forwarded to the element
451
+ * as a prop instead, it arrived as a caller's pick, and a caller's pick beats the element's
452
+ * variants (see {@link compositePropRuleClasses}), so FormInput's own `ui6` beat the `size="xs"`
453
+ * it renders its Input at. Only a composite passed as a prop at a call site takes that route.
454
+ *
455
+ * `element` is the layer's element. A key is a composite when the element declares it as one, so a
456
+ * plain-tag layer (which declares nothing) and a key naming no member are left as they are.
457
+ */
458
+ function inlineRuleComposites(config, element, bag) {
459
+ if (!isRef(element)) return bag;
460
+ const declared = resolveComponentProps(config, pathOf(element.__ref));
461
+ let inlined;
462
+ const rest = {};
463
+ for (const [key, value] of Object.entries(bag)) {
464
+ const bound = declared[key]?.type === "composite" ? declared[key].value : void 0;
465
+ const member = isRef(bound) ? memberNamed(bound.__ref, value) : void 0;
466
+ const composite = member ? config.resolve("composite", member) : void 0;
467
+ if (!member || !composite) {
468
+ rest[key] = value;
469
+ continue;
470
+ }
471
+ const { styles = {} } = composite.toJSON();
472
+ const at = member.indexOf("/");
473
+ inlined = mergeStyleBags(inlined ?? {}, {
474
+ ...styles,
475
+ [`--${varPrefixOf(config)}-composite-${member.slice(0, at)}`]: member.slice(at + 1)
476
+ });
477
+ }
478
+ return inlined ? mergeStyleBags(inlined, rest) : bag;
479
+ }
480
+ /** `over` laid on `base`, with a modifier block (`_md`, `_hover`) merged key by key rather than
481
+ * replaced, so a bag that adds one breakpoint value keeps the composite's others. */
482
+ function mergeStyleBags(base, over) {
483
+ const out = { ...base };
484
+ for (const [key, value] of Object.entries(over)) {
485
+ const prior = out[key];
486
+ out[key] = key.startsWith("_") && isPlainBlock(prior) && isPlainBlock(value) ? mergeStyleBags(prior, value) : value;
487
+ }
488
+ return out;
489
+ }
490
+ function isPlainBlock(value) {
491
+ return typeof value === "object" && value !== null && !isRef(value);
492
+ }
493
+ /**
381
494
  * The props a style rule's bag sends to each layer's ELEMENT rather than to the stylesheet, for one
382
495
  * instance. The same split the emitted module makes when it builds a layer's routed bundle.
383
496
  *
384
497
  * What makes a bag key CSS is that a style property claims it: that value lives in the stylesheet
385
498
  * under the rule's condition class. Everything else in the bag is a prop of the layer's element and
386
- * travels to it as one, whether or not the element declares it: a composite (`variant` on a
387
- * `Text`-backed layer, which the receiver resolves to its own class), the receiver's own variant or
499
+ * travels to it as one, whether or not the element declares it: the receiver's own variant or
388
500
  * scalar prop (`size: 'sm'` on an Icon layer), or an attribute of a plain element (`as: 'div'`,
389
501
  * `type: 'button'`, `aria-hidden`), or a value the rule hands a receiver's forward or slot
390
502
  * (`children: 'AI'` on a badge layer, `icon: 'XIcon'` on a button layer), which the emitted module
391
- * routes the same way. A style property under another name and motion stay out, since the
392
- * stylesheet and the motion classes already carry them.
503
+ * routes the same way. A style property under another name, a composite (written into the rule's
504
+ * declarations, {@link inlineRuleComposites}) and motion stay out, since the stylesheet and the
505
+ * motion classes already carry them.
393
506
  *
394
507
  * Forwarding rather than resolving is what makes a layer's styling live-editable at all: a class only
395
508
  * exists once a build has emitted the stylesheet, so an edit that makes a NEW style rule paints
@@ -416,7 +529,7 @@ function layerRoutedProps(config, path, state = {}) {
416
529
  }) ?? prop;
417
530
  if (config.resolve("style-property", styleKey)) continue;
418
531
  const decl = declared[prop];
419
- if (decl?.type === "styleProperty" || decl?.type === "motion") continue;
532
+ if (decl?.type === "styleProperty" || decl?.type === "motion" || decl?.type === "composite") continue;
420
533
  out[layer] ??= {};
421
534
  out[layer][prop] = authoredValue(config, prop, value);
422
535
  }
@@ -715,4 +828,4 @@ function classesUsedBy(config, stored, token, bagsOf) {
715
828
  return found;
716
829
  }
717
830
  //#endregion
718
- export { componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, stylePropClassBase, stylePropClassName, stylePropClasses, styleRuleMotionClasses };
831
+ export { 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 };
@@ -1078,6 +1078,7 @@ function classesFor(config, path, layer, state, extra) {
1078
1078
  parts.push(...styleRuleMotionClasses(config, path, state)[layer] ?? []);
1079
1079
  for (const rule of componentRuleClasses(config, path)) {
1080
1080
  if (rule.layer !== layer) continue;
1081
+ if (rule.composite) continue;
1081
1082
  if (Object.entries(rule.when).every(([prop, value]) => String(state[prop]) === String(value))) parts.push(rule.className);
1082
1083
  }
1083
1084
  parts.push(...extra);
@@ -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 = 20260928180801;
18
+ declare const CURRENT_SCHEMA_VERSION = 20260928215509;
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 = 20260928180801;
16
+ const CURRENT_SCHEMA_VERSION = 20260928215509;
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 > 20260928180801) 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 > 20260928215509) 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 ?? 20260928180801;
105
+ const target = options?.target ?? 20260928215509;
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 ?? 20260928180801;
163
+ const target = options?.target ?? 20260928215509;
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 ?? 20260928180801;
182
+ const target = options?.target ?? 20260928215509;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];