@bookklik/senangstart-css 0.2.8 → 0.2.10

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 (72) hide show
  1. package/dist/senangstart-css.js +2751 -1952
  2. package/dist/senangstart-css.min.js +266 -225
  3. package/dist/senangstart-tw.js +440 -77
  4. package/dist/senangstart-tw.min.js +1 -1
  5. package/docs/SYNTAX-REFERENCE.md +1731 -1590
  6. package/docs/guide/configuration.md +2 -2
  7. package/docs/guide/preflight.md +20 -1
  8. package/docs/guide/states.md +60 -0
  9. package/docs/ms/guide/configuration.md +2 -2
  10. package/docs/ms/guide/preflight.md +19 -0
  11. package/docs/ms/guide/states.md +60 -0
  12. package/docs/ms/reference/breakpoints.md +14 -0
  13. package/docs/ms/reference/colors.md +2 -2
  14. package/docs/ms/reference/space/height.md +10 -10
  15. package/docs/ms/reference/space/width.md +12 -12
  16. package/docs/ms/reference/visual/border-radius.md +50 -10
  17. package/docs/ms/reference/visual/contain.md +57 -0
  18. package/docs/ms/reference/visual/content-visibility.md +53 -0
  19. package/docs/ms/reference/visual/placeholder-color.md +92 -0
  20. package/docs/ms/reference/visual/writing-mode.md +53 -0
  21. package/docs/ms/reference/visual.md +6 -0
  22. package/docs/public/assets/senangstart-css.min.js +266 -225
  23. package/docs/public/llms.txt +63 -2
  24. package/docs/reference/breakpoints.md +14 -0
  25. package/docs/reference/colors.md +2 -2
  26. package/docs/reference/space/height.md +10 -10
  27. package/docs/reference/space/width.md +12 -12
  28. package/docs/reference/visual/border-radius.md +50 -10
  29. package/docs/reference/visual/contain.md +57 -0
  30. package/docs/reference/visual/content-visibility.md +53 -0
  31. package/docs/reference/visual/placeholder-color.md +92 -0
  32. package/docs/reference/visual/writing-mode.md +53 -0
  33. package/docs/reference/visual.md +7 -0
  34. package/docs/syntax-reference.json +2185 -2009
  35. package/package.json +1 -1
  36. package/public/senangstart.css +1 -1
  37. package/scripts/convert-tailwind.js +486 -89
  38. package/scripts/generate-docs.js +403 -403
  39. package/scripts/generate-llms-txt.js +28 -0
  40. package/src/cdn/senangstart-engine.js +37 -1927
  41. package/src/cdn/tw-conversion-engine.js +504 -78
  42. package/src/cli/commands/build.js +10 -0
  43. package/src/compiler/generators/css.js +400 -67
  44. package/src/compiler/generators/preflight.js +26 -13
  45. package/src/compiler/generators/typescript.js +3 -1
  46. package/src/compiler/index.js +27 -3
  47. package/src/compiler/parser.js +24 -7
  48. package/src/config/defaults.js +4 -1
  49. package/src/core/constants.js +5 -3
  50. package/src/definitions/index.js +7 -3
  51. package/src/definitions/layout.js +2 -2
  52. package/src/definitions/space.js +45 -19
  53. package/src/definitions/visual-performance.js +126 -0
  54. package/src/definitions/visual.js +25 -9
  55. package/src/index.js +47 -0
  56. package/src/utils/common.js +17 -5
  57. package/templates/senangstart.config.js +1 -1
  58. package/tests/helpers/test-utils.js +1 -1
  59. package/tests/integration/compiler.test.js +12 -1
  60. package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
  61. package/tests/unit/compiler/generators/css.test.js +1520 -6
  62. package/tests/unit/compiler/generators/preflight.test.js +31 -0
  63. package/tests/unit/compiler/parser.test.js +26 -0
  64. package/tests/unit/config/defaults.test.js +2 -2
  65. package/tests/unit/convert-tailwind.cli.test.js +95 -0
  66. package/tests/unit/convert-tailwind.coverage.test.js +225 -0
  67. package/tests/unit/convert-tailwind.test.js +61 -21
  68. package/tests/unit/core/tokenizer-core.test.js +102 -0
  69. package/tests/unit/definitions/index.test.js +108 -0
  70. package/tests/unit/definitions/layout_definitions.test.js +40 -0
  71. package/tests/unit/utils/common.test.js +26 -0
  72. package/scripts/bundle-jit.js +0 -45
@@ -108,6 +108,57 @@ const fontSizeScale = {
108
108
  "9xl": "hero", // 8rem → hero
109
109
  };
110
110
 
111
+ // Line height scale mapping Tailwind values to SenangStart semantic values
112
+ // Engine native values: none(1), tight(1.25), snug(1.375), normal(1.5), relaxed(1.625), loose(2)
113
+ const lineHeightScale = {
114
+ none: "none", // line-height: 1
115
+ tight: "tight", // line-height: 1.25
116
+ snug: "snug", // line-height: 1.375
117
+ normal: "normal", // line-height: 1.5
118
+ relaxed: "relaxed", // line-height: 1.625
119
+ loose: "loose" // line-height: 2
120
+ };
121
+
122
+ // Letter spacing scale mapping Tailwind values to SenangStart semantic values
123
+ // Engine native values: tighter(-0.05em), tight(-0.025em), normal(0), wide(0.025em), wider(0.05em), widest(0.1em)
124
+ const letterSpacingScale = {
125
+ tighter: "tighter", // letter-spacing: -0.05em
126
+ tight: "tight", // letter-spacing: -0.025em
127
+ normal: "normal", // letter-spacing: 0
128
+ wide: "wide", // letter-spacing: 0.025em
129
+ wider: "wider", // letter-spacing: 0.05em
130
+ widest: "widest" // letter-spacing: 0.1em
131
+ };
132
+
133
+ // Z-index scale mapping Tailwind values to SenangStart semantic values
134
+ // Engine native values: base(0), low(10), mid(50), high(100), top(9999)
135
+ const zIndexScale = {
136
+ 0: "base", // z-index: 0
137
+ 10: "low", // z-index: 10
138
+ 20: "low", // z-index: 20
139
+ 30: "low", // z-index: 30
140
+ 40: "low", // z-index: 40
141
+ 50: "mid", // z-index: 50
142
+ 60: "high", // z-index: 60
143
+ 70: "high", // z-index: 70
144
+ 80: "high", // z-index: 80
145
+ 90: "high", // z-index: 90
146
+ 100: "high", // z-index: 100
147
+ auto: "auto" // z-index: auto
148
+ };
149
+
150
+ // Fraction scale mapping Tailwind fractions to SenangStart semantic values
151
+ // Used for positioning (left-1/2) and transforms (translate-x-1/2)
152
+ const fractionScale = {
153
+ '1/2': 'half', // 50%
154
+ '1/3': 'third', // 33.33%
155
+ '2/3': 'third-2x', // 66.67%
156
+ '1/4': 'quarter', // 25%
157
+ '2/4': 'half', // 50% (alias)
158
+ '3/4': 'quarter-3x', // 75%
159
+ 'full': 'full', // 100%
160
+ };
161
+
111
162
  const layoutMappings = {
112
163
  container: "container",
113
164
  flex: "flex",
@@ -169,23 +220,108 @@ const layoutMappings = {
169
220
  };
170
221
 
171
222
  const visualKeywords = {
223
+ // Font style
172
224
  italic: "italic",
173
225
  "not-italic": "not-italic",
226
+
227
+ // Font smoothing
174
228
  antialiased: "antialiased",
229
+ "subpixel-antialiased": "subpixel-antialiased",
230
+
231
+ // Text transform
175
232
  uppercase: "uppercase",
176
233
  lowercase: "lowercase",
177
234
  capitalize: "capitalize",
178
235
  "normal-case": "normal-case",
236
+
237
+ // Text decoration
179
238
  underline: "underline",
239
+ overline: "overline",
180
240
  "line-through": "line-through",
181
241
  "no-underline": "no-underline",
242
+
243
+ // Text decoration style
244
+ "decoration-solid": "decoration-solid",
245
+ "decoration-double": "decoration-double",
246
+ "decoration-dotted": "decoration-dotted",
247
+ "decoration-dashed": "decoration-dashed",
248
+ "decoration-wavy": "decoration-wavy",
249
+
250
+ // Text overflow
182
251
  truncate: "truncate",
183
- "cursor-pointer": "cursor:pointer",
252
+ "text-ellipsis": "text-ellipsis",
253
+ "text-clip": "text-clip",
254
+
255
+ // Text wrap
256
+ "text-wrap": "text-wrap",
257
+ "text-nowrap": "text-nowrap",
258
+ "text-balance": "text-balance",
259
+ "text-pretty": "text-pretty",
260
+
261
+ // Whitespace
262
+ "whitespace-normal": "whitespace-normal",
263
+ "whitespace-nowrap": "whitespace-nowrap",
264
+ "whitespace-pre": "whitespace-pre",
265
+ "whitespace-pre-line": "whitespace-pre-line",
266
+ "whitespace-pre-wrap": "whitespace-pre-wrap",
267
+ "whitespace-break-spaces": "whitespace-break-spaces",
268
+
269
+ // Word break
270
+ "break-normal": "break-normal",
271
+ "break-words": "break-words",
272
+ "break-all": "break-all",
273
+ "break-keep": "break-keep",
274
+
275
+ // Hyphens
276
+ "hyphens-none": "hyphens-none",
277
+ "hyphens-manual": "hyphens-manual",
278
+ "hyphens-auto": "hyphens-auto",
279
+
280
+ // List style
281
+ "list-none": "list-none",
282
+ "list-disc": "list-disc",
283
+ "list-decimal": "list-decimal",
284
+ "list-inside": "list-inside",
285
+ "list-outside": "list-outside",
286
+
287
+ // Cursor
288
+ "cursor-auto": "cursor:auto",
184
289
  "cursor-default": "cursor:default",
290
+ "cursor-pointer": "cursor:pointer",
291
+ "cursor-wait": "cursor:wait",
292
+ "cursor-text": "cursor:text",
293
+ "cursor-move": "cursor:move",
185
294
  "cursor-not-allowed": "cursor:not-allowed",
295
+ "cursor-grab": "cursor:grab",
296
+ "cursor-grabbing": "cursor:grabbing",
297
+
298
+ // User select
186
299
  "select-none": "select:none",
187
300
  "select-text": "select:text",
188
301
  "select-all": "select:all",
302
+ "select-auto": "select:auto",
303
+
304
+ // Pointer events
305
+ "pointer-events-none": "pointer-events:none",
306
+ "pointer-events-auto": "pointer-events:auto",
307
+
308
+ // Appearance
309
+ "appearance-none": "appearance:none",
310
+ "appearance-auto": "appearance:auto",
311
+
312
+ // 3D Transforms
313
+ perspective: "perspective",
314
+ "perspective-origin": "perspective-origin",
315
+ "transform-style": "transform-style",
316
+ "backface-visibility": "backface",
317
+ mask: "mask",
318
+ "mask-image": "mask-image",
319
+ "mask-mode": "mask-mode",
320
+ "mask-origin": "mask-origin",
321
+ "mask-position": "mask-position",
322
+ "mask-repeat": "mask-repeat",
323
+ "mask-size": "mask-size",
324
+ "mask-type": "mask-type"
189
325
  };
190
326
 
191
327
  // ============================
@@ -229,35 +365,85 @@ function getBorderWidth(value, exact) {
229
365
 
230
366
  function convertClass(twClass, exact) {
231
367
  // Handle prefixes (hover:, sm:, md:, etc.)
368
+ // Added group-* and peer-* variant support
232
369
  const prefixMatch = twClass.match(
233
- /^(sm:|md:|lg:|xl:|2xl:|hover:|focus:|focus-visible:|active:|disabled:|dark:)(.+)$/
370
+ /^(sm:|md:|lg:|xl:|2xl:|hover:|focus:|focus-visible:|active:|disabled:|dark:|group-hover:|peer-hover:|group-focus:|peer-focus:|group-active:|peer-active:|peer-check:|group-open:|peer-open:)(.+)$/
234
371
  );
235
372
  let prefix = "",
236
- baseClass = twClass;
373
+ baseClass = twClass,
374
+ extraAttr = null;
375
+
237
376
  if (prefixMatch) {
238
377
  const rawPrefix = prefixMatch[1].slice(0, -1); // remove colon
378
+
379
+ // Responsive prefixes
239
380
  if (['sm', 'md', 'lg', 'xl', '2xl'].includes(rawPrefix)) {
240
381
  prefix = `tw-${rawPrefix}:`;
241
- } else {
382
+ }
383
+ // Group/Peer prefixes (map to standard state prefixes)
384
+ else if (rawPrefix.startsWith('group-') || rawPrefix.startsWith('peer-')) {
385
+ const stateMap = {
386
+ 'hover': 'hover',
387
+ 'focus': 'focus', // or focus-within if we strictly follow group logic, but SenangStart group logic handles mapped state triggers
388
+ 'active': 'active',
389
+ 'open': 'expanded', // map open -> expanded
390
+ 'check': 'checked' // map check -> checked
391
+ };
392
+
393
+ const variant = rawPrefix.split('-')[1]; // get 'hover' from 'group-hover'
394
+ const mappedState = stateMap[variant] || variant;
395
+
396
+ prefix = `${mappedState}:`;
397
+
398
+ // For peer variants, we must ensure the element listens to "peer"
399
+ if (rawPrefix.startsWith('peer-')) {
400
+ extraAttr = { cat: 'listens', val: 'peer' };
401
+ }
402
+ }
403
+ // Standard prefixes
404
+ else {
242
405
  prefix = prefixMatch[1];
243
406
  }
407
+
244
408
  baseClass = prefixMatch[2];
245
409
  }
246
410
 
411
+ // Handle 'group' class mapping
412
+ if (baseClass === 'group') {
413
+ return { cat: 'layout', val: 'hoverable focusable pressable expandable' };
414
+ }
415
+
416
+ // Handle 'peer' class mapping
417
+ if (baseClass === 'peer') {
418
+ return [
419
+ { cat: 'layout', val: 'hoverable focusable pressable expandable' },
420
+ { cat: 'interact', val: 'peer' }
421
+ ];
422
+ }
423
+
424
+ // Helper to attach extra attributes (like listens="peer")
425
+ const attachExtra = (result) => {
426
+ if (!result) return null;
427
+ if (extraAttr) {
428
+ return Array.isArray(result) ? [...result, extraAttr] : [result, extraAttr];
429
+ }
430
+ return result;
431
+ };
432
+
247
433
  // Layout mappings
248
434
  if (layoutMappings[baseClass])
249
- return { cat: "layout", val: prefix + layoutMappings[baseClass] };
435
+ return attachExtra({ cat: "layout", val: prefix + layoutMappings[baseClass] });
250
436
 
251
437
  // Visual keywords
252
438
  if (visualKeywords[baseClass])
253
- return { cat: "visual", val: prefix + visualKeywords[baseClass] };
439
+ return attachExtra({ cat: "visual", val: prefix + visualKeywords[baseClass] });
254
440
 
255
441
  // Text color
256
442
  const textColorMatch = baseClass.match(
257
443
  /^text-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)(?:-\d+)?)$/
258
444
  );
259
445
  if (textColorMatch)
260
- return { cat: "visual", val: prefix + "text:" + textColorMatch[1] };
446
+ return attachExtra({ cat: "visual", val: prefix + "text:" + textColorMatch[1] });
261
447
 
262
448
  // Text alignment
263
449
  if (
@@ -265,10 +451,10 @@ function convertClass(twClass, exact) {
265
451
  baseClass
266
452
  )
267
453
  )
268
- return {
454
+ return attachExtra({
269
455
  cat: "visual",
270
456
  val: prefix + "text:" + baseClass.replace("text-", ""),
271
- };
457
+ });
272
458
 
273
459
  // Text size
274
460
  const textSizeMatch = baseClass.match(
@@ -278,7 +464,21 @@ function convertClass(twClass, exact) {
278
464
  const size = exact
279
465
  ? `tw-${textSizeMatch[1]}`
280
466
  : fontSizeScale[textSizeMatch[1]] || textSizeMatch[1];
281
- return { cat: "visual", val: prefix + "text-size:" + size };
467
+ return attachExtra({ cat: "visual", val: prefix + "text-size:" + size });
468
+ }
469
+
470
+ // Line height
471
+ const leadingMatch = baseClass.match(/^leading-(\[.+\]|none|tight|snug|normal|relaxed|loose)$/);
472
+ if (leadingMatch) {
473
+ const val = leadingMatch[1];
474
+ return attachExtra({ cat: "visual", val: prefix + "leading:" + (lineHeightScale[val] || val) });
475
+ }
476
+
477
+ // Letter spacing
478
+ const trackingMatch = baseClass.match(/^tracking-(\[.+\]|tighter|tight|normal|wide|wider|widest)$/);
479
+ if (trackingMatch) {
480
+ const val = trackingMatch[1];
481
+ return attachExtra({ cat: "visual", val: prefix + "tracking:" + (letterSpacingScale[val] || val) });
282
482
  }
283
483
 
284
484
  // Background color
@@ -287,37 +487,56 @@ function convertClass(twClass, exact) {
287
487
  );
288
488
  if (bgMatch) {
289
489
  const colorVal = bgMatch[1];
290
- // Handle special values
490
+ // Handle special CSS keyword values - these are now natively supported
291
491
  if (colorVal === 'transparent') {
292
- return { cat: "visual", val: prefix + "bg:[transparent]" };
492
+ return attachExtra({ cat: "visual", val: prefix + "bg:transparent" });
293
493
  }
294
494
  if (colorVal === 'current') {
295
- return { cat: "visual", val: prefix + "bg:[currentColor]" };
495
+ return attachExtra({ cat: "visual", val: prefix + "bg:currentColor" });
296
496
  }
297
497
  if (colorVal === 'inherit') {
298
- return { cat: "visual", val: prefix + "bg:[inherit]" };
498
+ return attachExtra({ cat: "visual", val: prefix + "bg:inherit" });
299
499
  }
300
- return { cat: "visual", val: prefix + "bg:" + colorVal };
500
+ return attachExtra({ cat: "visual", val: prefix + "bg:" + colorVal });
301
501
  }
302
502
 
303
503
  // Border color
304
504
  const borderColorMatch = baseClass.match(
305
- /^border-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)(?:-\d+)?)$/
505
+ /^border-((?: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+)?)$/
306
506
  );
307
- if (borderColorMatch)
308
- return {
507
+ if (borderColorMatch) {
508
+ let colorVal = borderColorMatch[1];
509
+ // Map 'current' to 'currentColor' for CSS compatibility
510
+ if (colorVal === 'current') colorVal = 'currentColor';
511
+ return attachExtra({
309
512
  cat: "visual",
310
- val: prefix + "border:" + borderColorMatch[1],
311
- };
513
+ val: prefix + "border:" + colorVal,
514
+ });
515
+ }
516
+
517
+ // Directional border colors (border-t-*, border-r-*, border-b-*, border-l-*)
518
+ const borderSideColorMatch = baseClass.match(
519
+ /^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+)?)$/
520
+ );
521
+ if (borderSideColorMatch) {
522
+ const side = borderSideColorMatch[1]; // t, r, b, or l
523
+ let colorVal = borderSideColorMatch[2];
524
+ // Map 'current' to 'currentColor' for CSS compatibility
525
+ if (colorVal === 'current') colorVal = 'currentColor';
526
+ return attachExtra({
527
+ cat: "visual",
528
+ val: prefix + `border-${side}:${colorVal}`,
529
+ });
530
+ }
312
531
 
313
532
  // Padding
314
533
  const paddingMatch = baseClass.match(/^p([trblxy])?-(.+)$/);
315
534
  if (paddingMatch) {
316
535
  const side = paddingMatch[1] ? "-" + paddingMatch[1] : "";
317
- return {
536
+ return attachExtra({
318
537
  cat: "space",
319
538
  val: prefix + "p" + side + ":" + getSpacing(paddingMatch[2], exact),
320
- };
539
+ });
321
540
  }
322
541
 
323
542
  // Margin
@@ -339,17 +558,17 @@ function convertClass(twClass, exact) {
339
558
  val = `-${val}`;
340
559
  }
341
560
  }
342
- return { cat: "space", val: prefix + "m" + side + ":" + val };
561
+ return attachExtra({ cat: "space", val: prefix + "m" + side + ":" + val });
343
562
  }
344
563
 
345
564
  // Gap
346
565
  const gapMatch = baseClass.match(/^gap-([xy])?-?(.+)$/);
347
566
  if (gapMatch) {
348
567
  const axis = gapMatch[1] ? "-" + gapMatch[1] : "";
349
- return {
568
+ return attachExtra({
350
569
  cat: "space",
351
570
  val: prefix + "g" + axis + ":" + getSpacing(gapMatch[2], exact),
352
- };
571
+ });
353
572
  }
354
573
 
355
574
  // Width/Height with special values
@@ -360,7 +579,7 @@ function convertClass(twClass, exact) {
360
579
  // Special width values
361
580
  const specialWidthVals = { 'max': '[max-content]', 'min': '[min-content]', 'fit': '[fit-content]', 'prose': '[65ch]' };
362
581
  const val = specialWidthVals[rawVal] || getSpacing(rawVal, exact);
363
- return { cat: "space", val: prefix + prop + ":" + val };
582
+ return attachExtra({ cat: "space", val: prefix + prop + ":" + val });
364
583
  }
365
584
  const heightMatch = baseClass.match(/^(min-h|max-h|h)-(.+)$/);
366
585
  if (heightMatch) {
@@ -368,7 +587,7 @@ function convertClass(twClass, exact) {
368
587
  const rawVal = heightMatch[2];
369
588
  const specialHeightVals = { 'screen': '[100vh]', 'svh': '[100svh]', 'lvh': '[100lvh]', 'dvh': '[100dvh]', 'max': '[max-content]', 'min': '[min-content]', 'fit': '[fit-content]' };
370
589
  const val = specialHeightVals[rawVal] || getSpacing(rawVal, exact);
371
- return { cat: "space", val: prefix + prop + ":" + val };
590
+ return attachExtra({ cat: "space", val: prefix + prop + ":" + val });
372
591
  }
373
592
 
374
593
  // Border radius
@@ -380,7 +599,7 @@ function convertClass(twClass, exact) {
380
599
  ? "tw-DEFAULT"
381
600
  : `tw-${size}`
382
601
  : radiusScale[size] || "medium";
383
- return { cat: "visual", val: prefix + "rounded:" + scale };
602
+ return attachExtra({ cat: "visual", val: prefix + "rounded:" + scale });
384
603
  }
385
604
 
386
605
  // Shadow
@@ -392,7 +611,7 @@ function convertClass(twClass, exact) {
392
611
  ? "tw-DEFAULT"
393
612
  : `tw-${size}`
394
613
  : shadowScale[size] || "medium";
395
- return { cat: "visual", val: prefix + "shadow:" + scale };
614
+ return attachExtra({ cat: "visual", val: prefix + "shadow:" + scale });
396
615
  }
397
616
 
398
617
  // Font weight
@@ -400,7 +619,7 @@ function convertClass(twClass, exact) {
400
619
  /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/
401
620
  );
402
621
  if (fontWeightMatch)
403
- return { cat: "visual", val: prefix + "font:tw-" + fontWeightMatch[1] };
622
+ return attachExtra({ cat: "visual", val: prefix + "font:tw-" + fontWeightMatch[1] });
404
623
 
405
624
  // Border width
406
625
  const borderWidthMatch = baseClass.match(
@@ -415,98 +634,156 @@ function convertClass(twClass, exact) {
415
634
  ? "-" + borderWidthMatch[1] + "-w"
416
635
  : "-w";
417
636
  const width = borderWidthMatch[2] || "1";
418
- return {
637
+ return attachExtra({
419
638
  cat: "visual",
420
639
  val: prefix + "border" + side + ":" + getBorderWidth(width, exact),
421
- };
640
+ });
422
641
  }
423
642
 
424
643
  // Positional properties (top-0, right-0, bottom-0, left-0, inset-0, etc.)
425
- const positionMatch = baseClass.match(/^(top|right|bottom|left|inset|inset-x|inset-y)-(\d+|px|auto|full|\[.+\])$/);
644
+ // Includes fraction support: left-1/2, top-1/3, etc.
645
+ 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|\[.+\])$/);
426
646
  if (positionMatch) {
427
647
  const prop = positionMatch[1];
428
648
  let val = positionMatch[2];
429
- // Handle 0 specially
430
- if (val === '0') {
431
- val = 'none';
432
- } else if (val.startsWith('[') && val.endsWith(']')) {
649
+ if (val.startsWith('[') && val.endsWith(']')) {
433
650
  // Keep arbitrary values as-is
651
+ } else if (fractionScale[val]) {
652
+ // Map fractions to semantic names (1/2 → half, etc.)
653
+ val = fractionScale[val];
654
+ } else if (val === '0') {
655
+ // Keep 0 as-is for positioning (CSS: top: 0, not top: none)
656
+ val = '0';
434
657
  } else {
435
658
  val = getSpacing(val, exact);
436
659
  }
437
- return { cat: "layout", val: prefix + prop + ":" + val };
660
+ return attachExtra({ cat: "layout", val: prefix + prop + ":" + val });
661
+ }
662
+
663
+ // Translate transform utilities: translate-x-*, translate-y-*, -translate-x-*, -translate-y-*
664
+ const translateMatch = baseClass.match(/^(-?)translate-([xy])-(\d+|px|full|1\/2|1\/3|2\/3|1\/4|2\/4|3\/4|\[.+\])$/);
665
+ if (translateMatch) {
666
+ const isNeg = translateMatch[1] === '-';
667
+ const axis = translateMatch[2];
668
+ let val = translateMatch[3];
669
+
670
+ // Map fractions and values
671
+ if (val.startsWith('[') && val.endsWith(']')) {
672
+ // Keep arbitrary values as-is, but handle negative
673
+ if (isNeg) {
674
+ const inner = val.slice(1, -1);
675
+ val = `[-${inner}]`;
676
+ }
677
+ } else if (fractionScale[val]) {
678
+ val = fractionScale[val];
679
+ if (isNeg) val = `-${val}`;
680
+ } else if (val === '0') {
681
+ val = '0';
682
+ } else {
683
+ val = getSpacing(val, exact);
684
+ if (isNeg) val = `-${val}`;
685
+ }
686
+
687
+ return attachExtra({ cat: "visual", val: prefix + `translate-${axis}:${val}` });
438
688
  }
439
689
 
440
690
  // Outline none
441
691
  if (baseClass === 'outline-none') {
442
- return { cat: "visual", val: prefix + "outline:none" };
692
+ return attachExtra({ cat: "visual", val: prefix + "outline:none" });
443
693
  }
444
694
 
445
695
  // Order
446
696
  const orderMatch = baseClass.match(/^order-(\d+|first|last|none)$/);
447
697
  if (orderMatch) {
448
- return { cat: "layout", val: prefix + "order:" + orderMatch[1] };
698
+ return attachExtra({ cat: "layout", val: prefix + "order:" + orderMatch[1] });
699
+ }
700
+
701
+ // Z-index
702
+ const zIndexMatch = baseClass.match(/^-?z-(\d+|auto)$/);
703
+ if (zIndexMatch) {
704
+ const isNeg = baseClass.startsWith("-");
705
+ const val = zIndexMatch[1];
706
+ let zIndexVal = zIndexScale[val] || val;
707
+ if (isNeg) {
708
+ zIndexVal = `-${zIndexVal}`;
709
+ }
710
+ return attachExtra({ cat: "layout", val: prefix + "z:" + zIndexVal });
711
+ }
712
+
713
+ // Flex basis
714
+ const basisMatch = baseClass.match(/^basis-(\[.+\]|\d+\.?\d*|auto|full|1\/2|1\/3|2\/3|1\/4|2\/4|3\/4)$/);
715
+ if (basisMatch) {
716
+ let val = basisMatch[1];
717
+ if (val.startsWith('[') && val.endsWith(']')) {
718
+ // Keep arbitrary values as-is
719
+ } else if (fractionScale[val]) {
720
+ // Map fractions to semantic names (1/2 → half, etc.)
721
+ val = fractionScale[val];
722
+ } else if (val === '0') {
723
+ val = '0';
724
+ }
725
+ return attachExtra({ cat: "layout", val: prefix + "basis:" + val });
449
726
  }
450
727
 
451
728
  // Grid columns
452
729
  const gridColsMatch = baseClass.match(/^grid-cols-(\d+|none)$/);
453
730
  if (gridColsMatch) {
454
- return { cat: "layout", val: prefix + "grid-cols:" + gridColsMatch[1] };
731
+ return attachExtra({ cat: "layout", val: prefix + "grid-cols:" + gridColsMatch[1] });
455
732
  }
456
733
 
457
734
  // Column span
458
735
  const colSpanMatch = baseClass.match(/^col-span-(\d+|full)$/);
459
736
  if (colSpanMatch) {
460
- return { cat: "layout", val: prefix + "col-span:" + colSpanMatch[1] };
737
+ return attachExtra({ cat: "layout", val: prefix + "col-span:" + colSpanMatch[1] });
461
738
  }
462
739
 
463
740
  // Grid rows
464
741
  const gridRowsMatch = baseClass.match(/^grid-rows-(\d+|none)$/);
465
742
  if (gridRowsMatch) {
466
- return { cat: "layout", val: prefix + "grid-rows:" + gridRowsMatch[1] };
743
+ return attachExtra({ cat: "layout", val: prefix + "grid-rows:" + gridRowsMatch[1] });
467
744
  }
468
745
 
469
746
  // Row span
470
747
  const rowSpanMatch = baseClass.match(/^row-span-(\d+|full)$/);
471
748
  if (rowSpanMatch) {
472
- return { cat: "layout", val: prefix + "row-span:" + rowSpanMatch[1] };
749
+ return attachExtra({ cat: "layout", val: prefix + "row-span:" + rowSpanMatch[1] });
473
750
  }
474
751
 
475
752
  // Opacity
476
753
  const opacityMatch = baseClass.match(/^opacity-(\d+)$/);
477
754
  if (opacityMatch) {
478
- return { cat: "visual", val: prefix + "opacity:" + opacityMatch[1] };
755
+ return attachExtra({ cat: "visual", val: prefix + "opacity:" + opacityMatch[1] });
479
756
  }
480
757
 
481
758
  // Gradient direction (bg-gradient-to-*)
482
759
  const bgGradientMatch = baseClass.match(/^bg-gradient-to-(t|tr|r|br|b|bl|l|tl)$/);
483
760
  if (bgGradientMatch) {
484
- return { cat: "visual", val: prefix + "bg-image:gradient-to-" + bgGradientMatch[1] };
761
+ return attachExtra({ cat: "visual", val: prefix + "bg-image:gradient-to-" + bgGradientMatch[1] });
485
762
  }
486
763
 
487
764
  // Gradient from-* (starting color)
488
765
  const fromMatch = baseClass.match(/^from-(.+)$/);
489
766
  if (fromMatch) {
490
- return { cat: "visual", val: prefix + "from:" + fromMatch[1] };
767
+ return attachExtra({ cat: "visual", val: prefix + "from:" + fromMatch[1] });
491
768
  }
492
769
 
493
770
  // Gradient via-* (middle color)
494
771
  const viaMatch = baseClass.match(/^via-(.+)$/);
495
772
  if (viaMatch) {
496
- return { cat: "visual", val: prefix + "via:" + viaMatch[1] };
773
+ return attachExtra({ cat: "visual", val: prefix + "via:" + viaMatch[1] });
497
774
  }
498
775
 
499
776
  // Gradient to-* (ending color) - Note: must come after bg-gradient-to-*
500
777
  const toMatch = baseClass.match(/^to-(.+)$/);
501
778
  if (toMatch) {
502
- return { cat: "visual", val: prefix + "to:" + toMatch[1] };
779
+ return attachExtra({ cat: "visual", val: prefix + "to:" + toMatch[1] });
503
780
  }
504
781
 
505
782
  // Transition utilities
506
783
  const transitionMatch = baseClass.match(/^transition(?:-(all|colors|opacity|shadow|transform|none))?$/);
507
784
  if (transitionMatch) {
508
785
  const type = transitionMatch[1] || 'all';
509
- return { cat: "visual", val: prefix + "transition:" + type };
786
+ return attachExtra({ cat: "visual", val: prefix + "transition:" + type });
510
787
  }
511
788
 
512
789
  // Duration utilities
@@ -522,13 +799,13 @@ function convertClass(twClass, exact) {
522
799
  else if (ms <= 300) durationVal = 'slow';
523
800
  else if (ms <= 500) durationVal = 'slower';
524
801
  else durationVal = 'lazy';
525
- return { cat: "visual", val: prefix + "duration:" + durationVal };
802
+ return attachExtra({ cat: "visual", val: prefix + "duration:" + durationVal });
526
803
  }
527
804
 
528
805
  // Ease utilities
529
806
  const easeMatch = baseClass.match(/^ease-(linear|in|out|in-out)$/);
530
807
  if (easeMatch) {
531
- return { cat: "visual", val: prefix + "ease:" + easeMatch[1] };
808
+ return attachExtra({ cat: "visual", val: prefix + "ease:" + easeMatch[1] });
532
809
  }
533
810
 
534
811
  // Ring utilities - Convert to native ring utilities
@@ -537,105 +814,237 @@ function convertClass(twClass, exact) {
537
814
  if (ringMatch) {
538
815
  const width = ringMatch[1] || '3';
539
816
  if (width === '0') {
540
- return { cat: "visual", val: prefix + "ring:none" };
817
+ return attachExtra({ cat: "visual", val: prefix + "ring:none" });
541
818
  }
542
819
  // Map Tailwind ring widths to SenangStart semantic values
543
820
  const ringScale = {
544
821
  '1': 'thin', '2': 'regular', '3': 'small', '4': 'medium', '8': 'big'
545
822
  };
546
823
  const scale = ringScale[width] || `[${width}px]`;
547
- return { cat: "visual", val: prefix + "ring:" + scale };
824
+ return attachExtra({ cat: "visual", val: prefix + "ring:" + scale });
548
825
  }
549
826
 
550
827
  // Ring offset - converts to native ring-offset utility
551
828
  const ringOffsetMatch = baseClass.match(/^ring-offset-(\d+)$/);
552
829
  if (ringOffsetMatch) {
553
- return { cat: "visual", val: prefix + "ring-offset:" + ringOffsetMatch[1] };
830
+ return attachExtra({ cat: "visual", val: prefix + "ring-offset:" + ringOffsetMatch[1] });
554
831
  }
555
832
 
556
833
  // Ring color - converts to native ring-color utility
557
834
  const ringColorMatch = baseClass.match(/^ring-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)(?:-\d+)?)$/);
558
835
  if (ringColorMatch) {
559
- return { cat: "visual", val: prefix + "ring-color:" + ringColorMatch[1] };
836
+ return attachExtra({ cat: "visual", val: prefix + "ring-color:" + ringColorMatch[1] });
560
837
  }
561
838
 
562
839
  // Divide color - directional (check divide-x and divide-y BEFORE generic divide)
563
840
  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)(?:-\d+)?)$/);
564
841
  if (divideXMatch) {
565
- return {
842
+ return attachExtra({
566
843
  cat: "visual",
567
844
  val: prefix + "divide-x:" + divideXMatch[1],
568
- };
845
+ });
569
846
  }
570
847
 
571
848
  const divideYMatch = baseClass.match(/^divide-y-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)(?:-\d+)?)$/);
572
849
  if (divideYMatch) {
573
- return {
850
+ return attachExtra({
574
851
  cat: "visual",
575
852
  val: prefix + "divide-y:" + divideYMatch[1],
576
- };
853
+ });
577
854
  }
578
855
 
579
856
  // Divide color - all directions (check divide-x and divide-y AFTER generic divide)
580
857
  const divideColorMatch = baseClass.match(/^divide-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)(?:-\d+)?)$/);
581
858
  if (divideColorMatch) {
582
- return {
859
+ return attachExtra({
583
860
  cat: "visual",
584
861
  val: prefix + "divide:" + divideColorMatch[1],
585
- };
862
+ });
586
863
  }
587
864
 
588
865
  // Divide width - all directions
589
866
  const divideWidthMatch = baseClass.match(/^divide-(\d+)$/);
590
867
  if (divideWidthMatch) {
591
- return {
868
+ return attachExtra({
592
869
  cat: "visual",
593
870
  val: prefix + "divide-w:" + getBorderWidth(divideWidthMatch[1], exact),
594
- };
871
+ });
595
872
  }
596
873
 
597
874
  // Divide reverse (check these FIRST as they are specific)
598
875
  if (baseClass === 'divide-x-reverse') {
599
- return { cat: "visual", val: prefix + "divide-x:reverse" };
876
+ return attachExtra({ cat: "visual", val: prefix + "divide-x:reverse" });
600
877
  }
601
878
  if (baseClass === 'divide-y-reverse') {
602
- return { cat: "visual", val: prefix + "divide-y:reverse" };
879
+ return attachExtra({ cat: "visual", val: prefix + "divide-y:reverse" });
603
880
  }
604
881
 
605
882
  // Divide width - directional
606
883
  const divideXWidthMatch = baseClass.match(/^divide-x-(\d+)$/);
607
884
  if (divideXWidthMatch) {
608
- return {
885
+ return attachExtra({
609
886
  cat: "visual",
610
887
  val: prefix + "divide-x-w:" + getBorderWidth(divideXWidthMatch[1], exact),
611
- };
888
+ });
612
889
  }
613
890
 
614
891
  // Divide width (implicit x/y from Tailwind divide-x/y without number is usually 1px)
615
892
  // Tailwind: divide-x = border-right-width: 1px (or left if reverse).
616
893
  // SenangStart: divide-x-w:thin
617
894
  if (baseClass === 'divide-x') {
618
- return { cat: "visual", val: prefix + "divide-x-w:thin" };
895
+ return attachExtra({ cat: "visual", val: prefix + "divide-x-w:thin" });
619
896
  }
620
897
  if (baseClass === 'divide-y') {
621
- return { cat: "visual", val: prefix + "divide-y-w:thin" };
898
+ return attachExtra({ cat: "visual", val: prefix + "divide-y-w:thin" });
622
899
  }
623
900
 
624
901
  const divideYWidthMatch = baseClass.match(/^divide-y-(\d+)$/);
625
902
  if (divideYWidthMatch) {
626
- return {
903
+ return attachExtra({
627
904
  cat: "visual",
628
905
  val: prefix + "divide-y-w:" + getBorderWidth(divideYWidthMatch[1], exact),
629
- };
906
+ });
630
907
  }
631
908
 
632
909
  // Divide style
633
910
  const divideStyleMatch = baseClass.match(/^divide-(solid|dashed|dotted|double|none)$/);
634
911
  if (divideStyleMatch) {
635
- return {
912
+ return attachExtra({
636
913
  cat: "visual",
637
914
  val: prefix + "divide-style:" + divideStyleMatch[1], // Fixed category from 'color' to 'visual'
915
+ });
916
+ }
917
+
918
+ // Border style
919
+ const borderStyleMatch = baseClass.match(/^border-(solid|dashed|dotted|double|none)$/);
920
+ if (borderStyleMatch) {
921
+ return attachExtra({
922
+ cat: "visual",
923
+ val: prefix + "border-style:" + borderStyleMatch[1],
924
+ });
925
+ }
926
+
927
+ // Filter utilities
928
+ // Blur
929
+ const blurMatch = baseClass.match(/^blur-(0|sm|md|lg|xl|2xl|3xl)$/);
930
+ if (blurMatch) {
931
+ const blurScale = {
932
+ '0': 'none',
933
+ 'sm': 'tiny',
934
+ 'md': 'small',
935
+ 'lg': 'medium',
936
+ 'xl': 'big',
937
+ '2xl': 'giant',
938
+ '3xl': 'vast'
939
+ };
940
+ return attachExtra({
941
+ cat: "visual",
942
+ val: prefix + "blur:" + blurScale[blurMatch[1]],
943
+ });
944
+ }
945
+
946
+ // Brightness
947
+ const brightnessMatch = baseClass.match(/^brightness-(0|50|75|90|95|100|105|110|125|150|200)$/);
948
+ if (brightnessMatch) {
949
+ const brightnessScale = {
950
+ '0': 'dim',
951
+ '50': 'dim',
952
+ '75': 'dark',
953
+ '90': 'dark',
954
+ '95': 'dark',
955
+ '100': 'normal',
956
+ '105': 'bright',
957
+ '110': 'bright',
958
+ '125': 'vivid',
959
+ '150': 'vivid',
960
+ '200': 'vivid'
638
961
  };
962
+ return attachExtra({
963
+ cat: "visual",
964
+ val: prefix + "brightness:" + brightnessScale[brightnessMatch[1]],
965
+ });
966
+ }
967
+
968
+ // Contrast
969
+ const contrastMatch = baseClass.match(/^contrast-(0|50|75|100|125|150|200)$/);
970
+ if (contrastMatch) {
971
+ const contrastScale = {
972
+ '0': 'low',
973
+ '50': 'low',
974
+ '75': 'reduced',
975
+ '100': 'normal',
976
+ '125': 'high',
977
+ '150': 'high',
978
+ '200': 'max'
979
+ };
980
+ return attachExtra({
981
+ cat: "visual",
982
+ val: prefix + "contrast:" + contrastScale[contrastMatch[1]],
983
+ });
984
+ }
985
+
986
+ // Grayscale
987
+ const grayscaleMatch = baseClass.match(/^grayscale(0)?$/);
988
+ if (grayscaleMatch) {
989
+ const val = grayscaleMatch[1] === '0' ? 'none' : 'full';
990
+ return attachExtra({
991
+ cat: "visual",
992
+ val: prefix + "grayscale:" + val,
993
+ });
994
+ }
995
+
996
+ // Hue rotate
997
+ const hueRotateMatch = baseClass.match(/^hue-rotate-(0|15|30|60|90|180)$/);
998
+ if (hueRotateMatch) {
999
+ return attachExtra({
1000
+ cat: "visual",
1001
+ val: prefix + "hue-rotate:" + hueRotateMatch[1],
1002
+ });
1003
+ }
1004
+
1005
+ // Invert
1006
+ const invertMatch = baseClass.match(/^invert(0)?$/);
1007
+ if (invertMatch) {
1008
+ const val = invertMatch[1] === '0' ? 'none' : 'full';
1009
+ return attachExtra({
1010
+ cat: "visual",
1011
+ val: prefix + "invert:" + val,
1012
+ });
1013
+ }
1014
+
1015
+ // Saturate
1016
+ const saturateMatch = baseClass.match(/^saturate-(0|50|100|150|200)$/);
1017
+ if (saturateMatch) {
1018
+ const saturateScale = {
1019
+ '0': 'none',
1020
+ '50': 'low',
1021
+ '100': 'normal',
1022
+ '150': 'high',
1023
+ '200': 'vivid'
1024
+ };
1025
+ return attachExtra({
1026
+ cat: "visual",
1027
+ val: prefix + "saturate:" + saturateScale[saturateMatch[1]],
1028
+ });
1029
+ }
1030
+
1031
+ // Sepia
1032
+ const sepiaMatch = baseClass.match(/^sepia(0)?$/);
1033
+ if (sepiaMatch) {
1034
+ const val = sepiaMatch[1] === '0' ? 'none' : 'full';
1035
+ return attachExtra({
1036
+ cat: "visual",
1037
+ val: prefix + "sepia:" + val,
1038
+ });
1039
+ }
1040
+
1041
+ // Animation utilities
1042
+ const animateMatch = baseClass.match(/^animate-(none|spin|ping|pulse|bounce)$/);
1043
+ if (animateMatch) {
1044
+ return attachExtra({
1045
+ cat: "visual",
1046
+ val: prefix + "animate:" + animateMatch[1],
1047
+ });
639
1048
  }
640
1049
 
641
1050
  return null;
@@ -649,27 +1058,42 @@ function convertClasses(classString, exact) {
649
1058
  const layout = [],
650
1059
  space = [],
651
1060
  visual = [],
1061
+ interact = [],
1062
+ listens = [],
652
1063
  unknown = [];
653
1064
 
1065
+ // Helper to push unique
1066
+ const pushUnique = (arr, val) => {
1067
+ if (!arr.includes(val)) arr.push(val);
1068
+ };
1069
+
654
1070
  for (const cls of classes) {
655
1071
  const result = convertClass(cls, exact);
1072
+
656
1073
  if (result) {
657
- if (result.cat === "layout") layout.push(result.val);
658
- else if (result.cat === "space") space.push(result.val);
659
- else if (result.cat === "visual") visual.push(result.val);
1074
+ // Normalize to array to support 1-to-many mapping
1075
+ const results = Array.isArray(result) ? result : [result];
1076
+
1077
+ for (const res of results) {
1078
+ if (res.cat === "layout") pushUnique(layout, res.val);
1079
+ else if (res.cat === "space") pushUnique(space, res.val);
1080
+ else if (res.cat === "visual") pushUnique(visual, res.val);
1081
+ else if (res.cat === "interact") pushUnique(interact, res.val);
1082
+ else if (res.cat === "listens") pushUnique(listens, res.val);
1083
+ }
660
1084
  } else {
661
1085
  unknown.push(cls);
662
1086
  }
663
1087
  }
664
1088
 
665
- return { layout, space, visual, unknown };
1089
+ return { layout, space, visual, interact, listens, unknown };
666
1090
  }
667
1091
 
668
1092
  function convertHTML(html, exact) {
669
1093
  return html.replace(
670
1094
  /class=(['"])([^"']+)\1/g,
671
1095
  (match, quote, classValue) => {
672
- const { layout, space, visual, unknown } = convertClasses(
1096
+ const { layout, space, visual, interact, listens, unknown } = convertClasses(
673
1097
  classValue,
674
1098
  exact
675
1099
  );
@@ -677,6 +1101,8 @@ function convertHTML(html, exact) {
677
1101
  if (layout.length) attrs.push(`layout="${layout.join(" ")}"`);
678
1102
  if (space.length) attrs.push(`space="${space.join(" ")}"`);
679
1103
  if (visual.length) attrs.push(`visual="${visual.join(" ")}"`);
1104
+ if (interact.length) attrs.push(`interact="${interact.join(" ")}"`);
1105
+ if (listens.length) attrs.push(`listens="${listens.join(" ")}"`);
680
1106
  if (unknown.length) attrs.push(`class="${unknown.join(" ")}"`);
681
1107
  return attrs.join(" ") || 'class=""';
682
1108
  }