@animus-ui/system 0.1.0-next.23 → 0.1.0-next.30

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/dist/index.js CHANGED
@@ -1,237 +1,18 @@
1
- import { a as borderShorthand, c as numericOrStringScale, i as gridItemRatio, l as numericScale, n as size, o as createTransform, r as gridItem, s as createScale, t as percentageOrAbsolute, u as stringScale } from "./size-Dge_rsuz.js";
2
- import { createContext, createElement, forwardRef, useContext, useRef } from "react";
3
- //#region src/runtime/resolveClasses.ts
1
+ import { n as createComponent, t as createClassResolver } from "./createClassResolver-Dny76K15.js";
2
+ import { a as borderShorthand, c as numericOrStringScale, i as gridItemRatio, l as numericScale, n as size, o as createTransform, r as gridItem, s as createScale, t as percentageOrAbsolute, u as stringScale } from "./size-BjymBo7z.js";
3
+ import { createContext, createElement, forwardRef, useContext } from "react";
4
+ //#region src/utils/deepMerge.ts
4
5
  /**
5
- * CSS properties that accept unitless numeric values.
6
- * Bare numerics on properties NOT in this set receive `px`.
7
- */
8
- const UNITLESS_PROPERTIES = new Set([
9
- "animation-iteration-count",
10
- "border-image-outset",
11
- "border-image-slice",
12
- "border-image-width",
13
- "box-flex",
14
- "box-flex-group",
15
- "box-ordinal-group",
16
- "column-count",
17
- "columns",
18
- "flex",
19
- "flex-grow",
20
- "flex-positive",
21
- "flex-shrink",
22
- "flex-negative",
23
- "flex-order",
24
- "font-weight",
25
- "grid-area",
26
- "grid-column",
27
- "grid-column-end",
28
- "grid-column-span",
29
- "grid-column-start",
30
- "grid-row",
31
- "grid-row-end",
32
- "grid-row-span",
33
- "grid-row-start",
34
- "line-clamp",
35
- "line-height",
36
- "opacity",
37
- "order",
38
- "orphans",
39
- "tab-size",
40
- "widows",
41
- "z-index",
42
- "zoom",
43
- "fill-opacity",
44
- "flood-opacity",
45
- "stop-opacity",
46
- "stroke-dasharray",
47
- "stroke-dashoffset",
48
- "stroke-miterlimit",
49
- "stroke-opacity",
50
- "stroke-width"
51
- ]);
52
- /**
53
- * Apply unit fallback to a value for a given CSS property.
54
- */
55
- function applyUnitFallback(value, cssProperty) {
56
- if (typeof value === "number") {
57
- if (UNITLESS_PROPERTIES.has(cssProperty)) return String(value);
58
- return `${value}px`;
59
- }
60
- return String(value);
61
- }
62
- /**
63
- * Serialize a system prop value to a lookup key matching the Rust
64
- * css_generator's serialize_value_key output format.
65
- */
66
- function serializeValueKey(value) {
67
- if (typeof value === "number" || typeof value === "string") return String(value);
68
- if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.keys(value).sort().map((k) => `${k}:${value[k]}`).join("|");
69
- return String(value);
70
- }
71
- /**
72
- * Resolve a dynamic prop value through scale lookup → transform → unit fallback.
73
- */
74
- function resolveValue(value, dc) {
75
- const key = String(value);
76
- const scaleResolved = dc.scaleValues?.[key];
77
- if (scaleResolved != null) {
78
- const transformed = dc.transform ? dc.transform(scaleResolved) : scaleResolved;
79
- return String(transformed);
80
- }
81
- return applyUnitFallback(dc.transform ? dc.transform(value) : value, dc.varName);
82
- }
83
- /**
84
- * Resolve className parts from props, using extracted configuration.
85
- * This is the shared logic between createComponent and createClassResolver.
86
- */
87
- function resolveClasses(baseClassName, props, config, systemPropMap, dynamicPropConfig) {
88
- const classes = [baseClassName];
89
- let dynStyle;
90
- if (config.variants) for (const [prop, vc] of Object.entries(config.variants)) {
91
- const value = props[prop] ?? vc.default;
92
- if (value != null) classes.push(`${baseClassName}--${prop}-${value}`);
93
- }
94
- if (config.compounds) for (const compound of config.compounds) {
95
- let match = true;
96
- for (const [prop, expected] of Object.entries(compound.conditions)) {
97
- const current = props[prop] ?? config.variants?.[prop]?.default;
98
- if (Array.isArray(expected) ? !expected.includes(current) : current !== expected) {
99
- match = false;
100
- break;
101
- }
102
- }
103
- if (match) classes.push(compound.className);
104
- }
105
- if (config.states) {
106
- for (const state of config.states) if (props[state]) classes.push(`${baseClassName}--${state}`);
107
- }
108
- const systemPropNames = config.systemPropNames || [];
109
- if (systemPropNames.length > 0) {
110
- const { customPropMap, customDynamicConfig } = config;
111
- for (const propName of systemPropNames) {
112
- if (!(propName in props)) continue;
113
- const propValue = props[propName];
114
- if (propValue == null) continue;
115
- const key = serializeValueKey(propValue);
116
- const cls = customPropMap?.[propName]?.[key] ?? systemPropMap?.[propName]?.[key];
117
- if (cls) classes.push(cls);
118
- else {
119
- const dc = customDynamicConfig?.[propName] ?? dynamicPropConfig?.[propName];
120
- if (dc) {
121
- if (!dynStyle) dynStyle = {};
122
- if (typeof propValue === "object" && propValue !== null && !Array.isArray(propValue)) for (const [bp, bpVal] of Object.entries(propValue)) {
123
- if (bpVal == null) continue;
124
- if (bp === "_") {
125
- classes.push(dc.slotClass);
126
- const finalVal = resolveValue(bpVal, dc);
127
- dynStyle[dc.varName] = finalVal;
128
- } else {
129
- classes.push(`${dc.slotClass}-${bp}`);
130
- const varName = `${dc.varName}-${bp}`;
131
- const finalVal = resolveValue(bpVal, dc);
132
- dynStyle[varName] = finalVal;
133
- }
134
- }
135
- else {
136
- classes.push(dc.slotClass);
137
- const finalVal = resolveValue(propValue, dc);
138
- dynStyle[dc.varName] = finalVal;
139
- }
140
- }
141
- }
142
- }
143
- }
144
- return {
145
- classes,
146
- dynamicStyle: dynStyle
147
- };
148
- }
149
- //#endregion
150
- //#region src/runtime/index.ts
151
- /**
152
- * Create a lightweight component that applies extracted CSS class names.
153
- * Replaces Emotion's styled() for extracted components.
154
- *
155
- * The element parameter accepts either an HTML tag string (e.g. 'button') or
156
- * a React component reference (e.g. NextLink). When a component reference is
157
- * used, prop forwarding skips the HTML-attribute validity check — all
158
- * non-filtered props are forwarded to the component.
159
- *
160
- * The optional systemPropMap parameter provides the shared prop→value→className
161
- * lookup table, served as a virtual module by the Vite plugin.
162
- *
163
- * The optional dynamicPropConfig parameter provides CSS variable fallback
164
- * metadata for props with detected dynamic usage.
165
- */
166
- function createComponent(element, className, config, systemPropMap, dynamicPropConfig) {
167
- const variantProps = config.variants ? Object.keys(config.variants) : [];
168
- const stateProps = config.states || [];
169
- const systemPropNames = config.systemPropNames || [];
170
- const filterProps = new Set([
171
- "as",
172
- ...variantProps,
173
- ...stateProps,
174
- ...systemPropNames
175
- ]);
176
- const Component = forwardRef((props, ref) => {
177
- const renderElement = props.as || element;
178
- const isComponentElement = typeof renderElement !== "string";
179
- const prevDynKey = useRef("");
180
- const prevDynStyle = useRef(null);
181
- const { classes, dynamicStyle } = resolveClasses(className, props, config, systemPropMap, dynamicPropConfig);
182
- if (props.className) classes.push(props.className);
183
- const domProps = {
184
- ref,
185
- className: classes.join(" ")
186
- };
187
- for (const [key, value] of Object.entries(props)) {
188
- if (key === "className") continue;
189
- if (filterProps.has(key)) continue;
190
- if (!isComponentElement) {}
191
- domProps[key] = value;
192
- }
193
- if (dynamicStyle) {
194
- const dynKey = Object.entries(dynamicStyle).map(([k, v]) => `${k}:${v}`).join("|");
195
- if (dynKey !== prevDynKey.current) {
196
- prevDynKey.current = dynKey;
197
- prevDynStyle.current = dynamicStyle;
198
- }
199
- domProps.style = props.style ? {
200
- ...props.style,
201
- ...prevDynStyle.current
202
- } : prevDynStyle.current;
203
- }
204
- return createElement(renderElement, domProps);
205
- });
206
- Component.displayName = className;
207
- Component.__variantKeys = new Set(variantProps);
208
- return Object.assign(Component, { extend: () => {
209
- throw new Error(`Cannot extend extracted component "${className}" at runtime. Extensions must be authored in source code using the builder API (e.g. import the original component and call .extend() there) so the extraction pipeline can resolve them at build time.`);
210
- } });
211
- }
212
- //#endregion
213
- //#region src/runtime/createClassResolver.ts
214
- /**
215
- * createClassResolver — framework-agnostic className resolution.
216
- *
217
- * Produced by .asClass() terminal. Same resolution logic as createComponent
218
- * (variants, states, compounds, system props) but returns a className string
219
- * instead of a React element.
6
+ * Deep merge utility replaces lodash.merge for variant accumulation.
220
7
  */
221
- function createClassResolver(className, config, systemPropMap, dynamicPropConfig) {
222
- return (props) => {
223
- const { classes } = resolveClasses(className, props || {}, config, systemPropMap, dynamicPropConfig);
224
- return classes.join(" ");
225
- };
226
- }
227
- //#endregion
228
- //#region src/AnimusExtended.ts
229
- function deepMerge$1(target, source) {
8
+ function deepMerge(target, source) {
230
9
  const result = { ...target };
231
- for (const key of Object.keys(source)) if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) result[key] = deepMerge$1(target[key], source[key]);
10
+ for (const key of Object.keys(source)) if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) result[key] = deepMerge(target[key], source[key]);
232
11
  else result[key] = source[key];
233
12
  return result;
234
13
  }
14
+ //#endregion
15
+ //#region src/AnimusExtended.ts
235
16
  var AnimusExtendedWithAll = class {
236
17
  propRegistry = {};
237
18
  groupRegistry = {};
@@ -267,9 +48,6 @@ var AnimusExtendedWithAll = class {
267
48
  asClass() {
268
49
  return createClassResolver("", this._buildComponentConfig());
269
50
  }
270
- build() {
271
- return Object.assign((() => ({})), { extend: this.extend.bind(this) });
272
- }
273
51
  _buildComponentConfig() {
274
52
  const variantConfig = {};
275
53
  for (const [key, vc] of Object.entries(this.variants)) {
@@ -290,12 +68,12 @@ var AnimusExtendedWithAll = class {
290
68
  };
291
69
  var AnimusExtendedWithSystem = class extends AnimusExtendedWithAll {
292
70
  props(config) {
293
- return new AnimusExtendedWithAll(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, this.activeGroups, deepMerge$1({}, config), this.compounds);
71
+ return new AnimusExtendedWithAll(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, this.activeGroups, deepMerge({}, config), this.compounds);
294
72
  }
295
73
  };
296
74
  var AnimusExtendedWithStates = class extends AnimusExtendedWithSystem {
297
- groups(config) {
298
- return new AnimusExtendedWithSystem(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, deepMerge$1(this.activeGroups, config), this.custom, this.compounds);
75
+ system(config) {
76
+ return new AnimusExtendedWithSystem(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, deepMerge(this.activeGroups, config), this.custom, this.compounds);
299
77
  }
300
78
  };
301
79
  var AnimusExtendedWithCompounds = class AnimusExtendedWithCompounds extends AnimusExtendedWithStates {
@@ -306,7 +84,7 @@ var AnimusExtendedWithCompounds = class AnimusExtendedWithCompounds extends Anim
306
84
  }]);
307
85
  }
308
86
  states(config) {
309
- return new AnimusExtendedWithStates(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, deepMerge$1(this.statesConfig, config), this.activeGroups, this.custom, this.compounds);
87
+ return new AnimusExtendedWithStates(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, deepMerge(this.statesConfig, config), this.activeGroups, this.custom, this.compounds);
310
88
  }
311
89
  };
312
90
  var AnimusExtendedWithVariants = class AnimusExtendedWithVariants extends AnimusExtendedWithCompounds {
@@ -318,31 +96,22 @@ var AnimusExtendedWithVariants = class AnimusExtendedWithVariants extends Animus
318
96
  }
319
97
  variant(options) {
320
98
  const prop = options.prop || "variant";
321
- return new AnimusExtendedWithVariants(this.propRegistry, this.groupRegistry, this.baseStyles, deepMerge$1(this.variants, { [prop]: options }), this.statesConfig, this.activeGroups, this.custom, this.compounds);
99
+ return new AnimusExtendedWithVariants(this.propRegistry, this.groupRegistry, this.baseStyles, deepMerge(this.variants, { [prop]: options }), this.statesConfig, this.activeGroups, this.custom, this.compounds);
322
100
  }
323
101
  };
324
102
  var AnimusExtendedWithBase = class extends AnimusExtendedWithVariants {
325
103
  variant(options) {
326
104
  const prop = options.prop || "variant";
327
- return new AnimusExtendedWithVariants(this.propRegistry, this.groupRegistry, this.baseStyles, deepMerge$1(this.variants, { [prop]: options }), this.statesConfig, this.activeGroups, this.custom, this.compounds);
105
+ return new AnimusExtendedWithVariants(this.propRegistry, this.groupRegistry, this.baseStyles, deepMerge(this.variants, { [prop]: options }), this.statesConfig, this.activeGroups, this.custom, this.compounds);
328
106
  }
329
107
  };
330
108
  var AnimusExtended = class extends AnimusExtendedWithBase {
331
109
  styles(config) {
332
- return new AnimusExtendedWithBase(this.propRegistry, this.groupRegistry, deepMerge$1(this.baseStyles, config), this.variants, this.statesConfig, this.activeGroups, this.custom, this.compounds);
110
+ return new AnimusExtendedWithBase(this.propRegistry, this.groupRegistry, deepMerge(this.baseStyles, config), this.variants, this.statesConfig, this.activeGroups, this.custom, this.compounds);
333
111
  }
334
112
  };
335
113
  //#endregion
336
114
  //#region src/Animus.ts
337
- /**
338
- * Deep merge utility — replaces lodash.merge for variant accumulation.
339
- */
340
- function deepMerge(target, source) {
341
- const result = { ...target };
342
- for (const key of Object.keys(source)) if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) result[key] = deepMerge(target[key], source[key]);
343
- else result[key] = source[key];
344
- return result;
345
- }
346
115
  var AnimusWithAll = class {
347
116
  propRegistry = {};
348
117
  groupRegistry = {};
@@ -378,9 +147,6 @@ var AnimusWithAll = class {
378
147
  asClass() {
379
148
  return createClassResolver("", this._buildComponentConfig());
380
149
  }
381
- build() {
382
- return Object.assign((() => ({})), { extend: this.extend.bind(this) });
383
- }
384
150
  _buildComponentConfig() {
385
151
  const variantConfig = {};
386
152
  for (const [key, vc] of Object.entries(this.variants)) {
@@ -411,7 +177,7 @@ var AnimusWithStates = class extends AnimusWithSystem {
411
177
  constructor(props, groups, base, variants, states, compounds = []) {
412
178
  super(props, groups, base, variants, states, {}, compounds);
413
179
  }
414
- groups(config) {
180
+ system(config) {
415
181
  return new AnimusWithSystem(this.propRegistry, this.groupRegistry, this.baseStyles, this.variants, this.statesConfig, config, this.compounds);
416
182
  }
417
183
  };
@@ -523,64 +289,78 @@ function compose(slots, options) {
523
289
  return result;
524
290
  }
525
291
  //#endregion
526
- //#region src/PropertyBuilder.ts
527
- var PropertyBuilder = class PropertyBuilder {
528
- #props;
529
- #groups;
530
- constructor(props, groups) {
531
- this.#props = props || {};
532
- this.#groups = groups || {};
533
- }
534
- addGroup(name, config) {
535
- const newGroup = { [name]: Object.keys(config) };
536
- return new PropertyBuilder({
537
- ...this.#props,
538
- ...config
539
- }, {
540
- ...this.#groups,
541
- ...newGroup
542
- });
543
- }
544
- build() {
545
- return {
546
- propRegistry: this.#props,
547
- groupRegistry: this.#groups
548
- };
549
- }
550
- };
551
- //#endregion
552
292
  //#region src/SystemBuilder.ts
553
293
  var SystemBuilder = class SystemBuilder {
554
294
  #propRegistry;
555
295
  #groupRegistry;
556
- #globalStyles;
557
- constructor(propRegistry, groupRegistry, globalStyles) {
296
+ constructor(propRegistry, groupRegistry) {
558
297
  this.#propRegistry = propRegistry || {};
559
298
  this.#groupRegistry = groupRegistry || {};
560
- this.#globalStyles = globalStyles;
561
299
  }
562
- withProperties(cb) {
563
- const result = cb(new PropertyBuilder());
564
- return new SystemBuilder(result.propRegistry, result.groupRegistry, this.#globalStyles);
300
+ addGroup(name, config) {
301
+ 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.`);
302
+ for (const key of Object.keys(config)) if (key in this.#propRegistry) {
303
+ const existing = this.#propRegistry[key];
304
+ const incoming = config[key];
305
+ if (existing.property !== incoming.property || existing.scale !== incoming.scale || existing.transform !== incoming.transform || existing.negative !== incoming.negative) throw new Error(`Prop "${key}" already registered with a different definition. Existing: property="${existing.property}", scale="${String(existing.scale)}". Incoming: property="${incoming.property}", scale="${String(incoming.scale)}".`);
306
+ }
307
+ const nextProps = {
308
+ ...this.#propRegistry,
309
+ ...config
310
+ };
311
+ const newGroup = { [name]: Object.keys(config) };
312
+ return new SystemBuilder(nextProps, {
313
+ ...this.#groupRegistry,
314
+ ...newGroup
315
+ });
316
+ }
317
+ addProps(config) {
318
+ 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.`);
319
+ for (const key of Object.keys(config)) if (key in this.#propRegistry) {
320
+ const existing = this.#propRegistry[key];
321
+ const incoming = config[key];
322
+ if (existing.property !== incoming.property || existing.scale !== incoming.scale || existing.transform !== incoming.transform || existing.negative !== incoming.negative) throw new Error(`Prop "${key}" already registered with a different definition.`);
323
+ }
324
+ return new SystemBuilder({
325
+ ...this.#propRegistry,
326
+ ...config
327
+ }, this.#groupRegistry);
565
328
  }
566
- withGlobalStyles(styles) {
567
- return new SystemBuilder(this.#propRegistry, this.#groupRegistry, styles);
329
+ includes(_systems) {
330
+ return this;
568
331
  }
569
332
  build() {
570
333
  const animus = new Animus(this.#propRegistry, this.#groupRegistry);
571
- const globalStyles = this.#globalStyles;
572
- return Object.assign(animus, { serialize: () => {
573
- return serializeInstance(this.#propRegistry, this.#groupRegistry, globalStyles);
334
+ const propRegistry = this.#propRegistry;
335
+ const groupRegistry = this.#groupRegistry;
336
+ const system = Object.assign(animus, { toConfig: () => {
337
+ return serializeInstance(propRegistry, groupRegistry);
574
338
  } });
339
+ const createGlobalStyles = (styles) => {
340
+ return {
341
+ __brand: "GlobalStyleBlock",
342
+ styles,
343
+ serialize(propConfig, transforms) {
344
+ return styles;
345
+ }
346
+ };
347
+ };
348
+ return {
349
+ system,
350
+ createGlobalStyles
351
+ };
575
352
  }
576
353
  };
577
- function serializeInstance(propRegistry, groupRegistry, globalStyles) {
354
+ function serializeInstance(propRegistry, groupRegistry) {
578
355
  const serialized = {};
579
356
  const transforms = {};
580
357
  for (const [propName, entry] of Object.entries(propRegistry)) {
581
358
  const s = { property: entry.property };
582
359
  if (entry.properties && entry.properties.length > 0) s.properties = [...entry.properties];
583
- if (typeof entry.scale === "string") s.scale = entry.scale;
360
+ const scale = entry.scale;
361
+ if (typeof scale === "string") s.scale = scale;
362
+ else if (scale && typeof scale === "object") s.scale = scale;
363
+ if (entry.negative) s.negative = true;
584
364
  if (entry.transform) {
585
365
  const fn = entry.transform;
586
366
  const name = fn.transformName ?? fn.name;
@@ -592,13 +372,11 @@ function serializeInstance(propRegistry, groupRegistry, globalStyles) {
592
372
  if (entry.currentVar) s.currentVar = entry.currentVar;
593
373
  serialized[propName] = s;
594
374
  }
595
- const result = {
375
+ return {
596
376
  propConfig: JSON.stringify(serialized),
597
377
  groupRegistry: JSON.stringify(groupRegistry),
598
378
  transforms
599
379
  };
600
- if (globalStyles) result.globalStyles = globalStyles;
601
- return result;
602
380
  }
603
381
  function createSystem() {
604
382
  return new SystemBuilder();
@@ -621,57 +399,46 @@ function merge(target, ...sources) {
621
399
  }
622
400
  return target;
623
401
  }
624
- function mapValues(obj, fn) {
402
+ /**
403
+ * Resolve a dot-path string against a nested object.
404
+ * walkDotPath({ gray: { 50: '#fafafa' } }, 'gray.50') → '#fafafa'
405
+ * The `_` identity key is handled: 'primary' resolves to obj.primary._ if obj.primary is an object with _.
406
+ */
407
+ function walkDotPath(obj, path) {
408
+ const parts = path.split(".");
409
+ let current = obj;
410
+ for (const part of parts) {
411
+ if (!isObject(current)) return void 0;
412
+ current = current[part];
413
+ }
414
+ return current;
415
+ }
416
+ /**
417
+ * Flatten a nested object into a flat Record with dot-path keys.
418
+ * The `_` key is an identity marker — it produces the parent key without suffix.
419
+ * { gray: { 50: '#fafafa' } } → { 'gray.50': '#fafafa' }
420
+ * { primary: { _: 'ember', hover: 'x' } } → { 'primary': 'ember', 'primary.hover': 'x' }
421
+ * CSS variable names use dash-join, computed at the serialization boundary (not here).
422
+ */
423
+ function flattenToDotPaths(object, path) {
625
424
  const result = {};
626
- for (const key of Object.keys(obj)) result[key] = fn(obj[key], key, obj);
425
+ for (const key of Object.keys(object)) {
426
+ const nextKey = path ? key === "_" ? path : `${path}.${key}` : key;
427
+ const current = object[key];
428
+ if (isObject(current)) Object.assign(result, flattenToDotPaths(current, nextKey));
429
+ else result[nextKey] = current;
430
+ }
627
431
  return result;
628
432
  }
629
- //#endregion
630
- //#region src/theme/flattenScale.ts
631
- function flattenScale(object, path) {
632
- return Object.keys(object).reduce((carry, key) => {
633
- const nextKey = path ? `${path}${key === "_" ? "" : `-${key}`}` : key;
634
- const current = object[key];
635
- if (isObject(current)) return {
636
- ...carry,
637
- ...flattenScale(current, nextKey)
638
- };
639
- return {
640
- ...carry,
641
- [nextKey]: object[key]
642
- };
643
- }, {});
433
+ /**
434
+ * Convert a dot-path key to a dash-join key for CSS variable naming.
435
+ * 'gray.50' 'gray-50'
436
+ * 'primary.hover' 'primary-hover'
437
+ */
438
+ function dotToDash(dotPath) {
439
+ return dotPath.replace(/\./g, "-");
644
440
  }
645
441
  //#endregion
646
- //#region src/theme/serializeTokens.ts
647
- const templateBreakpoints = (value, alias, theme) => {
648
- if (isObject(value)) {
649
- const { _, ...rest } = value;
650
- const css = { [alias]: _ };
651
- if (theme) {
652
- const breakpoints = theme.breakpoints;
653
- Object.keys(breakpoints).forEach((key) => {
654
- if (rest[key]) css[breakpoints[key]] = { [alias]: rest[key] };
655
- });
656
- }
657
- return css;
658
- }
659
- return { [alias]: value };
660
- };
661
- const serializeTokens = (tokens, prefix, theme) => {
662
- const tokenReferences = {};
663
- const tokenVariables = {};
664
- Object.keys(tokens).forEach((key) => {
665
- const varName = `--${prefix}-${key.replace("$", "")}`;
666
- tokenReferences[key] = `var(${varName})`;
667
- merge(tokenVariables, templateBreakpoints(tokens[key], varName, theme));
668
- });
669
- return {
670
- tokens: tokenReferences,
671
- variables: tokenVariables
672
- };
673
- };
674
- //#endregion
675
442
  //#region src/theme/createTheme.ts
676
443
  const CSS_NAMED_COLORS = new Set([
677
444
  "aliceblue",
@@ -846,13 +613,20 @@ function isValidCSSColor(value) {
846
613
  if (CSS_NAMED_COLORS.has(v.toLowerCase())) return true;
847
614
  return false;
848
615
  }
849
- /** Validate that mode aliases reference existing color keys. */
850
- function validateModeAliases(modeName, aliases, colorSet, availableColors, prefix) {
616
+ /**
617
+ * Validate that mode aliases reference existing color keys via dot-path traversal.
618
+ * Aliases are dot-path strings like 'gray.50' that must resolve in the nested color structure.
619
+ */
620
+ function validateModeAliases(modeName, aliases, nestedColors, flatColorKeys, prefix) {
851
621
  for (const [key, value] of Object.entries(aliases)) {
852
- const aliasPath = prefix ? `${prefix}-${key}` : key;
853
- if (typeof value === "string") {
854
- if (!colorSet.has(value)) throw new Error(`addColorModes: mode '${modeName}' references unknown color '${value}' for alias '${aliasPath}'. Available colors: ${availableColors.slice(0, 10).join(", ")}${availableColors.length > 10 ? ", ..." : ""}`);
855
- } else if (isObject(value)) validateModeAliases(modeName, value, colorSet, availableColors, aliasPath);
622
+ const aliasPath = prefix ? `${prefix}.${key}` : key;
623
+ if (key === "_") {
624
+ if (typeof value === "string") {
625
+ if (walkDotPath(nestedColors, value) === void 0) throw new Error(`addColorModes: mode '${modeName}' references unknown color '${value}' for alias '${prefix || key}'. Available colors: ${flatColorKeys.slice(0, 10).join(", ")}${flatColorKeys.length > 10 ? ", ..." : ""}`);
626
+ } else if (isObject(value)) validateModeAliases(modeName, value, nestedColors, flatColorKeys, prefix);
627
+ } else if (typeof value === "string") {
628
+ if (walkDotPath(nestedColors, value) === void 0) throw new Error(`addColorModes: mode '${modeName}' references unknown color '${value}' for alias '${aliasPath}'. Available colors: ${flatColorKeys.slice(0, 10).join(", ")}${flatColorKeys.length > 10 ? ", ..." : ""}`);
629
+ } else if (isObject(value)) validateModeAliases(modeName, value, nestedColors, flatColorKeys, aliasPath);
856
630
  }
857
631
  }
858
632
  /** Validate all color entries, throwing on invalid values. */
@@ -860,270 +634,274 @@ function validateColors(colors) {
860
634
  for (const [key, value] of Object.entries(colors)) if (isObject(value)) validateColors(value);
861
635
  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.`);
862
636
  }
637
+ function createState(theme) {
638
+ return {
639
+ theme: theme || { breakpoints: {} },
640
+ emittedScales: /* @__PURE__ */ new Set(),
641
+ contextualVars: /* @__PURE__ */ new Map()
642
+ };
643
+ }
644
+ function copyState(state, nextTheme) {
645
+ const next = {
646
+ theme: nextTheme,
647
+ emittedScales: new Set(state.emittedScales),
648
+ contextualVars: /* @__PURE__ */ new Map()
649
+ };
650
+ for (const [scale, vars] of state.contextualVars) next.contextualVars.set(scale, [...vars]);
651
+ return next;
652
+ }
653
+ /**
654
+ * ThemeScales — the final phase. Has addScale, extendScale, declareContextualVars, build.
655
+ * Also allows addColors and addColorModes for augmentation.
656
+ */
863
657
  var ThemeBuilder = class ThemeBuilder {
864
- #theme = {};
865
- #emittedScales = /* @__PURE__ */ new Set();
866
- #contextualVars = /* @__PURE__ */ new Map();
867
- constructor(baseTheme) {
868
- if (baseTheme.breakpoints) {
869
- for (const [key, value] of Object.entries(baseTheme.breakpoints)) if (typeof value !== "number" || value < 0) throw new Error(`createTheme: breakpoint '${key}' must be a non-negative number, got ${JSON.stringify(value)}`);
658
+ /** @internal */ _state;
659
+ constructor(state) {
660
+ this._state = state;
661
+ }
662
+ addBreakpoints(breakpoints) {
663
+ for (const [key, value] of Object.entries(breakpoints)) if (typeof value !== "number" || value < 0) throw new Error(`addBreakpoints: breakpoint '${key}' must be a non-negative number, got ${JSON.stringify(value)}`);
664
+ const nextTheme = merge({}, this._state.theme, { breakpoints });
665
+ return new ThemeBuilder(copyState(this._state, nextTheme));
666
+ }
667
+ from(builtTheme) {
668
+ const raw = {};
669
+ for (const key of Object.keys(builtTheme)) {
670
+ const val = builtTheme[key];
671
+ if (typeof val !== "function") raw[key] = val;
870
672
  }
871
- this.#theme = baseTheme;
872
- }
873
- /** Create a new builder checkpoint, carrying forward emittedScales and contextualVars state. */
874
- #checkpoint(nextTheme) {
875
- const next = new ThemeBuilder(nextTheme);
876
- for (const s of this.#emittedScales) next.#emittedScales.add(s);
877
- for (const [scale, vars] of this.#contextualVars) next.#contextualVars.set(scale, [...vars]);
673
+ const nextTheme = merge({}, this._state.theme, raw);
674
+ const next = new ThemeBuilder(copyState(this._state, nextTheme));
675
+ const manifest = builtTheme.manifest;
676
+ if (manifest?.variableMap) for (const tokenPath of Object.keys(manifest.variableMap)) {
677
+ const scale = tokenPath.split(".")[0];
678
+ next._state.emittedScales.add(scale === "colors" ? "colors" : scale);
679
+ }
680
+ if (manifest?.contextualVars) for (const [scale, vars] of Object.entries(manifest.contextualVars)) next._state.contextualVars.set(scale, [...vars]);
878
681
  return next;
879
682
  }
880
- /**
881
- * @param colors A map of color tokens. Immediately converted to CSS variables `--color-${key}`.
882
- * @example .addColors({ navy: 'navy', hyper: 'purple' })
883
- */
884
683
  addColors(colors) {
885
684
  validateColors(colors);
886
- const flatColors = flattenScale(colors);
887
- const { variables, tokens } = serializeTokens(flatColors, "color", this.#theme);
888
- const nextTheme = merge({}, this.#theme, {
889
- colors: tokens,
890
- _variables: { root: variables },
891
- _tokens: { colors: flatColors }
892
- });
893
- const next = this.#checkpoint(nextTheme);
894
- next.#emittedScales.add("colors");
685
+ const nextTheme = merge({}, this._state.theme, { colors });
686
+ const next = new ThemeBuilder(copyState(this._state, nextTheme));
687
+ next._state.emittedScales.add("colors");
895
688
  return next;
896
689
  }
897
- /**
898
- * @param initialMode Default color mode key.
899
- * @param modeConfig Map of color modes with semantic aliases pointing to palette keys.
900
- * @example .addColorModes('dark', { dark: { primary: 'ember' }, light: { primary: 'void' } })
901
- */
902
690
  addColorModes(initialMode, modeConfig) {
903
- const availableColors = this.#theme._tokens?.colors ? Object.keys(this.#theme._tokens.colors) : Object.keys(this.#theme.colors || {});
904
- const colorSet = new Set(availableColors);
905
- for (const [modeName, modeAliases] of Object.entries(modeConfig)) validateModeAliases(modeName, modeAliases, colorSet, availableColors, "");
906
- const modes = mapValues(modeConfig, (mode) => flattenScale(mode));
907
- const { tokens: colors, variables } = serializeTokens(mapValues(merge({}, this.#theme.modes?.[initialMode], modes[initialMode]), (color) => this.#theme.colors[color]), "color", this.#theme);
908
- const getColorValue = (color) => this.#theme._tokens?.colors?.[color];
909
- const nextTheme = merge({}, this.#theme, {
910
- colors,
911
- modes,
912
- mode: initialMode,
913
- _getColorValue: getColorValue,
914
- _variables: { mode: variables },
915
- _tokens: { modes: mapValues(modes, (mode) => mapValues(mode, getColorValue)) }
691
+ const nestedColors = this._state.theme.colors || {};
692
+ const flatColors = flattenToDotPaths(nestedColors);
693
+ const flatColorKeys = Object.keys(flatColors);
694
+ for (const [modeName, modeAliases] of Object.entries(modeConfig)) validateModeAliases(modeName, modeAliases, nestedColors, flatColorKeys, "");
695
+ const nextTheme = merge({}, this._state.theme, {
696
+ modes: modeConfig,
697
+ mode: initialMode
916
698
  });
917
- return this.#checkpoint(nextTheme);
699
+ return new ThemeBuilder(copyState(this._state, nextTheme));
918
700
  }
919
- /**
920
- * Add a named scale to the theme.
921
- *
922
- * @param config.name - Scale name (e.g. 'space', 'sizes')
923
- * @param config.values - Scale value map
924
- * @param config.emit - When true, generates CSS variables (default: false)
925
- *
926
- * @example
927
- * .addScale({ name: 'space', values: { 0: '0', 8: '0.5rem', 16: '1rem' } })
928
- * .addScale({ name: 'sizes', emit: true, values: { navHeight: '48px' } })
929
- */
930
701
  addScale(config) {
931
702
  const { name, values, emit } = config;
932
- const flattened = flattenScale(values);
933
- let nextTheme;
934
- if (emit) {
935
- const { variables, tokens } = serializeTokens(flattened, name, this.#theme);
936
- nextTheme = merge({}, this.#theme, {
937
- [name]: tokens,
938
- _variables: { [name]: variables },
939
- _tokens: { [name]: flattened }
940
- });
941
- } else nextTheme = merge({}, this.#theme, { [name]: flattened });
942
- const next = this.#checkpoint(nextTheme);
943
- if (emit) next.#emittedScales.add(name);
703
+ const nextTheme = merge({}, this._state.theme, { [name]: values });
704
+ const next = new ThemeBuilder(copyState(this._state, nextTheme));
705
+ if (emit) next._state.emittedScales.add(name);
944
706
  return next;
945
707
  }
946
- /**
947
- * Declare contextual CSS variables as phantom members of their scales.
948
- * These names appear in the scale's type but resolve to CSS custom properties
949
- * (`--{name}`) instead of token values. They cascade through the DOM like `currentColor`.
950
- *
951
- * @param vars - Object mapping scale names to arrays of contextual var names.
952
- *
953
- * @example
954
- * .addContextualVars({
955
- * colors: ['current-bg', 'current-border-color'],
956
- * })
957
- */
958
- addContextualVars(vars) {
959
- for (const scale of Object.keys(vars)) if (!(scale in this.#theme)) throw new Error(`addContextualVars: scale '${scale}' not found — call addColors or addScale first`);
960
- const next = this.#checkpoint(this.#theme);
708
+ declareContextualVars(vars) {
709
+ for (const scale of Object.keys(vars)) if (!(scale in this._state.theme)) throw new Error(`declareContextualVars: scale '${scale}' not found — call addColors or addScale first`);
710
+ const next = new ThemeBuilder(copyState(this._state, this._state.theme));
961
711
  for (const [scale, names] of Object.entries(vars)) {
962
- const existing = next.#contextualVars.get(scale) || [];
963
- next.#contextualVars.set(scale, [...existing, ...names]);
712
+ const existing = next._state.contextualVars.get(scale) || [];
713
+ next._state.contextualVars.set(scale, [...existing, ...names]);
964
714
  }
965
715
  return next;
966
716
  }
717
+ extendScale(key, updateFn) {
718
+ const nextTheme = merge({}, this._state.theme, { [key]: updateFn(this._state.theme[key]) });
719
+ return new ThemeBuilder(copyState(this._state, nextTheme));
720
+ }
967
721
  /**
968
- * @param key A current key of theme to update with computed values.
969
- * @example .updateScale('fonts', ({ basic }) => ({ basicFallback: `${basic}, Montserrat` }))
722
+ * Finalize the theme build.
723
+ * Flattens nested data at the boundary produces manifest and serialize().
970
724
  */
971
- updateScale(key, updateFn) {
972
- const nextTheme = merge({}, this.#theme, { [key]: updateFn(this.#theme[key]) });
973
- return this.#checkpoint(nextTheme);
974
- }
975
- /** Finalize the theme build. Returns the theme with a non-enumerable `.manifest` property. */
976
725
  build() {
977
- resolveThemeTokenRefs(this.#theme, this.#emittedScales);
978
- const { variables } = serializeTokens(mapValues(this.#theme.breakpoints, (val) => `${val}px`), "breakpoint", this.#theme);
726
+ const theme = merge({}, this._state.theme);
727
+ const emittedScales = this._state.emittedScales;
728
+ const contextualVars = this._state.contextualVars;
729
+ const { tokenMap, variableMap, variables, modeVariables, modeTokens } = flattenTheme(theme, emittedScales);
730
+ resolveTokenRefs(tokenMap, variableMap, emittedScales);
731
+ const bpVariables = {};
732
+ if (theme.breakpoints && isObject(theme.breakpoints)) for (const [key, value] of Object.entries(theme.breakpoints)) bpVariables[`--breakpoint-${key}`] = `${value}px`;
979
733
  let contextualVarsSerialized;
980
- if (this.#contextualVars.size > 0) {
734
+ if (contextualVars.size > 0) {
981
735
  contextualVarsSerialized = {};
982
- for (const [scale, vars] of this.#contextualVars) contextualVarsSerialized[scale] = vars;
736
+ for (const [scale, vars] of contextualVars) contextualVarsSerialized[scale] = vars;
983
737
  }
984
- const theme = merge({}, this.#theme, {
985
- _variables: { breakpoints: variables },
986
- _tokens: {},
987
- ...contextualVarsSerialized ? { _contextualVars: contextualVarsSerialized } : {}
988
- });
989
- const manifest = assembleManifest(theme);
738
+ const variableCss = buildVariableCss(variables, bpVariables, modeVariables);
739
+ const manifest = {
740
+ tokenMap: {
741
+ ...tokenMap,
742
+ ...Object.fromEntries(Object.entries(theme.breakpoints || {}).map(([k, v]) => [`breakpoints.${k}`, String(v)]))
743
+ },
744
+ variableMap,
745
+ modes: modeTokens,
746
+ variableCss,
747
+ ...contextualVarsSerialized ? { contextualVars: contextualVarsSerialized } : {}
748
+ };
990
749
  Object.defineProperty(theme, "manifest", {
991
750
  value: manifest,
992
751
  enumerable: false,
993
752
  configurable: false,
994
753
  writable: false
995
754
  });
755
+ Object.defineProperty(theme, "serialize", {
756
+ value: () => ({
757
+ scalesJson: JSON.stringify(manifest.tokenMap),
758
+ variableMapJson: JSON.stringify(manifest.variableMap),
759
+ variableCss: manifest.variableCss,
760
+ contextualVarsJson: JSON.stringify(manifest.contextualVars ?? {})
761
+ }),
762
+ enumerable: false,
763
+ configurable: false,
764
+ writable: false
765
+ });
766
+ Object.defineProperty(theme, "varRef", {
767
+ value: (tokenPath) => {
768
+ const varName = variableMap[tokenPath];
769
+ if (varName) return `var(${varName})`;
770
+ const dotIdx = tokenPath.indexOf(".");
771
+ if (dotIdx === -1) return void 0;
772
+ const scale = tokenPath.slice(0, dotIdx);
773
+ const key = tokenPath.slice(dotIdx + 1);
774
+ const scaleObj = theme[scale];
775
+ if (!isObject(scaleObj)) return void 0;
776
+ const val = walkDotPath(scaleObj, key);
777
+ return val !== void 0 ? String(val) : void 0;
778
+ },
779
+ enumerable: false,
780
+ configurable: false,
781
+ writable: false
782
+ });
996
783
  return theme;
997
784
  }
998
785
  };
999
- function createTheme(base) {
1000
- return new ThemeBuilder(base);
786
+ function createTheme() {
787
+ return new ThemeBuilder(createState());
1001
788
  }
1002
- /** Token ref pattern: {scale.key} */
789
+ /** Token ref pattern: {scale.key} or {scale.key.sub} */
1003
790
  const TOKEN_REF_RE = /\{([^}]+)\}/g;
1004
791
  /**
1005
- * Resolve token refs ({scale.key}) in all scale values.
1006
- * Only refs to emitted scales (those with CSS variables) are valid.
1007
- * Runs once at build() time after all scales have been collected.
1008
- */
1009
- function resolveThemeTokenRefs(theme, emittedScales) {
1010
- for (const [scaleName, scaleValue] of Object.entries(theme)) {
1011
- if (scaleName.startsWith("_")) continue;
1012
- if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes") continue;
1013
- if (typeof scaleValue === "function") continue;
1014
- if (!isObject(scaleValue)) continue;
1015
- for (const [key, value] of Object.entries(scaleValue)) {
1016
- if (typeof value !== "string") continue;
1017
- if (!value.includes("{")) continue;
1018
- const resolved = value.replace(TOKEN_REF_RE, (match, ref) => {
1019
- const dotIdx = ref.indexOf(".");
1020
- if (dotIdx === -1) return match;
1021
- const refScale = ref.slice(0, dotIdx);
1022
- const refKey = ref.slice(dotIdx + 1);
1023
- if (refScale === scaleName) {
1024
- console.warn(`[animus] Self-referential token ref {${ref}} in scale '${scaleName}' — skipped`);
1025
- return match;
1026
- }
1027
- const targetScale = theme[refScale];
1028
- if (!targetScale || !isObject(targetScale)) {
1029
- console.warn(`[animus] Token ref {${ref}} references unknown scale '${refScale}'`);
1030
- return match;
1031
- }
1032
- const resolvedValue = targetScale[refKey];
1033
- if (resolvedValue === void 0) {
1034
- console.warn(`[animus] Token ref {${ref}} — key '${refKey}' not found in scale '${refScale}'`);
1035
- return match;
1036
- }
1037
- return String(resolvedValue);
1038
- });
1039
- if (resolved !== value) {
1040
- scaleValue[key] = resolved;
1041
- if (theme._tokens?.[scaleName]) theme._tokens[scaleName][key] = resolved;
1042
- if (theme._variables?.[scaleName]) {
1043
- const varName = `--${scaleName}-${key.replace("$", "")}`;
1044
- if (theme._variables[scaleName][varName] !== void 0) theme._variables[scaleName][varName] = resolved;
1045
- }
1046
- }
1047
- }
1048
- }
1049
- }
1050
- /**
1051
- * Assemble a ThemeManifest from the built theme object.
1052
- *
1053
- * This is the single source of truth for the plugin — no string-matching
1054
- * or re-flattening needed downstream.
792
+ * Flatten the nested theme into dot-path keyed token map and CSS variable declarations.
793
+ * This is the ONLY place where flattening happens.
1055
794
  */
1056
- function assembleManifest(theme) {
795
+ function flattenTheme(theme, emittedScales) {
1057
796
  const tokenMap = {};
1058
797
  const variableMap = {};
798
+ const variables = {};
799
+ const modeVariables = {};
800
+ const modeTokens = {};
1059
801
  for (const [scaleName, scaleValue] of Object.entries(theme)) {
1060
802
  if (scaleName.startsWith("_")) continue;
1061
803
  if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes") continue;
1062
804
  if (typeof scaleValue === "function") continue;
1063
- if (isObject(scaleValue)) flattenTokens(tokenMap, variableMap, scaleName, scaleValue);
805
+ if (!isObject(scaleValue)) continue;
806
+ const flat = flattenToDotPaths(scaleValue);
807
+ const isEmitted = emittedScales.has(scaleName);
808
+ for (const [dotKey, rawValue] of Object.entries(flat)) {
809
+ const tokenPath = `${scaleName}.${dotKey}`;
810
+ const dashKey = dotToDash(dotKey);
811
+ const varName = `--${scaleName === "colors" ? "color" : scaleName}-${dashKey}`;
812
+ if (isEmitted) {
813
+ tokenMap[tokenPath] = `var(${varName})`;
814
+ variableMap[tokenPath] = varName;
815
+ variables[varName] = String(rawValue);
816
+ } else tokenMap[tokenPath] = String(rawValue);
817
+ }
1064
818
  }
1065
- const modes = {};
1066
- if (theme._tokens?.modes && isObject(theme._tokens.modes)) for (const [modeName, modeTokens] of Object.entries(theme._tokens.modes)) {
1067
- if (!isObject(modeTokens)) continue;
1068
- const modeMap = {};
1069
- flattenModeEntries(modeMap, modeTokens, "");
1070
- modes[modeName] = modeMap;
819
+ if (theme.modes && isObject(theme.modes) && theme.colors && isObject(theme.colors)) {
820
+ const flatColors = flattenToDotPaths(theme.colors);
821
+ for (const [modeName, modeAliases] of Object.entries(theme.modes)) {
822
+ if (!isObject(modeAliases)) continue;
823
+ const flatAliases = flattenToDotPaths(modeAliases);
824
+ const modeVars = {};
825
+ const modeVals = {};
826
+ for (const [aliasDotKey, colorRef] of Object.entries(flatAliases)) {
827
+ if (typeof colorRef !== "string") continue;
828
+ const varName = `--color-${dotToDash(aliasDotKey)}`;
829
+ const rawValue = flatColors[colorRef];
830
+ modeVals[`colors.${aliasDotKey}`] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
831
+ modeVars[varName] = rawValue !== void 0 ? String(rawValue) : String(colorRef);
832
+ }
833
+ modeVariables[modeName] = modeVars;
834
+ modeTokens[modeName] = modeVals;
835
+ }
836
+ const initialMode = theme.mode;
837
+ if (initialMode && modeVariables[initialMode]) {
838
+ const initialModeVars = {};
839
+ const flatInitialAliases = flattenToDotPaths(theme.modes[initialMode]);
840
+ for (const [aliasDotKey, colorRef] of Object.entries(flatInitialAliases)) {
841
+ if (typeof colorRef !== "string") continue;
842
+ const varName = `--color-${dotToDash(aliasDotKey)}`;
843
+ const paletteVarName = variableMap[`colors.${colorRef}`];
844
+ if (paletteVarName) initialModeVars[varName] = `var(${paletteVarName})`;
845
+ tokenMap[`colors.${aliasDotKey}`] = `var(${varName})`;
846
+ variableMap[`colors.${aliasDotKey}`] = varName;
847
+ }
848
+ Object.assign(variables, initialModeVars);
849
+ }
1071
850
  }
1072
851
  return {
1073
852
  tokenMap,
1074
853
  variableMap,
1075
- modes,
1076
- variableCss: buildManifestVariableCss(theme),
1077
- contextualVars: theme._contextualVars && typeof theme._contextualVars === "object" && Object.keys(theme._contextualVars).length > 0 ? theme._contextualVars : void 0
854
+ variables,
855
+ modeVariables,
856
+ modeTokens
1078
857
  };
1079
858
  }
1080
- /** Flatten a scale into tokenMap and extract variable references into variableMap. */
1081
- function flattenTokens(tokenMap, variableMap, prefix, obj, parentKey = "") {
1082
- for (const [key, value] of Object.entries(obj)) {
1083
- const fullKey = parentKey ? `${parentKey}-${key}` : key;
1084
- if (isObject(value)) flattenTokens(tokenMap, variableMap, prefix, value, fullKey);
1085
- else {
1086
- const tokenPath = `${prefix}.${fullKey}`;
1087
- const strValue = String(value);
1088
- tokenMap[tokenPath] = strValue;
1089
- if (strValue.startsWith("var(") && strValue.endsWith(")")) variableMap[tokenPath] = strValue.slice(4, -1);
1090
- }
1091
- }
1092
- }
1093
- /** Flatten mode token entries (resolved values) into a flat key→value map. */
1094
- function flattenModeEntries(modeMap, obj, prefix) {
1095
- for (const [key, value] of Object.entries(obj)) {
1096
- const namePart = key === "_" ? prefix : prefix ? `${prefix}-${key}` : key;
1097
- if (typeof value === "string" || typeof value === "number") modeMap[`colors.${namePart}`] = String(value);
1098
- else if (isObject(value)) flattenModeEntries(modeMap, value, namePart);
859
+ /**
860
+ * Resolve token refs ({scale.key}) in all flattened token values.
861
+ * Operates on the flattened tokenMap — does NOT mutate the nested theme.
862
+ */
863
+ function resolveTokenRefs(tokenMap, variableMap, emittedScales) {
864
+ for (const [tokenPath, value] of Object.entries(tokenMap)) {
865
+ if (typeof value !== "string") continue;
866
+ if (!value.includes("{")) continue;
867
+ if (value.startsWith("var(")) continue;
868
+ const scaleName = tokenPath.split(".")[0];
869
+ const resolved = value.replace(TOKEN_REF_RE, (match, ref) => {
870
+ if (ref.split(".")[0] === scaleName) {
871
+ console.warn(`[animus] Self-referential token ref {${ref}} in scale '${scaleName}' — skipped`);
872
+ return match;
873
+ }
874
+ let lookupPath = ref;
875
+ let opacity;
876
+ const slashIdx = ref.indexOf("/");
877
+ if (slashIdx !== -1) {
878
+ lookupPath = ref.slice(0, slashIdx);
879
+ opacity = ref.slice(slashIdx + 1);
880
+ }
881
+ const refValue = tokenMap[lookupPath];
882
+ if (refValue === void 0) {
883
+ console.warn(`[animus] Token ref {${ref}} — path '${lookupPath}' not found in token map`);
884
+ return match;
885
+ }
886
+ if (refValue.startsWith("var(") && opacity) return refValue;
887
+ return opacity ? refValue : refValue;
888
+ });
889
+ if (resolved !== value) tokenMap[tokenPath] = resolved;
1099
890
  }
1100
891
  }
1101
- /** Build CSS variable blocks from theme._variables and _tokens.modes. */
1102
- function buildManifestVariableCss(theme) {
892
+ /** Build CSS variable blocks from flattened data. */
893
+ function buildVariableCss(rootVariables, breakpointVariables, modeVariables) {
1103
894
  const parts = [];
1104
- if (theme._variables && isObject(theme._variables)) {
1105
- const rootLines = [];
1106
- for (const categoryValue of Object.values(theme._variables)) {
1107
- if (!isObject(categoryValue)) continue;
1108
- for (const [cssVar, cssValue] of Object.entries(categoryValue)) if (typeof cssValue === "string") rootLines.push(` ${cssVar}: ${cssValue};`);
1109
- }
1110
- if (rootLines.length > 0) parts.push(`:root {\n${rootLines.join("\n")}\n}`);
1111
- }
1112
- if (theme._tokens?.modes && isObject(theme._tokens.modes)) for (const [modeName, modeTokens] of Object.entries(theme._tokens.modes)) {
1113
- if (!isObject(modeTokens)) continue;
895
+ const rootLines = [];
896
+ for (const [varName, value] of Object.entries(rootVariables)) rootLines.push(` ${varName}: ${value};`);
897
+ for (const [varName, value] of Object.entries(breakpointVariables)) rootLines.push(` ${varName}: ${value};`);
898
+ if (rootLines.length > 0) parts.push(`:root {\n${rootLines.join("\n")}\n}`);
899
+ for (const [modeName, modeVars] of Object.entries(modeVariables)) {
1114
900
  const modeLines = [];
1115
- flattenModeTokensCss(modeLines, modeTokens, "");
901
+ for (const [varName, value] of Object.entries(modeVars)) modeLines.push(` ${varName}: ${value};`);
1116
902
  if (modeLines.length > 0) parts.push(`[data-color-mode="${modeName}"] {\n${modeLines.join("\n")}\n}`);
1117
903
  }
1118
904
  return parts.join("\n\n");
1119
905
  }
1120
- /** Recursively flatten mode tokens into CSS variable declaration lines. */
1121
- function flattenModeTokensCss(lines, obj, prefix) {
1122
- for (const [key, value] of Object.entries(obj)) {
1123
- const namePart = key === "_" ? prefix : prefix ? `${prefix}-${key}` : key;
1124
- if (typeof value === "string" || typeof value === "number") lines.push(` --color-${namePart}: ${value};`);
1125
- else if (isObject(value)) flattenModeTokensCss(lines, value, namePart);
1126
- }
1127
- }
1128
906
  //#endregion
1129
- export { Animus, AnimusExtended, AnimusExtendedWithAll, AnimusWithAll, PropertyBuilder, SystemBuilder, ThemeBuilder, borderShorthand, compose, createClassResolver, createComponent, createScale, createSystem, createTheme, createTransform, flattenScale, gridItem, gridItemRatio, numericOrStringScale, numericScale, percentageOrAbsolute, serializeTokens, size, stringScale };
907
+ export { Animus, AnimusExtended, AnimusExtendedWithAll, AnimusWithAll, SystemBuilder, ThemeBuilder, borderShorthand, compose, createClassResolver, createComponent, createScale, createSystem, createTheme, createTransform, gridItem, gridItemRatio, numericOrStringScale, numericScale, percentageOrAbsolute, size, stringScale };