@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Dev Command
|
|
3
|
+
* Watch mode with live compilation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chokidar from 'chokidar';
|
|
7
|
+
import { build } from './build.js';
|
|
8
|
+
import logger from '../../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Dev command handler - watches files and rebuilds on changes
|
|
12
|
+
*/
|
|
13
|
+
export async function dev(options = {}) {
|
|
14
|
+
logger.watch('Starting development mode...');
|
|
15
|
+
|
|
16
|
+
// Initial build
|
|
17
|
+
await build(options);
|
|
18
|
+
|
|
19
|
+
// Watch patterns
|
|
20
|
+
const watchPatterns = [
|
|
21
|
+
'./**/*.html',
|
|
22
|
+
'./**/*.htm',
|
|
23
|
+
'./src/**/*.{html,jsx,tsx,vue,svelte}',
|
|
24
|
+
'./pages/**/*.{html,jsx,tsx}',
|
|
25
|
+
'./components/**/*.{html,jsx,tsx,vue,svelte}'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Ignore patterns
|
|
29
|
+
const ignorePatterns = [
|
|
30
|
+
'**/node_modules/**',
|
|
31
|
+
'**/dist/**',
|
|
32
|
+
'**/.git/**',
|
|
33
|
+
'**/public/**'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Create watcher
|
|
37
|
+
const watcher = chokidar.watch(watchPatterns, {
|
|
38
|
+
ignored: ignorePatterns,
|
|
39
|
+
persistent: true,
|
|
40
|
+
ignoreInitial: true
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Debounce timer
|
|
44
|
+
let debounceTimer = null;
|
|
45
|
+
|
|
46
|
+
function debouncedBuild() {
|
|
47
|
+
if (debounceTimer) {
|
|
48
|
+
clearTimeout(debounceTimer);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
debounceTimer = setTimeout(async () => {
|
|
52
|
+
logger.watch('Change detected, rebuilding...');
|
|
53
|
+
await build(options);
|
|
54
|
+
}, 100);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Watch events
|
|
58
|
+
watcher
|
|
59
|
+
.on('change', (path) => {
|
|
60
|
+
logger.info(`Changed: ${path}`);
|
|
61
|
+
debouncedBuild();
|
|
62
|
+
})
|
|
63
|
+
.on('add', (path) => {
|
|
64
|
+
logger.info(`Added: ${path}`);
|
|
65
|
+
debouncedBuild();
|
|
66
|
+
})
|
|
67
|
+
.on('unlink', (path) => {
|
|
68
|
+
logger.info(`Removed: ${path}`);
|
|
69
|
+
debouncedBuild();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
logger.watch('Watching for changes... (Ctrl+C to stop)');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default dev;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Init Command
|
|
3
|
+
* Creates senangstart.config.js in the project root
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import logger from '../../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
export async function init() {
|
|
15
|
+
const configPath = join(process.cwd(), 'senangstart.config.js');
|
|
16
|
+
|
|
17
|
+
// Check if config already exists
|
|
18
|
+
if (existsSync(configPath)) {
|
|
19
|
+
logger.warn('senangstart.config.js already exists');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Read template
|
|
24
|
+
const templatePath = join(__dirname, '../../../templates/senangstart.config.js');
|
|
25
|
+
let template;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
template = readFileSync(templatePath, 'utf-8');
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// Fallback template if file not found
|
|
31
|
+
template = `/**
|
|
32
|
+
* SenangStart CSS Configuration
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
content: [
|
|
37
|
+
'./**/*.html',
|
|
38
|
+
'./src/**/*.{html,jsx,tsx,vue,svelte}',
|
|
39
|
+
'./components/**/*.{html,jsx,tsx}'
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
output: {
|
|
43
|
+
css: './public/senangstart.css',
|
|
44
|
+
minify: true,
|
|
45
|
+
aiContext: './.cursorrules',
|
|
46
|
+
typescript: './types/senang.d.ts'
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
theme: {
|
|
50
|
+
// Add custom spacing, colors, etc.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Write config file
|
|
57
|
+
writeFileSync(configPath, template);
|
|
58
|
+
|
|
59
|
+
logger.success('Created senangstart.config.js');
|
|
60
|
+
logger.info('Edit this file to customize your theme');
|
|
61
|
+
logger.info('Run "senangstart dev" to start watching files');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default init;
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SenangStart CSS - CLI Entry Point
|
|
5
|
+
* The Intent-First CSS Engine
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { program } from 'commander';
|
|
9
|
+
import { init } from './commands/init.js';
|
|
10
|
+
import { build } from './commands/build.js';
|
|
11
|
+
import { dev } from './commands/dev.js';
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('senangstart')
|
|
15
|
+
.description('SenangStart CSS - The Intent-First CSS Engine\n\n "Speak Human. Compile to Logic."')
|
|
16
|
+
.version('1.0.0');
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('init')
|
|
20
|
+
.description('Initialize a new senangstart.config.js')
|
|
21
|
+
.action(init);
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('build')
|
|
25
|
+
.description('Compile CSS from source files')
|
|
26
|
+
.option('--minify', 'Minify CSS output')
|
|
27
|
+
.option('--config <path>', 'Path to config file', 'senangstart.config.js')
|
|
28
|
+
.action(build);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('dev')
|
|
32
|
+
.description('Watch mode with live compilation')
|
|
33
|
+
.option('--config <path>', 'Path to config file', 'senangstart.config.js')
|
|
34
|
+
.action(dev);
|
|
35
|
+
|
|
36
|
+
program.parse();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - AI Context Generator
|
|
3
|
+
* Generates .cursorrules or ai-context.md for LLM assistance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate AI context file content
|
|
8
|
+
* @param {Object} config - Configuration object
|
|
9
|
+
* @returns {string} - AI context markdown content
|
|
10
|
+
*/
|
|
11
|
+
export function generateAIContext(config) {
|
|
12
|
+
const { theme } = config;
|
|
13
|
+
|
|
14
|
+
return `# SenangStart CSS - AI Context Guide
|
|
15
|
+
|
|
16
|
+
> This file helps AI assistants understand the SenangStart CSS framework.
|
|
17
|
+
|
|
18
|
+
## Philosophy
|
|
19
|
+
"Speak Human. Compile to Logic."
|
|
20
|
+
Use natural adjectives instead of arbitrary numbers.
|
|
21
|
+
|
|
22
|
+
## Tri-Attribute Syntax
|
|
23
|
+
|
|
24
|
+
| Attribute | Purpose | Example |
|
|
25
|
+
|-----------|---------|---------|
|
|
26
|
+
| \`layout\` | Structure & position | \`layout="flex col center"\` |
|
|
27
|
+
| \`space\` | Sizing & spacing | \`space="p:medium g:small"\` |
|
|
28
|
+
| \`visual\` | Colors & appearance | \`visual="bg:white rounded:big"\` |
|
|
29
|
+
|
|
30
|
+
## Spacing Scale (Natural Objects)
|
|
31
|
+
|
|
32
|
+
| Keyword | Size | Mental Model |
|
|
33
|
+
|---------|------|--------------|
|
|
34
|
+
| \`none\` | ${theme.spacing.none} | No space |
|
|
35
|
+
| \`tiny\` | ${theme.spacing.tiny} | Pebble - Borders, offsets |
|
|
36
|
+
| \`small\` | ${theme.spacing.small} | Matchbox - Group related items |
|
|
37
|
+
| \`medium\` | ${theme.spacing.medium} | Smartphone - Standard default |
|
|
38
|
+
| \`big\` | ${theme.spacing.big} | Laptop - Separate sections |
|
|
39
|
+
| \`giant\` | ${theme.spacing.giant} | Door - Layout divisions |
|
|
40
|
+
| \`vast\` | ${theme.spacing.vast} | House - Hero sections |
|
|
41
|
+
|
|
42
|
+
## Natural Language Mapping
|
|
43
|
+
|
|
44
|
+
When users say... | Do this...
|
|
45
|
+
------------------|------------
|
|
46
|
+
"tighten it up" | Scale DOWN (medium → small → tiny)
|
|
47
|
+
"give it air" / "breathe" | Scale UP (medium → big → giant)
|
|
48
|
+
"make it pop" | Add \`shadow:big\`, increase \`rounded\`
|
|
49
|
+
"subtle" | Use \`shadow:small\`, \`rounded:small\`
|
|
50
|
+
"compact" | Use \`p:small g:tiny\`
|
|
51
|
+
"spacious" | Use \`p:big g:medium\` or larger
|
|
52
|
+
|
|
53
|
+
## Layout Keywords
|
|
54
|
+
|
|
55
|
+
- **Display:** \`flex\`, \`grid\`, \`block\`, \`hidden\`
|
|
56
|
+
- **Direction:** \`row\`, \`col\`, \`row-reverse\`, \`col-reverse\`
|
|
57
|
+
- **Alignment:** \`center\`, \`between\`, \`start\`, \`end\`
|
|
58
|
+
- **Position:** \`absolute\`, \`relative\`, \`fixed\`, \`sticky\`
|
|
59
|
+
- **Z-Index:** \`z:base\`, \`z:low\`, \`z:mid\`, \`z:high\`, \`z:top\`
|
|
60
|
+
|
|
61
|
+
## Space Syntax
|
|
62
|
+
|
|
63
|
+
Pattern: \`[breakpoint]:[property]:[scale]\`
|
|
64
|
+
|
|
65
|
+
- \`p:medium\` → padding: 16px
|
|
66
|
+
- \`m-b:big\` → margin-bottom: 32px
|
|
67
|
+
- \`g:small\` → gap: 8px
|
|
68
|
+
- \`tab:p:big\` → padding: 32px on tablet+
|
|
69
|
+
- \`w:[350px]\` → width: 350px (escape hatch)
|
|
70
|
+
|
|
71
|
+
## Visual Keywords
|
|
72
|
+
|
|
73
|
+
- **Background:** \`bg:white\`, \`bg:primary\`, \`bg:[#hex]\`
|
|
74
|
+
- **Text:** \`text:dark\`, \`text:grey\`, \`text-size:big\`
|
|
75
|
+
- **Font:** \`font:bold\`, \`font:medium\`, \`font:normal\`
|
|
76
|
+
- **Rounded:** \`rounded:small\`, \`rounded:medium\`, \`rounded:big\`, \`rounded:round\`
|
|
77
|
+
- **Shadow:** \`shadow:small\`, \`shadow:medium\`, \`shadow:big\`
|
|
78
|
+
|
|
79
|
+
## Responsive Prefixes
|
|
80
|
+
|
|
81
|
+
| Prefix | Breakpoint |
|
|
82
|
+
|--------|------------|
|
|
83
|
+
| \`mob:\` | ${theme.screens.mob}+ |
|
|
84
|
+
| \`tab:\` | ${theme.screens.tab}+ |
|
|
85
|
+
| \`lap:\` | ${theme.screens.lap}+ |
|
|
86
|
+
| \`desk:\` | ${theme.screens.desk}+ |
|
|
87
|
+
|
|
88
|
+
## Color Palette
|
|
89
|
+
|
|
90
|
+
${Object.entries(theme.colors).map(([key, value]) => `- \`${key}\`: ${value}`).join('\n')}
|
|
91
|
+
|
|
92
|
+
## Common Patterns
|
|
93
|
+
|
|
94
|
+
### Card
|
|
95
|
+
\`\`\`html
|
|
96
|
+
<div
|
|
97
|
+
layout="flex col"
|
|
98
|
+
space="p:medium"
|
|
99
|
+
visual="bg:white rounded:big shadow:medium"
|
|
100
|
+
>
|
|
101
|
+
Content
|
|
102
|
+
</div>
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
### Centered Container
|
|
106
|
+
\`\`\`html
|
|
107
|
+
<div space="max-w:[1200px] m-x:auto p-x:medium">
|
|
108
|
+
Content
|
|
109
|
+
</div>
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
### Navigation
|
|
113
|
+
\`\`\`html
|
|
114
|
+
<nav
|
|
115
|
+
layout="flex between center fixed z:top"
|
|
116
|
+
space="w:[100%] p-x:big p-y:small"
|
|
117
|
+
visual="bg:white shadow:small"
|
|
118
|
+
>
|
|
119
|
+
Navigation items
|
|
120
|
+
</nav>
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
*Generated by SenangStart CSS*
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default { generateAIContext };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - CSS Generator
|
|
3
|
+
* Generates CSS from tokens using attribute selectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate CSS custom properties from config
|
|
8
|
+
* @param {Object} config - Configuration object
|
|
9
|
+
* @returns {string} - CSS custom properties block
|
|
10
|
+
*/
|
|
11
|
+
export function generateCSSVariables(config) {
|
|
12
|
+
const { theme } = config;
|
|
13
|
+
let css = ':root {\n';
|
|
14
|
+
|
|
15
|
+
// Spacing variables
|
|
16
|
+
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
17
|
+
css += ` --s-${key}: ${value};\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Radius variables
|
|
21
|
+
for (const [key, value] of Object.entries(theme.radius)) {
|
|
22
|
+
css += ` --r-${key}: ${value};\n`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Shadow variables
|
|
26
|
+
for (const [key, value] of Object.entries(theme.shadow)) {
|
|
27
|
+
css += ` --shadow-${key}: ${value};\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Font size variables
|
|
31
|
+
for (const [key, value] of Object.entries(theme.fontSize)) {
|
|
32
|
+
css += ` --font-${key}: ${value};\n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Font weight variables
|
|
36
|
+
for (const [key, value] of Object.entries(theme.fontWeight)) {
|
|
37
|
+
css += ` --fw-${key}: ${value};\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Color variables
|
|
41
|
+
for (const [key, value] of Object.entries(theme.colors)) {
|
|
42
|
+
css += ` --c-${key}: ${value};\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Z-index variables
|
|
46
|
+
for (const [key, value] of Object.entries(theme.zIndex)) {
|
|
47
|
+
css += ` --z-${key}: ${value};\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
css += '}\n\n';
|
|
51
|
+
return css;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate CSS rule for a layout token
|
|
56
|
+
*/
|
|
57
|
+
function generateLayoutRule(token, config) {
|
|
58
|
+
const { property, value } = token;
|
|
59
|
+
|
|
60
|
+
const layoutMap = {
|
|
61
|
+
// Display
|
|
62
|
+
'flex': 'display: flex;',
|
|
63
|
+
'grid': 'display: grid;',
|
|
64
|
+
'block': 'display: block;',
|
|
65
|
+
'inline': 'display: inline-block;',
|
|
66
|
+
'hidden': 'display: none;',
|
|
67
|
+
|
|
68
|
+
// Flex direction
|
|
69
|
+
'row': 'flex-direction: row;',
|
|
70
|
+
'col': 'flex-direction: column;',
|
|
71
|
+
'row-reverse': 'flex-direction: row-reverse;',
|
|
72
|
+
'col-reverse': 'flex-direction: column-reverse;',
|
|
73
|
+
|
|
74
|
+
// Alignment
|
|
75
|
+
'center': 'justify-content: center; align-items: center;',
|
|
76
|
+
'start': 'justify-content: flex-start; align-items: flex-start;',
|
|
77
|
+
'end': 'justify-content: flex-end; align-items: flex-end;',
|
|
78
|
+
'between': 'justify-content: space-between;',
|
|
79
|
+
'around': 'justify-content: space-around;',
|
|
80
|
+
'evenly': 'justify-content: space-evenly;',
|
|
81
|
+
|
|
82
|
+
// Wrap
|
|
83
|
+
'wrap': 'flex-wrap: wrap;',
|
|
84
|
+
'nowrap': 'flex-wrap: nowrap;',
|
|
85
|
+
|
|
86
|
+
// Position
|
|
87
|
+
'absolute': 'position: absolute;',
|
|
88
|
+
'relative': 'position: relative;',
|
|
89
|
+
'fixed': 'position: fixed;',
|
|
90
|
+
'sticky': 'position: sticky;'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Z-index
|
|
94
|
+
if (property === 'z') {
|
|
95
|
+
return `z-index: var(--z-${value});`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Overflow
|
|
99
|
+
if (property === 'overflow') {
|
|
100
|
+
return `overflow: ${value};`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return layoutMap[property] || '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate CSS rule for a space token
|
|
108
|
+
*/
|
|
109
|
+
function generateSpaceRule(token, config) {
|
|
110
|
+
const { property, value, isArbitrary } = token;
|
|
111
|
+
|
|
112
|
+
// Determine the CSS value
|
|
113
|
+
const cssValue = isArbitrary ? value : `var(--s-${value})`;
|
|
114
|
+
|
|
115
|
+
// Handle special values
|
|
116
|
+
if (value === 'auto') {
|
|
117
|
+
const autoValue = 'auto';
|
|
118
|
+
|
|
119
|
+
const propertyMap = {
|
|
120
|
+
'm': `margin: ${autoValue};`,
|
|
121
|
+
'm-x': `margin-left: ${autoValue}; margin-right: ${autoValue};`,
|
|
122
|
+
'm-y': `margin-top: ${autoValue}; margin-bottom: ${autoValue};`,
|
|
123
|
+
'm-t': `margin-top: ${autoValue};`,
|
|
124
|
+
'm-r': `margin-right: ${autoValue};`,
|
|
125
|
+
'm-b': `margin-bottom: ${autoValue};`,
|
|
126
|
+
'm-l': `margin-left: ${autoValue};`
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return propertyMap[property] || '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const propertyMap = {
|
|
133
|
+
// Padding
|
|
134
|
+
'p': `padding: ${cssValue};`,
|
|
135
|
+
'p-t': `padding-top: ${cssValue};`,
|
|
136
|
+
'p-r': `padding-right: ${cssValue};`,
|
|
137
|
+
'p-b': `padding-bottom: ${cssValue};`,
|
|
138
|
+
'p-l': `padding-left: ${cssValue};`,
|
|
139
|
+
'p-x': `padding-left: ${cssValue}; padding-right: ${cssValue};`,
|
|
140
|
+
'p-y': `padding-top: ${cssValue}; padding-bottom: ${cssValue};`,
|
|
141
|
+
|
|
142
|
+
// Margin
|
|
143
|
+
'm': `margin: ${cssValue};`,
|
|
144
|
+
'm-t': `margin-top: ${cssValue};`,
|
|
145
|
+
'm-r': `margin-right: ${cssValue};`,
|
|
146
|
+
'm-b': `margin-bottom: ${cssValue};`,
|
|
147
|
+
'm-l': `margin-left: ${cssValue};`,
|
|
148
|
+
'm-x': `margin-left: ${cssValue}; margin-right: ${cssValue};`,
|
|
149
|
+
'm-y': `margin-top: ${cssValue}; margin-bottom: ${cssValue};`,
|
|
150
|
+
|
|
151
|
+
// Gap
|
|
152
|
+
'g': `gap: ${cssValue};`,
|
|
153
|
+
'g-x': `column-gap: ${cssValue};`,
|
|
154
|
+
'g-y': `row-gap: ${cssValue};`,
|
|
155
|
+
|
|
156
|
+
// Sizing
|
|
157
|
+
'w': `width: ${cssValue};`,
|
|
158
|
+
'h': `height: ${cssValue};`,
|
|
159
|
+
'min-w': `min-width: ${cssValue};`,
|
|
160
|
+
'max-w': `max-width: ${cssValue};`,
|
|
161
|
+
'min-h': `min-height: ${cssValue};`,
|
|
162
|
+
'max-h': `max-height: ${cssValue};`
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return propertyMap[property] || '';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate CSS rule for a visual token
|
|
170
|
+
*/
|
|
171
|
+
function generateVisualRule(token, config) {
|
|
172
|
+
const { property, value, isArbitrary } = token;
|
|
173
|
+
|
|
174
|
+
const rules = {
|
|
175
|
+
// Background
|
|
176
|
+
'bg': () => {
|
|
177
|
+
const cssValue = isArbitrary ? value : `var(--c-${value})`;
|
|
178
|
+
return `background-color: ${cssValue};`;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Text color
|
|
182
|
+
'text': () => {
|
|
183
|
+
// Check if it's alignment
|
|
184
|
+
if (['left', 'center', 'right'].includes(value)) {
|
|
185
|
+
return `text-align: ${value};`;
|
|
186
|
+
}
|
|
187
|
+
const cssValue = isArbitrary ? value : `var(--c-${value})`;
|
|
188
|
+
return `color: ${cssValue};`;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Font size
|
|
192
|
+
'text-size': () => {
|
|
193
|
+
const cssValue = isArbitrary ? value : `var(--font-${value})`;
|
|
194
|
+
return `font-size: ${cssValue};`;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Font weight
|
|
198
|
+
'font': () => {
|
|
199
|
+
const cssValue = `var(--fw-${value})`;
|
|
200
|
+
return `font-weight: ${cssValue};`;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Border color
|
|
204
|
+
'border': () => {
|
|
205
|
+
const cssValue = isArbitrary ? value : `var(--c-${value})`;
|
|
206
|
+
return `border-color: ${cssValue}; border-style: solid;`;
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Border width
|
|
210
|
+
'border-w': () => {
|
|
211
|
+
const cssValue = isArbitrary ? value : `var(--s-${value})`;
|
|
212
|
+
return `border-width: ${cssValue}; border-style: solid;`;
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// Border radius
|
|
216
|
+
'rounded': () => {
|
|
217
|
+
const cssValue = `var(--r-${value})`;
|
|
218
|
+
return `border-radius: ${cssValue};`;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// Box shadow
|
|
222
|
+
'shadow': () => {
|
|
223
|
+
const cssValue = `var(--shadow-${value})`;
|
|
224
|
+
return `box-shadow: ${cssValue};`;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Opacity
|
|
228
|
+
'opacity': () => {
|
|
229
|
+
const cssValue = isArbitrary ? value : value;
|
|
230
|
+
return `opacity: ${cssValue};`;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const generator = rules[property];
|
|
235
|
+
return generator ? generator() : '';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Generate a single CSS rule from a token
|
|
240
|
+
*/
|
|
241
|
+
export function generateRule(token, config) {
|
|
242
|
+
const { raw, attrType, breakpoint, state } = token;
|
|
243
|
+
|
|
244
|
+
let cssDeclaration = '';
|
|
245
|
+
|
|
246
|
+
switch (attrType) {
|
|
247
|
+
case 'layout':
|
|
248
|
+
cssDeclaration = generateLayoutRule(token, config);
|
|
249
|
+
break;
|
|
250
|
+
case 'space':
|
|
251
|
+
cssDeclaration = generateSpaceRule(token, config);
|
|
252
|
+
break;
|
|
253
|
+
case 'visual':
|
|
254
|
+
cssDeclaration = generateVisualRule(token, config);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!cssDeclaration) return '';
|
|
259
|
+
|
|
260
|
+
// Build selector
|
|
261
|
+
let selector = `[${attrType}~="${raw}"]`;
|
|
262
|
+
|
|
263
|
+
// Add state pseudo-class
|
|
264
|
+
if (state) {
|
|
265
|
+
selector += `:${state}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return `${selector} { ${cssDeclaration} }\n`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate complete CSS from tokens
|
|
273
|
+
* @param {Array} tokens - Array of token objects
|
|
274
|
+
* @param {Object} config - Configuration object
|
|
275
|
+
* @returns {string} - Complete CSS string
|
|
276
|
+
*/
|
|
277
|
+
export function generateCSS(tokens, config) {
|
|
278
|
+
let css = '';
|
|
279
|
+
|
|
280
|
+
// Add CSS variables
|
|
281
|
+
css += generateCSSVariables(config);
|
|
282
|
+
|
|
283
|
+
// Add base reset styles
|
|
284
|
+
css += `/* SenangStart CSS - Base Reset */
|
|
285
|
+
*, *::before, *::after {
|
|
286
|
+
box-sizing: border-box;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Layout utilities */
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
// Group tokens by breakpoint
|
|
293
|
+
const baseTokens = [];
|
|
294
|
+
const breakpointTokens = {
|
|
295
|
+
mob: [],
|
|
296
|
+
tab: [],
|
|
297
|
+
lap: [],
|
|
298
|
+
desk: []
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
for (const token of tokens) {
|
|
302
|
+
if (token.breakpoint) {
|
|
303
|
+
breakpointTokens[token.breakpoint]?.push(token);
|
|
304
|
+
} else {
|
|
305
|
+
baseTokens.push(token);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Generate base rules
|
|
310
|
+
for (const token of baseTokens) {
|
|
311
|
+
css += generateRule(token, config);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Generate responsive rules
|
|
315
|
+
const { screens } = config.theme;
|
|
316
|
+
|
|
317
|
+
for (const [bp, bpTokens] of Object.entries(breakpointTokens)) {
|
|
318
|
+
if (bpTokens.length > 0) {
|
|
319
|
+
css += `\n@media (min-width: ${screens[bp]}) {\n`;
|
|
320
|
+
for (const token of bpTokens) {
|
|
321
|
+
css += ' ' + generateRule(token, config);
|
|
322
|
+
}
|
|
323
|
+
css += '}\n';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return css;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Minify CSS by removing whitespace and comments
|
|
332
|
+
*/
|
|
333
|
+
export function minifyCSS(css) {
|
|
334
|
+
return css
|
|
335
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
|
|
336
|
+
.replace(/\s+/g, ' ') // Collapse whitespace
|
|
337
|
+
.replace(/\s*{\s*/g, '{') // Remove space around {
|
|
338
|
+
.replace(/\s*}\s*/g, '}') // Remove space around }
|
|
339
|
+
.replace(/\s*;\s*/g, ';') // Remove space around ;
|
|
340
|
+
.replace(/\s*:\s*/g, ':') // Remove space around :
|
|
341
|
+
.trim();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export default { generateCSS, generateCSSVariables, generateRule, minifyCSS };
|