@animus-ui/system 0.1.1 → 0.1.3

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 (42) hide show
  1. package/README.md +125 -4
  2. package/dist/Animus.d.ts +2 -3
  3. package/dist/Animus.d.ts.map +1 -1
  4. package/dist/AnimusExtended.d.ts +2 -3
  5. package/dist/AnimusExtended.d.ts.map +1 -1
  6. package/dist/SystemBuilder.d.ts +55 -8
  7. package/dist/SystemBuilder.d.ts.map +1 -1
  8. package/dist/appearance/index.d.ts +75 -0
  9. package/dist/appearance/index.d.ts.map +1 -0
  10. package/dist/appearance/index.js +179 -0
  11. package/dist/bootstrap/createAppearanceBootstrap.d.ts +71 -0
  12. package/dist/bootstrap/createAppearanceBootstrap.d.ts.map +1 -0
  13. package/dist/bootstrap/index.d.ts +2 -0
  14. package/dist/bootstrap/index.d.ts.map +1 -0
  15. package/dist/bootstrap/index.js +98 -0
  16. package/dist/composeWithContext.d.ts.map +1 -1
  17. package/dist/composeWithContext.js +43 -22
  18. package/dist/conditions.d.ts +163 -0
  19. package/dist/conditions.d.ts.map +1 -0
  20. package/dist/{createComposedFamily-DSE2ZuRE.js → createComposedFamily-DJ2aqfqN.js} +141 -67
  21. package/dist/index.d.ts +5 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +648 -25
  24. package/dist/runtime/index.d.ts +2 -0
  25. package/dist/runtime/index.d.ts.map +1 -1
  26. package/dist/runtime/resolveClasses.d.ts.map +1 -1
  27. package/dist/runtime-entry.js +1 -1
  28. package/dist/selectors.d.ts +22 -0
  29. package/dist/selectors.d.ts.map +1 -1
  30. package/dist/theme/createTheme.d.ts +47 -4
  31. package/dist/theme/createTheme.d.ts.map +1 -1
  32. package/dist/types/component.d.ts +27 -10
  33. package/dist/types/component.d.ts.map +1 -1
  34. package/dist/types/config.d.ts +124 -11
  35. package/dist/types/config.d.ts.map +1 -1
  36. package/dist/types/props.d.ts +0 -4
  37. package/dist/types/props.d.ts.map +1 -1
  38. package/dist/types/theme.d.ts +132 -0
  39. package/dist/types/theme.d.ts.map +1 -1
  40. package/dist/utils/deepMerge.d.ts +0 -3
  41. package/dist/utils/deepMerge.d.ts.map +1 -1
  42. package/package.json +12 -4
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { n as createClassResolver, r as createComponent, t as createComposedFamily } from "./createComposedFamily-DSE2ZuRE.js";
1
+ import { n as createClassResolver, r as createComponent, t as createComposedFamily } from "./createComposedFamily-DJ2aqfqN.js";
2
2
  import { compose } from "./compose.js";
3
3
  import { a as borderShorthand, c as numericOrStringScale, i as gridItemRatio, l as numericScale, n as size, o as createTransform, r as gridItem, s as createScale, t as percentageOrAbsolute, u as stringScale } from "./size-CusLCguT.js";
4
4
  //#region src/utils/deepMerge.ts
5
5
  /**
6
6
  * Deep merge utility — replaces lodash.merge for variant accumulation.
7
7
  */
8
+ const isMergeable = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
8
9
  function deepMerge(target, source) {
9
10
  const result = { ...target };
10
- for (const key of Object.keys(source)) if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) result[key] = deepMerge(target[key], source[key]);
11
+ for (const key of Object.keys(source)) if (isMergeable(source[key]) && isMergeable(target[key])) result[key] = deepMerge(target[key], source[key]);
11
12
  else result[key] = source[key];
12
13
  return result;
13
14
  }
@@ -228,6 +229,149 @@ var Animus = class extends AnimusWithBase {
228
229
  }
229
230
  };
230
231
  //#endregion
232
+ //#region src/conditions.ts
233
+ /**
234
+ * Built-in condition aliases (design D8, increment 06).
235
+ *
236
+ * The Panda-compatible media-feature set: motion, print, orientation, contrast,
237
+ * and OS color-scheme preferences. Every alias is a pure `@media` feature query
238
+ * (`kind: 'media'`). Color-mode aliases (`_dark` / `_light`) are DELIBERATELY
239
+ * excluded — they are selector-kind (`[data-color-mode]`) surface owned by the
240
+ * `system-color-scheme` change, not media features.
241
+ *
242
+ * ORDER BAND (inc-03 full-pass): built-ins occupy a reserved band BELOW the user
243
+ * band — one slot of 10 per alias in table row order, 300–380. The user band
244
+ * starts at 500 (`mergeConditions` floors allocation at 490 → first user alias
245
+ * is 500), so built-ins and user registrations never interleave on collision.
246
+ * This mirrors the selector registry's real layout (built-ins 10–440, users
247
+ * 500+).
248
+ *
249
+ * The static type-level mirror is `BuiltInConditionAlias` in `types/config.ts`
250
+ * (added to `KnownUnderscoreKey`'s VALIDATING branch, exactly like
251
+ * `BuiltInSelectorAlias`). Built-ins are NOT members of the augmentable
252
+ * `Conditions` interface: interface members would make publication permanently
253
+ * non-empty, killing inc-04's graceful-degradation contract. The
254
+ * `types.test-d.tsx` drift positives keep the two in sync.
255
+ */
256
+ const BUILT_IN_CONDITIONS = {
257
+ _motionReduce: {
258
+ value: "@media (prefers-reduced-motion: reduce)",
259
+ order: 300,
260
+ kind: "media"
261
+ },
262
+ _motionSafe: {
263
+ value: "@media (prefers-reduced-motion: no-preference)",
264
+ order: 310,
265
+ kind: "media"
266
+ },
267
+ _print: {
268
+ value: "@media print",
269
+ order: 320,
270
+ kind: "media"
271
+ },
272
+ _portrait: {
273
+ value: "@media (orientation: portrait)",
274
+ order: 330,
275
+ kind: "media"
276
+ },
277
+ _landscape: {
278
+ value: "@media (orientation: landscape)",
279
+ order: 340,
280
+ kind: "media"
281
+ },
282
+ _moreContrast: {
283
+ value: "@media (prefers-contrast: more)",
284
+ order: 350,
285
+ kind: "media"
286
+ },
287
+ _lessContrast: {
288
+ value: "@media (prefers-contrast: less)",
289
+ order: 360,
290
+ kind: "media"
291
+ },
292
+ _osDark: {
293
+ value: "@media (prefers-color-scheme: dark)",
294
+ order: 370,
295
+ kind: "media"
296
+ },
297
+ _osLight: {
298
+ value: "@media (prefers-color-scheme: light)",
299
+ order: 380,
300
+ kind: "media"
301
+ }
302
+ };
303
+ /** The supported at-rule prefixes, longest-first so `@media`/`@container`/
304
+ * `@supports` match unambiguously. */
305
+ const CONDITION_PREFIXES = [
306
+ ["@media", "media"],
307
+ ["@container", "container"],
308
+ ["@supports", "supports"]
309
+ ];
310
+ /**
311
+ * Infer the condition kind from an at-rule value's prefix (design D3).
312
+ * Throws (fail-loud, build time) when the value does not begin with a
313
+ * supported at-rule name. The compile-time complement is `addConditions()`'s
314
+ * value constraint (`@media`/`@container`/`@supports`-prefixed template
315
+ * literals); this runtime throw remains the backstop for untyped callers.
316
+ */
317
+ function inferConditionKind(value) {
318
+ for (const [prefix, kind] of CONDITION_PREFIXES) if (value.startsWith(prefix)) return kind;
319
+ throw new Error(`addConditions: value "${value}" must begin with @media, @container, or @supports`);
320
+ }
321
+ /**
322
+ * Merge user-provided condition aliases with a base map.
323
+ *
324
+ * - User aliases override built-ins of the same name, preserving the existing
325
+ * order (mirrors `mergeSelectors`).
326
+ * - New aliases allocate orders CONTINUING from the highest existing order
327
+ * (floored at 490, so the first user alias lands at 500) rather than
328
+ * restarting at 500 each call — chained `.addConditions()` calls must not
329
+ * collide on order 500.
330
+ * - Fails loud when a condition alias name is already reserved by the SELECTOR
331
+ * alias registry (`reservedSelectorNames`): the two registries share the `_`
332
+ * namespace at the call site and their names MUST be disjoint, else a block
333
+ * key would be ambiguous. The error names the offending alias and both
334
+ * registries.
335
+ */
336
+ function mergeConditions(base, custom, reservedSelectorNames = /* @__PURE__ */ new Set()) {
337
+ const merged = { ...base };
338
+ let nextOrder = Math.max(490, ...Object.values(merged).map((c) => c.order)) + 10;
339
+ for (const [key, value] of Object.entries(custom)) {
340
+ if (reservedSelectorNames.has(key)) throw new Error(`addConditions: alias "${key}" is already registered in the selector alias registry — condition and selector alias names must be disjoint (rename the condition alias or the selector alias).`);
341
+ const kind = inferConditionKind(value);
342
+ if (key in merged) merged[key] = {
343
+ value,
344
+ order: merged[key].order,
345
+ kind
346
+ };
347
+ else {
348
+ merged[key] = {
349
+ value,
350
+ order: nextOrder,
351
+ kind
352
+ };
353
+ nextOrder += 10;
354
+ }
355
+ }
356
+ return merged;
357
+ }
358
+ /** Get the sorted alias keys for deterministic cascade ordering. */
359
+ function getSortedConditionKeys(map) {
360
+ return Object.keys(map).sort((a, b) => map[a].order - map[b].order);
361
+ }
362
+ /**
363
+ * Serialize the condition map for the extraction pipeline.
364
+ * Emits `alias → { value, order, kind }` in registry (cascade) order so the
365
+ * JSON is deterministic. Distinct in SHAPE from the selector map (which
366
+ * flattens to `alias → selector`) — the object entry is forward-compatible
367
+ * with a future multi-valued `values: string[]` property (design D3).
368
+ */
369
+ function serializeConditionMap(map) {
370
+ const conditions = {};
371
+ for (const key of getSortedConditionKeys(map)) conditions[key] = map[key];
372
+ return conditions;
373
+ }
374
+ //#endregion
231
375
  //#region src/keyframes.ts
232
376
  const fnv1a = (input) => {
233
377
  let hash = 2166136261;
@@ -423,27 +567,52 @@ function serializeSelectorMap(map) {
423
567
  }
424
568
  //#endregion
425
569
  //#region src/SystemBuilder.ts
570
+ function orderedPropertiesEqual(existing, incoming) {
571
+ if (existing === incoming) return true;
572
+ if (!existing || !incoming || existing.length !== incoming.length) return false;
573
+ return existing.every((property, index) => property === incoming[index]);
574
+ }
575
+ function arePropDefinitionsEqual(existing, incoming) {
576
+ return existing.property === incoming.property && orderedPropertiesEqual(existing.properties, incoming.properties) && existing.scale === incoming.scale && existing.variable === incoming.variable && existing.negative === incoming.negative && existing.strict === incoming.strict && existing.currentVar === incoming.currentVar && existing.transform === incoming.transform;
577
+ }
426
578
  var SystemBuilder = class SystemBuilder {
427
579
  #propRegistry;
428
580
  #groupRegistry;
429
581
  #selectorRegistry;
430
582
  #includesRegistry;
431
- constructor(propRegistry, groupRegistry, selectorRegistry, includesRegistry) {
583
+ #conditionRegistry;
584
+ constructor(propRegistry, groupRegistry, selectorRegistry, includesRegistry, conditionRegistry) {
432
585
  this.#propRegistry = propRegistry || {};
433
586
  this.#groupRegistry = groupRegistry || {};
434
587
  this.#selectorRegistry = selectorRegistry || { ...BUILT_IN_SELECTORS };
435
588
  this.#includesRegistry = includesRegistry || [];
589
+ this.#conditionRegistry = conditionRegistry || { ...BUILT_IN_CONDITIONS };
436
590
  }
437
591
  addSelectors(selectors) {
592
+ for (const name of Object.keys(selectors)) if (name in this.#conditionRegistry) throw new Error(`addSelectors: "${name}" is already registered as a condition alias; a name resolves through exactly one registry. Pick a distinct name.`);
438
593
  const merged = mergeSelectors(this.#selectorRegistry, selectors);
439
- return new SystemBuilder(this.#propRegistry, this.#groupRegistry, merged, this.#includesRegistry);
594
+ return new SystemBuilder(this.#propRegistry, this.#groupRegistry, merged, this.#includesRegistry, this.#conditionRegistry);
595
+ }
596
+ /**
597
+ * Register condition aliases (`_motionReduce`, `_cardSm`, …) → at-rule
598
+ * condition strings (`@media …` / `@container …` / `@supports …`).
599
+ * Recognized as block keys in style objects; user aliases override built-ins
600
+ * of the same name (design D3). Keys are constrained to `_`-prefixed aliases
601
+ * and values to `@`-prefixed at-rule strings — a value that does not begin
602
+ * with an at-rule name is a compile-time type error (design D9; the runtime
603
+ * `inferConditionKind` throw is defense-in-depth). The registered keys are
604
+ * accumulated into the phantom `Conds` union and surfaced on `build()`.
605
+ */
606
+ addConditions(conditions) {
607
+ const merged = mergeConditions(this.#conditionRegistry, conditions, new Set(Object.keys(this.#selectorRegistry)));
608
+ return new SystemBuilder(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, merged);
440
609
  }
441
610
  addGroup(name, config) {
442
611
  if (name in this.#propRegistry) throw new Error(`Group name "${name}" collides with an existing prop name. Group names and prop names must be disjoint.`);
443
612
  for (const key of Object.keys(config)) if (key in this.#propRegistry) {
444
613
  const existing = this.#propRegistry[key];
445
614
  const incoming = config[key];
446
- if (existing.property !== incoming.property || existing.scale !== incoming.scale || existing.transform !== incoming.transform || existing.negative !== incoming.negative) throw new Error(`Prop "${key}" already registered with a different definition. Existing: property="${existing.property}", scale="${String(existing.scale)}". Incoming: property="${incoming.property}", scale="${String(incoming.scale)}".`);
615
+ if (!arePropDefinitionsEqual(existing, incoming)) throw new Error(`Prop "${key}" already registered with a different definition. Existing: property="${existing.property}", scale="${String(existing.scale)}". Incoming: property="${incoming.property}", scale="${String(incoming.scale)}".`);
447
616
  }
448
617
  const nextProps = {
449
618
  ...this.#propRegistry,
@@ -453,31 +622,33 @@ var SystemBuilder = class SystemBuilder {
453
622
  return new SystemBuilder(nextProps, {
454
623
  ...this.#groupRegistry,
455
624
  ...newGroup
456
- }, this.#selectorRegistry, this.#includesRegistry);
625
+ }, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry);
457
626
  }
458
627
  addProps(config) {
459
628
  for (const key of Object.keys(config)) if (key in this.#groupRegistry) throw new Error(`Prop name "${key}" collides with an existing group name. Group names and prop names must be disjoint.`);
460
629
  for (const key of Object.keys(config)) if (key in this.#propRegistry) {
461
630
  const existing = this.#propRegistry[key];
462
631
  const incoming = config[key];
463
- if (existing.property !== incoming.property || existing.scale !== incoming.scale || existing.transform !== incoming.transform || existing.negative !== incoming.negative) throw new Error(`Prop "${key}" already registered with a different definition.`);
632
+ if (!arePropDefinitionsEqual(existing, incoming)) throw new Error(`Prop "${key}" already registered with a different definition.`);
464
633
  }
465
634
  return new SystemBuilder({
466
635
  ...this.#propRegistry,
467
636
  ...config
468
- }, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry);
637
+ }, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry);
469
638
  }
470
639
  build() {
471
640
  const animus = new Animus(this.#propRegistry, this.#groupRegistry);
472
641
  const propRegistry = this.#propRegistry;
473
642
  const groupRegistry = this.#groupRegistry;
474
643
  const selectorRegistry = this.#selectorRegistry;
644
+ const conditionRegistry = this.#conditionRegistry;
475
645
  const system = Object.assign(animus, { toConfig: () => {
476
- return serializeInstance(propRegistry, groupRegistry, selectorRegistry);
646
+ return serializeInstance(propRegistry, groupRegistry, selectorRegistry, conditionRegistry);
477
647
  } });
478
- const createGlobalStyles = ((styles) => ({
648
+ const createGlobalStyles = ((styles, options) => ({
479
649
  __brand: "GlobalStyleBlock",
480
- styles
650
+ styles,
651
+ ...options?.fontFaces?.length ? { fontFaces: [...options.fontFaces] } : {}
481
652
  }));
482
653
  const createKeyframes = ((frames) => keyframes(frames));
483
654
  return {
@@ -487,7 +658,7 @@ var SystemBuilder = class SystemBuilder {
487
658
  };
488
659
  }
489
660
  };
490
- function serializeInstance(propRegistry, groupRegistry, selectorRegistry) {
661
+ function serializeInstance(propRegistry, groupRegistry, selectorRegistry, conditionRegistry) {
491
662
  const serialized = {};
492
663
  const transforms = {};
493
664
  for (const [propName, entry] of Object.entries(propRegistry)) {
@@ -509,11 +680,13 @@ function serializeInstance(propRegistry, groupRegistry, selectorRegistry) {
509
680
  serialized[propName] = s;
510
681
  }
511
682
  const { selectors } = serializeSelectorMap(selectorRegistry);
683
+ const conditions = serializeConditionMap(conditionRegistry);
512
684
  return {
513
685
  propConfig: JSON.stringify(serialized),
514
686
  groupRegistry: JSON.stringify(groupRegistry),
515
687
  transforms,
516
- selectorAliases: JSON.stringify(selectors)
688
+ selectorAliases: JSON.stringify(selectors),
689
+ conditionAliases: JSON.stringify(conditions)
517
690
  };
518
691
  }
519
692
  function createSystem(config) {
@@ -618,6 +791,109 @@ function validateModeAliases(modeName, aliases, nestedColors, flatColorKeys, pre
618
791
  } else if (isObject(value)) validateModeAliases(modeName, value, nestedColors, flatColorKeys, aliasPath);
619
792
  }
620
793
  }
794
+ /**
795
+ * Reserved mode name (D4). "System" is modeled as the ABSENCE of
796
+ * `data-color-mode`, never as a mode: an attribute value `system` would defeat
797
+ * the `:root:not([data-color-mode])` guard while matching no mode block.
798
+ */
799
+ const RESERVED_MODE_NAME = "system";
800
+ /**
801
+ * Theme keys owned by the color-mode options. A scale may not claim them —
802
+ * they are skipped by `flattenTheme` and read back as option objects in
803
+ * `build()`, so a same-named scale would emit zero tokens AND fabricate a
804
+ * manifest field from its values.
805
+ *
806
+ * NOTE: the pre-existing `mode` / `modes` / `breakpoints` structural keys have
807
+ * the same hole and are deliberately NOT covered here (out of scope).
808
+ */
809
+ const RESERVED_THEME_KEYS = new Set(["systemPreference", "browserColorScheme"]);
810
+ const COLOR_SCHEME_VALUES = new Set([
811
+ "light",
812
+ "dark",
813
+ "normal"
814
+ ]);
815
+ /**
816
+ * The axis → scheme pairs the mapping forces: the mode named for the OS light
817
+ * preference must classify `light`, the dark one `dark`. Single source for
818
+ * BOTH the default fill and the conflict check in
819
+ * {@link resolveColorModeOptions} — one rule, one table.
820
+ */
821
+ const MAPPING_FORCED_SCHEMES = [["light", "light"], ["dark", "dark"]];
822
+ /**
823
+ * Merge an incoming option object over the one already on the theme, mirroring
824
+ * `merge`'s per-key override. Returns `undefined` when neither side supplied
825
+ * one, so an unconfigured theme never gains the key (byte parity, G4).
826
+ */
827
+ function mergeOptionObject(existing, incoming) {
828
+ const hasExisting = isObject(existing);
829
+ const hasIncoming = isObject(incoming);
830
+ if (!hasExisting && !hasIncoming) return void 0;
831
+ return {
832
+ ...hasExisting ? existing : {},
833
+ ...hasIncoming ? incoming : {}
834
+ };
835
+ }
836
+ /** Reject the reserved mode name (D4) wherever a mode set is declared or merged. */
837
+ function validateReservedModeNames(modeNames) {
838
+ for (const modeName of modeNames) if (modeName === RESERVED_MODE_NAME) throw new Error(`addColorModes: '${RESERVED_MODE_NAME}' is a reserved mode name — the system preference is represented by the absence of the data-color-mode attribute, never by a declared mode. Rename it. Declared modes: ${modeNames.join(", ")}`);
839
+ }
840
+ /**
841
+ * Normalize AND validate the optional system-participation options against the
842
+ * declared modes, returning the normalized classification. One entry point on
843
+ * purpose: the D3-amendment fill (mapping-named modes default to their forced
844
+ * schemes) must always run before totality is checked, so exposing fill and
845
+ * validation separately would make "validate an unfilled map" representable.
846
+ * An explicit entry survives the fill's spread and is still conflict-checked,
847
+ * so a wrong value errors rather than being silently corrected. `undefined`
848
+ * passes through untouched — the classification stays opt-in, and a theme with
849
+ * only `systemPreference` emits no `color-scheme` (byte parity, G4).
850
+ *
851
+ * ALWAYS called with MERGED state — `theme.modes` unions across `addColorModes`
852
+ * calls and `from()` composition, so a per-call view both misses invalidation
853
+ * (a later mode declaration un-totals a carried classification) and invents
854
+ * false rejections (a mapping naming a mode declared by an earlier call).
855
+ * Run at both gates: `addColorModes` (fail fast) and `build()` (authoritative —
856
+ * `from()` composition never passes through `addColorModes`; only build()'s
857
+ * resolved map feeds emission and the manifest).
858
+ *
859
+ * Error tone mirrors `validateModeAliases`: `addColorModes:` prefix plus the
860
+ * available names.
861
+ */
862
+ function resolveColorModeOptions(modeNames, systemPreference, suppliedBrowserColorScheme) {
863
+ let browserColorScheme = suppliedBrowserColorScheme;
864
+ if (browserColorScheme && systemPreference) {
865
+ const forced = {};
866
+ for (const [axis, scheme] of MAPPING_FORCED_SCHEMES) {
867
+ const modeName = systemPreference[axis];
868
+ if (typeof modeName === "string") forced[modeName] = scheme;
869
+ }
870
+ browserColorScheme = {
871
+ ...forced,
872
+ ...suppliedBrowserColorScheme
873
+ };
874
+ }
875
+ const available = `Available modes: ${modeNames.join(", ")}`;
876
+ const declared = new Set(modeNames);
877
+ if (systemPreference) for (const axis of ["light", "dark"]) {
878
+ const modeName = systemPreference[axis];
879
+ if (typeof modeName !== "string" || modeName === "") throw new Error(`addColorModes: systemPreference requires both 'light' and 'dark' to name a declared mode — '${axis}' is missing. ${available}`);
880
+ if (modeName === RESERVED_MODE_NAME) throw new Error(`addColorModes: '${RESERVED_MODE_NAME}' is reserved and cannot be used as systemPreference.${axis} — the system preference is represented by the absence of the data-color-mode attribute. ${available}`);
881
+ if (!declared.has(modeName)) throw new Error(`addColorModes: systemPreference.${axis} references unknown mode '${modeName}'. ${available}`);
882
+ }
883
+ if (!browserColorScheme) return browserColorScheme;
884
+ for (const [modeName, value] of Object.entries(browserColorScheme)) {
885
+ if (!declared.has(modeName)) throw new Error(`addColorModes: browserColorScheme references unknown mode '${modeName}'. ${available}`);
886
+ if (!COLOR_SCHEME_VALUES.has(value)) throw new Error(`addColorModes: browserColorScheme['${modeName}'] must be 'light', 'dark', or 'normal', got ${JSON.stringify(value)}.`);
887
+ }
888
+ for (const modeName of modeNames) if (!(modeName in browserColorScheme)) throw new Error(`addColorModes: browserColorScheme must classify every declared mode — mode '${modeName}' is unclassified. ${available}`);
889
+ if (!systemPreference) return browserColorScheme;
890
+ for (const [axis, expected] of MAPPING_FORCED_SCHEMES) {
891
+ const modeName = systemPreference[axis];
892
+ const classification = browserColorScheme[modeName];
893
+ if (classification !== expected) throw new Error(`addColorModes: browserColorScheme conflicts with systemPreference — mode '${modeName}' is mapped to the OS ${axis} preference but classified '${classification}'; expected '${expected}'.`);
894
+ }
895
+ return browserColorScheme;
896
+ }
621
897
  /** Validate all color entries, throwing on invalid values. */
622
898
  function validateColors(colors) {
623
899
  for (const [key, value] of Object.entries(colors)) if (isObject(value)) validateColors(value);
@@ -627,14 +903,19 @@ function createState(theme) {
627
903
  return {
628
904
  theme: theme || { breakpoints: {} },
629
905
  emittedScales: /* @__PURE__ */ new Set(),
630
- contextualVars: /* @__PURE__ */ new Map()
906
+ contextualVars: /* @__PURE__ */ new Map(),
907
+ contextualVarRegistrations: /* @__PURE__ */ new Map(),
908
+ hasLegacyManifestSource: false
631
909
  };
632
910
  }
633
911
  function copyState(state, nextTheme) {
634
912
  const next = {
635
913
  theme: nextTheme,
636
914
  emittedScales: new Set(state.emittedScales),
637
- contextualVars: /* @__PURE__ */ new Map()
915
+ contextualVars: /* @__PURE__ */ new Map(),
916
+ contextualVarRegistrations: new Map(state.contextualVarRegistrations),
917
+ ...state.carriedManifestV2 ? { carriedManifestV2: { ...state.carriedManifestV2 } } : {},
918
+ hasLegacyManifestSource: state.hasLegacyManifestSource
638
919
  };
639
920
  for (const [scale, vars] of state.contextualVars) next.contextualVars.set(scale, [...vars]);
640
921
  return next;
@@ -667,6 +948,17 @@ var ThemeBuilder = class ThemeBuilder {
667
948
  next._state.emittedScales.add(scale === "colors" ? "colors" : scale);
668
949
  }
669
950
  if (manifest?.contextualVars) for (const [scale, vars] of Object.entries(manifest.contextualVars)) next._state.contextualVars.set(scale, [...vars]);
951
+ if (manifest) if (manifest.manifestVersion === 2) {
952
+ next._state.carriedManifestV2 = {
953
+ tokenDefinitions: manifest.tokenDefinitions,
954
+ modeAliasDefinitions: manifest.modeAliasDefinitions,
955
+ registrations: manifest.registrations,
956
+ emitterVersion: manifest.emitterVersion,
957
+ contractHash: manifest.contractHash,
958
+ cssFragments: manifest.cssFragments
959
+ };
960
+ if (manifest.registrations) for (const [name, registration] of Object.entries(manifest.registrations)) next._state.contextualVarRegistrations.set(name, registration);
961
+ } else next._state.hasLegacyManifestSource = true;
670
962
  return next;
671
963
  }
672
964
  addColors(colors) {
@@ -676,31 +968,46 @@ var ThemeBuilder = class ThemeBuilder {
676
968
  next._state.emittedScales.add("colors");
677
969
  return next;
678
970
  }
679
- addColorModes(initialMode, modeConfig) {
971
+ addColorModes(initialMode, modeConfig, options) {
680
972
  const nestedColors = this._state.theme.colors || {};
681
973
  const flatColors = flattenToDotPaths(nestedColors);
682
974
  const flatColorKeys = Object.keys(flatColors);
975
+ const existingModes = isObject(this._state.theme.modes) ? this._state.theme.modes : {};
976
+ const modeNames = Object.keys({
977
+ ...existingModes,
978
+ ...modeConfig
979
+ });
980
+ validateReservedModeNames(modeNames);
683
981
  for (const [modeName, modeAliases] of Object.entries(modeConfig)) validateModeAliases(modeName, modeAliases, nestedColors, flatColorKeys, "");
982
+ const systemPreference = mergeOptionObject(this._state.theme.systemPreference, options?.systemPreference);
983
+ const browserColorScheme = mergeOptionObject(this._state.theme.browserColorScheme, options?.browserColorScheme);
984
+ resolveColorModeOptions(modeNames, systemPreference, browserColorScheme);
684
985
  const nextTheme = merge({}, this._state.theme, {
685
986
  modes: modeConfig,
686
- mode: initialMode
987
+ mode: initialMode,
988
+ ...systemPreference ? { systemPreference } : {},
989
+ ...browserColorScheme ? { browserColorScheme } : {}
687
990
  });
688
991
  return new ThemeBuilder(copyState(this._state, nextTheme));
689
992
  }
690
993
  addScale(config) {
691
994
  const { name, values, emit } = config;
995
+ if (RESERVED_THEME_KEYS.has(name)) throw new Error(`addScale: '${name}' is a reserved theme key owned by addColorModes options — it is skipped by the token flatten pass and read back as an option object, so a scale by this name would emit no tokens. Choose another scale name.`);
692
996
  const nextTheme = merge({}, this._state.theme, { [name]: values });
693
997
  const next = new ThemeBuilder(copyState(this._state, nextTheme));
694
998
  if (emit) next._state.emittedScales.add(name);
695
999
  return next;
696
1000
  }
697
- declareContextualVars(vars) {
1001
+ declareContextualVars(vars, registrations) {
698
1002
  for (const scale of Object.keys(vars)) if (!(scale in this._state.theme)) throw new Error(`declareContextualVars: scale '${scale}' not found — call addColors or addScale first`);
699
1003
  const next = new ThemeBuilder(copyState(this._state, this._state.theme));
700
1004
  for (const [scale, names] of Object.entries(vars)) {
701
1005
  const existing = next._state.contextualVars.get(scale) || [];
702
1006
  next._state.contextualVars.set(scale, [...existing, ...names]);
703
1007
  }
1008
+ if (registrations) {
1009
+ for (const [name, registration] of Object.entries(registrations)) if (registration) next._state.contextualVarRegistrations.set(name, registration);
1010
+ }
704
1011
  return next;
705
1012
  }
706
1013
  extendScale(key, updateFn) {
@@ -715,7 +1022,11 @@ var ThemeBuilder = class ThemeBuilder {
715
1022
  const theme = merge({}, this._state.theme);
716
1023
  const emittedScales = this._state.emittedScales;
717
1024
  const contextualVars = this._state.contextualVars;
718
- const { tokenMap, variableMap, variables, modeVariables, modeTokens } = flattenTheme(theme, emittedScales);
1025
+ const systemPreference = isObject(theme.systemPreference) ? theme.systemPreference : void 0;
1026
+ const mergedModeNames = isObject(theme.modes) ? Object.keys(theme.modes) : [];
1027
+ validateReservedModeNames(mergedModeNames);
1028
+ const browserColorScheme = resolveColorModeOptions(mergedModeNames, systemPreference, isObject(theme.browserColorScheme) ? theme.browserColorScheme : void 0);
1029
+ const { tokenMap, variableMap, variables, modeVariables, modeTokens, tokenDefinitions, modeAliasDefinitions } = flattenTheme(theme, emittedScales);
719
1030
  resolveTokenRefs(tokenMap, variableMap, emittedScales);
720
1031
  const bpVariables = {};
721
1032
  if (theme.breakpoints && isObject(theme.breakpoints)) for (const [key, value] of Object.entries(theme.breakpoints)) bpVariables[`--breakpoint-${key}`] = `${value}px`;
@@ -724,7 +1035,44 @@ var ThemeBuilder = class ThemeBuilder {
724
1035
  contextualVarsSerialized = {};
725
1036
  for (const [scale, vars] of contextualVars) contextualVarsSerialized[scale] = vars;
726
1037
  }
727
- const variableCss = buildVariableCss(variables, bpVariables, modeVariables);
1038
+ const propertyCss = buildPropertyRegistrationCss(contextualVars, this._state.contextualVarRegistrations);
1039
+ const baseVariableCss = buildVariableCss(variables, bpVariables, modeVariables, {
1040
+ initialMode: typeof theme.mode === "string" ? theme.mode : void 0,
1041
+ systemPreference,
1042
+ browserColorScheme
1043
+ });
1044
+ const variableCss = propertyCss ? baseVariableCss ? `${propertyCss}\n\n${baseVariableCss}` : propertyCss : baseVariableCss;
1045
+ let manifestV2Fields = {};
1046
+ if (!this._state.hasLegacyManifestSource) {
1047
+ const registrations = Object.fromEntries(this._state.contextualVarRegistrations);
1048
+ const cssFragments = [];
1049
+ if (propertyCss) cssFragments.push({
1050
+ id: "registrations",
1051
+ kind: "registrations",
1052
+ cssText: propertyCss
1053
+ });
1054
+ if (baseVariableCss) cssFragments.push({
1055
+ id: "base",
1056
+ kind: "base",
1057
+ cssText: baseVariableCss
1058
+ });
1059
+ manifestV2Fields = {
1060
+ manifestVersion: 2,
1061
+ tokenDefinitions,
1062
+ modeAliasDefinitions,
1063
+ registrations,
1064
+ emitterVersion: EMITTER_VERSION,
1065
+ contractHash: computeContractHash({
1066
+ tokenDefinitions,
1067
+ modeAliasDefinitions,
1068
+ initialMode: typeof theme.mode === "string" ? theme.mode : void 0,
1069
+ registrations,
1070
+ systemPreference,
1071
+ browserColorScheme
1072
+ }),
1073
+ cssFragments
1074
+ };
1075
+ }
728
1076
  const manifest = {
729
1077
  tokenMap: {
730
1078
  ...tokenMap,
@@ -733,7 +1081,10 @@ var ThemeBuilder = class ThemeBuilder {
733
1081
  variableMap,
734
1082
  modes: modeTokens,
735
1083
  variableCss,
736
- ...contextualVarsSerialized ? { contextualVars: contextualVarsSerialized } : {}
1084
+ ...contextualVarsSerialized ? { contextualVars: contextualVarsSerialized } : {},
1085
+ ...systemPreference ? { systemPreference } : {},
1086
+ ...browserColorScheme ? { browserColorScheme } : {},
1087
+ ...manifestV2Fields
737
1088
  };
738
1089
  Object.defineProperty(theme, "manifest", {
739
1090
  value: manifest,
@@ -787,9 +1138,11 @@ function flattenTheme(theme, emittedScales) {
787
1138
  const variables = {};
788
1139
  const modeVariables = {};
789
1140
  const modeTokens = {};
1141
+ const tokenDefinitions = {};
1142
+ const modeAliasDefinitions = {};
790
1143
  for (const [scaleName, scaleValue] of Object.entries(theme)) {
791
1144
  if (scaleName.startsWith("_")) continue;
792
- if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes") continue;
1145
+ if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes" || scaleName === "systemPreference" || scaleName === "browserColorScheme") continue;
793
1146
  if (typeof scaleValue === "function") continue;
794
1147
  if (!isObject(scaleValue)) continue;
795
1148
  const flat = flattenToDotPaths(scaleValue);
@@ -798,6 +1151,7 @@ function flattenTheme(theme, emittedScales) {
798
1151
  const tokenPath = `${scaleName}.${dotKey}`;
799
1152
  const dashKey = dotToDash(dotKey);
800
1153
  const varName = `--${scaleName === "colors" ? "color" : scaleName}-${dashKey}`;
1154
+ tokenDefinitions[tokenPath] = parseTokenDefinition(String(rawValue));
801
1155
  if (isEmitted) {
802
1156
  tokenMap[tokenPath] = `var(${varName})`;
803
1157
  variableMap[tokenPath] = varName;
@@ -812,15 +1166,18 @@ function flattenTheme(theme, emittedScales) {
812
1166
  const flatAliases = flattenToDotPaths(modeAliases);
813
1167
  const modeVars = {};
814
1168
  const modeVals = {};
1169
+ const modeAliasDefs = {};
815
1170
  for (const [aliasDotKey, colorRef] of Object.entries(flatAliases)) {
816
1171
  if (typeof colorRef !== "string") continue;
817
1172
  const varName = `--color-${dotToDash(aliasDotKey)}`;
1173
+ modeAliasDefs[aliasDotKey] = colorRef;
818
1174
  const rawValue = flatColors[colorRef];
819
1175
  modeVals[`colors.${aliasDotKey}`] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
820
1176
  modeVars[varName] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
821
1177
  }
822
1178
  modeVariables[modeName] = modeVars;
823
1179
  modeTokens[modeName] = modeVals;
1180
+ modeAliasDefinitions[modeName] = modeAliasDefs;
824
1181
  }
825
1182
  const initialMode = theme.mode;
826
1183
  if (initialMode && modeVariables[initialMode]) {
@@ -842,10 +1199,239 @@ function flattenTheme(theme, emittedScales) {
842
1199
  variableMap,
843
1200
  variables,
844
1201
  modeVariables,
845
- modeTokens
1202
+ modeTokens,
1203
+ tokenDefinitions,
1204
+ modeAliasDefinitions
846
1205
  };
847
1206
  }
848
1207
  /**
1208
+ * Classify a RAW token value into its authored form (manifest v2, D6). MUST
1209
+ * run before `resolveTokenRefs` — resolution rewrites the string, and the
1210
+ * authored graph cannot be reconstructed from resolved CSS (D8). Uses
1211
+ * `matchAll` so the shared global {@link TOKEN_REF_RE} never carries a stale
1212
+ * `lastIndex` between callers.
1213
+ */
1214
+ function parseTokenDefinition(rawValue) {
1215
+ const references = [];
1216
+ for (const match of rawValue.matchAll(TOKEN_REF_RE)) {
1217
+ const ref = match[1];
1218
+ const slashIdx = ref.indexOf("/");
1219
+ references.push(slashIdx === -1 ? { path: ref } : {
1220
+ path: ref.slice(0, slashIdx),
1221
+ opacity: ref.slice(slashIdx + 1)
1222
+ });
1223
+ }
1224
+ if (references.length === 0) return {
1225
+ kind: "literal",
1226
+ value: rawValue
1227
+ };
1228
+ return {
1229
+ kind: "reference",
1230
+ value: rawValue,
1231
+ references
1232
+ };
1233
+ }
1234
+ /**
1235
+ * Version of the CSS emitter that composed a manifest's fragments. Bump when
1236
+ * ANY emitted byte changes for the same authored input — composition (D8)
1237
+ * regenerates dirty sections "under the pinned emitter version", and this is
1238
+ * that pin.
1239
+ */
1240
+ const EMITTER_VERSION = 1;
1241
+ /**
1242
+ * Sorted-key canonical form for hashing: object keys are emitted in sorted
1243
+ * order at every depth so the digest is independent of insertion order;
1244
+ * arrays keep authored order (reference order is contractual).
1245
+ */
1246
+ function canonicalize(value) {
1247
+ if (Array.isArray(value)) return value.map(canonicalize);
1248
+ if (isObject(value)) {
1249
+ const record = value;
1250
+ const sorted = {};
1251
+ for (const key of Object.keys(record).sort()) sorted[key] = canonicalize(record[key]);
1252
+ return sorted;
1253
+ }
1254
+ return value;
1255
+ }
1256
+ /**
1257
+ * sha256 hex digest. Primary path is `node:crypto` `createHash('sha256')`,
1258
+ * reached via `process.getBuiltinModule` so this module gains NO static
1259
+ * `node:crypto` import edge: built themes execute inside client bundles today
1260
+ * (e2e fixtures inline `createTheme(…).build()` at module init), and a static
1261
+ * builtin import is exactly what `./bootstrap` documents as forbidden in app
1262
+ * bundles. Non-Node runtimes use the pure fallback below, which produces
1263
+ * identical hex for identical input — determinism holds across environments.
1264
+ */
1265
+ function sha256Hex(input) {
1266
+ const nodeCrypto = globalThis.process?.getBuiltinModule?.("node:crypto");
1267
+ if (nodeCrypto?.createHash) return nodeCrypto.createHash("sha256").update(input).digest("hex");
1268
+ return sha256HexFallback(input);
1269
+ }
1270
+ /** SHA-256 round constants (FIPS 180-4 §4.2.2). */
1271
+ const SHA256_K = new Uint32Array([
1272
+ 1116352408,
1273
+ 1899447441,
1274
+ 3049323471,
1275
+ 3921009573,
1276
+ 961987163,
1277
+ 1508970993,
1278
+ 2453635748,
1279
+ 2870763221,
1280
+ 3624381080,
1281
+ 310598401,
1282
+ 607225278,
1283
+ 1426881987,
1284
+ 1925078388,
1285
+ 2162078206,
1286
+ 2614888103,
1287
+ 3248222580,
1288
+ 3835390401,
1289
+ 4022224774,
1290
+ 264347078,
1291
+ 604807628,
1292
+ 770255983,
1293
+ 1249150122,
1294
+ 1555081692,
1295
+ 1996064986,
1296
+ 2554220882,
1297
+ 2821834349,
1298
+ 2952996808,
1299
+ 3210313671,
1300
+ 3336571891,
1301
+ 3584528711,
1302
+ 113926993,
1303
+ 338241895,
1304
+ 666307205,
1305
+ 773529912,
1306
+ 1294757372,
1307
+ 1396182291,
1308
+ 1695183700,
1309
+ 1986661051,
1310
+ 2177026350,
1311
+ 2456956037,
1312
+ 2730485921,
1313
+ 2820302411,
1314
+ 3259730800,
1315
+ 3345764771,
1316
+ 3516065817,
1317
+ 3600352804,
1318
+ 4094571909,
1319
+ 275423344,
1320
+ 430227734,
1321
+ 506948616,
1322
+ 659060556,
1323
+ 883997877,
1324
+ 958139571,
1325
+ 1322822218,
1326
+ 1537002063,
1327
+ 1747873779,
1328
+ 1955562222,
1329
+ 2024104815,
1330
+ 2227730452,
1331
+ 2361852424,
1332
+ 2428436474,
1333
+ 2756734187,
1334
+ 3204031479,
1335
+ 3329325298
1336
+ ]);
1337
+ function rotr(x, n) {
1338
+ return (x >>> n | x << 32 - n) >>> 0;
1339
+ }
1340
+ /**
1341
+ * UTF-8 bytes of `input` without WHATWG `TextEncoder`: the Rust system-loader
1342
+ * evaluates the system bundle in QuickJS, which provides ES built-ins only —
1343
+ * no Node globals and no WHATWG APIs. Iterating by code point handles
1344
+ * surrogate pairs; lone surrogates cannot reach this path because the input
1345
+ * is well-formed `JSON.stringify` output (ES2019 escapes them as `\uXXXX`).
1346
+ */
1347
+ function utf8Bytes(input) {
1348
+ const bytes = [];
1349
+ for (const char of input) {
1350
+ const code = char.codePointAt(0);
1351
+ if (code < 128) bytes.push(code);
1352
+ else if (code < 2048) bytes.push(192 | code >> 6, 128 | code & 63);
1353
+ else if (code < 65536) bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
1354
+ else bytes.push(240 | code >> 18, 128 | code >> 12 & 63, 128 | code >> 6 & 63, 128 | code & 63);
1355
+ }
1356
+ return new Uint8Array(bytes);
1357
+ }
1358
+ /**
1359
+ * Pure SHA-256 (FIPS 180-4) over the UTF-8 bytes of `input` — the non-Node
1360
+ * fallback for {@link sha256Hex}. Not a security surface: the contract hash
1361
+ * is a content digest for composition-identity comparison only.
1362
+ */
1363
+ function sha256HexFallback(input) {
1364
+ const bytes = utf8Bytes(input);
1365
+ const bitLength = bytes.length * 8;
1366
+ const paddedLength = (bytes.length + 8 >> 6) + 1 << 6 >>> 0;
1367
+ const padded = new Uint8Array(paddedLength);
1368
+ padded.set(bytes);
1369
+ padded[bytes.length] = 128;
1370
+ const view = new DataView(padded.buffer);
1371
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
1372
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
1373
+ const state = new Uint32Array([
1374
+ 1779033703,
1375
+ 3144134277,
1376
+ 1013904242,
1377
+ 2773480762,
1378
+ 1359893119,
1379
+ 2600822924,
1380
+ 528734635,
1381
+ 1541459225
1382
+ ]);
1383
+ const w = new Uint32Array(64);
1384
+ for (let offset = 0; offset < paddedLength; offset += 64) {
1385
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
1386
+ for (let i = 16; i < 64; i++) {
1387
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
1388
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
1389
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1;
1390
+ }
1391
+ let a = state[0];
1392
+ let b = state[1];
1393
+ let c = state[2];
1394
+ let d = state[3];
1395
+ let e = state[4];
1396
+ let f = state[5];
1397
+ let g = state[6];
1398
+ let h = state[7];
1399
+ for (let i = 0; i < 64; i++) {
1400
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1401
+ const ch = e & f ^ ~e & g;
1402
+ const temp1 = h + S1 + ch + SHA256_K[i] + w[i] >>> 0;
1403
+ const temp2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
1404
+ h = g;
1405
+ g = f;
1406
+ f = e;
1407
+ e = d + temp1 >>> 0;
1408
+ d = c;
1409
+ c = b;
1410
+ b = a;
1411
+ a = temp1 + temp2 >>> 0;
1412
+ }
1413
+ state[0] += a;
1414
+ state[1] += b;
1415
+ state[2] += c;
1416
+ state[3] += d;
1417
+ state[4] += e;
1418
+ state[5] += f;
1419
+ state[6] += g;
1420
+ state[7] += h;
1421
+ }
1422
+ let hex = "";
1423
+ for (const word of state) hex += word.toString(16).padStart(8, "0");
1424
+ return hex;
1425
+ }
1426
+ /**
1427
+ * Stable digest over the canonical authored inputs. Identical authored input
1428
+ * ⇒ identical hash, across processes: keys sort at every depth and
1429
+ * `JSON.stringify` drops `undefined`-valued members deterministically.
1430
+ */
1431
+ function computeContractHash(input) {
1432
+ return sha256Hex(JSON.stringify(canonicalize(input)));
1433
+ }
1434
+ /**
849
1435
  * Resolve token refs ({scale.key}) in all flattened token values.
850
1436
  * Operates on the flattened tokenMap — does NOT mutate the nested theme.
851
1437
  */
@@ -882,19 +1468,56 @@ function resolveTokenRefs(tokenMap, _variableMap, _emittedScales) {
882
1468
  if (resolved !== value) tokenMap[tokenPath] = resolved;
883
1469
  }
884
1470
  }
1471
+ /**
1472
+ * Build `@property` registration rules for registered contextual vars.
1473
+ * The emitted custom property is `--${name}` — the same name the Rust resolver
1474
+ * maps a bare contextual var value to. Rules emit in declaration order and only
1475
+ * for names that are genuinely declared contextual vars. Returns `''` when no
1476
+ * registration metadata was supplied (opt-in / byte-identical guarantee).
1477
+ */
1478
+ function buildPropertyRegistrationCss(contextualVars, registrations) {
1479
+ if (registrations.size === 0) return "";
1480
+ const declaredNames = /* @__PURE__ */ new Set();
1481
+ for (const names of contextualVars.values()) for (const name of names) declaredNames.add(name);
1482
+ const blocks = [];
1483
+ for (const [name, registration] of registrations) {
1484
+ if (!declaredNames.has(name)) continue;
1485
+ const descriptors = [`syntax: "${registration.syntax}";`, `inherits: ${registration.inherits};`];
1486
+ if (registration.initialValue !== void 0) descriptors.push(`initial-value: ${registration.initialValue};`);
1487
+ blocks.push(`@property --${name} { ${descriptors.join(" ")} }`);
1488
+ }
1489
+ return blocks.join("\n");
1490
+ }
885
1491
  /** Build CSS variable blocks from flattened data. */
886
- function buildVariableCss(rootVariables, breakpointVariables, modeVariables) {
1492
+ function buildVariableCss(rootVariables, breakpointVariables, modeVariables, systemEmission = {}) {
1493
+ const { initialMode, systemPreference, browserColorScheme } = systemEmission;
887
1494
  const parts = [];
888
1495
  const rootLines = [];
889
1496
  for (const [varName, value] of Object.entries(rootVariables)) rootLines.push(` ${varName}: ${value};`);
890
1497
  for (const [varName, value] of Object.entries(breakpointVariables)) rootLines.push(` ${varName}: ${value};`);
1498
+ if (browserColorScheme && initialMode) {
1499
+ const initialScheme = browserColorScheme[initialMode];
1500
+ if (initialScheme) rootLines.push(` color-scheme: ${initialScheme};`);
1501
+ }
891
1502
  if (rootLines.length > 0) parts.push(`:root {\n${rootLines.join("\n")}\n}`);
1503
+ if (systemPreference) for (const scheme of ["light", "dark"]) {
1504
+ const modeName = systemPreference[scheme];
1505
+ const mediaLines = [];
1506
+ const modeVars = modeVariables[modeName];
1507
+ if (modeVars) for (const [varName, value] of Object.entries(modeVars)) mediaLines.push(` ${varName}: ${value};`);
1508
+ const mediaScheme = browserColorScheme?.[modeName];
1509
+ if (mediaScheme) mediaLines.push(` color-scheme: ${mediaScheme};`);
1510
+ if (mediaLines.length === 0) continue;
1511
+ parts.push(`@media (prefers-color-scheme: ${scheme}) {\n :root:not([data-color-mode]) {\n${mediaLines.join("\n")}\n }\n}`);
1512
+ }
892
1513
  for (const [modeName, modeVars] of Object.entries(modeVariables)) {
893
1514
  const modeLines = [];
894
1515
  for (const [varName, value] of Object.entries(modeVars)) modeLines.push(` ${varName}: ${value};`);
1516
+ const modeScheme = browserColorScheme?.[modeName];
1517
+ if (modeScheme) modeLines.push(` color-scheme: ${modeScheme};`);
895
1518
  if (modeLines.length > 0) parts.push(`[data-color-mode="${modeName}"] {\n${modeLines.join("\n")}\n}`);
896
1519
  }
897
1520
  return parts.join("\n\n");
898
1521
  }
899
1522
  //#endregion
900
- export { Animus, AnimusExtended, AnimusExtendedWithAll, AnimusWithAll, BUILT_IN_SELECTORS, SystemBuilder, ThemeBuilder, borderShorthand, compose, createClassResolver, createComponent, createComposedFamily, createScale, createSystem, createTheme, createTransform, gridItem, gridItemRatio, numericOrStringScale, numericScale, percentageOrAbsolute, size, stringScale };
1523
+ export { Animus, AnimusExtended, AnimusExtendedWithAll, AnimusWithAll, BUILT_IN_CONDITIONS, BUILT_IN_SELECTORS, SystemBuilder, ThemeBuilder, borderShorthand, compose, createClassResolver, createComponent, createComposedFamily, createScale, createSystem, createTheme, createTransform, gridItem, gridItemRatio, numericOrStringScale, numericScale, percentageOrAbsolute, size, stringScale };