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