@animus-ui/system 0.1.9 → 0.1.12
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 +124 -51
- package/dist/Animus.d.ts +3 -2
- package/dist/Animus.d.ts.map +1 -1
- package/dist/AnimusExtended.d.ts +3 -2
- package/dist/AnimusExtended.d.ts.map +1 -1
- package/dist/SystemBuilder.d.ts +184 -155
- package/dist/SystemBuilder.d.ts.map +1 -1
- package/dist/appearance/index.d.ts +0 -68
- package/dist/appearance/index.d.ts.map +1 -1
- package/dist/appearance/index.js +0 -91
- package/dist/assert-root-slot-C3Lp9MNJ.js +10 -0
- package/dist/asset.d.ts +4 -13
- package/dist/asset.d.ts.map +1 -1
- package/dist/bootstrap/createAppearanceBootstrap.d.ts +0 -57
- package/dist/bootstrap/createAppearanceBootstrap.d.ts.map +1 -1
- package/dist/bootstrap/index.d.ts.map +1 -1
- package/dist/bootstrap/index.js +4 -58
- package/dist/class-resolver.d.ts +6 -0
- package/dist/class-resolver.d.ts.map +1 -0
- package/dist/class-resolver.js +2 -0
- package/dist/compose.d.ts +2 -16
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +3 -17
- package/dist/composeWithContext.d.ts +4 -15
- package/dist/composeWithContext.d.ts.map +1 -1
- package/dist/composeWithContext.js +5 -16
- package/dist/conditions.d.ts +24 -141
- package/dist/conditions.d.ts.map +1 -1
- package/dist/createClassResolver-EpwTL9Sa.js +228 -0
- package/dist/createComposedFamily-C4NovMcd.js +115 -0
- package/dist/groups/index.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +318 -408
- package/dist/keyframes.d.ts +2 -23
- package/dist/keyframes.d.ts.map +1 -1
- package/dist/runtime/assert-root-slot.d.ts +2 -13
- package/dist/runtime/assert-root-slot.d.ts.map +1 -1
- package/dist/runtime/createClassResolver.d.ts +10 -9
- package/dist/runtime/createClassResolver.d.ts.map +1 -1
- package/dist/runtime/createComposedFamily.d.ts +2 -7
- package/dist/runtime/createComposedFamily.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +0 -16
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/is-dev.d.ts +2 -42
- package/dist/runtime/is-dev.d.ts.map +1 -1
- package/dist/runtime/resolveClasses.d.ts +8 -33
- package/dist/runtime/resolveClasses.d.ts.map +1 -1
- package/dist/runtime/witness.d.ts +0 -8
- package/dist/runtime/witness.d.ts.map +1 -1
- package/dist/runtime-entry.d.ts +2 -7
- package/dist/runtime-entry.d.ts.map +1 -1
- package/dist/runtime-entry.js +2 -1
- package/dist/selectors.d.ts +6 -45
- package/dist/selectors.d.ts.map +1 -1
- package/dist/{size-DHdmBc64.js → size-C6jqxiQy.js} +7 -34
- package/dist/theme/createTheme.d.ts +26 -95
- package/dist/theme/createTheme.d.ts.map +1 -1
- package/dist/theme/flattenScale.d.ts +4 -10
- package/dist/theme/flattenScale.d.ts.map +1 -1
- package/dist/theme/resolveReferences.d.ts +7 -35
- package/dist/theme/resolveReferences.d.ts.map +1 -1
- package/dist/theme/serializeTokens.d.ts +0 -3
- package/dist/theme/serializeTokens.d.ts.map +1 -1
- package/dist/theme/types.d.ts +4 -26
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/utils.d.ts +1 -18
- package/dist/theme/utils.d.ts.map +1 -1
- package/dist/transforms/border.d.ts +1 -4
- package/dist/transforms/border.d.ts.map +1 -1
- package/dist/transforms/createTransform.d.ts +6 -31
- package/dist/transforms/createTransform.d.ts.map +1 -1
- package/dist/transforms/grid.d.ts +2 -12
- package/dist/transforms/grid.d.ts.map +1 -1
- package/dist/transforms/size.d.ts +2 -8
- package/dist/transforms/size.d.ts.map +1 -1
- package/dist/types/component.d.ts +9 -105
- package/dist/types/component.d.ts.map +1 -1
- package/dist/types/config.d.ts +12 -141
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/theme.d.ts +8 -165
- package/dist/types/theme.d.ts.map +1 -1
- package/dist/utils/deepMerge.d.ts.map +1 -1
- package/package.json +17 -4
- package/dist/assert-root-slot-B83scqPh.js +0 -21
- package/dist/createComposedFamily-3ZugjBpU.js +0 -472
package/dist/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as createClassResolver } from "./createClassResolver-EpwTL9Sa.js";
|
|
2
|
+
import { n as createComponent, t as createComposedFamily } from "./createComposedFamily-C4NovMcd.js";
|
|
2
3
|
import { compose } from "./compose.js";
|
|
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-
|
|
4
|
+
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-C6jqxiQy.js";
|
|
4
5
|
//#region src/utils/deepMerge.ts
|
|
5
|
-
/**
|
|
6
|
-
* Deep merge utility — replaces lodash.merge for variant accumulation.
|
|
7
|
-
*/
|
|
8
6
|
const isMergeable = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
7
|
function deepMerge(target, source) {
|
|
10
8
|
const result = { ...target };
|
|
@@ -37,17 +35,20 @@ var AnimusExtendedWithAll = class {
|
|
|
37
35
|
return new AnimusExtended(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, this.activeGroups, this.custom, this.compounds);
|
|
38
36
|
}
|
|
39
37
|
asElement(component) {
|
|
40
|
-
const
|
|
38
|
+
const config = this._buildComponentConfig();
|
|
39
|
+
const Component = createComponent(component, "", config);
|
|
41
40
|
const extendFn = this.extend.bind(this);
|
|
42
41
|
return Object.assign(Component, { extend: extendFn });
|
|
43
42
|
}
|
|
44
43
|
asComponent(AsComponent) {
|
|
45
|
-
const
|
|
44
|
+
const config = this._buildComponentConfig();
|
|
45
|
+
const Component = createComponent(AsComponent, "", config);
|
|
46
46
|
const extendFn = this.extend.bind(this);
|
|
47
47
|
return Object.assign(Component, { extend: extendFn });
|
|
48
48
|
}
|
|
49
49
|
asClass() {
|
|
50
|
-
|
|
50
|
+
const config = this._buildComponentConfig();
|
|
51
|
+
return createClassResolver("", config);
|
|
51
52
|
}
|
|
52
53
|
_buildComponentConfig() {
|
|
53
54
|
const variantConfig = {};
|
|
@@ -136,17 +137,20 @@ var AnimusWithAll = class {
|
|
|
136
137
|
return new AnimusExtended(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, this.activeGroups, this.custom, this.compounds);
|
|
137
138
|
}
|
|
138
139
|
asElement(component) {
|
|
139
|
-
const
|
|
140
|
+
const config = this._buildComponentConfig();
|
|
141
|
+
const Component = createComponent(component, "", config);
|
|
140
142
|
const extendFn = this.extend.bind(this);
|
|
141
143
|
return Object.assign(Component, { extend: extendFn });
|
|
142
144
|
}
|
|
143
145
|
asComponent(AsComponent) {
|
|
144
|
-
const
|
|
146
|
+
const config = this._buildComponentConfig();
|
|
147
|
+
const Component = createComponent(AsComponent, "", config);
|
|
145
148
|
const extendFn = this.extend.bind(this);
|
|
146
149
|
return Object.assign(Component, { extend: extendFn });
|
|
147
150
|
}
|
|
148
151
|
asClass() {
|
|
149
|
-
|
|
152
|
+
const config = this._buildComponentConfig();
|
|
153
|
+
return createClassResolver("", config);
|
|
150
154
|
}
|
|
151
155
|
_buildComponentConfig() {
|
|
152
156
|
const variantConfig = {};
|
|
@@ -230,14 +234,10 @@ var Animus = class extends AnimusWithBase {
|
|
|
230
234
|
};
|
|
231
235
|
//#endregion
|
|
232
236
|
//#region src/asset.ts
|
|
233
|
-
/** The reserved scheme marking an unsubstituted asset reference. */
|
|
234
237
|
const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
|
|
235
238
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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.
|
|
239
|
+
* Brands the specifier only — aliases, exports maps and hashing belong to the
|
|
240
|
+
* host bundler. A literal URL string stays a valid alternative.
|
|
241
241
|
*/
|
|
242
242
|
function asset(specifier) {
|
|
243
243
|
return ASSET_PLACEHOLDER_PREFIX + specifier;
|
|
@@ -245,27 +245,8 @@ function asset(specifier) {
|
|
|
245
245
|
//#endregion
|
|
246
246
|
//#region src/conditions.ts
|
|
247
247
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* The Panda-compatible media-feature set: motion, print, orientation, contrast,
|
|
251
|
-
* and OS color-scheme preferences. Every alias is a pure `@media` feature query
|
|
252
|
-
* (`kind: 'media'`). Color-mode aliases (`_dark` / `_light`) are DELIBERATELY
|
|
253
|
-
* excluded — they are selector-kind (`[data-color-mode]`) surface owned by the
|
|
254
|
-
* `system-color-scheme` change, not media features.
|
|
255
|
-
*
|
|
256
|
-
* ORDER BAND (inc-03 full-pass): built-ins occupy a reserved band BELOW the user
|
|
257
|
-
* band — one slot of 10 per alias in table row order, 300–380. The user band
|
|
258
|
-
* starts at 500 (`mergeConditions` floors allocation at 490 → first user alias
|
|
259
|
-
* is 500), so built-ins and user registrations never interleave on collision.
|
|
260
|
-
* This mirrors the selector registry's real layout (built-ins 10–440, users
|
|
261
|
-
* 500+).
|
|
262
|
-
*
|
|
263
|
-
* The static type-level mirror is `BuiltInConditionAlias` in `types/config.ts`
|
|
264
|
-
* (added to `KnownUnderscoreKey`'s VALIDATING branch, exactly like
|
|
265
|
-
* `BuiltInSelectorAlias`). Built-ins are NOT members of the augmentable
|
|
266
|
-
* `Conditions` interface: interface members would make publication permanently
|
|
267
|
-
* non-empty, killing inc-04's graceful-degradation contract. The
|
|
268
|
-
* `types.test-d.tsx` drift positives keep the two in sync.
|
|
248
|
+
* Media-feature aliases only — color-mode aliases are selector-kind. The
|
|
249
|
+
* reserved order band (300–380) sits below the user band (500+).
|
|
269
250
|
*/
|
|
270
251
|
const BUILT_IN_CONDITIONS = {
|
|
271
252
|
_motionReduce: {
|
|
@@ -314,38 +295,22 @@ const BUILT_IN_CONDITIONS = {
|
|
|
314
295
|
kind: "media"
|
|
315
296
|
}
|
|
316
297
|
};
|
|
317
|
-
/** The supported at-rule prefixes, longest-first so `@media`/`@container`/
|
|
318
|
-
* `@supports` match unambiguously. */
|
|
319
298
|
const CONDITION_PREFIXES = [
|
|
320
299
|
["@media", "media"],
|
|
321
300
|
["@container", "container"],
|
|
322
301
|
["@supports", "supports"]
|
|
323
302
|
];
|
|
324
303
|
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* supported at-rule name. The compile-time complement is `addConditions()`'s
|
|
328
|
-
* value constraint (`@media`/`@container`/`@supports`-prefixed template
|
|
329
|
-
* literals); this runtime throw remains the backstop for untyped callers.
|
|
304
|
+
* Throws on an unsupported prefix — the runtime backstop for untyped
|
|
305
|
+
* callers, complementing `addConditions()`'s value constraint.
|
|
330
306
|
*/
|
|
331
307
|
function inferConditionKind(value) {
|
|
332
308
|
for (const [prefix, kind] of CONDITION_PREFIXES) if (value.startsWith(prefix)) return kind;
|
|
333
309
|
throw new Error(`addConditions: value "${value}" must begin with @media, @container, or @supports`);
|
|
334
310
|
}
|
|
335
311
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* - User aliases override built-ins of the same name, preserving the existing
|
|
339
|
-
* order (mirrors `mergeSelectors`).
|
|
340
|
-
* - New aliases allocate orders CONTINUING from the highest existing order
|
|
341
|
-
* (floored at 490, so the first user alias lands at 500) rather than
|
|
342
|
-
* restarting at 500 each call — chained `.addConditions()` calls must not
|
|
343
|
-
* collide on order 500.
|
|
344
|
-
* - Fails loud when a condition alias name is already reserved by the SELECTOR
|
|
345
|
-
* alias registry (`reservedSelectorNames`): the two registries share the `_`
|
|
346
|
-
* namespace at the call site and their names MUST be disjoint, else a block
|
|
347
|
-
* key would be ambiguous. The error names the offending alias and both
|
|
348
|
-
* registries.
|
|
312
|
+
* Overrides keep the existing order; new aliases continue past the highest
|
|
313
|
+
* existing order (floor 490) so chained calls never collide on 500.
|
|
349
314
|
*/
|
|
350
315
|
function mergeConditions(base, custom, reservedSelectorNames = /* @__PURE__ */ new Set()) {
|
|
351
316
|
const merged = { ...base };
|
|
@@ -369,16 +334,12 @@ function mergeConditions(base, custom, reservedSelectorNames = /* @__PURE__ */ n
|
|
|
369
334
|
}
|
|
370
335
|
return merged;
|
|
371
336
|
}
|
|
372
|
-
/** Get the sorted alias keys for deterministic cascade ordering. */
|
|
373
337
|
function getSortedConditionKeys(map) {
|
|
374
338
|
return Object.keys(map).sort((a, b) => map[a].order - map[b].order);
|
|
375
339
|
}
|
|
376
340
|
/**
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* JSON is deterministic. Distinct in SHAPE from the selector map (which
|
|
380
|
-
* flattens to `alias → selector`) — the object entry is forward-compatible
|
|
381
|
-
* with a future multi-valued `values: string[]` property (design D3).
|
|
341
|
+
* Emits `alias → { value, order, kind }` in cascade order so the JSON is
|
|
342
|
+
* deterministic.
|
|
382
343
|
*/
|
|
383
344
|
function serializeConditionMap(map) {
|
|
384
345
|
const conditions = {};
|
|
@@ -432,12 +393,6 @@ function keyframes(map) {
|
|
|
432
393
|
}
|
|
433
394
|
//#endregion
|
|
434
395
|
//#region src/selectors.ts
|
|
435
|
-
/**
|
|
436
|
-
* Built-in selector aliases.
|
|
437
|
-
*
|
|
438
|
-
* Compound selectors (e.g. `_disabled`) target multiple CSS selectors
|
|
439
|
-
* via comma-separation to cover native, ARIA, and data attribute conventions.
|
|
440
|
-
*/
|
|
441
396
|
const BUILT_IN_SELECTORS = {
|
|
442
397
|
_link: {
|
|
443
398
|
selector: "&:link",
|
|
@@ -541,13 +496,8 @@ const BUILT_IN_SELECTORS = {
|
|
|
541
496
|
}
|
|
542
497
|
};
|
|
543
498
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
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.
|
|
499
|
+
* Order allocation continues past the highest existing order (floor 490, so
|
|
500
|
+
* the first custom alias is 500); restarting per call would collide.
|
|
551
501
|
*/
|
|
552
502
|
function mergeSelectors(base, custom) {
|
|
553
503
|
const merged = { ...base };
|
|
@@ -565,15 +515,9 @@ function mergeSelectors(base, custom) {
|
|
|
565
515
|
}
|
|
566
516
|
return merged;
|
|
567
517
|
}
|
|
568
|
-
/** Get the sorted alias keys for deterministic cascade ordering. */
|
|
569
518
|
function getSortedAliasKeys(map) {
|
|
570
519
|
return Object.keys(map).sort((a, b) => map[a].order - map[b].order);
|
|
571
520
|
}
|
|
572
|
-
/**
|
|
573
|
-
* Serialize the selector map for the extraction pipeline.
|
|
574
|
-
* Emits a flat `Record<string, string>` (alias → selector) plus
|
|
575
|
-
* the ordered key list for cascade determinism.
|
|
576
|
-
*/
|
|
577
521
|
function serializeSelectorMap(map) {
|
|
578
522
|
const selectors = {};
|
|
579
523
|
const order = getSortedAliasKeys(map);
|
|
@@ -585,6 +529,68 @@ function serializeSelectorMap(map) {
|
|
|
585
529
|
}
|
|
586
530
|
//#endregion
|
|
587
531
|
//#region src/SystemBuilder.ts
|
|
532
|
+
/**
|
|
533
|
+
* No console output: a warn here would ship in production consumer bundles,
|
|
534
|
+
* and the extraction host shims `console`. The record is the witness.
|
|
535
|
+
*/
|
|
536
|
+
function legacyVerbWitness(source, verb, sourceIndex) {
|
|
537
|
+
const record = source.getVocabularyRecord?.();
|
|
538
|
+
if (!record) return null;
|
|
539
|
+
const names = [...record.keyframes.map((entry) => entry.name), ...record.globalStyles.map((entry) => entry.name)];
|
|
540
|
+
if (names.length === 0) return null;
|
|
541
|
+
return {
|
|
542
|
+
code: "animus.vocabulary.legacy-verb",
|
|
543
|
+
verb,
|
|
544
|
+
source: `${verb} source #${sourceIndex}`,
|
|
545
|
+
names
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Frozen two levels deep (frame entries + stop bodies) so later mutation of
|
|
550
|
+
* the caller's collection cannot reach a sealed record; deeper values alias.
|
|
551
|
+
*/
|
|
552
|
+
function snapshotFrameData(frames) {
|
|
553
|
+
const copy = {};
|
|
554
|
+
for (const [key, entry] of Object.entries(frames)) {
|
|
555
|
+
const stops = {};
|
|
556
|
+
for (const [stop, body] of Object.entries(entry.frames ?? {})) stops[stop] = Object.freeze({ ...body });
|
|
557
|
+
copy[key] = Object.freeze({
|
|
558
|
+
name: entry.name,
|
|
559
|
+
frames: Object.freeze(stops)
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return Object.freeze(copy);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* A name collision resolves to the incoming side, and the winner takes the
|
|
566
|
+
* incoming declaration position so record order stays declaration order.
|
|
567
|
+
*/
|
|
568
|
+
function mergeVocabularyEntries(existingEntries, existingCollisions, incoming, incomingOrigin) {
|
|
569
|
+
const entries = existingEntries.map((entry) => ({ ...entry }));
|
|
570
|
+
const collisions = [...existingCollisions];
|
|
571
|
+
for (const input of incoming) {
|
|
572
|
+
const existingIndex = entries.findIndex((entry) => entry.name === input.name);
|
|
573
|
+
if (existingIndex !== -1) {
|
|
574
|
+
const loser = entries[existingIndex];
|
|
575
|
+
collisions.push({
|
|
576
|
+
code: "animus.vocabulary.collision",
|
|
577
|
+
name: input.name,
|
|
578
|
+
winner: incomingOrigin,
|
|
579
|
+
loser: loser.origin
|
|
580
|
+
});
|
|
581
|
+
console.warn(`animus: vocabulary "${input.name}" is registered by both ${loser.origin} and ${incomingOrigin} — ${incomingOrigin} wins; rename one entry (animus.vocabulary.collision)`);
|
|
582
|
+
entries.splice(existingIndex, 1);
|
|
583
|
+
}
|
|
584
|
+
entries.push({
|
|
585
|
+
...input,
|
|
586
|
+
origin: incomingOrigin
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
entries,
|
|
591
|
+
collisions
|
|
592
|
+
};
|
|
593
|
+
}
|
|
588
594
|
const snapshotTransformBySource = /* @__PURE__ */ new WeakMap();
|
|
589
595
|
function snapshotTransform(source) {
|
|
590
596
|
const cached = snapshotTransformBySource.get(source);
|
|
@@ -607,12 +613,10 @@ function snapshotTransform(source) {
|
|
|
607
613
|
return wrapper;
|
|
608
614
|
}
|
|
609
615
|
/**
|
|
610
|
-
*
|
|
611
|
-
* being callable
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
* script in the Rust system-loader mirrors it by necessity (it cannot
|
|
615
|
-
* import TS) and points back here.
|
|
616
|
+
* A built system also carries a `.system()` chain method, so only
|
|
617
|
+
* `system.toConfig` being callable discriminates a bundle from an instance.
|
|
618
|
+
* The QuickJS capture script in the Rust system-loader reimplements the same
|
|
619
|
+
* discriminator — it cannot import this file, so change both together.
|
|
616
620
|
*/
|
|
617
621
|
function isLibraryBundle(value) {
|
|
618
622
|
const system = value?.system;
|
|
@@ -639,13 +643,6 @@ function arePropDefinitionsEqual(existing, incoming, structuralScale = false) {
|
|
|
639
643
|
function orderedMembersEqual(existing, incoming) {
|
|
640
644
|
return existing.length === incoming.length && existing.every((member, index) => member === incoming[index]);
|
|
641
645
|
}
|
|
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
646
|
function divergentPropError(key, existing, incoming, existingOrigin, incomingOrigin) {
|
|
650
647
|
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)}".`);
|
|
651
648
|
}
|
|
@@ -657,7 +654,10 @@ var SystemBuilder = class SystemBuilder {
|
|
|
657
654
|
#conditionRegistry;
|
|
658
655
|
#extendProvenance;
|
|
659
656
|
#extendCount;
|
|
660
|
-
|
|
657
|
+
#vocabularyRegistry;
|
|
658
|
+
#vocabularyCollisions;
|
|
659
|
+
#legacyVerbWitnesses;
|
|
660
|
+
constructor(propRegistry, groupRegistry, selectorRegistry, includesRegistry, conditionRegistry, extendProvenance, extendCount, vocabularyRegistry, vocabularyCollisions, legacyVerbWitnesses) {
|
|
661
661
|
this.#propRegistry = propRegistry || {};
|
|
662
662
|
this.#groupRegistry = groupRegistry || {};
|
|
663
663
|
this.#selectorRegistry = selectorRegistry || { ...BUILT_IN_SELECTORS };
|
|
@@ -665,6 +665,9 @@ var SystemBuilder = class SystemBuilder {
|
|
|
665
665
|
this.#conditionRegistry = conditionRegistry || { ...BUILT_IN_CONDITIONS };
|
|
666
666
|
this.#extendProvenance = extendProvenance || /* @__PURE__ */ new Map();
|
|
667
667
|
this.#extendCount = extendCount || 0;
|
|
668
|
+
this.#vocabularyRegistry = vocabularyRegistry || [];
|
|
669
|
+
this.#vocabularyCollisions = vocabularyCollisions || [];
|
|
670
|
+
this.#legacyVerbWitnesses = legacyVerbWitnesses || [];
|
|
668
671
|
}
|
|
669
672
|
#originOf(provenanceKey) {
|
|
670
673
|
const index = this.#extendProvenance.get(provenanceKey);
|
|
@@ -672,7 +675,9 @@ var SystemBuilder = class SystemBuilder {
|
|
|
672
675
|
}
|
|
673
676
|
from(source) {
|
|
674
677
|
const instance = isLibraryBundle(source) ? source.system : source;
|
|
675
|
-
|
|
678
|
+
const fromCount = this.#legacyVerbWitnesses.filter((entry) => entry.verb === "from").length;
|
|
679
|
+
const witness = legacyVerbWitness(instance, "from", fromCount + 1);
|
|
680
|
+
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, [...this.#includesRegistry, instance], this.#conditionRegistry, this.#extendProvenance, this.#extendCount, this.#vocabularyRegistry, this.#vocabularyCollisions, witness ? [...this.#legacyVerbWitnesses, witness] : this.#legacyVerbWitnesses);
|
|
676
681
|
}
|
|
677
682
|
extend(source) {
|
|
678
683
|
const instance = isLibraryBundle(source) ? source.system : source;
|
|
@@ -750,57 +755,43 @@ var SystemBuilder = class SystemBuilder {
|
|
|
750
755
|
...this.#conditionRegistry,
|
|
751
756
|
...conditionOverrides
|
|
752
757
|
}, newConditions, new Set(Object.keys(nextSelectors)));
|
|
753
|
-
|
|
758
|
+
const sourceRecord = instance.getVocabularyRecord?.();
|
|
759
|
+
if (!sourceRecord) throw new Error("extend: source system is not sealed (or was built by an older @animus-ui/system) — registered vocabulary travels only on sealed instances. Call seal() on the source bundle and export the sealed instance.");
|
|
760
|
+
let nextVocabulary = this.#vocabularyRegistry;
|
|
761
|
+
let nextVocabularyCollisions = this.#vocabularyCollisions;
|
|
762
|
+
const inheritedEntries = [...sourceRecord.keyframes.map((entry) => ({
|
|
763
|
+
kind: "keyframes",
|
|
764
|
+
name: entry.name,
|
|
765
|
+
frames: entry.frames
|
|
766
|
+
})), ...sourceRecord.globalStyles.map((entry) => ({
|
|
767
|
+
kind: "globalStyles",
|
|
768
|
+
name: entry.name,
|
|
769
|
+
styles: entry.styles,
|
|
770
|
+
...entry.fontFaces ? { fontFaces: entry.fontFaces } : {}
|
|
771
|
+
}))];
|
|
772
|
+
if (inheritedEntries.length > 0) {
|
|
773
|
+
const merged = mergeVocabularyEntries(this.#vocabularyRegistry, this.#vocabularyCollisions, inheritedEntries, incomingOrigin);
|
|
774
|
+
nextVocabulary = merged.entries;
|
|
775
|
+
nextVocabularyCollisions = merged.collisions;
|
|
776
|
+
}
|
|
777
|
+
return new SystemBuilder(nextProps, nextGroups, nextSelectors, [...this.#includesRegistry, instance], nextConditions, provenance, sourceIndex, nextVocabulary, nextVocabularyCollisions, this.#legacyVerbWitnesses);
|
|
754
778
|
}
|
|
755
779
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
* CONDITION registry (built-in or registered earlier in this chain) maps to
|
|
759
|
-
* the branded `ReservedByConditionRegistry` instead of its selector string —
|
|
760
|
-
* the compile-time complement of the cross-registry throw below. The clash is
|
|
761
|
-
* checked in VALUE position so `S` stays a naked inference site; subtracting
|
|
762
|
-
* the reserved names from the KEY position is impossible — `Exclude` is a
|
|
763
|
-
* silent no-op against a template-literal pattern. `NarrowedAliases` keeps a
|
|
764
|
-
* widened `Conds` from swallowing the whole `_` namespace.
|
|
765
|
-
*
|
|
766
|
-
* It wraps the ACCUMULATION too: a widened registration contributes nothing
|
|
767
|
-
* to `Sels` rather than widening it, so the pattern can never reach the
|
|
768
|
-
* published `RegistryBrand` — see `addConditions` for the full argument.
|
|
780
|
+
* Registers `_` aliases → `&`-relative selectors. The cross-registry clash
|
|
781
|
+
* is checked in VALUE position: `Exclude` on a `_${string}` key is a no-op.
|
|
769
782
|
*/
|
|
770
783
|
addSelectors(selectors) {
|
|
771
784
|
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.`);
|
|
772
785
|
const merged = mergeSelectors(this.#selectorRegistry, selectors);
|
|
773
|
-
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, merged, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount);
|
|
786
|
+
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, merged, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount, this.#vocabularyRegistry, this.#vocabularyCollisions, this.#legacyVerbWitnesses);
|
|
774
787
|
}
|
|
775
788
|
/**
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* Recognized as block keys in style objects; user aliases override built-ins
|
|
779
|
-
* of the same name (design D3). Keys are constrained to `_`-prefixed aliases
|
|
780
|
-
* and values to `@`-prefixed at-rule strings — a value that does not begin
|
|
781
|
-
* with an at-rule name is a compile-time type error (design D9; the runtime
|
|
782
|
-
* `inferConditionKind` throw is defense-in-depth). The registered keys are
|
|
783
|
-
* accumulated into the phantom `Conds` union and surfaced on `build()`.
|
|
784
|
-
*
|
|
785
|
-
* A key owned by the SELECTOR registry (built-in or registered earlier in
|
|
786
|
-
* this chain) maps to the branded `ReservedBySelectorRegistry` — see
|
|
787
|
-
* `addSelectors` for why the clash is checked in value position. Overriding a
|
|
788
|
-
* built-in CONDITION alias stays legal (design D3); only the OPPOSITE
|
|
789
|
-
* registry is subtracted.
|
|
790
|
-
*
|
|
791
|
-
* `NarrowedAliases` wraps the ACCUMULATION, not just the gate: a widened
|
|
792
|
-
* registration (`Record<`_${string}`, …>`) contributes nothing to `Conds`.
|
|
793
|
-
* Accumulating the pattern instead would carry it out through `build()`'s
|
|
794
|
-
* `RegistryBrand`, and a consumer publishing `ConditionsOf` into the
|
|
795
|
-
* augmentable `Conditions` interface would then have every `_` key type as
|
|
796
|
-
* registered — `UnknownConditionAlias` would never fire again, anywhere in
|
|
797
|
-
* that project. The gate keeps its own `NarrowedAliases` regardless:
|
|
798
|
-
* `extend()`-sourced unions can still arrive widened from a system built
|
|
799
|
-
* before this narrowing.
|
|
789
|
+
* Registers `_` aliases → at-rule strings; user aliases override built-ins
|
|
790
|
+
* of the same name. A selector-owned key maps to the branded rejection.
|
|
800
791
|
*/
|
|
801
792
|
addConditions(conditions) {
|
|
802
793
|
const merged = mergeConditions(this.#conditionRegistry, conditions, new Set(Object.keys(this.#selectorRegistry)));
|
|
803
|
-
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, merged, this.#extendProvenance, this.#extendCount);
|
|
794
|
+
return new SystemBuilder(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, merged, this.#extendProvenance, this.#extendCount, this.#vocabularyRegistry, this.#vocabularyCollisions, this.#legacyVerbWitnesses);
|
|
804
795
|
}
|
|
805
796
|
addGroup(name, config) {
|
|
806
797
|
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.`);
|
|
@@ -817,10 +808,11 @@ var SystemBuilder = class SystemBuilder {
|
|
|
817
808
|
...config
|
|
818
809
|
};
|
|
819
810
|
const newGroup = { [name]: Object.keys(config) };
|
|
820
|
-
|
|
811
|
+
const nextGroups = {
|
|
821
812
|
...this.#groupRegistry,
|
|
822
813
|
...newGroup
|
|
823
|
-
}
|
|
814
|
+
};
|
|
815
|
+
return new SystemBuilder(nextProps, nextGroups, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount, this.#vocabularyRegistry, this.#vocabularyCollisions, this.#legacyVerbWitnesses);
|
|
824
816
|
}
|
|
825
817
|
addProps(config) {
|
|
826
818
|
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.`);
|
|
@@ -832,42 +824,123 @@ var SystemBuilder = class SystemBuilder {
|
|
|
832
824
|
throw new Error(`Prop "${key}" already registered with a different definition.`);
|
|
833
825
|
}
|
|
834
826
|
}
|
|
835
|
-
|
|
827
|
+
const nextProps = {
|
|
836
828
|
...this.#propRegistry,
|
|
837
829
|
...config
|
|
838
|
-
}
|
|
830
|
+
};
|
|
831
|
+
return new SystemBuilder(nextProps, this.#groupRegistry, this.#selectorRegistry, this.#includesRegistry, this.#conditionRegistry, this.#extendProvenance, this.#extendCount, this.#vocabularyRegistry, this.#vocabularyCollisions, this.#legacyVerbWitnesses);
|
|
839
832
|
}
|
|
840
833
|
build() {
|
|
841
|
-
const
|
|
834
|
+
const propSource = Object.fromEntries(Object.entries(this.#propRegistry).map(([key, entry]) => [key, { ...entry }]));
|
|
835
|
+
const groupSource = Object.fromEntries(Object.entries(this.#groupRegistry).map(([key, members]) => [key, [...members]]));
|
|
842
836
|
const snapshot = createRegistrySnapshot(this.#propRegistry, this.#groupRegistry, this.#selectorRegistry, this.#conditionRegistry);
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
837
|
+
const mintInstance = () => {
|
|
838
|
+
const animus = new Animus(Object.fromEntries(Object.entries(propSource).map(([key, entry]) => [key, { ...entry }])), Object.fromEntries(Object.entries(groupSource).map(([key, members]) => [key, [...members]])));
|
|
839
|
+
const instance = Object.assign(animus, { toConfig: () => {
|
|
840
|
+
return serializeInstance(snapshot.props, snapshot.groups, snapshot.selectors, snapshot.conditions);
|
|
841
|
+
} });
|
|
842
|
+
Object.defineProperty(instance, "getRegistrySnapshot", {
|
|
843
|
+
value: () => snapshot,
|
|
844
|
+
enumerable: false
|
|
845
|
+
});
|
|
846
|
+
return instance;
|
|
847
|
+
};
|
|
848
|
+
const system = mintInstance();
|
|
849
|
+
const legacyVerbWitnessRecord = this.#legacyVerbWitnesses;
|
|
850
850
|
const createGlobalStyles = ((styles, options) => ({
|
|
851
851
|
__brand: "GlobalStyleBlock",
|
|
852
852
|
styles,
|
|
853
853
|
...options?.fontFaces?.length ? { fontFaces: [...options.fontFaces] } : {}
|
|
854
854
|
}));
|
|
855
855
|
const createKeyframes = ((frames) => keyframes(frames));
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
856
|
+
const makeBundle = (entries, collisions, localCallCount) => {
|
|
857
|
+
let consumedBy;
|
|
858
|
+
const registerEntries = (label, incoming) => {
|
|
859
|
+
if (consumedBy === "seal") throw new Error(`${label}: this system is already sealed — registration happens between build() and seal().`);
|
|
860
|
+
if (consumedBy === "register") throw new Error(`${label}: this bundle was superseded by a later registration call — registration is linear; chain the calls and seal the final bundle.`);
|
|
861
|
+
const merged = mergeVocabularyEntries(entries, collisions, incoming, `local registration #${localCallCount + 1}`);
|
|
862
|
+
consumedBy = "register";
|
|
863
|
+
return makeBundle(merged.entries, merged.collisions, localCallCount + 1);
|
|
864
|
+
};
|
|
865
|
+
const registerKeyframes = (map) => {
|
|
866
|
+
const incoming = [];
|
|
867
|
+
for (const [name, collection] of Object.entries(map)) {
|
|
868
|
+
if (!collection || collection.__brand !== "Keyframes" || typeof collection.__frames !== "object") throw new TypeError(`registerKeyframes: "${name}" is not a createKeyframes collection — register the factory return value itself.`);
|
|
869
|
+
incoming.push({
|
|
870
|
+
kind: "keyframes",
|
|
871
|
+
name,
|
|
872
|
+
frames: snapshotFrameData(collection.__frames)
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return registerEntries("registerKeyframes", incoming);
|
|
876
|
+
};
|
|
877
|
+
const registerGlobalStyles = (map) => {
|
|
878
|
+
const incoming = [];
|
|
879
|
+
for (const [name, block] of Object.entries(map)) {
|
|
880
|
+
if (!block || block.__brand !== "GlobalStyleBlock" || typeof block.styles !== "object") throw new TypeError(`registerGlobalStyles: "${name}" is not a createGlobalStyles block — register the factory return value itself.`);
|
|
881
|
+
const blockValue = block;
|
|
882
|
+
const styles = Object.freeze(Object.fromEntries(Object.entries(blockValue.styles).map(([selector, body]) => [selector, Object.freeze({ ...body })])));
|
|
883
|
+
incoming.push({
|
|
884
|
+
kind: "globalStyles",
|
|
885
|
+
name,
|
|
886
|
+
styles,
|
|
887
|
+
...blockValue.fontFaces?.length ? { fontFaces: Object.freeze(blockValue.fontFaces.map((face) => Object.freeze({ ...face }))) } : {}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return registerEntries("registerGlobalStyles", incoming);
|
|
891
|
+
};
|
|
892
|
+
const seal = () => {
|
|
893
|
+
if (consumedBy === "seal") throw new Error("seal: this system is already sealed — seal() returns exactly one instance per build().");
|
|
894
|
+
if (consumedBy === "register") throw new Error("seal: this bundle was superseded by a later registration call — registration is linear; seal the final bundle.");
|
|
895
|
+
const record = Object.freeze({
|
|
896
|
+
version: 1,
|
|
897
|
+
keyframes: Object.freeze(entries.filter((entry) => entry.kind === "keyframes").map((entry) => Object.freeze({
|
|
898
|
+
name: entry.name,
|
|
899
|
+
frames: entry.frames
|
|
900
|
+
}))),
|
|
901
|
+
globalStyles: Object.freeze(entries.filter((entry) => entry.kind === "globalStyles").map((entry) => Object.freeze({
|
|
902
|
+
name: entry.name,
|
|
903
|
+
styles: entry.styles,
|
|
904
|
+
...entry.fontFaces ? { fontFaces: entry.fontFaces } : {}
|
|
905
|
+
}))),
|
|
906
|
+
collisions: Object.freeze(collisions.map((entry) => Object.freeze({ ...entry }))),
|
|
907
|
+
legacyVerbs: Object.freeze(legacyVerbWitnessRecord.map((entry) => ({
|
|
908
|
+
...entry,
|
|
909
|
+
names: entry.names.filter((name) => !entries.some((kept) => kept.name === name))
|
|
910
|
+
})).filter((entry) => entry.names.length > 0).map((entry) => Object.freeze({
|
|
911
|
+
...entry,
|
|
912
|
+
names: Object.freeze([...entry.names])
|
|
913
|
+
})))
|
|
914
|
+
});
|
|
915
|
+
const sealed = mintInstance();
|
|
916
|
+
Object.defineProperty(sealed, "getVocabularyRecord", {
|
|
917
|
+
value: () => record,
|
|
918
|
+
enumerable: false
|
|
919
|
+
});
|
|
920
|
+
for (const member of ["registerKeyframes", "registerGlobalStyles"]) Object.defineProperty(sealed, member, {
|
|
921
|
+
value: () => {
|
|
922
|
+
throw new Error(`${member}: this system is sealed — registration happens between build() and seal().`);
|
|
923
|
+
},
|
|
924
|
+
enumerable: false
|
|
925
|
+
});
|
|
926
|
+
consumedBy = "seal";
|
|
927
|
+
return sealed;
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
system,
|
|
931
|
+
createGlobalStyles,
|
|
932
|
+
createKeyframes,
|
|
933
|
+
registerKeyframes,
|
|
934
|
+
registerGlobalStyles,
|
|
935
|
+
seal
|
|
936
|
+
};
|
|
860
937
|
};
|
|
938
|
+
return makeBundle(this.#vocabularyRegistry.map((entry) => ({ ...entry })), [...this.#vocabularyCollisions], 0);
|
|
861
939
|
}
|
|
862
940
|
};
|
|
863
941
|
/**
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* (`properties` arrays, object/array scales) are copies, so neither the
|
|
867
|
-
* builder's onward chaining nor post-build mutation of the instance's public
|
|
868
|
-
* registry fields reaches serialized or merged output. Transform functions
|
|
869
|
-
* are cached immutable forwarding wrappers: behavior survives without keeping
|
|
870
|
-
* mutable serialization metadata live.
|
|
942
|
+
* Copies and freezes containers, entries, and the mutable values inside a
|
|
943
|
+
* prop, so later mutation reaches neither serialized nor merged output.
|
|
871
944
|
*/
|
|
872
945
|
function createRegistrySnapshot(propRegistry, groupRegistry, selectorRegistry, conditionRegistry) {
|
|
873
946
|
const props = {};
|
|
@@ -933,33 +1006,24 @@ function serializeInstance(propRegistry, groupRegistry, selectorRegistry, condit
|
|
|
933
1006
|
};
|
|
934
1007
|
}
|
|
935
1008
|
function createSystem(config) {
|
|
936
|
-
|
|
1009
|
+
const includes = config?.includes ?? [];
|
|
1010
|
+
const witnesses = [];
|
|
1011
|
+
includes.forEach((source, index) => {
|
|
1012
|
+
const witness = legacyVerbWitness(source, "includes", index + 1);
|
|
1013
|
+
if (witness) witnesses.push(witness);
|
|
1014
|
+
});
|
|
1015
|
+
return new SystemBuilder(void 0, void 0, void 0, includes, void 0, void 0, void 0, void 0, void 0, witnesses);
|
|
937
1016
|
}
|
|
938
1017
|
//#endregion
|
|
939
1018
|
//#region src/theme/resolveReferences.ts
|
|
940
1019
|
/**
|
|
941
|
-
*
|
|
942
|
-
* openspec change first-class-extension).
|
|
943
|
-
*
|
|
944
|
-
* Replaces the old single-pass, insertion-order-dependent rewrite: references
|
|
945
|
-
* resolve against the COMPLETE flattened map via depth-first traversal of the
|
|
946
|
-
* reference DAG, so declaration order is never observable (G3), references
|
|
947
|
-
* inside emitted scales resolve into the emitted variable declarations
|
|
948
|
-
* instead of leaking literally into CSS (G2), and a token resolves to the
|
|
949
|
-
* same value whether its scale is emitted or inlined (G1 — an emitted target
|
|
950
|
-
* substitutes as `var()`, which the cascade late-binds to exactly the value
|
|
951
|
-
* the inlined form substitutes directly).
|
|
952
|
-
*
|
|
953
|
-
* Pure module: consumes the flatten pass's maps, returns fresh maps, mutates
|
|
954
|
-
* nothing. Runs in QuickJS — ES built-ins only, no Node/WHATWG APIs.
|
|
1020
|
+
* Runs inside QuickJS: ES built-ins only, no Node or WHATWG APIs.
|
|
955
1021
|
*/
|
|
956
|
-
/** Token ref pattern: {scale.key}, {scale.key.sub}, or {path/NN} (opacity). */
|
|
957
1022
|
const TOKEN_REF_RE$1 = /\{([^}]+)\}/g;
|
|
958
|
-
/** DFS node states for cycle detection. */
|
|
959
1023
|
const UNVISITED = 0;
|
|
960
1024
|
const RESOLVING = 1;
|
|
961
1025
|
const RESOLVED = 2;
|
|
962
|
-
/**
|
|
1026
|
+
/** `matchAll`, not `exec`: a shared global regex would carry `lastIndex`. */
|
|
963
1027
|
function parseReferences(value) {
|
|
964
1028
|
const references = [];
|
|
965
1029
|
for (const match of value.matchAll(TOKEN_REF_RE$1)) {
|
|
@@ -977,26 +1041,12 @@ function parseReferences(value) {
|
|
|
977
1041
|
return references;
|
|
978
1042
|
}
|
|
979
1043
|
/**
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
* - `tokenMap`/`variableMap`/`variables` are `flattenTheme`'s outputs: an
|
|
983
|
-
* emitted path's tokenMap entry is its `var()` indirection and its RAW
|
|
984
|
-
* authored value is parked in `variables`; a non-emitted path's tokenMap
|
|
985
|
-
* entry IS its raw value.
|
|
986
|
-
* - Substitution per reference: emitted target → `var(--target)`; non-emitted
|
|
987
|
-
* target → its resolved literal; `{path/NN}` keeps the existing
|
|
988
|
-
* `color-mix` output shape (`/0` → `transparent`, `/100` → the base).
|
|
989
|
-
* - Unresolvable target: warn once per missing path, keep the literal — a
|
|
990
|
-
* SUPPORTED pattern (a kit theme may reference tokens its consumer
|
|
991
|
-
* provides later). Dangling-reference errors arrive with the explicit
|
|
992
|
-
* replacement form (D5, increment 04).
|
|
993
|
-
* - Reference cycle: hard error naming the cycle's token paths in traversal
|
|
994
|
-
* order, regardless of emission flags (emission-invariant failure, G1).
|
|
1044
|
+
* An unresolvable reference warns and keeps its literal: a kit theme may
|
|
1045
|
+
* reference tokens its consumer supplies later.
|
|
995
1046
|
*/
|
|
996
1047
|
function resolveReferences(tokenMap, variableMap, variables) {
|
|
997
1048
|
const paths = Object.keys(tokenMap).sort();
|
|
998
1049
|
const known = new Set(paths);
|
|
999
|
-
/** The authored (raw) value behind a path — see the contract note above. */
|
|
1000
1050
|
const rawValueOf = (path) => {
|
|
1001
1051
|
const varName = variableMap[path];
|
|
1002
1052
|
if (varName !== void 0 && variables[varName] !== void 0) return variables[varName];
|
|
@@ -1012,7 +1062,6 @@ function resolveReferences(tokenMap, variableMap, variables) {
|
|
|
1012
1062
|
}
|
|
1013
1063
|
const state = /* @__PURE__ */ new Map();
|
|
1014
1064
|
const resolved = /* @__PURE__ */ new Map();
|
|
1015
|
-
/** Active DFS trail — the cycle report slices it from the revisited node. */
|
|
1016
1065
|
const trail = [];
|
|
1017
1066
|
const warnedMissing = /* @__PURE__ */ new Set();
|
|
1018
1067
|
const substitute = (reference, match) => {
|
|
@@ -1075,7 +1124,6 @@ function resolveReferences(tokenMap, variableMap, variables) {
|
|
|
1075
1124
|
}
|
|
1076
1125
|
//#endregion
|
|
1077
1126
|
//#region src/theme/utils.ts
|
|
1078
|
-
/** Type guard: true for non-null, non-array objects */
|
|
1079
1127
|
function isObject(value) {
|
|
1080
1128
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1081
1129
|
}
|
|
@@ -1091,11 +1139,6 @@ function merge(target, ...sources) {
|
|
|
1091
1139
|
}
|
|
1092
1140
|
return target;
|
|
1093
1141
|
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Resolve a dot-path string against a nested object.
|
|
1096
|
-
* walkDotPath({ gray: { 50: '#fafafa' } }, 'gray.50') → '#fafafa'
|
|
1097
|
-
* The `_` identity key is handled: 'primary' resolves to obj.primary._ if obj.primary is an object with _.
|
|
1098
|
-
*/
|
|
1099
1142
|
function walkDotPath(obj, path) {
|
|
1100
1143
|
const parts = path.split(".");
|
|
1101
1144
|
let current = obj;
|
|
@@ -1106,11 +1149,7 @@ function walkDotPath(obj, path) {
|
|
|
1106
1149
|
return current;
|
|
1107
1150
|
}
|
|
1108
1151
|
/**
|
|
1109
|
-
*
|
|
1110
|
-
* The `_` key is an identity marker — it produces the parent key without suffix.
|
|
1111
|
-
* { gray: { 50: '#fafafa' } } → { 'gray.50': '#fafafa' }
|
|
1112
|
-
* { primary: { _: 'ember', hover: 'x' } } → { 'primary': 'ember', 'primary.hover': 'x' }
|
|
1113
|
-
* CSS variable names use dash-join, computed at the serialization boundary (not here).
|
|
1152
|
+
* A `_` key is an identity marker: it takes the parent path with no suffix.
|
|
1114
1153
|
*/
|
|
1115
1154
|
function flattenToDotPaths(object, path) {
|
|
1116
1155
|
const result = {};
|
|
@@ -1122,15 +1161,9 @@ function flattenToDotPaths(object, path) {
|
|
|
1122
1161
|
}
|
|
1123
1162
|
return result;
|
|
1124
1163
|
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Convert a dot-path key to a dash-join key for CSS variable naming.
|
|
1127
|
-
* 'gray.50' → 'gray-50'
|
|
1128
|
-
* 'primary.hover' → 'primary-hover'
|
|
1129
|
-
*/
|
|
1130
1164
|
function dotToDash(dotPath) {
|
|
1131
1165
|
return dotPath.replace(/\./g, "-");
|
|
1132
1166
|
}
|
|
1133
|
-
/** Map over object values — matches lodash.mapValues overload signatures */
|
|
1134
1167
|
//#endregion
|
|
1135
1168
|
//#region src/theme/createTheme.ts
|
|
1136
1169
|
const COLOR_FUNCTION_PREFIXES = [
|
|
@@ -1145,7 +1178,6 @@ const COLOR_FUNCTION_PREFIXES = [
|
|
|
1145
1178
|
"color(",
|
|
1146
1179
|
"color-mix("
|
|
1147
1180
|
];
|
|
1148
|
-
/** Validate that a value is a valid CSS <color>. */
|
|
1149
1181
|
function isValidCSSColor(value) {
|
|
1150
1182
|
if (typeof value !== "string") return false;
|
|
1151
1183
|
const v = value.trim();
|
|
@@ -1156,10 +1188,6 @@ function isValidCSSColor(value) {
|
|
|
1156
1188
|
if (/^[a-zA-Z]+$/.test(v)) return true;
|
|
1157
1189
|
return false;
|
|
1158
1190
|
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Validate that mode aliases reference existing color keys via dot-path traversal.
|
|
1161
|
-
* Aliases are dot-path strings like 'gray.50' that must resolve in the nested color structure.
|
|
1162
|
-
*/
|
|
1163
1191
|
function validateModeAliases(modeName, aliases, nestedColors, flatColorKeys, prefix) {
|
|
1164
1192
|
for (const [key, value] of Object.entries(aliases)) {
|
|
1165
1193
|
const aliasPath = prefix ? `${prefix}.${key}` : key;
|
|
@@ -1173,12 +1201,11 @@ function validateModeAliases(modeName, aliases, nestedColors, flatColorKeys, pre
|
|
|
1173
1201
|
}
|
|
1174
1202
|
}
|
|
1175
1203
|
/**
|
|
1176
|
-
*
|
|
1177
|
-
* `data-color-mode
|
|
1178
|
-
* the `:root:not([data-color-mode])` guard while matching no mode block.
|
|
1204
|
+
* The system preference is the ABSENCE of `data-color-mode`; a mode named
|
|
1205
|
+
* `system` would defeat the `:root:not([data-color-mode])` guard.
|
|
1179
1206
|
*/
|
|
1180
1207
|
const RESERVED_MODE_NAME = "system";
|
|
1181
|
-
const RESERVED_THEME_KEYS = new Set([
|
|
1208
|
+
const RESERVED_THEME_KEYS = /* @__PURE__ */ new Set([
|
|
1182
1209
|
"breakpoints",
|
|
1183
1210
|
"modes",
|
|
1184
1211
|
"mode",
|
|
@@ -1190,22 +1217,16 @@ const RESERVED_THEME_KEYS = new Set([
|
|
|
1190
1217
|
"serialize",
|
|
1191
1218
|
"varRef"
|
|
1192
1219
|
]);
|
|
1193
|
-
const COLOR_SCHEME_VALUES = new Set([
|
|
1220
|
+
const COLOR_SCHEME_VALUES = /* @__PURE__ */ new Set([
|
|
1194
1221
|
"light",
|
|
1195
1222
|
"dark",
|
|
1196
1223
|
"normal"
|
|
1197
1224
|
]);
|
|
1198
|
-
/**
|
|
1199
|
-
* The axis → scheme pairs the mapping forces: the mode named for the OS light
|
|
1200
|
-
* preference must classify `light`, the dark one `dark`. Single source for
|
|
1201
|
-
* BOTH the default fill and the conflict check in
|
|
1202
|
-
* {@link resolveColorModeOptions} — one rule, one table.
|
|
1203
|
-
*/
|
|
1225
|
+
/** OS-preference axis → the `color-scheme` a mode mapped to it must carry. */
|
|
1204
1226
|
const MAPPING_FORCED_SCHEMES = [["light", "light"], ["dark", "dark"]];
|
|
1205
1227
|
/**
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
* one, so an unconfigured theme never gains the key (byte parity, G4).
|
|
1228
|
+
* `undefined` when neither side supplied one, so an unconfigured theme never
|
|
1229
|
+
* gains the key.
|
|
1209
1230
|
*/
|
|
1210
1231
|
function mergeOptionObject(existing, incoming) {
|
|
1211
1232
|
const hasExisting = isObject(existing);
|
|
@@ -1216,31 +1237,12 @@ function mergeOptionObject(existing, incoming) {
|
|
|
1216
1237
|
...hasIncoming ? incoming : {}
|
|
1217
1238
|
};
|
|
1218
1239
|
}
|
|
1219
|
-
/** Reject the reserved mode name (D4) wherever a mode set is declared or merged. */
|
|
1220
1240
|
function validateReservedModeNames(modeNames) {
|
|
1221
1241
|
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(", ")}`);
|
|
1222
1242
|
}
|
|
1223
1243
|
/**
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
* purpose: the D3-amendment fill (mapping-named modes default to their forced
|
|
1227
|
-
* schemes) must always run before totality is checked, so exposing fill and
|
|
1228
|
-
* validation separately would make "validate an unfilled map" representable.
|
|
1229
|
-
* An explicit entry survives the fill's spread and is still conflict-checked,
|
|
1230
|
-
* so a wrong value errors rather than being silently corrected. `undefined`
|
|
1231
|
-
* passes through untouched — the classification stays opt-in, and a theme with
|
|
1232
|
-
* only `systemPreference` emits no `color-scheme` (byte parity, G4).
|
|
1233
|
-
*
|
|
1234
|
-
* ALWAYS called with MERGED state — `theme.modes` unions across `addColorModes`
|
|
1235
|
-
* calls and `from()` composition, so a per-call view both misses invalidation
|
|
1236
|
-
* (a later mode declaration un-totals a carried classification) and invents
|
|
1237
|
-
* false rejections (a mapping naming a mode declared by an earlier call).
|
|
1238
|
-
* Run at both gates: `addColorModes` (fail fast) and `build()` (authoritative —
|
|
1239
|
-
* `from()` composition never passes through `addColorModes`; only build()'s
|
|
1240
|
-
* resolved map feeds emission and the manifest).
|
|
1241
|
-
*
|
|
1242
|
-
* Error tone mirrors `validateModeAliases`: `addColorModes:` prefix plus the
|
|
1243
|
-
* available names.
|
|
1244
|
+
* Fills mapping-named modes with their forced schemes, then validates. Takes
|
|
1245
|
+
* the MERGED mode set: a later declaration can un-total a carried map.
|
|
1244
1246
|
*/
|
|
1245
1247
|
function resolveColorModeOptions(modeNames, systemPreference, suppliedBrowserColorScheme) {
|
|
1246
1248
|
let browserColorScheme = suppliedBrowserColorScheme;
|
|
@@ -1278,12 +1280,8 @@ function resolveColorModeOptions(modeNames, systemPreference, suppliedBrowserCol
|
|
|
1278
1280
|
return browserColorScheme;
|
|
1279
1281
|
}
|
|
1280
1282
|
/**
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
* base chains must terminate (a cycle can never fill coverage). Runs at both
|
|
1284
|
-
* gates like the other mode options — `addColorModes` (fail fast) and
|
|
1285
|
-
* `build()` (authoritative: extend/from composition merges modes without
|
|
1286
|
-
* passing through `addColorModes`).
|
|
1283
|
+
* Validates against the MERGED mode set: composition merges modes without
|
|
1284
|
+
* passing through `addColorModes`, so `build()` runs this again.
|
|
1287
1285
|
*/
|
|
1288
1286
|
function validateModeBases(modeNames, modeBases) {
|
|
1289
1287
|
if (!modeBases) return;
|
|
@@ -1304,7 +1302,6 @@ function validateModeBases(modeNames, modeBases) {
|
|
|
1304
1302
|
}
|
|
1305
1303
|
}
|
|
1306
1304
|
}
|
|
1307
|
-
/** Validate all color entries, throwing on invalid values. */
|
|
1308
1305
|
function validateColors(colors) {
|
|
1309
1306
|
for (const [key, value] of Object.entries(colors)) if (isObject(value)) validateColors(value);
|
|
1310
1307
|
else if (!isValidCSSColor(value)) throw new Error(`addColors: '${String(value)}' is not a valid CSS <color> value for key '${key}'. Expected hex (#fff), rgb(), hsl(), oklch(), named color, transparent, or currentColor.`);
|
|
@@ -1341,12 +1338,8 @@ function copyState(state, nextTheme) {
|
|
|
1341
1338
|
return next;
|
|
1342
1339
|
}
|
|
1343
1340
|
/**
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
* never share a path spelling. A prefix relation between two tracked paths
|
|
1347
|
-
* is then a GENUINE structural divergence (one sibling authored a leaf
|
|
1348
|
-
* value where the other authored a nested branch) — the review-F1 case that
|
|
1349
|
-
* per-leaf value comparison alone cannot see.
|
|
1341
|
+
* Leaf-path flatten WITHOUT the `_` identity collapse, so a leaf and a branch
|
|
1342
|
+
* can never share a spelling and a prefix relation is a real divergence.
|
|
1350
1343
|
*/
|
|
1351
1344
|
function flattenLeafPathsExact(object, path) {
|
|
1352
1345
|
const result = {};
|
|
@@ -1359,12 +1352,8 @@ function flattenLeafPathsExact(object, path) {
|
|
|
1359
1352
|
return result;
|
|
1360
1353
|
}
|
|
1361
1354
|
/**
|
|
1362
|
-
*
|
|
1363
|
-
*
|
|
1364
|
-
* copies before folding: a consumed kit's built theme must never be
|
|
1365
|
-
* corrupted by composition, and prior builder state must never be shared by
|
|
1366
|
-
* reference — a one-level copy lets branching a builder cross-contaminate
|
|
1367
|
-
* both branches and lets build() outputs mutate after the fact.
|
|
1355
|
+
* `merge` adopts and MUTATES nested source objects, so every builder step deep
|
|
1356
|
+
* copies first: a consumed kit's theme and prior state must survive intact.
|
|
1368
1357
|
*/
|
|
1369
1358
|
function deepCopyPlain(value) {
|
|
1370
1359
|
if (Array.isArray(value)) return value.map(deepCopyPlain);
|
|
@@ -1376,7 +1365,6 @@ function deepCopyPlain(value) {
|
|
|
1376
1365
|
}
|
|
1377
1366
|
return value;
|
|
1378
1367
|
}
|
|
1379
|
-
/** Structural equality over plain theme data (records, arrays, primitives). */
|
|
1380
1368
|
function plainDataEqual(a, b) {
|
|
1381
1369
|
if (Object.is(a, b)) return true;
|
|
1382
1370
|
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => plainDataEqual(item, b[index]));
|
|
@@ -1389,10 +1377,8 @@ function plainDataEqual(a, b) {
|
|
|
1389
1377
|
return false;
|
|
1390
1378
|
}
|
|
1391
1379
|
/**
|
|
1392
|
-
* Re-
|
|
1393
|
-
* manifest
|
|
1394
|
-
* emitted-scale/contextual-var/manifest-v2 carry semantics: the manifest is
|
|
1395
|
-
* non-enumerable, so the ordinary key-copy loop never sees it (D6/D8).
|
|
1380
|
+
* Re-seeds emitted scales, contextual vars and the manifest-v2 carry from a
|
|
1381
|
+
* source manifest — non-enumerable, so key-copy loops never see it.
|
|
1396
1382
|
*/
|
|
1397
1383
|
function reseedStateFromManifest(state, manifest, mergeExtensionState = false) {
|
|
1398
1384
|
if (manifest?.emittedScales) for (const scale of manifest.emittedScales) state.emittedScales.add(scale);
|
|
@@ -1405,29 +1391,27 @@ function reseedStateFromManifest(state, manifest, mergeExtensionState = false) {
|
|
|
1405
1391
|
}
|
|
1406
1392
|
if (manifest?.contextualVars) for (const [scale, vars] of Object.entries(manifest.contextualVars)) {
|
|
1407
1393
|
const existing = mergeExtensionState ? state.contextualVars.get(scale) : void 0;
|
|
1408
|
-
state.contextualVars.set(scale, existing ? [
|
|
1409
|
-
}
|
|
1410
|
-
if (manifest)
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1394
|
+
state.contextualVars.set(scale, existing ? [.../* @__PURE__ */ new Set([...existing, ...vars])] : [...vars]);
|
|
1395
|
+
}
|
|
1396
|
+
if (manifest) {
|
|
1397
|
+
if (manifest.manifestVersion === 2) {
|
|
1398
|
+
state.carriedManifestV2 = {
|
|
1399
|
+
tokenDefinitions: manifest.tokenDefinitions,
|
|
1400
|
+
emittedScales: manifest.emittedScales,
|
|
1401
|
+
modeAliasDefinitions: manifest.modeAliasDefinitions,
|
|
1402
|
+
registrations: manifest.registrations,
|
|
1403
|
+
emitterVersion: manifest.emitterVersion,
|
|
1404
|
+
contractHash: manifest.contractHash,
|
|
1405
|
+
cssFragments: manifest.cssFragments
|
|
1406
|
+
};
|
|
1407
|
+
if (manifest.registrations) for (const [name, registration] of Object.entries(manifest.registrations)) {
|
|
1408
|
+
const existing = state.contextualVarRegistrations.get(name);
|
|
1409
|
+
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`);
|
|
1410
|
+
state.contextualVarRegistrations.set(name, registration);
|
|
1411
|
+
}
|
|
1412
|
+
} else state.hasLegacyManifestSource = true;
|
|
1413
|
+
}
|
|
1426
1414
|
}
|
|
1427
|
-
/**
|
|
1428
|
-
* ThemeScales — the final phase. Has addScale, extendScale, declareContextualVars, build.
|
|
1429
|
-
* Also allows addColors and addColorModes for augmentation.
|
|
1430
|
-
*/
|
|
1431
1415
|
var ThemeBuilder = class ThemeBuilder {
|
|
1432
1416
|
/** @internal */ _state;
|
|
1433
1417
|
constructor(state) {
|
|
@@ -1439,15 +1423,8 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1439
1423
|
return new ThemeBuilder(copyState(this._state, nextTheme));
|
|
1440
1424
|
}
|
|
1441
1425
|
/**
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
* through unchanged. Do not add a stage gate; do not flip the merge
|
|
1445
|
-
* direction. Those semantics ship under the new verb only.
|
|
1446
|
-
*
|
|
1447
|
-
* @deprecated Use `extend(source)` — the single extension verb on both
|
|
1448
|
-
* builders: the extended source seeds the base and later local calls win.
|
|
1449
|
-
* `from()` keeps these frozen source-wins semantics for at least one minor
|
|
1450
|
-
* release after `extend()` ships.
|
|
1426
|
+
* Source-WINS precedence over prior builder state, callable at any stage.
|
|
1427
|
+
* @deprecated Use `extend(source)` — the source seeds the base, local wins.
|
|
1451
1428
|
*/
|
|
1452
1429
|
from(builtTheme) {
|
|
1453
1430
|
const source = isLibraryBundle(builtTheme) ? builtTheme.theme ?? builtTheme.tokens ?? {} : builtTheme;
|
|
@@ -1462,19 +1439,8 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1462
1439
|
return next;
|
|
1463
1440
|
}
|
|
1464
1441
|
/**
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
* set — seeds the builder as the BASE, and local calls made after
|
|
1468
|
-
* `extend()` win on conflict (the mirror of `from()`'s source-wins).
|
|
1469
|
-
* Chainable and repeatable, but only before augmentation methods ("inherit
|
|
1470
|
-
* first, then extend" — enforced by the phantom builder stage). Accepts a
|
|
1471
|
-
* built theme or a library bundle, consuming the bundle's theme half
|
|
1472
|
-
* (`theme ?? tokens`, D9) and ignoring the rest.
|
|
1473
|
-
*
|
|
1474
|
-
* Sibling conflicts fail loud (D3/G4): a leaf path defined divergently by
|
|
1475
|
-
* two extended sources throws naming the path and both sources
|
|
1476
|
-
* positionally; equal values coalesce silently, and the consumer's own
|
|
1477
|
-
* post-extend `add*` calls override silently (NS-4).
|
|
1442
|
+
* Inherits a built theme or library bundle as the BASE; local calls after
|
|
1443
|
+
* `extend()` win, and leaves that two sources define divergently throw.
|
|
1478
1444
|
*/
|
|
1479
1445
|
extend(source) {
|
|
1480
1446
|
const themeHalf = isLibraryBundle(source) ? source.theme ?? source.tokens ?? {} : source;
|
|
@@ -1585,10 +1551,6 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1585
1551
|
const nextTheme = merge(deepCopyPlain(this._state.theme), { [key]: updateFn(this._state.theme[key]) });
|
|
1586
1552
|
return new ThemeBuilder(copyState(this._state, nextTheme));
|
|
1587
1553
|
}
|
|
1588
|
-
/**
|
|
1589
|
-
* Finalize the theme build.
|
|
1590
|
-
* Flattens nested data at the boundary — produces manifest and serialize().
|
|
1591
|
-
*/
|
|
1592
1554
|
build() {
|
|
1593
1555
|
const theme = deepCopyPlain(this._state.theme);
|
|
1594
1556
|
const emittedScales = this._state.emittedScales;
|
|
@@ -1721,14 +1683,11 @@ var ThemeBuilder = class ThemeBuilder {
|
|
|
1721
1683
|
function createTheme() {
|
|
1722
1684
|
return new ThemeBuilder(createState());
|
|
1723
1685
|
}
|
|
1724
|
-
/**
|
|
1686
|
+
/** Authored token reference: `{scale.key}` or `{scale.key/opacity}`. */
|
|
1725
1687
|
const TOKEN_REF_RE = /\{([^}]+)\}/g;
|
|
1726
1688
|
/**
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
* longer computed here — they resolve AFTER `resolveReferences` (see
|
|
1730
|
-
* {@link resolveModeValueMaps}), closing the G2 gap where mode-override
|
|
1731
|
-
* declarations bypassed the resolver.
|
|
1689
|
+
* The only flatten pass: nested theme → dot-path token map and variable
|
|
1690
|
+
* declarations. Mode value maps resolve later, after `resolveReferences`.
|
|
1732
1691
|
*/
|
|
1733
1692
|
function flattenTheme(theme, emittedScales, effectiveModes) {
|
|
1734
1693
|
const tokenMap = {};
|
|
@@ -1775,17 +1734,15 @@ function flattenTheme(theme, emittedScales, effectiveModes) {
|
|
|
1775
1734
|
tokenDefinitions
|
|
1776
1735
|
};
|
|
1777
1736
|
}
|
|
1778
|
-
/**
|
|
1737
|
+
/** Sorted-key rebuild — the serialized wire must not expose insertion order. */
|
|
1779
1738
|
function sortRecordByKey(record) {
|
|
1780
1739
|
const sorted = {};
|
|
1781
1740
|
for (const key of Object.keys(record).sort()) sorted[key] = record[key];
|
|
1782
1741
|
return sorted;
|
|
1783
1742
|
}
|
|
1784
1743
|
/**
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
* Empty when the theme has no modes or no colors, mirroring the original
|
|
1788
|
-
* flatten-pass guard.
|
|
1744
|
+
* The AUTHORED alias graph: mode → alias dot-path → authored color dot-path,
|
|
1745
|
+
* never a resolved value.
|
|
1789
1746
|
*/
|
|
1790
1747
|
function collectAuthoredModeAliases(theme) {
|
|
1791
1748
|
const authored = {};
|
|
@@ -1802,13 +1759,8 @@ function collectAuthoredModeAliases(theme) {
|
|
|
1802
1759
|
return authored;
|
|
1803
1760
|
}
|
|
1804
1761
|
/**
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
* covers them — otherwise the build fails listing the uncovered set.
|
|
1808
|
-
* Inherited modes are exempt (a kit's own asymmetry is pre-existing
|
|
1809
|
-
* behavior and must round-trip byte-identically). Returns the effective
|
|
1810
|
-
* alias map per mode (base-chain fills + authored, authored winning) and
|
|
1811
|
-
* one aggregated fill report per mode for the build diagnostic.
|
|
1762
|
+
* A CONSUMER-declared mode leaving inherited aliases uncovered must name a
|
|
1763
|
+
* base whose chain covers them; inherited modes are exempt.
|
|
1812
1764
|
*/
|
|
1813
1765
|
function resolveModeCoverage(authoredModeAliases, modeBases, inheritedModes, inheritedModeAliases) {
|
|
1814
1766
|
const effectiveModes = {};
|
|
@@ -1824,7 +1776,7 @@ function resolveModeCoverage(authoredModeAliases, modeBases, inheritedModes, inh
|
|
|
1824
1776
|
const stillUncovered = [];
|
|
1825
1777
|
for (const alias of uncovered) {
|
|
1826
1778
|
let cursor = base;
|
|
1827
|
-
const seen = new Set([modeName]);
|
|
1779
|
+
const seen = /* @__PURE__ */ new Set([modeName]);
|
|
1828
1780
|
let resolved;
|
|
1829
1781
|
while (cursor !== void 0 && !seen.has(cursor)) {
|
|
1830
1782
|
seen.add(cursor);
|
|
@@ -1854,12 +1806,8 @@ function resolveModeCoverage(authoredModeAliases, modeBases, inheritedModes, inh
|
|
|
1854
1806
|
};
|
|
1855
1807
|
}
|
|
1856
1808
|
/**
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
* the resolved declaration value; inlined → the resolved literal), never
|
|
1860
|
-
* the raw flattened string — the G2 closure for `[data-color-mode]` blocks.
|
|
1861
|
-
* Modes iterate in sorted name order and lines in sorted property-name
|
|
1862
|
-
* order, so mode declaration/insertion order is never observable (G3).
|
|
1809
|
+
* Per-mode declarations carry the RESOLVED value of their target color, never
|
|
1810
|
+
* the raw flattened string. Modes and lines sort, hiding authoring order.
|
|
1863
1811
|
*/
|
|
1864
1812
|
function resolveModeValueMaps(effectiveModes, variableMap, variables, tokenMap) {
|
|
1865
1813
|
const modeVariables = {};
|
|
@@ -1892,12 +1840,8 @@ function resolveModeValueMaps(effectiveModes, variableMap, variables, tokenMap)
|
|
|
1892
1840
|
};
|
|
1893
1841
|
}
|
|
1894
1842
|
/**
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
* build — unconditional on usage — naming the referencing token (with its
|
|
1898
|
-
* positional origin), the replacement call's scale, and the dropped keys.
|
|
1899
|
-
* Targets never defined ANYWHERE stay warn-and-literal (supported kit
|
|
1900
|
-
* pattern); a re-added key is simply known again and passes.
|
|
1843
|
+
* A reference to a target dropped by `addScale({ replace: true })` fails the
|
|
1844
|
+
* build. Targets never defined anywhere stay warn-and-literal.
|
|
1901
1845
|
*/
|
|
1902
1846
|
function assertNoDroppedReferences(tokenDefinitions, flatTokenMap, droppedTokenPaths, extendProvenance) {
|
|
1903
1847
|
if (droppedTokenPaths.size === 0) return;
|
|
@@ -1918,9 +1862,8 @@ function assertNoDroppedReferences(tokenDefinitions, flatTokenMap, droppedTokenP
|
|
|
1918
1862
|
}
|
|
1919
1863
|
if (violations.length > 0) throw new Error(`build: dangling token reference(s) after explicit scale replacement — ${violations.join("; ")}. Dropped keys: ${[...droppedNamed].sort().join(", ")}.`);
|
|
1920
1864
|
}
|
|
1921
|
-
/** A resolved value still carrying a `{…}` reference — never shippable
|
|
1865
|
+
/** A resolved value still carrying a `{…}` reference — never shippable. */
|
|
1922
1866
|
const UNRESOLVED_REF_RE = /\{[^}]+\}/;
|
|
1923
|
-
/** Custom-property names referenced through `var(--name)` in a value. */
|
|
1924
1867
|
const VAR_REF_NAME_RE = /var\(\s*(--[\w-]+)/g;
|
|
1925
1868
|
function varRefNames(value) {
|
|
1926
1869
|
const names = [];
|
|
@@ -1928,18 +1871,8 @@ function varRefNames(value) {
|
|
|
1928
1871
|
return names;
|
|
1929
1872
|
}
|
|
1930
1873
|
/**
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
* direct or TRANSITIVE never-defined target — is withheld from emitted CSS.
|
|
1934
|
-
*
|
|
1935
|
-
* Withholding must follow `var()` indirection too: flattenTheme synthesizes
|
|
1936
|
-
* the initial mode's aliases as `var(--target)` BEFORE resolution, so a
|
|
1937
|
-
* withheld target would otherwise leave an emitted alias pointing at a
|
|
1938
|
-
* declaration that exists nowhere. A declaration is dangling when a var()
|
|
1939
|
-
* target of its value was DECLARED in this build but withheld everywhere
|
|
1940
|
-
* the declaration can see (:root for root declarations; the same mode
|
|
1941
|
-
* block or :root for mode declarations); chains drop to a fixpoint. Var
|
|
1942
|
-
* names never declared here (breakpoints, contextual vars) are exempt.
|
|
1874
|
+
* Withholds any declaration whose value still contains `{…}`, then — to a
|
|
1875
|
+
* fixpoint — any whose `var()` target was withheld everywhere it can see.
|
|
1943
1876
|
*/
|
|
1944
1877
|
function omitUnresolvedDeclarations(variables, modeVariables) {
|
|
1945
1878
|
const omitted = [];
|
|
@@ -1988,11 +1921,8 @@ function omitUnresolvedDeclarations(variables, modeVariables) {
|
|
|
1988
1921
|
};
|
|
1989
1922
|
}
|
|
1990
1923
|
/**
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
* authored graph cannot be reconstructed from resolved CSS (D8). Uses
|
|
1994
|
-
* `matchAll` so the shared global {@link TOKEN_REF_RE} never carries a stale
|
|
1995
|
-
* `lastIndex` between callers.
|
|
1924
|
+
* Classifies a RAW token value into its authored form. `matchAll` keeps the
|
|
1925
|
+
* shared global {@link TOKEN_REF_RE} from carrying a stale `lastIndex`.
|
|
1996
1926
|
*/
|
|
1997
1927
|
function parseTokenDefinition(rawValue) {
|
|
1998
1928
|
const references = [];
|
|
@@ -2016,15 +1946,12 @@ function parseTokenDefinition(rawValue) {
|
|
|
2016
1946
|
}
|
|
2017
1947
|
/**
|
|
2018
1948
|
* Version of the CSS emitter that composed a manifest's fragments. Bump when
|
|
2019
|
-
*
|
|
2020
|
-
* regenerates dirty sections "under the pinned emitter version", and this is
|
|
2021
|
-
* that pin.
|
|
1949
|
+
* any emitted byte changes for the same authored input.
|
|
2022
1950
|
*/
|
|
2023
1951
|
const EMITTER_VERSION = 1;
|
|
2024
1952
|
/**
|
|
2025
|
-
*
|
|
2026
|
-
* order
|
|
2027
|
-
* arrays keep authored order (reference order is contractual).
|
|
1953
|
+
* Keys sort at every depth so the digest ignores insertion order; arrays keep
|
|
1954
|
+
* authored order, which is contractual.
|
|
2028
1955
|
*/
|
|
2029
1956
|
function canonicalize(value) {
|
|
2030
1957
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
@@ -2037,13 +1964,8 @@ function canonicalize(value) {
|
|
|
2037
1964
|
return value;
|
|
2038
1965
|
}
|
|
2039
1966
|
/**
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2042
|
-
* `node:crypto` import edge: built themes execute inside client bundles today
|
|
2043
|
-
* (e2e fixtures inline `createTheme(…).build()` at module init), and a static
|
|
2044
|
-
* builtin import is exactly what `./bootstrap` documents as forbidden in app
|
|
2045
|
-
* bundles. Non-Node runtimes use the pure fallback below, which produces
|
|
2046
|
-
* identical hex for identical input — determinism holds across environments.
|
|
1967
|
+
* Reached through `process.getBuiltinModule` so this module gains no static
|
|
1968
|
+
* `node:crypto` import edge — built themes run inside client bundles.
|
|
2047
1969
|
*/
|
|
2048
1970
|
function sha256Hex(input) {
|
|
2049
1971
|
const nodeCrypto = globalThis.process?.getBuiltinModule?.("node:crypto");
|
|
@@ -2121,11 +2043,8 @@ function rotr(x, n) {
|
|
|
2121
2043
|
return (x >>> n | x << 32 - n) >>> 0;
|
|
2122
2044
|
}
|
|
2123
2045
|
/**
|
|
2124
|
-
* UTF-8 bytes
|
|
2125
|
-
*
|
|
2126
|
-
* no Node globals and no WHATWG APIs. Iterating by code point handles
|
|
2127
|
-
* surrogate pairs; lone surrogates cannot reach this path because the input
|
|
2128
|
-
* is well-formed `JSON.stringify` output (ES2019 escapes them as `\uXXXX`).
|
|
2046
|
+
* UTF-8 bytes without `TextEncoder`: the Rust system-loader evaluates this
|
|
2047
|
+
* bundle in QuickJS, which provides ES built-ins only.
|
|
2129
2048
|
*/
|
|
2130
2049
|
function utf8Bytes(input) {
|
|
2131
2050
|
const bytes = [];
|
|
@@ -2139,9 +2058,8 @@ function utf8Bytes(input) {
|
|
|
2139
2058
|
return new Uint8Array(bytes);
|
|
2140
2059
|
}
|
|
2141
2060
|
/**
|
|
2142
|
-
* Pure SHA-256 (FIPS 180-4)
|
|
2143
|
-
*
|
|
2144
|
-
* is a content digest for composition-identity comparison only.
|
|
2061
|
+
* Pure SHA-256 (FIPS 180-4), the non-Node fallback for {@link sha256Hex}. Not
|
|
2062
|
+
* a security surface — the contract hash is a content digest only.
|
|
2145
2063
|
*/
|
|
2146
2064
|
function sha256HexFallback(input) {
|
|
2147
2065
|
const bytes = utf8Bytes(input);
|
|
@@ -2163,7 +2081,7 @@ function sha256HexFallback(input) {
|
|
|
2163
2081
|
528734635,
|
|
2164
2082
|
1541459225
|
|
2165
2083
|
]);
|
|
2166
|
-
const w = new Uint32Array(64);
|
|
2084
|
+
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
2167
2085
|
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
2168
2086
|
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
2169
2087
|
for (let i = 16; i < 64; i++) {
|
|
@@ -2206,20 +2124,13 @@ function sha256HexFallback(input) {
|
|
|
2206
2124
|
for (const word of state) hex += word.toString(16).padStart(8, "0");
|
|
2207
2125
|
return hex;
|
|
2208
2126
|
}
|
|
2209
|
-
/**
|
|
2210
|
-
* Stable digest over the canonical authored inputs. Identical authored input
|
|
2211
|
-
* ⇒ identical hash, across processes: keys sort at every depth and
|
|
2212
|
-
* `JSON.stringify` drops `undefined`-valued members deterministically.
|
|
2213
|
-
*/
|
|
2127
|
+
/** Identical authored input ⇒ identical hash, across processes. */
|
|
2214
2128
|
function computeContractHash(input) {
|
|
2215
2129
|
return sha256Hex(JSON.stringify(canonicalize(input)));
|
|
2216
2130
|
}
|
|
2217
2131
|
/**
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
* maps a bare contextual var value to. Rules emit in declaration order and only
|
|
2221
|
-
* for names that are genuinely declared contextual vars. Returns `''` when no
|
|
2222
|
-
* registration metadata was supplied (opt-in / byte-identical guarantee).
|
|
2132
|
+
* `@property` rules for registered contextual vars, emitted as `--${name}` —
|
|
2133
|
+
* the name the Rust resolver maps a bare contextual var to. `''` when none.
|
|
2223
2134
|
*/
|
|
2224
2135
|
function buildPropertyRegistrationCss(contextualVars, registrations) {
|
|
2225
2136
|
if (registrations.size === 0) return "";
|
|
@@ -2234,7 +2145,6 @@ function buildPropertyRegistrationCss(contextualVars, registrations) {
|
|
|
2234
2145
|
}
|
|
2235
2146
|
return blocks.join("\n");
|
|
2236
2147
|
}
|
|
2237
|
-
/** Build CSS variable blocks from flattened data. */
|
|
2238
2148
|
function buildVariableCss(rootVariables, breakpointVariables, modeVariables, systemEmission = {}) {
|
|
2239
2149
|
const { initialMode, systemPreference, browserColorScheme } = systemEmission;
|
|
2240
2150
|
const parts = [];
|