@bookklik/senangstart-css 0.2.10 → 0.2.12
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/.agent/skills/add-utility/SKILL.md +65 -0
- package/.agent/workflows/add-utility.md +2 -0
- package/.agent/workflows/build.md +2 -0
- package/.agent/workflows/dev.md +2 -0
- package/AGENTS.md +30 -0
- package/dist/senangstart-css.js +362 -151
- package/dist/senangstart-css.min.js +175 -174
- package/dist/senangstart-tw.js +4 -4
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/ms/reference/visual/ring-color.md +2 -2
- package/docs/ms/reference/visual/ring-offset.md +3 -3
- package/docs/ms/reference/visual/ring.md +5 -5
- package/docs/public/assets/senangstart-css.min.js +175 -174
- package/docs/public/llms.txt +10 -10
- package/docs/reference/visual/ring-color.md +2 -2
- package/docs/reference/visual/ring-offset.md +3 -3
- package/docs/reference/visual/ring.md +5 -5
- package/package.json +1 -1
- package/src/cdn/tw-conversion-engine.js +4 -4
- package/src/cli/commands/build.js +42 -14
- package/src/cli/commands/dev.js +157 -93
- package/src/compiler/generators/css.js +371 -199
- package/src/compiler/tokenizer.js +25 -23
- package/src/core/tokenizer-core.js +46 -19
- package/src/definitions/visual-borders.js +10 -10
- package/src/utils/common.js +456 -39
- package/src/utils/node-io.js +82 -0
- package/tests/integration/dev-recovery.test.js +231 -0
- package/tests/unit/cli/memory-limits.test.js +169 -0
- package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
- package/tests/unit/compiler/generators/css-errors.test.js +102 -0
- package/tests/unit/convert-tailwind.test.js +518 -442
- package/tests/unit/utils/common.test.js +376 -26
- package/tests/unit/utils/file-timeout.test.js +154 -0
- package/tests/unit/utils/theme-validation.test.js +181 -0
- package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
- package/tests/unit/convert-tailwind.cli.test.js +0 -95
- package/tests/unit/security.test.js +0 -206
- /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
|
@@ -184,7 +184,8 @@ export function generateCSSVariables(config) {
|
|
|
184
184
|
css += ' --ss-divide-y-reverse: 0;\n';
|
|
185
185
|
|
|
186
186
|
// Ring utility variables
|
|
187
|
-
css += ' --ring-inset:
|
|
187
|
+
css += ' --ring-inset: ;\n';
|
|
188
|
+
css += ' --ss-ring-color: var(--c-primary);\n';
|
|
188
189
|
|
|
189
190
|
css += '}\n\n';
|
|
190
191
|
return css;
|
|
@@ -382,7 +383,7 @@ function generateLayoutRule(token, config) {
|
|
|
382
383
|
// Helper function to resolve positioning value
|
|
383
384
|
const resolvePositioningValue = (val, arb) => {
|
|
384
385
|
if (arb) return val;
|
|
385
|
-
if (val === '0') return '0';
|
|
386
|
+
if (!val || val === '0') return '0';
|
|
386
387
|
// Check for negative percentage adjective
|
|
387
388
|
if (val.startsWith('-')) {
|
|
388
389
|
const positiveVal = val.substring(1);
|
|
@@ -641,8 +642,8 @@ function generateSpaceRule(token, config) {
|
|
|
641
642
|
cssValue = value;
|
|
642
643
|
} else {
|
|
643
644
|
// Check for negative value
|
|
644
|
-
const isNegative = value.startsWith('-');
|
|
645
|
-
const cleanValue = isNegative ? value.substring(1) : value;
|
|
645
|
+
const isNegative = value && value.startsWith('-');
|
|
646
|
+
const cleanValue = isNegative ? value.substring(1) : (value || '');
|
|
646
647
|
|
|
647
648
|
let baseValue;
|
|
648
649
|
if (cleanValue.startsWith('tw-')) {
|
|
@@ -730,7 +731,7 @@ function generateVisualRule(token, config) {
|
|
|
730
731
|
|
|
731
732
|
// Background Image
|
|
732
733
|
'bg-image': () => {
|
|
733
|
-
if (value === 'none') return 'background-image: none;';
|
|
734
|
+
if (!value || value === 'none') return 'background-image: none;';
|
|
734
735
|
|
|
735
736
|
// Handle gradient definitions
|
|
736
737
|
if (value.startsWith('gradient-to-')) {
|
|
@@ -1134,6 +1135,9 @@ function generateVisualRule(token, config) {
|
|
|
1134
1135
|
|
|
1135
1136
|
// Outline Color
|
|
1136
1137
|
'outline': () => {
|
|
1138
|
+
if (value === 'none') {
|
|
1139
|
+
return 'outline: none;';
|
|
1140
|
+
}
|
|
1137
1141
|
const cssValue = resolveColorValue(value, isArbitrary);
|
|
1138
1142
|
return `outline-color: ${cssValue};`;
|
|
1139
1143
|
},
|
|
@@ -1191,7 +1195,12 @@ function generateVisualRule(token, config) {
|
|
|
1191
1195
|
|
|
1192
1196
|
// Set both the variable and the box-shadow that uses it
|
|
1193
1197
|
// This allows ring:[size] to work on its own or with ring-color:[color]
|
|
1194
|
-
return `--ss-ring-width: ${width}; box-shadow: var(--ring-inset) 0 0 0 calc(var(--ss-ring-width) + var(--ss-ring-offset-width, 0px)) var(--
|
|
1198
|
+
return `--ss-ring-width: ${width}; box-shadow: var(--ring-inset) 0 0 0 calc(var(--ss-ring-width) + var(--ss-ring-offset-width, 0px)) var(--ss-ring-color);`;
|
|
1199
|
+
},
|
|
1200
|
+
|
|
1201
|
+
// Ring Inset
|
|
1202
|
+
'ring-inset': () => {
|
|
1203
|
+
return '--ring-inset: inset;';
|
|
1195
1204
|
},
|
|
1196
1205
|
|
|
1197
1206
|
// Box shadow
|
|
@@ -2067,6 +2076,32 @@ function generateVisualRule(token, config) {
|
|
|
2067
2076
|
return generator ? generator() : '';
|
|
2068
2077
|
}
|
|
2069
2078
|
|
|
2079
|
+
/**
|
|
2080
|
+
* Validate a CSS rule declaration
|
|
2081
|
+
* @param {string} declaration - CSS declaration (e.g., "property: value;")
|
|
2082
|
+
* @returns {boolean} - True if valid
|
|
2083
|
+
*/
|
|
2084
|
+
function isValidCSSRule(declaration) {
|
|
2085
|
+
if (!declaration || typeof declaration !== 'string') {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
declaration = declaration.trim();
|
|
2090
|
+
if (!declaration) return false;
|
|
2091
|
+
|
|
2092
|
+
if (!declaration.endsWith(';')) return false;
|
|
2093
|
+
|
|
2094
|
+
const parts = declaration.substring(0, declaration.length - 1).split(':');
|
|
2095
|
+
if (parts.length < 2) return false;
|
|
2096
|
+
|
|
2097
|
+
const property = parts[0].trim();
|
|
2098
|
+
const value = parts.slice(1).join(':').trim();
|
|
2099
|
+
|
|
2100
|
+
if (!property || !value) return false;
|
|
2101
|
+
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2070
2105
|
/**
|
|
2071
2106
|
* Generate a single CSS rule from a token
|
|
2072
2107
|
* @param {Object} token - Token object
|
|
@@ -2074,98 +2109,141 @@ function generateVisualRule(token, config) {
|
|
|
2074
2109
|
* @param {boolean} skipDarkWrapper - If true, don't add dark mode wrapper (used when generating inside dark block)
|
|
2075
2110
|
*/
|
|
2076
2111
|
export function generateRule(token, config, skipDarkWrapper = false, interactIds = new Set()) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2112
|
+
try {
|
|
2113
|
+
if (!token || typeof token !== 'object') {
|
|
2114
|
+
console.warn('[SenangStart] Invalid token object:', token);
|
|
2115
|
+
return '';
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const { raw, attrType, breakpoint, state } = token;
|
|
2119
|
+
|
|
2120
|
+
if (!attrType || typeof attrType !== 'string') {
|
|
2121
|
+
console.warn('[SenangStart] Invalid token attrType:', attrType);
|
|
2122
|
+
return '';
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
if (!raw || typeof raw !== 'string') {
|
|
2126
|
+
console.warn('[SenangStart] Invalid token raw:', raw);
|
|
2127
|
+
return '';
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
let cssDeclaration = '';
|
|
2131
|
+
|
|
2132
|
+
switch (attrType) {
|
|
2133
|
+
case 'layout':
|
|
2134
|
+
try {
|
|
2135
|
+
cssDeclaration = generateLayoutRule(token, config);
|
|
2136
|
+
} catch (e) {
|
|
2137
|
+
console.warn(`[SenangStart] Error generating layout rule for "${raw}": ${e.message}`);
|
|
2138
|
+
return '';
|
|
2139
|
+
}
|
|
2140
|
+
break;
|
|
2141
|
+
case 'space':
|
|
2142
|
+
try {
|
|
2143
|
+
cssDeclaration = generateSpaceRule(token, config);
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
console.warn(`[SenangStart] Error generating space rule for "${raw}": ${e.message}`);
|
|
2146
|
+
return '';
|
|
2147
|
+
}
|
|
2148
|
+
break;
|
|
2149
|
+
case 'visual':
|
|
2150
|
+
try {
|
|
2151
|
+
cssDeclaration = generateVisualRule(token, config);
|
|
2152
|
+
} catch (e) {
|
|
2153
|
+
console.warn(`[SenangStart] Error generating visual rule for "${raw}": ${e.message}`);
|
|
2154
|
+
return '';
|
|
2155
|
+
}
|
|
2156
|
+
break;
|
|
2157
|
+
default:
|
|
2158
|
+
console.warn(`[SenangStart] Unknown attrType: ${attrType}`);
|
|
2159
|
+
return '';
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if (!cssDeclaration) return '';
|
|
2163
|
+
|
|
2164
|
+
if (!isValidCSSRule(cssDeclaration)) {
|
|
2165
|
+
console.warn(`[SenangStart] Invalid CSS rule generated for "${raw}": ${cssDeclaration}`);
|
|
2166
|
+
return '';
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Check if this is a divide utility (needs special selector)
|
|
2170
|
+
const isDivide = raw && raw.startsWith('divide');
|
|
2171
|
+
|
|
2172
|
+
// Build selector
|
|
2173
|
+
let selector = '';
|
|
2174
|
+
|
|
2110
2175
|
if (isDivide) {
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden]):${state}`;
|
|
2176
|
+
// Divide utilities use special child selector pattern
|
|
2177
|
+
selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden])`;
|
|
2114
2178
|
} else {
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2179
|
+
selector = `[${attrType}~="${raw}"]`;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Add state pseudo-class (but not for 'dark' - it's handled separately)
|
|
2183
|
+
if (state && state !== 'dark') {
|
|
2184
|
+
if (isDivide) {
|
|
2185
|
+
// For divide utilities, add state to the element after tilde
|
|
2186
|
+
// Divide utilities don't support group/peer states yet to avoid complexity
|
|
2187
|
+
selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden]):${state}`;
|
|
2188
|
+
} else {
|
|
2189
|
+
// Helper to map state to CSS selector
|
|
2190
|
+
const getStateSelector = (s) => {
|
|
2191
|
+
const map = {
|
|
2192
|
+
'expanded': '[aria-expanded="true"]',
|
|
2193
|
+
'selected': '[aria-selected="true"]',
|
|
2194
|
+
'disabled': ':disabled'
|
|
2195
|
+
};
|
|
2196
|
+
return map[s] || `:${s}`;
|
|
2121
2197
|
};
|
|
2122
|
-
return map[s] || `:${s}`;
|
|
2123
|
-
};
|
|
2124
2198
|
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2199
|
+
const selectors = [];
|
|
2200
|
+
|
|
2201
|
+
// 1. Standard State Selector
|
|
2202
|
+
selectors.push(`${selector}${getStateSelector(state)}`);
|
|
2203
|
+
|
|
2204
|
+
// 2. Group & Peer State Selectors
|
|
2205
|
+
// Only for supported triggers
|
|
2206
|
+
const groupTriggers = {
|
|
2207
|
+
'hover': 'hoverable',
|
|
2208
|
+
'focus': 'focusable',
|
|
2209
|
+
'focus-visible': 'focusable',
|
|
2210
|
+
'active': 'pressable',
|
|
2211
|
+
'expanded': 'expandable',
|
|
2212
|
+
'selected': 'selectable'
|
|
2213
|
+
};
|
|
2214
|
+
|
|
2215
|
+
if (groupTriggers[state]) {
|
|
2216
|
+
const parentAttr = groupTriggers[state];
|
|
2217
|
+
// For focus, we trigger on focus-within of the container
|
|
2218
|
+
let triggerState = state;
|
|
2219
|
+
if (state === 'focus' || state === 'focus-visible') triggerState = 'focus-within';
|
|
2220
|
+
|
|
2221
|
+
const triggerSelector = getStateSelector(triggerState);
|
|
2222
|
+
|
|
2223
|
+
// Group Selector
|
|
2224
|
+
// [layout~="hoverable"]:not([layout~="disabled"]):hover [visual~="..."]
|
|
2225
|
+
const groupSelector = `[layout~="${parentAttr}"]:not([layout~="disabled"])${triggerSelector} ${selector}`;
|
|
2226
|
+
selectors.push(groupSelector);
|
|
2227
|
+
|
|
2228
|
+
// Peer Selectors
|
|
2229
|
+
// [interact~="id"]:not([layout~="disabled"]):hover ~ [listens~="id"][visual~="..."]
|
|
2230
|
+
if (interactIds && interactIds.size > 0) {
|
|
2231
|
+
for (const id of interactIds) {
|
|
2232
|
+
const peerSelector = `[interact~="${id}"]:not([layout~="disabled"])${triggerSelector} ~ [listens~="${id}"]${selector}`;
|
|
2233
|
+
selectors.push(peerSelector);
|
|
2234
|
+
}
|
|
2160
2235
|
}
|
|
2161
2236
|
}
|
|
2237
|
+
|
|
2238
|
+
selector = selectors.join(',\n');
|
|
2162
2239
|
}
|
|
2163
|
-
|
|
2164
|
-
selector = selectors.join(',\n');
|
|
2165
2240
|
}
|
|
2241
|
+
|
|
2242
|
+
return `${selector} { ${cssDeclaration} }\n`;
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
console.warn(`[SenangStart] Error in generateRule: ${e.message}`);
|
|
2245
|
+
return '';
|
|
2166
2246
|
}
|
|
2167
|
-
|
|
2168
|
-
return `${selector} { ${cssDeclaration} }\n`;
|
|
2169
2247
|
}
|
|
2170
2248
|
|
|
2171
2249
|
/**
|
|
@@ -2190,24 +2268,48 @@ function getDarkModeSelector(config) {
|
|
|
2190
2268
|
}
|
|
2191
2269
|
|
|
2192
2270
|
/**
|
|
2193
|
-
* Generate CSS from tokens
|
|
2271
|
+
* Generate CSS from tokens with detailed error reporting
|
|
2272
|
+
* Each token is processed in isolation - one failure doesn't crash the build
|
|
2194
2273
|
* @param {Array} tokens - Array of token objects
|
|
2195
2274
|
* @param {Object} config - Configuration object
|
|
2196
|
-
* @returns {
|
|
2275
|
+
* @returns {Object} - { css: string, errors: Array<{type, token, message}> }
|
|
2197
2276
|
*/
|
|
2198
|
-
export function
|
|
2199
|
-
|
|
2277
|
+
export function generateCSSWithErrors(tokens, config) {
|
|
2278
|
+
const errors = [];
|
|
2279
|
+
try {
|
|
2280
|
+
let css = '';
|
|
2200
2281
|
|
|
2201
|
-
|
|
2202
|
-
|
|
2282
|
+
// Validate inputs
|
|
2283
|
+
if (!config || typeof config !== 'object') {
|
|
2284
|
+
errors.push({ type: 'config', message: 'Invalid config provided' });
|
|
2285
|
+
return { css: '', errors };
|
|
2286
|
+
}
|
|
2203
2287
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2288
|
+
if (!Array.isArray(tokens)) {
|
|
2289
|
+
errors.push({ type: 'tokens', message: 'Invalid tokens provided' });
|
|
2290
|
+
return { css: '', errors };
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// Add CSS variables
|
|
2294
|
+
try {
|
|
2295
|
+
css += generateCSSVariables(config);
|
|
2296
|
+
} catch (e) {
|
|
2297
|
+
errors.push({ type: 'variables', message: e.message });
|
|
2298
|
+
console.warn(`[SenangStart] Error generating CSS variables: ${e.message}`);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
// Add Preflight base styles if enabled (default: true)
|
|
2302
|
+
if (config.preflight !== false) {
|
|
2303
|
+
try {
|
|
2304
|
+
css += generatePreflight(config);
|
|
2305
|
+
} catch (e) {
|
|
2306
|
+
errors.push({ type: 'preflight', message: e.message });
|
|
2307
|
+
console.warn(`[SenangStart] Error generating preflight: ${e.message}`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2208
2310
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2311
|
+
// Add animation keyframes
|
|
2312
|
+
css += `/* SenangStart CSS - Animation Keyframes */
|
|
2211
2313
|
@keyframes spin {
|
|
2212
2314
|
to { transform: rotate(360deg); }
|
|
2213
2315
|
}
|
|
@@ -2225,128 +2327,198 @@ export function generateCSS(tokens, config) {
|
|
|
2225
2327
|
/* SenangStart CSS - Utility Classes */
|
|
2226
2328
|
`;
|
|
2227
2329
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
// Initialize breakpoint collections from config
|
|
2234
|
-
const { screens } = config.theme;
|
|
2235
|
-
for (const bp of Object.keys(screens)) {
|
|
2236
|
-
breakpointTokens[bp] = [];
|
|
2237
|
-
}
|
|
2330
|
+
// Group tokens by breakpoint and dark mode
|
|
2331
|
+
const baseTokens = [];
|
|
2332
|
+
const darkTokens = [];
|
|
2333
|
+
const breakpointTokens = {};
|
|
2238
2334
|
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
breakpointTokens[token.breakpoint] = [];
|
|
2335
|
+
// Initialize breakpoint collections from config
|
|
2336
|
+
const { screens } = config.theme || {};
|
|
2337
|
+
if (screens && typeof screens === 'object') {
|
|
2338
|
+
for (const bp of Object.keys(screens)) {
|
|
2339
|
+
breakpointTokens[bp] = [];
|
|
2245
2340
|
}
|
|
2246
|
-
breakpointTokens[token.breakpoint].push(token);
|
|
2247
|
-
} else {
|
|
2248
|
-
baseTokens.push(token);
|
|
2249
2341
|
}
|
|
2250
|
-
}
|
|
2251
2342
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2343
|
+
for (const token of tokens) {
|
|
2344
|
+
try {
|
|
2345
|
+
if (token && typeof token === 'object') {
|
|
2346
|
+
if (token.state === 'dark') {
|
|
2347
|
+
darkTokens.push(token);
|
|
2348
|
+
} else if (token.breakpoint) {
|
|
2349
|
+
if (!breakpointTokens[token.breakpoint]) {
|
|
2350
|
+
breakpointTokens[token.breakpoint] = [];
|
|
2351
|
+
}
|
|
2352
|
+
breakpointTokens[token.breakpoint].push(token);
|
|
2353
|
+
} else {
|
|
2354
|
+
baseTokens.push(token);
|
|
2355
|
+
}
|
|
2356
|
+
} else {
|
|
2357
|
+
errors.push({ type: 'token_format', token: token, message: 'Token is not an object' });
|
|
2358
|
+
}
|
|
2359
|
+
} catch (e) {
|
|
2360
|
+
errors.push({ type: 'token_processing', token: token?.raw, message: e.message });
|
|
2361
|
+
console.warn(`[SenangStart] Error processing token: ${e.message}`);
|
|
2362
|
+
}
|
|
2257
2363
|
}
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
// Track display properties to handle conflicts like Tailwind
|
|
2261
|
-
// When responsive display property conflicts with base display property on the same element,
|
|
2262
|
-
// we need to add reset rules in the responsive media query
|
|
2263
|
-
const displayProps = ['flex', 'grid', 'inline-flex', 'inline-grid', 'block', 'inline', 'hidden', 'contents'];
|
|
2264
|
-
|
|
2265
|
-
// Map: attrType -> Set of raw values that have display properties in base
|
|
2266
|
-
// e.g., { 'layout' => new Set(['hidden', 'block']) }
|
|
2267
|
-
const baseDisplayTokens = new Map();
|
|
2268
2364
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2365
|
+
// Collect interact IDs for Peer selector generation
|
|
2366
|
+
const interactIds = new Set();
|
|
2367
|
+
for (const token of tokens) {
|
|
2368
|
+
try {
|
|
2369
|
+
if (token && token.attrType === 'interact' && token.raw) {
|
|
2370
|
+
interactIds.add(token.raw);
|
|
2371
|
+
}
|
|
2372
|
+
} catch (e) {
|
|
2373
|
+
errors.push({ type: 'interact_collection', token: token?.raw, message: e.message });
|
|
2374
|
+
console.warn(`[SenangStart] Error collecting interact IDs: ${e.message}`);
|
|
2274
2375
|
}
|
|
2275
|
-
baseDisplayTokens.get(token.attrType).add(token.raw);
|
|
2276
2376
|
}
|
|
2277
|
-
}
|
|
2278
2377
|
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
}
|
|
2378
|
+
// Track display properties to handle conflicts like Tailwind
|
|
2379
|
+
const displayProps = ['flex', 'grid', 'inline-flex', 'inline-grid', 'block', 'inline', 'hidden', 'contents'];
|
|
2380
|
+
const baseDisplayTokens = new Map();
|
|
2283
2381
|
|
|
2284
|
-
|
|
2382
|
+
// Find display properties in base tokens
|
|
2383
|
+
for (const token of baseTokens) {
|
|
2384
|
+
try {
|
|
2385
|
+
if (token.attrType && displayProps.includes(token.property)) {
|
|
2386
|
+
if (!baseDisplayTokens.has(token.attrType)) {
|
|
2387
|
+
baseDisplayTokens.set(token.attrType, new Set());
|
|
2388
|
+
}
|
|
2389
|
+
baseDisplayTokens.get(token.attrType).add(token.raw);
|
|
2390
|
+
}
|
|
2391
|
+
} catch (e) {
|
|
2392
|
+
errors.push({ type: 'display_track', token: token?.raw, message: e.message });
|
|
2393
|
+
console.warn(`[SenangStart] Error tracking display properties: ${e.message}`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2285
2396
|
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2397
|
+
// Generate base rules
|
|
2398
|
+
for (const token of baseTokens) {
|
|
2399
|
+
try {
|
|
2400
|
+
const rule = generateRule(token, config, false, interactIds);
|
|
2401
|
+
if (rule) {
|
|
2402
|
+
css += rule;
|
|
2403
|
+
} else {
|
|
2404
|
+
errors.push({ type: 'rule_generation', token: token.raw, message: 'No rule generated' });
|
|
2405
|
+
}
|
|
2406
|
+
} catch (e) {
|
|
2407
|
+
errors.push({ type: 'rule_generation', token: token.raw, message: e.message });
|
|
2408
|
+
console.warn(`[SenangStart] Error generating base rule: ${e.message}`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2289
2411
|
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2412
|
+
// Generate responsive rules
|
|
2413
|
+
for (const [bp, bpTokens] of Object.entries(breakpointTokens)) {
|
|
2414
|
+
try {
|
|
2415
|
+
if (bpTokens.length > 0) {
|
|
2416
|
+
// Use screen value if defined, otherwise use breakpoint name itself
|
|
2417
|
+
const screenWidth = screens && screens[bp] ? screens[bp] : bp;
|
|
2418
|
+
css += `\n@media (min-width: ${screenWidth}) {\n`;
|
|
2295
2419
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2420
|
+
const processedResetSelectors = new Set();
|
|
2421
|
+
for (const bpToken of bpTokens) {
|
|
2422
|
+
try {
|
|
2423
|
+
if (bpToken.attrType && displayProps.includes(bpToken.property)) {
|
|
2424
|
+
if (baseDisplayTokens.has(bpToken.attrType)) {
|
|
2425
|
+
const baseDisplays = baseDisplayTokens.get(bpToken.attrType);
|
|
2426
|
+
if (baseDisplays.size > 0 && !baseDisplays.has(bpToken.raw) && !processedResetSelectors.has(bpToken.raw)) {
|
|
2427
|
+
const selector = `[${bpToken.attrType}~="${bpToken.raw}"]`;
|
|
2428
|
+
css += ` ${selector} { display: revert-layer; }\n`;
|
|
2429
|
+
processedResetSelectors.add(bpToken.raw);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
} catch (e) {
|
|
2434
|
+
errors.push({ type: 'display_reset', token: bpToken.raw, message: e.message });
|
|
2435
|
+
console.warn(`[SenangStart] Error generating display reset: ${e.message}`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2302
2438
|
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2439
|
+
for (const token of bpTokens) {
|
|
2440
|
+
try {
|
|
2441
|
+
const rule = generateRule(token, config, false, interactIds);
|
|
2442
|
+
if (rule) {
|
|
2443
|
+
css += ' ' + rule;
|
|
2444
|
+
} else {
|
|
2445
|
+
errors.push({ type: 'responsive_rule', token: token.raw, message: 'No rule generated' });
|
|
2446
|
+
}
|
|
2447
|
+
} catch (e) {
|
|
2448
|
+
errors.push({ type: 'responsive_rule', token: token.raw, message: e.message });
|
|
2449
|
+
console.warn(`[SenangStart] Error generating responsive rule: ${e.message}`);
|
|
2312
2450
|
}
|
|
2313
2451
|
}
|
|
2452
|
+
css += '}\n';
|
|
2314
2453
|
}
|
|
2454
|
+
} catch (e) {
|
|
2455
|
+
errors.push({ type: 'breakpoint_generation', message: `Error generating breakpoint ${bp}: ${e.message}` });
|
|
2456
|
+
console.warn(`[SenangStart] Error generating breakpoint ${bp}: ${e.message}`);
|
|
2315
2457
|
}
|
|
2316
|
-
|
|
2317
|
-
// Generate responsive token rules
|
|
2318
|
-
for (const token of bpTokens) {
|
|
2319
|
-
css += ' ' + generateRule(token, config, false, interactIds);
|
|
2320
|
-
}
|
|
2321
|
-
css += '}\n';
|
|
2322
2458
|
}
|
|
2323
|
-
}
|
|
2324
2459
|
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2460
|
+
// Generate dark mode rules
|
|
2461
|
+
if (darkTokens.length > 0) {
|
|
2462
|
+
try {
|
|
2463
|
+
const darkMode = config.darkMode || 'media';
|
|
2464
|
+
const darkSelector = getDarkModeSelector(config);
|
|
2329
2465
|
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2466
|
+
if (darkMode === 'media') {
|
|
2467
|
+
css += `\n/* Dark Mode (prefers-color-scheme) */\n`;
|
|
2468
|
+
css += `@media (prefers-color-scheme: dark) {\n`;
|
|
2469
|
+
for (const token of darkTokens) {
|
|
2470
|
+
try {
|
|
2471
|
+
const rule = generateRule(token, config, true, interactIds);
|
|
2472
|
+
if (rule) {
|
|
2473
|
+
css += ' ' + rule;
|
|
2474
|
+
} else {
|
|
2475
|
+
errors.push({ type: 'dark_rule', token: token.raw, message: 'No rule generated' });
|
|
2476
|
+
}
|
|
2477
|
+
} catch (e) {
|
|
2478
|
+
errors.push({ type: 'dark_rule', token: token.raw, message: e.message });
|
|
2479
|
+
console.warn(`[SenangStart] Error generating dark rule (media): ${e.message}`);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
css += '}\n';
|
|
2483
|
+
} else {
|
|
2484
|
+
css += `\n/* Dark Mode (${darkSelector}) */\n`;
|
|
2485
|
+
for (const token of darkTokens) {
|
|
2486
|
+
try {
|
|
2487
|
+
const baseRule = generateRule(token, config, true, interactIds);
|
|
2488
|
+
if (baseRule) {
|
|
2489
|
+
const wrappedRule = baseRule.replace(/^(\[[^\]]+\])/, `${darkSelector} $1`);
|
|
2490
|
+
css += wrappedRule;
|
|
2491
|
+
} else {
|
|
2492
|
+
errors.push({ type: 'dark_rule', token: token.raw, message: 'No rule generated' });
|
|
2493
|
+
}
|
|
2494
|
+
} catch (e) {
|
|
2495
|
+
errors.push({ type: 'dark_rule', token: token.raw, message: e.message });
|
|
2496
|
+
console.warn(`[SenangStart] Error generating dark rule (selector): ${e.message}`);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
} catch (e) {
|
|
2501
|
+
errors.push({ type: 'dark_mode_generation', message: e.message });
|
|
2502
|
+
console.warn(`[SenangStart] Error generating dark mode rules: ${e.message}`);
|
|
2346
2503
|
}
|
|
2347
2504
|
}
|
|
2505
|
+
|
|
2506
|
+
return { css, errors };
|
|
2507
|
+
} catch (e) {
|
|
2508
|
+
errors.push({ type: 'fatal', message: e.message });
|
|
2509
|
+
console.error(`[SenangStart] Fatal error in generateCSSWithErrors: ${e.message}`);
|
|
2510
|
+
return { css: '', errors };
|
|
2348
2511
|
}
|
|
2349
|
-
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* Generate CSS from tokens (Backward compatible wrapper)
|
|
2516
|
+
* @param {Array} tokens - Array of token objects
|
|
2517
|
+
* @param {Object} config - Configuration object
|
|
2518
|
+
* @returns {string} - Generated CSS
|
|
2519
|
+
*/
|
|
2520
|
+
export function generateCSS(tokens, config) {
|
|
2521
|
+
const { css } = generateCSSWithErrors(tokens, config);
|
|
2350
2522
|
return css;
|
|
2351
2523
|
}
|
|
2352
2524
|
|