@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/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;