chaincss 2.1.30 → 2.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +397 -65
- package/dist/cli/index.js +312 -375
- package/dist/compiler/btt.d.ts +11 -72
- package/dist/compiler/component-generator.d.ts +10 -0
- package/dist/compiler/index.js +198 -331
- package/dist/compiler/recipe.d.ts +35 -0
- package/dist/compiler/scanner.d.ts +5 -0
- package/dist/compiler/timeline.d.ts +29 -0
- package/dist/index.js +253 -397
- package/dist/plugins/vite.js +110 -186
- package/package.json +1 -1
- package/src/compiler/btt.ts +126 -901
- package/src/compiler/component-generator.ts +87 -0
- package/src/compiler/recipe.ts +145 -0
- package/src/compiler/scanner.ts +46 -0
- package/src/compiler/timeline.ts +128 -0
package/src/compiler/btt.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// src/compiler/btt.ts
|
|
2
|
+
/**
|
|
3
|
+
* ChainCSS Build-Time Compiler
|
|
4
|
+
* Core compilation, AT-rules, CSS property loading, source maps
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import fs from 'fs';
|
|
2
8
|
import path from 'path';
|
|
3
9
|
import https from 'https';
|
|
@@ -13,377 +19,47 @@ import { animationPresets, createAnimation } from './animations.js';
|
|
|
13
19
|
import { helpers } from './helpers.js';
|
|
14
20
|
import type { AnimationConfig } from './animations.js';
|
|
15
21
|
import { chain, setTokenContext } from './Chain.js';
|
|
22
|
+
import { takeSnapshot as ts, isTimelineEnabled as timelineActive } from './timeline.js';
|
|
16
23
|
|
|
17
24
|
// ============================================================================
|
|
18
|
-
// Re-
|
|
25
|
+
// Re-exports from split modules
|
|
19
26
|
// ============================================================================
|
|
20
27
|
export { setBreakpoints } from './breakpoints.js';
|
|
21
28
|
export { chain, enableDebug } from './Chain.js';
|
|
22
29
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
selector: string;
|
|
31
|
-
styles: Record<string, any>;
|
|
32
|
-
source: string;
|
|
33
|
-
hash: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface StyleChange {
|
|
37
|
-
id: string;
|
|
38
|
-
timestamp: number;
|
|
39
|
-
selector: string;
|
|
40
|
-
property: string;
|
|
41
|
-
oldValue: any;
|
|
42
|
-
newValue: any;
|
|
43
|
-
type: 'add' | 'remove' | 'modify';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let styleHistory: StyleSnapshot[] = [];
|
|
47
|
-
let styleChanges: StyleChange[] = [];
|
|
48
|
-
let timelineEnabled = false;
|
|
49
|
-
let currentSnapshotId = 0;
|
|
50
|
-
|
|
51
|
-
// Enable/disable timeline tracking
|
|
52
|
-
export function enableTimeline(enable: boolean = true): void {
|
|
53
|
-
timelineEnabled = enable;
|
|
54
|
-
if (!enable) {
|
|
55
|
-
styleHistory = [];
|
|
56
|
-
styleChanges = [];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function getStyleHistory(): StyleSnapshot[] {
|
|
61
|
-
return styleHistory;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getStyleChanges(): StyleChange[] {
|
|
65
|
-
return styleChanges;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function getStyleDiff(snapshotId1: string, snapshotId2: string): Record<string, any> {
|
|
69
|
-
const snapshot1 = styleHistory.find(s => s.id === snapshotId1);
|
|
70
|
-
const snapshot2 = styleHistory.find(s => s.id === snapshotId2);
|
|
71
|
-
|
|
72
|
-
if (!snapshot1 || !snapshot2) {
|
|
73
|
-
return { error: 'Snapshot not found' };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const diff: Record<string, any> = {
|
|
77
|
-
added: {},
|
|
78
|
-
removed: {},
|
|
79
|
-
modified: {}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Find added and modified properties
|
|
83
|
-
for (const [key, value] of Object.entries(snapshot2.styles)) {
|
|
84
|
-
if (!(key in snapshot1.styles)) {
|
|
85
|
-
diff.added[key] = value;
|
|
86
|
-
} else if (JSON.stringify(snapshot1.styles[key]) !== JSON.stringify(value)) {
|
|
87
|
-
diff.modified[key] = {
|
|
88
|
-
old: snapshot1.styles[key],
|
|
89
|
-
new: value
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Find removed properties
|
|
95
|
-
for (const [key, value] of Object.entries(snapshot1.styles)) {
|
|
96
|
-
if (!(key in snapshot2.styles)) {
|
|
97
|
-
diff.removed[key] = value;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return diff;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function takeSnapshot(selector: string, styles: Record<string, any>, source: string): string {
|
|
105
|
-
if (!timelineEnabled) return '';
|
|
106
|
-
|
|
107
|
-
const hash = JSON.stringify(styles);
|
|
108
|
-
const existing = styleHistory.find(s => s.selector === selector && s.hash === hash);
|
|
109
|
-
if (existing) return existing.id;
|
|
110
|
-
|
|
111
|
-
const id = `snapshot_${currentSnapshotId++}`;
|
|
112
|
-
const snapshot: StyleSnapshot = {
|
|
113
|
-
id,
|
|
114
|
-
timestamp: Date.now(),
|
|
115
|
-
selector,
|
|
116
|
-
styles: { ...styles },
|
|
117
|
-
source,
|
|
118
|
-
hash
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
styleHistory.push(snapshot);
|
|
122
|
-
|
|
123
|
-
const previousSnapshot = styleHistory.slice(-2)[0];
|
|
124
|
-
if (previousSnapshot && previousSnapshot.selector === selector) {
|
|
125
|
-
for (const [key, value] of Object.entries(styles)) {
|
|
126
|
-
const oldValue = previousSnapshot.styles[key];
|
|
127
|
-
if (!(key in previousSnapshot.styles)) {
|
|
128
|
-
styleChanges.push({
|
|
129
|
-
id: `change_${Date.now()}_${Math.random()}`,
|
|
130
|
-
timestamp: Date.now(),
|
|
131
|
-
selector,
|
|
132
|
-
property: key,
|
|
133
|
-
oldValue: undefined,
|
|
134
|
-
newValue: value,
|
|
135
|
-
type: 'add'
|
|
136
|
-
});
|
|
137
|
-
} else if (JSON.stringify(oldValue) !== JSON.stringify(value)) {
|
|
138
|
-
styleChanges.push({
|
|
139
|
-
id: `change_${Date.now()}_${Math.random()}`,
|
|
140
|
-
timestamp: Date.now(),
|
|
141
|
-
selector,
|
|
142
|
-
property: key,
|
|
143
|
-
oldValue,
|
|
144
|
-
newValue: value,
|
|
145
|
-
type: 'modify'
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
for (const [key] of Object.entries(previousSnapshot.styles)) {
|
|
151
|
-
if (!(key in styles)) {
|
|
152
|
-
styleChanges.push({
|
|
153
|
-
id: `change_${Date.now()}_${Math.random()}`,
|
|
154
|
-
timestamp: Date.now(),
|
|
155
|
-
selector,
|
|
156
|
-
property: key,
|
|
157
|
-
oldValue: previousSnapshot.styles[key],
|
|
158
|
-
newValue: undefined,
|
|
159
|
-
type: 'remove'
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return id;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function exportTimeline(): string {
|
|
169
|
-
return JSON.stringify({
|
|
170
|
-
history: styleHistory,
|
|
171
|
-
changes: styleChanges,
|
|
172
|
-
exportedAt: Date.now()
|
|
173
|
-
}, null, 2);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function clearTimeline(): void {
|
|
177
|
-
styleHistory = [];
|
|
178
|
-
styleChanges = [];
|
|
179
|
-
currentSnapshotId = 0;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// Framework Component Generators
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
interface ComponentInfo {
|
|
187
|
-
name: string;
|
|
188
|
-
selector: string;
|
|
189
|
-
styles: Record<string, any>;
|
|
190
|
-
propsDefinition?: Record<string, any>;
|
|
191
|
-
framework: 'react' | 'vue' | 'svelte' | 'solid' | 'auto';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function detectFrameworkFromProject(): 'react' | 'vue' | 'svelte' | 'solid' {
|
|
195
|
-
try {
|
|
196
|
-
require.resolve('react/package.json');
|
|
197
|
-
return 'react';
|
|
198
|
-
} catch (e) {}
|
|
199
|
-
try {
|
|
200
|
-
require.resolve('vue/package.json');
|
|
201
|
-
return 'vue';
|
|
202
|
-
} catch (e) {}
|
|
203
|
-
try {
|
|
204
|
-
require.resolve('svelte/package.json');
|
|
205
|
-
return 'svelte';
|
|
206
|
-
} catch (e) {}
|
|
207
|
-
try {
|
|
208
|
-
require.resolve('solid-js/package.json');
|
|
209
|
-
return 'solid';
|
|
210
|
-
} catch (e) {}
|
|
211
|
-
return 'react';
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function generateReactComponent(info: ComponentInfo): string {
|
|
215
|
-
const propsInterface = info.propsDefinition
|
|
216
|
-
? Object.entries(info.propsDefinition)
|
|
217
|
-
.map(([key, type]) => ` ${key}?: ${type};`)
|
|
218
|
-
.join('\n')
|
|
219
|
-
: ' [key: string]: any;';
|
|
220
|
-
|
|
221
|
-
return `// Auto-generated by ChainCSS
|
|
222
|
-
import React from 'react';
|
|
223
|
-
import styles from './${info.name}.class.js';
|
|
224
|
-
import './${info.name}.css';
|
|
225
|
-
|
|
226
|
-
export interface ${info.name}Props {
|
|
227
|
-
className?: string;
|
|
228
|
-
children?: React.ReactNode;
|
|
229
|
-
${propsInterface}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export const ${info.name}: React.FC<${info.name}Props> = ({
|
|
233
|
-
className,
|
|
234
|
-
children,
|
|
235
|
-
...props
|
|
236
|
-
}) => {
|
|
237
|
-
const combinedClassName = [styles.${info.selector.replace(/^\./, '')}, className]
|
|
238
|
-
.filter(Boolean)
|
|
239
|
-
.join(' ');
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<div className={combinedClassName} {...props}>
|
|
243
|
-
{children}
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
${info.name}.displayName = 'ChainCSS${info.name}';
|
|
249
|
-
|
|
250
|
-
export default ${info.name};
|
|
251
|
-
`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function generateVueComponent(info: ComponentInfo): string {
|
|
255
|
-
const propsDefinition = info.propsDefinition
|
|
256
|
-
? Object.entries(info.propsDefinition)
|
|
257
|
-
.map(([key, type]) => ` ${key}: { type: ${type}, default: null },`)
|
|
258
|
-
.join('\n')
|
|
259
|
-
: '';
|
|
260
|
-
|
|
261
|
-
return `<!-- Auto-generated by ChainCSS -->
|
|
262
|
-
<template>
|
|
263
|
-
<component
|
|
264
|
-
:is="tag"
|
|
265
|
-
:class="combinedClass"
|
|
266
|
-
v-bind="$attrs"
|
|
267
|
-
>
|
|
268
|
-
<slot />
|
|
269
|
-
</component>
|
|
270
|
-
</template>
|
|
271
|
-
|
|
272
|
-
<script>
|
|
273
|
-
import styles from './${info.name}.class.js';
|
|
274
|
-
import './${info.name}.css';
|
|
275
|
-
|
|
276
|
-
export default {
|
|
277
|
-
name: 'ChainCSS${info.name}',
|
|
278
|
-
props: {
|
|
279
|
-
tag: {
|
|
280
|
-
type: String,
|
|
281
|
-
default: 'div'
|
|
282
|
-
},
|
|
283
|
-
className: {
|
|
284
|
-
type: String,
|
|
285
|
-
default: ''
|
|
286
|
-
},
|
|
287
|
-
${propsDefinition}
|
|
288
|
-
},
|
|
289
|
-
computed: {
|
|
290
|
-
combinedClass() {
|
|
291
|
-
return [styles.${info.selector.replace(/^\./, '')}, this.className]
|
|
292
|
-
.filter(Boolean)
|
|
293
|
-
.join(' ');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
</script>
|
|
298
|
-
`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function generateSvelteComponent(info: ComponentInfo): string {
|
|
302
|
-
return `<!-- Auto-generated by ChainCSS -->
|
|
303
|
-
<script>
|
|
304
|
-
import styles from './${info.name}.class.js';
|
|
305
|
-
import './${info.name}.css';
|
|
306
|
-
|
|
307
|
-
export let className = '';
|
|
308
|
-
export let tag = 'div';
|
|
309
|
-
|
|
310
|
-
$: combinedClass = [styles.${info.selector.replace(/^\./, '')}, className]
|
|
311
|
-
.filter(Boolean)
|
|
312
|
-
.join(' ');
|
|
313
|
-
</script>
|
|
314
|
-
|
|
315
|
-
<svelte:element this={tag} class={combinedClass}>
|
|
316
|
-
<slot />
|
|
317
|
-
</svelte:element>
|
|
318
|
-
`;
|
|
319
|
-
}
|
|
30
|
+
// Timeline
|
|
31
|
+
export {
|
|
32
|
+
enableTimeline, getStyleHistory, getStyleChanges,
|
|
33
|
+
getStyleDiff, exportTimeline, clearTimeline,
|
|
34
|
+
takeSnapshot, isTimelineEnabled
|
|
35
|
+
} from './timeline.js';
|
|
36
|
+
export type { StyleSnapshot, StyleChange } from './timeline.js';
|
|
320
37
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
import { splitProps } from 'solid-js';
|
|
324
|
-
import styles from './${info.name}.class.js';
|
|
325
|
-
import './${info.name}.css';
|
|
38
|
+
// Scanner
|
|
39
|
+
export { scanContent, scanFileForStyles } from './scanner.js';
|
|
326
40
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
.filter(Boolean)
|
|
331
|
-
.join(' ');
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
<div class={combinedClass()} {...others}>
|
|
335
|
-
{local.children}
|
|
336
|
-
</div>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
`;
|
|
340
|
-
}
|
|
41
|
+
// Recipe System
|
|
42
|
+
export { recipe } from './recipe.js';
|
|
43
|
+
export type { RecipeOptions, Recipe } from './recipe.js';
|
|
341
44
|
|
|
342
|
-
//
|
|
343
|
-
export
|
|
344
|
-
|
|
345
|
-
if (framework === 'auto') {
|
|
346
|
-
framework = detectFrameworkFromProject();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
switch (framework) {
|
|
350
|
-
case 'react':
|
|
351
|
-
return generateReactComponent(info);
|
|
352
|
-
case 'vue':
|
|
353
|
-
return generateVueComponent(info);
|
|
354
|
-
case 'svelte':
|
|
355
|
-
return generateSvelteComponent(info);
|
|
356
|
-
case 'solid':
|
|
357
|
-
return generateSolidComponent(info);
|
|
358
|
-
default:
|
|
359
|
-
return generateReactComponent(info);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
45
|
+
// Component Generator
|
|
46
|
+
export { generateComponentCode, detectFramework } from './component-generator.js';
|
|
47
|
+
export type { ComponentInfo } from './component-generator.js';
|
|
362
48
|
|
|
363
49
|
// ============================================================================
|
|
364
|
-
//
|
|
50
|
+
// Source Maps
|
|
365
51
|
// ============================================================================
|
|
366
52
|
|
|
367
53
|
let enableSourceComments = true;
|
|
368
54
|
|
|
369
55
|
function getSourceLocation(): string | null {
|
|
370
56
|
if (!enableSourceComments) return null;
|
|
371
|
-
|
|
372
57
|
const stack = new Error().stack;
|
|
373
58
|
if (!stack) return null;
|
|
374
|
-
|
|
375
|
-
const stackLines = stack.split('\n');
|
|
376
|
-
|
|
377
|
-
for (let i = 0; i < stackLines.length; i++) {
|
|
378
|
-
const line = stackLines[i];
|
|
59
|
+
for (const line of stack.split('\n')) {
|
|
379
60
|
const match = line.match(/([^/]+\.chain\.js):(\d+):\d+/);
|
|
380
|
-
if (match) {
|
|
381
|
-
const fileName = match[1];
|
|
382
|
-
const lineNumber = match[2];
|
|
383
|
-
return `${fileName}:${lineNumber}`;
|
|
384
|
-
}
|
|
61
|
+
if (match) return `${match[1]}:${match[2]}`;
|
|
385
62
|
}
|
|
386
|
-
|
|
387
63
|
return null;
|
|
388
64
|
}
|
|
389
65
|
|
|
@@ -402,40 +78,24 @@ function addSourceComment(css: string, sourceLocation: string | null): string {
|
|
|
402
78
|
|
|
403
79
|
const fetchWithHttps = (url: string): Promise<any> => {
|
|
404
80
|
return new Promise((resolve, reject) => {
|
|
405
|
-
const timeout = setTimeout(() => {
|
|
406
|
-
req.destroy();
|
|
407
|
-
reject(new Error('Request timeout'));
|
|
408
|
-
}, 3000);
|
|
409
|
-
|
|
81
|
+
const timeout = setTimeout(() => { req.destroy(); reject(new Error('Request timeout')); }, 3000);
|
|
410
82
|
const req = https.get(url, (response) => {
|
|
411
83
|
clearTimeout(timeout);
|
|
412
84
|
let data = '';
|
|
413
85
|
response.on('data', (chunk: string) => data += chunk);
|
|
414
86
|
response.on('end', () => {
|
|
415
|
-
try {
|
|
416
|
-
resolve(JSON.parse(data));
|
|
417
|
-
} catch (error) {
|
|
418
|
-
reject(error);
|
|
419
|
-
}
|
|
87
|
+
try { resolve(JSON.parse(data)); } catch (error) { reject(error); }
|
|
420
88
|
});
|
|
421
89
|
});
|
|
422
|
-
|
|
423
|
-
req.on('error', (error) => {
|
|
424
|
-
clearTimeout(timeout);
|
|
425
|
-
reject(error);
|
|
426
|
-
});
|
|
90
|
+
req.on('error', (error) => { clearTimeout(timeout); reject(error); });
|
|
427
91
|
});
|
|
428
92
|
};
|
|
429
93
|
|
|
430
94
|
const loadCSSProperties = async (): Promise<string[]> => {
|
|
431
|
-
if (chains.cachedValidProperties
|
|
432
|
-
return chains.cachedValidProperties;
|
|
433
|
-
}
|
|
434
|
-
|
|
95
|
+
if (chains.cachedValidProperties?.length > 0) return chains.cachedValidProperties;
|
|
435
96
|
try {
|
|
436
97
|
const url = 'https://raw.githubusercontent.com/mdn/data/main/css/properties.json';
|
|
437
98
|
let data: any;
|
|
438
|
-
|
|
439
99
|
if (typeof fetch !== 'undefined') {
|
|
440
100
|
const controller = new AbortController();
|
|
441
101
|
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
@@ -445,26 +105,17 @@ const loadCSSProperties = async (): Promise<string[]> => {
|
|
|
445
105
|
} else {
|
|
446
106
|
data = await fetchWithHttps(url);
|
|
447
107
|
}
|
|
448
|
-
|
|
449
108
|
const allProperties = Object.keys(data);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
allProperties.forEach(prop => {
|
|
453
|
-
const baseProp = prop.replace(/^-(webkit|moz|ms|o)-/, '');
|
|
454
|
-
baseProperties.add(baseProp);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
chains.cachedValidProperties = Array.from(baseProperties).sort();
|
|
109
|
+
chains.cachedValidProperties = allProperties.map(p => p.replace(/^-(webkit|moz|ms|o)-/, '')).filter((v, i, a) => a.indexOf(v) === i).sort();
|
|
458
110
|
return chains.cachedValidProperties;
|
|
459
|
-
|
|
460
|
-
} catch (error) {
|
|
111
|
+
} catch {
|
|
461
112
|
chains.cachedValidProperties = COMMON_CSS_PROPERTIES;
|
|
462
113
|
return chains.cachedValidProperties;
|
|
463
114
|
}
|
|
464
115
|
};
|
|
465
116
|
|
|
466
117
|
// ============================================================================
|
|
467
|
-
// Chain Object
|
|
118
|
+
// Chain Object
|
|
468
119
|
// ============================================================================
|
|
469
120
|
|
|
470
121
|
export interface ChainObject {
|
|
@@ -472,7 +123,6 @@ export interface ChainObject {
|
|
|
472
123
|
cachedValidProperties: string[];
|
|
473
124
|
classMap: Record<string, string>;
|
|
474
125
|
atomicStats: any;
|
|
475
|
-
componentMap?: Map<string, any>;
|
|
476
126
|
initializeProperties: () => Promise<void>;
|
|
477
127
|
getCachedProperties: () => string[] | null;
|
|
478
128
|
}
|
|
@@ -482,30 +132,17 @@ export const chains: ChainObject = {
|
|
|
482
132
|
cachedValidProperties: [],
|
|
483
133
|
classMap: {},
|
|
484
134
|
atomicStats: null,
|
|
485
|
-
|
|
486
135
|
async initializeProperties() {
|
|
487
|
-
if (this.cachedValidProperties
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
const properties = await loadCSSProperties();
|
|
491
|
-
this.cachedValidProperties = properties;
|
|
136
|
+
if (this.cachedValidProperties?.length > 0) return;
|
|
137
|
+
this.cachedValidProperties = await loadCSSProperties();
|
|
492
138
|
},
|
|
493
|
-
|
|
494
|
-
getCachedProperties() {
|
|
495
|
-
return this.cachedValidProperties;
|
|
496
|
-
}
|
|
139
|
+
getCachedProperties() { return this.cachedValidProperties; }
|
|
497
140
|
};
|
|
498
141
|
|
|
499
142
|
let atomicOptimizer: AtomicOptimizer | null = null;
|
|
500
|
-
|
|
501
|
-
export function setAtomicOptimizer(optimizer: AtomicOptimizer | null): void {
|
|
502
|
-
atomicOptimizer = optimizer;
|
|
503
|
-
}
|
|
504
|
-
|
|
143
|
+
export function setAtomicOptimizer(optimizer: AtomicOptimizer | null): void { atomicOptimizer = optimizer; }
|
|
505
144
|
export function configureAtomic(opts: Record<string, any>): void {
|
|
506
|
-
if (atomicOptimizer)
|
|
507
|
-
Object.assign(atomicOptimizer.options, opts);
|
|
508
|
-
}
|
|
145
|
+
if (atomicOptimizer) Object.assign(atomicOptimizer.options, opts);
|
|
509
146
|
}
|
|
510
147
|
|
|
511
148
|
// ============================================================================
|
|
@@ -513,40 +150,25 @@ export function configureAtomic(opts: Record<string, any>): void {
|
|
|
513
150
|
// ============================================================================
|
|
514
151
|
|
|
515
152
|
export const tokens = originalToken;
|
|
516
|
-
|
|
517
153
|
export function createTokens(tokenValues: Record<string, any>): DesignTokens {
|
|
518
154
|
const tokenObj = new DesignTokens(tokenValues);
|
|
519
|
-
// Also set the token context in Chain.ts
|
|
520
155
|
setTokenContext(tokenObj);
|
|
521
156
|
return tokenObj;
|
|
522
157
|
}
|
|
523
158
|
|
|
524
159
|
// ============================================================================
|
|
525
|
-
//
|
|
160
|
+
// Types
|
|
526
161
|
// ============================================================================
|
|
527
162
|
|
|
528
163
|
export interface AtRule {
|
|
529
164
|
type: 'media' | 'keyframes' | 'font-face' | 'supports' | 'container' | 'layer' | 'counter-style' | 'property';
|
|
530
|
-
query?: string;
|
|
531
|
-
condition?: string;
|
|
532
|
-
name?: string;
|
|
533
|
-
styles?: any;
|
|
165
|
+
query?: string; condition?: string; name?: string; styles?: any;
|
|
534
166
|
steps?: Record<string, Record<string, string>>;
|
|
535
|
-
properties?: Record<string, string>;
|
|
536
|
-
descriptors?: Record<string, string>;
|
|
167
|
+
properties?: Record<string, string>; descriptors?: Record<string, string>;
|
|
537
168
|
}
|
|
538
169
|
|
|
539
|
-
export interface NestedRule {
|
|
540
|
-
|
|
541
|
-
styles: Record<string, string | number>;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export interface ThemeBlock {
|
|
545
|
-
name: string;
|
|
546
|
-
styles: StyleDefinition;
|
|
547
|
-
tokens: any;
|
|
548
|
-
fallback: any;
|
|
549
|
-
}
|
|
170
|
+
export interface NestedRule { selector: string; styles: Record<string, string | number>; }
|
|
171
|
+
export interface ThemeBlock { name: string; styles: StyleDefinition; tokens: any; fallback: any; }
|
|
550
172
|
|
|
551
173
|
export interface StyleDefinition {
|
|
552
174
|
selectors: string[];
|
|
@@ -557,590 +179,193 @@ export interface StyleDefinition {
|
|
|
557
179
|
[cssProperty: string]: any;
|
|
558
180
|
}
|
|
559
181
|
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// AT-Rule Processing
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
560
186
|
function processAtRule(rule: AtRule, parentSelectors: string[] | null = null): string {
|
|
561
187
|
let output = '';
|
|
562
|
-
|
|
563
|
-
switch(rule.type) {
|
|
188
|
+
switch (rule.type) {
|
|
564
189
|
case 'media':
|
|
565
190
|
output = `@media ${rule.query} {\n`;
|
|
566
191
|
if (rule.styles && typeof rule.styles === 'object') {
|
|
567
|
-
let
|
|
568
|
-
for (const prop in rule.styles) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (ruleBody.trim()) {
|
|
573
|
-
const selector = (parentSelectors && parentSelectors.length > 0)
|
|
574
|
-
? parentSelectors.join(', ')
|
|
575
|
-
: '.unknown-selector';
|
|
576
|
-
const sourceLocation = getSourceLocation();
|
|
577
|
-
if (enableSourceComments && sourceLocation) {
|
|
578
|
-
output += ` /* Generated from: ${sourceLocation} */\n`;
|
|
579
|
-
}
|
|
580
|
-
output += ` ${selector} {\n${ruleBody} }\n`;
|
|
192
|
+
let body = '';
|
|
193
|
+
for (const prop in rule.styles) body += ` ${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${rule.styles[prop]};\n`;
|
|
194
|
+
if (body.trim()) {
|
|
195
|
+
const sel = parentSelectors?.length ? parentSelectors.join(', ') : '.unknown-selector';
|
|
196
|
+
output += ` ${sel} {\n${body} }\n`;
|
|
581
197
|
}
|
|
582
198
|
}
|
|
583
199
|
output += '}\n';
|
|
584
200
|
break;
|
|
585
|
-
|
|
586
201
|
case 'keyframes':
|
|
587
202
|
output = `@keyframes ${rule.name} {\n`;
|
|
588
203
|
for (const step in rule.steps) {
|
|
589
204
|
output += ` ${step} {\n`;
|
|
590
205
|
for (const prop in rule.steps[step]) {
|
|
591
|
-
if (prop !== 'selectors') {
|
|
592
|
-
const kebabKey = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
593
|
-
output += ` ${kebabKey}: ${rule.steps[step][prop]};\n`;
|
|
594
|
-
}
|
|
206
|
+
if (prop !== 'selectors') output += ` ${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${rule.steps[step][prop]};\n`;
|
|
595
207
|
}
|
|
596
208
|
output += ' }\n';
|
|
597
209
|
}
|
|
598
210
|
output += '}\n';
|
|
599
211
|
break;
|
|
600
|
-
|
|
601
212
|
case 'font-face':
|
|
602
213
|
output = '@font-face {\n';
|
|
603
214
|
for (const prop in rule.properties) {
|
|
604
|
-
if (prop !== 'selectors') {
|
|
605
|
-
const kebabKey = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
606
|
-
output += ` ${kebabKey}: ${rule.properties[prop]};\n`;
|
|
607
|
-
}
|
|
215
|
+
if (prop !== 'selectors') output += ` ${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${rule.properties[prop]};\n`;
|
|
608
216
|
}
|
|
609
217
|
output += '}\n';
|
|
610
218
|
break;
|
|
611
|
-
|
|
612
|
-
default:
|
|
613
|
-
// Handle other AT-rules
|
|
614
|
-
output = '';
|
|
615
|
-
break;
|
|
616
219
|
}
|
|
617
|
-
|
|
618
220
|
return output;
|
|
619
221
|
}
|
|
620
222
|
|
|
621
223
|
// ============================================================================
|
|
622
|
-
//
|
|
224
|
+
// Compile & Run
|
|
623
225
|
// ============================================================================
|
|
624
226
|
|
|
625
227
|
export const run = (...args: any[]): string => {
|
|
626
|
-
// Validate inputs
|
|
627
228
|
if (args.length === 0) return '';
|
|
628
|
-
|
|
629
|
-
const validStyles = args.filter(value => value && typeof value === 'object');
|
|
229
|
+
const validStyles = args.filter(v => v && typeof v === 'object');
|
|
630
230
|
if (validStyles.length === 0) return '';
|
|
631
231
|
|
|
632
232
|
let cssOutput = '';
|
|
633
233
|
const styleObjs: any[] = [];
|
|
634
|
-
|
|
234
|
+
|
|
635
235
|
args.forEach((value) => {
|
|
636
236
|
if (!value) return;
|
|
637
237
|
styleObjs.push(value);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
let mainRuleBody = '';
|
|
647
|
-
let subRulesOutput = '';
|
|
238
|
+
|
|
239
|
+
if (value.type && !value.selectors) { cssOutput += processAtRule(value) + '\n'; return; }
|
|
240
|
+
if (!value.selectors) return;
|
|
241
|
+
|
|
242
|
+
let mainBody = '', subOutput = '';
|
|
243
|
+
for (const key in value) {
|
|
244
|
+
if (!value.hasOwnProperty(key)) continue;
|
|
245
|
+
if (['selectors','atRules','hover','nestedRules','use','nest','themes','_componentName','_generateComponent','_framework','_propsDefinition'].includes(key)) continue;
|
|
648
246
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// Skip metadata and handled special keys
|
|
653
|
-
if ([
|
|
654
|
-
'selectors', 'atRules', 'hover', 'nestedRules', 'use', 'nest', 'themes',
|
|
655
|
-
'_componentName', '_generateComponent', '_framework', '_propsDefinition'
|
|
656
|
-
].includes(key)) continue;
|
|
657
|
-
|
|
658
|
-
// Handle AT-rules
|
|
659
|
-
if (key === 'atRules' && Array.isArray(value[key])) {
|
|
660
|
-
value[key].forEach((rule: any) => {
|
|
661
|
-
subRulesOutput += processAtRule(rule, value.selectors);
|
|
662
|
-
});
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Handle Nested Rules
|
|
667
|
-
if (key === 'nestedRules' && Array.isArray(value[key])) {
|
|
668
|
-
value[key].forEach((rule: any) => {
|
|
669
|
-
let nestedBody = '';
|
|
670
|
-
for (const prop in rule.styles) {
|
|
671
|
-
const kebabKey = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
672
|
-
nestedBody += ` ${kebabKey}: ${rule.styles[prop]};\n`;
|
|
673
|
-
}
|
|
674
|
-
if (nestedBody) {
|
|
675
|
-
subRulesOutput += `${value.selectors.join(', ')} ${rule.selector} {\n${nestedBody} }\n`;
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Handle Hover State
|
|
682
|
-
if (key === 'hover' && typeof value[key] === 'object') {
|
|
683
|
-
let hoverBody = '';
|
|
684
|
-
for (const hoverKey in value[key]) {
|
|
685
|
-
const kebabKey = hoverKey.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
686
|
-
hoverBody += ` ${kebabKey}: ${value[key][hoverKey]};\n`;
|
|
687
|
-
}
|
|
688
|
-
if (hoverBody) {
|
|
689
|
-
subRulesOutput += `${value.selectors.join(', ')}:hover {\n${hoverBody}}\n`;
|
|
690
|
-
}
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Standard CSS Property
|
|
695
|
-
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
696
|
-
mainRuleBody += ` ${kebabKey}: ${value[key]};\n`;
|
|
247
|
+
if (key === 'atRules' && Array.isArray(value[key])) {
|
|
248
|
+
value[key].forEach((r: any) => { subOutput += processAtRule(r, value.selectors); });
|
|
249
|
+
continue;
|
|
697
250
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
251
|
+
if (key === 'nestedRules' && Array.isArray(value[key])) {
|
|
252
|
+
value[key].forEach((r: any) => {
|
|
253
|
+
let nb = '';
|
|
254
|
+
for (const p in r.styles) nb += ` ${p.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${r.styles[p]};\n`;
|
|
255
|
+
if (nb) subOutput += `${value.selectors.join(', ')} ${r.selector} {\n${nb} }\n`;
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
701
258
|
}
|
|
702
|
-
|
|
259
|
+
if (key === 'hover' && typeof value[key] === 'object') {
|
|
260
|
+
let hb = '';
|
|
261
|
+
for (const hk in value[key]) hb += ` ${hk.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${value[key][hk]};\n`;
|
|
262
|
+
if (hb) subOutput += `${value.selectors.join(', ')}:hover {\n${hb}}\n`;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
mainBody += ` ${key.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${value[key]};\n`;
|
|
703
266
|
}
|
|
267
|
+
if (mainBody.trim()) cssOutput += `${value.selectors.join(', ')} {\n${mainBody}}\n`;
|
|
268
|
+
cssOutput += subOutput;
|
|
704
269
|
});
|
|
705
|
-
|
|
706
|
-
// Cleanup whitespace
|
|
707
|
-
cssOutput = cssOutput.replace(/\n{3,}/g, '\n\n').trim();
|
|
708
|
-
|
|
709
|
-
// Handle Atomic Optimization inside recipes/runs
|
|
710
|
-
if (atomicOptimizer && atomicOptimizer.options.enabled) {
|
|
711
|
-
const result = atomicOptimizer.optimize(styleObjs);
|
|
712
|
-
return result.css;
|
|
713
|
-
}
|
|
714
270
|
|
|
271
|
+
cssOutput = cssOutput.replace(/\n{3,}/g, '\n\n').trim();
|
|
272
|
+
if (atomicOptimizer?.options.enabled) return atomicOptimizer.optimize(styleObjs).css;
|
|
715
273
|
return cssOutput;
|
|
716
274
|
};
|
|
717
275
|
|
|
718
|
-
function generateCSSFromCollected(collected: StyleDefinition[]): string {
|
|
719
|
-
let css = '';
|
|
720
|
-
for (const style of collected) {
|
|
721
|
-
if (!style.selectors) continue;
|
|
722
|
-
|
|
723
|
-
let normalStyles = '';
|
|
724
|
-
let hoverStyles = '';
|
|
725
|
-
|
|
726
|
-
for (const [key, value] of Object.entries(style)) {
|
|
727
|
-
if (key === 'selectors') continue;
|
|
728
|
-
|
|
729
|
-
if (key === 'hover' && typeof value === 'object') {
|
|
730
|
-
for (const [hoverKey, hoverValue] of Object.entries(value)) {
|
|
731
|
-
const kebabKey = hoverKey.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
732
|
-
hoverStyles += ` ${kebabKey}: ${hoverValue};\n`;
|
|
733
|
-
}
|
|
734
|
-
} else {
|
|
735
|
-
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
736
|
-
normalStyles += ` ${kebabKey}: ${value};\n`;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (normalStyles) {
|
|
741
|
-
css += `${style.selectors.join(', ')} {\n${normalStyles}}\n`;
|
|
742
|
-
}
|
|
743
|
-
if (hoverStyles) {
|
|
744
|
-
css += `${style.selectors.join(', ')}:hover {\n${hoverStyles}}\n`;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
return css;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
276
|
export const compile = (obj: Record<string, StyleDefinition>): string => {
|
|
751
277
|
let cssString = '';
|
|
752
278
|
const collected: StyleDefinition[] = [];
|
|
753
279
|
const processedSelectors = new Set<string>();
|
|
754
|
-
|
|
280
|
+
|
|
755
281
|
for (const key in obj) {
|
|
756
282
|
if (!obj.hasOwnProperty(key)) continue;
|
|
757
283
|
const element = obj[key];
|
|
758
|
-
|
|
284
|
+
|
|
759
285
|
if (element && (element as any).variants && typeof (element as any).compileAll === 'function') {
|
|
760
|
-
|
|
761
|
-
const recipeOutput = (element as any).compileAll(cleanKey);
|
|
762
|
-
cssString += recipeOutput + '\n';
|
|
286
|
+
cssString += (element as any).compileAll(key.includes('_') ? key.split('_').pop() : key) + '\n';
|
|
763
287
|
continue;
|
|
764
288
|
}
|
|
289
|
+
if (!element?.selectors?.[0]) continue;
|
|
765
290
|
|
|
766
|
-
// 1. Basic Validation
|
|
767
|
-
if (!element || !element.selectors || !element.selectors[0]) continue;
|
|
768
|
-
|
|
769
291
|
const selectorKey = element.selectors.join(',');
|
|
770
292
|
if (processedSelectors.has(selectorKey)) continue;
|
|
771
|
-
|
|
772
293
|
processedSelectors.add(selectorKey);
|
|
773
294
|
collected.push(element);
|
|
774
|
-
|
|
295
|
+
|
|
775
296
|
const sourceLocation = getSourceLocation();
|
|
776
|
-
let elementCSS = '';
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (timelineEnabled) {
|
|
297
|
+
let elementCSS = '', subRulesCSS = '';
|
|
298
|
+
|
|
299
|
+
// Timeline snapshot
|
|
300
|
+
if (timelineActive()) {
|
|
781
301
|
const styles: Record<string, any> = {};
|
|
782
302
|
for (const prop in element) {
|
|
783
|
-
if (!['selectors',
|
|
784
|
-
styles[prop] = element[prop];
|
|
785
|
-
}
|
|
303
|
+
if (!['selectors','atRules','hover','nestedRules','use','nest','themes'].includes(prop)) styles[prop] = element[prop];
|
|
786
304
|
}
|
|
787
|
-
|
|
305
|
+
ts(element.selectors[0], styles, sourceLocation || 'unknown');
|
|
788
306
|
}
|
|
789
|
-
|
|
790
|
-
// 3. Process Standard CSS Properties
|
|
307
|
+
|
|
791
308
|
for (const prop in element) {
|
|
792
309
|
if (prop.startsWith('.') || prop.startsWith('&')) continue;
|
|
793
|
-
|
|
794
|
-
if (['selectors', 'atRules', 'hover', 'use', 'nest', 'themes', 'nestedRules', '_componentName', '_generateComponent', '_framework'].includes(prop)) continue;
|
|
310
|
+
if (['selectors','atRules','hover','use','nest','themes','nestedRules','_componentName','_generateComponent','_framework'].includes(prop)) continue;
|
|
795
311
|
if (prop.startsWith('_') || !element.hasOwnProperty(prop)) continue;
|
|
796
|
-
|
|
797
312
|
const value = element[prop];
|
|
798
313
|
if (value === undefined || value === null) continue;
|
|
799
|
-
|
|
800
|
-
const kebabKey = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
801
|
-
elementCSS += ` ${kebabKey}: ${value};\n`;
|
|
314
|
+
elementCSS += ` ${prop.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${value};\n`;
|
|
802
315
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
let block = `${element.selectors.join(', ')} {\n${elementCSS}}\n`;
|
|
807
|
-
cssString += addSourceComment(block, sourceLocation);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// 5. Process Hover State
|
|
316
|
+
|
|
317
|
+
if (elementCSS.trim()) cssString += addSourceComment(`${element.selectors.join(', ')} {\n${elementCSS}}\n`, sourceLocation);
|
|
318
|
+
|
|
811
319
|
if (element.hover && typeof element.hover === 'object') {
|
|
812
|
-
let
|
|
813
|
-
for (const
|
|
814
|
-
|
|
815
|
-
hoverBody += ` ${hKebab}: ${element.hover[hProp]};\n`;
|
|
816
|
-
}
|
|
817
|
-
if (hoverBody) {
|
|
818
|
-
let block = `${element.selectors.join(', ')}:hover {\n${hoverBody}}\n`;
|
|
819
|
-
cssString += addSourceComment(block, sourceLocation);
|
|
820
|
-
}
|
|
320
|
+
let hb = '';
|
|
321
|
+
for (const hp in element.hover) hb += ` ${hp.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${element.hover[hp]};\n`;
|
|
322
|
+
if (hb) cssString += addSourceComment(`${element.selectors.join(', ')}:hover {\n${hb}}\n`, sourceLocation);
|
|
821
323
|
}
|
|
822
|
-
|
|
823
|
-
// 5.5 Process Nested Selectors (The missing link!)
|
|
324
|
+
|
|
824
325
|
for (const prop in element) {
|
|
825
|
-
// If the property starts with . or &, it's a nested selector
|
|
826
326
|
if ((prop.startsWith('.') || prop.startsWith('&')) && typeof element[prop] === 'object') {
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const parentSelector = element.selectors[0];
|
|
831
|
-
const subSelector = prop.startsWith('&')
|
|
832
|
-
? prop.replace('&', parentSelector)
|
|
833
|
-
: `${parentSelector} ${prop}`;
|
|
834
|
-
|
|
835
|
-
// Recursively compile this sub-block
|
|
836
|
-
// We wrap it in a mock StyleDefinition object so compile can eat it
|
|
837
|
-
cssString += compile({
|
|
838
|
-
[subSelector]: {
|
|
839
|
-
selectors: [subSelector],
|
|
840
|
-
...subElement
|
|
841
|
-
}
|
|
842
|
-
}) + '\n';
|
|
327
|
+
const parentSel = element.selectors[0];
|
|
328
|
+
const subSel = prop.startsWith('&') ? prop.replace('&', parentSel) : `${parentSel} ${prop}`;
|
|
329
|
+
cssString += compile({ [subSel]: { selectors: [subSel], ...element[prop] } }) + '\n';
|
|
843
330
|
}
|
|
844
331
|
}
|
|
845
|
-
|
|
846
|
-
// 6. Process At-Rules (Media Queries, Keyframes)
|
|
332
|
+
|
|
847
333
|
if (element.atRules && Array.isArray(element.atRules)) {
|
|
848
|
-
element.atRules.forEach((rule: AtRule) => {
|
|
849
|
-
subRulesCSS += processAtRule(rule, element.selectors);
|
|
850
|
-
});
|
|
334
|
+
element.atRules.forEach((rule: AtRule) => { subRulesCSS += processAtRule(rule, element.selectors); });
|
|
851
335
|
}
|
|
852
|
-
|
|
853
|
-
// 7. Process Themes
|
|
336
|
+
|
|
854
337
|
if (element.themes && Array.isArray(element.themes)) {
|
|
855
338
|
element.themes.forEach((theme: ThemeBlock) => {
|
|
856
339
|
if (theme.styles) {
|
|
857
|
-
let
|
|
858
|
-
for (const
|
|
859
|
-
if (
|
|
860
|
-
|
|
861
|
-
themeCSS += ` ${tKebab}: ${theme.styles[tProp]};\n`;
|
|
862
|
-
}
|
|
863
|
-
if (themeCSS) {
|
|
864
|
-
let block = `${theme.styles.selectors?.join(', ') || element.selectors.join(', ')} {\n${themeCSS}}\n`;
|
|
865
|
-
subRulesCSS += addSourceComment(block, sourceLocation);
|
|
340
|
+
let tc = '';
|
|
341
|
+
for (const tp in theme.styles) {
|
|
342
|
+
if (tp === 'selectors') continue;
|
|
343
|
+
tc += ` ${tp.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${theme.styles[tp]};\n`;
|
|
866
344
|
}
|
|
345
|
+
if (tc) subRulesCSS += addSourceComment(`${theme.styles.selectors?.join(', ') || element.selectors.join(', ')} {\n${tc}}\n`, sourceLocation);
|
|
867
346
|
}
|
|
868
347
|
});
|
|
869
348
|
}
|
|
870
|
-
|
|
349
|
+
|
|
871
350
|
cssString += subRulesCSS;
|
|
872
351
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (atomicOptimizer && atomicOptimizer.options.enabled) {
|
|
352
|
+
|
|
353
|
+
if (atomicOptimizer?.options.enabled) {
|
|
876
354
|
const result = atomicOptimizer.optimize(collected);
|
|
877
355
|
chains.cssOutput = result.css;
|
|
878
356
|
return result.css;
|
|
879
357
|
}
|
|
880
|
-
|
|
358
|
+
|
|
881
359
|
chains.cssOutput = cssString.trim();
|
|
882
360
|
return chains.cssOutput;
|
|
883
361
|
};
|
|
884
362
|
|
|
885
363
|
// ============================================================================
|
|
886
|
-
//
|
|
364
|
+
// Initialize
|
|
887
365
|
// ============================================================================
|
|
888
366
|
|
|
889
|
-
export interface RecipeOptions<TVariants extends Record<string, Record<string, any>>> {
|
|
890
|
-
base?: StyleDefinition | (() => StyleDefinition);
|
|
891
|
-
variants?: TVariants;
|
|
892
|
-
defaultVariants?: Partial<{ [K in keyof TVariants]: keyof TVariants[K] }>;
|
|
893
|
-
compoundVariants?: Array<{
|
|
894
|
-
variants: Partial<{ [K in keyof TVariants]: keyof TVariants[K] }>;
|
|
895
|
-
style: StyleDefinition | (() => StyleDefinition);
|
|
896
|
-
}>;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
export type Recipe<TVariants extends Record<string, Record<string, any>>> = {
|
|
900
|
-
(selection?: Partial<{ [K in keyof TVariants]: keyof TVariants[K] }>): StyleDefinition;
|
|
901
|
-
variants: TVariants;
|
|
902
|
-
defaultVariants: Partial<{ [K in keyof TVariants]: keyof TVariants[K] }>;
|
|
903
|
-
base: StyleDefinition;
|
|
904
|
-
getAllVariants: () => Array<Partial<{ [K in keyof TVariants]: keyof TVariants[K] }>>;
|
|
905
|
-
compileAll: () => string;
|
|
906
|
-
getVariantClassNames: () => Record<string, string>;
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
export function recipe<TVariants extends Record<string, Record<string, any>>>(
|
|
910
|
-
options: RecipeOptions<TVariants>
|
|
911
|
-
): Recipe<TVariants> {
|
|
912
|
-
const {
|
|
913
|
-
base,
|
|
914
|
-
variants = {} as TVariants,
|
|
915
|
-
defaultVariants = {},
|
|
916
|
-
compoundVariants = []
|
|
917
|
-
} = options;
|
|
918
|
-
|
|
919
|
-
const baseStyle = typeof base === 'function' ? (base as () => StyleDefinition)() : base;
|
|
920
|
-
const variantStyles: Record<string, Record<string, StyleDefinition>> = {};
|
|
921
|
-
|
|
922
|
-
for (const [variantName, variantMap] of Object.entries(variants)) {
|
|
923
|
-
variantStyles[variantName] = {};
|
|
924
|
-
for (const [variantKey, variantStyle] of Object.entries(variantMap as Record<string, any>)) {
|
|
925
|
-
variantStyles[variantName][variantKey] = typeof variantStyle === 'function'
|
|
926
|
-
? (variantStyle as () => StyleDefinition)()
|
|
927
|
-
: variantStyle;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const compoundStyles = compoundVariants.map(cv => ({
|
|
932
|
-
condition: cv.variants || cv,
|
|
933
|
-
style: typeof cv.style === 'function' ? (cv.style as () => StyleDefinition)() : cv.style
|
|
934
|
-
}));
|
|
935
|
-
|
|
936
|
-
function mergeStyles(...styles: (StyleDefinition | undefined)[]): StyleDefinition {
|
|
937
|
-
const merged: StyleDefinition = { selectors: [] } as StyleDefinition;
|
|
938
|
-
for (const style of styles) {
|
|
939
|
-
if (!style) continue;
|
|
940
|
-
for (const [key, value] of Object.entries(style)) {
|
|
941
|
-
if (key === 'selectors') {
|
|
942
|
-
// Prevent duplicate selectors
|
|
943
|
-
const newSelectors = Array.isArray(value) ? value : [value];
|
|
944
|
-
merged.selectors = [...new Set([...(merged.selectors || []), ...newSelectors])];
|
|
945
|
-
} else if (key === 'hover' && typeof value === 'object') {
|
|
946
|
-
if (!merged.hover) merged.hover = {};
|
|
947
|
-
Object.assign(merged.hover, value);
|
|
948
|
-
} else if (key !== 'selectors') {
|
|
949
|
-
(merged as any)[key] = value;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return merged;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function pick(variantSelection: Partial<Record<keyof TVariants, any>> = {}): StyleDefinition {
|
|
957
|
-
const selected = { ...defaultVariants, ...variantSelection } as Record<string, any>;
|
|
958
|
-
const stylesToMerge: StyleDefinition[] = [];
|
|
959
|
-
|
|
960
|
-
if (baseStyle) stylesToMerge.push(baseStyle);
|
|
961
|
-
for (const [variantName, variantValue] of Object.entries(selected)) {
|
|
962
|
-
const variantStyle = variantStyles[variantName]?.[variantValue];
|
|
963
|
-
if (variantStyle) stylesToMerge.push(variantStyle);
|
|
964
|
-
}
|
|
965
|
-
for (const cv of compoundStyles) {
|
|
966
|
-
const matches = Object.entries(cv.condition).every(
|
|
967
|
-
([key, value]) => selected[key] === value
|
|
968
|
-
);
|
|
969
|
-
if (matches && cv.style) stylesToMerge.push(cv.style);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
const merged = mergeStyles(...stylesToMerge);
|
|
973
|
-
let styleBuilder: any = chain();
|
|
974
|
-
|
|
975
|
-
for (const [prop, value] of Object.entries(merged)) {
|
|
976
|
-
if (prop === 'selectors' || prop === 'hover') continue;
|
|
977
|
-
if (styleBuilder[prop]) {
|
|
978
|
-
styleBuilder = styleBuilder[prop](value);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (merged.hover) {
|
|
983
|
-
styleBuilder = styleBuilder.hover();
|
|
984
|
-
for (const [hoverProp, hoverValue] of Object.entries(merged.hover)) {
|
|
985
|
-
if (styleBuilder[hoverProp]) {
|
|
986
|
-
styleBuilder = styleBuilder[hoverProp](hoverValue);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
styleBuilder = styleBuilder.end();
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const selectors = merged.selectors || [];
|
|
993
|
-
return styleBuilder.$el(...selectors);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
(pick as any).variants = variants;
|
|
997
|
-
(pick as any).defaultVariants = defaultVariants;
|
|
998
|
-
(pick as any).base = baseStyle;
|
|
999
|
-
|
|
1000
|
-
(pick as any).getAllVariants = (): Array<Partial<Record<keyof TVariants, any>>> => {
|
|
1001
|
-
const result: Array<Partial<Record<keyof TVariants, any>>> = [];
|
|
1002
|
-
const variantKeys = Object.keys(variants) as (keyof TVariants)[];
|
|
1003
|
-
|
|
1004
|
-
function generate(current: Partial<Record<keyof TVariants, any>>, index: number): void {
|
|
1005
|
-
if (index === variantKeys.length) {
|
|
1006
|
-
result.push({ ...current });
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
const variantName = variantKeys[index];
|
|
1010
|
-
for (const variantValue of Object.keys(variants[variantName] as Record<string, any>)) {
|
|
1011
|
-
current[variantName] = variantValue as any;
|
|
1012
|
-
generate(current, index + 1);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
generate({}, 0);
|
|
1017
|
-
return result;
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
// Get class names for all variants (useful for component libraries)
|
|
1021
|
-
(pick as any).getVariantClassNames = (): Record<string, string> => {
|
|
1022
|
-
const allVariants = (pick as any).getAllVariants();
|
|
1023
|
-
const classNames: Record<string, string> = {};
|
|
1024
|
-
|
|
1025
|
-
for (const variant of allVariants) {
|
|
1026
|
-
const variantKey = Object.entries(variant).map(([k, v]) => `${k}-${v}`).join('_');
|
|
1027
|
-
const styleDef = pick(variant);
|
|
1028
|
-
// Extract class name from selectors
|
|
1029
|
-
if (styleDef.selectors && styleDef.selectors[0]) {
|
|
1030
|
-
classNames[variantKey] = styleDef.selectors[0].replace(/^\./, '');
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
return classNames;
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
(pick as any).compileAll = (): string => {
|
|
1038
|
-
const allVariants = (pick as any).getAllVariants();
|
|
1039
|
-
const styles: StyleDefinition[] = [];
|
|
1040
|
-
|
|
1041
|
-
// Add base style
|
|
1042
|
-
if (baseStyle && baseStyle.selectors) {
|
|
1043
|
-
styles.push(baseStyle);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// Add all variant styles
|
|
1047
|
-
for (const variant of allVariants) {
|
|
1048
|
-
const styleDef = pick(variant);
|
|
1049
|
-
if (styleDef && styleDef.selectors) {
|
|
1050
|
-
styles.push(styleDef);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Also add individual variant styles for completeness
|
|
1055
|
-
for (const variantName of Object.keys(variants)) {
|
|
1056
|
-
for (const variantKey of Object.keys(variants[variantName])) {
|
|
1057
|
-
const variantStyle = variantStyles[variantName]?.[variantKey];
|
|
1058
|
-
if (variantStyle && variantStyle.selectors) {
|
|
1059
|
-
styles.push(variantStyle);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// Run compilation
|
|
1065
|
-
return run(...styles);
|
|
1066
|
-
};
|
|
1067
|
-
|
|
1068
|
-
return pick as Recipe<TVariants>;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* The "Brain": Extracts ChainCSS calls from raw text
|
|
1073
|
-
*/
|
|
1074
|
-
export const scanContent = (text: string): string[] => {
|
|
1075
|
-
// FIXED: Better regex for matching nested parentheses
|
|
1076
|
-
const regex = /(?:chain|\$t?)\(((?:[^()]|\([^()]*\))*)\)(?:\s*\.\s*[a-zA-Z0-9]+\s*\([^)]*\))*/g;
|
|
1077
|
-
const matches = text.match(regex) || [];
|
|
1078
|
-
return matches.map(m => m.replace(/\s+/g, ''));
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* The "Worker": Reads the file, uses the Brain, feeds the Optimizer
|
|
1083
|
-
*/
|
|
1084
|
-
export function scanFileForStyles(
|
|
1085
|
-
filePath: string,
|
|
1086
|
-
optimizer: any,
|
|
1087
|
-
source: string | null = null
|
|
1088
|
-
): { foundCount: number; errors: Error[] } {
|
|
1089
|
-
const errors: Error[] = [];
|
|
1090
|
-
let foundCount = 0;
|
|
1091
|
-
|
|
1092
|
-
try {
|
|
1093
|
-
const content = source !== null ? source : fs.readFileSync(filePath, 'utf8');
|
|
1094
|
-
if (!content || content.trim().length === 0) {
|
|
1095
|
-
return { foundCount: 0, errors };
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// FIXED: Better regex that matches nested parentheses
|
|
1099
|
-
const styleRegex = /(?:chain|\$)\(((?:[^()]|\([^()]*\))*)\)/g;
|
|
1100
|
-
|
|
1101
|
-
let match;
|
|
1102
|
-
|
|
1103
|
-
while ((match = styleRegex.exec(content)) !== null) {
|
|
1104
|
-
try {
|
|
1105
|
-
const styleBody = match[1].trim();
|
|
1106
|
-
|
|
1107
|
-
// Clean up quotes if it's a string inside the parens
|
|
1108
|
-
const cleanBody = styleBody.replace(/^['"`]|['"`]$/g, '');
|
|
1109
|
-
|
|
1110
|
-
if (cleanBody) {
|
|
1111
|
-
// Feed into the optimizer
|
|
1112
|
-
if (optimizer && typeof optimizer.trackStyles === 'function') {
|
|
1113
|
-
optimizer.trackStyles([{ selectors: { '&': cleanBody } }]);
|
|
1114
|
-
}
|
|
1115
|
-
foundCount++;
|
|
1116
|
-
}
|
|
1117
|
-
} catch (parseError) {
|
|
1118
|
-
errors.push(parseError as Error);
|
|
1119
|
-
if (process.env.DEBUG) {
|
|
1120
|
-
console.error(`[Scanner] Parse error in ${filePath}:`, parseError);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (foundCount > 0 && process.env.DEBUG) {
|
|
1126
|
-
console.log(chalk.magenta(`[Scanner] Found ${foundCount} styles in ${filePath}`));
|
|
1127
|
-
}
|
|
1128
|
-
} catch (err) {
|
|
1129
|
-
errors.push(err as Error);
|
|
1130
|
-
if (process.env.DEBUG) {
|
|
1131
|
-
console.error(`[Scanner] Error processing ${filePath}:`, err);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
return { foundCount, errors };
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// Initialize properties (non-blocking)
|
|
1139
367
|
chains.initializeProperties().catch((err: Error) => {
|
|
1140
|
-
if (process.env.DEBUG)
|
|
1141
|
-
console.error('Failed to load CSS properties:', err.message);
|
|
1142
|
-
}
|
|
368
|
+
if (process.env.DEBUG) console.error('Failed to load CSS properties:', err.message);
|
|
1143
369
|
});
|
|
1144
370
|
|
|
1145
|
-
|
|
1146
|
-
export { atomicOptimizer, chains as chainObject };
|
|
371
|
+
export { atomicOptimizer, chains as chainObject };
|