@bookklik/senangstart-css 0.2.8 → 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 (46) hide show
  1. package/dist/senangstart-css.js +2510 -1927
  2. package/dist/senangstart-css.min.js +211 -208
  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 +211 -208
  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 +1 -1
  19. package/scripts/convert-tailwind.js +191 -68
  20. package/scripts/generate-llms-txt.js +28 -0
  21. package/src/cdn/senangstart-engine.js +37 -1927
  22. package/src/cdn/tw-conversion-engine.js +203 -74
  23. package/src/compiler/generators/css.js +300 -54
  24. package/src/compiler/parser.js +14 -4
  25. package/src/config/defaults.js +1 -1
  26. package/src/core/constants.js +5 -3
  27. package/src/definitions/index.js +3 -2
  28. package/src/definitions/layout.js +2 -2
  29. package/src/definitions/space.js +45 -19
  30. package/src/index.js +47 -0
  31. package/templates/senangstart.config.js +1 -1
  32. package/tests/helpers/test-utils.js +1 -1
  33. package/tests/integration/compiler.test.js +12 -1
  34. package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
  35. package/tests/unit/compiler/generators/css.test.js +1418 -1
  36. package/tests/unit/compiler/generators/preflight.test.js +31 -0
  37. package/tests/unit/compiler/parser.test.js +26 -0
  38. package/tests/unit/config/defaults.test.js +2 -2
  39. package/tests/unit/convert-tailwind.cli.test.js +95 -0
  40. package/tests/unit/convert-tailwind.coverage.test.js +225 -0
  41. package/tests/unit/convert-tailwind.test.js +49 -20
  42. package/tests/unit/core/tokenizer-core.test.js +102 -0
  43. package/tests/unit/definitions/index.test.js +108 -0
  44. package/tests/unit/definitions/layout_definitions.test.js +40 -0
  45. package/tests/unit/utils/common.test.js +26 -0
  46. package/scripts/bundle-jit.js +0 -45
@@ -73,6 +73,34 @@ Example: `space="p:small tab:p:medium lap:p:big"`
73
73
  - `active:`, `disabled:`, `group-hover:`
74
74
  - `dark:`: `visual="bg:white dark:bg:slate-900"`
75
75
 
76
+ ## State Capabilities & Interaction
77
+
78
+ React to parent ("Group") or sibling ("Peer") states across all attributes.
79
+
80
+ ### 1. Group Capabilities
81
+ Add these to the `layout` attribute of a parent element:
82
+ - `hoverable`: Adds `:hover` support.
83
+ - `focusable`: Adds `:focus-within` support.
84
+ - `pressable`: Adds `:active` support.
85
+ - `expandable`: Reacts to `[aria-expanded="true"]`.
86
+ - `selectable`: Reacts to `[aria-selected="true"]`.
87
+
88
+ **Example:**
89
+ ```html
90
+ <div layout="flex hoverable" visual="bg:white">
91
+ <p visual="text:grey hover:text:primary">Hover parent to change me</p>
92
+ </div>
93
+ ```
94
+
95
+ ### 2. Peer Interactions
96
+ Use `interact="[id]"` on the trigger and `listens="[id]"` on the receiver.
97
+
98
+ **Example:**
99
+ ```html
100
+ <button layout="hoverable" interact="my-menu">Trigger</button>
101
+ <div layout="hidden hover:block" listens="my-menu">Target</div>
102
+ ```
103
+
76
104
  ## Configuration
77
105
  Use `senangstart.config.js` to override defaults.
78
106
  ```js
@@ -12,7 +12,7 @@ Complete reference for the color palette.
12
12
  | `dark` | #3E4A5D | **Brand dark** |
13
13
  | `light` | #DBEAFE | **Brand light** |
14
14
  | `primary` | #2563EB | **Brand primary** |
15
- | `secondary` | #DBEAFE | **Brand secondary** |
15
+ | `secondary` | #1E40AF | **Brand secondary** |
16
16
  | `success` | #10B981 | Positive feedback |
17
17
  | `warning` | #F59E0B | Cautions |
18
18
  | `danger` | #EF4444 | Errors |
@@ -104,7 +104,7 @@ Add custom colors in `senangstart.config.js`:
104
104
  export default {
105
105
  theme: {
106
106
  colors: {
107
- 'brand': '#8B5CF6',
107
+ 'brand': '#38BDF8',
108
108
  'accent': '#EC4899',
109
109
  'muted': '#9CA3AF',
110
110
  'surface': '#F9FAFB'
@@ -17,16 +17,16 @@ space="h:[value]"
17
17
 
18
18
  ## Scale Values
19
19
 
20
- `none`, `thin`, `regular`, `thick`, `tiny`, `tiny-2x`, `small`, `small-2x`, `small-3x`, `small-4x`, `medium`, `medium-2x`, `medium-3x`, `medium-4x`, `large`, `large-2x`, `large-3x`, `large-4x`, `big`, `big-2x`, `big-3x`, `big-4x`, `giant`, `giant-2x`, `giant-3x`, `giant-4x`, `vast`, `vast-2x`, `vast-3x`, `vast-4x`, `vast-5x`, `vast-6x`, `vast-7x`, `vast-8x`, `vast-9x`, `vast-10x`, `min`, `max`, `fit`
20
+ `none`, `thin`, `regular`, `thick`, `tiny`, `tiny-2x`, `small`, `small-2x`, `small-3x`, `small-4x`, `medium`, `medium-2x`, `medium-3x`, `medium-4x`, `large`, `large-2x`, `large-3x`, `large-4x`, `big`, `big-2x`, `big-3x`, `big-4x`, `giant`, `giant-2x`, `giant-3x`, `giant-4x`, `vast`, `vast-2x`, `vast-3x`, `vast-4x`, `vast-5x`, `vast-6x`, `vast-7x`, `vast-8x`, `vast-9x`, `vast-10x`, `min`, `max`, `fit`, `full`, `half`, `third`, `third-2x`, `quarter`, `quarter-2x`, `quarter-3x`, `1/1`, `1/2`, `1/3`, `2/3`, `1/4`, `2/4`, `3/4`
21
21
 
22
22
  ## Examples
23
23
 
24
24
  ```html
25
+ <div space="h:full">Full height</div>
26
+ <div space="h:half">Half height</div>
25
27
  <div space="h:[100vh]">Full viewport height</div>
26
28
  <div space="min-h:[400px]">Min height</div>
27
29
  <div space="h:max">Content height</div>
28
- <div space="max-h:max">Max content height</div>
29
- <div space="min-h:min">Min content height</div>
30
30
  ```
31
31
 
32
32
  ## Preview
@@ -36,11 +36,11 @@ space="h:[value]"
36
36
  ### Height Control
37
37
 
38
38
  <div layout="flex col" space="g:medium">
39
- <p space="m:none" visual="text:neutral-600 dark:text:neutral-400 text-sm"><code>space="h:[100%]"</code> - Set fixed heights</p>
39
+ <p space="m:none" visual="text:neutral-600 dark:text:neutral-400 text-sm"><code>space="h:full"</code> - Set fixed heights</p>
40
40
  <div layout="flex" space="g:small p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium" style="height: 120px;">
41
- <div space="h:[100%] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[100%]</div>
42
- <div space="h:[80px] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[80px]</div>
43
- <div space="h:[60px] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[60px]</div>
41
+ <div space="h:full p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:full</div>
42
+ <div space="h:third-2x p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:third-2x</div>
43
+ <div space="h:half p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:half</div>
44
44
  </div>
45
45
  </div>
46
46
 
@@ -49,9 +49,9 @@ space="h:[value]"
49
49
 
50
50
  ```html
51
51
  <div layout="flex" space="g:small p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium" style="height: 120px;">
52
- <div space="h:[100%] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[100%]</div>
53
- <div space="h:[80px] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[80px]</div>
54
- <div space="h:[60px] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[60px]</div>
52
+ <div space="h:full p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:full</div>
53
+ <div space="h:third-2x p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:third-2x</div>
54
+ <div space="h:half p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:half</div>
55
55
  </div>
56
56
  ```
57
57
 
@@ -17,17 +17,17 @@ space="w:[value]"
17
17
 
18
18
  ## Scale Values
19
19
 
20
- `none`, `thin`, `regular`, `thick`, `tiny`, `tiny-2x`, `small`, `small-2x`, `small-3x`, `small-4x`, `medium`, `medium-2x`, `medium-3x`, `medium-4x`, `large`, `large-2x`, `large-3x`, `large-4x`, `big`, `big-2x`, `big-3x`, `big-4x`, `giant`, `giant-2x`, `giant-3x`, `giant-4x`, `vast`, `vast-2x`, `vast-3x`, `vast-4x`, `vast-5x`, `vast-6x`, `vast-7x`, `vast-8x`, `vast-9x`, `vast-10x`, `min`, `max`, `fit`
20
+ `none`, `thin`, `regular`, `thick`, `tiny`, `tiny-2x`, `small`, `small-2x`, `small-3x`, `small-4x`, `medium`, `medium-2x`, `medium-3x`, `medium-4x`, `large`, `large-2x`, `large-3x`, `large-4x`, `big`, `big-2x`, `big-3x`, `big-4x`, `giant`, `giant-2x`, `giant-3x`, `giant-4x`, `vast`, `vast-2x`, `vast-3x`, `vast-4x`, `vast-5x`, `vast-6x`, `vast-7x`, `vast-8x`, `vast-9x`, `vast-10x`, `min`, `max`, `fit`, `full`, `half`, `third`, `third-2x`, `quarter`, `quarter-2x`, `quarter-3x`, `1/1`, `1/2`, `1/3`, `2/3`, `1/4`, `2/4`, `3/4`
21
21
 
22
22
  ## Examples
23
23
 
24
24
  ```html
25
- <div space="w:[100%]">Full width</div>
25
+ <div space="w:full">Full width</div>
26
+ <div space="w:half">Half width</div>
27
+ <div space="w:third">Third width</div>
28
+ <div space="w:quarter-3x">Three quarters</div>
26
29
  <div space="max-w:[1200px]">Max width container</div>
27
- <div space="min-w:[300px]">Min width</div>
28
30
  <div space="w:max">Content width</div>
29
- <div space="max-w:max">Max content width</div>
30
- <div space="min-w:min">Min content width</div>
31
31
  ```
32
32
 
33
33
  ## Preview
@@ -37,11 +37,11 @@ space="w:[value]"
37
37
  ### Width Control
38
38
 
39
39
  <div layout="flex col" space="g:medium">
40
- <p space="m:none" visual="text:neutral-600 dark:text:neutral-400 text-sm"><code>space="w:[100%]"</code> - Set fixed or percentage widths</p>
40
+ <p space="m:none" visual="text:neutral-600 dark:text:neutral-400 text-sm"><code>space="w:full"</code> - Set fixed or percentage widths</p>
41
41
  <div layout="flex col" space="g:small p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
42
- <div space="w:[100%] p:small" visual="bg:primary text:white rounded:small">w:[100%]</div>
43
- <div space="w:[75%] p:small" visual="bg:primary text:white rounded:small">w:[75%]</div>
44
- <div space="w:[50%] p:small" visual="bg:primary text:white rounded:small">w:[50%]</div>
42
+ <div space="w:full p:small" visual="bg:primary text:white rounded:small">w:full</div>
43
+ <div space="w:quarter-3x p:small" visual="bg:primary text:white rounded:small">w:quarter-3x</div>
44
+ <div space="w:half p:small" visual="bg:primary text:white rounded:small">w:half</div>
45
45
  </div>
46
46
  </div>
47
47
 
@@ -50,9 +50,9 @@ space="w:[value]"
50
50
 
51
51
  ```html
52
52
  <div layout="flex col" space="g:small p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
53
- <div space="w:[100%] p:small" visual="bg:primary text:white rounded:small">w:[100%]</div>
54
- <div space="w:[75%] p:small" visual="bg:primary text:white rounded:small">w:[75%]</div>
55
- <div space="w:[50%] p:small" visual="bg:primary text:white rounded:small">w:[50%]</div>
53
+ <div space="w:full p:small" visual="bg:primary text:white rounded:small">w:full</div>
54
+ <div space="w:quarter-3x p:small" visual="bg:primary text:white rounded:small">w:quarter-3x</div>
55
+ <div space="w:half p:small" visual="bg:primary text:white rounded:small">w:half</div>
56
56
  </div>
57
57
  ```
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookklik/senangstart-css",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Fluent Style Utilities for Humans and AI",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -80,7 +80,7 @@
80
80
  --c-dark: #3E4A5D;
81
81
  --c-light: #DBEAFE;
82
82
  --c-primary: #2563EB;
83
- --c-secondary: #DBEAFE;
83
+ --c-secondary: #1E40AF;
84
84
  --c-success: #10B981;
85
85
  --c-warning: #F59E0B;
86
86
  --c-danger: #EF4444;
@@ -13,54 +13,96 @@ import { readFileSync, writeFileSync } from 'fs';
13
13
  import { argv } from 'process';
14
14
  import { resolve, join, sep } from 'path';
15
15
 
16
+ /**
17
+ * Basic path validation to ensure we're within the allowed directory
18
+ * @param {string} filePath - Path to validate
19
+ */
20
+ function isValidFilePath(filePath) {
21
+ try {
22
+ const resolvedPath = resolve(filePath);
23
+ const rootPath = process.cwd();
24
+ // Case-insensitive comparison for Windows
25
+ if (process.platform === 'win32') {
26
+ return resolvedPath.toLowerCase().startsWith(rootPath.toLowerCase());
27
+ }
28
+ return resolvedPath.startsWith(rootPath);
29
+ } catch (e) {
30
+ return false;
31
+ }
32
+ }
33
+
16
34
  // ======================
17
35
  // SPACING SCALE MAPPING
18
36
  // ======================
19
37
  // Maps Tailwind spacing numbers to SenangStart semantic scale
20
38
  const spacingScale = {
21
39
  '0': 'none',
22
- 'px': '[1px]',
23
- '0.5': 'tiny',
40
+ 'px': 'thin',
41
+ '0.5': 'regular',
24
42
  '1': 'tiny',
25
- '1.5': 'tiny',
26
- '2': 'tiny',
27
- '2.5': 'small',
28
- '3': 'small',
29
- '3.5': 'small',
30
- '4': 'small',
31
- '5': 'medium',
32
- '6': 'medium',
33
- '7': 'medium',
34
- '8': 'big',
35
- '9': 'big',
36
- '10': 'big',
37
- '11': 'big',
38
- '12': 'giant',
39
- '14': 'giant',
40
- '16': 'giant',
41
- '20': 'vast',
42
- '24': 'vast',
43
- '28': 'vast',
44
- '32': 'vast',
45
- '36': 'vast',
43
+ '1.5': 'tiny-2x',
44
+ '2': 'small',
45
+ '2.5': 'small-2x',
46
+ '3': 'small-3x',
47
+ '3.5': 'small-4x',
48
+ '4': 'medium',
49
+ '5': 'medium-2x',
50
+ '6': 'medium-3x',
51
+ '7': 'medium-4x',
52
+ '8': 'large',
53
+ '9': 'large-2x',
54
+ '10': 'large-3x',
55
+ '11': 'large-4x',
56
+ '12': 'big',
57
+ '14': 'big-2x',
58
+ '16': 'big-3x',
59
+ '20': 'big-4x',
60
+ '24': 'giant',
61
+ '28': 'giant-2x',
62
+ '32': 'giant-3x',
63
+ '36': 'giant-4x',
46
64
  '40': 'vast',
47
- '44': 'vast',
48
- '48': 'vast',
49
- '52': 'vast',
50
- '56': 'vast',
51
- '60': 'vast',
52
- '64': 'vast',
53
- '72': 'vast',
54
- '80': 'vast',
55
- '96': 'vast',
56
- 'full': '[100%]',
65
+ '44': 'vast-2x',
66
+ '48': 'vast-3x',
67
+ '52': 'vast-4x',
68
+ '56': 'vast-5x',
69
+ '60': 'vast-6x',
70
+ '64': 'vast-7x',
71
+ '72': 'vast-8x',
72
+ '80': 'vast-9x',
73
+ '96': 'vast-10x',
74
+ 'full': 'full',
57
75
  'screen': '[100vw]',
58
- 'min': '[min-content]',
59
- 'max': '[max-content]',
60
- 'fit': '[fit-content]',
76
+ 'min': 'min',
77
+ 'max': 'max',
78
+ 'fit': 'fit',
61
79
  'auto': 'auto'
62
80
  };
63
81
 
82
+ // ======================
83
+ // PERCENTAGE ADJECTIVES
84
+ // ======================
85
+ // Maps Tailwind fractional values to SenangStart percentage adjectives
86
+ const percentageAdjectives = {
87
+ '1/2': 'half',
88
+ '1/3': 'third',
89
+ '2/3': 'third-2x',
90
+ '1/4': 'quarter',
91
+ '2/4': 'quarter-2x',
92
+ '3/4': 'quarter-3x',
93
+ // Less common fractions - keep as arbitrary for precision
94
+ '1/5': '[20%]',
95
+ '2/5': '[40%]',
96
+ '3/5': '[60%]',
97
+ '4/5': '[80%]',
98
+ '1/6': '[16.666667%]',
99
+ '5/6': '[83.333333%]',
100
+ '1/12': '[8.333333%]',
101
+ '5/12': '[41.666667%]',
102
+ '7/12': '[58.333333%]',
103
+ '11/12': '[91.666667%]'
104
+ };
105
+
64
106
  // ======================
65
107
  // BORDER RADIUS MAPPING
66
108
  // ======================
@@ -352,24 +394,32 @@ function getSpacingScale(value, options = {}) {
352
394
  if (value.startsWith('[') && value.endsWith(']')) {
353
395
  return value;
354
396
  }
355
- // Special keywords that don't map to tw- scale
397
+ // Special keywords that use semantic values
356
398
  const specialKeywords = ['full', 'screen', 'min', 'max', 'fit', 'auto'];
357
399
  if (specialKeywords.includes(value)) {
358
400
  const specialMap = {
359
- 'full': '[100%]',
401
+ 'full': 'full',
360
402
  'screen': '[100vw]',
361
- 'min': '[min-content]',
362
- 'max': '[max-content]',
363
- 'fit': '[fit-content]',
403
+ 'min': 'min',
404
+ 'max': 'max',
405
+ 'fit': 'fit',
364
406
  'auto': 'auto'
365
407
  };
366
408
  return specialMap[value];
367
409
  }
410
+ // Check for percentage adjectives
411
+ if (percentageAdjectives[value]) {
412
+ return percentageAdjectives[value];
413
+ }
368
414
  // Output tw- prefix for numeric scale
369
415
  return `tw-${value}`;
370
416
  }
371
417
 
372
418
  // Semantic mode (default)
419
+ // Check for percentage adjectives first
420
+ if (percentageAdjectives[value]) {
421
+ return percentageAdjectives[value];
422
+ }
373
423
  if (spacingScale[value]) {
374
424
  return spacingScale[value];
375
425
  }
@@ -386,11 +436,12 @@ function getSpacingScale(value, options = {}) {
386
436
  // ======================
387
437
  const borderWidthScale = {
388
438
  '0': 'none',
389
- '1': 'thin',
439
+ 'px': 'thin',
440
+ '1': 'tiny',
390
441
  '2': 'regular',
391
442
  '3': 'thick',
392
- '4': 'tiny',
393
- '8': 'small'
443
+ '4': 'medium',
444
+ '8': 'large'
394
445
  };
395
446
 
396
447
  function getBorderWidth(value, options = {}) {
@@ -480,6 +531,19 @@ function convertClass(twClass, options = {}) {
480
531
  value: prefix + "border:" + borderColorMatch[1],
481
532
  };
482
533
 
534
+ // Directional border colors (border-t-*, border-r-*, border-b-*, border-l-*)
535
+ const borderSideColorMatch = baseClass.match(
536
+ /^border-([trbl])-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black|transparent|current|inherit)(?:-\d+)?)$/
537
+ );
538
+ if (borderSideColorMatch) {
539
+ const side = borderSideColorMatch[1]; // t, r, b, or l
540
+ const colorVal = borderSideColorMatch[2];
541
+ return {
542
+ category: 'visual',
543
+ value: prefix + `border-${side}:${colorVal}`,
544
+ };
545
+ }
546
+
483
547
  // Padding
484
548
  const paddingMatch = baseClass.match(/^p([trblxy])?-(.+)$/);
485
549
  if (paddingMatch) {
@@ -528,7 +592,11 @@ function convertClass(twClass, options = {}) {
528
592
  const prop = widthMatch[1];
529
593
  const rawVal = widthMatch[2];
530
594
  // Special width values
531
- const specialWidthVals = { 'max': '[max-content]', 'min': '[min-content]', 'fit': '[fit-content]', 'prose': '[65ch]' };
595
+ const specialWidthVals = { 'max': 'max', 'min': 'min', 'fit': 'fit', 'prose': '[65ch]' };
596
+ // Check for percentage adjective first (e.g., 1/2 → half)
597
+ if (percentageAdjectives[rawVal]) {
598
+ return { category: 'space', value: prefix + prop + ":" + percentageAdjectives[rawVal] };
599
+ }
532
600
  const val = specialWidthVals[rawVal] || getSpacingScale(rawVal, options);
533
601
  return { category: 'space', value: prefix + prop + ":" + val };
534
602
  }
@@ -536,7 +604,11 @@ function convertClass(twClass, options = {}) {
536
604
  if (heightMatch) {
537
605
  const prop = heightMatch[1];
538
606
  const rawVal = heightMatch[2];
539
- const specialHeightVals = { 'screen': '[100vh]', 'svh': '[100svh]', 'lvh': '[100lvh]', 'dvh': '[100dvh]', 'max': '[max-content]', 'min': '[min-content]', 'fit': '[fit-content]' };
607
+ const specialHeightVals = { 'screen': '[100vh]', 'svh': '[100svh]', 'lvh': '[100lvh]', 'dvh': '[100dvh]', 'max': 'max', 'min': 'min', 'fit': 'fit' };
608
+ // Check for percentage adjective first (e.g., 1/2 → half)
609
+ if (percentageAdjectives[rawVal]) {
610
+ return { category: 'space', value: prefix + prop + ":" + percentageAdjectives[rawVal] };
611
+ }
540
612
  const val = specialHeightVals[rawVal] || getSpacingScale(rawVal, options);
541
613
  return { category: 'space', value: prefix + prop + ":" + val };
542
614
  }
@@ -587,7 +659,7 @@ function convertClass(twClass, options = {}) {
587
659
  const width = borderWidthMatch[2] || "1";
588
660
  return {
589
661
  category: 'visual',
590
- value: prefix + "border" + side + ":[" + width + "px]",
662
+ value: prefix + "border" + side + ":" + getBorderWidth(width, options),
591
663
  };
592
664
  }
593
665
 
@@ -621,6 +693,26 @@ function convertClass(twClass, options = {}) {
621
693
  return { category: 'layout', value: prefix + "row-span:" + rowSpanMatch[1] };
622
694
  }
623
695
 
696
+ // Positional properties (top-0, right-0, bottom-0, left-0, inset-0, etc.)
697
+ // Includes fraction support: left-1/2, top-1/3, etc.
698
+ const positionMatch = baseClass.match(/^(top|right|bottom|left|inset|inset-x|inset-y)-(\d+|px|auto|full|1\/2|1\/3|2\/3|1\/4|2\/4|3\/4|\[.+\])$/);
699
+ if (positionMatch) {
700
+ const prop = positionMatch[1];
701
+ let val = positionMatch[2];
702
+ // Handle 0 specially
703
+ if (val === '0') {
704
+ val = 'none';
705
+ } else if (val.startsWith('[') && val.endsWith(']')) {
706
+ // Keep arbitrary values as-is
707
+ } else if (percentageAdjectives[val]) {
708
+ // Map fractions to semantic names (1/2 → half, etc.)
709
+ val = percentageAdjectives[val];
710
+ } else {
711
+ val = getSpacingScale(val, options);
712
+ }
713
+ return { category: "layout", value: prefix + prop + ":" + val };
714
+ }
715
+
624
716
  // Opacity
625
717
  const opacityMatch = baseClass.match(/^opacity-(\d+)$/);
626
718
  if (opacityMatch) {
@@ -655,19 +747,19 @@ function convertClass(twClass, options = {}) {
655
747
  // 3D TRANSFORMS
656
748
  // ======================
657
749
 
658
- // Perspective
659
- const perspectiveMatch = baseClass.match(/^perspective(?:-(.+))?$/);
660
- if (perspectiveMatch) {
661
- const val = perspectiveMatch[1] || 'normal';
662
- return { category: 'visual', value: prefix + "perspective:" + val };
663
- }
664
-
665
750
  // Perspective origin
666
751
  const perspectiveOriginMatch = baseClass.match(/^perspective-origin(?:-(.+))?$/);
667
752
  if (perspectiveOriginMatch) {
668
753
  const val = perspectiveOriginMatch[1] || 'center';
669
754
  return { category: 'visual', value: prefix + "perspective-origin:" + val };
670
755
  }
756
+
757
+ // Perspective
758
+ const perspectiveMatch = baseClass.match(/^perspective(?:-(.+))?$/);
759
+ if (perspectiveMatch) {
760
+ const val = perspectiveMatch[1] || 'normal';
761
+ return { category: 'visual', value: prefix + "perspective:" + val };
762
+ }
671
763
 
672
764
  // Rotate X/Y/Z (3D rotation)
673
765
  const rotateAxisMatch = baseClass.match(/^rotate-([xyz])(?:-(.+))?$/);
@@ -707,11 +799,32 @@ function convertClass(twClass, options = {}) {
707
799
  return { category: 'visual', value: prefix + `skew-${axis}:${val}` };
708
800
  }
709
801
 
710
- // Translate X/Y/Z
711
- const translateAxisMatch = baseClass.match(/^translate-([xyz])(?:-(.+))?$/);
802
+ // Translate X/Y/Z - includes negative values and fractions
803
+ // Positive: translate-x-1/2 translate-x:half
804
+ // Negative: -translate-x-1/2 → translate-x:-half
805
+ const translateAxisMatch = baseClass.match(/^(-?)translate-([xyz])(?:-(.+))?$/);
712
806
  if (translateAxisMatch) {
713
- const axis = translateAxisMatch[1].toLowerCase();
714
- const val = translateAxisMatch[2] || '0';
807
+ const isNeg = translateAxisMatch[1] === '-';
808
+ const axis = translateAxisMatch[2].toLowerCase();
809
+ let val = translateAxisMatch[3] || '0';
810
+
811
+ // Map fractions and values
812
+ if (val.startsWith('[') && val.endsWith(']')) {
813
+ // Keep arbitrary values as-is, but handle negative
814
+ if (isNeg) {
815
+ const inner = val.slice(1, -1);
816
+ val = `[-${inner}]`;
817
+ }
818
+ } else if (percentageAdjectives[val]) {
819
+ val = percentageAdjectives[val];
820
+ if (isNeg) val = `-${val}`;
821
+ } else if (val === '0') {
822
+ val = '0';
823
+ } else {
824
+ val = getSpacingScale(val, options);
825
+ if (isNeg) val = `-${val}`;
826
+ }
827
+
715
828
  return { category: 'visual', value: prefix + `translate-${axis}:${val}` };
716
829
  }
717
830
 
@@ -737,11 +850,7 @@ function convertClass(twClass, options = {}) {
737
850
  }
738
851
 
739
852
  // Mask
740
- const maskMatch = baseClass.match(/^mask(?:-(.+))?$/);
741
- if (maskMatch) {
742
- const val = maskMatch[1] || 'none';
743
- return { category: 'visual', value: prefix + "mask:" + val };
744
- }
853
+
745
854
 
746
855
  // Mask image
747
856
  const maskImageMatch = baseClass.match(/^mask-image(?:-(.+))?$/);
@@ -792,6 +901,13 @@ function convertClass(twClass, options = {}) {
792
901
  return { category: 'visual', value: prefix + "mask-type:" + val };
793
902
  }
794
903
 
904
+ // Mask (generic) - Check this LAST
905
+ const maskMatch = baseClass.match(/^mask(?:-(.+))?$/);
906
+ if (maskMatch) {
907
+ const val = maskMatch[1] || 'none';
908
+ return { category: 'visual', value: prefix + "mask:" + val };
909
+ }
910
+
795
911
  // Divide utilities - check these BEFORE other visual checks to avoid conflicts
796
912
  const divideXMatch = baseClass.match(/^divide-x-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black|transparent|current|inherit)(?:-\d+)?)$/);
797
913
  if (divideXMatch) {
@@ -871,7 +987,7 @@ function convertClass(twClass, options = {}) {
871
987
  /**
872
988
  * Convert HTML string with Tailwind classes to SenangStart syntax
873
989
  */
874
- export function convertHTML(html, options = {}) {
990
+ function convertHTML(html, options = {}) {
875
991
  // Match elements with class attribute
876
992
  const classRegex = /class\s*=\s*["']([^"']+)["']/gi;
877
993
 
@@ -948,9 +1064,9 @@ export function convertClasses(classString, options = {}) {
948
1064
 
949
1065
  /**
950
1066
  * CLI entry point
1067
+ * @param {string[]} args - Command line arguments (defaults to process.argv.slice(2))
951
1068
  */
952
- function main() {
953
- const args = argv.slice(2);
1069
+ function main(args = argv.slice(2)) {
954
1070
 
955
1071
  // Parse --exact flag
956
1072
  const exactMode = args.includes('--exact') || args.includes('-e');
@@ -994,7 +1110,14 @@ Examples:
994
1110
  }
995
1111
 
996
1112
  // File mode
997
- const inputFile = args.find(arg => !arg.startsWith('-') && args.indexOf(arg) !== args.indexOf('-o') + 1 && args.indexOf(arg) !== args.indexOf('--output') + 1);
1113
+ const inputFile = args.find((arg, index) => {
1114
+ if (arg.startsWith('-')) return false;
1115
+ if (index > 0) {
1116
+ const prevArg = args[index - 1];
1117
+ if (prevArg === '-o' || prevArg === '--output' || prevArg === '--string') return false;
1118
+ }
1119
+ return true;
1120
+ });
998
1121
 
999
1122
  if (!inputFile) {
1000
1123
  console.error('Error: Input file required');
@@ -1038,4 +1161,4 @@ if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` ||
1038
1161
  }
1039
1162
 
1040
1163
  // Export for testing
1041
- export { convertClass, spacingScale, layoutMappings, visualKeywordMappings };
1164
+ export { convertClass, spacingScale, percentageAdjectives, layoutMappings, visualKeywordMappings, main, convertHTML, isValidFilePath };
@@ -173,6 +173,34 @@ Example: \`space="p:small tab:p:medium lap:p:big"\`
173
173
  - \`active:\`, \`disabled:\`, \`group-hover:\`
174
174
  - \`dark:\`: \`visual="bg:white dark:bg:slate-900"\`
175
175
 
176
+ ## State Capabilities & Interaction
177
+
178
+ React to parent ("Group") or sibling ("Peer") states across all attributes.
179
+
180
+ ### 1. Group Capabilities
181
+ Add these to the \`layout\` attribute of a parent element:
182
+ - \`hoverable\`: Adds \`:hover\` support.
183
+ - \`focusable\`: Adds \`:focus-within\` support.
184
+ - \`pressable\`: Adds \`:active\` support.
185
+ - \`expandable\`: Reacts to \`[aria-expanded="true"]\`.
186
+ - \`selectable\`: Reacts to \`[aria-selected="true"]\`.
187
+
188
+ **Example:**
189
+ \`\`\`html
190
+ <div layout="flex hoverable" visual="bg:white">
191
+ <p visual="text:grey hover:text:primary">Hover parent to change me</p>
192
+ </div>
193
+ \`\`\`
194
+
195
+ ### 2. Peer Interactions
196
+ Use \`interact="[id]"\` on the trigger and \`listens="[id]"\` on the receiver.
197
+
198
+ **Example:**
199
+ \`\`\`html
200
+ <button layout="hoverable" interact="my-menu">Trigger</button>
201
+ <div layout="hidden hover:block" listens="my-menu">Target</div>
202
+ \`\`\`
203
+
176
204
  ## Configuration
177
205
  Use \`senangstart.config.js\` to override defaults.
178
206
  \`\`\`js