@bookklik/senangstart-css 0.2.9 → 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 (69) 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 +607 -180
  7. package/dist/senangstart-css.min.js +234 -195
  8. package/dist/senangstart-tw.js +274 -8
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/SYNTAX-REFERENCE.md +1731 -1590
  11. package/docs/guide/preflight.md +20 -1
  12. package/docs/ms/guide/preflight.md +19 -0
  13. package/docs/ms/reference/breakpoints.md +14 -0
  14. package/docs/ms/reference/visual/border-radius.md +50 -10
  15. package/docs/ms/reference/visual/contain.md +57 -0
  16. package/docs/ms/reference/visual/content-visibility.md +53 -0
  17. package/docs/ms/reference/visual/placeholder-color.md +92 -0
  18. package/docs/ms/reference/visual/ring-color.md +2 -2
  19. package/docs/ms/reference/visual/ring-offset.md +3 -3
  20. package/docs/ms/reference/visual/ring.md +5 -5
  21. package/docs/ms/reference/visual/writing-mode.md +53 -0
  22. package/docs/ms/reference/visual.md +6 -0
  23. package/docs/public/assets/senangstart-css.min.js +234 -195
  24. package/docs/public/llms.txt +45 -12
  25. package/docs/reference/breakpoints.md +14 -0
  26. package/docs/reference/visual/border-radius.md +50 -10
  27. package/docs/reference/visual/contain.md +57 -0
  28. package/docs/reference/visual/content-visibility.md +53 -0
  29. package/docs/reference/visual/placeholder-color.md +92 -0
  30. package/docs/reference/visual/ring-color.md +2 -2
  31. package/docs/reference/visual/ring-offset.md +3 -3
  32. package/docs/reference/visual/ring.md +5 -5
  33. package/docs/reference/visual/writing-mode.md +53 -0
  34. package/docs/reference/visual.md +7 -0
  35. package/docs/syntax-reference.json +2185 -2009
  36. package/package.json +1 -1
  37. package/scripts/convert-tailwind.js +300 -26
  38. package/scripts/generate-docs.js +403 -403
  39. package/src/cdn/senangstart-engine.js +5 -5
  40. package/src/cdn/tw-conversion-engine.js +305 -8
  41. package/src/cli/commands/build.js +51 -13
  42. package/src/cli/commands/dev.js +157 -93
  43. package/src/compiler/generators/css.js +467 -208
  44. package/src/compiler/generators/preflight.js +26 -13
  45. package/src/compiler/generators/typescript.js +3 -1
  46. package/src/compiler/index.js +27 -3
  47. package/src/compiler/parser.js +13 -6
  48. package/src/compiler/tokenizer.js +25 -23
  49. package/src/config/defaults.js +3 -0
  50. package/src/core/tokenizer-core.js +46 -19
  51. package/src/definitions/index.js +4 -1
  52. package/src/definitions/visual-borders.js +10 -10
  53. package/src/definitions/visual-performance.js +126 -0
  54. package/src/definitions/visual.js +25 -9
  55. package/src/utils/common.js +456 -27
  56. package/src/utils/node-io.js +82 -0
  57. package/tests/integration/dev-recovery.test.js +231 -0
  58. package/tests/unit/cli/memory-limits.test.js +169 -0
  59. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  60. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  61. package/tests/unit/compiler/generators/css.test.js +102 -5
  62. package/tests/unit/convert-tailwind.test.js +518 -431
  63. package/tests/unit/utils/common.test.js +376 -26
  64. package/tests/unit/utils/file-timeout.test.js +154 -0
  65. package/tests/unit/utils/theme-validation.test.js +181 -0
  66. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  67. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  68. package/tests/unit/security.test.js +0 -206
  69. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -82,6 +82,19 @@ export function generateCSSVariables(config) {
82
82
  css += ` --c-${key}: ${value};\n`;
83
83
  }
84
84
 
85
+ // Placeholder color variable
86
+ if (theme.placeholder) {
87
+ css += ` --placeholder-color: ${theme.placeholder};\n`;
88
+ } else {
89
+ css += ' --placeholder-color: #9ca3af;\n';
90
+ }
91
+
92
+ // Gradient direction variables for better gradient support
93
+ css += ' --gradient-from: transparent;\n';
94
+ css += ' --gradient-via: transparent;\n';
95
+ css += ' --gradient-to: transparent;\n';
96
+ css += ' --gradient-stops: var(--gradient-from), var(--gradient-via), var(--gradient-to);\n';
97
+
85
98
  // Z-index variables
86
99
  for (const [key, value] of Object.entries(theme.zIndex)) {
87
100
  css += ` --z-${key}: ${value};\n`;
@@ -166,6 +179,14 @@ export function generateCSSVariables(config) {
166
179
  css += ` --tw-font-${key}: ${value};\n`;
167
180
  }
168
181
 
182
+ // Divide reverse variables (used by divide-x:reverse and divide-y:reverse)
183
+ css += ' --ss-divide-x-reverse: 0;\n';
184
+ css += ' --ss-divide-y-reverse: 0;\n';
185
+
186
+ // Ring utility variables
187
+ css += ' --ring-inset: ;\n';
188
+ css += ' --ss-ring-color: var(--c-primary);\n';
189
+
169
190
  css += '}\n\n';
170
191
  return css;
171
192
  }
@@ -309,6 +330,37 @@ function generateLayoutRule(token, config) {
309
330
  return `object-position: ${cssValue};`;
310
331
  }
311
332
 
333
+ // Content Visibility
334
+ if (property === 'content-visibility') {
335
+ return `content-visibility: ${value};`;
336
+ }
337
+
338
+ // Contain
339
+ if (property === 'contain') {
340
+ const containMap = {
341
+ 'none': 'none',
342
+ 'strict': 'strict',
343
+ 'content': 'content',
344
+ 'size': 'size',
345
+ 'layout': 'layout',
346
+ 'style': 'style',
347
+ 'paint': 'paint'
348
+ };
349
+ const cssValue = isArbitrary ? value : (containMap[value] || value);
350
+ return `contain: ${cssValue};`;
351
+ }
352
+
353
+ // Writing Mode (for RTL support)
354
+ if (property === 'writing') {
355
+ const writingMap = {
356
+ 'horizontal-tb': 'horizontal-tb',
357
+ 'vertical-rl': 'vertical-rl',
358
+ 'vertical-lr': 'vertical-lr'
359
+ };
360
+ const cssValue = isArbitrary ? value.replace(/_/g, ' ') : (writingMap[value] || value);
361
+ return `writing-mode: ${cssValue};`;
362
+ }
363
+
312
364
  // Percentage adjectives for positioning utilities
313
365
  const positioningPercentages = {
314
366
  'full': '100%',
@@ -331,7 +383,7 @@ function generateLayoutRule(token, config) {
331
383
  // Helper function to resolve positioning value
332
384
  const resolvePositioningValue = (val, arb) => {
333
385
  if (arb) return val;
334
- if (val === '0') return '0';
386
+ if (!val || val === '0') return '0';
335
387
  // Check for negative percentage adjective
336
388
  if (val.startsWith('-')) {
337
389
  const positiveVal = val.substring(1);
@@ -590,8 +642,8 @@ function generateSpaceRule(token, config) {
590
642
  cssValue = value;
591
643
  } else {
592
644
  // Check for negative value
593
- const isNegative = value.startsWith('-');
594
- const cleanValue = isNegative ? value.substring(1) : value;
645
+ const isNegative = value && value.startsWith('-');
646
+ const cleanValue = isNegative ? value.substring(1) : (value || '');
595
647
 
596
648
  let baseValue;
597
649
  if (cleanValue.startsWith('tw-')) {
@@ -679,7 +731,7 @@ function generateVisualRule(token, config) {
679
731
 
680
732
  // Background Image
681
733
  'bg-image': () => {
682
- if (value === 'none') return 'background-image: none;';
734
+ if (!value || value === 'none') return 'background-image: none;';
683
735
 
684
736
  // Handle gradient definitions
685
737
  if (value.startsWith('gradient-to-')) {
@@ -951,33 +1003,33 @@ function generateVisualRule(token, config) {
951
1003
  // Border width
952
1004
  'border-w': () => {
953
1005
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
954
- return `border-width: ${cssValue}; border-style: solid;`;
1006
+ return `border-width: ${cssValue};`;
955
1007
  },
956
1008
 
957
1009
  // Border width - directional
958
1010
  'border-t-w': () => {
959
1011
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
960
- return `border-top-width: ${cssValue}; border-top-style: solid;`;
1012
+ return `border-top-width: ${cssValue};`;
961
1013
  },
962
1014
  'border-b-w': () => {
963
1015
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
964
- return `border-bottom-width: ${cssValue}; border-bottom-style: solid;`;
1016
+ return `border-bottom-width: ${cssValue};`;
965
1017
  },
966
1018
  'border-l-w': () => {
967
1019
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
968
- return `border-left-width: ${cssValue}; border-left-style: solid;`;
1020
+ return `border-left-width: ${cssValue};`;
969
1021
  },
970
1022
  'border-r-w': () => {
971
1023
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
972
- return `border-right-width: ${cssValue}; border-right-style: solid;`;
1024
+ return `border-right-width: ${cssValue};`;
973
1025
  },
974
1026
  'border-x-w': () => {
975
1027
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
976
- return `border-left-width: ${cssValue}; border-right-width: ${cssValue}; border-left-style: solid; border-right-style: solid;`;
1028
+ return `border-left-width: ${cssValue}; border-right-width: ${cssValue};`;
977
1029
  },
978
1030
  'border-y-w': () => {
979
1031
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
980
- return `border-top-width: ${cssValue}; border-bottom-width: ${cssValue}; border-top-style: solid; border-bottom-style: solid;`;
1032
+ return `border-top-width: ${cssValue}; border-bottom-width: ${cssValue};`;
981
1033
  },
982
1034
 
983
1035
  // Border style
@@ -990,6 +1042,40 @@ function generateVisualRule(token, config) {
990
1042
  return `border-radius: var(--r-${value});`;
991
1043
  },
992
1044
 
1045
+ // Directional border radius
1046
+ 'rounded-t': () => {
1047
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1048
+ return `border-top-left-radius: ${cssValue}; border-top-right-radius: ${cssValue};`;
1049
+ },
1050
+ 'rounded-b': () => {
1051
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1052
+ return `border-bottom-left-radius: ${cssValue}; border-bottom-right-radius: ${cssValue};`;
1053
+ },
1054
+ 'rounded-l': () => {
1055
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1056
+ return `border-top-left-radius: ${cssValue}; border-bottom-left-radius: ${cssValue};`;
1057
+ },
1058
+ 'rounded-r': () => {
1059
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1060
+ return `border-top-right-radius: ${cssValue}; border-bottom-right-radius: ${cssValue};`;
1061
+ },
1062
+ 'rounded-tl': () => {
1063
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1064
+ return `border-top-left-radius: ${cssValue};`;
1065
+ },
1066
+ 'rounded-tr': () => {
1067
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1068
+ return `border-top-right-radius: ${cssValue};`;
1069
+ },
1070
+ 'rounded-bl': () => {
1071
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1072
+ return `border-bottom-left-radius: ${cssValue};`;
1073
+ },
1074
+ 'rounded-br': () => {
1075
+ const cssValue = isArbitrary ? value : `var(--r-${value})`;
1076
+ return `border-bottom-right-radius: ${cssValue};`;
1077
+ },
1078
+
993
1079
  // =====================
994
1080
  // DIVIDE UTILITIES
995
1081
  // =====================
@@ -1007,6 +1093,7 @@ function generateVisualRule(token, config) {
1007
1093
  return '--ss-divide-x-reverse: 1;';
1008
1094
  }
1009
1095
  const cssValue = resolveColorValue(value, isArbitrary);
1096
+ // Apply to both to ensure color is inherited, width will control visibility
1010
1097
  return `border-left-color: ${cssValue}; border-right-color: ${cssValue}; border-left-style: solid; border-right-style: solid;`;
1011
1098
  },
1012
1099
  'divide-y': () => {
@@ -1015,23 +1102,24 @@ function generateVisualRule(token, config) {
1015
1102
  return '--ss-divide-y-reverse: 1;';
1016
1103
  }
1017
1104
  const cssValue = resolveColorValue(value, isArbitrary);
1105
+ // Apply to both to ensure color is inherited, width will control visibility
1018
1106
  return `border-top-color: ${cssValue}; border-bottom-color: ${cssValue}; border-top-style: solid; border-bottom-style: solid;`;
1019
1107
  },
1020
1108
 
1021
1109
  // Divide width - all sides
1022
1110
  'divide-w': () => {
1023
1111
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
1024
- return `border-width: ${cssValue}; border-style: solid;`;
1112
+ return `border-top-width: calc(${cssValue} * (1 - var(--ss-divide-y-reverse))); border-bottom-width: calc(${cssValue} * var(--ss-divide-y-reverse)); border-left-width: calc(${cssValue} * (1 - var(--ss-divide-x-reverse))); border-right-width: calc(${cssValue} * var(--ss-divide-x-reverse));`;
1025
1113
  },
1026
1114
 
1027
1115
  // Divide width - directional
1028
1116
  'divide-x-w': () => {
1029
1117
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
1030
- return `border-left-width: ${cssValue}; border-right-width: ${cssValue}; border-left-style: solid; border-right-style: solid;`;
1118
+ return `border-right-width: calc(${cssValue} * var(--ss-divide-x-reverse)); border-left-width: calc(${cssValue} * (1 - var(--ss-divide-x-reverse)));`;
1031
1119
  },
1032
1120
  'divide-y-w': () => {
1033
1121
  const cssValue = isArbitrary ? value : `var(--s-${value})`;
1034
- return `border-top-width: ${cssValue}; border-bottom-width: ${cssValue}; border-top-style: solid; border-bottom-style: solid;`;
1122
+ return `border-bottom-width: calc(${cssValue} * var(--ss-divide-y-reverse)); border-top-width: calc(${cssValue} * (1 - var(--ss-divide-y-reverse)));`;
1035
1123
  },
1036
1124
 
1037
1125
  // Divide style
@@ -1047,6 +1135,9 @@ function generateVisualRule(token, config) {
1047
1135
 
1048
1136
  // Outline Color
1049
1137
  'outline': () => {
1138
+ if (value === 'none') {
1139
+ return 'outline: none;';
1140
+ }
1050
1141
  const cssValue = resolveColorValue(value, isArbitrary);
1051
1142
  return `outline-color: ${cssValue};`;
1052
1143
  },
@@ -1102,9 +1193,14 @@ function generateVisualRule(token, config) {
1102
1193
 
1103
1194
  const width = isArbitrary ? value : (ringPresets[value] || (parseInt(value) ? `${value}px` : `var(--s-${value})`));
1104
1195
 
1105
- // We set both the variable and the box-shadow that uses it
1196
+ // Set both the variable and the box-shadow that uses it
1106
1197
  // This allows ring:[size] to work on its own or with ring-color:[color]
1107
- return `--ss-ring-width: ${width}; box-shadow: var(--ss-ring-inset) 0 0 0 calc(var(--ss-ring-width) + var(--ss-ring-offset-width, 0px)) var(--ss-ring-color, currentColor);`;
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;';
1108
1204
  },
1109
1205
 
1110
1206
  // Box shadow
@@ -1980,6 +2076,32 @@ function generateVisualRule(token, config) {
1980
2076
  return generator ? generator() : '';
1981
2077
  }
1982
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
+
1983
2105
  /**
1984
2106
  * Generate a single CSS rule from a token
1985
2107
  * @param {Object} token - Token object
@@ -1987,98 +2109,141 @@ function generateVisualRule(token, config) {
1987
2109
  * @param {boolean} skipDarkWrapper - If true, don't add dark mode wrapper (used when generating inside dark block)
1988
2110
  */
1989
2111
  export function generateRule(token, config, skipDarkWrapper = false, interactIds = new Set()) {
1990
- const { raw, attrType, breakpoint, state } = token;
1991
-
1992
- let cssDeclaration = '';
1993
-
1994
- switch (attrType) {
1995
- case 'layout':
1996
- cssDeclaration = generateLayoutRule(token, config);
1997
- break;
1998
- case 'space':
1999
- cssDeclaration = generateSpaceRule(token, config);
2000
- break;
2001
- case 'visual':
2002
- cssDeclaration = generateVisualRule(token, config);
2003
- break;
2004
- }
2005
-
2006
- if (!cssDeclaration) return '';
2007
-
2008
- // Check if this is a divide utility (needs special selector)
2009
- const isDivide = raw.startsWith('divide');
2010
-
2011
- // Build selector
2012
- let selector = '';
2013
-
2014
- if (isDivide) {
2015
- // Divide utilities use special child selector pattern
2016
- selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden])`;
2017
- } else {
2018
- selector = `[${attrType}~="${raw}"]`;
2019
- }
2020
-
2021
- // Add state pseudo-class (but not for 'dark' - it's handled separately)
2022
- 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
+
2023
2175
  if (isDivide) {
2024
- // For divide utilities, add state to the element after tilde
2025
- // Divide utilities don't support group/peer states yet to avoid complexity
2026
- selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden]):${state}`;
2176
+ // Divide utilities use special child selector pattern
2177
+ selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden])`;
2027
2178
  } else {
2028
- // Helper to map state to CSS selector
2029
- const getStateSelector = (s) => {
2030
- const map = {
2031
- 'expanded': '[aria-expanded="true"]',
2032
- 'selected': '[aria-selected="true"]',
2033
- '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}`;
2034
2197
  };
2035
- return map[s] || `:${s}`;
2036
- };
2037
2198
 
2038
- const selectors = [];
2039
-
2040
- // 1. Standard State Selector
2041
- selectors.push(`${selector}${getStateSelector(state)}`);
2042
-
2043
- // 2. Group & Peer State Selectors
2044
- // Only for supported triggers
2045
- const groupTriggers = {
2046
- 'hover': 'hoverable',
2047
- 'focus': 'focusable',
2048
- 'focus-visible': 'focusable',
2049
- 'active': 'pressable',
2050
- 'expanded': 'expandable',
2051
- 'selected': 'selectable'
2052
- };
2053
-
2054
- if (groupTriggers[state]) {
2055
- const parentAttr = groupTriggers[state];
2056
- // For focus, we trigger on focus-within of the container
2057
- let triggerState = state;
2058
- if (state === 'focus' || state === 'focus-visible') triggerState = 'focus-within';
2059
-
2060
- const triggerSelector = getStateSelector(triggerState);
2061
-
2062
- // Group Selector
2063
- // [layout~="hoverable"]:not([layout~="disabled"]):hover [visual~="..."]
2064
- const groupSelector = `[layout~="${parentAttr}"]:not([layout~="disabled"])${triggerSelector} ${selector}`;
2065
- selectors.push(groupSelector);
2066
-
2067
- // Peer Selectors
2068
- // [interact~="id"]:not([layout~="disabled"]):hover ~ [listens~="id"][visual~="..."]
2069
- if (interactIds && interactIds.size > 0) {
2070
- for (const id of interactIds) {
2071
- const peerSelector = `[interact~="${id}"]:not([layout~="disabled"])${triggerSelector} ~ [listens~="${id}"]${selector}`;
2072
- 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
+ }
2073
2235
  }
2074
2236
  }
2237
+
2238
+ selector = selectors.join(',\n');
2075
2239
  }
2076
-
2077
- selector = selectors.join(',\n');
2078
2240
  }
2241
+
2242
+ return `${selector} { ${cssDeclaration} }\n`;
2243
+ } catch (e) {
2244
+ console.warn(`[SenangStart] Error in generateRule: ${e.message}`);
2245
+ return '';
2079
2246
  }
2080
-
2081
- return `${selector} { ${cssDeclaration} }\n`;
2082
2247
  }
2083
2248
 
2084
2249
  /**
@@ -2103,24 +2268,48 @@ function getDarkModeSelector(config) {
2103
2268
  }
2104
2269
 
2105
2270
  /**
2106
- * Generate complete 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
2107
2273
  * @param {Array} tokens - Array of token objects
2108
2274
  * @param {Object} config - Configuration object
2109
- * @returns {string} - Complete CSS string
2275
+ * @returns {Object} - { css: string, errors: Array<{type, token, message}> }
2110
2276
  */
2111
- export function generateCSS(tokens, config) {
2112
- let css = '';
2277
+ export function generateCSSWithErrors(tokens, config) {
2278
+ const errors = [];
2279
+ try {
2280
+ let css = '';
2113
2281
 
2114
- // Add CSS variables
2115
- 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
+ }
2116
2287
 
2117
- // Add Preflight base styles if enabled (default: true)
2118
- if (config.preflight !== false) {
2119
- css += generatePreflight(config);
2120
- }
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
+ }
2121
2300
 
2122
- // Add animation keyframes
2123
- css += `/* SenangStart CSS - Animation Keyframes */
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
+ }
2310
+
2311
+ // Add animation keyframes
2312
+ css += `/* SenangStart CSS - Animation Keyframes */
2124
2313
  @keyframes spin {
2125
2314
  to { transform: rotate(360deg); }
2126
2315
  }
@@ -2138,128 +2327,198 @@ export function generateCSS(tokens, config) {
2138
2327
  /* SenangStart CSS - Utility Classes */
2139
2328
  `;
2140
2329
 
2141
- // Group tokens by breakpoint and dark mode
2142
- const baseTokens = [];
2143
- const darkTokens = [];
2144
- const breakpointTokens = {};
2145
-
2146
- // Initialize breakpoint collections from config
2147
- const { screens } = config.theme;
2148
- for (const bp of Object.keys(screens)) {
2149
- breakpointTokens[bp] = [];
2150
- }
2330
+ // Group tokens by breakpoint and dark mode
2331
+ const baseTokens = [];
2332
+ const darkTokens = [];
2333
+ const breakpointTokens = {};
2151
2334
 
2152
- for (const token of tokens) {
2153
- if (token.state === 'dark') {
2154
- darkTokens.push(token);
2155
- } else if (token.breakpoint) {
2156
- if (!breakpointTokens[token.breakpoint]) {
2157
- 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] = [];
2158
2340
  }
2159
- breakpointTokens[token.breakpoint].push(token);
2160
- } else {
2161
- baseTokens.push(token);
2162
2341
  }
2163
- }
2164
2342
 
2165
- // Collect interact IDs for Peer selector generation
2166
- const interactIds = new Set();
2167
- for (const token of tokens) {
2168
- if (token.attrType === 'interact') {
2169
- 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
+ }
2170
2363
  }
2171
- }
2172
-
2173
- // Track display properties to handle conflicts like Tailwind
2174
- // When responsive display property conflicts with base display property on the same element,
2175
- // we need to add reset rules in the responsive media query
2176
- const displayProps = ['flex', 'grid', 'inline-flex', 'inline-grid', 'block', 'inline', 'hidden', 'contents'];
2177
-
2178
- // Map: attrType -> Set of raw values that have display properties in base
2179
- // e.g., { 'layout' => new Set(['hidden', 'block']) }
2180
- const baseDisplayTokens = new Map();
2181
2364
 
2182
- // Find display properties in base tokens
2183
- for (const token of baseTokens) {
2184
- if (token.attrType && displayProps.includes(token.property)) {
2185
- if (!baseDisplayTokens.has(token.attrType)) {
2186
- 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}`);
2187
2375
  }
2188
- baseDisplayTokens.get(token.attrType).add(token.raw);
2189
2376
  }
2190
- }
2191
2377
 
2192
- // Generate base rules
2193
- for (const token of baseTokens) {
2194
- css += generateRule(token, config, false, interactIds);
2195
- }
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();
2196
2381
 
2197
- // 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
+ }
2198
2396
 
2199
- for (const [bp, bpTokens] of Object.entries(breakpointTokens)) {
2200
- if (bpTokens.length > 0) {
2201
- 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
+ }
2202
2411
 
2203
- // Add display reset rules for responsive tokens that have display properties
2204
- // when the same attribute has ANY base display properties (for that attrType)
2205
- // This handles the case where the same element has multiple display tokens
2206
- // e.g., layout="hidden tw-lg:flex" - hidden is base, flex is responsive
2207
- 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`;
2208
2419
 
2209
- for (const bpToken of bpTokens) {
2210
- if (bpToken.attrType && displayProps.includes(bpToken.property)) {
2211
- // Check if there are any base tokens with display properties for this attrType
2212
- // AND the responsive token is different from base tokens
2213
- if (baseDisplayTokens.has(bpToken.attrType)) {
2214
- 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
+ }
2215
2438
 
2216
- // Only add reset if:
2217
- // 1. There are base display tokens for this attrType
2218
- // 2. This responsive token's raw value is different from base display tokens
2219
- // (meaning it's a different display property on the same element)
2220
- if (baseDisplays.size > 0 && !baseDisplays.has(bpToken.raw) && !processedResetSelectors.has(bpToken.raw)) {
2221
- // Add reset rule for this responsive token
2222
- const selector = `[${bpToken.attrType}~="${bpToken.raw}"]`;
2223
- css += ` ${selector} { display: revert-layer; }\n`;
2224
- 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}`);
2225
2450
  }
2226
2451
  }
2452
+ css += '}\n';
2227
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}`);
2228
2457
  }
2229
-
2230
- // Generate responsive token rules
2231
- for (const token of bpTokens) {
2232
- css += ' ' + generateRule(token, config, false, interactIds);
2233
- }
2234
- css += '}\n';
2235
2458
  }
2236
- }
2237
2459
 
2238
- // Generate dark mode rules
2239
- if (darkTokens.length > 0) {
2240
- const darkMode = config.darkMode || 'media';
2241
- 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);
2242
2465
 
2243
- if (darkMode === 'media') {
2244
- // Media query strategy
2245
- css += `\n/* Dark Mode (prefers-color-scheme) */\n`;
2246
- css += `@media (prefers-color-scheme: dark) {\n`;
2247
- for (const token of darkTokens) {
2248
- css += ' ' + generateRule(token, config, true, interactIds);
2249
- }
2250
- css += '}\n';
2251
- } else {
2252
- // Selector strategy (.dark class or custom selector)
2253
- css += `\n/* Dark Mode (${darkSelector}) */\n`;
2254
- for (const token of darkTokens) {
2255
- const baseRule = generateRule(token, config, true, interactIds);
2256
- // Wrap selector with dark parent
2257
- const wrappedRule = baseRule.replace(/^(\[[^\]]+\])/, `${darkSelector} $1`);
2258
- 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}`);
2259
2503
  }
2260
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 };
2261
2511
  }
2512
+ }
2262
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);
2263
2522
  return css;
2264
2523
  }
2265
2524