@bookklik/senangstart-css 0.2.7 → 0.2.9

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 (49) hide show
  1. package/dist/senangstart-css.js +9052 -2142
  2. package/dist/senangstart-css.min.js +1207 -119
  3. package/dist/senangstart-tw.js +170 -73
  4. package/dist/senangstart-tw.min.js +1 -1
  5. package/docs/guide/configuration.md +2 -2
  6. package/docs/guide/states.md +60 -0
  7. package/docs/ms/guide/configuration.md +2 -2
  8. package/docs/ms/guide/states.md +60 -0
  9. package/docs/ms/reference/colors.md +2 -2
  10. package/docs/ms/reference/space/height.md +10 -10
  11. package/docs/ms/reference/space/width.md +12 -12
  12. package/docs/public/assets/senangstart-css.min.js +1207 -119
  13. package/docs/public/llms.txt +28 -0
  14. package/docs/reference/colors.md +2 -2
  15. package/docs/reference/space/height.md +10 -10
  16. package/docs/reference/space/width.md +12 -12
  17. package/package.json +1 -1
  18. package/public/senangstart.css +1196 -0
  19. package/scripts/convert-tailwind.js +191 -68
  20. package/scripts/generate-llms-txt.js +28 -0
  21. package/src/cdn/senangstart-engine.js +36 -2268
  22. package/src/cdn/tw-conversion-engine.js +203 -74
  23. package/src/compiler/generators/css.js +309 -249
  24. package/src/compiler/parser.js +14 -4
  25. package/src/compiler/tokenizer.js +0 -1
  26. package/src/config/defaults.js +1 -1
  27. package/src/core/constants.js +5 -3
  28. package/src/core/tokenizer-core.js +3 -58
  29. package/src/definitions/index.js +3 -2
  30. package/src/definitions/layout.js +6 -2
  31. package/src/definitions/space.js +45 -19
  32. package/src/index.js +47 -0
  33. package/src/utils/common.js +27 -0
  34. package/templates/senangstart.config.js +1 -1
  35. package/tests/helpers/test-utils.js +1 -1
  36. package/tests/integration/compiler.test.js +12 -1
  37. package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
  38. package/tests/unit/compiler/generators/css.test.js +1418 -1
  39. package/tests/unit/compiler/generators/preflight.test.js +31 -0
  40. package/tests/unit/compiler/parser.test.js +26 -0
  41. package/tests/unit/config/defaults.test.js +2 -2
  42. package/tests/unit/convert-tailwind.cli.test.js +95 -0
  43. package/tests/unit/convert-tailwind.coverage.test.js +225 -0
  44. package/tests/unit/convert-tailwind.test.js +49 -20
  45. package/tests/unit/core/tokenizer-core.test.js +102 -0
  46. package/tests/unit/definitions/index.test.js +108 -0
  47. package/tests/unit/definitions/layout_definitions.test.js +40 -0
  48. package/tests/unit/utils/common.test.js +26 -0
  49. package/scripts/bundle-jit.js +0 -45
@@ -4,22 +4,36 @@
4
4
  */
5
5
 
6
6
  import { generatePreflight } from './preflight.js';
7
+ import { sanitizeValue } from '../../utils/common.js';
8
+ import { buildAllMaps } from '../../definitions/index.js';
9
+
10
+ // Initialize maps from definitions - Single Source of Truth
11
+ const { layoutMap, typographyKeywords } = buildAllMaps();
12
+
13
+ // Helper to sanitize arbitrary values using common utility
14
+ function sanitizeArbitraryValue(value) {
15
+ return sanitizeValue(value);
16
+ }
17
+
18
+ // CSS color keywords that should be passed through directly without var() wrapping
19
+ const CSS_COLOR_KEYWORDS = ['transparent', 'currentColor', 'inherit', 'initial', 'unset'];
7
20
 
8
21
  /**
9
- * Sanitize arbitrary value to prevent CSS injection
10
- * @param {string} value - Value to sanitize
11
- * @returns {string} - Sanitized value
22
+ * Resolve a color value to CSS
23
+ * @param {string} value - The color value
24
+ * @param {boolean} isArbitrary - Whether the value is arbitrary (wrapped in [])
25
+ * @returns {string} - The resolved CSS color value
12
26
  */
13
- function sanitizeArbitraryValue(value) {
14
- if (typeof value !== 'string') {
15
- return '';
27
+ function resolveColorValue(value, isArbitrary) {
28
+ if (isArbitrary) {
29
+ return value;
16
30
  }
17
- // Remove potentially dangerous characters that could break CSS syntax
18
- const dangerousChars = /[;}{]/g;
19
- if (dangerousChars.test(value)) {
20
- return value.replace(dangerousChars, '_');
31
+ // Check if it's a CSS keyword that should pass through directly
32
+ if (CSS_COLOR_KEYWORDS.includes(value)) {
33
+ return value;
21
34
  }
22
- return value;
35
+ // Otherwise wrap in CSS variable
36
+ return `var(--c-${value})`;
23
37
  }
24
38
 
25
39
  /**
@@ -88,8 +102,9 @@ export function generateCSSVariables(config) {
88
102
  '60': '15rem', '64': '16rem', '72': '18rem', '80': '20rem', '96': '24rem'
89
103
  };
90
104
  for (const [key, value] of Object.entries(twSpacing)) {
91
- css += ` --tw-${key}: ${value};\n`;
105
+ css += ` --tw-${key.replace(/\./g, '\\\\.')}: ${value};\n`;
92
106
  }
107
+
93
108
 
94
109
  // Tailwind Border Radius Scale
95
110
  const twRadius = {
@@ -97,7 +112,7 @@ export function generateCSSVariables(config) {
97
112
  'lg': '0.5rem', 'xl': '0.75rem', '2xl': '1rem', '3xl': '1.5rem', 'full': '9999px'
98
113
  };
99
114
  for (const [key, value] of Object.entries(twRadius)) {
100
- css += ` --tw-rounded-${key}: ${value};\n`;
115
+ css += ` --r-tw-${key}: ${value};\n`;
101
116
  }
102
117
 
103
118
  // Tailwind Shadow Scale
@@ -112,7 +127,7 @@ export function generateCSSVariables(config) {
112
127
  'none': '0 0 #0000'
113
128
  };
114
129
  for (const [key, value] of Object.entries(twShadow)) {
115
- css += ` --tw-shadow-${key}: ${value};\n`;
130
+ css += ` --shadow-tw-${key}: ${value};\n`;
116
131
  }
117
132
 
118
133
  // Tailwind Font Size Scale
@@ -161,95 +176,8 @@ export function generateCSSVariables(config) {
161
176
  function generateLayoutRule(token, config) {
162
177
  const { property, value, isArbitrary } = token;
163
178
 
164
- const layoutMap = {
165
- // Display
166
- 'flex': 'display: flex;',
167
- 'grid': 'display: grid;',
168
- 'inline-flex': 'display: inline-flex;',
169
- 'inline-grid': 'display: inline-grid;',
170
- 'block': 'display: block;',
171
- 'inline': 'display: inline-block;',
172
- 'hidden': 'display: none;',
173
-
174
- // Flex Direction
175
- 'row': 'flex-direction: row;',
176
- 'col': 'flex-direction: column;',
177
- 'row-reverse': 'flex-direction: row-reverse;',
178
- 'col-reverse': 'flex-direction: column-reverse;',
179
-
180
- // Flex Wrap
181
- 'wrap': 'flex-wrap: wrap;',
182
- 'nowrap': 'flex-wrap: nowrap;',
183
- 'wrap-reverse': 'flex-wrap: wrap-reverse;',
184
-
185
- // Flex Item
186
- 'grow': 'flex-grow: 1;',
187
- 'grow-0': 'flex-grow: 0;',
188
- 'shrink': 'flex-shrink: 1;',
189
- 'shrink-0': 'flex-shrink: 0;',
190
-
191
- // Grid Auto Flow
192
- 'grid-flow-row': 'grid-auto-flow: row;',
193
- 'grid-flow-col': 'grid-auto-flow: column;',
194
- 'grid-flow-dense': 'grid-auto-flow: dense;',
195
- 'grid-flow-row-dense': 'grid-auto-flow: row dense;',
196
- 'grid-flow-col-dense': 'grid-auto-flow: column dense;',
197
-
198
- // Shorthand Alignment (backwards compat - simple keywords)
199
- 'center': 'justify-content: center; align-items: center;',
200
- 'start': 'justify-content: flex-start; align-items: flex-start;',
201
- 'end': 'justify-content: flex-end; align-items: flex-end;',
202
- 'between': 'justify-content: space-between;',
203
- 'around': 'justify-content: space-around;',
204
- 'evenly': 'justify-content: space-evenly;',
205
-
206
- // Position
207
- 'absolute': 'position: absolute;',
208
- 'relative': 'position: relative;',
209
- 'fixed': 'position: fixed;',
210
- 'sticky': 'position: sticky;',
211
- 'static': 'position: static;',
212
-
213
- // Visibility
214
- 'visible': 'visibility: visible;',
215
- 'invisible': 'visibility: hidden;',
216
-
217
- // Isolation
218
- 'isolate': 'isolation: isolate;',
219
- 'isolate-auto': 'isolation: auto;',
220
-
221
- // Box Sizing
222
- 'box-border': 'box-sizing: border-box;',
223
- 'box-content': 'box-sizing: content-box;',
224
-
225
- // Float
226
- 'float-left': 'float: left;',
227
- 'float-right': 'float: right;',
228
- 'float-none': 'float: none;',
229
-
230
- // Clear
231
- 'clear-left': 'clear: left;',
232
- 'clear-right': 'clear: right;',
233
- 'clear-both': 'clear: both;',
234
- 'clear-none': 'clear: none;',
235
-
236
- // Table - Border Collapse
237
- 'border-collapse': 'border-collapse: collapse;',
238
- 'border-separate': 'border-collapse: separate;',
239
-
240
- // Table - Table Layout
241
- 'table-auto': 'table-layout: auto;',
242
- 'table-fixed': 'table-layout: fixed;',
243
-
244
- // Table - Caption Side
245
- 'caption-top': 'caption-side: top;',
246
- 'caption-bottom': 'caption-side: bottom;',
247
-
248
- // Container
249
- 'container': 'width: 100%; margin-left: auto; margin-right: auto;'
250
- };
251
-
252
179
  // Check for simple layout keywords first (property === value means it's a keyword like 'flex', 'grid', etc.)
180
+ // layoutMap is now imported from definitions
253
181
  if (property === value && layoutMap[property]) {
254
182
  return layoutMap[property];
255
183
  }
@@ -381,27 +309,64 @@ function generateLayoutRule(token, config) {
381
309
  return `object-position: ${cssValue};`;
382
310
  }
383
311
 
312
+ // Percentage adjectives for positioning utilities
313
+ const positioningPercentages = {
314
+ 'full': '100%',
315
+ 'half': '50%',
316
+ 'third': '33.333333%',
317
+ 'third-2x': '66.666667%',
318
+ 'quarter': '25%',
319
+ 'quarter-2x': '50%',
320
+ 'quarter-3x': '75%',
321
+ // Keep fractional values for backwards compatibility
322
+ '1/1': '100%',
323
+ '1/2': '50%',
324
+ '1/3': '33.333333%',
325
+ '2/3': '66.666667%',
326
+ '1/4': '25%',
327
+ '2/4': '50%',
328
+ '3/4': '75%'
329
+ };
330
+
331
+ // Helper function to resolve positioning value
332
+ const resolvePositioningValue = (val, arb) => {
333
+ if (arb) return val;
334
+ if (val === '0') return '0';
335
+ // Check for negative percentage adjective
336
+ if (val.startsWith('-')) {
337
+ const positiveVal = val.substring(1);
338
+ if (positioningPercentages[positiveVal]) {
339
+ return `-${positioningPercentages[positiveVal]}`;
340
+ }
341
+ }
342
+ // Check for percentage adjective
343
+ if (positioningPercentages[val]) {
344
+ return positioningPercentages[val];
345
+ }
346
+ return `var(--s-${val})`;
347
+ };
348
+
384
349
  // Inset (all sides)
385
350
  if (property === 'inset') {
386
- const cssValue = isArbitrary ? value : (value === '0' ? '0' : `var(--s-${value})`);
351
+ const cssValue = resolvePositioningValue(value, isArbitrary);
387
352
  return `inset: ${cssValue};`;
388
353
  }
389
354
 
390
355
  // Individual positioning: top, right, bottom, left
391
356
  if (['top', 'right', 'bottom', 'left'].includes(property)) {
392
- const cssValue = isArbitrary ? value : (value === '0' ? '0' : `var(--s-${value})`);
357
+ const cssValue = resolvePositioningValue(value, isArbitrary);
393
358
  return `${property}: ${cssValue};`;
394
359
  }
395
360
 
396
361
  // Inset X (left + right)
397
362
  if (property === 'inset-x') {
398
- const cssValue = isArbitrary ? value : (value === '0' ? '0' : `var(--s-${value})`);
363
+ const cssValue = resolvePositioningValue(value, isArbitrary);
399
364
  return `left: ${cssValue}; right: ${cssValue};`;
400
365
  }
401
366
 
402
367
  // Inset Y (top + bottom)
403
368
  if (property === 'inset-y') {
404
- const cssValue = isArbitrary ? value : (value === '0' ? '0' : `var(--s-${value})`);
369
+ const cssValue = resolvePositioningValue(value, isArbitrary);
405
370
  return `top: ${cssValue}; bottom: ${cssValue};`;
406
371
  }
407
372
 
@@ -586,6 +551,39 @@ function generateSpaceRule(token, config) {
586
551
  return propMap[property] || '';
587
552
  }
588
553
 
554
+ // Percentage adjectives for sizing utilities (human-readable alternatives to fractions)
555
+ const percentageAdjectives = {
556
+ 'full': '100%',
557
+ 'half': '50%',
558
+ 'third': '33.333333%',
559
+ 'third-2x': '66.666667%',
560
+ 'quarter': '25%',
561
+ 'quarter-2x': '50%',
562
+ 'quarter-3x': '75%',
563
+ // Keep fractional values for backwards compatibility
564
+ '1/1': '100%',
565
+ '1/2': '50%',
566
+ '1/3': '33.333333%',
567
+ '2/3': '66.666667%',
568
+ '1/4': '25%',
569
+ '2/4': '50%',
570
+ '3/4': '75%'
571
+ };
572
+
573
+ // Check if this is a sizing utility with a percentage adjective
574
+ if (sizingProps.includes(property) && percentageAdjectives[value]) {
575
+ const cssVal = percentageAdjectives[value];
576
+ const propMap = {
577
+ 'w': `width: ${cssVal};`,
578
+ 'h': `height: ${cssVal};`,
579
+ 'min-w': `min-width: ${cssVal};`,
580
+ 'max-w': `max-width: ${cssVal};`,
581
+ 'min-h': `min-height: ${cssVal};`,
582
+ 'max-h': `max-height: ${cssVal};`
583
+ };
584
+ return propMap[property] || '';
585
+ }
586
+
589
587
  // Determine the CSS value
590
588
  let cssValue;
591
589
  if (isArbitrary) {
@@ -598,7 +596,7 @@ function generateSpaceRule(token, config) {
598
596
  let baseValue;
599
597
  if (cleanValue.startsWith('tw-')) {
600
598
  const twValue = cleanValue.slice(3); // Remove 'tw-' prefix
601
- baseValue = `var(--tw-${twValue})`;
599
+ baseValue = `var(--tw-${twValue.replace(/\./g, '\\\\.')})`;
602
600
  } else {
603
601
  baseValue = `var(--s-${cleanValue})`;
604
602
  }
@@ -667,100 +665,7 @@ function generateVisualRule(token, config) {
667
665
  const { property, value, isArbitrary } = token;
668
666
 
669
667
  // Static typography keywords
670
- const typographyKeywords = {
671
- // Font Style
672
- 'italic': 'font-style: italic;',
673
- 'not-italic': 'font-style: normal;',
674
-
675
- // Font Stretch
676
- 'font-stretch-condensed': 'font-stretch: condensed;',
677
- 'font-stretch-expanded': 'font-stretch: expanded;',
678
- 'font-stretch-normal': 'font-stretch: normal;',
679
-
680
- // Font Smoothing
681
- 'antialiased': '-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;',
682
- 'subpixel-antialiased': '-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;',
683
-
684
- // Font Variant Numeric
685
- 'normal-nums': 'font-variant-numeric: normal;',
686
- 'ordinal': 'font-variant-numeric: ordinal;',
687
- 'slashed-zero': 'font-variant-numeric: slashed-zero;',
688
- 'lining-nums': 'font-variant-numeric: lining-nums;',
689
- 'oldstyle-nums': 'font-variant-numeric: oldstyle-nums;',
690
- 'proportional-nums': 'font-variant-numeric: proportional-nums;',
691
- 'tabular-nums': 'font-variant-numeric: tabular-nums;',
692
-
693
- // Text Transform
694
- 'uppercase': 'text-transform: uppercase;',
695
- 'lowercase': 'text-transform: lowercase;',
696
- 'capitalize': 'text-transform: capitalize;',
697
- 'normal-case': 'text-transform: none;',
698
-
699
- // Text Decoration Line
700
- 'underline': 'text-decoration-line: underline;',
701
- 'overline': 'text-decoration-line: overline;',
702
- 'line-through': 'text-decoration-line: line-through;',
703
- 'no-underline': 'text-decoration-line: none;',
704
-
705
- // Text Decoration Style
706
- 'decoration-solid': 'text-decoration-style: solid;',
707
- 'decoration-double': 'text-decoration-style: double;',
708
- 'decoration-dotted': 'text-decoration-style: dotted;',
709
- 'decoration-dashed': 'text-decoration-style: dashed;',
710
- 'decoration-wavy': 'text-decoration-style: wavy;',
711
-
712
- // Text Overflow
713
- 'truncate': 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
714
- 'text-ellipsis': 'text-overflow: ellipsis;',
715
- 'text-clip': 'text-overflow: clip;',
716
-
717
- // Text Wrap
718
- 'text-wrap': 'text-wrap: wrap;',
719
- 'text-nowrap': 'text-wrap: nowrap;',
720
- 'text-balance': 'text-wrap: balance;',
721
- 'text-pretty': 'text-wrap: pretty;',
722
-
723
- // Whitespace
724
- 'whitespace-normal': 'white-space: normal;',
725
- 'whitespace-nowrap': 'white-space: nowrap;',
726
- 'whitespace-pre': 'white-space: pre;',
727
- 'whitespace-pre-line': 'white-space: pre-line;',
728
- 'whitespace-pre-wrap': 'white-space: pre-wrap;',
729
- 'whitespace-break-spaces': 'white-space: break-spaces;',
730
-
731
- // Word Break
732
- 'break-normal': 'overflow-wrap: normal; word-break: normal;',
733
- 'break-words': 'overflow-wrap: break-word;',
734
- 'break-all': 'word-break: break-all;',
735
- 'break-keep': 'word-break: keep-all;',
736
-
737
- // Hyphens
738
- 'hyphens-none': 'hyphens: none;',
739
- 'hyphens-manual': 'hyphens: manual;',
740
- 'hyphens-auto': 'hyphens: auto;',
741
-
742
- // Vertical Align
743
- 'align-baseline': 'vertical-align: baseline;',
744
- 'align-top': 'vertical-align: top;',
745
- 'align-middle': 'vertical-align: middle;',
746
- 'align-bottom': 'vertical-align: bottom;',
747
- 'align-text-top': 'vertical-align: text-top;',
748
- 'align-text-bottom': 'vertical-align: text-bottom;',
749
- 'align-sub': 'vertical-align: sub;',
750
- 'align-super': 'vertical-align: super;',
751
-
752
- // List Style Type
753
- 'list-none': 'list-style-type: none;',
754
- 'list-disc': 'list-style-type: disc;',
755
- 'list-decimal': 'list-style-type: decimal;',
756
- 'list-square': 'list-style-type: square;',
757
-
758
- // List Style Position
759
- 'list-inside': 'list-style-position: inside;',
760
- 'list-outside': 'list-style-position: outside;'
761
- };
762
-
763
- // Check static keywords first
668
+ // Check static keywords first (imported from definitions)
764
669
  if (typographyKeywords[property]) {
765
670
  return typographyKeywords[property];
766
671
  }
@@ -768,12 +673,34 @@ function generateVisualRule(token, config) {
768
673
  const rules = {
769
674
  // Background Color
770
675
  'bg': () => {
771
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
676
+ const cssValue = resolveColorValue(value, isArbitrary);
772
677
  return `background-color: ${cssValue};`;
773
678
  },
774
679
 
775
680
  // Background Image
776
681
  'bg-image': () => {
682
+ if (value === 'none') return 'background-image: none;';
683
+
684
+ // Handle gradient definitions
685
+ if (value.startsWith('gradient-to-')) {
686
+ const directionMap = {
687
+ 't': 'to top',
688
+ 'tr': 'to top right',
689
+ 'r': 'to right',
690
+ 'br': 'to bottom right',
691
+ 'b': 'to bottom',
692
+ 'bl': 'to bottom left',
693
+ 'l': 'to left',
694
+ 'tl': 'to top left'
695
+ };
696
+ const directionCode = value.replace('gradient-to-', '');
697
+ const direction = directionMap[directionCode];
698
+
699
+ if (direction) {
700
+ return `background-image: linear-gradient(${direction}, var(--ss-gradient-stops, transparent));`;
701
+ }
702
+ }
703
+
777
704
  const cssValue = isArbitrary ? sanitizeArbitraryValue(`url(${value})`) : `url(${value})`;
778
705
  return `background-image: ${cssValue};`;
779
706
  },
@@ -852,13 +779,29 @@ function generateVisualRule(token, config) {
852
779
  'bg-blend': () => {
853
780
  return `background-blend-mode: ${value};`;
854
781
  },
782
+
783
+ // Gradient Color Stops
784
+ 'from': () => {
785
+ const cssValue = resolveColorValue(value, isArbitrary);
786
+ return `--ss-gradient-from: ${cssValue}; --ss-gradient-to: rgb(255 255 255 / 0); --ss-gradient-stops: var(--ss-gradient-from), var(--ss-gradient-to);`;
787
+ },
788
+
789
+ 'via': () => {
790
+ const cssValue = resolveColorValue(value, isArbitrary);
791
+ return `--ss-gradient-to: rgb(255 255 255 / 0); --ss-gradient-stops: var(--ss-gradient-from), ${cssValue}, var(--ss-gradient-to);`;
792
+ },
793
+
794
+ 'to': () => {
795
+ const cssValue = resolveColorValue(value, isArbitrary);
796
+ return `--ss-gradient-to: ${cssValue};`;
797
+ },
855
798
 
856
799
  // Text color
857
800
  'text': () => {
858
801
  if (['left', 'center', 'right', 'justify'].includes(value)) {
859
802
  return `text-align: ${value};`;
860
803
  }
861
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
804
+ const cssValue = resolveColorValue(value, isArbitrary);
862
805
  return `color: ${cssValue};`;
863
806
  },
864
807
 
@@ -951,7 +894,7 @@ function generateVisualRule(token, config) {
951
894
 
952
895
  // Text decoration color
953
896
  'decoration': () => {
954
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
897
+ const cssValue = resolveColorValue(value, isArbitrary);
955
898
  return `text-decoration-color: ${cssValue};`;
956
899
  },
957
900
 
@@ -975,33 +918,33 @@ function generateVisualRule(token, config) {
975
918
 
976
919
  // Border color
977
920
  'border': () => {
978
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
921
+ const cssValue = resolveColorValue(value, isArbitrary);
979
922
  return `border-color: ${cssValue}; border-style: solid;`;
980
923
  },
981
924
 
982
925
  // Border color - directional
983
926
  'border-t': () => {
984
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
927
+ const cssValue = resolveColorValue(value, isArbitrary);
985
928
  return `border-top-color: ${cssValue}; border-top-style: solid;`;
986
929
  },
987
930
  'border-b': () => {
988
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
931
+ const cssValue = resolveColorValue(value, isArbitrary);
989
932
  return `border-bottom-color: ${cssValue}; border-bottom-style: solid;`;
990
933
  },
991
934
  'border-l': () => {
992
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
935
+ const cssValue = resolveColorValue(value, isArbitrary);
993
936
  return `border-left-color: ${cssValue}; border-left-style: solid;`;
994
937
  },
995
938
  'border-r': () => {
996
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
939
+ const cssValue = resolveColorValue(value, isArbitrary);
997
940
  return `border-right-color: ${cssValue}; border-right-style: solid;`;
998
941
  },
999
942
  'border-x': () => {
1000
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
943
+ const cssValue = resolveColorValue(value, isArbitrary);
1001
944
  return `border-left-color: ${cssValue}; border-right-color: ${cssValue}; border-left-style: solid; border-right-style: solid;`;
1002
945
  },
1003
946
  'border-y': () => {
1004
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
947
+ const cssValue = resolveColorValue(value, isArbitrary);
1005
948
  return `border-top-color: ${cssValue}; border-bottom-color: ${cssValue}; border-top-style: solid; border-bottom-style: solid;`;
1006
949
  },
1007
950
 
@@ -1053,7 +996,7 @@ function generateVisualRule(token, config) {
1053
996
 
1054
997
  // Divide color - all sides
1055
998
  'divide': () => {
1056
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
999
+ const cssValue = resolveColorValue(value, isArbitrary);
1057
1000
  return `border-color: ${cssValue}; border-style: solid;`;
1058
1001
  },
1059
1002
 
@@ -1063,7 +1006,7 @@ function generateVisualRule(token, config) {
1063
1006
  if (value === 'reverse') {
1064
1007
  return '--ss-divide-x-reverse: 1;';
1065
1008
  }
1066
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
1009
+ const cssValue = resolveColorValue(value, isArbitrary);
1067
1010
  return `border-left-color: ${cssValue}; border-right-color: ${cssValue}; border-left-style: solid; border-right-style: solid;`;
1068
1011
  },
1069
1012
  'divide-y': () => {
@@ -1071,7 +1014,7 @@ function generateVisualRule(token, config) {
1071
1014
  if (value === 'reverse') {
1072
1015
  return '--ss-divide-y-reverse: 1;';
1073
1016
  }
1074
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
1017
+ const cssValue = resolveColorValue(value, isArbitrary);
1075
1018
  return `border-top-color: ${cssValue}; border-bottom-color: ${cssValue}; border-top-style: solid; border-bottom-style: solid;`;
1076
1019
  },
1077
1020
 
@@ -1104,7 +1047,7 @@ function generateVisualRule(token, config) {
1104
1047
 
1105
1048
  // Outline Color
1106
1049
  'outline': () => {
1107
- const cssValue = isArbitrary ? value : `var(--c-${value})`;
1050
+ const cssValue = resolveColorValue(value, isArbitrary);
1108
1051
  return `outline-color: ${cssValue};`;
1109
1052
  },
1110
1053
 
@@ -1119,6 +1062,51 @@ function generateVisualRule(token, config) {
1119
1062
  return `outline-offset: ${cssValue};`;
1120
1063
  },
1121
1064
 
1065
+ // Ring Width
1066
+ 'ring-w': () => {
1067
+ const cssValue = isArbitrary ? value : `var(--s-${value})`;
1068
+ return `--ss-ring-width: ${cssValue};`;
1069
+ },
1070
+
1071
+ // Ring Color
1072
+ 'ring-color': () => {
1073
+ const cssValue = resolveColorValue(value, isArbitrary);
1074
+ return `--ss-ring-color: ${cssValue};`;
1075
+ },
1076
+
1077
+ // Ring Offset Width
1078
+ 'ring-offset': () => {
1079
+ const cssValue = isArbitrary ? value : `var(--s-${value})`;
1080
+ return `--ss-ring-offset-width: ${cssValue};`;
1081
+ },
1082
+
1083
+ // Ring Offset Color
1084
+ 'ring-offset-color': () => {
1085
+ const cssValue = resolveColorValue(value, isArbitrary);
1086
+ return `--ss-ring-offset-color: ${cssValue};`;
1087
+ },
1088
+
1089
+ // Ring (Main utility)
1090
+ 'ring': () => {
1091
+ if (value === 'none') {
1092
+ return 'box-shadow: 0 0 #0000;';
1093
+ }
1094
+
1095
+ const ringPresets = {
1096
+ 'thin': '1px',
1097
+ 'regular': '2px',
1098
+ 'small': '4px',
1099
+ 'medium': '6px',
1100
+ 'big': '8px'
1101
+ };
1102
+
1103
+ const width = isArbitrary ? value : (ringPresets[value] || (parseInt(value) ? `${value}px` : `var(--s-${value})`));
1104
+
1105
+ // We set both the variable and the box-shadow that uses it
1106
+ // 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);`;
1108
+ },
1109
+
1122
1110
  // Box shadow
1123
1111
  'shadow': () => {
1124
1112
  return `box-shadow: var(--shadow-${value});`;
@@ -1596,11 +1584,31 @@ function generateVisualRule(token, config) {
1596
1584
  'translate-x': () => {
1597
1585
  const translatePresets = {
1598
1586
  'full': '100%',
1587
+ 'half': '50%',
1588
+ 'third': '33.333333%',
1589
+ 'third-2x': '66.666667%',
1590
+ 'quarter': '25%',
1591
+ 'quarter-2x': '50%',
1592
+ 'quarter-3x': '75%',
1593
+ // Legacy fraction format (for backward compatibility)
1599
1594
  '1/2': '50%',
1600
1595
  '1/3': '33.333333%',
1601
1596
  '2/3': '66.666667%',
1602
1597
  '1/4': '25%',
1603
- '3/4': '75%'
1598
+ '3/4': '75%',
1599
+ // Negatives (prefixed with -)
1600
+ '-full': '-100%',
1601
+ '-half': '-50%',
1602
+ '-third': '-33.333333%',
1603
+ '-third-2x': '-66.666667%',
1604
+ '-quarter': '-25%',
1605
+ '-quarter-2x': '-50%',
1606
+ '-quarter-3x': '-75%',
1607
+ '-1/2': '-50%',
1608
+ '-1/3': '-33.333333%',
1609
+ '-2/3': '-66.666667%',
1610
+ '-1/4': '-25%',
1611
+ '-3/4': '-75%'
1604
1612
  };
1605
1613
  const cssValue = isArbitrary ? value : (translatePresets[value] || `var(--s-${value})`);
1606
1614
  return `transform: translateX(${cssValue});`;
@@ -1609,42 +1617,36 @@ function generateVisualRule(token, config) {
1609
1617
  'translate-y': () => {
1610
1618
  const translatePresets = {
1611
1619
  'full': '100%',
1620
+ 'half': '50%',
1621
+ 'third': '33.333333%',
1622
+ 'third-2x': '66.666667%',
1623
+ 'quarter': '25%',
1624
+ 'quarter-2x': '50%',
1625
+ 'quarter-3x': '75%',
1626
+ // Legacy fraction format (for backward compatibility)
1612
1627
  '1/2': '50%',
1613
1628
  '1/3': '33.333333%',
1614
1629
  '2/3': '66.666667%',
1615
1630
  '1/4': '25%',
1616
- '3/4': '75%'
1631
+ '3/4': '75%',
1632
+ // Negatives (prefixed with -)
1633
+ '-full': '-100%',
1634
+ '-half': '-50%',
1635
+ '-third': '-33.333333%',
1636
+ '-third-2x': '-66.666667%',
1637
+ '-quarter': '-25%',
1638
+ '-quarter-2x': '-50%',
1639
+ '-quarter-3x': '-75%',
1640
+ '-1/2': '-50%',
1641
+ '-1/3': '-33.333333%',
1642
+ '-2/3': '-66.666667%',
1643
+ '-1/4': '-25%',
1644
+ '-3/4': '-75%'
1617
1645
  };
1618
1646
  const cssValue = isArbitrary ? value : (translatePresets[value] || `var(--s-${value})`);
1619
1647
  return `transform: translateY(${cssValue});`;
1620
1648
  },
1621
1649
 
1622
- '-translate-x': () => {
1623
- const translatePresets = {
1624
- 'full': '-100%',
1625
- '1/2': '-50%',
1626
- '1/3': '-33.333333%',
1627
- '2/3': '-66.666667%',
1628
- '1/4': '-25%',
1629
- '3/4': '-75%'
1630
- };
1631
- const cssValue = isArbitrary ? `-${value}` : (translatePresets[value] || `calc(var(--s-${value}) * -1)`);
1632
- return `transform: translateX(${cssValue});`;
1633
- },
1634
-
1635
- '-translate-y': () => {
1636
- const translatePresets = {
1637
- 'full': '-100%',
1638
- '1/2': '-50%',
1639
- '1/3': '-33.333333%',
1640
- '2/3': '-66.666667%',
1641
- '1/4': '-25%',
1642
- '3/4': '-75%'
1643
- };
1644
- const cssValue = isArbitrary ? `-${value}` : (translatePresets[value] || `calc(var(--s-${value}) * -1)`);
1645
- return `transform: translateY(${cssValue});`;
1646
- },
1647
-
1648
1650
  // Skew
1649
1651
  'skew-x': () => {
1650
1652
  const cssValue = isArbitrary ? value : `${value}deg`;
@@ -1984,7 +1986,7 @@ function generateVisualRule(token, config) {
1984
1986
  * @param {Object} config - Configuration object
1985
1987
  * @param {boolean} skipDarkWrapper - If true, don't add dark mode wrapper (used when generating inside dark block)
1986
1988
  */
1987
- export function generateRule(token, config, skipDarkWrapper = false) {
1989
+ export function generateRule(token, config, skipDarkWrapper = false, interactIds = new Set()) {
1988
1990
  const { raw, attrType, breakpoint, state } = token;
1989
1991
 
1990
1992
  let cssDeclaration = '';
@@ -2020,9 +2022,59 @@ export function generateRule(token, config, skipDarkWrapper = false) {
2020
2022
  if (state && state !== 'dark') {
2021
2023
  if (isDivide) {
2022
2024
  // For divide utilities, add state to the element after tilde
2025
+ // Divide utilities don't support group/peer states yet to avoid complexity
2023
2026
  selector = `[${attrType}~="${raw}"] > :not([hidden]) ~ :not([hidden]):${state}`;
2024
2027
  } else {
2025
- selector += `:${state}`;
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'
2034
+ };
2035
+ return map[s] || `:${s}`;
2036
+ };
2037
+
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);
2073
+ }
2074
+ }
2075
+ }
2076
+
2077
+ selector = selectors.join(',\n');
2026
2078
  }
2027
2079
  }
2028
2080
 
@@ -2110,6 +2162,14 @@ export function generateCSS(tokens, config) {
2110
2162
  }
2111
2163
  }
2112
2164
 
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);
2170
+ }
2171
+ }
2172
+
2113
2173
  // Track display properties to handle conflicts like Tailwind
2114
2174
  // When responsive display property conflicts with base display property on the same element,
2115
2175
  // we need to add reset rules in the responsive media query
@@ -2131,7 +2191,7 @@ export function generateCSS(tokens, config) {
2131
2191
 
2132
2192
  // Generate base rules
2133
2193
  for (const token of baseTokens) {
2134
- css += generateRule(token, config);
2194
+ css += generateRule(token, config, false, interactIds);
2135
2195
  }
2136
2196
 
2137
2197
  // Generate responsive rules
@@ -2169,7 +2229,7 @@ export function generateCSS(tokens, config) {
2169
2229
 
2170
2230
  // Generate responsive token rules
2171
2231
  for (const token of bpTokens) {
2172
- css += ' ' + generateRule(token, config);
2232
+ css += ' ' + generateRule(token, config, false, interactIds);
2173
2233
  }
2174
2234
  css += '}\n';
2175
2235
  }
@@ -2185,14 +2245,14 @@ export function generateCSS(tokens, config) {
2185
2245
  css += `\n/* Dark Mode (prefers-color-scheme) */\n`;
2186
2246
  css += `@media (prefers-color-scheme: dark) {\n`;
2187
2247
  for (const token of darkTokens) {
2188
- css += ' ' + generateRule(token, config, true);
2248
+ css += ' ' + generateRule(token, config, true, interactIds);
2189
2249
  }
2190
2250
  css += '}\n';
2191
2251
  } else {
2192
2252
  // Selector strategy (.dark class or custom selector)
2193
2253
  css += `\n/* Dark Mode (${darkSelector}) */\n`;
2194
2254
  for (const token of darkTokens) {
2195
- const baseRule = generateRule(token, config, true);
2255
+ const baseRule = generateRule(token, config, true, interactIds);
2196
2256
  // Wrap selector with dark parent
2197
2257
  const wrappedRule = baseRule.replace(/^(\[[^\]]+\])/, `${darkSelector} $1`);
2198
2258
  css += wrappedRule;