@bookklik/senangstart-css 0.1.0
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 +209 -0
- package/package.json +41 -0
- package/src/cdn/jit.js +503 -0
- package/src/cli/commands/build.js +169 -0
- package/src/cli/commands/dev.js +75 -0
- package/src/cli/commands/init.js +64 -0
- package/src/cli/index.js +36 -0
- package/src/compiler/generators/ai-context.js +128 -0
- package/src/compiler/generators/css.js +344 -0
- package/src/compiler/generators/typescript.js +125 -0
- package/src/compiler/index.js +50 -0
- package/src/compiler/parser.js +67 -0
- package/src/compiler/tokenizer.js +142 -0
- package/src/config/defaults.js +137 -0
- package/src/utils/logger.js +48 -0
- package/templates/senangstart.config.js +35 -0
package/src/cdn/jit.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Browser JIT Runtime
|
|
3
|
+
* Zero-config, browser-based CSS compilation
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <script src="https://cdn.senangstart.dev/jit.js"></script>
|
|
7
|
+
*
|
|
8
|
+
* Or with custom config:
|
|
9
|
+
* <script type="senangstart/config">{ "theme": { "colors": { "brand": "#8B5CF6" } } }</script>
|
|
10
|
+
* <script src="https://cdn.senangstart.dev/jit.js"></script>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
(function() {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// DEFAULT CONFIGURATION
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
const defaultConfig = {
|
|
21
|
+
theme: {
|
|
22
|
+
spacing: {
|
|
23
|
+
'none': '0px',
|
|
24
|
+
'tiny': '4px',
|
|
25
|
+
'small': '8px',
|
|
26
|
+
'medium': '16px',
|
|
27
|
+
'big': '32px',
|
|
28
|
+
'giant': '64px',
|
|
29
|
+
'vast': '128px'
|
|
30
|
+
},
|
|
31
|
+
radius: {
|
|
32
|
+
'none': '0px',
|
|
33
|
+
'small': '4px',
|
|
34
|
+
'medium': '8px',
|
|
35
|
+
'big': '16px',
|
|
36
|
+
'round': '9999px'
|
|
37
|
+
},
|
|
38
|
+
shadow: {
|
|
39
|
+
'none': 'none',
|
|
40
|
+
'small': '0 1px 2px rgba(0,0,0,0.05)',
|
|
41
|
+
'medium': '0 4px 6px rgba(0,0,0,0.1)',
|
|
42
|
+
'big': '0 10px 15px rgba(0,0,0,0.15)',
|
|
43
|
+
'giant': '0 25px 50px rgba(0,0,0,0.25)'
|
|
44
|
+
},
|
|
45
|
+
fontSize: {
|
|
46
|
+
'tiny': '12px',
|
|
47
|
+
'small': '14px',
|
|
48
|
+
'medium': '16px',
|
|
49
|
+
'big': '20px',
|
|
50
|
+
'giant': '32px',
|
|
51
|
+
'vast': '48px'
|
|
52
|
+
},
|
|
53
|
+
fontWeight: {
|
|
54
|
+
'normal': '400',
|
|
55
|
+
'medium': '500',
|
|
56
|
+
'bold': '700'
|
|
57
|
+
},
|
|
58
|
+
screens: {
|
|
59
|
+
'mob': '480px',
|
|
60
|
+
'tab': '768px',
|
|
61
|
+
'lap': '1024px',
|
|
62
|
+
'desk': '1280px'
|
|
63
|
+
},
|
|
64
|
+
colors: {
|
|
65
|
+
'white': '#FFFFFF',
|
|
66
|
+
'black': '#000000',
|
|
67
|
+
'grey': '#6B7280',
|
|
68
|
+
'dark': '#3E4A5D', // Brand dark
|
|
69
|
+
'light': '#DBEAFE', // Brand light/secondary
|
|
70
|
+
'primary': '#2563EB', // Brand primary
|
|
71
|
+
'secondary': '#DBEAFE', // Brand secondary
|
|
72
|
+
'success': '#10B981',
|
|
73
|
+
'warning': '#F59E0B',
|
|
74
|
+
'danger': '#EF4444'
|
|
75
|
+
},
|
|
76
|
+
zIndex: {
|
|
77
|
+
'base': '0',
|
|
78
|
+
'low': '10',
|
|
79
|
+
'mid': '50',
|
|
80
|
+
'high': '100',
|
|
81
|
+
'top': '9999'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ============================================
|
|
87
|
+
// CONFIG LOADER
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
90
|
+
function loadInlineConfig() {
|
|
91
|
+
const configEl = document.querySelector('script[type="senangstart/config"]');
|
|
92
|
+
if (!configEl) return {};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(configEl.textContent);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('[SenangStart] Invalid config JSON:', e);
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeConfig(user) {
|
|
103
|
+
const merged = JSON.parse(JSON.stringify(defaultConfig));
|
|
104
|
+
|
|
105
|
+
if (user.theme) {
|
|
106
|
+
for (const key of Object.keys(merged.theme)) {
|
|
107
|
+
if (user.theme[key]) {
|
|
108
|
+
merged.theme[key] = { ...merged.theme[key], ...user.theme[key] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return merged;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// CSS VARIABLE GENERATOR
|
|
118
|
+
// ============================================
|
|
119
|
+
|
|
120
|
+
function generateCSSVariables(config) {
|
|
121
|
+
const { theme } = config;
|
|
122
|
+
let css = ':root {\n';
|
|
123
|
+
|
|
124
|
+
// Spacing
|
|
125
|
+
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
126
|
+
css += ` --s-${key}: ${value};\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Radius
|
|
130
|
+
for (const [key, value] of Object.entries(theme.radius)) {
|
|
131
|
+
css += ` --r-${key}: ${value};\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Shadow
|
|
135
|
+
for (const [key, value] of Object.entries(theme.shadow)) {
|
|
136
|
+
css += ` --shadow-${key}: ${value};\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Font size
|
|
140
|
+
for (const [key, value] of Object.entries(theme.fontSize)) {
|
|
141
|
+
css += ` --font-${key}: ${value};\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Font weight
|
|
145
|
+
for (const [key, value] of Object.entries(theme.fontWeight)) {
|
|
146
|
+
css += ` --fw-${key}: ${value};\n`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Colors
|
|
150
|
+
for (const [key, value] of Object.entries(theme.colors)) {
|
|
151
|
+
css += ` --c-${key}: ${value};\n`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Z-index
|
|
155
|
+
for (const [key, value] of Object.entries(theme.zIndex)) {
|
|
156
|
+
css += ` --z-${key}: ${value};\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
css += '}\n\n';
|
|
160
|
+
css += '*, *::before, *::after { box-sizing: border-box; }\n\n';
|
|
161
|
+
|
|
162
|
+
return css;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================
|
|
166
|
+
// LAYOUT KEYWORDS
|
|
167
|
+
// ============================================
|
|
168
|
+
|
|
169
|
+
const layoutKeywords = {
|
|
170
|
+
'flex': 'display: flex;',
|
|
171
|
+
'grid': 'display: grid;',
|
|
172
|
+
'block': 'display: block;',
|
|
173
|
+
'inline': 'display: inline-block;',
|
|
174
|
+
'hidden': 'display: none;',
|
|
175
|
+
'row': 'flex-direction: row;',
|
|
176
|
+
'col': 'flex-direction: column;',
|
|
177
|
+
'row-reverse': 'flex-direction: row-reverse;',
|
|
178
|
+
'col-reverse': 'flex-direction: column-reverse;',
|
|
179
|
+
'center': 'justify-content: center; align-items: center;',
|
|
180
|
+
'start': 'justify-content: flex-start; align-items: flex-start;',
|
|
181
|
+
'end': 'justify-content: flex-end; align-items: flex-end;',
|
|
182
|
+
'between': 'justify-content: space-between;',
|
|
183
|
+
'around': 'justify-content: space-around;',
|
|
184
|
+
'evenly': 'justify-content: space-evenly;',
|
|
185
|
+
'wrap': 'flex-wrap: wrap;',
|
|
186
|
+
'nowrap': 'flex-wrap: nowrap;',
|
|
187
|
+
'absolute': 'position: absolute;',
|
|
188
|
+
'relative': 'position: relative;',
|
|
189
|
+
'fixed': 'position: fixed;',
|
|
190
|
+
'sticky': 'position: sticky;'
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ============================================
|
|
194
|
+
// RULE GENERATORS
|
|
195
|
+
// ============================================
|
|
196
|
+
|
|
197
|
+
const breakpoints = ['mob', 'tab', 'lap', 'desk'];
|
|
198
|
+
const states = ['hover', 'focus', 'active', 'disabled'];
|
|
199
|
+
|
|
200
|
+
function parseToken(raw) {
|
|
201
|
+
const token = {
|
|
202
|
+
raw,
|
|
203
|
+
breakpoint: null,
|
|
204
|
+
state: null,
|
|
205
|
+
property: null,
|
|
206
|
+
value: null,
|
|
207
|
+
isArbitrary: false
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const parts = raw.split(':');
|
|
211
|
+
let idx = 0;
|
|
212
|
+
|
|
213
|
+
// Check for breakpoint
|
|
214
|
+
if (breakpoints.includes(parts[0])) {
|
|
215
|
+
token.breakpoint = parts[0];
|
|
216
|
+
idx++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check for state
|
|
220
|
+
if (states.includes(parts[idx])) {
|
|
221
|
+
token.state = parts[idx];
|
|
222
|
+
idx++;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Property
|
|
226
|
+
if (idx < parts.length) {
|
|
227
|
+
token.property = parts[idx];
|
|
228
|
+
idx++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Value
|
|
232
|
+
if (idx < parts.length) {
|
|
233
|
+
let value = parts.slice(idx).join(':');
|
|
234
|
+
const arbitraryMatch = value.match(/^\[(.+)\]$/);
|
|
235
|
+
if (arbitraryMatch) {
|
|
236
|
+
token.value = arbitraryMatch[1].replace(/_/g, ' ');
|
|
237
|
+
token.isArbitrary = true;
|
|
238
|
+
} else {
|
|
239
|
+
token.value = value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return token;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function generateLayoutRule(token) {
|
|
247
|
+
const { property, value } = token;
|
|
248
|
+
|
|
249
|
+
// Z-index
|
|
250
|
+
if (property === 'z') {
|
|
251
|
+
return `z-index: var(--z-${value});`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Overflow
|
|
255
|
+
if (property === 'overflow') {
|
|
256
|
+
return `overflow: ${value};`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return layoutKeywords[property] || '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function generateSpaceRule(token) {
|
|
263
|
+
const { property, value, isArbitrary } = token;
|
|
264
|
+
|
|
265
|
+
if (value === 'auto') {
|
|
266
|
+
const autoMap = {
|
|
267
|
+
'm': 'margin: auto;',
|
|
268
|
+
'm-x': 'margin-left: auto; margin-right: auto;',
|
|
269
|
+
'm-y': 'margin-top: auto; margin-bottom: auto;',
|
|
270
|
+
'm-t': 'margin-top: auto;',
|
|
271
|
+
'm-r': 'margin-right: auto;',
|
|
272
|
+
'm-b': 'margin-bottom: auto;',
|
|
273
|
+
'm-l': 'margin-left: auto;'
|
|
274
|
+
};
|
|
275
|
+
return autoMap[property] || '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const cssValue = isArbitrary ? value : `var(--s-${value})`;
|
|
279
|
+
|
|
280
|
+
const map = {
|
|
281
|
+
'p': `padding: ${cssValue};`,
|
|
282
|
+
'p-t': `padding-top: ${cssValue};`,
|
|
283
|
+
'p-r': `padding-right: ${cssValue};`,
|
|
284
|
+
'p-b': `padding-bottom: ${cssValue};`,
|
|
285
|
+
'p-l': `padding-left: ${cssValue};`,
|
|
286
|
+
'p-x': `padding-left: ${cssValue}; padding-right: ${cssValue};`,
|
|
287
|
+
'p-y': `padding-top: ${cssValue}; padding-bottom: ${cssValue};`,
|
|
288
|
+
'm': `margin: ${cssValue};`,
|
|
289
|
+
'm-t': `margin-top: ${cssValue};`,
|
|
290
|
+
'm-r': `margin-right: ${cssValue};`,
|
|
291
|
+
'm-b': `margin-bottom: ${cssValue};`,
|
|
292
|
+
'm-l': `margin-left: ${cssValue};`,
|
|
293
|
+
'm-x': `margin-left: ${cssValue}; margin-right: ${cssValue};`,
|
|
294
|
+
'm-y': `margin-top: ${cssValue}; margin-bottom: ${cssValue};`,
|
|
295
|
+
'g': `gap: ${cssValue};`,
|
|
296
|
+
'g-x': `column-gap: ${cssValue};`,
|
|
297
|
+
'g-y': `row-gap: ${cssValue};`,
|
|
298
|
+
'w': `width: ${cssValue};`,
|
|
299
|
+
'h': `height: ${cssValue};`,
|
|
300
|
+
'min-w': `min-width: ${cssValue};`,
|
|
301
|
+
'max-w': `max-width: ${cssValue};`,
|
|
302
|
+
'min-h': `min-height: ${cssValue};`,
|
|
303
|
+
'max-h': `max-height: ${cssValue};`
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return map[property] || '';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function generateVisualRule(token) {
|
|
310
|
+
const { property, value, isArbitrary } = token;
|
|
311
|
+
|
|
312
|
+
const rules = {
|
|
313
|
+
'bg': () => {
|
|
314
|
+
const cssValue = isArbitrary ? value : `var(--c-${value})`;
|
|
315
|
+
return `background-color: ${cssValue};`;
|
|
316
|
+
},
|
|
317
|
+
'text': () => {
|
|
318
|
+
if (['left', 'center', 'right'].includes(value)) {
|
|
319
|
+
return `text-align: ${value};`;
|
|
320
|
+
}
|
|
321
|
+
const cssValue = isArbitrary ? value : `var(--c-${value})`;
|
|
322
|
+
return `color: ${cssValue};`;
|
|
323
|
+
},
|
|
324
|
+
'text-size': () => {
|
|
325
|
+
const cssValue = isArbitrary ? value : `var(--font-${value})`;
|
|
326
|
+
return `font-size: ${cssValue};`;
|
|
327
|
+
},
|
|
328
|
+
'font': () => `font-weight: var(--fw-${value});`,
|
|
329
|
+
'border': () => {
|
|
330
|
+
const cssValue = isArbitrary ? value : `var(--c-${value})`;
|
|
331
|
+
return `border-color: ${cssValue}; border-style: solid;`;
|
|
332
|
+
},
|
|
333
|
+
'border-w': () => {
|
|
334
|
+
const cssValue = isArbitrary ? value : `var(--s-${value})`;
|
|
335
|
+
return `border-width: ${cssValue}; border-style: solid;`;
|
|
336
|
+
},
|
|
337
|
+
'rounded': () => `border-radius: var(--r-${value});`,
|
|
338
|
+
'shadow': () => `box-shadow: var(--shadow-${value});`,
|
|
339
|
+
'opacity': () => `opacity: ${value};`
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const gen = rules[property];
|
|
343
|
+
return gen ? gen() : '';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function generateRule(raw, attrType) {
|
|
347
|
+
// Handle simple layout keywords
|
|
348
|
+
if (attrType === 'layout' && layoutKeywords[raw]) {
|
|
349
|
+
return `[layout~="${raw}"] { ${layoutKeywords[raw]} }\n`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const token = parseToken(raw);
|
|
353
|
+
let cssDeclaration = '';
|
|
354
|
+
|
|
355
|
+
switch (attrType) {
|
|
356
|
+
case 'layout':
|
|
357
|
+
cssDeclaration = generateLayoutRule(token);
|
|
358
|
+
break;
|
|
359
|
+
case 'space':
|
|
360
|
+
cssDeclaration = generateSpaceRule(token);
|
|
361
|
+
break;
|
|
362
|
+
case 'visual':
|
|
363
|
+
cssDeclaration = generateVisualRule(token);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!cssDeclaration) return '';
|
|
368
|
+
|
|
369
|
+
let selector = `[${attrType}~="${raw}"]`;
|
|
370
|
+
if (token.state) {
|
|
371
|
+
selector += `:${token.state}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return `${selector} { ${cssDeclaration} }\n`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================
|
|
378
|
+
// DOM SCANNER
|
|
379
|
+
// ============================================
|
|
380
|
+
|
|
381
|
+
function scanDOM() {
|
|
382
|
+
const tokens = {
|
|
383
|
+
layout: new Set(),
|
|
384
|
+
space: new Set(),
|
|
385
|
+
visual: new Set()
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const elements = document.querySelectorAll('[layout], [space], [visual]');
|
|
389
|
+
|
|
390
|
+
elements.forEach(el => {
|
|
391
|
+
['layout', 'space', 'visual'].forEach(attr => {
|
|
392
|
+
const value = el.getAttribute(attr);
|
|
393
|
+
if (value) {
|
|
394
|
+
value.split(/\s+/).forEach(token => {
|
|
395
|
+
if (token) tokens[attr].add(token);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return tokens;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================
|
|
405
|
+
// CSS COMPILER
|
|
406
|
+
// ============================================
|
|
407
|
+
|
|
408
|
+
function compileCSS(tokens, config) {
|
|
409
|
+
let css = generateCSSVariables(config);
|
|
410
|
+
|
|
411
|
+
const baseRules = [];
|
|
412
|
+
const mediaRules = {
|
|
413
|
+
mob: [],
|
|
414
|
+
tab: [],
|
|
415
|
+
lap: [],
|
|
416
|
+
desk: []
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
for (const [attrType, values] of Object.entries(tokens)) {
|
|
420
|
+
for (const raw of values) {
|
|
421
|
+
const rule = generateRule(raw, attrType);
|
|
422
|
+
if (rule) {
|
|
423
|
+
// Check for breakpoint prefix
|
|
424
|
+
const bpMatch = raw.match(/^(mob|tab|lap|desk):/);
|
|
425
|
+
if (bpMatch) {
|
|
426
|
+
mediaRules[bpMatch[1]].push(rule);
|
|
427
|
+
} else {
|
|
428
|
+
baseRules.push(rule);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Add base rules
|
|
435
|
+
css += baseRules.join('');
|
|
436
|
+
|
|
437
|
+
// Add media queries
|
|
438
|
+
const { screens } = config.theme;
|
|
439
|
+
for (const [bp, rules] of Object.entries(mediaRules)) {
|
|
440
|
+
if (rules.length > 0) {
|
|
441
|
+
css += `\n@media (min-width: ${screens[bp]}) {\n`;
|
|
442
|
+
css += rules.map(r => ' ' + r).join('');
|
|
443
|
+
css += '}\n';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return css;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================
|
|
451
|
+
// STYLE INJECTION
|
|
452
|
+
// ============================================
|
|
453
|
+
|
|
454
|
+
function injectStyles(css) {
|
|
455
|
+
let styleEl = document.getElementById('senangstart-jit');
|
|
456
|
+
if (!styleEl) {
|
|
457
|
+
styleEl = document.createElement('style');
|
|
458
|
+
styleEl.id = 'senangstart-jit';
|
|
459
|
+
document.head.appendChild(styleEl);
|
|
460
|
+
}
|
|
461
|
+
styleEl.textContent = css;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ============================================
|
|
465
|
+
// INITIALIZATION
|
|
466
|
+
// ============================================
|
|
467
|
+
|
|
468
|
+
function init() {
|
|
469
|
+
const userConfig = loadInlineConfig();
|
|
470
|
+
const config = mergeConfig(userConfig);
|
|
471
|
+
|
|
472
|
+
const tokens = scanDOM();
|
|
473
|
+
const css = compileCSS(tokens, config);
|
|
474
|
+
injectStyles(css);
|
|
475
|
+
|
|
476
|
+
// Watch for DOM changes
|
|
477
|
+
const observer = new MutationObserver(() => {
|
|
478
|
+
const newTokens = scanDOM();
|
|
479
|
+
const newCSS = compileCSS(newTokens, config);
|
|
480
|
+
injectStyles(newCSS);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
observer.observe(document.body, {
|
|
484
|
+
childList: true,
|
|
485
|
+
subtree: true,
|
|
486
|
+
attributes: true,
|
|
487
|
+
attributeFilter: ['layout', 'space', 'visual']
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
console.log('%c[SenangStart CSS]%c JIT runtime initialized ✓',
|
|
491
|
+
'color: #2563EB; font-weight: bold;',
|
|
492
|
+
'color: #10B981;'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Run on DOMContentLoaded or immediately if already loaded
|
|
497
|
+
if (document.readyState === 'loading') {
|
|
498
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
499
|
+
} else {
|
|
500
|
+
init();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
})();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Build Command
|
|
3
|
+
* One-time compilation of CSS from source files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { defaultConfig, mergeConfig } from '../../config/defaults.js';
|
|
9
|
+
import { parseSource } from '../../compiler/parser.js';
|
|
10
|
+
import { tokenizeAll } from '../../compiler/tokenizer.js';
|
|
11
|
+
import { generateCSS, minifyCSS } from '../../compiler/generators/css.js';
|
|
12
|
+
import { generateAIContext } from '../../compiler/generators/ai-context.js';
|
|
13
|
+
import { generateTypeScript } from '../../compiler/generators/typescript.js';
|
|
14
|
+
import logger from '../../utils/logger.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find files matching content patterns
|
|
18
|
+
*/
|
|
19
|
+
function findFiles(patterns) {
|
|
20
|
+
const allFiles = [];
|
|
21
|
+
const extensions = ['html', 'htm', 'jsx', 'tsx', 'vue', 'svelte'];
|
|
22
|
+
|
|
23
|
+
function walk(dir) {
|
|
24
|
+
try {
|
|
25
|
+
const entries = readdirSync(dir);
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const fullPath = join(dir, entry);
|
|
28
|
+
try {
|
|
29
|
+
const stat = statSync(fullPath);
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
if (!entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist') {
|
|
32
|
+
walk(fullPath);
|
|
33
|
+
}
|
|
34
|
+
} else if (stat.isFile()) {
|
|
35
|
+
const ext = entry.split('.').pop().toLowerCase();
|
|
36
|
+
if (extensions.includes(ext)) {
|
|
37
|
+
allFiles.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Skip
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Skip
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
walk(process.cwd());
|
|
50
|
+
return allFiles;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load user config
|
|
55
|
+
*/
|
|
56
|
+
async function loadConfig(configPath) {
|
|
57
|
+
const fullPath = join(process.cwd(), configPath);
|
|
58
|
+
|
|
59
|
+
if (!existsSync(fullPath)) {
|
|
60
|
+
logger.warn('No config file found, using defaults');
|
|
61
|
+
return defaultConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const userConfig = await import('file://' + fullPath);
|
|
66
|
+
return mergeConfig(userConfig.default || userConfig);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logger.error(`Failed to load config: ${e.message}`);
|
|
69
|
+
return defaultConfig;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ensure directory exists
|
|
75
|
+
*/
|
|
76
|
+
function ensureDir(filePath) {
|
|
77
|
+
const dir = dirname(filePath);
|
|
78
|
+
if (!existsSync(dir)) {
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build command handler
|
|
85
|
+
*/
|
|
86
|
+
export async function build(options = {}) {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
|
|
89
|
+
logger.build('Starting build...');
|
|
90
|
+
|
|
91
|
+
// Load config
|
|
92
|
+
const config = await loadConfig(options.config || 'senangstart.config.js');
|
|
93
|
+
|
|
94
|
+
// Override minify if specified
|
|
95
|
+
if (options.minify) {
|
|
96
|
+
config.output.minify = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Find source files
|
|
100
|
+
const files = await findFiles(config.content);
|
|
101
|
+
|
|
102
|
+
if (files.length === 0) {
|
|
103
|
+
logger.warn('No source files found');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.info(`Found ${files.length} source files`);
|
|
108
|
+
|
|
109
|
+
// Parse all files
|
|
110
|
+
const allTokens = {
|
|
111
|
+
layout: new Set(),
|
|
112
|
+
space: new Set(),
|
|
113
|
+
visual: new Set()
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const filePath of files) {
|
|
117
|
+
try {
|
|
118
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
119
|
+
const parsed = parseSource(content);
|
|
120
|
+
|
|
121
|
+
parsed.layout.forEach(t => allTokens.layout.add(t));
|
|
122
|
+
parsed.space.forEach(t => allTokens.space.add(t));
|
|
123
|
+
parsed.visual.forEach(t => allTokens.visual.add(t));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
logger.warn(`Could not parse ${filePath}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Tokenize
|
|
130
|
+
const tokens = tokenizeAll(allTokens);
|
|
131
|
+
|
|
132
|
+
logger.info(`Extracted ${tokens.length} unique tokens`);
|
|
133
|
+
|
|
134
|
+
// Generate CSS
|
|
135
|
+
let css = generateCSS(tokens, config);
|
|
136
|
+
|
|
137
|
+
if (config.output.minify) {
|
|
138
|
+
css = minifyCSS(css);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Write CSS
|
|
142
|
+
const cssPath = join(process.cwd(), config.output.css);
|
|
143
|
+
ensureDir(cssPath);
|
|
144
|
+
writeFileSync(cssPath, css);
|
|
145
|
+
logger.success(`Generated ${config.output.css}`);
|
|
146
|
+
|
|
147
|
+
// Generate AI context
|
|
148
|
+
if (config.output.aiContext) {
|
|
149
|
+
const aiContext = generateAIContext(config);
|
|
150
|
+
const aiPath = join(process.cwd(), config.output.aiContext);
|
|
151
|
+
ensureDir(aiPath);
|
|
152
|
+
writeFileSync(aiPath, aiContext);
|
|
153
|
+
logger.success(`Generated ${config.output.aiContext}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Generate TypeScript definitions
|
|
157
|
+
if (config.output.typescript) {
|
|
158
|
+
const tsTypes = generateTypeScript(config);
|
|
159
|
+
const tsPath = join(process.cwd(), config.output.typescript);
|
|
160
|
+
ensureDir(tsPath);
|
|
161
|
+
writeFileSync(tsPath, tsTypes);
|
|
162
|
+
logger.success(`Generated ${config.output.typescript}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const elapsed = Date.now() - startTime;
|
|
166
|
+
logger.build(`Build completed in ${elapsed}ms`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default build;
|