@bookklik/senangstart-css 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +362 -151
  7. package/dist/senangstart-css.min.js +175 -174
  8. package/dist/senangstart-tw.js +4 -4
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/ms/reference/visual/ring-color.md +2 -2
  11. package/docs/ms/reference/visual/ring-offset.md +3 -3
  12. package/docs/ms/reference/visual/ring.md +5 -5
  13. package/docs/public/assets/senangstart-css.min.js +175 -174
  14. package/docs/public/llms.txt +10 -10
  15. package/docs/reference/visual/ring-color.md +2 -2
  16. package/docs/reference/visual/ring-offset.md +3 -3
  17. package/docs/reference/visual/ring.md +5 -5
  18. package/package.json +1 -1
  19. package/src/cdn/tw-conversion-engine.js +4 -4
  20. package/src/cli/commands/build.js +42 -14
  21. package/src/cli/commands/dev.js +157 -93
  22. package/src/compiler/generators/css.js +371 -199
  23. package/src/compiler/tokenizer.js +25 -23
  24. package/src/core/tokenizer-core.js +46 -19
  25. package/src/definitions/visual-borders.js +10 -10
  26. package/src/utils/common.js +456 -39
  27. package/src/utils/node-io.js +82 -0
  28. package/tests/integration/dev-recovery.test.js +231 -0
  29. package/tests/unit/cli/memory-limits.test.js +169 -0
  30. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  31. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  32. package/tests/unit/convert-tailwind.test.js +518 -442
  33. package/tests/unit/utils/common.test.js +376 -26
  34. package/tests/unit/utils/file-timeout.test.js +154 -0
  35. package/tests/unit/utils/theme-validation.test.js +181 -0
  36. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  37. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  38. package/tests/unit/security.test.js +0 -206
  39. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -1,833 +0,0 @@
1
-
2
- import { test } from 'node:test';
3
- import assert from 'node:assert';
4
- import {
5
- generateCSS,
6
- generateCSSVariables,
7
- minifyCSS
8
- } from '../../../../src/compiler/generators/css.js';
9
- import { createTestConfig } from '../../../helpers/test-utils.js';
10
-
11
- test('CSS Generator Coverage', async (t) => {
12
- const config = createTestConfig();
13
-
14
- await t.test('generateCSSVariables - Tailwind Scales', () => {
15
- const css = generateCSSVariables(config);
16
- assert.ok(css.includes('--tw-text-xs: 0.75rem'));
17
- assert.ok(css.includes('--tw-leading-xs: 1rem'));
18
- assert.ok(css.includes('--tw-font-bold: 700'));
19
- });
20
-
21
- await t.test('generateCSSVariables - Custom Line Heights', () => {
22
- const customConfig = createTestConfig({
23
- theme: {
24
- fontSizeLineHeight: {
25
- 'base': '1.5',
26
- 'large': '1.2'
27
- }
28
- }
29
- });
30
- const css = generateCSSVariables(customConfig);
31
- assert.ok(css.includes('--font-lh-base: 1.5'));
32
- assert.ok(css.includes('--font-lh-large: 1.2'));
33
- });
34
-
35
- await t.test('Layout - Grid Column Span Full', () => {
36
- const token = { property: 'col-span', value: 'full', attrType: 'layout', raw: 'col-span:full' };
37
- const css = generateCSS([token], config);
38
- assert.ok(css.includes('grid-column: 1 / -1'));
39
- });
40
-
41
- await t.test('Layout - Grid Column Start/End', () => {
42
- const start = { property: 'col-start', value: '2', attrType: 'layout', raw: 'col-start:2' };
43
- const end = { property: 'col-end', value: '4', attrType: 'layout', raw: 'col-end:4' };
44
- const css = generateCSS([start, end], config);
45
- assert.ok(css.includes('grid-column-start: 2'));
46
- assert.ok(css.includes('grid-column-end: 4'));
47
- });
48
-
49
- await t.test('Layout - Grid Row Span Full', () => {
50
- const token = { property: 'row-span', value: 'full', attrType: 'layout', raw: 'row-span:full' };
51
- const css = generateCSS([token], config);
52
- assert.ok(css.includes('grid-row: 1 / -1'));
53
- });
54
-
55
- await t.test('Layout - Grid Row Start/End', () => {
56
- const start = { property: 'row-start', value: '1', attrType: 'layout', raw: 'row-start:1' };
57
- const end = { property: 'row-end', value: '3', attrType: 'layout', raw: 'row-end:3' };
58
- const css = generateCSS([start, end], config);
59
- assert.ok(css.includes('grid-row-start: 1'));
60
- assert.ok(css.includes('grid-row-end: 3'));
61
- });
62
-
63
- await t.test('Layout - Grid Auto Columns', () => {
64
- const auto = { property: 'auto-cols', value: 'min', attrType: 'layout', raw: 'auto-cols:min' };
65
- const arb = { property: 'auto-cols', value: 'minmax(100px, auto)', isArbitrary: true, attrType: 'layout', raw: 'auto-cols:[minmax(100px, auto)]' };
66
- const css = generateCSS([auto, arb], config);
67
- assert.ok(css.includes('grid-auto-columns: min-content'));
68
- assert.ok(css.includes('grid-auto-columns: minmax(100px, auto)'));
69
- });
70
-
71
- await t.test('Layout - Grid Auto Rows', () => {
72
- const auto = { property: 'auto-rows', value: 'fr', attrType: 'layout', raw: 'auto-rows:fr' };
73
- const arb = { property: 'auto-rows', value: '200px', isArbitrary: true, attrType: 'layout', raw: 'auto-rows:[200px]' };
74
- const css = generateCSS([auto, arb], config);
75
- assert.ok(css.includes('grid-auto-rows: minmax(0, 1fr)'));
76
- assert.ok(css.includes('grid-auto-rows: 200px'));
77
- });
78
-
79
- await t.test('Layout - Border Spacing', () => {
80
- const base = { property: 'border-spacing', value: 'medium', attrType: 'layout', raw: 'border-spacing:medium' };
81
- const x = { property: 'border-spacing-x', value: 'small', attrType: 'layout', raw: 'border-spacing-x:small' };
82
- const y = { property: 'border-spacing-y', value: 'big', attrType: 'layout', raw: 'border-spacing-y:big' };
83
- const css = generateCSS([base, x, y], config);
84
- assert.ok(css.includes('border-spacing: var(--s-medium)'));
85
- assert.ok(css.includes('border-spacing: var(--s-small) 0'));
86
- assert.ok(css.includes('border-spacing: 0 var(--s-big)'));
87
- });
88
-
89
- await t.test('Visual - Transforms and 3D', () => {
90
- const tokens = [
91
- { property: 'translate-x', value: 'full', attrType: 'visual', raw: 'translate-x:full' },
92
- { property: 'translate-x', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'translate-x:[10px]' },
93
- { property: 'rotate-x', value: '45', attrType: 'visual', raw: 'rotate-x:45' },
94
- { property: 'rotate-x', value: '45deg', isArbitrary: true, attrType: 'visual', raw: 'rotate-x:[45deg]' },
95
- { property: 'rotate-y', value: '45', attrType: 'visual', raw: 'rotate-y:45' },
96
- { property: 'rotate-y', value: '45deg', isArbitrary: true, attrType: 'visual', raw: 'rotate-y:[45deg]' },
97
- { property: 'rotate-z', value: '45', attrType: 'visual', raw: 'rotate-z:45' },
98
- { property: 'rotate-z', value: '45deg', isArbitrary: true, attrType: 'visual', raw: 'rotate-z:[45deg]' },
99
- { property: 'skew-x', value: '10', attrType: 'visual', raw: 'skew-x:10' },
100
- { property: 'skew-x', value: '10deg', isArbitrary: true, attrType: 'visual', raw: 'skew-x:[10deg]' },
101
- { property: 'skew-y', value: '10', attrType: 'visual', raw: 'skew-y:10' },
102
- { property: 'skew-y', value: '10deg', isArbitrary: true, attrType: 'visual', raw: 'skew-y:[10deg]' },
103
- { property: '-skew-x', value: '10', attrType: 'visual', raw: '-skew-x:10' },
104
- { property: '-skew-x', value: '10deg', isArbitrary: true, attrType: 'visual', raw: '-skew-x:[10deg]' },
105
- { property: '-skew-y', value: '10', attrType: 'visual', raw: '-skew-y:10' },
106
- { property: '-skew-y', value: '10deg', isArbitrary: true, attrType: 'visual', raw: '-skew-y:[10deg]' },
107
- { property: 'translate-z', value: 'near', attrType: 'visual', raw: 'translate-z:near' },
108
- { property: 'translate-z', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'translate-z:[10px]' },
109
- { property: 'origin', value: 'top-right', attrType: 'visual', raw: 'origin:top-right' },
110
- { property: 'origin', value: 'custom', attrType: 'visual', raw: 'origin:custom' },
111
- { property: 'origin', value: '10%_20%', isArbitrary: true, attrType: 'visual', raw: 'origin:[10%_20%]' },
112
- { property: 'perspective', value: 'big', attrType: 'visual', raw: 'perspective:big' },
113
- { property: 'perspective', value: 'unknown', attrType: 'visual', raw: 'perspective:unknown' },
114
- { property: 'perspective', value: '500px', isArbitrary: true, attrType: 'visual', raw: 'perspective:[500px]' },
115
- { property: 'perspective-origin', value: 'bottom-left', attrType: 'visual', raw: 'perspective-origin:bottom-left' },
116
- { property: 'perspective-origin', value: 'custom', attrType: 'visual', raw: 'perspective-origin:custom' },
117
- { property: 'perspective-origin', value: '10%_20%', isArbitrary: true, attrType: 'visual', raw: 'perspective-origin:[10%_20%]' }
118
- ];
119
- const css = generateCSS(tokens, config);
120
- assert.ok(css.includes('rotateX(45deg)'));
121
- assert.ok(css.includes('skewX(-10deg)'));
122
-
123
- // Branch coverage: directional translation & rotation defaults
124
- const transDefaults = [
125
- { property: 'translate-y', value: 'unknown', attrType: 'visual', raw: 'translate-y:unknown' },
126
- { property: 'translate-z', value: 'unknown', attrType: 'visual', raw: 'translate-z:unknown' },
127
- { property: 'rotate-x', value: '90', attrType: 'visual', raw: 'rotate-x:90' },
128
- { property: 'rotate-y', value: '90', attrType: 'visual', raw: 'rotate-y:90' },
129
- { property: 'rotate-z', value: '90', attrType: 'visual', raw: 'rotate-z:90' }
130
- ];
131
- const cssTransDefaults = generateCSS(transDefaults, config);
132
- assert.ok(cssTransDefaults.includes('translateY(var(--s-unknown))'));
133
- assert.ok(cssTransDefaults.includes('translateZ(var(--s-unknown))'));
134
- assert.ok(cssTransDefaults.includes('rotateX(90deg)'));
135
- });
136
-
137
- await t.test('Visual - Transitions and Animations', () => {
138
- const tokens = [
139
- { property: 'transition', value: 'colors', attrType: 'visual', raw: 'transition:colors' },
140
- { property: 'transition', value: 'opacity,transform', isArbitrary: true, attrType: 'visual', raw: 'transition:[opacity,transform]' },
141
- { property: 'duration', value: '150ms', isArbitrary: true, attrType: 'visual', raw: 'duration:[150ms]' },
142
- { property: 'duration', value: '0.5s', isArbitrary: true, attrType: 'visual', raw: 'duration:[0.5s]' },
143
- { property: 'ease', value: 'in-out', attrType: 'visual', raw: 'ease:in-out' },
144
- { property: 'ease', value: 'cubic-bezier(0,0,0,0)', isArbitrary: true, attrType: 'visual', raw: 'ease:[cubic-bezier(0,0,0,0)]' },
145
- { property: 'delay', value: '100', attrType: 'visual', raw: 'delay:100' },
146
- { property: 'delay', value: '0.2s', isArbitrary: true, attrType: 'visual', raw: 'delay:[0.2s]' },
147
- { property: 'animate', value: 'spin', attrType: 'visual', raw: 'animate:spin' },
148
- { property: 'animate', value: 'none', attrType: 'visual', raw: 'animate:none' },
149
- { property: 'animate', value: 'my-anim_2s_linear', isArbitrary: true, attrType: 'visual', raw: 'animate:[my-anim_2s_linear]' },
150
- { property: 'animation-duration', value: 'fast', attrType: 'visual', raw: 'animation-duration:fast' },
151
- { property: 'animation-delay', value: 'fast', attrType: 'visual', raw: 'animation-delay:fast' }
152
- ];
153
- const css = generateCSS(tokens, config);
154
- assert.ok(css.includes('transition:'));
155
- assert.ok(css.includes('transition-duration: 150ms'));
156
- assert.ok(css.includes('transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)'));
157
- assert.ok(css.includes('animation: my-anim 2s linear'));
158
- assert.ok(css.includes('animation-duration: 150ms'));
159
- assert.ok(css.includes('animation-delay: 150ms'));
160
-
161
- // Branch coverage: fallback defaults
162
- const defaults = [
163
- { property: 'duration', value: 'unknown', attrType: 'visual', raw: 'duration:unknown' },
164
- { property: 'ease', value: 'unknown', attrType: 'visual', raw: 'ease:unknown' },
165
- { property: 'delay', value: 'unknown', attrType: 'visual', raw: 'delay:unknown' },
166
- { property: 'animation-duration', value: 'unknown', attrType: 'visual', raw: 'animation-duration:unknown' },
167
- { property: 'animation-duration', value: '1s', isArbitrary: true, attrType: 'visual', raw: 'animation-duration:[1s]' },
168
- { property: 'animation-delay', value: '0.5s', isArbitrary: true, attrType: 'visual', raw: 'animation-delay:[0.5s]' }
169
- ];
170
- const cssDefaults = generateCSS(defaults, config);
171
- assert.ok(cssDefaults.includes('transition-duration: 200ms'));
172
- assert.ok(cssDefaults.includes('transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)'));
173
- assert.ok(cssDefaults.includes('transition-delay: 200ms'));
174
- assert.ok(cssDefaults.includes('animation-duration: 1s'));
175
- assert.ok(cssDefaults.includes('animation-delay: 0.5s'));
176
- });
177
-
178
- await t.test('Visual - Filters and Blends', () => {
179
- const tokens = [
180
- { property: 'blur', value: 'small', attrType: 'visual', raw: 'blur:small' },
181
- { property: 'blur', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'blur:[10px]' },
182
- { property: '-blur', value: 'small', attrType: 'visual', raw: '-blur:small' },
183
- { property: 'brightness', value: '50', attrType: 'visual', raw: 'brightness:50' },
184
- { property: 'brightness', value: '0.5', isArbitrary: true, attrType: 'visual', raw: 'brightness:[0.5]' },
185
- { property: 'contrast', value: '50', attrType: 'visual', raw: 'contrast:50' },
186
- { property: 'grayscale', value: '50', attrType: 'visual', raw: 'grayscale:50' },
187
- { property: 'hue-rotate', value: '90', attrType: 'visual', raw: 'hue-rotate:90' },
188
- { property: '-hue-rotate', value: '90', attrType: 'visual', raw: '-hue-rotate:90' },
189
- { property: 'invert', value: '50', attrType: 'visual', raw: 'invert:50' },
190
- { property: 'saturate', value: '50', attrType: 'visual', raw: 'saturate:50' },
191
- { property: 'sepia', value: '50', attrType: 'visual', raw: 'sepia:50' },
192
- { property: 'drop-shadow', value: 'small', attrType: 'visual', raw: 'drop-shadow:small' },
193
- { property: 'drop-shadow', value: '0_0_5px_rgba(0,0,0,0.5)', isArbitrary: true, attrType: 'visual', raw: 'drop-shadow:[0_0_5px_rgba(0,0,0,0.5)]' },
194
- { property: 'backdrop-blur', value: 'small', attrType: 'visual', raw: 'backdrop-blur:small' },
195
- { property: 'brightness', value: '150', isArbitrary: true, attrType: 'visual', raw: 'brightness:[150]' },
196
- { property: 'contrast', value: '150', isArbitrary: true, attrType: 'visual', raw: 'contrast:[150]' },
197
- { property: 'grayscale', value: '50%', isArbitrary: true, attrType: 'visual', raw: 'grayscale:[50%]' },
198
- { property: 'hue-rotate', value: '45deg', isArbitrary: true, attrType: 'visual', raw: 'hue-rotate:[45deg]' },
199
- { property: 'invert', value: '50%', isArbitrary: true, attrType: 'visual', raw: 'invert:[50%]' },
200
- { property: 'saturate', value: '150', isArbitrary: true, attrType: 'visual', raw: 'saturate:[150]' },
201
- { property: 'sepia', value: '50%', isArbitrary: true, attrType: 'visual', raw: 'sepia:[50%]' },
202
- { property: 'backdrop-brightness', value: '150', isArbitrary: true, attrType: 'visual', raw: 'backdrop-brightness:[150]' },
203
- { property: 'backdrop-contrast', value: '150', isArbitrary: true, attrType: 'visual', raw: 'backdrop-contrast:[150]' },
204
- { property: 'backdrop-grayscale', value: '50%', isArbitrary: true, attrType: 'visual', raw: 'backdrop-grayscale:[50%]' },
205
- { property: 'backdrop-hue-rotate', value: '45deg', isArbitrary: true, attrType: 'visual', raw: 'backdrop-hue-rotate:[45deg]' },
206
- { property: 'backdrop-invert', value: '50%', isArbitrary: true, attrType: 'visual', raw: 'backdrop-invert:[50%]' },
207
- { property: 'backdrop-opacity', value: '0.5', isArbitrary: true, attrType: 'visual', raw: 'backdrop-opacity:[0.5]' },
208
- { property: 'backdrop-saturate', value: '150', isArbitrary: true, attrType: 'visual', raw: 'backdrop-saturate:[150]' },
209
- { property: 'backdrop-sepia', value: '50%', isArbitrary: true, attrType: 'visual', raw: 'backdrop-sepia:[50%]' },
210
- { property: 'mix-blend', value: 'multiply', attrType: 'visual', raw: 'mix-blend:multiply' },
211
- { property: 'bg-blend', value: 'screen', attrType: 'visual', raw: 'bg-blend:screen' }
212
- ];
213
- const css = generateCSS(tokens, config);
214
- assert.ok(css.includes('filter: brightness(150)'));
215
- assert.ok(css.includes('backdrop-filter: brightness(150)'));
216
- assert.ok(css.includes('filter: blur(4px)'));
217
- assert.ok(css.includes('filter: blur(10px)'));
218
- assert.ok(css.includes('backdrop-filter: blur(4px)'));
219
- assert.ok(css.includes('mix-blend-mode: multiply'));
220
- assert.ok(css.includes('background-blend-mode: screen'));
221
-
222
- // Branch coverage: fallback defaults // Filters unknown
223
- assert.match(generateCSS([{ property: 'backdrop-blur', value: 'unknown', attrType: 'visual', raw: 'backdrop-blur:unknown' }], createTestConfig()), /backdrop-filter: blur\(8px\)/);
224
- const filterDefaults = [
225
- { property: 'brightness', value: 'unknown', attrType: 'visual', raw: 'brightness:unknown' },
226
- { property: 'contrast', value: 'unknown', attrType: 'visual', raw: 'contrast:unknown' },
227
- { property: 'drop-shadow', value: 'unknown', attrType: 'visual', raw: 'drop-shadow:unknown' },
228
- { property: 'grayscale', value: 'unknown', attrType: 'visual', raw: 'grayscale:unknown' },
229
- { property: 'invert', value: 'unknown', attrType: 'visual', raw: 'invert:unknown' },
230
- { property: 'saturate', value: 'unknown', attrType: 'visual', raw: 'saturate:unknown' },
231
- { property: 'sepia', value: 'unknown', attrType: 'visual', raw: 'sepia:unknown' },
232
- { property: 'blur', value: 'unknown', attrType: 'visual', raw: 'blur:unknown' },
233
- { property: 'backdrop-blur', value: 'unknown', attrType: 'visual', raw: 'backdrop-blur:unknown' },
234
- { property: 'backdrop-brightness', value: 'unknown', attrType: 'visual', raw: 'backdrop-brightness:unknown' },
235
- { property: 'backdrop-contrast', value: 'unknown', attrType: 'visual', raw: 'backdrop-contrast:unknown' },
236
- { property: 'backdrop-grayscale', value: 'unknown', attrType: 'visual', raw: 'backdrop-grayscale:unknown' },
237
- { property: 'backdrop-invert', value: 'unknown', attrType: 'visual', raw: 'backdrop-invert:unknown' },
238
- { property: 'backdrop-opacity', value: 'unknown', attrType: 'visual', raw: 'backdrop-opacity:unknown' },
239
- { property: 'backdrop-saturate', value: 'unknown', attrType: 'visual', raw: 'backdrop-saturate:unknown' },
240
- { property: 'backdrop-sepia', value: 'unknown', attrType: 'visual', raw: 'backdrop-sepia:unknown' }
241
- ];
242
- const cssFilterDefaults = generateCSS(filterDefaults, config);
243
- assert.ok(cssFilterDefaults.includes('filter: brightness(1)'));
244
- assert.ok(cssFilterDefaults.includes('filter: blur(8px)'));
245
- assert.ok(cssFilterDefaults.includes('backdrop-filter: blur(8px)'));
246
- assert.ok(cssFilterDefaults.includes('backdrop-filter: opacity(1)'));
247
- });
248
-
249
- await t.test('Visual - Borders and Outlines', () => {
250
- const tokens = [
251
- { property: 'border-w', value: '2', attrType: 'visual', raw: 'border-w:2' },
252
- { property: 'border-t-w', value: '2', attrType: 'visual', raw: 'border-t-w:2' },
253
- { property: 'border-l-w', value: '2', attrType: 'visual', raw: 'border-l-w:2' },
254
- { property: 'border-r-w', value: '2', attrType: 'visual', raw: 'border-r-w:2' },
255
- { property: 'border-b-w', value: '2', attrType: 'visual', raw: 'border-b-w:2' },
256
- { property: 'border-x-w', value: '2', attrType: 'visual', raw: 'border-x-w:2' },
257
- { property: 'border-y-w', value: '2', attrType: 'visual', raw: 'border-y-w:2' },
258
- { property: 'border', value: 'blue-500', attrType: 'visual', raw: 'border:blue-500' },
259
- { property: 'border-t', value: 'blue-500', attrType: 'visual', raw: 'border-t:blue-500' },
260
- { property: 'border-r', value: 'blue-500', attrType: 'visual', raw: 'border-r:blue-500' },
261
- { property: 'border-l', value: 'blue-500', attrType: 'visual', raw: 'border-l:blue-500' },
262
- { property: 'border-b', value: 'blue-500', attrType: 'visual', raw: 'border-b:blue-500' },
263
- { property: 'border-x', value: 'blue-500', attrType: 'visual', raw: 'border-x:blue-500' },
264
- { property: 'border-y', value: 'blue-500', attrType: 'visual', raw: 'border-y:blue-500' },
265
- { property: 'outline', value: 'blue-500', attrType: 'visual', raw: 'outline:blue-500' },
266
- { property: 'outline-offset', value: 'small', attrType: 'visual', raw: 'outline-offset:small' },
267
- { property: 'outline-offset', value: '2px', isArbitrary: true, attrType: 'visual', raw: 'outline-offset:[2px]' },
268
- { property: 'ring', value: 'none', attrType: 'visual', raw: 'ring:none' },
269
- { property: 'ring', value: 'thin', attrType: 'visual', raw: 'ring:thin' },
270
- { property: 'ring-color', value: 'blue-500', attrType: 'visual', raw: 'ring-color:blue-500' },
271
- { property: 'ring-w', value: '2', attrType: 'visual', raw: 'ring-w:2' },
272
- { property: 'ring-offset', value: 'regular', attrType: 'visual', raw: 'ring-offset:regular' },
273
- { property: 'ring-offset-color', value: 'white', attrType: 'visual', raw: 'ring-offset-color:white' },
274
- { property: 'rounded', value: 'medium', attrType: 'visual', raw: 'rounded:medium' },
275
- { property: 'mask-image', value: 'custom.png', isArbitrary: true, attrType: 'visual', raw: 'mask-image:[custom.png]' },
276
- { property: 'mask-position', value: '10px_20px', isArbitrary: true, attrType: 'visual', raw: 'mask-position:[10px_20px]' },
277
- { property: 'mask-size', value: '50%_auto', isArbitrary: true, attrType: 'visual', raw: 'mask-size:[50%_auto]' },
278
- { property: 'mask-repeat', value: 'custom-repeat', attrType: 'visual', raw: 'mask-repeat:custom-repeat' },
279
- { property: 'mask-origin', value: 'custom-origin', attrType: 'visual', raw: 'mask-origin:custom-origin' }
280
- ];
281
- const css = generateCSS(tokens, config);
282
- assert.ok(css.includes('mask-image: url(custom.png)'));
283
- assert.ok(css.includes('mask-position: 10px 20px'));
284
- assert.ok(css.includes('mask-size: 50% auto'));
285
- assert.ok(css.includes('mask-repeat: custom-repeat'));
286
- assert.ok(css.includes('mask-origin: custom-origin'));
287
- assert.ok(css.includes('border-width: var(--s-2)'));
288
- assert.ok(css.includes('border-top-width: var(--s-2)'));
289
- assert.ok(css.includes('border-bottom-width: var(--s-2)'));
290
- assert.ok(css.includes('border-left-width: var(--s-2)'));
291
- assert.ok(css.includes('border-right-width: var(--s-2)'));
292
- assert.ok(css.includes('border-color: var(--c-blue-500)'));
293
- assert.ok(css.includes('border-top-color: var(--c-blue-500)'));
294
- assert.ok(css.includes('border-radius: var(--r-medium)'));
295
- assert.ok(css.includes('outline-color: var(--c-blue-500)'));
296
- assert.ok(css.includes('outline-offset: var(--s-small)'));
297
- assert.ok(css.includes('outline-offset: 2px'));
298
- assert.ok(css.includes('--ss-ring-color: var(--c-blue-500)'));
299
- assert.ok(css.includes('--ss-ring-width: 1px'));
300
- assert.ok(css.includes('--ss-ring-offset-width: var(--s-regular)'));
301
- });
302
-
303
- await t.test('Space - Special Sizing (min, max, fit)', () => {
304
- const w_min = { property: 'w', value: 'min', attrType: 'space', raw: 'w:min' };
305
- const h_max = { property: 'h', value: 'max', attrType: 'space', raw: 'h:max' };
306
- const css = generateCSS([w_min, h_max], config);
307
- assert.ok(css.includes('width: min-content'));
308
- assert.ok(css.includes('height: max-content'));
309
- });
310
-
311
- await t.test('Space - Tailwind Prefix & Negative', () => {
312
- const pos = { property: 'p', value: 'tw-4', attrType: 'space', raw: 'p:tw-4' };
313
- const neg = { property: 'm', value: '-tw-2', attrType: 'space', raw: 'm:-tw-2' };
314
- const css = generateCSS([pos, neg], config);
315
- assert.ok(css.includes('var(--tw-4)'));
316
- assert.ok(css.includes('calc(var(--tw-2) * -1)'));
317
- });
318
-
319
- await t.test('Space - Percentage Adjectives', () => {
320
- // Width with percentage adjectives
321
- const w_full = { property: 'w', value: 'full', attrType: 'space', raw: 'w:full' };
322
- const w_half = { property: 'w', value: 'half', attrType: 'space', raw: 'w:half' };
323
- const w_third = { property: 'w', value: 'third', attrType: 'space', raw: 'w:third' };
324
- const w_third_2x = { property: 'w', value: 'third-2x', attrType: 'space', raw: 'w:third-2x' };
325
- const w_quarter = { property: 'w', value: 'quarter', attrType: 'space', raw: 'w:quarter' };
326
- const w_quarter_3x = { property: 'w', value: 'quarter-3x', attrType: 'space', raw: 'w:quarter-3x' };
327
-
328
- // Height with percentage adjectives
329
- const h_half = { property: 'h', value: 'half', attrType: 'space', raw: 'h:half' };
330
-
331
- // Fractional values (backwards compatibility)
332
- const w_1_2 = { property: 'w', value: '1/2', attrType: 'space', raw: 'w:1/2' };
333
- const h_2_3 = { property: 'h', value: '2/3', attrType: 'space', raw: 'h:2/3' };
334
-
335
- const css = generateCSS([w_full, w_half, w_third, w_third_2x, w_quarter, w_quarter_3x, h_half, w_1_2, h_2_3], config);
336
- assert.ok(css.includes('width: 100%'));
337
- assert.ok(css.includes('width: 50%'));
338
- assert.ok(css.includes('width: 33.333333%'));
339
- assert.ok(css.includes('width: 66.666667%'));
340
- assert.ok(css.includes('width: 25%'));
341
- assert.ok(css.includes('width: 75%'));
342
- assert.ok(css.includes('height: 50%'));
343
- });
344
-
345
- await t.test('Layout - Positioning Percentage Adjectives', () => {
346
- // Positioning with percentage adjectives (for centering patterns)
347
- const top_half = { property: 'top', value: 'half', attrType: 'layout', raw: 'top:half' };
348
- const left_half = { property: 'left', value: 'half', attrType: 'layout', raw: 'left:half' };
349
- const right_quarter = { property: 'right', value: 'quarter', attrType: 'layout', raw: 'right:quarter' };
350
-
351
- // Negative percentage adjectives
352
- const top_neg_half = { property: 'top', value: '-half', attrType: 'layout', raw: 'top:-half' };
353
-
354
- // Fractional values
355
- const left_1_3 = { property: 'left', value: '1/3', attrType: 'layout', raw: 'left:1/3' };
356
-
357
- // Inset with percentage
358
- const inset_full = { property: 'inset', value: 'full', attrType: 'layout', raw: 'inset:full' };
359
-
360
- const css = generateCSS([top_half, left_half, right_quarter, top_neg_half, left_1_3, inset_full], config);
361
- assert.ok(css.includes('top: 50%'));
362
- assert.ok(css.includes('left: 50%'));
363
- assert.ok(css.includes('right: 25%'));
364
- assert.ok(css.includes('top: -50%'));
365
- assert.ok(css.includes('left: 33.333333%'));
366
- assert.ok(css.includes('inset: 100%'));
367
- });
368
-
369
- await t.test('Visual - Background Image URL and Gradients', () => {
370
- const url = { property: 'bg-image', value: 'hero.jpg', attrType: 'visual', raw: 'bg-image:hero.jpg' };
371
- const arb = { property: 'bg-image', value: 'custom.png', isArbitrary: true, attrType: 'visual', raw: 'bg-image:[custom.png]' };
372
- const grad = { property: 'bg-image', value: 'gradient-to-t', attrType: 'visual', raw: 'bg-image:gradient-to-t' };
373
- const css = generateCSS([url, arb, grad], config);
374
- assert.ok(css.includes('background-image: url(hero.jpg)'));
375
- assert.ok(css.includes('background-image: url(custom.png)'));
376
- assert.ok(css.includes('linear-gradient(to top,'));
377
- });
378
-
379
- await t.test('Visual - Background Attachment & Clip', () => {
380
- const attach = { property: 'bg-attachment', value: 'fixed', attrType: 'visual', raw: 'bg-attachment:fixed' };
381
- const clip_text = { property: 'bg-clip', value: 'text', attrType: 'visual', raw: 'bg-clip:text' };
382
- const css = generateCSS([attach, clip_text], config);
383
- assert.ok(css.includes('background-attachment: fixed'));
384
- assert.ok(css.includes('background-clip: text'));
385
- });
386
-
387
- await t.test('Visual - Opacity Edge Cases', () => {
388
- const arb = { property: 'opacity', value: 'inherit', isArbitrary: true, attrType: 'visual', raw: 'opacity:[inherit]' };
389
- const css = generateCSS([arb], config);
390
- assert.ok(css.includes('opacity: inherit'));
391
- });
392
-
393
- await t.test('Integration - Peer Selectors', () => {
394
- const tokens = [
395
- { raw: 'uid1', attrType: 'interact' },
396
- {
397
- raw: 'bg-primary',
398
- attrType: 'visual',
399
- property: 'bg',
400
- value: 'primary',
401
- state: 'hover'
402
- }
403
- ];
404
- const css = generateCSS(tokens, config);
405
- assert.ok(css.includes('[interact~="uid1"]'), 'interact selector missing');
406
- assert.ok(css.includes('~ [listens~="uid1"]'), 'listens selector missing');
407
- });
408
-
409
- await t.test('Integration - Divide with State', () => {
410
- const token = {
411
- raw: 'divide:primary',
412
- attrType: 'visual',
413
- property: 'divide',
414
- value: 'primary',
415
- state: 'hover'
416
- };
417
- const css = generateCSS([token], config);
418
- assert.ok(css.includes('> :not([hidden]) ~ :not([hidden]):hover'));
419
- });
420
-
421
- await t.test('Integration - Unknown Breakpoints', () => {
422
- const tokens = [
423
- {
424
- raw: 'unknown:block',
425
- attrType: 'layout',
426
- property: 'block',
427
- value: 'block',
428
- breakpoint: 'unknown-bp'
429
- }
430
- ];
431
- const css = generateCSS(tokens, config);
432
- assert.ok(css.includes('@media'));
433
- });
434
-
435
- await t.test('Integration - Animation Keyframes', () => {
436
- const css = generateCSS([], config);
437
- assert.ok(css.includes('@keyframes spin'));
438
- });
439
-
440
- await t.test('Integration - Custom Dark Selector', () => {
441
- const token = { state: 'dark', property: 'bg', value: 'black', attrType: 'visual', raw: 'dark:bg:black' };
442
- const configCustom = createTestConfig({ darkMode: ['selector', '[data-theme="dark"]'] });
443
- const css = generateCSS([token], configCustom);
444
- assert.ok(css.includes('[data-theme="dark"] [visual~="dark:bg:black"]'));
445
- });
446
-
447
- await t.test('Visual - Font Weight Base', () => {
448
- const token = { property: 'font', value: 'bold', attrType: 'visual', raw: 'font:bold' };
449
- const css = generateCSS([token], config);
450
- assert.ok(css.includes('font-weight: var(--fw-bold)'));
451
- });
452
-
453
- await t.test('Visual - Text Size Base', () => {
454
- const token = { property: 'text-size', value: 'medium', attrType: 'visual', raw: 'text-size:medium' };
455
- const css = generateCSS([token], config);
456
- assert.ok(css.includes('font-size: var(--font-medium)'));
457
- assert.ok(css.includes('line-height: var(--font-lh-medium)'));
458
- });
459
-
460
- await t.test('Layout - Grid Row Span Numeric', () => {
461
- const token = { property: 'row-span', value: '2', attrType: 'layout', raw: 'row-span:2' };
462
- const css = generateCSS([token], config);
463
- assert.ok(css.includes('grid-row: span 2 / span 2'));
464
- });
465
-
466
- await t.test('Layout - Grid Template Columns and Rows', () => {
467
- const cols_none = { property: 'grid-cols', value: 'none', attrType: 'layout', raw: 'grid-cols:none' };
468
- const cols_sub = { property: 'grid-cols', value: 'subgrid', attrType: 'layout', raw: 'grid-cols:subgrid' };
469
- const cols_arb = { property: 'grid-cols', value: '200px_1fr', isArbitrary: true, attrType: 'layout', raw: 'grid-cols:[200px_1fr]' };
470
-
471
- const rows_none = { property: 'grid-rows', value: 'none', attrType: 'layout', raw: 'grid-rows:none' };
472
- const rows_sub = { property: 'grid-rows', value: 'subgrid', attrType: 'layout', raw: 'grid-rows:subgrid' };
473
- const rows_arb = { property: 'grid-rows', value: '100px_auto', isArbitrary: true, attrType: 'layout', raw: 'grid-rows:[100px_auto]' };
474
-
475
- const css = generateCSS([cols_none, cols_sub, cols_arb, rows_none, rows_sub, rows_arb], config);
476
- assert.ok(css.includes('grid-template-columns: none'));
477
- assert.ok(css.includes('grid-template-columns: subgrid'));
478
- assert.ok(css.includes('grid-template-columns: 200px 1fr'));
479
- assert.ok(css.includes('grid-template-rows: none'));
480
- assert.ok(css.includes('grid-template-rows: subgrid'));
481
- assert.ok(css.includes('grid-template-rows: 100px auto'));
482
- });
483
-
484
- await t.test('Layout - Overflow and Aspect Ratio', () => {
485
- const ov = { property: 'overflow', value: 'hidden', attrType: 'layout', raw: 'overflow:hidden' };
486
- const ovX = { property: 'overflow-x', value: 'scroll', attrType: 'layout', raw: 'overflow-x:scroll' };
487
- const ovY = { property: 'overflow-y', value: 'auto', attrType: 'layout', raw: 'overflow-y:auto' };
488
- const aspectSquare = { property: 'aspect', value: 'square', attrType: 'layout', raw: 'aspect:square' };
489
- const aspectVideo = { property: 'aspect', value: 'video', attrType: 'layout', raw: 'aspect:video' };
490
- const aspectArb = { property: 'aspect', value: '21/9', isArbitrary: true, attrType: 'layout', raw: 'aspect:[21/9]' };
491
-
492
- const css = generateCSS([ov, ovX, ovY, aspectSquare, aspectVideo, aspectArb], config);
493
- assert.ok(css.includes('overflow: hidden'));
494
- assert.ok(css.includes('overflow-x: scroll'));
495
- assert.ok(css.includes('overflow-y: auto'));
496
- assert.ok(css.includes('aspect-ratio: 1 / 1'));
497
- assert.ok(css.includes('aspect-ratio: 16 / 9'));
498
- assert.ok(css.includes('aspect-ratio: 21/9'));
499
- });
500
-
501
- await t.test('Layout - Object Fit and Position', () => {
502
- const objFit = { property: 'object', value: 'cover', attrType: 'layout', raw: 'object:cover' };
503
- const objPos = { property: 'object-pos', value: 'center_top', isArbitrary: true, attrType: 'layout', raw: 'object-pos:[center_top]' };
504
- const placeItems = { property: 'place-items', value: 'center', attrType: 'layout', raw: 'place-items:center' };
505
- const placeSelf = { property: 'place-self', value: 'end', attrType: 'layout', raw: 'place-self:end' };
506
- const justifyItems = { property: 'justify-items', value: 'center', attrType: 'layout', raw: 'justify-items:center' };
507
- const justifySelf = { property: 'justify-self', value: 'start', attrType: 'layout', raw: 'justify-self:start' };
508
- const alignContent = { property: 'content', value: 'center', attrType: 'layout', raw: 'content:center' };
509
- const placeContent = { property: 'place-content', value: 'evenly', attrType: 'layout', raw: 'place-content:evenly' };
510
- const justify = { property: 'justify', value: 'between', attrType: 'layout', raw: 'justify:between' };
511
-
512
- const css = generateCSS([objFit, objPos, placeItems, placeSelf, justifyItems, justifySelf, alignContent, placeContent, justify], config);
513
- assert.ok(css.includes('object-fit: cover'));
514
- assert.ok(css.includes('object-position: center top'));
515
- assert.ok(css.includes('place-items: center'));
516
- assert.ok(css.includes('place-self: end'));
517
- assert.ok(css.includes('justify-items: center'));
518
- assert.ok(css.includes('justify-self: start'));
519
- assert.ok(css.includes('align-content: center'));
520
- assert.ok(css.includes('place-content: space-evenly'));
521
- assert.ok(css.includes('justify-content: space-between'));
522
- });
523
-
524
- await t.test('Layout - Inset and Positioning', () => {
525
- const insetArb = { property: 'inset', value: '10px', isArbitrary: true, attrType: 'layout', raw: 'inset:[10px]' };
526
- const topArb = { property: 'top', value: '5px', isArbitrary: true, attrType: 'layout', raw: 'top:[5px]' };
527
- const insetX = { property: 'inset-x', value: '0', attrType: 'layout', raw: 'inset-x:0' };
528
- const insetY = { property: 'inset-y', value: 'medium', attrType: 'layout', raw: 'inset-y:medium' };
529
-
530
- const css = generateCSS([insetArb, topArb, insetX, insetY], config);
531
- assert.ok(css.includes('inset: 10px'));
532
- assert.ok(css.includes('top: 5px'));
533
- assert.ok(css.includes('left: 0; right: 0;'));
534
- assert.ok(css.includes('top: var(--s-medium); bottom: var(--s-medium);'));
535
- });
536
-
537
- await t.test('Layout - Columns and Overscroll', () => {
538
- const cols = { property: 'cols', value: '3', attrType: 'layout', raw: 'cols:3' };
539
- const over = { property: 'overscroll', value: 'contain', attrType: 'layout', raw: 'overscroll:contain' };
540
- const overX = { property: 'overscroll-x', value: 'none', attrType: 'layout', raw: 'overscroll-x:none' };
541
- const overY = { property: 'overscroll-y', value: 'auto', attrType: 'layout', raw: 'overscroll-y:auto' };
542
-
543
- const css = generateCSS([cols, over, overX, overY], config);
544
- assert.ok(css.includes('columns: 3'));
545
- assert.ok(css.includes('overscroll-behavior: contain'));
546
- assert.ok(css.includes('overscroll-behavior-x: none'));
547
- assert.ok(css.includes('overscroll-behavior-y: auto'));
548
- });
549
-
550
- await t.test('Layout - Flex and Order', () => {
551
- const basis = { property: 'basis', value: 'medium', attrType: 'layout', raw: 'basis:medium' };
552
- const basisArb = { property: 'basis', value: '10px', isArbitrary: true, attrType: 'layout', raw: 'basis:[10px]' };
553
- const flex1 = { property: 'flex', value: '1', attrType: 'layout', raw: 'flex:1' };
554
- const flexAuto = { property: 'flex', value: 'auto', attrType: 'layout', raw: 'flex:auto' };
555
- const flexInitial = { property: 'flex', value: 'initial', attrType: 'layout', raw: 'flex:initial' };
556
- const flexNone = { property: 'flex', value: 'none', attrType: 'layout', raw: 'flex:none' };
557
- const orderFirst = { property: 'order', value: 'first', attrType: 'layout', raw: 'order:first' };
558
- const orderLast = { property: 'order', value: 'last', attrType: 'layout', raw: 'order:last' };
559
- const orderNone = { property: 'order', value: 'none', attrType: 'layout', raw: 'order:none' };
560
- const orderVal = { property: 'order', value: '5', attrType: 'layout', raw: 'order:5' };
561
-
562
- const css = generateCSS([basis, basisArb, flex1, flexAuto, flexInitial, flexNone, orderFirst, orderLast, orderNone, orderVal], config);
563
- assert.ok(css.includes('flex-basis: var(--s-medium)'));
564
- assert.ok(css.includes('flex-basis: 10px'));
565
- assert.ok(css.includes('order: -9999'));
566
- });
567
-
568
- await t.test('Layout - Map Branch Coverage', () => {
569
- const tokens = [
570
- { property: 'items', value: 'start', attrType: 'layout', raw: 'items:start' },
571
- { property: 'items', value: 'custom', attrType: 'layout', raw: 'items:custom' },
572
- { property: 'self', value: 'center', attrType: 'layout', raw: 'self:center' },
573
- { property: 'self', value: 'custom', attrType: 'layout', raw: 'self:custom' },
574
- { property: 'place-content', value: 'between', attrType: 'layout', raw: 'place-content:between' },
575
- { property: 'place-content', value: 'custom', attrType: 'layout', raw: 'place-content:custom' },
576
- { property: 'justify-items', value: 'start', attrType: 'layout', raw: 'justify-items:start' },
577
- { property: 'justify-items', value: 'custom', attrType: 'layout', raw: 'justify-items:custom' },
578
- { property: 'justify-self', value: 'end', attrType: 'layout', raw: 'justify-self:end' },
579
- { property: 'justify-self', value: 'custom', attrType: 'layout', raw: 'justify-self:custom' },
580
- { property: 'align-content', value: 'center', attrType: 'layout', raw: 'align-content:center' },
581
- { property: 'align-content', value: 'custom', attrType: 'layout', raw: 'align-content:custom' },
582
- { property: 'object-fit', value: 'cover', attrType: 'layout', raw: 'object-fit:cover' },
583
- { property: 'object-fit', value: 'custom', attrType: 'layout', raw: 'object-fit:custom' },
584
- { property: 'object-position', value: 'bottom', attrType: 'layout', raw: 'object-position:bottom' },
585
- { property: 'object-position', value: 'custom', attrType: 'layout', raw: 'object-position:custom' },
586
- { property: 'overflow', value: 'hidden', attrType: 'layout', raw: 'overflow:hidden' },
587
- { property: 'overflow', value: 'visible_scroll', attrType: 'layout', raw: 'overflow:visible_scroll' },
588
- { property: 'overflow-x', value: 'auto', attrType: 'layout', raw: 'overflow-x:auto' },
589
- { property: 'overflow-y', value: 'scroll', attrType: 'layout', raw: 'overflow-y:scroll' },
590
- { property: 'overscroll', value: 'none', attrType: 'layout', raw: 'overscroll:none' },
591
- { property: 'overscroll-x', value: 'contain', attrType: 'layout', raw: 'overscroll-x:contain' },
592
- { property: 'overscroll-y', value: 'auto', attrType: 'layout', raw: 'overscroll-y:auto' }
593
- ];
594
- const css = generateCSS(tokens, config);
595
- assert.ok(css.includes('align-items: flex-start'));
596
- assert.ok(css.includes('align-items: custom'));
597
- });
598
-
599
- await t.test('Branch Coverage - Full Sweep', () => {
600
- const tokens = [
601
- // Scale
602
- { property: 'scale', value: '50', attrType: 'visual', raw: 'scale:50' },
603
- { property: 'scale', value: '0.5', isArbitrary: true, attrType: 'visual', raw: 'scale:[0.5]' },
604
- { property: 'scale-x', value: '50', attrType: 'visual', raw: 'scale-x:50' },
605
- { property: 'scale-x', value: '0.5', isArbitrary: true, attrType: 'visual', raw: 'scale-x:[0.5]' },
606
- { property: 'scale-y', value: '50', attrType: 'visual', raw: 'scale-y:50' },
607
- { property: 'scale-y', value: '0.5', isArbitrary: true, attrType: 'visual', raw: 'scale-y:[0.5]' },
608
- // Typography
609
- { property: 'font-family', value: 'sans', attrType: 'visual', raw: 'font-family:sans' },
610
- { property: 'font-family', value: 'Arial', isArbitrary: true, attrType: 'visual', raw: 'font-family:[Arial]' },
611
- { property: 'letter-spacing', value: 'wide', attrType: 'visual', raw: 'letter-spacing:wide' },
612
- { property: 'letter-spacing', value: '0.1em', isArbitrary: true, attrType: 'visual', raw: 'letter-spacing:[0.1em]' },
613
- { property: 'line-height', value: 'loose', attrType: 'visual', raw: 'line-height:loose' },
614
- { property: 'line-height', value: '2', isArbitrary: true, attrType: 'visual', raw: 'line-height:[2]' },
615
- // Rotate
616
- { property: 'rotate', value: '45', attrType: 'visual', raw: 'rotate:45' },
617
- { property: 'rotate', value: '45deg', isArbitrary: true, attrType: 'visual', raw: 'rotate:[45deg]' },
618
- // Translation
619
- { property: 'translate-x', value: 'medium', attrType: 'visual', raw: 'translate-x:medium' },
620
- { property: 'translate-y', value: 'medium', attrType: 'visual', raw: 'translate-y:medium' },
621
- { property: '-translate-x', value: 'medium', attrType: 'visual', raw: '-translate-x:medium' },
622
- { property: '-translate-x', value: '10px', isArbitrary: true, attrType: 'visual', raw: '-translate-x:[10px]' },
623
- { property: '-translate-y', value: 'medium', attrType: 'visual', raw: '-translate-y:medium' },
624
- { property: '-translate-y', value: '10px', isArbitrary: true, attrType: 'visual', raw: '-translate-y:[10px]' },
625
- // Skew
626
- { property: 'skew-x', value: '10', attrType: 'visual', raw: 'skew-x:10' },
627
- { property: 'skew-y', value: '10', attrType: 'visual', raw: 'skew-y:10' },
628
- { property: '-skew-x', value: '10', attrType: 'visual', raw: '-skew-x:10' },
629
- { property: '-skew-y', value: '10', attrType: 'visual', raw: '-skew-y:10' },
630
- // Interactivity
631
- { property: 'resize', value: 'both', attrType: 'visual', raw: 'resize:both' },
632
- { property: 'resize', value: 'custom', attrType: 'visual', raw: 'resize:custom' },
633
- // SVG
634
- { property: 'fill', value: 'red-500', attrType: 'visual', raw: 'fill:red-500' },
635
- { property: 'fill', value: 'none', attrType: 'visual', raw: 'fill:none' },
636
- { property: 'fill', value: 'current', attrType: 'visual', raw: 'fill:current' },
637
- { property: 'fill', value: '#ff0000', isArbitrary: true, attrType: 'visual', raw: 'fill:[#ff0000]' },
638
- { property: 'stroke', value: 'red-500', attrType: 'visual', raw: 'stroke:red-500' },
639
- { property: 'stroke', value: 'none', attrType: 'visual', raw: 'stroke:none' },
640
- { property: 'stroke', value: 'current', attrType: 'visual', raw: 'stroke:current' },
641
- { property: 'stroke', value: '#ff0000', isArbitrary: true, attrType: 'visual', raw: 'stroke:[#ff0000]' },
642
- { property: 'stroke-w', value: '2', attrType: 'visual', raw: 'stroke-w:2' },
643
- { property: 'stroke-w', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'stroke-w:[10px]' }
644
- ];
645
- const css = generateCSS(tokens, config);
646
- assert.ok(css.length > 0);
647
- });
648
-
649
- await t.test('Interactivity - Scroll and Touch Snap', () => {
650
- // Scroll margin/padding (static and arbitrary)
651
- const tokens = [
652
- { property: 'scroll-m', value: 'big', attrType: 'visual', raw: 'scroll-m:big' },
653
- { property: 'scroll-m', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m:[10px]' },
654
- { property: 'scroll-m-t', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m-t:[10px]' },
655
- { property: 'scroll-m-r', value: 'small', attrType: 'visual', raw: 'scroll-m-r:small' },
656
- { property: 'scroll-m-r', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m-r:[5px]' },
657
- { property: 'scroll-m-b', value: 'small', attrType: 'visual', raw: 'scroll-m-b:small' },
658
- { property: 'scroll-m-b', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m-b:[5px]' },
659
- { property: 'scroll-m-l', value: 'small', attrType: 'visual', raw: 'scroll-m-l:small' },
660
- { property: 'scroll-m-l', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m-l:[5px]' },
661
- { property: 'scroll-m-x', value: 'small', attrType: 'visual', raw: 'scroll-m-x:small' },
662
- { property: 'scroll-m-x', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m-x:[5px]' },
663
- { property: 'scroll-m-y', value: 'small', attrType: 'visual', raw: 'scroll-m-y:small' },
664
- { property: 'scroll-m-y', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-m-y:[5px]' },
665
- { property: 'scroll-p', value: 'big', attrType: 'visual', raw: 'scroll-p:big' },
666
- { property: 'scroll-p', value: '20px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p:[20px]' },
667
- { property: 'scroll-p-t', value: '10px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p-t:[10px]' },
668
- { property: 'scroll-p-r', value: 'small', attrType: 'visual', raw: 'scroll-p-r:small' },
669
- { property: 'scroll-p-r', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p-r:[5px]' },
670
- { property: 'scroll-p-b', value: 'small', attrType: 'visual', raw: 'scroll-p-b:small' },
671
- { property: 'scroll-p-b', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p-b:[5px]' },
672
- { property: 'scroll-p-l', value: 'small', attrType: 'visual', raw: 'scroll-p-l:small' },
673
- { property: 'scroll-p-l', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p-l:[5px]' },
674
- { property: 'scroll-p-x', value: 'small', attrType: 'visual', raw: 'scroll-p-x:small' },
675
- { property: 'scroll-p-x', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p-x:[5px]' },
676
- { property: 'scroll-p-y', value: 'small', attrType: 'visual', raw: 'scroll-p-y:small' },
677
- { property: 'scroll-p-y', value: '5px', isArbitrary: true, attrType: 'visual', raw: 'scroll-p-y:[5px]' },
678
- // Snap type presets and arbitrary (covers snapMap[value] || value)
679
- { property: 'snap', value: 'x', attrType: 'visual', raw: 'snap:x' },
680
- { property: 'snap', value: 'custom-mandatory', attrType: 'visual', raw: 'snap:custom-mandatory' },
681
- // Touch action presets and arbitrary
682
- { property: 'touch', value: 'auto', attrType: 'visual', raw: 'touch:auto' },
683
- { property: 'touch', value: 'custom-action', attrType: 'visual', raw: 'touch:custom-action' },
684
- // Will change presets and arbitrary
685
- { property: 'will-change', value: 'scroll', attrType: 'visual', raw: 'will-change:scroll' },
686
- { property: 'will-change', value: 'filter', attrType: 'visual', raw: 'will-change:filter' },
687
- // Other interactivity
688
- { property: 'accent', value: 'red-500', attrType: 'visual', raw: 'accent:red-500' },
689
- { property: 'accent', value: '#ff0000', isArbitrary: true, attrType: 'visual', raw: 'accent:[#ff0000]' },
690
- { property: 'appearance', value: 'none', attrType: 'visual', raw: 'appearance:none' },
691
- { property: 'caret', value: 'blue-500', attrType: 'visual', raw: 'caret:blue-500' },
692
- { property: 'caret', value: '#0000ff', isArbitrary: true, attrType: 'visual', raw: 'caret:[#0000ff]' },
693
- { property: 'cursor', value: 'pointer', attrType: 'visual', raw: 'cursor:pointer' },
694
- { property: 'select', value: 'none', attrType: 'visual', raw: 'select:none' },
695
- { property: 'field-sizing', value: 'content', attrType: 'visual', raw: 'field-sizing:content' },
696
- { property: 'forced-colors', value: 'none', attrType: 'visual', raw: 'forced-colors:none' }
697
- ];
698
-
699
- const css = generateCSS(tokens, config);
700
- assert.ok(css.includes('scroll-margin: var(--s-big)'));
701
- assert.ok(css.includes('scroll-margin: 10px'));
702
- assert.ok(css.includes('scroll-margin-top: 10px'));
703
- assert.ok(css.includes('scroll-padding: var(--s-big)'));
704
- assert.ok(css.includes('scroll-padding: 20px'));
705
- assert.ok(css.includes('scroll-snap-type: x mandatory'));
706
- assert.ok(css.includes('scroll-snap-type: custom-mandatory'));
707
- assert.ok(css.includes('touch-action: auto'));
708
- assert.ok(css.includes('touch-action: custom-action'));
709
- assert.ok(css.includes('will-change: scroll-position'));
710
- assert.ok(css.includes('will-change: filter'));
711
- assert.ok(css.includes('accent-color: var(--c-red-500)'));
712
- assert.ok(css.includes('accent-color: #ff0000'));
713
- });
714
-
715
- await t.test('Integration - Branch Edge Cases', () => {
716
- // 1. Dark mode missing in config (defaults to 'media')
717
- const darkToken = { property: 'bg', value: 'red-500', state: 'dark', attrType: 'visual', raw: 'dark:bg:red-500' };
718
- const configNoDark = createTestConfig({ darkMode: undefined });
719
- const cssDark = generateCSS([darkToken], configNoDark);
720
- assert.ok(cssDark.includes('@media (prefers-color-scheme: dark)'));
721
-
722
- // 2. Responsive display reset - already covered but let's ensure the 'has(bpToken.raw)' branch
723
- const baseFlex = { property: 'flex', attrType: 'layout', raw: 'flex' };
724
- const respFlex = { property: 'flex', breakpoint: 'tab', attrType: 'layout', raw: 'flex' }; // Same as base, should NOT trigger reset
725
- const cssReset = generateCSS([baseFlex, respFlex], config);
726
- // Should NOT contain revert-layer because they are the same
727
- assert.ok(!cssReset.includes('revert-layer'));
728
-
729
- // 3. Token without attrType or unknown category
730
- const noAttr = { property: 'display', value: 'block', raw: 'display-block' };
731
- const unknownCat = { property: 'display', value: 'block', attrType: 'unknown', raw: 'unknown:block' };
732
- const cssNoAttr = generateCSS([noAttr, unknownCat], config);
733
- assert.ok(cssNoAttr.length > 0);
734
-
735
- // 4. Unknown visual property
736
- const unknownProp = { property: 'unknown-visual', value: 'value', attrType: 'visual', raw: 'unknown-visual:value' };
737
- const cssUnknown = generateCSS([unknownProp], config);
738
- assert.ok(!cssUnknown.includes('unknown-visual'));
739
- });
740
-
741
- await t.test('Integration - Preflight', () => {
742
- const configPreflight = createTestConfig({ preflight: true });
743
- const css = generateCSS([], configPreflight);
744
- assert.ok(css.includes('SenangStart Preflight') || css.includes('box-sizing: border-box'));
745
- });
746
-
747
- await t.test('Integration - Responsive Display Reset', () => {
748
- const tokens = [
749
- { raw: 'hidden', attrType: 'layout', property: 'hidden', value: 'hidden' },
750
- { raw: 'tab:flex', attrType: 'layout', property: 'flex', value: 'flex', breakpoint: 'tab' }
751
- ];
752
- const css = generateCSS(tokens, config);
753
- assert.ok(css.includes('[layout~="tab:flex"] { display: revert-layer; }'));
754
- });
755
- await t.test('Interactivity and Snap Utilities', () => {
756
- const config = createTestConfig();
757
- const res = generateCSS([
758
- { property: 'transform-style', value: 'preserve-3d', attrType: 'visual', raw: 'transform-style:preserve-3d' },
759
- { property: 'backface', value: 'hidden', attrType: 'visual', raw: 'backface:hidden' },
760
- { property: 'color-scheme', value: 'dark', attrType: 'visual', raw: 'color-scheme:dark' },
761
- { property: 'pointer-events', value: 'none', attrType: 'visual', raw: 'pointer-events:none' },
762
- { property: 'scroll', value: 'smooth', attrType: 'visual', raw: 'scroll:smooth' },
763
- { property: 'snap-align', value: 'center', attrType: 'visual', raw: 'snap-align:center' },
764
- { property: 'snap-stop', value: 'always', attrType: 'visual', raw: 'snap-stop:always' },
765
- { property: 'accent', value: 'blue-500', attrType: 'visual', raw: 'accent:blue-500' },
766
- { property: 'caret', value: 'red-500', attrType: 'visual', raw: 'caret:red-500' },
767
- { property: 'scroll-m-x', value: '4', attrType: 'visual', raw: 'scroll-m-x:4' },
768
- { property: 'scroll-m-y', value: '4', attrType: 'visual', raw: 'scroll-m-y:4' },
769
- { property: 'scroll-p-x', value: '4', attrType: 'visual', raw: 'scroll-p-x:4' },
770
- { property: 'scroll-p-y', value: '4', attrType: 'visual', raw: 'scroll-p-y:4' },
771
- { property: 'touch', value: 'none', attrType: 'visual', raw: 'touch:none' },
772
- { property: 'select', value: 'none', attrType: 'visual', raw: 'select:none' },
773
- { property: 'will-change', value: 'transform', attrType: 'visual', raw: 'will-change:transform' }
774
- ], config);
775
-
776
- assert.match(res, /transform-style: preserve-3d/);
777
- assert.match(res, /backface-visibility: hidden/);
778
- assert.match(res, /color-scheme: dark/);
779
- assert.match(res, /pointer-events: none/);
780
- assert.match(res, /scroll-behavior: smooth/);
781
- assert.match(res, /scroll-snap-align: center/);
782
- assert.match(res, /scroll-snap-stop: always/);
783
- assert.match(res, /accent-color: var\(--c-blue-500\)/);
784
- assert.match(res, /caret-color: var\(--c-red-500\)/);
785
- assert.match(res, /scroll-margin-left: var\(--s-4\); scroll-margin-right: var\(--s-4\);/);
786
- assert.match(res, /touch-action: none/);
787
- assert.match(res, /user-select: none/);
788
- assert.match(res, /will-change: transform/);
789
- });
790
-
791
- await t.test('Space Shortcuts', () => {
792
- const config = createTestConfig();
793
- const res = generateCSS([
794
- { property: 'p-x', value: '4', attrType: 'space', raw: 'p-x:4' },
795
- { property: 'p-y', value: '4', attrType: 'space', raw: 'p-y:4' },
796
- { property: 'm-x', value: '4', attrType: 'space', raw: 'm-x:4' },
797
- { property: 'm-y', value: '4', attrType: 'space', raw: 'm-y:4' },
798
- { property: 'g-x', value: '4', attrType: 'space', raw: 'g-x:4' },
799
- { property: 'g-y', value: '4', attrType: 'space', raw: 'g-y:4' }
800
- ], config);
801
-
802
- assert.match(res, /padding-left: var\(--s-4\); padding-right: var\(--s-4\);/);
803
- assert.match(res, /padding-top: var\(--s-4\); padding-bottom: var\(--s-4\);/);
804
- assert.match(res, /margin-left: var\(--s-4\); margin-right: var\(--s-4\);/);
805
- assert.match(res, /margin-top: var\(--s-4\); margin-bottom: var\(--s-4\);/);
806
- assert.match(res, /column-gap: var\(--s-4\);/);
807
- assert.match(res, /row-gap: var\(--s-4\);/);
808
- });
809
-
810
- await t.test('Dark Mode Selector Strategy', () => {
811
- const tokens = [{ property: 'opacity', value: '50', attrType: 'visual', state: 'dark', raw: 'dark:opacity:50' }];
812
- const config = createTestConfig({ darkMode: 'selector' });
813
- const res = generateCSS(tokens, config);
814
- assert.match(res, /\.dark \[visual~="dark:opacity:50"\]/);
815
-
816
- // Custom selector
817
- const config2 = createTestConfig({ darkMode: ['selector', '[data-theme="dark"]'] });
818
- const res2 = generateCSS(tokens, config2);
819
- assert.match(res2, /\[data-theme="dark"\] \[visual~="dark:opacity:50"\]/);
820
- });
821
-
822
- await t.test('Minify CSS', () => {
823
- const input = `
824
- /* comment */
825
- .foo {
826
- color: red;
827
- margin: 10px 20px ;
828
- }
829
- `;
830
- const minified = minifyCSS(input);
831
- assert.strictEqual(minified, '.foo{color:red;margin:10px 20px;}');
832
- });
833
- });