@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.
Files changed (39) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +362 -151
  7. package/dist/senangstart-css.min.js +175 -174
  8. package/dist/senangstart-tw.js +4 -4
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/ms/reference/visual/ring-color.md +2 -2
  11. package/docs/ms/reference/visual/ring-offset.md +3 -3
  12. package/docs/ms/reference/visual/ring.md +5 -5
  13. package/docs/public/assets/senangstart-css.min.js +175 -174
  14. package/docs/public/llms.txt +10 -10
  15. package/docs/reference/visual/ring-color.md +2 -2
  16. package/docs/reference/visual/ring-offset.md +3 -3
  17. package/docs/reference/visual/ring.md +5 -5
  18. package/package.json +1 -1
  19. package/src/cdn/tw-conversion-engine.js +4 -4
  20. package/src/cli/commands/build.js +42 -14
  21. package/src/cli/commands/dev.js +157 -93
  22. package/src/compiler/generators/css.js +371 -199
  23. package/src/compiler/tokenizer.js +25 -23
  24. package/src/core/tokenizer-core.js +46 -19
  25. package/src/definitions/visual-borders.js +10 -10
  26. package/src/utils/common.js +456 -39
  27. package/src/utils/node-io.js +82 -0
  28. package/tests/integration/dev-recovery.test.js +231 -0
  29. package/tests/unit/cli/memory-limits.test.js +169 -0
  30. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  31. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  32. package/tests/unit/convert-tailwind.test.js +518 -442
  33. package/tests/unit/utils/common.test.js +376 -26
  34. package/tests/unit/utils/file-timeout.test.js +154 -0
  35. package/tests/unit/utils/theme-validation.test.js +181 -0
  36. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  37. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  38. package/tests/unit/security.test.js +0 -206
  39. /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: 0 0 0 0;\n';
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(--c-primary);`;
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
- const { raw, attrType, breakpoint, state } = token;
2078
-
2079
- let cssDeclaration = '';
2080
-
2081
- switch (attrType) {
2082
- case 'layout':
2083
- cssDeclaration = generateLayoutRule(token, config);
2084
- break;
2085
- case 'space':
2086
- cssDeclaration = generateSpaceRule(token, config);
2087
- break;
2088
- case 'visual':
2089
- cssDeclaration = generateVisualRule(token, config);
2090
- break;
2091
- }
2092
-
2093
- if (!cssDeclaration) return '';
2094
-
2095
- // Check if this is a divide utility (needs special selector)
2096
- const isDivide = raw.startsWith('divide');
2097
-
2098
- // Build selector
2099
- let selector = '';
2100
-
2101
- if (isDivide) {
2102
- // Divide utilities use special child selector pattern
2103
- selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden])`;
2104
- } else {
2105
- selector = `[${attrType}~="${raw}"]`;
2106
- }
2107
-
2108
- // Add state pseudo-class (but not for 'dark' - it's handled separately)
2109
- if (state && state !== 'dark') {
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
- // For divide utilities, add state to the element after tilde
2112
- // Divide utilities don't support group/peer states yet to avoid complexity
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
- // Helper to map state to CSS selector
2116
- const getStateSelector = (s) => {
2117
- const map = {
2118
- 'expanded': '[aria-expanded="true"]',
2119
- 'selected': '[aria-selected="true"]',
2120
- 'disabled': ':disabled'
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
- const selectors = [];
2126
-
2127
- // 1. Standard State Selector
2128
- selectors.push(`${selector}${getStateSelector(state)}`);
2129
-
2130
- // 2. Group & Peer State Selectors
2131
- // Only for supported triggers
2132
- const groupTriggers = {
2133
- 'hover': 'hoverable',
2134
- 'focus': 'focusable',
2135
- 'focus-visible': 'focusable',
2136
- 'active': 'pressable',
2137
- 'expanded': 'expandable',
2138
- 'selected': 'selectable'
2139
- };
2140
-
2141
- if (groupTriggers[state]) {
2142
- const parentAttr = groupTriggers[state];
2143
- // For focus, we trigger on focus-within of the container
2144
- let triggerState = state;
2145
- if (state === 'focus' || state === 'focus-visible') triggerState = 'focus-within';
2146
-
2147
- const triggerSelector = getStateSelector(triggerState);
2148
-
2149
- // Group Selector
2150
- // [layout~="hoverable"]:not([layout~="disabled"]):hover [visual~="..."]
2151
- const groupSelector = `[layout~="${parentAttr}"]:not([layout~="disabled"])${triggerSelector} ${selector}`;
2152
- selectors.push(groupSelector);
2153
-
2154
- // Peer Selectors
2155
- // [interact~="id"]:not([layout~="disabled"]):hover ~ [listens~="id"][visual~="..."]
2156
- if (interactIds && interactIds.size > 0) {
2157
- for (const id of interactIds) {
2158
- const peerSelector = `[interact~="${id}"]:not([layout~="disabled"])${triggerSelector} ~ [listens~="${id}"]${selector}`;
2159
- selectors.push(peerSelector);
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 {string} - Generated CSS
2275
+ * @returns {Object} - { css: string, errors: Array<{type, token, message}> }
2197
2276
  */
2198
- export function generateCSS(tokens, config) {
2199
- let css = '';
2277
+ export function generateCSSWithErrors(tokens, config) {
2278
+ const errors = [];
2279
+ try {
2280
+ let css = '';
2200
2281
 
2201
- // Add CSS variables
2202
- css += generateCSSVariables(config);
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
- // Add Preflight base styles if enabled (default: true)
2205
- if (config.preflight !== false) {
2206
- css += generatePreflight(config);
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
- // Add animation keyframes
2210
- css += `/* SenangStart CSS - Animation Keyframes */
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
- // Group tokens by breakpoint and dark mode
2229
- const baseTokens = [];
2230
- const darkTokens = [];
2231
- const breakpointTokens = {};
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
- for (const token of tokens) {
2240
- if (token.state === 'dark') {
2241
- darkTokens.push(token);
2242
- } else if (token.breakpoint) {
2243
- if (!breakpointTokens[token.breakpoint]) {
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
- // Collect interact IDs for Peer selector generation
2253
- const interactIds = new Set();
2254
- for (const token of tokens) {
2255
- if (token.attrType === 'interact') {
2256
- interactIds.add(token.raw);
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
- // Find display properties in base tokens
2270
- for (const token of baseTokens) {
2271
- if (token.attrType && displayProps.includes(token.property)) {
2272
- if (!baseDisplayTokens.has(token.attrType)) {
2273
- baseDisplayTokens.set(token.attrType, new Set());
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
- // Generate base rules
2280
- for (const token of baseTokens) {
2281
- css += generateRule(token, config, false, interactIds);
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
- // Generate responsive rules
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
- for (const [bp, bpTokens] of Object.entries(breakpointTokens)) {
2287
- if (bpTokens.length > 0) {
2288
- css += `\n@media (min-width: ${screens[bp]}) {\n`;
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
- // Add display reset rules for responsive tokens that have display properties
2291
- // when the same attribute has ANY base display properties (for that attrType)
2292
- // This handles the case where the same element has multiple display tokens
2293
- // e.g., layout="hidden tw-lg:flex" - hidden is base, flex is responsive
2294
- const processedResetSelectors = new Set();
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
- for (const bpToken of bpTokens) {
2297
- if (bpToken.attrType && displayProps.includes(bpToken.property)) {
2298
- // Check if there are any base tokens with display properties for this attrType
2299
- // AND the responsive token is different from base tokens
2300
- if (baseDisplayTokens.has(bpToken.attrType)) {
2301
- const baseDisplays = baseDisplayTokens.get(bpToken.attrType);
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
- // Only add reset if:
2304
- // 1. There are base display tokens for this attrType
2305
- // 2. This responsive token's raw value is different from base display tokens
2306
- // (meaning it's a different display property on the same element)
2307
- if (baseDisplays.size > 0 && !baseDisplays.has(bpToken.raw) && !processedResetSelectors.has(bpToken.raw)) {
2308
- // Add reset rule for this responsive token
2309
- const selector = `[${bpToken.attrType}~="${bpToken.raw}"]`;
2310
- css += ` ${selector} { display: revert-layer; }\n`;
2311
- processedResetSelectors.add(bpToken.raw);
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
- // Generate dark mode rules
2326
- if (darkTokens.length > 0) {
2327
- const darkMode = config.darkMode || 'media';
2328
- const darkSelector = getDarkModeSelector(config);
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
- if (darkMode === 'media') {
2331
- // Media query strategy
2332
- css += `\n/* Dark Mode (prefers-color-scheme) */\n`;
2333
- css += `@media (prefers-color-scheme: dark) {\n`;
2334
- for (const token of darkTokens) {
2335
- css += ' ' + generateRule(token, config, true, interactIds);
2336
- }
2337
- css += '}\n';
2338
- } else {
2339
- // Selector strategy (.dark class or custom selector)
2340
- css += `\n/* Dark Mode (${darkSelector}) */\n`;
2341
- for (const token of darkTokens) {
2342
- const baseRule = generateRule(token, config, true, interactIds);
2343
- // Wrap selector with dark parent
2344
- const wrappedRule = baseRule.replace(/^(\[[^\]]+\])/, `${darkSelector} $1`);
2345
- css += wrappedRule;
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