@animus-ui/system 0.1.0-next.21 → 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.
Files changed (41) hide show
  1. package/dist/Animus.d.ts +2 -7
  2. package/dist/Animus.d.ts.map +1 -1
  3. package/dist/AnimusExtended.d.ts +2 -7
  4. package/dist/AnimusExtended.d.ts.map +1 -1
  5. package/dist/SystemBuilder.d.ts +16 -13
  6. package/dist/SystemBuilder.d.ts.map +1 -1
  7. package/dist/compose.d.ts +2 -4
  8. package/dist/compose.d.ts.map +1 -1
  9. package/dist/createClassResolver-Dny76K15.js +227 -0
  10. package/dist/groups/index.d.ts +1 -0
  11. package/dist/groups/index.d.ts.map +1 -1
  12. package/dist/groups/index.js +3 -2
  13. package/dist/index.d.ts +5 -6
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +339 -527
  16. package/dist/runtime/index.d.ts.map +1 -1
  17. package/dist/runtime-entry.d.ts +9 -0
  18. package/dist/runtime-entry.d.ts.map +1 -0
  19. package/dist/runtime-entry.js +2 -0
  20. package/dist/theme/createTheme.d.ts +46 -53
  21. package/dist/theme/createTheme.d.ts.map +1 -1
  22. package/dist/theme/index.d.ts +0 -2
  23. package/dist/theme/index.d.ts.map +1 -1
  24. package/dist/theme/serializeTokens.d.ts +2 -2
  25. package/dist/theme/serializeTokens.d.ts.map +1 -1
  26. package/dist/theme/utils.d.ts +20 -0
  27. package/dist/theme/utils.d.ts.map +1 -1
  28. package/dist/types/component.d.ts +45 -26
  29. package/dist/types/component.d.ts.map +1 -1
  30. package/dist/types/config.d.ts +11 -2
  31. package/dist/types/config.d.ts.map +1 -1
  32. package/dist/types/props.d.ts +13 -25
  33. package/dist/types/props.d.ts.map +1 -1
  34. package/dist/types/theme.d.ts +38 -19
  35. package/dist/types/theme.d.ts.map +1 -1
  36. package/dist/utils/deepMerge.d.ts +5 -0
  37. package/dist/utils/deepMerge.d.ts.map +1 -0
  38. package/package.json +6 -1
  39. package/dist/PropertyBuilder.d.ts +0 -11
  40. package/dist/PropertyBuilder.d.ts.map +0 -1
  41. /package/dist/{size-Dge_rsuz.js → size-BjymBo7z.js} +0 -0
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;
@@ -589,15 +369,14 @@ function serializeInstance(propRegistry, groupRegistry, globalStyles) {
589
369
  transforms[name] = fn;
590
370
  }
591
371
  }
372
+ if (entry.currentVar) s.currentVar = entry.currentVar;
592
373
  serialized[propName] = s;
593
374
  }
594
- const result = {
375
+ return {
595
376
  propConfig: JSON.stringify(serialized),
596
377
  groupRegistry: JSON.stringify(groupRegistry),
597
378
  transforms
598
379
  };
599
- if (globalStyles) result.globalStyles = globalStyles;
600
- return result;
601
380
  }
602
381
  function createSystem() {
603
382
  return new SystemBuilder();
@@ -620,57 +399,46 @@ function merge(target, ...sources) {
620
399
  }
621
400
  return target;
622
401
  }
623
- 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) {
624
424
  const result = {};
625
- 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
+ }
626
431
  return result;
627
432
  }
628
- //#endregion
629
- //#region src/theme/flattenScale.ts
630
- function flattenScale(object, path) {
631
- return Object.keys(object).reduce((carry, key) => {
632
- const nextKey = path ? `${path}${key === "_" ? "" : `-${key}`}` : key;
633
- const current = object[key];
634
- if (isObject(current)) return {
635
- ...carry,
636
- ...flattenScale(current, nextKey)
637
- };
638
- return {
639
- ...carry,
640
- [nextKey]: object[key]
641
- };
642
- }, {});
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, "-");
643
440
  }
644
441
  //#endregion
645
- //#region src/theme/serializeTokens.ts
646
- const templateBreakpoints = (value, alias, theme) => {
647
- if (isObject(value)) {
648
- const { _, ...rest } = value;
649
- const css = { [alias]: _ };
650
- if (theme) {
651
- const breakpoints = theme.breakpoints;
652
- Object.keys(breakpoints).forEach((key) => {
653
- if (rest[key]) css[breakpoints[key]] = { [alias]: rest[key] };
654
- });
655
- }
656
- return css;
657
- }
658
- return { [alias]: value };
659
- };
660
- const serializeTokens = (tokens, prefix, theme) => {
661
- const tokenReferences = {};
662
- const tokenVariables = {};
663
- Object.keys(tokens).forEach((key) => {
664
- const varName = `--${prefix}-${key.replace("$", "")}`;
665
- tokenReferences[key] = `var(${varName})`;
666
- merge(tokenVariables, templateBreakpoints(tokens[key], varName, theme));
667
- });
668
- return {
669
- tokens: tokenReferences,
670
- variables: tokenVariables
671
- };
672
- };
673
- //#endregion
674
442
  //#region src/theme/createTheme.ts
675
443
  const CSS_NAMED_COLORS = new Set([
676
444
  "aliceblue",
@@ -845,13 +613,20 @@ function isValidCSSColor(value) {
845
613
  if (CSS_NAMED_COLORS.has(v.toLowerCase())) return true;
846
614
  return false;
847
615
  }
848
- /** Validate that mode aliases reference existing color keys. */
849
- 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) {
850
621
  for (const [key, value] of Object.entries(aliases)) {
851
- const aliasPath = prefix ? `${prefix}-${key}` : key;
852
- if (typeof value === "string") {
853
- 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 ? ", ..." : ""}`);
854
- } 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);
855
630
  }
856
631
  }
857
632
  /** Validate all color entries, throwing on invalid values. */
@@ -859,237 +634,274 @@ function validateColors(colors) {
859
634
  for (const [key, value] of Object.entries(colors)) if (isObject(value)) validateColors(value);
860
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.`);
861
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
+ */
862
657
  var ThemeBuilder = class ThemeBuilder {
863
- #theme = {};
864
- #emittedScales = /* @__PURE__ */ new Set();
865
- constructor(baseTheme) {
866
- this.#theme = baseTheme;
867
- }
868
- /** Create a new builder checkpoint, carrying forward emittedScales state. */
869
- #checkpoint(nextTheme) {
870
- const next = new ThemeBuilder(nextTheme);
871
- for (const s of this.#emittedScales) next.#emittedScales.add(s);
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;
672
+ }
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]);
872
681
  return next;
873
682
  }
874
- /**
875
- * @param colors A map of color tokens. Immediately converted to CSS variables `--color-${key}`.
876
- * @example .addColors({ navy: 'navy', hyper: 'purple' })
877
- */
878
683
  addColors(colors) {
879
684
  validateColors(colors);
880
- const flatColors = flattenScale(colors);
881
- const { variables, tokens } = serializeTokens(flatColors, "color", this.#theme);
882
- const nextTheme = merge({}, this.#theme, {
883
- colors: tokens,
884
- _variables: { root: variables },
885
- _tokens: { colors: flatColors }
886
- });
887
- const next = this.#checkpoint(nextTheme);
888
- 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");
889
688
  return next;
890
689
  }
891
- /**
892
- * @param initialMode Default color mode key.
893
- * @param modeConfig Map of color modes with semantic aliases pointing to palette keys.
894
- * @example .addColorModes('dark', { dark: { primary: 'ember' }, light: { primary: 'void' } })
895
- */
896
690
  addColorModes(initialMode, modeConfig) {
897
- const availableColors = this.#theme._tokens?.colors ? Object.keys(this.#theme._tokens.colors) : Object.keys(this.#theme.colors || {});
898
- const colorSet = new Set(availableColors);
899
- for (const [modeName, modeAliases] of Object.entries(modeConfig)) validateModeAliases(modeName, modeAliases, colorSet, availableColors, "");
900
- const modes = mapValues(modeConfig, (mode) => flattenScale(mode));
901
- const { tokens: colors, variables } = serializeTokens(mapValues(merge({}, this.#theme.modes?.[initialMode], modes[initialMode]), (color) => this.#theme.colors[color]), "color", this.#theme);
902
- const getColorValue = (color) => this.#theme._tokens?.colors?.[color];
903
- const nextTheme = merge({}, this.#theme, {
904
- colors,
905
- modes,
906
- mode: initialMode,
907
- _getColorValue: getColorValue,
908
- _variables: { mode: variables },
909
- _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
910
698
  });
911
- return this.#checkpoint(nextTheme);
699
+ return new ThemeBuilder(copyState(this._state, nextTheme));
912
700
  }
913
- /**
914
- * Add a named scale to the theme.
915
- *
916
- * @param config.name - Scale name (e.g. 'space', 'sizes')
917
- * @param config.values - Scale value map
918
- * @param config.emit - When true, generates CSS variables (default: false)
919
- *
920
- * @example
921
- * .addScale({ name: 'space', values: { 0: '0', 8: '0.5rem', 16: '1rem' } })
922
- * .addScale({ name: 'sizes', emit: true, values: { navHeight: '48px' } })
923
- */
924
701
  addScale(config) {
925
702
  const { name, values, emit } = config;
926
- const flattened = flattenScale(values);
927
- let nextTheme;
928
- if (emit) {
929
- const { variables, tokens } = serializeTokens(flattened, name, this.#theme);
930
- nextTheme = merge({}, this.#theme, {
931
- [name]: tokens,
932
- _variables: { [name]: variables },
933
- _tokens: { [name]: flattened }
934
- });
935
- } else nextTheme = merge({}, this.#theme, { [name]: flattened });
936
- const next = this.#checkpoint(nextTheme);
937
- 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);
938
706
  return next;
939
707
  }
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));
711
+ for (const [scale, names] of Object.entries(vars)) {
712
+ const existing = next._state.contextualVars.get(scale) || [];
713
+ next._state.contextualVars.set(scale, [...existing, ...names]);
714
+ }
715
+ return next;
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
+ }
940
721
  /**
941
- * @param key A current key of theme to update with computed values.
942
- * @example .updateScale('fonts', ({ basic }) => ({ basicFallback: `${basic}, Montserrat` }))
722
+ * Finalize the theme build.
723
+ * Flattens nested data at the boundary produces manifest and serialize().
943
724
  */
944
- updateScale(key, updateFn) {
945
- const nextTheme = merge({}, this.#theme, { [key]: updateFn(this.#theme[key]) });
946
- return this.#checkpoint(nextTheme);
947
- }
948
- /** Finalize the theme build. Returns the theme with a non-enumerable `.manifest` property. */
949
725
  build() {
950
- resolveThemeTokenRefs(this.#theme, this.#emittedScales);
951
- const { variables } = serializeTokens(mapValues(this.#theme.breakpoints, (val) => `${val}px`), "breakpoint", this.#theme);
952
- const theme = merge({}, this.#theme, {
953
- _variables: { breakpoints: variables },
954
- _tokens: {}
955
- });
956
- const manifest = assembleManifest(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`;
733
+ let contextualVarsSerialized;
734
+ if (contextualVars.size > 0) {
735
+ contextualVarsSerialized = {};
736
+ for (const [scale, vars] of contextualVars) contextualVarsSerialized[scale] = vars;
737
+ }
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
+ };
957
749
  Object.defineProperty(theme, "manifest", {
958
750
  value: manifest,
959
751
  enumerable: false,
960
752
  configurable: false,
961
753
  writable: false
962
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
+ });
963
783
  return theme;
964
784
  }
965
785
  };
966
- function createTheme(base) {
967
- return new ThemeBuilder(base);
786
+ function createTheme() {
787
+ return new ThemeBuilder(createState());
968
788
  }
969
- /** Token ref pattern: {scale.key} */
789
+ /** Token ref pattern: {scale.key} or {scale.key.sub} */
970
790
  const TOKEN_REF_RE = /\{([^}]+)\}/g;
971
791
  /**
972
- * Resolve token refs ({scale.key}) in all scale values.
973
- * Only refs to emitted scales (those with CSS variables) are valid.
974
- * Runs once at build() time after all scales have been collected.
792
+ * Flatten the nested theme into dot-path keyed token map and CSS variable declarations.
793
+ * This is the ONLY place where flattening happens.
975
794
  */
976
- function resolveThemeTokenRefs(theme, emittedScales) {
977
- for (const [scaleName, scaleValue] of Object.entries(theme)) {
978
- if (scaleName.startsWith("_")) continue;
979
- if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes") continue;
980
- if (typeof scaleValue === "function") continue;
981
- if (!isObject(scaleValue)) continue;
982
- for (const [key, value] of Object.entries(scaleValue)) {
983
- if (typeof value !== "string") continue;
984
- if (!value.includes("{")) continue;
985
- const resolved = value.replace(TOKEN_REF_RE, (match, ref) => {
986
- const dotIdx = ref.indexOf(".");
987
- if (dotIdx === -1) return match;
988
- const refScale = ref.slice(0, dotIdx);
989
- const refKey = ref.slice(dotIdx + 1);
990
- if (refScale === scaleName) {
991
- console.warn(`[animus] Self-referential token ref {${ref}} in scale '${scaleName}' — skipped`);
992
- return match;
993
- }
994
- const targetScale = theme[refScale];
995
- if (!targetScale || !isObject(targetScale)) {
996
- console.warn(`[animus] Token ref {${ref}} references unknown scale '${refScale}'`);
997
- return match;
998
- }
999
- const resolvedValue = targetScale[refKey];
1000
- if (resolvedValue === void 0) {
1001
- console.warn(`[animus] Token ref {${ref}} — key '${refKey}' not found in scale '${refScale}'`);
1002
- return match;
1003
- }
1004
- return String(resolvedValue);
1005
- });
1006
- if (resolved !== value) {
1007
- scaleValue[key] = resolved;
1008
- if (theme._tokens?.[scaleName]) theme._tokens[scaleName][key] = resolved;
1009
- if (theme._variables?.[scaleName]) {
1010
- const varName = `--${scaleName}-${key.replace("$", "")}`;
1011
- if (theme._variables[scaleName][varName] !== void 0) theme._variables[scaleName][varName] = resolved;
1012
- }
1013
- }
1014
- }
1015
- }
1016
- }
1017
- /**
1018
- * Assemble a ThemeManifest from the built theme object.
1019
- *
1020
- * This is the single source of truth for the plugin — no string-matching
1021
- * or re-flattening needed downstream.
1022
- */
1023
- function assembleManifest(theme) {
795
+ function flattenTheme(theme, emittedScales) {
1024
796
  const tokenMap = {};
1025
797
  const variableMap = {};
798
+ const variables = {};
799
+ const modeVariables = {};
800
+ const modeTokens = {};
1026
801
  for (const [scaleName, scaleValue] of Object.entries(theme)) {
1027
802
  if (scaleName.startsWith("_")) continue;
1028
803
  if (scaleName === "breakpoints" || scaleName === "mode" || scaleName === "modes") continue;
1029
804
  if (typeof scaleValue === "function") continue;
1030
- 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
+ }
1031
818
  }
1032
- const modes = {};
1033
- if (theme._tokens?.modes && isObject(theme._tokens.modes)) for (const [modeName, modeTokens] of Object.entries(theme._tokens.modes)) {
1034
- if (!isObject(modeTokens)) continue;
1035
- const modeMap = {};
1036
- flattenModeEntries(modeMap, modeTokens, "");
1037
- 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
+ }
1038
850
  }
1039
851
  return {
1040
852
  tokenMap,
1041
853
  variableMap,
1042
- modes,
1043
- variableCss: buildManifestVariableCss(theme)
854
+ variables,
855
+ modeVariables,
856
+ modeTokens
1044
857
  };
1045
858
  }
1046
- /** Flatten a scale into tokenMap and extract variable references into variableMap. */
1047
- function flattenTokens(tokenMap, variableMap, prefix, obj, parentKey = "") {
1048
- for (const [key, value] of Object.entries(obj)) {
1049
- const fullKey = parentKey ? `${parentKey}-${key}` : key;
1050
- if (isObject(value)) flattenTokens(tokenMap, variableMap, prefix, value, fullKey);
1051
- else {
1052
- const tokenPath = `${prefix}.${fullKey}`;
1053
- const strValue = String(value);
1054
- tokenMap[tokenPath] = strValue;
1055
- if (strValue.startsWith("var(") && strValue.endsWith(")")) variableMap[tokenPath] = strValue.slice(4, -1);
1056
- }
1057
- }
1058
- }
1059
- /** Flatten mode token entries (resolved values) into a flat key→value map. */
1060
- function flattenModeEntries(modeMap, obj, prefix) {
1061
- for (const [key, value] of Object.entries(obj)) {
1062
- const namePart = key === "_" ? prefix : prefix ? `${prefix}-${key}` : key;
1063
- if (typeof value === "string" || typeof value === "number") modeMap[`colors.${namePart}`] = String(value);
1064
- 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;
1065
890
  }
1066
891
  }
1067
- /** Build CSS variable blocks from theme._variables and _tokens.modes. */
1068
- function buildManifestVariableCss(theme) {
892
+ /** Build CSS variable blocks from flattened data. */
893
+ function buildVariableCss(rootVariables, breakpointVariables, modeVariables) {
1069
894
  const parts = [];
1070
- if (theme._variables && isObject(theme._variables)) {
1071
- const rootLines = [];
1072
- for (const categoryValue of Object.values(theme._variables)) {
1073
- if (!isObject(categoryValue)) continue;
1074
- for (const [cssVar, cssValue] of Object.entries(categoryValue)) if (typeof cssValue === "string") rootLines.push(` ${cssVar}: ${cssValue};`);
1075
- }
1076
- if (rootLines.length > 0) parts.push(`:root {\n${rootLines.join("\n")}\n}`);
1077
- }
1078
- if (theme._tokens?.modes && isObject(theme._tokens.modes)) for (const [modeName, modeTokens] of Object.entries(theme._tokens.modes)) {
1079
- 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)) {
1080
900
  const modeLines = [];
1081
- flattenModeTokensCss(modeLines, modeTokens, "");
901
+ for (const [varName, value] of Object.entries(modeVars)) modeLines.push(` ${varName}: ${value};`);
1082
902
  if (modeLines.length > 0) parts.push(`[data-color-mode="${modeName}"] {\n${modeLines.join("\n")}\n}`);
1083
903
  }
1084
904
  return parts.join("\n\n");
1085
905
  }
1086
- /** Recursively flatten mode tokens into CSS variable declaration lines. */
1087
- function flattenModeTokensCss(lines, obj, prefix) {
1088
- for (const [key, value] of Object.entries(obj)) {
1089
- const namePart = key === "_" ? prefix : prefix ? `${prefix}-${key}` : key;
1090
- if (typeof value === "string" || typeof value === "number") lines.push(` --color-${namePart}: ${value};`);
1091
- else if (isObject(value)) flattenModeTokensCss(lines, value, namePart);
1092
- }
1093
- }
1094
906
  //#endregion
1095
- 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 };