@yahoo/uds-create-config 4.54.0 → 4.55.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 (63) hide show
  1. package/dist/capture/class-grammar.js +2 -3
  2. package/dist/configs/platform.d.ts +8 -1
  3. package/dist/configs/react-native-system.d.ts +21 -9
  4. package/dist/configs/react-native-system.js +1 -1
  5. package/dist/configs/system.js +3 -1
  6. package/dist/css/value-type.js +24 -0
  7. package/dist/entities/documentation/Documentation.d.ts +1 -1
  8. package/dist/entities/native/NativeStyleProperty.d.ts +14 -9
  9. package/dist/entities/native/NativeStyleProperty.js +23 -2
  10. package/dist/entities/native/NativeToken.d.ts +10 -3
  11. package/dist/entities/native/NativeToken.js +39 -4
  12. package/dist/entities/system/Component.js +2 -1
  13. package/dist/entities/system/GlobalStyle.d.ts +2 -2
  14. package/dist/entities/system/GlobalStyle.js +2 -1
  15. package/dist/entities/system/Modifier.d.ts +3 -1
  16. package/dist/entities/system/Modifier.js +3 -1
  17. package/dist/entities/system/Settings.d.ts +9 -1
  18. package/dist/entities/system/Settings.js +6 -2
  19. package/dist/entities/system/StyleProperty.d.ts +39 -3
  20. package/dist/entities/system/StyleProperty.js +141 -18
  21. package/dist/entities/system/Token.d.ts +14 -1
  22. package/dist/entities/system/Token.js +21 -3
  23. package/dist/entities/system/calc.d.ts +28 -0
  24. package/dist/entities/system/calc.js +121 -0
  25. package/dist/entities/system/color.d.ts +29 -4
  26. package/dist/entities/system/color.js +61 -8
  27. package/dist/entities/system/component-module.js +15 -4
  28. package/dist/entities/system/composed-values.d.ts +3 -2
  29. package/dist/entities/system/composed-values.js +20 -1
  30. package/dist/entities/system/defineComponent.d.ts +3 -3
  31. package/dist/entities/system/link-plan.js +10 -1
  32. package/dist/entities/system/linked-token-values.d.ts +34 -0
  33. package/dist/entities/system/linked-token-values.js +135 -0
  34. package/dist/entities/system/shadow.d.ts +9 -7
  35. package/dist/entities/system/shadow.js +8 -7
  36. package/dist/entities/system/style-bag.d.ts +1 -1
  37. package/dist/entities/system/token-index.js +45 -2
  38. package/dist/entities/system/token-values.d.ts +3 -2
  39. package/dist/entities/system/token-values.js +9 -9
  40. package/dist/framework/Config.d.ts +14 -18
  41. package/dist/framework/Config.js +105 -55
  42. package/dist/framework/class-names.d.ts +47 -7
  43. package/dist/framework/class-names.js +115 -14
  44. package/dist/framework/defineEntity.d.ts +3 -1
  45. package/dist/framework/defineEntity.js +3 -1
  46. package/dist/framework/inline-sources.d.ts +3 -3
  47. package/dist/framework/inline-sources.js +1 -1
  48. package/dist/framework/projections.d.ts +84 -3
  49. package/dist/framework/projections.js +315 -20
  50. package/dist/framework/ref-graph.js +1 -1
  51. package/dist/framework/registry.d.ts +7 -0
  52. package/dist/framework/registry.js +2 -1
  53. package/dist/framework/schema-version.d.ts +1 -1
  54. package/dist/framework/schema-version.js +5 -5
  55. package/dist/framework/utils/group.d.ts +2 -1
  56. package/dist/framework/utils/refs.d.ts +19 -2
  57. package/dist/framework/utils/refs.js +100 -12
  58. package/dist/index.d.ts +12 -10
  59. package/dist/index.js +10 -8
  60. package/dist/migrations/20261001162046_oklch_and_emit_fields.d.ts +25 -0
  61. package/dist/migrations/20261001162046_oklch_and_emit_fields.js +10 -0
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +2 -2
@@ -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 = 20261001131000;
75
+ declare const SERIALIZED_CONFIG_VERSION = 20261001162046;
76
76
  interface SerializedConfig {
77
77
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
78
78
  readonly version: number;
@@ -1470,19 +1470,15 @@ declare class Config {
1470
1470
  /**
1471
1471
  * A slug this config doesn't declare, answered by a source that does.
1472
1472
  *
1473
- * It exists for a published chain two deep. Materializing keeps a ref whose target has no value to
1474
- * collapse into — a `style-property`, a component's `file` — so a component borrowed from `mid`
1475
- * arrives in `top` still spelling `style-property:bg@base`, where `base` is a pin only `mid` declared.
1476
- * `top` has no way to name it, because the slug belongs to the system that wrote the ref. Answering
1477
- * `undefined` makes the ref unresolvable, which surfaces as `Cannot inline "style-property:bg@base"`
1478
- * and takes the whole publish down. Depth one works only because there the slug is the consumer's own.
1473
+ * It supports refs in older published chains that still spell a transitive slug directly. New
1474
+ * borrowed bodies scope that name through their owner (`@mid~base`) so a consumer's own `base`
1475
+ * cannot shadow it, but an artifact written before that scoping may still carry `@base`.
1479
1476
  *
1480
1477
  * Read the way {@link configFor} already describes resolution: it hops configs, each answering for
1481
1478
  * the links it declares. This is the same hop for the source address rather than the entity.
1482
1479
  *
1483
- * Local first, always. {@link sourceFor} tries {@link ownSourceFor} before reaching here, so a config
1484
- * that declares `base` itself keeps meaning its own, and a diamond resolves through the pin its owner
1485
- * wrote.
1480
+ * Local first for an ordinary slug. {@link sourceFor} tries {@link ownSourceFor} before reaching
1481
+ * here, so a config that declares `base` itself keeps meaning its own.
1486
1482
  *
1487
1483
  * Memoized per instance including the misses: a dangling sourced ref is read repeatedly, since
1488
1484
  * `list`, `sourceClosure` and `toJSON` all pass over it, and each miss would otherwise re-walk every
@@ -2422,19 +2418,19 @@ declare class Config {
2422
2418
  */
2423
2419
  recordedSources(): Record<string, RecordedSource>;
2424
2420
  private recordSources;
2421
+ /** Record one source without walking every other link this config declares. */
2422
+ private recordSource;
2425
2423
  /**
2426
2424
  * Every slug this config's refs name, declared here or not — what {@link recordSources} owes a
2427
2425
  * closure for.
2428
2426
  *
2429
- * Declared slugs are only half of it. Materializing a published chain keeps a ref whose target has no
2430
- * value to collapse into, so a component borrowed from `mid` arrives holding `style-property:bg@base`,
2431
- * a slug only `mid` declares. Recording only what this config pins leaves that ref with nothing behind
2432
- * it, and a consumer holding the artifact alone reads a borrowed component whose style property
2433
- * resolves to nothing.
2427
+ * Declared slugs are only half of it. Materializing a published chain keeps a ref whose target has
2428
+ * no value to collapse into, so a component borrowed from `mid` can arrive holding
2429
+ * `style-property:bg@mid~base`. Recording only the pins this config declares would leave that
2430
+ * routed ref with nothing behind it.
2434
2431
  *
2435
- * Reachability is already what a closure means; this applies the same rule to the source address
2436
- * rather than the target. {@link sourceThrough} is what answers for such a slug, and the pair is why
2437
- * the recorded artifact is self-contained.
2432
+ * Reachability is already what a closure means; this applies the same rule to the source address.
2433
+ * {@link sourceFor} follows the route, and the recorded artifact remains self-contained.
2438
2434
  *
2439
2435
  * Skipped entirely for a config that borrows nothing, so the common serialize walks nothing extra.
2440
2436
  */
@@ -5,6 +5,7 @@ import { describePatch } from "./plan.js";
5
5
  import { applyPathDelta, isPlainObject, namedFields, pathSegments, sectionDelta, touchedFields } from "./utils/field-path.js";
6
6
  import { METADATA_KEYS, declaredField } from "./utils/field-schema.js";
7
7
  import { dataSchema, resolveInputSchema } from "./schemas.js";
8
+ import { isComposedValue } from "../entities/system/composed-values.js";
8
9
  import { borrowedSlug, classifyOverlay } from "./source-integrity.js";
9
10
  import { kindBorrowable, titleCase } from "./registry.js";
10
11
  import { isGroupBody } from "./utils/group.js";
@@ -145,11 +146,13 @@ function movesSource(patch, sourceIdentity) {
145
146
  * nothing here.
146
147
  */
147
148
  function reslug(entries, { from, to }) {
148
- if (!entries.has(from)) return entries;
149
- const held = entries.get(from);
149
+ const moved = [...entries].filter(([slug]) => slug === from || slug.startsWith(`${from}~`));
150
+ if (moved.length === 0) return entries;
150
151
  const next = new Map(entries);
151
- next.delete(from);
152
- if (to !== void 0) next.set(to, held);
152
+ for (const [slug, held] of moved) {
153
+ next.delete(slug);
154
+ if (to !== void 0) next.set(`${to}${slug.slice(from.length)}`, held);
155
+ }
153
156
  return next;
154
157
  }
155
158
  /** A source as given — live, wrapped, or serialized — as the `LiveConfig` the state holds. */
@@ -2495,16 +2498,23 @@ var Config = class Config {
2495
2498
  */
2496
2499
  extensionBody(kind, extension) {
2497
2500
  const source = this.configFor(extension);
2501
+ if (!source) return void 0;
2498
2502
  const base = source?.resolve(kind, pathOf(extension));
2499
2503
  if (!base) return void 0;
2500
2504
  const slug = sourceOf(extension);
2501
2505
  const owns = (kindName) => source?.ownedKinds().includes(kindName) ?? false;
2502
- return extendBody(slug ? qualifyRefs({
2506
+ const merged = extendBody(slug ? qualifyRefs({
2503
2507
  value: base.toJSON(),
2504
2508
  source: slug,
2505
2509
  owns,
2506
2510
  namesItAlready: (target) => this.borrowsExactly(target, slug)
2507
2511
  }) : base.toJSON(), extension.extend ?? {});
2512
+ return this.entityOf(kind)?.prepareLinkedBody?.({
2513
+ path: pathOf(extension),
2514
+ body: merged,
2515
+ source,
2516
+ config: this
2517
+ }) ?? merged;
2508
2518
  }
2509
2519
  /**
2510
2520
  * Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
@@ -2531,7 +2541,16 @@ var Config = class Config {
2531
2541
  if (cached === null && !strict) return void 0;
2532
2542
  const entry = this.entityOf(kind);
2533
2543
  if (!entry) return void 0;
2534
- const merged = this.extensionBody(kind, extension);
2544
+ let merged;
2545
+ try {
2546
+ merged = this.extensionBody(kind, extension);
2547
+ } catch (error) {
2548
+ if (!strict) {
2549
+ cache.set(cacheKey, null);
2550
+ return;
2551
+ }
2552
+ throw error;
2553
+ }
2535
2554
  if (!merged) return void 0;
2536
2555
  const parsed = dataSchema(entry.schemas, "create", {
2537
2556
  config: this,
@@ -2573,10 +2592,18 @@ var Config = class Config {
2573
2592
  ok: false,
2574
2593
  error: `This system has no "${kind}", so nothing of that kind can be borrowed from "${slug}".`
2575
2594
  };
2576
- const merged = this.extensionBody(kind, {
2577
- __ref: ref(kind, path, slug).__ref,
2578
- extend: extend ?? {}
2579
- });
2595
+ let merged;
2596
+ try {
2597
+ merged = this.extensionBody(kind, {
2598
+ __ref: ref(kind, path, slug).__ref,
2599
+ extend: extend ?? {}
2600
+ });
2601
+ } catch (error) {
2602
+ return {
2603
+ ok: false,
2604
+ error: String(error)
2605
+ };
2606
+ }
2580
2607
  if (!merged) return { ok: true };
2581
2608
  const parsed = dataSchema(entry.schemas, "create", {
2582
2609
  config: this,
@@ -2834,6 +2861,14 @@ var Config = class Config {
2834
2861
  * where the other side can change without us), and the Create badges.
2835
2862
  */
2836
2863
  sourceFor(slug) {
2864
+ const hop = slug.indexOf("~");
2865
+ if (hop > 0 && hop < slug.length - 1) {
2866
+ const head = slug.slice(0, hop);
2867
+ const tail = slug.slice(hop + 1);
2868
+ const live = this.liveSourceFor(head);
2869
+ if (live) return live.sourceFor(tail) ?? this.ownSourceFor(slug);
2870
+ return this.ownSourceFor(slug) ?? this.sourceFor(head)?.sourceFor(tail) ?? this.sourceThrough(slug);
2871
+ }
2837
2872
  return this.ownSourceFor(slug) ?? this.sourceThrough(slug);
2838
2873
  }
2839
2874
  /** This config's own answer for a slug — attached, else the closure it recorded. No hop, which is
@@ -2857,19 +2892,15 @@ var Config = class Config {
2857
2892
  /**
2858
2893
  * A slug this config doesn't declare, answered by a source that does.
2859
2894
  *
2860
- * It exists for a published chain two deep. Materializing keeps a ref whose target has no value to
2861
- * collapse into — a `style-property`, a component's `file` — so a component borrowed from `mid`
2862
- * arrives in `top` still spelling `style-property:bg@base`, where `base` is a pin only `mid` declared.
2863
- * `top` has no way to name it, because the slug belongs to the system that wrote the ref. Answering
2864
- * `undefined` makes the ref unresolvable, which surfaces as `Cannot inline "style-property:bg@base"`
2865
- * and takes the whole publish down. Depth one works only because there the slug is the consumer's own.
2895
+ * It supports refs in older published chains that still spell a transitive slug directly. New
2896
+ * borrowed bodies scope that name through their owner (`@mid~base`) so a consumer's own `base`
2897
+ * cannot shadow it, but an artifact written before that scoping may still carry `@base`.
2866
2898
  *
2867
2899
  * Read the way {@link configFor} already describes resolution: it hops configs, each answering for
2868
2900
  * the links it declares. This is the same hop for the source address rather than the entity.
2869
2901
  *
2870
- * Local first, always. {@link sourceFor} tries {@link ownSourceFor} before reaching here, so a config
2871
- * that declares `base` itself keeps meaning its own, and a diamond resolves through the pin its owner
2872
- * wrote.
2902
+ * Local first for an ordinary slug. {@link sourceFor} tries {@link ownSourceFor} before reaching
2903
+ * here, so a config that declares `base` itself keeps meaning its own.
2873
2904
  *
2874
2905
  * Memoized per instance including the misses: a dangling sourced ref is read repeatedly, since
2875
2906
  * `list`, `sourceClosure` and `toJSON` all pass over it, and each miss would otherwise re-walk every
@@ -4823,47 +4854,66 @@ var Config = class Config {
4823
4854
  recordSources() {
4824
4855
  const recorded = new Map(this.state.recorded);
4825
4856
  for (const slug of this.reachedSlugs()) {
4826
- const source = this.liveSourceFor(slug) ?? this.sourceThrough(slug);
4827
- if (!source) continue;
4828
- const items = {};
4829
- const kinds = /* @__PURE__ */ new Set();
4830
- for (const target of this.sourceClosure(slug)) {
4831
- const kind = kindOf(target);
4832
- const path = pathOf(target);
4833
- const entity = source.resolve(kind, path);
4834
- if (!entity) continue;
4835
- kinds.add(kind);
4836
- items[kind] ??= {};
4837
- items[kind][path] = entity.toJSON();
4838
- }
4839
- if (kinds.size === 0) continue;
4840
- const state = this.state.sourceStates.get(slug);
4841
- recorded.set(slug, {
4842
- ...state?.status === "attached" ? { resolved: state.resolved } : {},
4843
- config: {
4844
- version: SERIALIZED_CONFIG_VERSION,
4845
- name: source.name,
4846
- ownedKinds: [...kinds],
4847
- items,
4848
- groups: groupsAbove(source, items)
4849
- }
4850
- });
4857
+ const closure = this.recordSource(slug);
4858
+ if (closure) recorded.set(slug, closure);
4851
4859
  }
4852
4860
  return recorded;
4853
4861
  }
4862
+ /** Record one source without walking every other link this config declares. */
4863
+ recordSource(slug) {
4864
+ const source = this.sourceFor(slug);
4865
+ if (!source) return void 0;
4866
+ if (this.state.recorded.has(slug) && !this.liveSourceFor(slug) && source === this.ownSourceFor(slug)) return;
4867
+ const items = {};
4868
+ const kinds = /* @__PURE__ */ new Set();
4869
+ for (const target of this.sourceClosure(slug)) {
4870
+ const kind = kindOf(target);
4871
+ const path = pathOf(target);
4872
+ const entity = source.resolve(kind, path);
4873
+ if (!entity) continue;
4874
+ kinds.add(kind);
4875
+ items[kind] ??= {};
4876
+ items[kind][path] = entity.toJSON();
4877
+ }
4878
+ const settings = source.resolve("settings", "settings");
4879
+ if (settings && source.ownedKinds().includes("settings")) {
4880
+ kinds.add("settings");
4881
+ items.settings = { settings: settings.toJSON() };
4882
+ }
4883
+ if (kinds.size === 0) return void 0;
4884
+ const nestedSlugs = /* @__PURE__ */ new Set();
4885
+ for (const entries of Object.values(items)) for (const body of Object.values(entries)) for (const target of collectRefs(body)) {
4886
+ const nested = sourceOf(target);
4887
+ if (nested !== void 0) nestedSlugs.add(nested);
4888
+ }
4889
+ const nestedSources = Object.fromEntries([...nestedSlugs].flatMap((nested) => {
4890
+ const closure = source.liveSourceFor(nested) ? source.recordSource(nested) : source.state.recorded.get(nested) ?? source.recordSource(nested);
4891
+ return closure ? [[nested, closure]] : [];
4892
+ }));
4893
+ const state = this.state.sourceStates.get(slug);
4894
+ return {
4895
+ ...state?.status === "attached" ? { resolved: state.resolved } : {},
4896
+ config: {
4897
+ version: SERIALIZED_CONFIG_VERSION,
4898
+ name: source.name,
4899
+ ownedKinds: [...kinds],
4900
+ items,
4901
+ ...Object.keys(nestedSources).length > 0 ? { sources: nestedSources } : {},
4902
+ groups: groupsAbove(source, items)
4903
+ }
4904
+ };
4905
+ }
4854
4906
  /**
4855
4907
  * Every slug this config's refs name, declared here or not — what {@link recordSources} owes a
4856
4908
  * closure for.
4857
4909
  *
4858
- * Declared slugs are only half of it. Materializing a published chain keeps a ref whose target has no
4859
- * value to collapse into, so a component borrowed from `mid` arrives holding `style-property:bg@base`,
4860
- * a slug only `mid` declares. Recording only what this config pins leaves that ref with nothing behind
4861
- * it, and a consumer holding the artifact alone reads a borrowed component whose style property
4862
- * resolves to nothing.
4910
+ * Declared slugs are only half of it. Materializing a published chain keeps a ref whose target has
4911
+ * no value to collapse into, so a component borrowed from `mid` can arrive holding
4912
+ * `style-property:bg@mid~base`. Recording only the pins this config declares would leave that
4913
+ * routed ref with nothing behind it.
4863
4914
  *
4864
- * Reachability is already what a closure means; this applies the same rule to the source address
4865
- * rather than the target. {@link sourceThrough} is what answers for such a slug, and the pair is why
4866
- * the recorded artifact is self-contained.
4915
+ * Reachability is already what a closure means; this applies the same rule to the source address.
4916
+ * {@link sourceFor} follows the route, and the recorded artifact remains self-contained.
4867
4917
  *
4868
4918
  * Skipped entirely for a config that borrows nothing, so the common serialize walks nothing extra.
4869
4919
  */
@@ -5264,7 +5314,7 @@ var Config = class Config {
5264
5314
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5265
5315
  hydrateFrom(stored, options) {
5266
5316
  const claimed = schemaVersionOf(stored);
5267
- if (claimed > 20261001131e3) throw new SchemaVersionTooNew(claimed);
5317
+ if (claimed > 20261001162046) throw new SchemaVersionTooNew(claimed);
5268
5318
  const wire = upgradeSerializedConfig(stored);
5269
5319
  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\`.`);
5270
5320
  const leftover = wire.options;
@@ -5369,7 +5419,7 @@ function refusePhantomsIn({ kind, path, body }) {
5369
5419
  for (const phantom of phantomsIn(body)) rejectData(`${kind}:${path} ${phantom.at.join(".")} holds ${phantom.spelled}${PHANTOM_IN_DATA}`, phantom.at);
5370
5420
  }
5371
5421
  function isPathSegment(meta, kind, raw) {
5372
- if (typeof raw !== "object" || raw === null || Array.isArray(raw) || isRef(raw)) return false;
5422
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw) || isRef(raw) || isComposedValue(raw)) return false;
5373
5423
  const entry = meta.entities.get(kind);
5374
5424
  if (!entry) return false;
5375
5425
  const create = dataSchema(entry.schemas, "create");
@@ -5586,7 +5636,7 @@ function extensionChange(entry, kind, path, raw, validate = false) {
5586
5636
  * every load.
5587
5637
  */
5588
5638
  function itemChange(entry, kind, path, raw, validate = false) {
5589
- const def = isRef(raw) || typeof raw !== "object" || raw === null ? { [entry.primaryField]: raw } : raw;
5639
+ const def = isRef(raw) || isComposedValue(raw) || typeof raw !== "object" || raw === null ? { [entry.primaryField]: raw } : raw;
5590
5640
  if (validate) {
5591
5641
  const parsed = wholeBodySchema(entry)?.safeParse(def);
5592
5642
  if (parsed && !parsed.success) {
@@ -232,12 +232,45 @@ declare function stylePropClassBase(config: Config, path: string): string;
232
232
  * The value is a LEAF, which is what a call site writes; an empty leaf is a boolean prop's `true`
233
233
  * arm, whose class is the bare stem (`uds-border`) because a flag names no value.
234
234
  *
235
- * A slash becomes a dash and a DOT an underscore, because a class name can hold neither: `.uds-p-0.5`
236
- * is not one selector but two (`uds-p-0` and `5`), so the rule the sheet wrote could never match the
237
- * element the module classed. `spacing/0.5` is a real token in a real system, which is how that
238
- * surfaced.
235
+ * A slash in a leaf is a group separator and becomes a dash (`red/500` → `bg-red-500`), except in a
236
+ * leaf that is a fraction, which keeps it (`w-1/2`). A dot is kept (`p-0.5`); the stylesheet writes
237
+ * the class through {@link classSelector}, which escapes both.
239
238
  */
240
239
  declare function stylePropClassName(config: Config, path: string, leaf: string): string;
240
+ /** Each value of a prop that names its own class, as the class it is written as. Shared with the
241
+ * emitted runtime, which writes the same class for the same value. */
242
+ declare function stylePropOwnClasses(config: Config, path: string): Map<string, string>;
243
+ /**
244
+ * A leaf as a class spells it. Whitespace becomes a hyphen: a class list splits on it, so
245
+ * `my custom background` would put three classes on the element and match none of the one rule.
246
+ * The emitted `_style-props` runtime applies the same transform to the values a call site passes.
247
+ */
248
+ declare function classLeaf(leaf: string): string;
249
+ /**
250
+ * A class as a selector: `.p-0\.5`, `.w-1\/2`, `.md\:flex`, `.\32xl\:p-4`. Every character outside
251
+ * an identifier's own is escaped, and a leading digit, which an identifier cannot start with, is
252
+ * written as its code point.
253
+ */
254
+ declare function classSelector(name: string): string;
255
+ /**
256
+ * Whether a prop's bracketed value has to be written as its arbitrary class rather than inline.
257
+ * An inline style sets each of the prop's `properties` on the element and nothing else, so a prop
258
+ * with a `selector`, its own `declarations`, or a property keyed by a token ref would paint wrong.
259
+ */
260
+ declare function arbitraryNeedsClass(config: Config, path: string): boolean;
261
+ /**
262
+ * The class a bracketed value is written as — `blur` and `[3px]` → `uds-blur-[3px]`. A space inside
263
+ * the brackets is spelled `_`, which the stylesheet reads back as one.
264
+ */
265
+ declare function arbitraryClassName({
266
+ config,
267
+ path,
268
+ value
269
+ }: {
270
+ readonly config: Config;
271
+ readonly path: string; /** The authored value, brackets included. */
272
+ readonly value: string;
273
+ }): string;
241
274
  /** What a prop bag amounts to once styling is taken out of it. */
242
275
  interface SplitStyleProps {
243
276
  /** The classes the bag's style props produce, space-joined. */
@@ -254,8 +287,9 @@ interface SplitStyleProps {
254
287
  * but the CLASS NAMES both produce come from {@link stylePropClassName}, so the two can't disagree
255
288
  * about what a value is called.
256
289
  *
257
- * An off-scale value can't be a class, so it lands in `style` on the returned rest; an explicit
258
- * `style` the caller passed still wins, since that's the escape hatch of last resort.
290
+ * An off-scale value lands in `style` on the returned rest, unless the prop needs its arbitrary class
291
+ * ({@link arbitraryNeedsClass}); an explicit `style` the caller passed still wins, since that's the
292
+ * escape hatch of last resort.
259
293
  */
260
294
  declare function splitStyleProps({
261
295
  config,
@@ -296,5 +330,11 @@ declare function stylePropClasses(config: Config, path: string): Map<string, str
296
330
  * the system can predict it. Covering those needs the document as a second source of use.
297
331
  */
298
332
  declare function modifierUtilitiesUsed(config: Config): Set<string>;
333
+ /**
334
+ * A class split at the colons that separate its modifiers, as `md:hover:[&_a]:bg-[url(a:b)]` is
335
+ * `md`, `hover`, `[&_a]` and `bg-[url(a:b)]`. A colon inside brackets or parentheses belongs to the
336
+ * value. The last segment is the utility.
337
+ */
338
+ declare function splitVariants(cls: string): string[];
299
339
  //#endregion
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 };
340
+ export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
@@ -1,9 +1,9 @@
1
- import { isRef, kindOf, memberOf, pathOf, refLeaf } from "./utils/refs.js";
1
+ import { isRef, kindOf, memberOf, pathOf, refLeaf, refOfKey } from "./utils/refs.js";
2
2
  import { forwardsOf, rootLayerOf } from "../entities/system/element.js";
3
3
  import { createSliceMemo } from "./memo.js";
4
- import { classPrefixOf, varPrefixOf } from "../entities/system/Settings.js";
4
+ import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
5
5
  import { storedPreview } from "./preview-store.js";
6
- import { authoredValue, className, negatedCssValue, renderStyleValue, resolveComponentProps, stylePropLeafForToken, stylePropValueLeaves, stylePropertyPathFor } from "./projections.js";
6
+ import { authoredValue, className, negatedCssValue, renderStyleValue, resolveComponentProps, stylePropEntries, stylePropLeafForToken, stylePropValueLeaves, stylePropertyPathFor } from "./projections.js";
7
7
  import { ruleApplies } from "./layer-styles.js";
8
8
  //#region src/framework/class-names.ts
9
9
  /**
@@ -279,7 +279,10 @@ function componentCompositeClasses(config, path) {
279
279
  /** The class a CSS motion preset is applied by. Namespaced, so it doesn't share a space with the
280
280
  * style-prop utilities; the `@keyframes` it runs keep their own name. */
281
281
  function motionClassName(config, path) {
282
- return `${classPrefixOf(config)}-motion-${path.replaceAll("/", "-")}`;
282
+ return prefixed({
283
+ prefix: classPrefixOf(config),
284
+ name: `motion-${path.replaceAll("/", "-")}`
285
+ });
283
286
  }
284
287
  /**
285
288
  * Every MOTION class a component's style rules apply — a layer bag naming a preset (`motion: 'spin'`).
@@ -472,7 +475,10 @@ function inlineRuleComposites(config, element, bag) {
472
475
  const at = member.indexOf("/");
473
476
  inlined = mergeStyleBags(inlined ?? {}, {
474
477
  ...styles,
475
- [`--${varPrefixOf(config)}-composite-${member.slice(0, at)}`]: member.slice(at + 1)
478
+ [`--${prefixed({
479
+ prefix: varPrefixOf(config),
480
+ name: `composite-${member.slice(0, at)}`
481
+ })}`]: member.slice(at + 1)
476
482
  });
477
483
  }
478
484
  return inlined ? mergeStyleBags(inlined, rest) : bag;
@@ -587,15 +593,63 @@ function stylePropClassBase(config, path) {
587
593
  * The value is a LEAF, which is what a call site writes; an empty leaf is a boolean prop's `true`
588
594
  * arm, whose class is the bare stem (`uds-border`) because a flag names no value.
589
595
  *
590
- * A slash becomes a dash and a DOT an underscore, because a class name can hold neither: `.uds-p-0.5`
591
- * is not one selector but two (`uds-p-0` and `5`), so the rule the sheet wrote could never match the
592
- * element the module classed. `spacing/0.5` is a real token in a real system, which is how that
593
- * surfaced.
596
+ * A slash in a leaf is a group separator and becomes a dash (`red/500` → `bg-red-500`), except in a
597
+ * leaf that is a fraction, which keeps it (`w-1/2`). A dot is kept (`p-0.5`); the stylesheet writes
598
+ * the class through {@link classSelector}, which escapes both.
594
599
  */
595
600
  function stylePropClassName(config, path, leaf) {
601
+ const own = ownClassNames(config, path).get(leaf);
602
+ if (own !== void 0) return prefixed({
603
+ prefix: classPrefixOf(config),
604
+ name: own
605
+ });
596
606
  const base = stylePropClassBase(config, path);
597
- const suffix = leaf.replaceAll("/", "-").replaceAll(".", "_").replace(/\s+/g, "-");
598
- return suffix ? `${base}-${suffix}` : base;
607
+ const suffix = classLeaf(leaf);
608
+ if (!suffix) return base;
609
+ return base ? `${base}-${suffix}` : suffix;
610
+ }
611
+ /** Leaf → the class it names, per prop's entries, for the values that name their own. */
612
+ const ownClassNameCache = /* @__PURE__ */ new WeakMap();
613
+ /** The values of a prop that name their own class, by leaf. */
614
+ function ownClassNames(config, path) {
615
+ const entries = stylePropEntries(config, path);
616
+ let own = ownClassNameCache.get(entries);
617
+ if (!own) {
618
+ const built = /* @__PURE__ */ new Map();
619
+ for (const entry of entries) if (entry.className !== void 0 && !built.has(entry.leaf)) built.set(entry.leaf, entry.className);
620
+ own = built;
621
+ ownClassNameCache.set(entries, own);
622
+ }
623
+ return own;
624
+ }
625
+ /** Each value of a prop that names its own class, as the class it is written as. Shared with the
626
+ * emitted runtime, which writes the same class for the same value. */
627
+ function stylePropOwnClasses(config, path) {
628
+ const out = /* @__PURE__ */ new Map();
629
+ for (const leaf of ownClassNames(config, path).keys()) out.set(leaf, stylePropClassName(config, path, leaf));
630
+ return out;
631
+ }
632
+ /**
633
+ * A leaf as a class spells it. Whitespace becomes a hyphen: a class list splits on it, so
634
+ * `my custom background` would put three classes on the element and match none of the one rule.
635
+ * The emitted `_style-props` runtime applies the same transform to the values a call site passes.
636
+ */
637
+ function classLeaf(leaf) {
638
+ const spaced = leaf.replace(/\s+/g, "-");
639
+ return /^\d+\/\d+$/.test(spaced) ? spaced : spaced.replaceAll("/", "-");
640
+ }
641
+ /**
642
+ * A class as a selector: `.p-0\.5`, `.w-1\/2`, `.md\:flex`, `.\32xl\:p-4`. Every character outside
643
+ * an identifier's own is escaped, and a leading digit, which an identifier cannot start with, is
644
+ * written as its code point.
645
+ */
646
+ function classSelector(name) {
647
+ const escaped = name.replace(/[^A-Za-z0-9_\-\u00a0-\uffff]/g, (char) => `\\${char}`);
648
+ const leading = /^-?\d/.exec(escaped);
649
+ if (!leading) return `.${escaped}`;
650
+ const at = leading[0].length - 1;
651
+ const digit = escaped.charCodeAt(at).toString(16);
652
+ return `.${escaped.slice(0, at)}\\${digit} ${escaped.slice(at + 1)}`;
599
653
  }
600
654
  /** An off-scale value and what it really is — `[13px]` → `13px`, else `undefined`. The brackets mark
601
655
  * a value the system doesn't tokenize; they aren't part of it. No class can exist for one, because a
@@ -613,6 +667,23 @@ function unescapeArbitrary(value) {
613
667
  return value.replace(/url\([^)]*\)|_/g, (span) => span === "_" ? " " : span);
614
668
  }
615
669
  /**
670
+ * Whether a prop's bracketed value has to be written as its arbitrary class rather than inline.
671
+ * An inline style sets each of the prop's `properties` on the element and nothing else, so a prop
672
+ * with a `selector`, its own `declarations`, or a property keyed by a token ref would paint wrong.
673
+ */
674
+ function arbitraryNeedsClass(config, path) {
675
+ const { properties = [], declarations, selector } = config.resolve("style-property", path)?.toJSON() ?? {};
676
+ return Boolean(selector) || Object.keys(declarations ?? {}).length > 0 || properties.some((property) => refOfKey(property) !== void 0);
677
+ }
678
+ /**
679
+ * The class a bracketed value is written as — `blur` and `[3px]` → `uds-blur-[3px]`. A space inside
680
+ * the brackets is spelled `_`, which the stylesheet reads back as one.
681
+ */
682
+ function arbitraryClassName({ config, path, value }) {
683
+ const inner = value.slice(1, -1).replace(/\s+/g, "_");
684
+ return `${stylePropClassBase(config, path)}-[${inner}]`;
685
+ }
686
+ /**
616
687
  * Split a prop bag into a class string and the props that aren't styling.
617
688
  *
618
689
  * This is the same dispatch the emitted `_style-props` runtime performs, over a live config instead
@@ -621,8 +692,9 @@ function unescapeArbitrary(value) {
621
692
  * but the CLASS NAMES both produce come from {@link stylePropClassName}, so the two can't disagree
622
693
  * about what a value is called.
623
694
  *
624
- * An off-scale value can't be a class, so it lands in `style` on the returned rest; an explicit
625
- * `style` the caller passed still wins, since that's the escape hatch of last resort.
695
+ * An off-scale value lands in `style` on the returned rest, unless the prop needs its arbitrary class
696
+ * ({@link arbitraryNeedsClass}); an explicit `style` the caller passed still wins, since that's the
697
+ * escape hatch of last resort.
626
698
  */
627
699
  function splitStyleProps({ config, bag, through }) {
628
700
  const classes = [];
@@ -656,6 +728,14 @@ function splitStyleProps({ config, bag, through }) {
656
728
  if (value === void 0 || value === null) continue;
657
729
  const { arbitrary, properties = [], negates } = prop.toJSON();
658
730
  const off = arbitrary ? offScale(value) : void 0;
731
+ if (off !== void 0 && arbitraryNeedsClass(config, name)) {
732
+ classes.push(arbitraryClassName({
733
+ config,
734
+ path: name,
735
+ value: String(value)
736
+ }));
737
+ continue;
738
+ }
659
739
  if (off !== void 0) {
660
740
  const declared = negates ? negatedCssValue(off) : off;
661
741
  for (const property of properties) style[reactStyleKey(property)] = declared;
@@ -827,5 +907,26 @@ function classesUsedBy(config, stored, token, bagsOf) {
827
907
  byVocabulary.set(token, found);
828
908
  return found;
829
909
  }
910
+ /**
911
+ * A class split at the colons that separate its modifiers, as `md:hover:[&_a]:bg-[url(a:b)]` is
912
+ * `md`, `hover`, `[&_a]` and `bg-[url(a:b)]`. A colon inside brackets or parentheses belongs to the
913
+ * value. The last segment is the utility.
914
+ */
915
+ function splitVariants(cls) {
916
+ const segments = [];
917
+ let depth = 0;
918
+ let start = 0;
919
+ for (let at = 0; at < cls.length; at++) {
920
+ const char = cls[at];
921
+ if (char === "[" || char === "(") depth++;
922
+ else if ((char === "]" || char === ")") && depth > 0) depth--;
923
+ else if (char === ":" && depth === 0) {
924
+ segments.push(cls.slice(start, at));
925
+ start = at + 1;
926
+ }
927
+ }
928
+ segments.push(cls.slice(start));
929
+ return segments;
930
+ }
830
931
  //#endregion
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 };
932
+ export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
@@ -192,6 +192,7 @@ GI extends GroupInputs = Record<never, never>> = Omit<RefSchema, 'extend'> & {
192
192
  * does call zod's `extend` on it.
193
193
  */
194
194
  extend<F2 extends ShapeOverride = Record<never, never>, G2 extends ShapeOverride = Record<never, never>, S2 extends UserSchemas = never>(overrides: {
195
+ prepareLinkedBody?: EntityKind['prepareLinkedBody'];
195
196
  fields?: F2;
196
197
  groupFields?: G2;
197
198
  subEntities?: Partial<Record<string, SubEntityClass | null>>;
@@ -353,7 +354,8 @@ declare function defineEntity<F extends FieldsSchema, G extends FieldsSchema = z
353
354
  * Declare it alongside `bulkOverlay` for a kind whose minted rows should stay editable; without it,
354
355
  * an edit to one has nowhere to land.
355
356
  */
356
- bulkOverlayWrite?: (args: BulkOverlayWrite) => Record<string, unknown>;
357
+ bulkOverlayWrite?: (args: BulkOverlayWrite) => Record<string, unknown>; /** Adapt a pinned source body before linked validation. */
358
+ prepareLinkedBody?: EntityKind['prepareLinkedBody'];
357
359
  /**
358
360
  * Edges this kind's data implies by name rather than by ref, and how to rewrite one — the pair that
359
361
  * makes a record keyed by another kind's item names behave like a reference: reported by
@@ -27,7 +27,8 @@ const EXTENDABLE_KEYS = new Set([
27
27
  "schemas",
28
28
  "label",
29
29
  "labelPlural",
30
- "entityName"
30
+ "entityName",
31
+ "prepareLinkedBody"
31
32
  ]);
32
33
  /** Merge an override into the base's shape, key by key — using zod's real `extend`/`omit` on the
33
34
  * plain object, where they are the right tools. A named key replaces or adds a field; `null`
@@ -122,6 +123,7 @@ function defineEntity(spec) {
122
123
  bulkOverlay: spec.bulkOverlay,
123
124
  bulkDirectives: spec.bulkDirectives,
124
125
  bulkOverlayWrite: spec.bulkOverlayWrite,
126
+ prepareLinkedBody: spec.prepareLinkedBody,
125
127
  nameEdges: spec.nameEdges,
126
128
  styleBags: spec.styleBags,
127
129
  rewriteName: spec.rewriteName,
@@ -13,9 +13,9 @@ import { Ref } from "./utils/refs.js";
13
13
  * to become ours must not keep pointing anywhere.
14
14
  */
15
15
  /**
16
- * What to do with a sourced ref that names something with no value — a component, a `file`, a style
17
- * property. There is nothing to substitute for one, so it needs its own answer, and which answer
18
- * depends on whether the pin survives the edit.
16
+ * What to do with a sourced identity ref — a modifier, component, `file`, or style property. There is
17
+ * no token value to substitute for one, so it needs its own answer, and which answer depends on
18
+ * whether the pin survives the edit.
19
19
  *
20
20
  * **It survives** for publishing and for reading a repo-local chain. The seam stays, so the ref stays:
21
21
  * `keepIdentityRefs`. A borrowed component's `file` has to keep naming the system that owns the
@@ -40,7 +40,7 @@ function inlineSourcedRefs(value, config, slug, options) {
40
40
  const target = config.resolveRef(value);
41
41
  const body = target?.toJSON();
42
42
  if (!target && !declaresGroup(owner, value)) rejectData(`Cannot inline "${value.__ref}": "${source}" has no ${kindOf(value)} at "${pathOf(value)}". A pin is exact, so this is a ref into a version that does not declare it.`);
43
- if (body?.value === void 0) {
43
+ if (kindOf(value) !== "token" || body?.value === void 0) {
44
44
  if (options?.keepIdentityRefs) return value;
45
45
  const adopted = options?.adopted?.get(entityRefOf(value.__ref));
46
46
  if (adopted !== void 0) return rewriteRefs(value, entityRefOf(value.__ref), adopted);