@animus-ui/system 0.1.3 → 0.1.4
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.
- package/README.md +31 -6
- package/dist/SystemBuilder.d.ts +143 -9
- package/dist/SystemBuilder.d.ts.map +1 -1
- package/dist/asset.d.ts +25 -0
- package/dist/asset.d.ts.map +1 -0
- package/dist/groups/index.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +855 -146
- package/dist/selectors.d.ts +5 -1
- package/dist/selectors.d.ts.map +1 -1
- package/dist/{size-CusLCguT.js → size-CDBRRzkb.js} +35 -8
- package/dist/theme/createTheme.d.ts +125 -13
- package/dist/theme/createTheme.d.ts.map +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/resolveReferences.d.ts +49 -0
- package/dist/theme/resolveReferences.d.ts.map +1 -0
- package/dist/transforms/createTransform.d.ts +22 -0
- package/dist/transforms/createTransform.d.ts.map +1 -1
- package/dist/types/theme.d.ts +28 -4
- package/dist/types/theme.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as createClassResolver, r as createComponent, t as createComposedFamily } from "./createComposedFamily-DJ2aqfqN.js";
|
|
2
2
|
import { compose } from "./compose.js";
|
|
3
|
-
import { a as borderShorthand, c as
|
|
3
|
+
import { a as borderShorthand, c as numericScale, d as createTransform, i as gridItemRatio, l as stringScale, n as size, o as createScale, r as gridItem, s as numericOrStringScale, t as percentageOrAbsolute, u as areTransformsEqual } from "./size-CDBRRzkb.js";
|
|
4
4
|
//#region src/utils/deepMerge.ts
|
|
5
5
|
/**
|
|
6
6
|
* Deep merge utility — replaces lodash.merge for variant accumulation.
|
|
@@ -229,6 +229,20 @@ var Animus = class extends AnimusWithBase {
|
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
//#endregion
|
|
232
|
+
//#region src/asset.ts
|
|
233
|
+
/** The reserved scheme marking an unsubstituted asset reference. */
|
|
234
|
+
const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
|
|
235
|
+
/**
|
|
236
|
+
* Reference a package-owned asset (e.g. a font file) by module specifier:
|
|
237
|
+
* `asset('@acme/tokens/fonts/inter.woff2')`. Resolution — aliases, exports
|
|
238
|
+
* maps, `base`, content hashing — belongs to the host bundler; this function
|
|
239
|
+
* only brands the specifier into its placeholder form. A literal URL string
|
|
240
|
+
* (`'/fonts/inter.woff2'`) remains the pass-through alternative.
|
|
241
|
+
*/
|
|
242
|
+
function asset(specifier) {
|
|
243
|
+
return ASSET_PLACEHOLDER_PREFIX + specifier;
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
232
246
|
//#region src/conditions.ts
|
|
233
247
|
/**
|
|
234
248
|
* Built-in condition aliases (design D8, increment 06).
|
|
@@ -529,11 +543,15 @@ const BUILT_IN_SELECTORS = {
|
|
|
529
543
|
/**
|
|
530
544
|
* Merge user-provided selectors with built-in defaults.
|
|
531
545
|
* User selectors override built-in aliases of the same name.
|
|
532
|
-
* New aliases
|
|
546
|
+
* New aliases allocate orders CONTINUING from the highest existing order
|
|
547
|
+
* (floored at 490, so the first user alias lands at 500) rather than
|
|
548
|
+
* restarting at 500 each call (mirrors `mergeConditions`) — aliases from
|
|
549
|
+
* successive `.addSelectors()`/`extend()` merges must not collide on
|
|
550
|
+
* order 500.
|
|
533
551
|
*/
|
|
534
552
|
function mergeSelectors(base, custom) {
|
|
535
553
|
const merged = { ...base };
|
|
536
|
-
let nextOrder =
|
|
554
|
+
let nextOrder = Math.max(490, ...Object.values(merged).map((s) => s.order)) + 10;
|
|
537
555
|
for (const [key, selector] of Object.entries(custom)) if (key in merged) merged[key] = {
|
|
538
556
|
selector,
|
|
539
557
|
order: merged[key].order
|
|
@@ -567,13 +585,69 @@ function serializeSelectorMap(map) {
|
|
|
567
585
|
}
|
|
568
586
|
//#endregion
|
|
569
587
|
//#region src/SystemBuilder.ts
|
|
588
|
+
const snapshotTransformBySource = /* @__PURE__ */ new WeakMap();
|
|
589
|
+
function snapshotTransform(source) {
|
|
590
|
+
const cached = snapshotTransformBySource.get(source);
|
|
591
|
+
if (cached) return cached;
|
|
592
|
+
const wrapper = (value, property, props) => source(value, property, props);
|
|
593
|
+
Object.defineProperty(wrapper, "name", { value: source.name });
|
|
594
|
+
const sourceText = source.toString();
|
|
595
|
+
Object.defineProperty(wrapper, "toString", { value: () => sourceText });
|
|
596
|
+
const named = source;
|
|
597
|
+
if (named.transformName !== void 0) Object.defineProperty(wrapper, "transformName", {
|
|
598
|
+
value: named.transformName,
|
|
599
|
+
enumerable: true
|
|
600
|
+
});
|
|
601
|
+
if (named.transformSource !== void 0) Object.defineProperty(wrapper, "transformSource", {
|
|
602
|
+
value: named.transformSource,
|
|
603
|
+
enumerable: true
|
|
604
|
+
});
|
|
605
|
+
Object.freeze(wrapper);
|
|
606
|
+
snapshotTransformBySource.set(source, wrapper);
|
|
607
|
+
return wrapper;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The one runtime discriminator for a library bundle: `system.toConfig`
|
|
611
|
+
* being callable. A built system instance also has a `.system()` CHAIN
|
|
612
|
+
* METHOD, so presence of a `system` key alone cannot discriminate the two
|
|
613
|
+
* shapes. Both builders' `from()` use this guard; the QuickJS capture
|
|
614
|
+
* script in the Rust system-loader mirrors it by necessity (it cannot
|
|
615
|
+
* import TS) and points back here.
|
|
616
|
+
*/
|
|
617
|
+
function isLibraryBundle(value) {
|
|
618
|
+
const system = value?.system;
|
|
619
|
+
return Boolean(system) && typeof system?.toConfig === "function";
|
|
620
|
+
}
|
|
570
621
|
function orderedPropertiesEqual(existing, incoming) {
|
|
571
622
|
if (existing === incoming) return true;
|
|
572
623
|
if (!existing || !incoming || existing.length !== incoming.length) return false;
|
|
573
624
|
return existing.every((property, index) => property === incoming[index]);
|
|
574
625
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
626
|
+
function scalesEqual(existing, incoming) {
|
|
627
|
+
if (existing === incoming) return true;
|
|
628
|
+
if (!existing || !incoming || typeof existing !== typeof incoming) return false;
|
|
629
|
+
if (typeof existing === "string" || typeof incoming === "string") return false;
|
|
630
|
+
if (Array.isArray(existing) || Array.isArray(incoming)) return Array.isArray(existing) && Array.isArray(incoming) && existing.length === incoming.length && existing.every((value, index) => value === incoming[index]);
|
|
631
|
+
const existingMap = existing;
|
|
632
|
+
const incomingMap = incoming;
|
|
633
|
+
const existingKeys = Object.keys(existingMap).sort();
|
|
634
|
+
return orderedMembersEqual(existingKeys, Object.keys(incomingMap).sort()) && existingKeys.every((key) => existingMap[key] === incomingMap[key]);
|
|
635
|
+
}
|
|
636
|
+
function arePropDefinitionsEqual(existing, incoming, structuralScale = false) {
|
|
637
|
+
return existing.property === incoming.property && orderedPropertiesEqual(existing.properties, incoming.properties) && (structuralScale ? scalesEqual(existing.scale, incoming.scale) : existing.scale === incoming.scale) && existing.variable === incoming.variable && existing.negative === incoming.negative && existing.strict === incoming.strict && existing.currentVar === incoming.currentVar && areTransformsEqual(existing.transform, incoming.transform);
|
|
638
|
+
}
|
|
639
|
+
function orderedMembersEqual(existing, incoming) {
|
|
640
|
+
return existing.length === incoming.length && existing.every((member, index) => member === incoming[index]);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Divergent-prop error naming both definitions AND both origins — used by the
|
|
644
|
+
* `extend()` merge (sibling/dual-version conflicts, design D3/G4) and by
|
|
645
|
+
* `addGroup`/`addProps` when the colliding entry arrived through `extend()`
|
|
646
|
+
* (origin labels "extended source #n" / "builder state"). When no extension
|
|
647
|
+
* provenance exists, the pre-existing origin-less messages are kept verbatim.
|
|
648
|
+
*/
|
|
649
|
+
function divergentPropError(key, existing, incoming, existingOrigin, incomingOrigin) {
|
|
650
|
+
return /* @__PURE__ */ new Error(`Prop "${key}" already registered with a different definition. Existing (${existingOrigin}): property="${existing.property}", scale="${String(existing.scale)}". Incoming (${incomingOrigin}): property="${incoming.property}", scale="${String(incoming.scale)}".`);
|
|
577
651
|
}
|
|
578
652
|
var SystemBuilder = class SystemBuilder {
|
|
579
653
|
#propRegistry;
|
|
@@ -581,17 +655,107 @@ var SystemBuilder = class SystemBuilder {
|
|
|
581
655
|
#selectorRegistry;
|
|
582
656
|
#includesRegistry;
|
|
583
657
|
#conditionRegistry;
|
|
584
|
-
|
|
658
|
+
#extendProvenance;
|
|
659
|
+
#extendCount;
|
|
660
|
+
constructor(propRegistry, groupRegistry, selectorRegistry, includesRegistry, conditionRegistry, extendProvenance, extendCount) {
|
|
585
661
|
this.#propRegistry = propRegistry || {};
|
|
586
662
|
this.#groupRegistry = groupRegistry || {};
|
|
587
663
|
this.#selectorRegistry = selectorRegistry || { ...BUILT_IN_SELECTORS };
|
|
588
664
|
this.#includesRegistry = includesRegistry || [];
|
|
589
665
|
this.#conditionRegistry = conditionRegistry || { ...BUILT_IN_CONDITIONS };
|
|
666
|
+
this.#extendProvenance = extendProvenance || /* @__PURE__ */ new Map();
|
|
667
|
+
this.#extendCount = extendCount || 0;
|
|
668
|
+
}
|
|
669
|
+
#originOf(provenanceKey) {
|
|
670
|
+
const index = this.#extendProvenance.get(provenanceKey);
|
|
671
|
+
return index === void 0 ? "builder state" : `extended source #${index}`;
|
|
672
|
+
}
|
|
673
|
+
from(source) {
|
|
674
|
+
const instance = isLibraryBundle(source) ? source.system : source;
|
|
675
|
+
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, [...this.#includesRegistry, instance], this.#conditionRegistry, this.#extendProvenance, this.#extendCount);
|
|
676
|
+
}
|
|
677
|
+
extend(source) {
|
|
678
|
+
const instance = isLibraryBundle(source) ? source.system : source;
|
|
679
|
+
const snapshot = instance.getRegistrySnapshot?.();
|
|
680
|
+
if (!snapshot) throw new Error("extend: source system carries no registry snapshot — it was built by an older @animus-ui/system. Rebuild the source against this version (a lossy toConfig() reconstruction is never substituted).");
|
|
681
|
+
const sourceIndex = this.#extendCount + 1;
|
|
682
|
+
const incomingOrigin = `extended source #${sourceIndex}`;
|
|
683
|
+
const provenance = new Map(this.#extendProvenance);
|
|
684
|
+
const nextProps = { ...this.#propRegistry };
|
|
685
|
+
for (const [name, incoming] of Object.entries(snapshot.props)) {
|
|
686
|
+
if (name in this.#groupRegistry) throw new Error(`extend: prop "${name}" (${incomingOrigin}) collides with an existing group name (${this.#originOf(`group:${name}`)}). Group names and prop names must be disjoint.`);
|
|
687
|
+
const existing = nextProps[name];
|
|
688
|
+
if (!existing) {
|
|
689
|
+
nextProps[name] = incoming;
|
|
690
|
+
provenance.set(`prop:${name}`, sourceIndex);
|
|
691
|
+
} else if (!arePropDefinitionsEqual(existing, incoming, true)) throw divergentPropError(name, existing, incoming, this.#originOf(`prop:${name}`), incomingOrigin);
|
|
692
|
+
}
|
|
693
|
+
const nextGroups = { ...this.#groupRegistry };
|
|
694
|
+
for (const [name, incoming] of Object.entries(snapshot.groups)) {
|
|
695
|
+
const existing = nextGroups[name];
|
|
696
|
+
if (!existing) {
|
|
697
|
+
if (name in nextProps) throw new Error(`extend: group name "${name}" (${incomingOrigin}) collides with an existing prop name (${this.#originOf(`prop:${name}`)}). Group names and prop names must be disjoint.`);
|
|
698
|
+
nextGroups[name] = [...incoming];
|
|
699
|
+
provenance.set(`group:${name}`, sourceIndex);
|
|
700
|
+
} else if (!orderedMembersEqual(existing, incoming)) throw new Error(`extend: group "${name}" already registered with different membership. Existing (${this.#originOf(`group:${name}`)}): [${existing.join(", ")}]. Incoming (${incomingOrigin}): [${incoming.join(", ")}].`);
|
|
701
|
+
}
|
|
702
|
+
const selectorOverrides = {};
|
|
703
|
+
const newSelectors = {};
|
|
704
|
+
for (const [name, incoming] of Object.entries(snapshot.selectors)) {
|
|
705
|
+
const builtIn = BUILT_IN_SELECTORS[name];
|
|
706
|
+
if (builtIn && builtIn.selector === incoming.selector) continue;
|
|
707
|
+
if (name in this.#conditionRegistry) throw new Error(`extend: selector alias "${name}" (${incomingOrigin}) is already registered as a condition alias (${this.#originOf(`condition:${name}`)}); a name resolves through exactly one registry. Pick a distinct name.`);
|
|
708
|
+
const existing = this.#selectorRegistry[name];
|
|
709
|
+
if (!existing) {
|
|
710
|
+
newSelectors[name] = incoming.selector;
|
|
711
|
+
provenance.set(`selector:${name}`, sourceIndex);
|
|
712
|
+
} else if (existing.selector !== incoming.selector) {
|
|
713
|
+
const existingIndex = provenance.get(`selector:${name}`);
|
|
714
|
+
if (existingIndex === void 0) {
|
|
715
|
+
selectorOverrides[name] = {
|
|
716
|
+
selector: incoming.selector,
|
|
717
|
+
order: existing.order
|
|
718
|
+
};
|
|
719
|
+
provenance.set(`selector:${name}`, sourceIndex);
|
|
720
|
+
} else throw new Error(`extend: selector alias "${name}" already registered with a different selector. Existing (extended source #${existingIndex}): "${existing.selector}". Incoming (${incomingOrigin}): "${incoming.selector}".`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const nextSelectors = mergeSelectors({
|
|
724
|
+
...this.#selectorRegistry,
|
|
725
|
+
...selectorOverrides
|
|
726
|
+
}, newSelectors);
|
|
727
|
+
const conditionOverrides = {};
|
|
728
|
+
const newConditions = {};
|
|
729
|
+
for (const [name, incoming] of Object.entries(snapshot.conditions)) {
|
|
730
|
+
const builtIn = BUILT_IN_CONDITIONS[name];
|
|
731
|
+
if (builtIn && builtIn.value === incoming.value) continue;
|
|
732
|
+
if (name in nextSelectors) throw new Error(`extend: condition alias "${name}" (${incomingOrigin}) is already registered as a selector alias (${this.#originOf(`selector:${name}`)}); a name resolves through exactly one registry. Pick a distinct name.`);
|
|
733
|
+
const existing = this.#conditionRegistry[name];
|
|
734
|
+
if (!existing) {
|
|
735
|
+
newConditions[name] = incoming.value;
|
|
736
|
+
provenance.set(`condition:${name}`, sourceIndex);
|
|
737
|
+
} else if (existing.value !== incoming.value) {
|
|
738
|
+
const existingIndex = provenance.get(`condition:${name}`);
|
|
739
|
+
if (existingIndex === void 0) {
|
|
740
|
+
conditionOverrides[name] = {
|
|
741
|
+
value: incoming.value,
|
|
742
|
+
order: existing.order,
|
|
743
|
+
kind: incoming.kind
|
|
744
|
+
};
|
|
745
|
+
provenance.set(`condition:${name}`, sourceIndex);
|
|
746
|
+
} else throw new Error(`extend: condition alias "${name}" already registered with a different condition. Existing (extended source #${existingIndex}): "${existing.value}". Incoming (${incomingOrigin}): "${incoming.value}".`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const nextConditions = mergeConditions({
|
|
750
|
+
...this.#conditionRegistry,
|
|
751
|
+
...conditionOverrides
|
|
752
|
+
}, newConditions, new Set(Object.keys(nextSelectors)));
|
|
753
|
+
return new SystemBuilder(nextProps, nextGroups, nextSelectors, [...this.#includesRegistry, instance], nextConditions, provenance, sourceIndex);
|
|
590
754
|
}
|
|
591
755
|
addSelectors(selectors) {
|
|
592
756
|
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.`);
|
|
593
757
|
const merged = mergeSelectors(this.#selectorRegistry, selectors);
|
|
594
|
-
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, merged, this.#includesRegistry, this.#conditionRegistry);
|
|
758
|
+
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, merged, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount);
|
|
595
759
|
}
|
|
596
760
|
/**
|
|
597
761
|
* Register condition aliases (`_motionReduce`, `_cardSm`, …) → at-rule
|
|
@@ -605,14 +769,17 @@ var SystemBuilder = class SystemBuilder {
|
|
|
605
769
|
*/
|
|
606
770
|
addConditions(conditions) {
|
|
607
771
|
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);
|
|
772
|
+
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, merged, this.#extendProvenance, this.#extendCount);
|
|
609
773
|
}
|
|
610
774
|
addGroup(name, config) {
|
|
611
775
|
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.`);
|
|
612
776
|
for (const key of Object.keys(config)) if (key in this.#propRegistry) {
|
|
613
777
|
const existing = this.#propRegistry[key];
|
|
614
778
|
const incoming = config[key];
|
|
615
|
-
if (!arePropDefinitionsEqual(existing, incoming
|
|
779
|
+
if (!arePropDefinitionsEqual(existing, incoming, this.#extendProvenance.has(`prop:${key}`))) {
|
|
780
|
+
if (this.#extendProvenance.has(`prop:${key}`)) throw divergentPropError(key, existing, incoming, this.#originOf(`prop:${key}`), "builder state");
|
|
781
|
+
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)}".`);
|
|
782
|
+
}
|
|
616
783
|
}
|
|
617
784
|
const nextProps = {
|
|
618
785
|
...this.#propRegistry,
|
|
@@ -622,29 +789,33 @@ var SystemBuilder = class SystemBuilder {
|
|
|
622
789
|
return new SystemBuilder(nextProps, {
|
|
623
790
|
...this.#groupRegistry,
|
|
624
791
|
...newGroup
|
|
625
|
-
}, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry);
|
|
792
|
+
}, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount);
|
|
626
793
|
}
|
|
627
794
|
addProps(config) {
|
|
628
795
|
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.`);
|
|
629
796
|
for (const key of Object.keys(config)) if (key in this.#propRegistry) {
|
|
630
797
|
const existing = this.#propRegistry[key];
|
|
631
798
|
const incoming = config[key];
|
|
632
|
-
if (!arePropDefinitionsEqual(existing, incoming
|
|
799
|
+
if (!arePropDefinitionsEqual(existing, incoming, this.#extendProvenance.has(`prop:${key}`))) {
|
|
800
|
+
if (this.#extendProvenance.has(`prop:${key}`)) throw divergentPropError(key, existing, incoming, this.#originOf(`prop:${key}`), "builder state");
|
|
801
|
+
throw new Error(`Prop "${key}" already registered with a different definition.`);
|
|
802
|
+
}
|
|
633
803
|
}
|
|
634
804
|
return new SystemBuilder({
|
|
635
805
|
...this.#propRegistry,
|
|
636
806
|
...config
|
|
637
|
-
}, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry);
|
|
807
|
+
}, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount);
|
|
638
808
|
}
|
|
639
809
|
build() {
|
|
640
|
-
const animus = new Animus(this.#propRegistry, this.#groupRegistry);
|
|
641
|
-
const
|
|
642
|
-
const groupRegistry = this.#groupRegistry;
|
|
643
|
-
const selectorRegistry = this.#selectorRegistry;
|
|
644
|
-
const conditionRegistry = this.#conditionRegistry;
|
|
810
|
+
const animus = new Animus(Object.fromEntries(Object.entries(this.#propRegistry).map(([key, entry]) => [key, { ...entry }])), Object.fromEntries(Object.entries(this.#groupRegistry).map(([key, members]) => [key, [...members]])));
|
|
811
|
+
const snapshot = createRegistrySnapshot(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, this.#conditionRegistry);
|
|
645
812
|
const system = Object.assign(animus, { toConfig: () => {
|
|
646
|
-
return serializeInstance(
|
|
813
|
+
return serializeInstance(snapshot.props, snapshot.groups, snapshot.selectors, snapshot.conditions);
|
|
647
814
|
} });
|
|
815
|
+
Object.defineProperty(system, "getRegistrySnapshot", {
|
|
816
|
+
value: () => snapshot,
|
|
817
|
+
enumerable: false
|
|
818
|
+
});
|
|
648
819
|
const createGlobalStyles = ((styles, options) => ({
|
|
649
820
|
__brand: "GlobalStyleBlock",
|
|
650
821
|
styles,
|
|
@@ -658,9 +829,41 @@ var SystemBuilder = class SystemBuilder {
|
|
|
658
829
|
};
|
|
659
830
|
}
|
|
660
831
|
};
|
|
832
|
+
/**
|
|
833
|
+
* Freeze the builder's registries into the build-time snapshot (design D7):
|
|
834
|
+
* containers, per-entry objects, and the mutable values nested inside a prop
|
|
835
|
+
* (`properties` arrays, object/array scales) are copies, so neither the
|
|
836
|
+
* builder's onward chaining nor post-build mutation of the instance's public
|
|
837
|
+
* registry fields reaches serialized or merged output. Transform functions
|
|
838
|
+
* are cached immutable forwarding wrappers: behavior survives without keeping
|
|
839
|
+
* mutable serialization metadata live.
|
|
840
|
+
*/
|
|
841
|
+
function createRegistrySnapshot(propRegistry, groupRegistry, selectorRegistry, conditionRegistry) {
|
|
842
|
+
const props = {};
|
|
843
|
+
for (const [name, entry] of Object.entries(propRegistry)) {
|
|
844
|
+
const copy = { ...entry };
|
|
845
|
+
if (copy.properties) copy.properties = Object.freeze([...copy.properties]);
|
|
846
|
+
if (copy.scale && typeof copy.scale === "object") copy.scale = Object.freeze(Array.isArray(copy.scale) ? [...copy.scale] : { ...copy.scale });
|
|
847
|
+
if (copy.transform) copy.transform = snapshotTransform(copy.transform);
|
|
848
|
+
props[name] = Object.freeze(copy);
|
|
849
|
+
}
|
|
850
|
+
const groups = {};
|
|
851
|
+
for (const [name, members] of Object.entries(groupRegistry)) groups[name] = Object.freeze([...members]);
|
|
852
|
+
const selectors = {};
|
|
853
|
+
for (const [name, entry] of Object.entries(selectorRegistry)) selectors[name] = Object.freeze({ ...entry });
|
|
854
|
+
const conditions = {};
|
|
855
|
+
for (const [name, entry] of Object.entries(conditionRegistry)) conditions[name] = Object.freeze({ ...entry });
|
|
856
|
+
return Object.freeze({
|
|
857
|
+
props: Object.freeze(props),
|
|
858
|
+
groups: Object.freeze(groups),
|
|
859
|
+
selectors: Object.freeze(selectors),
|
|
860
|
+
conditions: Object.freeze(conditions)
|
|
861
|
+
});
|
|
862
|
+
}
|
|
661
863
|
function serializeInstance(propRegistry, groupRegistry, selectorRegistry, conditionRegistry) {
|
|
662
864
|
const serialized = {};
|
|
663
865
|
const transforms = {};
|
|
866
|
+
const transformOwners = {};
|
|
664
867
|
for (const [propName, entry] of Object.entries(propRegistry)) {
|
|
665
868
|
const s = { property: entry.property };
|
|
666
869
|
if (entry.properties && entry.properties.length > 0) s.properties = [...entry.properties];
|
|
@@ -672,8 +875,11 @@ function serializeInstance(propRegistry, groupRegistry, selectorRegistry, condit
|
|
|
672
875
|
const fn = entry.transform;
|
|
673
876
|
const name = fn.transformName ?? fn.name;
|
|
674
877
|
if (name) {
|
|
878
|
+
const existing = transforms[name];
|
|
879
|
+
if (existing && !areTransformsEqual(existing, fn)) throw new Error(`Transform name "${name}" is registered by both props "${transformOwners[name]}" and "${propName}" with different function instances. Share one cached transform instance or give the transforms distinct names.`);
|
|
675
880
|
s.transform = name;
|
|
676
881
|
transforms[name] = fn;
|
|
882
|
+
transformOwners[name] = propName;
|
|
677
883
|
}
|
|
678
884
|
}
|
|
679
885
|
if (entry.currentVar) s.currentVar = entry.currentVar;
|
|
@@ -693,6 +899,144 @@ function createSystem(config) {
|
|
|
693
899
|
return new SystemBuilder(void 0, void 0, void 0, config?.includes ?? []);
|
|
694
900
|
}
|
|
695
901
|
//#endregion
|
|
902
|
+
//#region src/theme/resolveReferences.ts
|
|
903
|
+
/**
|
|
904
|
+
* Late-binding token-reference resolution over the flattened theme (D4,
|
|
905
|
+
* openspec change first-class-extension).
|
|
906
|
+
*
|
|
907
|
+
* Replaces the old single-pass, insertion-order-dependent rewrite: references
|
|
908
|
+
* resolve against the COMPLETE flattened map via depth-first traversal of the
|
|
909
|
+
* reference DAG, so declaration order is never observable (G3), references
|
|
910
|
+
* inside emitted scales resolve into the emitted variable declarations
|
|
911
|
+
* instead of leaking literally into CSS (G2), and a token resolves to the
|
|
912
|
+
* same value whether its scale is emitted or inlined (G1 — an emitted target
|
|
913
|
+
* substitutes as `var()`, which the cascade late-binds to exactly the value
|
|
914
|
+
* the inlined form substitutes directly).
|
|
915
|
+
*
|
|
916
|
+
* Pure module: consumes the flatten pass's maps, returns fresh maps, mutates
|
|
917
|
+
* nothing. Runs in QuickJS — ES built-ins only, no Node/WHATWG APIs.
|
|
918
|
+
*/
|
|
919
|
+
/** Token ref pattern: {scale.key}, {scale.key.sub}, or {path/NN} (opacity). */
|
|
920
|
+
const TOKEN_REF_RE$1 = /\{([^}]+)\}/g;
|
|
921
|
+
/** DFS node states for cycle detection. */
|
|
922
|
+
const UNVISITED = 0;
|
|
923
|
+
const RESOLVING = 1;
|
|
924
|
+
const RESOLVED = 2;
|
|
925
|
+
/** Parse every `{...}` occurrence via matchAll — no shared lastIndex. */
|
|
926
|
+
function parseReferences(value) {
|
|
927
|
+
const references = [];
|
|
928
|
+
for (const match of value.matchAll(TOKEN_REF_RE$1)) {
|
|
929
|
+
const text = match[1];
|
|
930
|
+
const slashIdx = text.indexOf("/");
|
|
931
|
+
references.push(slashIdx === -1 ? {
|
|
932
|
+
text,
|
|
933
|
+
path: text
|
|
934
|
+
} : {
|
|
935
|
+
text,
|
|
936
|
+
path: text.slice(0, slashIdx),
|
|
937
|
+
opacity: text.slice(slashIdx + 1)
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return references;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Resolve token references over the complete flattened maps.
|
|
944
|
+
*
|
|
945
|
+
* - `tokenMap`/`variableMap`/`variables` are `flattenTheme`'s outputs: an
|
|
946
|
+
* emitted path's tokenMap entry is its `var()` indirection and its RAW
|
|
947
|
+
* authored value is parked in `variables`; a non-emitted path's tokenMap
|
|
948
|
+
* entry IS its raw value.
|
|
949
|
+
* - Substitution per reference: emitted target → `var(--target)`; non-emitted
|
|
950
|
+
* target → its resolved literal; `{path/NN}` keeps the existing
|
|
951
|
+
* `color-mix` output shape (`/0` → `transparent`, `/100` → the base).
|
|
952
|
+
* - Unresolvable target: warn once per missing path, keep the literal — a
|
|
953
|
+
* SUPPORTED pattern (a kit theme may reference tokens its consumer
|
|
954
|
+
* provides later). Dangling-reference errors arrive with the explicit
|
|
955
|
+
* replacement form (D5, increment 04).
|
|
956
|
+
* - Reference cycle: hard error naming the cycle's token paths in traversal
|
|
957
|
+
* order, regardless of emission flags (emission-invariant failure, G1).
|
|
958
|
+
*/
|
|
959
|
+
function resolveReferences(tokenMap, variableMap, variables) {
|
|
960
|
+
const paths = Object.keys(tokenMap).sort();
|
|
961
|
+
const known = new Set(paths);
|
|
962
|
+
/** The authored (raw) value behind a path — see the contract note above. */
|
|
963
|
+
const rawValueOf = (path) => {
|
|
964
|
+
const varName = variableMap[path];
|
|
965
|
+
if (varName !== void 0 && variables[varName] !== void 0) return variables[varName];
|
|
966
|
+
return tokenMap[path];
|
|
967
|
+
};
|
|
968
|
+
const referencesByPath = /* @__PURE__ */ new Map();
|
|
969
|
+
for (const path of paths) {
|
|
970
|
+
const raw = rawValueOf(path);
|
|
971
|
+
if (typeof raw === "string" && raw.includes("{")) {
|
|
972
|
+
const references = parseReferences(raw);
|
|
973
|
+
if (references.length > 0) referencesByPath.set(path, references);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const state = /* @__PURE__ */ new Map();
|
|
977
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
978
|
+
/** Active DFS trail — the cycle report slices it from the revisited node. */
|
|
979
|
+
const trail = [];
|
|
980
|
+
const warnedMissing = /* @__PURE__ */ new Set();
|
|
981
|
+
const substitute = (reference, match) => {
|
|
982
|
+
if (!known.has(reference.path)) {
|
|
983
|
+
if (!warnedMissing.has(reference.path)) {
|
|
984
|
+
warnedMissing.add(reference.path);
|
|
985
|
+
console.warn(`[animus] Token ref {${reference.text}} — path '${reference.path}' not found in token map`);
|
|
986
|
+
}
|
|
987
|
+
return match;
|
|
988
|
+
}
|
|
989
|
+
const targetVar = variableMap[reference.path];
|
|
990
|
+
const targetValue = resolved.get(reference.path);
|
|
991
|
+
const base = targetVar !== void 0 && !targetValue.includes("{") ? `var(${targetVar})` : targetValue;
|
|
992
|
+
if (reference.opacity !== void 0) {
|
|
993
|
+
const alpha = Number.parseInt(reference.opacity, 10);
|
|
994
|
+
if (Number.isNaN(alpha)) return base;
|
|
995
|
+
if (alpha === 0) return "transparent";
|
|
996
|
+
if (alpha !== 100) return `color-mix(in srgb, ${base} ${alpha}%, transparent)`;
|
|
997
|
+
}
|
|
998
|
+
return base;
|
|
999
|
+
};
|
|
1000
|
+
const resolvePath = (path) => {
|
|
1001
|
+
const status = state.get(path) ?? UNVISITED;
|
|
1002
|
+
if (status === RESOLVED) return;
|
|
1003
|
+
if (status === RESOLVING) {
|
|
1004
|
+
const cycle = [...trail.slice(trail.indexOf(path)), path];
|
|
1005
|
+
throw new Error(`build: token reference cycle — ${cycle.map((cyclePath) => `'${cyclePath}'`).join(" → ")}. Reference cycles cannot be resolved; give one of these tokens a literal value.`);
|
|
1006
|
+
}
|
|
1007
|
+
state.set(path, RESOLVING);
|
|
1008
|
+
trail.push(path);
|
|
1009
|
+
const references = referencesByPath.get(path);
|
|
1010
|
+
if (references) {
|
|
1011
|
+
for (const reference of references) if (known.has(reference.path)) resolvePath(reference.path);
|
|
1012
|
+
}
|
|
1013
|
+
let value = rawValueOf(path);
|
|
1014
|
+
if (references) {
|
|
1015
|
+
let index = 0;
|
|
1016
|
+
value = value.replace(TOKEN_REF_RE$1, (match) => substitute(references[index++], match));
|
|
1017
|
+
}
|
|
1018
|
+
trail.pop();
|
|
1019
|
+
state.set(path, RESOLVED);
|
|
1020
|
+
resolved.set(path, value);
|
|
1021
|
+
};
|
|
1022
|
+
for (const path of paths) resolvePath(path);
|
|
1023
|
+
const outTokenMap = {};
|
|
1024
|
+
const outVariables = {};
|
|
1025
|
+
for (const path of paths) {
|
|
1026
|
+
const varName = variableMap[path];
|
|
1027
|
+
if (varName === void 0) outTokenMap[path] = resolved.get(path);
|
|
1028
|
+
else {
|
|
1029
|
+
outTokenMap[path] = tokenMap[path];
|
|
1030
|
+
if (variables[varName] !== void 0) outVariables[varName] = resolved.get(path);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (const varName of Object.keys(variables).sort()) if (outVariables[varName] === void 0) outVariables[varName] = variables[varName];
|
|
1034
|
+
return {
|
|
1035
|
+
tokenMap: outTokenMap,
|
|
1036
|
+
variables: outVariables
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
//#endregion
|
|
696
1040
|
//#region src/theme/utils.ts
|
|
697
1041
|
/** Type guard: true for non-null, non-array objects */
|
|
698
1042
|
function isObject(value) {
|
|
@@ -797,16 +1141,18 @@ function validateModeAliases(modeName, aliases, nestedColors, flatColorKeys, pre
|
|
|
797
1141
|
* the `:root:not([data-color-mode])` guard while matching no mode block.
|
|
798
1142
|
*/
|
|
799
1143
|
const RESERVED_MODE_NAME = "system";
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1144
|
+
const RESERVED_THEME_KEYS = new Set([
|
|
1145
|
+
"breakpoints",
|
|
1146
|
+
"modes",
|
|
1147
|
+
"mode",
|
|
1148
|
+
"systemPreference",
|
|
1149
|
+
"browserColorScheme",
|
|
1150
|
+
"modeBases",
|
|
1151
|
+
"__emitted",
|
|
1152
|
+
"manifest",
|
|
1153
|
+
"serialize",
|
|
1154
|
+
"varRef"
|
|
1155
|
+
]);
|
|
810
1156
|
const COLOR_SCHEME_VALUES = new Set([
|
|
811
1157
|
"light",
|
|
812
1158
|
"dark",
|
|
@@ -894,6 +1240,33 @@ function resolveColorModeOptions(modeNames, systemPreference, suppliedBrowserCol
|
|
|
894
1240
|
}
|
|
895
1241
|
return browserColorScheme;
|
|
896
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Validate the D6 `basedOn` mode-base map against the MERGED mode set: every
|
|
1245
|
+
* key and every base must name a declared mode, self-bases are rejected, and
|
|
1246
|
+
* base chains must terminate (a cycle can never fill coverage). Runs at both
|
|
1247
|
+
* gates like the other mode options — `addColorModes` (fail fast) and
|
|
1248
|
+
* `build()` (authoritative: extend/from composition merges modes without
|
|
1249
|
+
* passing through `addColorModes`).
|
|
1250
|
+
*/
|
|
1251
|
+
function validateModeBases(modeNames, modeBases) {
|
|
1252
|
+
if (!modeBases) return;
|
|
1253
|
+
const declared = new Set(modeNames);
|
|
1254
|
+
const available = `Available modes: ${modeNames.join(", ")}`;
|
|
1255
|
+
for (const [modeName, base] of Object.entries(modeBases)) {
|
|
1256
|
+
if (!declared.has(modeName)) throw new Error(`addColorModes: basedOn names unknown mode '${modeName}'. ${available}`);
|
|
1257
|
+
if (typeof base !== "string" || !declared.has(base)) throw new Error(`addColorModes: basedOn['${modeName}'] references unknown base mode '${String(base)}'. ${available}`);
|
|
1258
|
+
if (base === modeName) throw new Error(`addColorModes: basedOn['${modeName}'] cannot base a mode on itself.`);
|
|
1259
|
+
}
|
|
1260
|
+
for (const start of Object.keys(modeBases)) {
|
|
1261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1262
|
+
let cursor = start;
|
|
1263
|
+
while (cursor !== void 0) {
|
|
1264
|
+
if (seen.has(cursor)) throw new Error(`addColorModes: basedOn chain cycles — ${[...seen, cursor].map((mode) => `'${mode}'`).join(" → ")}. Give one mode in the chain a covered literal base.`);
|
|
1265
|
+
seen.add(cursor);
|
|
1266
|
+
cursor = modeBases[cursor];
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
897
1270
|
/** Validate all color entries, throwing on invalid values. */
|
|
898
1271
|
function validateColors(colors) {
|
|
899
1272
|
for (const [key, value] of Object.entries(colors)) if (isObject(value)) validateColors(value);
|
|
@@ -905,7 +1278,12 @@ function createState(theme) {
|
|
|
905
1278
|
emittedScales: /* @__PURE__ */ new Set(),
|
|
906
1279
|
contextualVars: /* @__PURE__ */ new Map(),
|
|
907
1280
|
contextualVarRegistrations: /* @__PURE__ */ new Map(),
|
|
908
|
-
hasLegacyManifestSource: false
|
|
1281
|
+
hasLegacyManifestSource: false,
|
|
1282
|
+
extendProvenance: /* @__PURE__ */ new Map(),
|
|
1283
|
+
extendCount: 0,
|
|
1284
|
+
inheritedModes: /* @__PURE__ */ new Set(),
|
|
1285
|
+
inheritedModeAliases: /* @__PURE__ */ new Set(),
|
|
1286
|
+
droppedTokenPaths: /* @__PURE__ */ new Map()
|
|
909
1287
|
};
|
|
910
1288
|
}
|
|
911
1289
|
function copyState(state, nextTheme) {
|
|
@@ -915,12 +1293,101 @@ function copyState(state, nextTheme) {
|
|
|
915
1293
|
contextualVars: /* @__PURE__ */ new Map(),
|
|
916
1294
|
contextualVarRegistrations: new Map(state.contextualVarRegistrations),
|
|
917
1295
|
...state.carriedManifestV2 ? { carriedManifestV2: { ...state.carriedManifestV2 } } : {},
|
|
918
|
-
hasLegacyManifestSource: state.hasLegacyManifestSource
|
|
1296
|
+
hasLegacyManifestSource: state.hasLegacyManifestSource,
|
|
1297
|
+
extendProvenance: new Map(state.extendProvenance),
|
|
1298
|
+
extendCount: state.extendCount,
|
|
1299
|
+
inheritedModes: new Set(state.inheritedModes),
|
|
1300
|
+
inheritedModeAliases: new Set(state.inheritedModeAliases),
|
|
1301
|
+
droppedTokenPaths: new Map(state.droppedTokenPaths)
|
|
919
1302
|
};
|
|
920
1303
|
for (const [scale, vars] of state.contextualVars) next.contextualVars.set(scale, [...vars]);
|
|
921
1304
|
return next;
|
|
922
1305
|
}
|
|
923
1306
|
/**
|
|
1307
|
+
* Exact leaf-path flatten for extend() provenance: like `flattenToDotPaths`
|
|
1308
|
+
* but WITHOUT the `_` identity-key collapse, so a leaf and a branch can
|
|
1309
|
+
* never share a path spelling. A prefix relation between two tracked paths
|
|
1310
|
+
* is then a GENUINE structural divergence (one sibling authored a leaf
|
|
1311
|
+
* value where the other authored a nested branch) — the review-F1 case that
|
|
1312
|
+
* per-leaf value comparison alone cannot see.
|
|
1313
|
+
*/
|
|
1314
|
+
function flattenLeafPathsExact(object, path) {
|
|
1315
|
+
const result = {};
|
|
1316
|
+
for (const key of Object.keys(object)) {
|
|
1317
|
+
const nextKey = path ? `${path}.${key}` : key;
|
|
1318
|
+
const current = object[key];
|
|
1319
|
+
if (isObject(current)) Object.assign(result, flattenLeafPathsExact(current, nextKey));
|
|
1320
|
+
else result[nextKey] = current;
|
|
1321
|
+
}
|
|
1322
|
+
return result;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Deep copy of plain theme data (records, arrays, primitives). `merge`
|
|
1326
|
+
* adopts and MUTATES nested source objects in place, so EVERY builder step
|
|
1327
|
+
* copies before folding: a consumed kit's built theme must never be
|
|
1328
|
+
* corrupted by composition, and prior builder state must never be shared by
|
|
1329
|
+
* reference — a one-level copy lets branching a builder cross-contaminate
|
|
1330
|
+
* both branches and lets build() outputs mutate after the fact.
|
|
1331
|
+
*/
|
|
1332
|
+
function deepCopyPlain(value) {
|
|
1333
|
+
if (Array.isArray(value)) return value.map(deepCopyPlain);
|
|
1334
|
+
if (isObject(value)) {
|
|
1335
|
+
const record = value;
|
|
1336
|
+
const copy = {};
|
|
1337
|
+
for (const key of Object.keys(record)) copy[key] = deepCopyPlain(record[key]);
|
|
1338
|
+
return copy;
|
|
1339
|
+
}
|
|
1340
|
+
return value;
|
|
1341
|
+
}
|
|
1342
|
+
/** Structural equality over plain theme data (records, arrays, primitives). */
|
|
1343
|
+
function plainDataEqual(a, b) {
|
|
1344
|
+
if (Object.is(a, b)) return true;
|
|
1345
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => plainDataEqual(item, b[index]));
|
|
1346
|
+
if (isObject(a) && isObject(b)) {
|
|
1347
|
+
const aRecord = a;
|
|
1348
|
+
const bRecord = b;
|
|
1349
|
+
const aKeys = Object.keys(aRecord);
|
|
1350
|
+
return aKeys.length === Object.keys(bRecord).length && aKeys.every((key) => Object.prototype.hasOwnProperty.call(bRecord, key) && plainDataEqual(aRecord[key], bRecord[key]));
|
|
1351
|
+
}
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Re-seed builder state from a source theme's manifest. This is `from()`'s
|
|
1356
|
+
* manifest read, factored out verbatim so `extend()` inherits the SAME
|
|
1357
|
+
* emitted-scale/contextual-var/manifest-v2 carry semantics: the manifest is
|
|
1358
|
+
* non-enumerable, so the ordinary key-copy loop never sees it (D6/D8).
|
|
1359
|
+
*/
|
|
1360
|
+
function reseedStateFromManifest(state, manifest, mergeExtensionState = false) {
|
|
1361
|
+
if (manifest?.emittedScales) for (const scale of manifest.emittedScales) state.emittedScales.add(scale);
|
|
1362
|
+
else if (manifest?.variableMap) {
|
|
1363
|
+
const emittedPaths = manifest.manifestVersion === 2 && manifest.tokenDefinitions ? Object.keys(manifest.variableMap).filter((tokenPath) => manifest.tokenDefinitions?.[tokenPath] !== void 0) : Object.keys(manifest.variableMap);
|
|
1364
|
+
for (const tokenPath of emittedPaths) {
|
|
1365
|
+
const scale = tokenPath.split(".")[0];
|
|
1366
|
+
state.emittedScales.add(scale === "colors" ? "colors" : scale);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (manifest?.contextualVars) for (const [scale, vars] of Object.entries(manifest.contextualVars)) {
|
|
1370
|
+
const existing = mergeExtensionState ? state.contextualVars.get(scale) : void 0;
|
|
1371
|
+
state.contextualVars.set(scale, existing ? [...new Set([...existing, ...vars])] : [...vars]);
|
|
1372
|
+
}
|
|
1373
|
+
if (manifest) if (manifest.manifestVersion === 2) {
|
|
1374
|
+
state.carriedManifestV2 = {
|
|
1375
|
+
tokenDefinitions: manifest.tokenDefinitions,
|
|
1376
|
+
emittedScales: manifest.emittedScales,
|
|
1377
|
+
modeAliasDefinitions: manifest.modeAliasDefinitions,
|
|
1378
|
+
registrations: manifest.registrations,
|
|
1379
|
+
emitterVersion: manifest.emitterVersion,
|
|
1380
|
+
contractHash: manifest.contractHash,
|
|
1381
|
+
cssFragments: manifest.cssFragments
|
|
1382
|
+
};
|
|
1383
|
+
if (manifest.registrations) for (const [name, registration] of Object.entries(manifest.registrations)) {
|
|
1384
|
+
const existing = state.contextualVarRegistrations.get(name);
|
|
1385
|
+
if (mergeExtensionState && existing && (existing.syntax !== registration.syntax || existing.inherits !== registration.inherits || existing.initialValue !== registration.initialValue)) throw new Error(`extend: contextual variable '${name}' has divergent @property registrations across extended themes`);
|
|
1386
|
+
state.contextualVarRegistrations.set(name, registration);
|
|
1387
|
+
}
|
|
1388
|
+
} else state.hasLegacyManifestSource = true;
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
924
1391
|
* ThemeScales — the final phase. Has addScale, extendScale, declareContextualVars, build.
|
|
925
1392
|
* Also allows addColors and addColorModes for augmentation.
|
|
926
1393
|
*/
|
|
@@ -931,39 +1398,94 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
931
1398
|
}
|
|
932
1399
|
addBreakpoints(breakpoints) {
|
|
933
1400
|
for (const [key, value] of Object.entries(breakpoints)) if (typeof value !== "number" || value < 0) throw new Error(`addBreakpoints: breakpoint '${key}' must be a non-negative number, got ${JSON.stringify(value)}`);
|
|
934
|
-
const nextTheme = merge(
|
|
1401
|
+
const nextTheme = merge(deepCopyPlain(this._state.theme), { breakpoints });
|
|
935
1402
|
return new ThemeBuilder(copyState(this._state, nextTheme));
|
|
936
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* FROZEN for the deprecation window (D1/G6): source-WINS precedence over
|
|
1406
|
+
* prior builder state, callable at ANY stage — the phantom Stage passes
|
|
1407
|
+
* through unchanged. Do not add a stage gate; do not flip the merge
|
|
1408
|
+
* direction. Those semantics ship under the new verb only.
|
|
1409
|
+
*
|
|
1410
|
+
* @deprecated Use `extend(source)` — the single extension verb on both
|
|
1411
|
+
* builders: the extended source seeds the base and later local calls win.
|
|
1412
|
+
* `from()` keeps these frozen source-wins semantics for at least one minor
|
|
1413
|
+
* release after `extend()` ships.
|
|
1414
|
+
*/
|
|
937
1415
|
from(builtTheme) {
|
|
1416
|
+
const source = isLibraryBundle(builtTheme) ? builtTheme.theme ?? builtTheme.tokens ?? {} : builtTheme;
|
|
938
1417
|
const raw = {};
|
|
939
|
-
for (const key of Object.keys(
|
|
940
|
-
const val =
|
|
1418
|
+
for (const key of Object.keys(source)) {
|
|
1419
|
+
const val = source[key];
|
|
941
1420
|
if (typeof val !== "function") raw[key] = val;
|
|
942
1421
|
}
|
|
943
|
-
const nextTheme = merge(
|
|
1422
|
+
const nextTheme = merge(deepCopyPlain(this._state.theme), deepCopyPlain(raw));
|
|
944
1423
|
const next = new ThemeBuilder(copyState(this._state, nextTheme));
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1424
|
+
reseedStateFromManifest(next._state, source.manifest);
|
|
1425
|
+
return next;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Extend this theme from a consumed library (D1/D2): the source's complete
|
|
1429
|
+
* configuration — tokens, modes, preferences, registrations, emitted-scale
|
|
1430
|
+
* set — seeds the builder as the BASE, and local calls made after
|
|
1431
|
+
* `extend()` win on conflict (the mirror of `from()`'s source-wins).
|
|
1432
|
+
* Chainable and repeatable, but only before augmentation methods ("inherit
|
|
1433
|
+
* first, then extend" — enforced by the phantom builder stage). Accepts a
|
|
1434
|
+
* built theme or a library bundle, consuming the bundle's theme half
|
|
1435
|
+
* (`theme ?? tokens`, D9) and ignoring the rest.
|
|
1436
|
+
*
|
|
1437
|
+
* Sibling conflicts fail loud (D3/G4): a leaf path defined divergently by
|
|
1438
|
+
* two extended sources throws naming the path and both sources
|
|
1439
|
+
* positionally; equal values coalesce silently, and the consumer's own
|
|
1440
|
+
* post-extend `add*` calls override silently (NS-4).
|
|
1441
|
+
*/
|
|
1442
|
+
extend(source) {
|
|
1443
|
+
const themeHalf = isLibraryBundle(source) ? source.theme ?? source.tokens ?? {} : source;
|
|
1444
|
+
const raw = {};
|
|
1445
|
+
for (const key of Object.keys(themeHalf)) {
|
|
1446
|
+
const val = themeHalf[key];
|
|
1447
|
+
if (typeof val !== "function") raw[key] = val;
|
|
949
1448
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1449
|
+
const sourceIndex = this._state.extendCount + 1;
|
|
1450
|
+
const provenance = new Map(this._state.extendProvenance);
|
|
1451
|
+
const existingLeaves = flattenLeafPathsExact(this._state.theme);
|
|
1452
|
+
const incomingLeaves = flattenLeafPathsExact(raw);
|
|
1453
|
+
const trackedPrefixes = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const [trackedPath, index] of provenance) for (let dot = trackedPath.lastIndexOf("."); dot !== -1; dot = trackedPath.lastIndexOf(".", dot - 1)) {
|
|
1455
|
+
const prefix = trackedPath.slice(0, dot);
|
|
1456
|
+
if (!trackedPrefixes.has(prefix)) trackedPrefixes.set(prefix, index);
|
|
1457
|
+
}
|
|
1458
|
+
for (const [path, value] of Object.entries(incomingLeaves)) {
|
|
1459
|
+
const priorIndex = provenance.get(path);
|
|
1460
|
+
if (priorIndex !== void 0) {
|
|
1461
|
+
const existing = existingLeaves[path];
|
|
1462
|
+
if (!plainDataEqual(existing, value)) throw new Error(`extend: path '${path}' is defined divergently by extended theme #${priorIndex} (${JSON.stringify(existing)}) and extended theme #${sourceIndex} (${JSON.stringify(value)}). Sibling themes must agree — override intentionally with an add* call after extend().`);
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
const branchIndex = trackedPrefixes.get(path);
|
|
1466
|
+
if (branchIndex !== void 0) throw new Error(`extend: path '${path}' is defined divergently by extended theme #${branchIndex} (a nested branch) and extended theme #${sourceIndex} (a leaf value). Sibling themes must agree — override intentionally with an add* call after extend().`);
|
|
1467
|
+
for (let dot = path.lastIndexOf("."); dot !== -1; dot = path.lastIndexOf(".", dot - 1)) {
|
|
1468
|
+
const ancestor = path.slice(0, dot);
|
|
1469
|
+
const ancestorIndex = provenance.get(ancestor);
|
|
1470
|
+
if (ancestorIndex !== void 0 && ancestorIndex !== sourceIndex) throw new Error(`extend: path '${ancestor}' is defined divergently by extended theme #${ancestorIndex} (a leaf value) and extended theme #${sourceIndex} (a nested branch). Sibling themes must agree — override intentionally with an add* call after extend().`);
|
|
1471
|
+
}
|
|
1472
|
+
provenance.set(path, sourceIndex);
|
|
1473
|
+
}
|
|
1474
|
+
const nextTheme = merge(deepCopyPlain(raw), deepCopyPlain(this._state.theme));
|
|
1475
|
+
const next = new ThemeBuilder(copyState(this._state, nextTheme));
|
|
1476
|
+
next._state.extendProvenance = provenance;
|
|
1477
|
+
next._state.extendCount = sourceIndex;
|
|
1478
|
+
if (isObject(raw.modes)) for (const [modeName, modeAliases] of Object.entries(raw.modes)) {
|
|
1479
|
+
next._state.inheritedModes.add(modeName);
|
|
1480
|
+
if (!isObject(modeAliases)) continue;
|
|
1481
|
+
for (const aliasPath of Object.keys(flattenToDotPaths(modeAliases))) next._state.inheritedModeAliases.add(aliasPath);
|
|
1482
|
+
}
|
|
1483
|
+
reseedStateFromManifest(next._state, themeHalf.manifest, true);
|
|
962
1484
|
return next;
|
|
963
1485
|
}
|
|
964
1486
|
addColors(colors) {
|
|
965
1487
|
validateColors(colors);
|
|
966
|
-
const nextTheme = merge(
|
|
1488
|
+
const nextTheme = merge(deepCopyPlain(this._state.theme), { colors });
|
|
967
1489
|
const next = new ThemeBuilder(copyState(this._state, nextTheme));
|
|
968
1490
|
next._state.emittedScales.add("colors");
|
|
969
1491
|
return next;
|
|
@@ -982,20 +1504,32 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
982
1504
|
const systemPreference = mergeOptionObject(this._state.theme.systemPreference, options?.systemPreference);
|
|
983
1505
|
const browserColorScheme = mergeOptionObject(this._state.theme.browserColorScheme, options?.browserColorScheme);
|
|
984
1506
|
resolveColorModeOptions(modeNames, systemPreference, browserColorScheme);
|
|
985
|
-
const
|
|
1507
|
+
const modeBases = mergeOptionObject(this._state.theme.modeBases, options?.basedOn);
|
|
1508
|
+
validateModeBases(modeNames, modeBases);
|
|
1509
|
+
const nextTheme = merge(deepCopyPlain(this._state.theme), {
|
|
986
1510
|
modes: modeConfig,
|
|
987
1511
|
mode: initialMode,
|
|
988
1512
|
...systemPreference ? { systemPreference } : {},
|
|
989
|
-
...browserColorScheme ? { browserColorScheme } : {}
|
|
1513
|
+
...browserColorScheme ? { browserColorScheme } : {},
|
|
1514
|
+
...modeBases ? { modeBases } : {}
|
|
990
1515
|
});
|
|
991
1516
|
return new ThemeBuilder(copyState(this._state, nextTheme));
|
|
992
1517
|
}
|
|
993
1518
|
addScale(config) {
|
|
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
|
|
996
|
-
const
|
|
1519
|
+
const { name, values, emit, replace } = config;
|
|
1520
|
+
if (RESERVED_THEME_KEYS.has(name)) throw new Error(`addScale: '${name}' is a reserved theme key owned by the builder or built-theme boundary, so a scale by this name cannot survive build(). Choose another scale name.`);
|
|
1521
|
+
const prior = this._state.theme[name];
|
|
1522
|
+
let nextTheme;
|
|
1523
|
+
if (replace) {
|
|
1524
|
+
nextTheme = deepCopyPlain(this._state.theme);
|
|
1525
|
+
nextTheme[name] = values;
|
|
1526
|
+
} else nextTheme = merge(deepCopyPlain(this._state.theme), { [name]: values });
|
|
997
1527
|
const next = new ThemeBuilder(copyState(this._state, nextTheme));
|
|
998
1528
|
if (emit) next._state.emittedScales.add(name);
|
|
1529
|
+
if (replace && isObject(prior)) {
|
|
1530
|
+
const kept = new Set(Object.keys(flattenToDotPaths(values)));
|
|
1531
|
+
for (const dotKey of Object.keys(flattenToDotPaths(prior))) if (!kept.has(dotKey)) next._state.droppedTokenPaths.set(`${name}.${dotKey}`, name);
|
|
1532
|
+
}
|
|
999
1533
|
return next;
|
|
1000
1534
|
}
|
|
1001
1535
|
declareContextualVars(vars, registrations) {
|
|
@@ -1011,7 +1545,7 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1011
1545
|
return next;
|
|
1012
1546
|
}
|
|
1013
1547
|
extendScale(key, updateFn) {
|
|
1014
|
-
const nextTheme = merge(
|
|
1548
|
+
const nextTheme = merge(deepCopyPlain(this._state.theme), { [key]: updateFn(this._state.theme[key]) });
|
|
1015
1549
|
return new ThemeBuilder(copyState(this._state, nextTheme));
|
|
1016
1550
|
}
|
|
1017
1551
|
/**
|
|
@@ -1019,24 +1553,44 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1019
1553
|
* Flattens nested data at the boundary — produces manifest and serialize().
|
|
1020
1554
|
*/
|
|
1021
1555
|
build() {
|
|
1022
|
-
const theme =
|
|
1556
|
+
const theme = deepCopyPlain(this._state.theme);
|
|
1023
1557
|
const emittedScales = this._state.emittedScales;
|
|
1024
1558
|
const contextualVars = this._state.contextualVars;
|
|
1025
1559
|
const systemPreference = isObject(theme.systemPreference) ? theme.systemPreference : void 0;
|
|
1026
1560
|
const mergedModeNames = isObject(theme.modes) ? Object.keys(theme.modes) : [];
|
|
1027
1561
|
validateReservedModeNames(mergedModeNames);
|
|
1028
1562
|
const browserColorScheme = resolveColorModeOptions(mergedModeNames, systemPreference, isObject(theme.browserColorScheme) ? theme.browserColorScheme : void 0);
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1563
|
+
const modeBases = isObject(theme.modeBases) ? theme.modeBases : void 0;
|
|
1564
|
+
validateModeBases(mergedModeNames, modeBases);
|
|
1565
|
+
if (isObject(theme.modes) && isObject(theme.colors)) {
|
|
1566
|
+
const nestedColors = theme.colors;
|
|
1567
|
+
const flatColorKeys = Object.keys(flattenToDotPaths(nestedColors));
|
|
1568
|
+
for (const [modeName, modeAliases] of Object.entries(theme.modes)) {
|
|
1569
|
+
if (!isObject(modeAliases)) continue;
|
|
1570
|
+
validateModeAliases(modeName, modeAliases, nestedColors, flatColorKeys, "");
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const modeAliasDefinitions = collectAuthoredModeAliases(theme);
|
|
1574
|
+
const { effectiveModes, coverageFills } = resolveModeCoverage(modeAliasDefinitions, modeBases, this._state.inheritedModes, this._state.inheritedModeAliases);
|
|
1575
|
+
for (const fill of coverageFills) console.info(`[animus] Mode '${fill.mode}': ${fill.count} alias(es) inherit from '${fill.base}'`);
|
|
1576
|
+
const { tokenMap: flatTokenMap, variableMap, variables: flatVariables, tokenDefinitions } = flattenTheme(theme, emittedScales, effectiveModes);
|
|
1577
|
+
assertNoDroppedReferences(tokenDefinitions, flatTokenMap, this._state.droppedTokenPaths, this._state.extendProvenance);
|
|
1578
|
+
const { tokenMap, variables } = resolveReferences(flatTokenMap, variableMap, flatVariables);
|
|
1579
|
+
const { modeVariables, modeTokens } = resolveModeValueMaps(effectiveModes, variableMap, variables, tokenMap);
|
|
1031
1580
|
const bpVariables = {};
|
|
1032
|
-
if (theme.breakpoints && isObject(theme.breakpoints))
|
|
1581
|
+
if (theme.breakpoints && isObject(theme.breakpoints)) {
|
|
1582
|
+
const breakpointEntries = Object.entries(theme.breakpoints).map(([key, value]) => [`--breakpoint-${key}`, `${value}px`]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1583
|
+
for (const [varName, value] of breakpointEntries) bpVariables[varName] = value;
|
|
1584
|
+
}
|
|
1033
1585
|
let contextualVarsSerialized;
|
|
1034
1586
|
if (contextualVars.size > 0) {
|
|
1035
1587
|
contextualVarsSerialized = {};
|
|
1036
1588
|
for (const [scale, vars] of contextualVars) contextualVarsSerialized[scale] = vars;
|
|
1037
1589
|
}
|
|
1038
1590
|
const propertyCss = buildPropertyRegistrationCss(contextualVars, this._state.contextualVarRegistrations);
|
|
1039
|
-
const
|
|
1591
|
+
const { emittableVariables, emittableModeVariables, omitted } = omitUnresolvedDeclarations(variables, modeVariables);
|
|
1592
|
+
if (omitted.length > 0) console.warn(`[animus] Omitted ${omitted.length} CSS declaration(s) whose token references never resolved: ${omitted.join(", ")}`);
|
|
1593
|
+
const baseVariableCss = buildVariableCss(emittableVariables, bpVariables, emittableModeVariables, {
|
|
1040
1594
|
initialMode: typeof theme.mode === "string" ? theme.mode : void 0,
|
|
1041
1595
|
systemPreference,
|
|
1042
1596
|
browserColorScheme
|
|
@@ -1059,26 +1613,30 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1059
1613
|
manifestV2Fields = {
|
|
1060
1614
|
manifestVersion: 2,
|
|
1061
1615
|
tokenDefinitions,
|
|
1616
|
+
emittedScales: [...emittedScales].sort(),
|
|
1062
1617
|
modeAliasDefinitions,
|
|
1063
1618
|
registrations,
|
|
1064
1619
|
emitterVersion: EMITTER_VERSION,
|
|
1065
1620
|
contractHash: computeContractHash({
|
|
1066
1621
|
tokenDefinitions,
|
|
1622
|
+
emittedScales: [...emittedScales].sort(),
|
|
1067
1623
|
modeAliasDefinitions,
|
|
1068
1624
|
initialMode: typeof theme.mode === "string" ? theme.mode : void 0,
|
|
1069
1625
|
registrations,
|
|
1070
1626
|
systemPreference,
|
|
1071
|
-
browserColorScheme
|
|
1627
|
+
browserColorScheme,
|
|
1628
|
+
modeBases
|
|
1072
1629
|
}),
|
|
1073
1630
|
cssFragments
|
|
1074
1631
|
};
|
|
1075
1632
|
}
|
|
1633
|
+
const sortedVariableMap = sortRecordByKey(variableMap);
|
|
1076
1634
|
const manifest = {
|
|
1077
1635
|
tokenMap: {
|
|
1078
1636
|
...tokenMap,
|
|
1079
|
-
...Object.fromEntries(Object.entries(theme.breakpoints || {}).map(([k, v]) => [`breakpoints.${k}`, String(v)]))
|
|
1637
|
+
...sortRecordByKey(Object.fromEntries(Object.entries(theme.breakpoints || {}).map(([k, v]) => [`breakpoints.${k}`, String(v)])))
|
|
1080
1638
|
},
|
|
1081
|
-
variableMap,
|
|
1639
|
+
variableMap: sortedVariableMap,
|
|
1082
1640
|
modes: modeTokens,
|
|
1083
1641
|
variableCss,
|
|
1084
1642
|
...contextualVarsSerialized ? { contextualVars: contextualVarsSerialized } : {},
|
|
@@ -1130,19 +1688,19 @@ function createTheme() {
|
|
|
1130
1688
|
const TOKEN_REF_RE = /\{([^}]+)\}/g;
|
|
1131
1689
|
/**
|
|
1132
1690
|
* Flatten the nested theme into dot-path keyed token map and CSS variable declarations.
|
|
1133
|
-
* This is the ONLY place where flattening happens.
|
|
1691
|
+
* This is the ONLY place where flattening happens. Mode VALUE maps are no
|
|
1692
|
+
* longer computed here — they resolve AFTER `resolveReferences` (see
|
|
1693
|
+
* {@link resolveModeValueMaps}), closing the G2 gap where mode-override
|
|
1694
|
+
* declarations bypassed the resolver.
|
|
1134
1695
|
*/
|
|
1135
|
-
function flattenTheme(theme, emittedScales) {
|
|
1696
|
+
function flattenTheme(theme, emittedScales, effectiveModes) {
|
|
1136
1697
|
const tokenMap = {};
|
|
1137
1698
|
const variableMap = {};
|
|
1138
1699
|
const variables = {};
|
|
1139
|
-
const modeVariables = {};
|
|
1140
|
-
const modeTokens = {};
|
|
1141
1700
|
const tokenDefinitions = {};
|
|
1142
|
-
const modeAliasDefinitions = {};
|
|
1143
1701
|
for (const [scaleName, scaleValue] of Object.entries(theme)) {
|
|
1144
1702
|
if (scaleName.startsWith("_")) continue;
|
|
1145
|
-
if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes" || scaleName === "systemPreference" || scaleName === "browserColorScheme") continue;
|
|
1703
|
+
if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes" || scaleName === "systemPreference" || scaleName === "browserColorScheme" || scaleName === "modeBases") continue;
|
|
1146
1704
|
if (typeof scaleValue === "function") continue;
|
|
1147
1705
|
if (!isObject(scaleValue)) continue;
|
|
1148
1706
|
const flat = flattenToDotPaths(scaleValue);
|
|
@@ -1159,54 +1717,242 @@ function flattenTheme(theme, emittedScales) {
|
|
|
1159
1717
|
} else tokenMap[tokenPath] = String(rawValue);
|
|
1160
1718
|
}
|
|
1161
1719
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const varName = `--color-${dotToDash(aliasDotKey)}`;
|
|
1173
|
-
modeAliasDefs[aliasDotKey] = colorRef;
|
|
1174
|
-
const rawValue = flatColors[colorRef];
|
|
1175
|
-
modeVals[`colors.${aliasDotKey}`] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
|
|
1176
|
-
modeVars[varName] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
|
|
1177
|
-
}
|
|
1178
|
-
modeVariables[modeName] = modeVars;
|
|
1179
|
-
modeTokens[modeName] = modeVals;
|
|
1180
|
-
modeAliasDefinitions[modeName] = modeAliasDefs;
|
|
1181
|
-
}
|
|
1182
|
-
const initialMode = theme.mode;
|
|
1183
|
-
if (initialMode && modeVariables[initialMode]) {
|
|
1184
|
-
const initialModeVars = {};
|
|
1185
|
-
const flatInitialAliases = flattenToDotPaths(theme.modes[initialMode]);
|
|
1186
|
-
for (const [aliasDotKey, colorRef] of Object.entries(flatInitialAliases)) {
|
|
1187
|
-
if (typeof colorRef !== "string") continue;
|
|
1188
|
-
const varName = `--color-${dotToDash(aliasDotKey)}`;
|
|
1189
|
-
const paletteVarName = variableMap[`colors.${colorRef}`];
|
|
1190
|
-
if (paletteVarName) initialModeVars[varName] = `var(${paletteVarName})`;
|
|
1191
|
-
tokenMap[`colors.${aliasDotKey}`] = `var(${varName})`;
|
|
1192
|
-
variableMap[`colors.${aliasDotKey}`] = varName;
|
|
1193
|
-
}
|
|
1194
|
-
Object.assign(variables, initialModeVars);
|
|
1720
|
+
const initialMode = theme.mode;
|
|
1721
|
+
const initialAliases = typeof initialMode === "string" ? effectiveModes[initialMode] : void 0;
|
|
1722
|
+
if (initialAliases) for (const [aliasDotKey, colorRef] of Object.entries(initialAliases)) {
|
|
1723
|
+
const varName = `--color-${dotToDash(aliasDotKey)}`;
|
|
1724
|
+
const paletteVarName = variableMap[`colors.${colorRef}`];
|
|
1725
|
+
if (paletteVarName) {
|
|
1726
|
+
if (paletteVarName !== varName) variables[varName] = `var(${paletteVarName})`;
|
|
1727
|
+
} else {
|
|
1728
|
+
const literal = tokenMap[`colors.${colorRef}`];
|
|
1729
|
+
if (literal !== void 0) variables[varName] = literal;
|
|
1195
1730
|
}
|
|
1731
|
+
tokenMap[`colors.${aliasDotKey}`] = `var(${varName})`;
|
|
1732
|
+
variableMap[`colors.${aliasDotKey}`] = varName;
|
|
1196
1733
|
}
|
|
1197
1734
|
return {
|
|
1198
1735
|
tokenMap,
|
|
1199
1736
|
variableMap,
|
|
1200
1737
|
variables,
|
|
1738
|
+
tokenDefinitions
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
/** Rebuild a record with lexicographically sorted keys (wire determinism, G3). */
|
|
1742
|
+
function sortRecordByKey(record) {
|
|
1743
|
+
const sorted = {};
|
|
1744
|
+
for (const key of Object.keys(record).sort()) sorted[key] = record[key];
|
|
1745
|
+
return sorted;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Collect the AUTHORED mode alias graph: mode name → alias dot-path → the
|
|
1749
|
+
* authored color dot-path (manifest v2, D6 — never a resolved value).
|
|
1750
|
+
* Empty when the theme has no modes or no colors, mirroring the original
|
|
1751
|
+
* flatten-pass guard.
|
|
1752
|
+
*/
|
|
1753
|
+
function collectAuthoredModeAliases(theme) {
|
|
1754
|
+
const authored = {};
|
|
1755
|
+
if (!isObject(theme.modes) || !isObject(theme.colors)) return authored;
|
|
1756
|
+
for (const [modeName, modeAliases] of Object.entries(theme.modes)) {
|
|
1757
|
+
if (!isObject(modeAliases)) continue;
|
|
1758
|
+
const defs = {};
|
|
1759
|
+
for (const [aliasDotKey, colorRef] of Object.entries(flattenToDotPaths(modeAliases))) {
|
|
1760
|
+
if (typeof colorRef !== "string") continue;
|
|
1761
|
+
defs[aliasDotKey] = colorRef;
|
|
1762
|
+
}
|
|
1763
|
+
authored[modeName] = defs;
|
|
1764
|
+
}
|
|
1765
|
+
return authored;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* D6 coverage over the merged mode set: a CONSUMER-declared mode leaving
|
|
1769
|
+
* inherited aliases uncovered must name a base (`basedOn`) whose chain
|
|
1770
|
+
* covers them — otherwise the build fails listing the uncovered set.
|
|
1771
|
+
* Inherited modes are exempt (a kit's own asymmetry is pre-existing
|
|
1772
|
+
* behavior and must round-trip byte-identically). Returns the effective
|
|
1773
|
+
* alias map per mode (base-chain fills + authored, authored winning) and
|
|
1774
|
+
* one aggregated fill report per mode for the build diagnostic.
|
|
1775
|
+
*/
|
|
1776
|
+
function resolveModeCoverage(authoredModeAliases, modeBases, inheritedModes, inheritedModeAliases) {
|
|
1777
|
+
const effectiveModes = {};
|
|
1778
|
+
const coverageFills = [];
|
|
1779
|
+
for (const modeName of Object.keys(authoredModeAliases)) {
|
|
1780
|
+
const authored = authoredModeAliases[modeName];
|
|
1781
|
+
const fills = {};
|
|
1782
|
+
if (!inheritedModes.has(modeName)) {
|
|
1783
|
+
const uncovered = [...inheritedModeAliases].filter((alias) => !(alias in authored)).sort();
|
|
1784
|
+
if (uncovered.length > 0) {
|
|
1785
|
+
const base = modeBases?.[modeName];
|
|
1786
|
+
if (base === void 0) throw new Error(`build: mode '${modeName}' leaves ${uncovered.length} inherited alias(es) uncovered and declares no base — uncovered: ${uncovered.join(", ")}. Add basedOn: { '${modeName}': '<mode>' } to addColorModes options or override every inherited alias.`);
|
|
1787
|
+
const stillUncovered = [];
|
|
1788
|
+
for (const alias of uncovered) {
|
|
1789
|
+
let cursor = base;
|
|
1790
|
+
const seen = new Set([modeName]);
|
|
1791
|
+
let resolved;
|
|
1792
|
+
while (cursor !== void 0 && !seen.has(cursor)) {
|
|
1793
|
+
seen.add(cursor);
|
|
1794
|
+
resolved = authoredModeAliases[cursor]?.[alias];
|
|
1795
|
+
if (resolved !== void 0) break;
|
|
1796
|
+
cursor = modeBases?.[cursor];
|
|
1797
|
+
}
|
|
1798
|
+
if (resolved === void 0) stillUncovered.push(alias);
|
|
1799
|
+
else fills[alias] = resolved;
|
|
1800
|
+
}
|
|
1801
|
+
if (stillUncovered.length > 0) throw new Error(`build: mode '${modeName}' resolves through base '${base}' but the chain never covers: ${stillUncovered.join(", ")}. Cover them in a chained mode or override them directly.`);
|
|
1802
|
+
coverageFills.push({
|
|
1803
|
+
mode: modeName,
|
|
1804
|
+
base,
|
|
1805
|
+
count: Object.keys(fills).length
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
effectiveModes[modeName] = {
|
|
1810
|
+
...fills,
|
|
1811
|
+
...authored
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
return {
|
|
1815
|
+
effectiveModes,
|
|
1816
|
+
coverageFills
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Resolve the per-mode value maps AFTER reference resolution: every mode
|
|
1821
|
+
* declaration carries the RESOLVED value of its target color (emitted →
|
|
1822
|
+
* the resolved declaration value; inlined → the resolved literal), never
|
|
1823
|
+
* the raw flattened string — the G2 closure for `[data-color-mode]` blocks.
|
|
1824
|
+
* Modes iterate in sorted name order and lines in sorted property-name
|
|
1825
|
+
* order, so mode declaration/insertion order is never observable (G3).
|
|
1826
|
+
*/
|
|
1827
|
+
function resolveModeValueMaps(effectiveModes, variableMap, variables, tokenMap) {
|
|
1828
|
+
const modeVariables = {};
|
|
1829
|
+
const modeTokens = {};
|
|
1830
|
+
const resolvedColorValue = (colorRef) => {
|
|
1831
|
+
const path = `colors.${colorRef}`;
|
|
1832
|
+
const varName = variableMap[path];
|
|
1833
|
+
if (varName !== void 0) {
|
|
1834
|
+
const declared = variables[varName];
|
|
1835
|
+
if (declared !== void 0) return declared;
|
|
1836
|
+
} else if (tokenMap[path] !== void 0) return tokenMap[path];
|
|
1837
|
+
return String(colorRef);
|
|
1838
|
+
};
|
|
1839
|
+
for (const modeName of Object.keys(effectiveModes).sort()) {
|
|
1840
|
+
const aliases = effectiveModes[modeName];
|
|
1841
|
+
const entries = Object.keys(aliases).map((aliasDotKey) => [`--color-${dotToDash(aliasDotKey)}`, aliasDotKey]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1842
|
+
const modeVars = {};
|
|
1843
|
+
const modeVals = {};
|
|
1844
|
+
for (const [varName, aliasDotKey] of entries) {
|
|
1845
|
+
const value = resolvedColorValue(aliases[aliasDotKey]);
|
|
1846
|
+
modeVars[varName] = value;
|
|
1847
|
+
modeVals[`colors.${aliasDotKey}`] = value;
|
|
1848
|
+
}
|
|
1849
|
+
modeVariables[modeName] = modeVars;
|
|
1850
|
+
modeTokens[modeName] = modeVals;
|
|
1851
|
+
}
|
|
1852
|
+
return {
|
|
1201
1853
|
modeVariables,
|
|
1202
|
-
modeTokens
|
|
1203
|
-
|
|
1204
|
-
|
|
1854
|
+
modeTokens
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* D5 enforcement: a reference whose target is absent from the merged map
|
|
1859
|
+
* AND was dropped by an explicit `addScale({ replace: true })` fails the
|
|
1860
|
+
* build — unconditional on usage — naming the referencing token (with its
|
|
1861
|
+
* positional origin), the replacement call's scale, and the dropped keys.
|
|
1862
|
+
* Targets never defined ANYWHERE stay warn-and-literal (supported kit
|
|
1863
|
+
* pattern); a re-added key is simply known again and passes.
|
|
1864
|
+
*/
|
|
1865
|
+
function assertNoDroppedReferences(tokenDefinitions, flatTokenMap, droppedTokenPaths, extendProvenance) {
|
|
1866
|
+
if (droppedTokenPaths.size === 0) return;
|
|
1867
|
+
const known = new Set(Object.keys(flatTokenMap));
|
|
1868
|
+
const violations = [];
|
|
1869
|
+
const droppedNamed = /* @__PURE__ */ new Set();
|
|
1870
|
+
for (const [tokenPath, definition] of Object.entries(tokenDefinitions)) {
|
|
1871
|
+
if (definition.kind !== "reference") continue;
|
|
1872
|
+
for (const reference of definition.references) {
|
|
1873
|
+
if (known.has(reference.path)) continue;
|
|
1874
|
+
const scale = droppedTokenPaths.get(reference.path);
|
|
1875
|
+
if (scale === void 0) continue;
|
|
1876
|
+
const provenanceIndex = extendProvenance.get(tokenPath);
|
|
1877
|
+
const origin = provenanceIndex === void 0 ? "builder state" : `extended theme #${provenanceIndex}`;
|
|
1878
|
+
violations.push(`'${tokenPath}' (${origin}) references '{${reference.path}}', dropped by addScale({ name: '${scale}', replace: true })`);
|
|
1879
|
+
droppedNamed.add(reference.path);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (violations.length > 0) throw new Error(`build: dangling token reference(s) after explicit scale replacement — ${violations.join("; ")}. Dropped keys: ${[...droppedNamed].sort().join(", ")}.`);
|
|
1883
|
+
}
|
|
1884
|
+
/** A resolved value still carrying a `{…}` reference — never shippable (G2). */
|
|
1885
|
+
const UNRESOLVED_REF_RE = /\{[^}]+\}/;
|
|
1886
|
+
/** Custom-property names referenced through `var(--name)` in a value. */
|
|
1887
|
+
const VAR_REF_NAME_RE = /var\(\s*(--[\w-]+)/g;
|
|
1888
|
+
function varRefNames(value) {
|
|
1889
|
+
const names = [];
|
|
1890
|
+
for (const match of value.matchAll(VAR_REF_NAME_RE)) names.push(match[1]);
|
|
1891
|
+
return names;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Split root and mode variable maps into shippable declarations and omitted
|
|
1895
|
+
* var names (G2): any value still containing `{…}` after resolution — a
|
|
1896
|
+
* direct or TRANSITIVE never-defined target — is withheld from emitted CSS.
|
|
1897
|
+
*
|
|
1898
|
+
* Withholding must follow `var()` indirection too: flattenTheme synthesizes
|
|
1899
|
+
* the initial mode's aliases as `var(--target)` BEFORE resolution, so a
|
|
1900
|
+
* withheld target would otherwise leave an emitted alias pointing at a
|
|
1901
|
+
* declaration that exists nowhere. A declaration is dangling when a var()
|
|
1902
|
+
* target of its value was DECLARED in this build but withheld everywhere
|
|
1903
|
+
* the declaration can see (:root for root declarations; the same mode
|
|
1904
|
+
* block or :root for mode declarations); chains drop to a fixpoint. Var
|
|
1905
|
+
* names never declared here (breakpoints, contextual vars) are exempt.
|
|
1906
|
+
*/
|
|
1907
|
+
function omitUnresolvedDeclarations(variables, modeVariables) {
|
|
1908
|
+
const omitted = [];
|
|
1909
|
+
const emittableVariables = {};
|
|
1910
|
+
const droppedRoot = /* @__PURE__ */ new Set();
|
|
1911
|
+
for (const [varName, value] of Object.entries(variables)) if (UNRESOLVED_REF_RE.test(value)) {
|
|
1912
|
+
omitted.push(varName);
|
|
1913
|
+
droppedRoot.add(varName);
|
|
1914
|
+
} else emittableVariables[varName] = value;
|
|
1915
|
+
let rootChanged = true;
|
|
1916
|
+
while (rootChanged) {
|
|
1917
|
+
rootChanged = false;
|
|
1918
|
+
for (const [varName, value] of Object.entries(emittableVariables)) if (varRefNames(value).some((name) => droppedRoot.has(name))) {
|
|
1919
|
+
delete emittableVariables[varName];
|
|
1920
|
+
droppedRoot.add(varName);
|
|
1921
|
+
omitted.push(varName);
|
|
1922
|
+
rootChanged = true;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const emittableModeVariables = {};
|
|
1926
|
+
for (const [modeName, modeVars] of Object.entries(modeVariables)) {
|
|
1927
|
+
const kept = {};
|
|
1928
|
+
const droppedInMode = /* @__PURE__ */ new Set();
|
|
1929
|
+
const dropFromMode = (varName) => {
|
|
1930
|
+
omitted.push(`${varName} ([data-color-mode="${modeName}"])`);
|
|
1931
|
+
droppedInMode.add(varName);
|
|
1932
|
+
};
|
|
1933
|
+
for (const [varName, value] of Object.entries(modeVars)) if (UNRESOLVED_REF_RE.test(value)) dropFromMode(varName);
|
|
1934
|
+
else kept[varName] = value;
|
|
1935
|
+
const dangling = (name) => (droppedInMode.has(name) || droppedRoot.has(name)) && !(name in kept) && !(name in emittableVariables);
|
|
1936
|
+
let modeChanged = true;
|
|
1937
|
+
while (modeChanged) {
|
|
1938
|
+
modeChanged = false;
|
|
1939
|
+
for (const [varName, value] of Object.entries(kept)) if (varRefNames(value).some(dangling)) {
|
|
1940
|
+
delete kept[varName];
|
|
1941
|
+
dropFromMode(varName);
|
|
1942
|
+
modeChanged = true;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
emittableModeVariables[modeName] = kept;
|
|
1946
|
+
}
|
|
1947
|
+
return {
|
|
1948
|
+
emittableVariables,
|
|
1949
|
+
emittableModeVariables,
|
|
1950
|
+
omitted
|
|
1205
1951
|
};
|
|
1206
1952
|
}
|
|
1207
1953
|
/**
|
|
1208
1954
|
* Classify a RAW token value into its authored form (manifest v2, D6). MUST
|
|
1209
|
-
* run before `
|
|
1955
|
+
* run before `resolveReferences` — resolution rewrites the string, and the
|
|
1210
1956
|
* authored graph cannot be reconstructed from resolved CSS (D8). Uses
|
|
1211
1957
|
* `matchAll` so the shared global {@link TOKEN_REF_RE} never carries a stale
|
|
1212
1958
|
* `lastIndex` between callers.
|
|
@@ -1432,43 +2178,6 @@ function computeContractHash(input) {
|
|
|
1432
2178
|
return sha256Hex(JSON.stringify(canonicalize(input)));
|
|
1433
2179
|
}
|
|
1434
2180
|
/**
|
|
1435
|
-
* Resolve token refs ({scale.key}) in all flattened token values.
|
|
1436
|
-
* Operates on the flattened tokenMap — does NOT mutate the nested theme.
|
|
1437
|
-
*/
|
|
1438
|
-
function resolveTokenRefs(tokenMap, _variableMap, _emittedScales) {
|
|
1439
|
-
for (const [tokenPath, value] of Object.entries(tokenMap)) {
|
|
1440
|
-
if (typeof value !== "string") continue;
|
|
1441
|
-
if (!value.includes("{")) continue;
|
|
1442
|
-
if (value.startsWith("var(")) continue;
|
|
1443
|
-
const scaleName = tokenPath.split(".")[0];
|
|
1444
|
-
const resolved = value.replace(TOKEN_REF_RE, (match, ref) => {
|
|
1445
|
-
if (ref.split(".")[0] === scaleName) {
|
|
1446
|
-
console.warn(`[animus] Self-referential token ref {${ref}} in scale '${scaleName}' — skipped`);
|
|
1447
|
-
return match;
|
|
1448
|
-
}
|
|
1449
|
-
let lookupPath = ref;
|
|
1450
|
-
let opacity;
|
|
1451
|
-
const slashIdx = ref.indexOf("/");
|
|
1452
|
-
if (slashIdx !== -1) {
|
|
1453
|
-
lookupPath = ref.slice(0, slashIdx);
|
|
1454
|
-
opacity = ref.slice(slashIdx + 1);
|
|
1455
|
-
}
|
|
1456
|
-
const refValue = tokenMap[lookupPath];
|
|
1457
|
-
if (refValue === void 0) {
|
|
1458
|
-
console.warn(`[animus] Token ref {${ref}} — path '${lookupPath}' not found in token map`);
|
|
1459
|
-
return match;
|
|
1460
|
-
}
|
|
1461
|
-
if (opacity) {
|
|
1462
|
-
const alpha = Number.parseInt(opacity, 10);
|
|
1463
|
-
if (alpha === 0) return "transparent";
|
|
1464
|
-
if (alpha !== 100) return `color-mix(in srgb, ${refValue} ${alpha}%, transparent)`;
|
|
1465
|
-
}
|
|
1466
|
-
return refValue;
|
|
1467
|
-
});
|
|
1468
|
-
if (resolved !== value) tokenMap[tokenPath] = resolved;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
2181
|
* Build `@property` registration rules for registered contextual vars.
|
|
1473
2182
|
* The emitted custom property is `--${name}` — the same name the Rust resolver
|
|
1474
2183
|
* maps a bare contextual var value to. Rules emit in declaration order and only
|
|
@@ -1520,4 +2229,4 @@ function buildVariableCss(rootVariables, breakpointVariables, modeVariables, sys
|
|
|
1520
2229
|
return parts.join("\n\n");
|
|
1521
2230
|
}
|
|
1522
2231
|
//#endregion
|
|
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 };
|
|
2232
|
+
export { ASSET_PLACEHOLDER_PREFIX, Animus, AnimusExtended, AnimusExtendedWithAll, AnimusWithAll, BUILT_IN_CONDITIONS, BUILT_IN_SELECTORS, SystemBuilder, ThemeBuilder, areTransformsEqual, asset, borderShorthand, compose, createClassResolver, createComponent, createComposedFamily, createScale, createSystem, createTheme, createTransform, gridItem, gridItemRatio, numericOrStringScale, numericScale, percentageOrAbsolute, size, stringScale };
|