@animus-ui/system 0.1.1 → 0.1.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.
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 +27 -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 +4 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +645 -23
  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,27 +622,28 @@ 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
648
  const createGlobalStyles = ((styles) => ({
479
649
  __brand: "GlobalStyleBlock",
@@ -487,7 +657,7 @@ var SystemBuilder = class SystemBuilder {
487
657
  };
488
658
  }
489
659
  };
490
- function serializeInstance(propRegistry, groupRegistry, selectorRegistry) {
660
+ function serializeInstance(propRegistry, groupRegistry, selectorRegistry, conditionRegistry) {
491
661
  const serialized = {};
492
662
  const transforms = {};
493
663
  for (const [propName, entry] of Object.entries(propRegistry)) {
@@ -509,11 +679,13 @@ function serializeInstance(propRegistry, groupRegistry, selectorRegistry) {
509
679
  serialized[propName] = s;
510
680
  }
511
681
  const { selectors } = serializeSelectorMap(selectorRegistry);
682
+ const conditions = serializeConditionMap(conditionRegistry);
512
683
  return {
513
684
  propConfig: JSON.stringify(serialized),
514
685
  groupRegistry: JSON.stringify(groupRegistry),
515
686
  transforms,
516
- selectorAliases: JSON.stringify(selectors)
687
+ selectorAliases: JSON.stringify(selectors),
688
+ conditionAliases: JSON.stringify(conditions)
517
689
  };
518
690
  }
519
691
  function createSystem(config) {
@@ -618,6 +790,109 @@ function validateModeAliases(modeName, aliases, nestedColors, flatColorKeys, pre
618
790
  } else if (isObject(value)) validateModeAliases(modeName, value, nestedColors, flatColorKeys, aliasPath);
619
791
  }
620
792
  }
793
+ /**
794
+ * Reserved mode name (D4). "System" is modeled as the ABSENCE of
795
+ * `data-color-mode`, never as a mode: an attribute value `system` would defeat
796
+ * the `:root:not([data-color-mode])` guard while matching no mode block.
797
+ */
798
+ const RESERVED_MODE_NAME = "system";
799
+ /**
800
+ * Theme keys owned by the color-mode options. A scale may not claim them —
801
+ * they are skipped by `flattenTheme` and read back as option objects in
802
+ * `build()`, so a same-named scale would emit zero tokens AND fabricate a
803
+ * manifest field from its values.
804
+ *
805
+ * NOTE: the pre-existing `mode` / `modes` / `breakpoints` structural keys have
806
+ * the same hole and are deliberately NOT covered here (out of scope).
807
+ */
808
+ const RESERVED_THEME_KEYS = new Set(["systemPreference", "browserColorScheme"]);
809
+ const COLOR_SCHEME_VALUES = new Set([
810
+ "light",
811
+ "dark",
812
+ "normal"
813
+ ]);
814
+ /**
815
+ * The axis → scheme pairs the mapping forces: the mode named for the OS light
816
+ * preference must classify `light`, the dark one `dark`. Single source for
817
+ * BOTH the default fill and the conflict check in
818
+ * {@link resolveColorModeOptions} — one rule, one table.
819
+ */
820
+ const MAPPING_FORCED_SCHEMES = [["light", "light"], ["dark", "dark"]];
821
+ /**
822
+ * Merge an incoming option object over the one already on the theme, mirroring
823
+ * `merge`'s per-key override. Returns `undefined` when neither side supplied
824
+ * one, so an unconfigured theme never gains the key (byte parity, G4).
825
+ */
826
+ function mergeOptionObject(existing, incoming) {
827
+ const hasExisting = isObject(existing);
828
+ const hasIncoming = isObject(incoming);
829
+ if (!hasExisting && !hasIncoming) return void 0;
830
+ return {
831
+ ...hasExisting ? existing : {},
832
+ ...hasIncoming ? incoming : {}
833
+ };
834
+ }
835
+ /** Reject the reserved mode name (D4) wherever a mode set is declared or merged. */
836
+ function validateReservedModeNames(modeNames) {
837
+ 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(", ")}`);
838
+ }
839
+ /**
840
+ * Normalize AND validate the optional system-participation options against the
841
+ * declared modes, returning the normalized classification. One entry point on
842
+ * purpose: the D3-amendment fill (mapping-named modes default to their forced
843
+ * schemes) must always run before totality is checked, so exposing fill and
844
+ * validation separately would make "validate an unfilled map" representable.
845
+ * An explicit entry survives the fill's spread and is still conflict-checked,
846
+ * so a wrong value errors rather than being silently corrected. `undefined`
847
+ * passes through untouched — the classification stays opt-in, and a theme with
848
+ * only `systemPreference` emits no `color-scheme` (byte parity, G4).
849
+ *
850
+ * ALWAYS called with MERGED state — `theme.modes` unions across `addColorModes`
851
+ * calls and `from()` composition, so a per-call view both misses invalidation
852
+ * (a later mode declaration un-totals a carried classification) and invents
853
+ * false rejections (a mapping naming a mode declared by an earlier call).
854
+ * Run at both gates: `addColorModes` (fail fast) and `build()` (authoritative —
855
+ * `from()` composition never passes through `addColorModes`; only build()'s
856
+ * resolved map feeds emission and the manifest).
857
+ *
858
+ * Error tone mirrors `validateModeAliases`: `addColorModes:` prefix plus the
859
+ * available names.
860
+ */
861
+ function resolveColorModeOptions(modeNames, systemPreference, suppliedBrowserColorScheme) {
862
+ let browserColorScheme = suppliedBrowserColorScheme;
863
+ if (browserColorScheme && systemPreference) {
864
+ const forced = {};
865
+ for (const [axis, scheme] of MAPPING_FORCED_SCHEMES) {
866
+ const modeName = systemPreference[axis];
867
+ if (typeof modeName === "string") forced[modeName] = scheme;
868
+ }
869
+ browserColorScheme = {
870
+ ...forced,
871
+ ...suppliedBrowserColorScheme
872
+ };
873
+ }
874
+ const available = `Available modes: ${modeNames.join(", ")}`;
875
+ const declared = new Set(modeNames);
876
+ if (systemPreference) for (const axis of ["light", "dark"]) {
877
+ const modeName = systemPreference[axis];
878
+ if (typeof modeName !== "string" || modeName === "") throw new Error(`addColorModes: systemPreference requires both 'light' and 'dark' to name a declared mode — '${axis}' is missing. ${available}`);
879
+ 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}`);
880
+ if (!declared.has(modeName)) throw new Error(`addColorModes: systemPreference.${axis} references unknown mode '${modeName}'. ${available}`);
881
+ }
882
+ if (!browserColorScheme) return browserColorScheme;
883
+ for (const [modeName, value] of Object.entries(browserColorScheme)) {
884
+ if (!declared.has(modeName)) throw new Error(`addColorModes: browserColorScheme references unknown mode '${modeName}'. ${available}`);
885
+ if (!COLOR_SCHEME_VALUES.has(value)) throw new Error(`addColorModes: browserColorScheme['${modeName}'] must be 'light', 'dark', or 'normal', got ${JSON.stringify(value)}.`);
886
+ }
887
+ for (const modeName of modeNames) if (!(modeName in browserColorScheme)) throw new Error(`addColorModes: browserColorScheme must classify every declared mode — mode '${modeName}' is unclassified. ${available}`);
888
+ if (!systemPreference) return browserColorScheme;
889
+ for (const [axis, expected] of MAPPING_FORCED_SCHEMES) {
890
+ const modeName = systemPreference[axis];
891
+ const classification = browserColorScheme[modeName];
892
+ 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}'.`);
893
+ }
894
+ return browserColorScheme;
895
+ }
621
896
  /** Validate all color entries, throwing on invalid values. */
622
897
  function validateColors(colors) {
623
898
  for (const [key, value] of Object.entries(colors)) if (isObject(value)) validateColors(value);
@@ -627,14 +902,19 @@ function createState(theme) {
627
902
  return {
628
903
  theme: theme || { breakpoints: {} },
629
904
  emittedScales: /* @__PURE__ */ new Set(),
630
- contextualVars: /* @__PURE__ */ new Map()
905
+ contextualVars: /* @__PURE__ */ new Map(),
906
+ contextualVarRegistrations: /* @__PURE__ */ new Map(),
907
+ hasLegacyManifestSource: false
631
908
  };
632
909
  }
633
910
  function copyState(state, nextTheme) {
634
911
  const next = {
635
912
  theme: nextTheme,
636
913
  emittedScales: new Set(state.emittedScales),
637
- contextualVars: /* @__PURE__ */ new Map()
914
+ contextualVars: /* @__PURE__ */ new Map(),
915
+ contextualVarRegistrations: new Map(state.contextualVarRegistrations),
916
+ ...state.carriedManifestV2 ? { carriedManifestV2: { ...state.carriedManifestV2 } } : {},
917
+ hasLegacyManifestSource: state.hasLegacyManifestSource
638
918
  };
639
919
  for (const [scale, vars] of state.contextualVars) next.contextualVars.set(scale, [...vars]);
640
920
  return next;
@@ -667,6 +947,17 @@ var ThemeBuilder = class ThemeBuilder {
667
947
  next._state.emittedScales.add(scale === "colors" ? "colors" : scale);
668
948
  }
669
949
  if (manifest?.contextualVars) for (const [scale, vars] of Object.entries(manifest.contextualVars)) next._state.contextualVars.set(scale, [...vars]);
950
+ if (manifest) if (manifest.manifestVersion === 2) {
951
+ next._state.carriedManifestV2 = {
952
+ tokenDefinitions: manifest.tokenDefinitions,
953
+ modeAliasDefinitions: manifest.modeAliasDefinitions,
954
+ registrations: manifest.registrations,
955
+ emitterVersion: manifest.emitterVersion,
956
+ contractHash: manifest.contractHash,
957
+ cssFragments: manifest.cssFragments
958
+ };
959
+ if (manifest.registrations) for (const [name, registration] of Object.entries(manifest.registrations)) next._state.contextualVarRegistrations.set(name, registration);
960
+ } else next._state.hasLegacyManifestSource = true;
670
961
  return next;
671
962
  }
672
963
  addColors(colors) {
@@ -676,31 +967,46 @@ var ThemeBuilder = class ThemeBuilder {
676
967
  next._state.emittedScales.add("colors");
677
968
  return next;
678
969
  }
679
- addColorModes(initialMode, modeConfig) {
970
+ addColorModes(initialMode, modeConfig, options) {
680
971
  const nestedColors = this._state.theme.colors || {};
681
972
  const flatColors = flattenToDotPaths(nestedColors);
682
973
  const flatColorKeys = Object.keys(flatColors);
974
+ const existingModes = isObject(this._state.theme.modes) ? this._state.theme.modes : {};
975
+ const modeNames = Object.keys({
976
+ ...existingModes,
977
+ ...modeConfig
978
+ });
979
+ validateReservedModeNames(modeNames);
683
980
  for (const [modeName, modeAliases] of Object.entries(modeConfig)) validateModeAliases(modeName, modeAliases, nestedColors, flatColorKeys, "");
981
+ const systemPreference = mergeOptionObject(this._state.theme.systemPreference, options?.systemPreference);
982
+ const browserColorScheme = mergeOptionObject(this._state.theme.browserColorScheme, options?.browserColorScheme);
983
+ resolveColorModeOptions(modeNames, systemPreference, browserColorScheme);
684
984
  const nextTheme = merge({}, this._state.theme, {
685
985
  modes: modeConfig,
686
- mode: initialMode
986
+ mode: initialMode,
987
+ ...systemPreference ? { systemPreference } : {},
988
+ ...browserColorScheme ? { browserColorScheme } : {}
687
989
  });
688
990
  return new ThemeBuilder(copyState(this._state, nextTheme));
689
991
  }
690
992
  addScale(config) {
691
993
  const { name, values, emit } = config;
994
+ 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
995
  const nextTheme = merge({}, this._state.theme, { [name]: values });
693
996
  const next = new ThemeBuilder(copyState(this._state, nextTheme));
694
997
  if (emit) next._state.emittedScales.add(name);
695
998
  return next;
696
999
  }
697
- declareContextualVars(vars) {
1000
+ declareContextualVars(vars, registrations) {
698
1001
  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
1002
  const next = new ThemeBuilder(copyState(this._state, this._state.theme));
700
1003
  for (const [scale, names] of Object.entries(vars)) {
701
1004
  const existing = next._state.contextualVars.get(scale) || [];
702
1005
  next._state.contextualVars.set(scale, [...existing, ...names]);
703
1006
  }
1007
+ if (registrations) {
1008
+ for (const [name, registration] of Object.entries(registrations)) if (registration) next._state.contextualVarRegistrations.set(name, registration);
1009
+ }
704
1010
  return next;
705
1011
  }
706
1012
  extendScale(key, updateFn) {
@@ -715,7 +1021,11 @@ var ThemeBuilder = class ThemeBuilder {
715
1021
  const theme = merge({}, this._state.theme);
716
1022
  const emittedScales = this._state.emittedScales;
717
1023
  const contextualVars = this._state.contextualVars;
718
- const { tokenMap, variableMap, variables, modeVariables, modeTokens } = flattenTheme(theme, emittedScales);
1024
+ const systemPreference = isObject(theme.systemPreference) ? theme.systemPreference : void 0;
1025
+ const mergedModeNames = isObject(theme.modes) ? Object.keys(theme.modes) : [];
1026
+ validateReservedModeNames(mergedModeNames);
1027
+ const browserColorScheme = resolveColorModeOptions(mergedModeNames, systemPreference, isObject(theme.browserColorScheme) ? theme.browserColorScheme : void 0);
1028
+ const { tokenMap, variableMap, variables, modeVariables, modeTokens, tokenDefinitions, modeAliasDefinitions } = flattenTheme(theme, emittedScales);
719
1029
  resolveTokenRefs(tokenMap, variableMap, emittedScales);
720
1030
  const bpVariables = {};
721
1031
  if (theme.breakpoints && isObject(theme.breakpoints)) for (const [key, value] of Object.entries(theme.breakpoints)) bpVariables[`--breakpoint-${key}`] = `${value}px`;
@@ -724,7 +1034,44 @@ var ThemeBuilder = class ThemeBuilder {
724
1034
  contextualVarsSerialized = {};
725
1035
  for (const [scale, vars] of contextualVars) contextualVarsSerialized[scale] = vars;
726
1036
  }
727
- const variableCss = buildVariableCss(variables, bpVariables, modeVariables);
1037
+ const propertyCss = buildPropertyRegistrationCss(contextualVars, this._state.contextualVarRegistrations);
1038
+ const baseVariableCss = buildVariableCss(variables, bpVariables, modeVariables, {
1039
+ initialMode: typeof theme.mode === "string" ? theme.mode : void 0,
1040
+ systemPreference,
1041
+ browserColorScheme
1042
+ });
1043
+ const variableCss = propertyCss ? baseVariableCss ? `${propertyCss}\n\n${baseVariableCss}` : propertyCss : baseVariableCss;
1044
+ let manifestV2Fields = {};
1045
+ if (!this._state.hasLegacyManifestSource) {
1046
+ const registrations = Object.fromEntries(this._state.contextualVarRegistrations);
1047
+ const cssFragments = [];
1048
+ if (propertyCss) cssFragments.push({
1049
+ id: "registrations",
1050
+ kind: "registrations",
1051
+ cssText: propertyCss
1052
+ });
1053
+ if (baseVariableCss) cssFragments.push({
1054
+ id: "base",
1055
+ kind: "base",
1056
+ cssText: baseVariableCss
1057
+ });
1058
+ manifestV2Fields = {
1059
+ manifestVersion: 2,
1060
+ tokenDefinitions,
1061
+ modeAliasDefinitions,
1062
+ registrations,
1063
+ emitterVersion: EMITTER_VERSION,
1064
+ contractHash: computeContractHash({
1065
+ tokenDefinitions,
1066
+ modeAliasDefinitions,
1067
+ initialMode: typeof theme.mode === "string" ? theme.mode : void 0,
1068
+ registrations,
1069
+ systemPreference,
1070
+ browserColorScheme
1071
+ }),
1072
+ cssFragments
1073
+ };
1074
+ }
728
1075
  const manifest = {
729
1076
  tokenMap: {
730
1077
  ...tokenMap,
@@ -733,7 +1080,10 @@ var ThemeBuilder = class ThemeBuilder {
733
1080
  variableMap,
734
1081
  modes: modeTokens,
735
1082
  variableCss,
736
- ...contextualVarsSerialized ? { contextualVars: contextualVarsSerialized } : {}
1083
+ ...contextualVarsSerialized ? { contextualVars: contextualVarsSerialized } : {},
1084
+ ...systemPreference ? { systemPreference } : {},
1085
+ ...browserColorScheme ? { browserColorScheme } : {},
1086
+ ...manifestV2Fields
737
1087
  };
738
1088
  Object.defineProperty(theme, "manifest", {
739
1089
  value: manifest,
@@ -787,9 +1137,11 @@ function flattenTheme(theme, emittedScales) {
787
1137
  const variables = {};
788
1138
  const modeVariables = {};
789
1139
  const modeTokens = {};
1140
+ const tokenDefinitions = {};
1141
+ const modeAliasDefinitions = {};
790
1142
  for (const [scaleName, scaleValue] of Object.entries(theme)) {
791
1143
  if (scaleName.startsWith("_")) continue;
792
- if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes") continue;
1144
+ if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes" || scaleName === "systemPreference" || scaleName === "browserColorScheme") continue;
793
1145
  if (typeof scaleValue === "function") continue;
794
1146
  if (!isObject(scaleValue)) continue;
795
1147
  const flat = flattenToDotPaths(scaleValue);
@@ -798,6 +1150,7 @@ function flattenTheme(theme, emittedScales) {
798
1150
  const tokenPath = `${scaleName}.${dotKey}`;
799
1151
  const dashKey = dotToDash(dotKey);
800
1152
  const varName = `--${scaleName === "colors" ? "color" : scaleName}-${dashKey}`;
1153
+ tokenDefinitions[tokenPath] = parseTokenDefinition(String(rawValue));
801
1154
  if (isEmitted) {
802
1155
  tokenMap[tokenPath] = `var(${varName})`;
803
1156
  variableMap[tokenPath] = varName;
@@ -812,15 +1165,18 @@ function flattenTheme(theme, emittedScales) {
812
1165
  const flatAliases = flattenToDotPaths(modeAliases);
813
1166
  const modeVars = {};
814
1167
  const modeVals = {};
1168
+ const modeAliasDefs = {};
815
1169
  for (const [aliasDotKey, colorRef] of Object.entries(flatAliases)) {
816
1170
  if (typeof colorRef !== "string") continue;
817
1171
  const varName = `--color-${dotToDash(aliasDotKey)}`;
1172
+ modeAliasDefs[aliasDotKey] = colorRef;
818
1173
  const rawValue = flatColors[colorRef];
819
1174
  modeVals[`colors.${aliasDotKey}`] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
820
1175
  modeVars[varName] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
821
1176
  }
822
1177
  modeVariables[modeName] = modeVars;
823
1178
  modeTokens[modeName] = modeVals;
1179
+ modeAliasDefinitions[modeName] = modeAliasDefs;
824
1180
  }
825
1181
  const initialMode = theme.mode;
826
1182
  if (initialMode && modeVariables[initialMode]) {
@@ -842,10 +1198,239 @@ function flattenTheme(theme, emittedScales) {
842
1198
  variableMap,
843
1199
  variables,
844
1200
  modeVariables,
845
- modeTokens
1201
+ modeTokens,
1202
+ tokenDefinitions,
1203
+ modeAliasDefinitions
846
1204
  };
847
1205
  }
848
1206
  /**
1207
+ * Classify a RAW token value into its authored form (manifest v2, D6). MUST
1208
+ * run before `resolveTokenRefs` — resolution rewrites the string, and the
1209
+ * authored graph cannot be reconstructed from resolved CSS (D8). Uses
1210
+ * `matchAll` so the shared global {@link TOKEN_REF_RE} never carries a stale
1211
+ * `lastIndex` between callers.
1212
+ */
1213
+ function parseTokenDefinition(rawValue) {
1214
+ const references = [];
1215
+ for (const match of rawValue.matchAll(TOKEN_REF_RE)) {
1216
+ const ref = match[1];
1217
+ const slashIdx = ref.indexOf("/");
1218
+ references.push(slashIdx === -1 ? { path: ref } : {
1219
+ path: ref.slice(0, slashIdx),
1220
+ opacity: ref.slice(slashIdx + 1)
1221
+ });
1222
+ }
1223
+ if (references.length === 0) return {
1224
+ kind: "literal",
1225
+ value: rawValue
1226
+ };
1227
+ return {
1228
+ kind: "reference",
1229
+ value: rawValue,
1230
+ references
1231
+ };
1232
+ }
1233
+ /**
1234
+ * Version of the CSS emitter that composed a manifest's fragments. Bump when
1235
+ * ANY emitted byte changes for the same authored input — composition (D8)
1236
+ * regenerates dirty sections "under the pinned emitter version", and this is
1237
+ * that pin.
1238
+ */
1239
+ const EMITTER_VERSION = 1;
1240
+ /**
1241
+ * Sorted-key canonical form for hashing: object keys are emitted in sorted
1242
+ * order at every depth so the digest is independent of insertion order;
1243
+ * arrays keep authored order (reference order is contractual).
1244
+ */
1245
+ function canonicalize(value) {
1246
+ if (Array.isArray(value)) return value.map(canonicalize);
1247
+ if (isObject(value)) {
1248
+ const record = value;
1249
+ const sorted = {};
1250
+ for (const key of Object.keys(record).sort()) sorted[key] = canonicalize(record[key]);
1251
+ return sorted;
1252
+ }
1253
+ return value;
1254
+ }
1255
+ /**
1256
+ * sha256 hex digest. Primary path is `node:crypto` `createHash('sha256')`,
1257
+ * reached via `process.getBuiltinModule` so this module gains NO static
1258
+ * `node:crypto` import edge: built themes execute inside client bundles today
1259
+ * (e2e fixtures inline `createTheme(…).build()` at module init), and a static
1260
+ * builtin import is exactly what `./bootstrap` documents as forbidden in app
1261
+ * bundles. Non-Node runtimes use the pure fallback below, which produces
1262
+ * identical hex for identical input — determinism holds across environments.
1263
+ */
1264
+ function sha256Hex(input) {
1265
+ const nodeCrypto = globalThis.process?.getBuiltinModule?.("node:crypto");
1266
+ if (nodeCrypto?.createHash) return nodeCrypto.createHash("sha256").update(input).digest("hex");
1267
+ return sha256HexFallback(input);
1268
+ }
1269
+ /** SHA-256 round constants (FIPS 180-4 §4.2.2). */
1270
+ const SHA256_K = new Uint32Array([
1271
+ 1116352408,
1272
+ 1899447441,
1273
+ 3049323471,
1274
+ 3921009573,
1275
+ 961987163,
1276
+ 1508970993,
1277
+ 2453635748,
1278
+ 2870763221,
1279
+ 3624381080,
1280
+ 310598401,
1281
+ 607225278,
1282
+ 1426881987,
1283
+ 1925078388,
1284
+ 2162078206,
1285
+ 2614888103,
1286
+ 3248222580,
1287
+ 3835390401,
1288
+ 4022224774,
1289
+ 264347078,
1290
+ 604807628,
1291
+ 770255983,
1292
+ 1249150122,
1293
+ 1555081692,
1294
+ 1996064986,
1295
+ 2554220882,
1296
+ 2821834349,
1297
+ 2952996808,
1298
+ 3210313671,
1299
+ 3336571891,
1300
+ 3584528711,
1301
+ 113926993,
1302
+ 338241895,
1303
+ 666307205,
1304
+ 773529912,
1305
+ 1294757372,
1306
+ 1396182291,
1307
+ 1695183700,
1308
+ 1986661051,
1309
+ 2177026350,
1310
+ 2456956037,
1311
+ 2730485921,
1312
+ 2820302411,
1313
+ 3259730800,
1314
+ 3345764771,
1315
+ 3516065817,
1316
+ 3600352804,
1317
+ 4094571909,
1318
+ 275423344,
1319
+ 430227734,
1320
+ 506948616,
1321
+ 659060556,
1322
+ 883997877,
1323
+ 958139571,
1324
+ 1322822218,
1325
+ 1537002063,
1326
+ 1747873779,
1327
+ 1955562222,
1328
+ 2024104815,
1329
+ 2227730452,
1330
+ 2361852424,
1331
+ 2428436474,
1332
+ 2756734187,
1333
+ 3204031479,
1334
+ 3329325298
1335
+ ]);
1336
+ function rotr(x, n) {
1337
+ return (x >>> n | x << 32 - n) >>> 0;
1338
+ }
1339
+ /**
1340
+ * UTF-8 bytes of `input` without WHATWG `TextEncoder`: the Rust system-loader
1341
+ * evaluates the system bundle in QuickJS, which provides ES built-ins only —
1342
+ * no Node globals and no WHATWG APIs. Iterating by code point handles
1343
+ * surrogate pairs; lone surrogates cannot reach this path because the input
1344
+ * is well-formed `JSON.stringify` output (ES2019 escapes them as `\uXXXX`).
1345
+ */
1346
+ function utf8Bytes(input) {
1347
+ const bytes = [];
1348
+ for (const char of input) {
1349
+ const code = char.codePointAt(0);
1350
+ if (code < 128) bytes.push(code);
1351
+ else if (code < 2048) bytes.push(192 | code >> 6, 128 | code & 63);
1352
+ else if (code < 65536) bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
1353
+ else bytes.push(240 | code >> 18, 128 | code >> 12 & 63, 128 | code >> 6 & 63, 128 | code & 63);
1354
+ }
1355
+ return new Uint8Array(bytes);
1356
+ }
1357
+ /**
1358
+ * Pure SHA-256 (FIPS 180-4) over the UTF-8 bytes of `input` — the non-Node
1359
+ * fallback for {@link sha256Hex}. Not a security surface: the contract hash
1360
+ * is a content digest for composition-identity comparison only.
1361
+ */
1362
+ function sha256HexFallback(input) {
1363
+ const bytes = utf8Bytes(input);
1364
+ const bitLength = bytes.length * 8;
1365
+ const paddedLength = (bytes.length + 8 >> 6) + 1 << 6 >>> 0;
1366
+ const padded = new Uint8Array(paddedLength);
1367
+ padded.set(bytes);
1368
+ padded[bytes.length] = 128;
1369
+ const view = new DataView(padded.buffer);
1370
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
1371
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
1372
+ const state = new Uint32Array([
1373
+ 1779033703,
1374
+ 3144134277,
1375
+ 1013904242,
1376
+ 2773480762,
1377
+ 1359893119,
1378
+ 2600822924,
1379
+ 528734635,
1380
+ 1541459225
1381
+ ]);
1382
+ const w = new Uint32Array(64);
1383
+ for (let offset = 0; offset < paddedLength; offset += 64) {
1384
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
1385
+ for (let i = 16; i < 64; i++) {
1386
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
1387
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
1388
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1;
1389
+ }
1390
+ let a = state[0];
1391
+ let b = state[1];
1392
+ let c = state[2];
1393
+ let d = state[3];
1394
+ let e = state[4];
1395
+ let f = state[5];
1396
+ let g = state[6];
1397
+ let h = state[7];
1398
+ for (let i = 0; i < 64; i++) {
1399
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1400
+ const ch = e & f ^ ~e & g;
1401
+ const temp1 = h + S1 + ch + SHA256_K[i] + w[i] >>> 0;
1402
+ const temp2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
1403
+ h = g;
1404
+ g = f;
1405
+ f = e;
1406
+ e = d + temp1 >>> 0;
1407
+ d = c;
1408
+ c = b;
1409
+ b = a;
1410
+ a = temp1 + temp2 >>> 0;
1411
+ }
1412
+ state[0] += a;
1413
+ state[1] += b;
1414
+ state[2] += c;
1415
+ state[3] += d;
1416
+ state[4] += e;
1417
+ state[5] += f;
1418
+ state[6] += g;
1419
+ state[7] += h;
1420
+ }
1421
+ let hex = "";
1422
+ for (const word of state) hex += word.toString(16).padStart(8, "0");
1423
+ return hex;
1424
+ }
1425
+ /**
1426
+ * Stable digest over the canonical authored inputs. Identical authored input
1427
+ * ⇒ identical hash, across processes: keys sort at every depth and
1428
+ * `JSON.stringify` drops `undefined`-valued members deterministically.
1429
+ */
1430
+ function computeContractHash(input) {
1431
+ return sha256Hex(JSON.stringify(canonicalize(input)));
1432
+ }
1433
+ /**
849
1434
  * Resolve token refs ({scale.key}) in all flattened token values.
850
1435
  * Operates on the flattened tokenMap — does NOT mutate the nested theme.
851
1436
  */
@@ -882,19 +1467,56 @@ function resolveTokenRefs(tokenMap, _variableMap, _emittedScales) {
882
1467
  if (resolved !== value) tokenMap[tokenPath] = resolved;
883
1468
  }
884
1469
  }
1470
+ /**
1471
+ * Build `@property` registration rules for registered contextual vars.
1472
+ * The emitted custom property is `--${name}` — the same name the Rust resolver
1473
+ * maps a bare contextual var value to. Rules emit in declaration order and only
1474
+ * for names that are genuinely declared contextual vars. Returns `''` when no
1475
+ * registration metadata was supplied (opt-in / byte-identical guarantee).
1476
+ */
1477
+ function buildPropertyRegistrationCss(contextualVars, registrations) {
1478
+ if (registrations.size === 0) return "";
1479
+ const declaredNames = /* @__PURE__ */ new Set();
1480
+ for (const names of contextualVars.values()) for (const name of names) declaredNames.add(name);
1481
+ const blocks = [];
1482
+ for (const [name, registration] of registrations) {
1483
+ if (!declaredNames.has(name)) continue;
1484
+ const descriptors = [`syntax: "${registration.syntax}";`, `inherits: ${registration.inherits};`];
1485
+ if (registration.initialValue !== void 0) descriptors.push(`initial-value: ${registration.initialValue};`);
1486
+ blocks.push(`@property --${name} { ${descriptors.join(" ")} }`);
1487
+ }
1488
+ return blocks.join("\n");
1489
+ }
885
1490
  /** Build CSS variable blocks from flattened data. */
886
- function buildVariableCss(rootVariables, breakpointVariables, modeVariables) {
1491
+ function buildVariableCss(rootVariables, breakpointVariables, modeVariables, systemEmission = {}) {
1492
+ const { initialMode, systemPreference, browserColorScheme } = systemEmission;
887
1493
  const parts = [];
888
1494
  const rootLines = [];
889
1495
  for (const [varName, value] of Object.entries(rootVariables)) rootLines.push(` ${varName}: ${value};`);
890
1496
  for (const [varName, value] of Object.entries(breakpointVariables)) rootLines.push(` ${varName}: ${value};`);
1497
+ if (browserColorScheme && initialMode) {
1498
+ const initialScheme = browserColorScheme[initialMode];
1499
+ if (initialScheme) rootLines.push(` color-scheme: ${initialScheme};`);
1500
+ }
891
1501
  if (rootLines.length > 0) parts.push(`:root {\n${rootLines.join("\n")}\n}`);
1502
+ if (systemPreference) for (const scheme of ["light", "dark"]) {
1503
+ const modeName = systemPreference[scheme];
1504
+ const mediaLines = [];
1505
+ const modeVars = modeVariables[modeName];
1506
+ if (modeVars) for (const [varName, value] of Object.entries(modeVars)) mediaLines.push(` ${varName}: ${value};`);
1507
+ const mediaScheme = browserColorScheme?.[modeName];
1508
+ if (mediaScheme) mediaLines.push(` color-scheme: ${mediaScheme};`);
1509
+ if (mediaLines.length === 0) continue;
1510
+ parts.push(`@media (prefers-color-scheme: ${scheme}) {\n :root:not([data-color-mode]) {\n${mediaLines.join("\n")}\n }\n}`);
1511
+ }
892
1512
  for (const [modeName, modeVars] of Object.entries(modeVariables)) {
893
1513
  const modeLines = [];
894
1514
  for (const [varName, value] of Object.entries(modeVars)) modeLines.push(` ${varName}: ${value};`);
1515
+ const modeScheme = browserColorScheme?.[modeName];
1516
+ if (modeScheme) modeLines.push(` color-scheme: ${modeScheme};`);
895
1517
  if (modeLines.length > 0) parts.push(`[data-color-mode="${modeName}"] {\n${modeLines.join("\n")}\n}`);
896
1518
  }
897
1519
  return parts.join("\n\n");
898
1520
  }
899
1521
  //#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 };
1522
+ 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 };