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