cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5260f1a5703bb69e6c7f7cf0ce8033a561cac8b5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/kumo-figma/src/parsers/tailwind-to-figma.test.ts

892lines · modecode

1import { describe, it, expect } from "vitest";
2import { parseTailwindClasses, parseBaseStyles } from "./tailwind-to-figma";
3
4describe("parseTailwindClasses", () => {
5 describe("Layout Properties", () => {
6 it("should parse height from Tailwind scale", () => {
7 expect(parseTailwindClasses("h-5")).toEqual({ height: 20 });
8 expect(parseTailwindClasses("h-9")).toEqual({ height: 36 });
9 expect(parseTailwindClasses("h-10")).toEqual({ height: 40 });
10 });
11
12 it("should parse custom height with decimal", () => {
13 expect(parseTailwindClasses("h-6.5")).toEqual({ height: 26 });
14 });
15
16 it("should parse height with fallback calculation for unknown values", () => {
17 // For values not in SPACING_SCALE, it should multiply by 4
18 expect(parseTailwindClasses("h-15")).toEqual({ height: 60 }); // 15 * 4
19 });
20
21 it("should parse size (square dimensions) from Tailwind scale", () => {
22 expect(parseTailwindClasses("size-3.5")).toEqual({
23 width: 14,
24 height: 14,
25 });
26 expect(parseTailwindClasses("size-6.5")).toEqual({
27 width: 26,
28 height: 26,
29 });
30 expect(parseTailwindClasses("size-9")).toEqual({ width: 36, height: 36 });
31 expect(parseTailwindClasses("size-10")).toEqual({
32 width: 40,
33 height: 40,
34 });
35 });
36
37 it("should parse size with fallback calculation for unknown values", () => {
38 // For values not in SPACING_SCALE, it should multiply by 4
39 expect(parseTailwindClasses("size-15")).toEqual({
40 width: 60,
41 height: 60,
42 }); // 15 * 4
43 });
44
45 it("should parse horizontal padding", () => {
46 expect(parseTailwindClasses("px-2")).toEqual({ paddingX: 8 });
47 expect(parseTailwindClasses("px-3")).toEqual({ paddingX: 12 });
48 expect(parseTailwindClasses("px-4")).toEqual({ paddingX: 16 });
49 });
50
51 it("should parse horizontal padding with decimal", () => {
52 expect(parseTailwindClasses("px-1.5")).toEqual({ paddingX: 6 });
53 });
54
55 it("should parse vertical padding", () => {
56 expect(parseTailwindClasses("py-0.5")).toEqual({ paddingY: 2 });
57 expect(parseTailwindClasses("py-1")).toEqual({ paddingY: 4 });
58 expect(parseTailwindClasses("py-2")).toEqual({ paddingY: 8 });
59 });
60
61 it("should parse gap", () => {
62 expect(parseTailwindClasses("gap-1")).toEqual({ gap: 4 });
63 expect(parseTailwindClasses("gap-1.5")).toEqual({ gap: 6 });
64 expect(parseTailwindClasses("gap-2")).toEqual({ gap: 8 });
65 });
66
67 it("should parse border radius", () => {
68 // Values match Tailwind v4 theme.css --radius-* definitions
69 expect(parseTailwindClasses("rounded-sm")).toEqual({ borderRadius: 4 }); // --radius-sm: 0.25rem = 4px
70 expect(parseTailwindClasses("rounded-md")).toEqual({ borderRadius: 6 }); // --radius-md: 0.375rem = 6px
71 expect(parseTailwindClasses("rounded-lg")).toEqual({ borderRadius: 8 }); // --radius-lg: 0.5rem = 8px
72 expect(parseTailwindClasses("rounded-xl")).toEqual({ borderRadius: 12 }); // --radius-xl: 0.75rem = 12px
73 expect(parseTailwindClasses("rounded-2xl")).toEqual({ borderRadius: 16 }); // --radius-2xl: 1rem = 16px
74 expect(parseTailwindClasses("rounded-3xl")).toEqual({ borderRadius: 24 }); // --radius-3xl: 1.5rem = 24px
75 expect(parseTailwindClasses("rounded-full")).toEqual({
76 borderRadius: 9999,
77 });
78 });
79
80 it("should parse default border radius", () => {
81 expect(parseTailwindClasses("rounded")).toEqual({ borderRadius: 4 });
82 });
83
84 it("should parse none border radius", () => {
85 expect(parseTailwindClasses("rounded-none")).toEqual({ borderRadius: 0 });
86 });
87 });
88
89 describe("Typography", () => {
90 it("should parse font sizes", () => {
91 expect(parseTailwindClasses("text-xs")).toEqual({ fontSize: 12 });
92 expect(parseTailwindClasses("text-sm")).toEqual({ fontSize: 13 }); // Kumo theme: --text-sm: 13px
93 expect(parseTailwindClasses("text-base")).toEqual({ fontSize: 14 }); // Kumo theme: --text-base: 14px
94 expect(parseTailwindClasses("text-lg")).toEqual({ fontSize: 16 }); // Kumo theme: --text-lg: 16px
95 expect(parseTailwindClasses("text-xl")).toEqual({ fontSize: 20 });
96 expect(parseTailwindClasses("text-2xl")).toEqual({ fontSize: 24 });
97 expect(parseTailwindClasses("text-3xl")).toEqual({ fontSize: 30 });
98 });
99 });
100
101 describe("Background Colors", () => {
102 it("should parse semantic background colors", () => {
103 expect(parseTailwindClasses("bg-kumo-brand")).toEqual({
104 fillVariable: "color-kumo-brand",
105 });
106 expect(parseTailwindClasses("bg-kumo-control")).toEqual({
107 fillVariable: "color-kumo-control",
108 });
109 expect(parseTailwindClasses("bg-kumo-base")).toEqual({
110 fillVariable: "color-kumo-base",
111 });
112 expect(parseTailwindClasses("bg-kumo-danger")).toEqual({
113 fillVariable: "color-kumo-danger",
114 });
115 expect(parseTailwindClasses("bg-kumo-info")).toEqual({
116 fillVariable: "color-kumo-info",
117 });
118 expect(parseTailwindClasses("bg-kumo-warning")).toEqual({
119 fillVariable: "color-kumo-warning",
120 });
121 });
122
123 it("should parse transparent and inherit backgrounds", () => {
124 expect(parseTailwindClasses("bg-transparent")).toEqual({
125 fillVariable: null,
126 });
127 expect(parseTailwindClasses("bg-inherit")).toEqual({
128 fillVariable: null,
129 });
130 });
131
132 it("should parse background colors with opacity modifiers", () => {
133 expect(parseTailwindClasses("bg-kumo-info/20")).toEqual({
134 fillVariable: "color-kumo-info/20",
135 fillOpacity: 0.2,
136 });
137 expect(parseTailwindClasses("bg-kumo-danger/50")).toEqual({
138 fillVariable: "color-kumo-danger/50",
139 fillOpacity: 0.5,
140 });
141 expect(parseTailwindClasses("bg-kumo-brand/80")).toEqual({
142 fillVariable: "color-kumo-brand/80",
143 fillOpacity: 0.8,
144 });
145 });
146
147 it("should handle unknown background colors gracefully", () => {
148 expect(parseTailwindClasses("bg-unknown")).toEqual({});
149 });
150 });
151
152 describe("Text Colors", () => {
153 it("should parse semantic text colors", () => {
154 expect(parseTailwindClasses("text-kumo-default")).toEqual({
155 textVariable: "text-color-kumo-default",
156 });
157 expect(parseTailwindClasses("text-kumo-inverse")).toEqual({
158 textVariable: "text-color-kumo-inverse",
159 });
160 expect(parseTailwindClasses("text-kumo-danger")).toEqual({
161 textVariable: "text-color-kumo-danger",
162 });
163 expect(parseTailwindClasses("text-kumo-link")).toEqual({
164 textVariable: "text-color-kumo-link",
165 });
166 expect(parseTailwindClasses("text-kumo-subtle")).toEqual({
167 textVariable: "text-color-kumo-subtle",
168 });
169 expect(parseTailwindClasses("text-kumo-strong")).toEqual({
170 textVariable: "text-color-kumo-strong",
171 });
172 });
173
174 it("should parse white text color with flag", () => {
175 expect(parseTailwindClasses("text-white")).toEqual({
176 textVariable: null,
177 isWhiteText: true,
178 });
179 });
180
181 it("should parse important text colors", () => {
182 expect(parseTailwindClasses("!text-white")).toEqual({
183 textVariable: null,
184 isWhiteText: true,
185 });
186 expect(parseTailwindClasses("!text-kumo-default")).toEqual({
187 textVariable: "text-color-kumo-default",
188 });
189 expect(parseTailwindClasses("!text-kumo-danger")).toEqual({
190 textVariable: "text-color-kumo-danger",
191 });
192 });
193
194 it("should handle unknown text colors gracefully", () => {
195 expect(parseTailwindClasses("text-unknown")).toEqual({});
196 });
197 });
198
199 describe("Borders and Rings", () => {
200 it("should detect border presence", () => {
201 expect(parseTailwindClasses("border")).toEqual({
202 hasBorder: true,
203 strokeWeight: 1,
204 });
205 });
206
207 it("should parse border colors", () => {
208 expect(parseTailwindClasses("border border-kumo-line")).toEqual({
209 hasBorder: true,
210 strokeVariable: "color-kumo-line",
211 strokeWeight: 1,
212 });
213 expect(parseTailwindClasses("border border-kumo-danger")).toEqual({
214 hasBorder: true,
215 strokeVariable: "color-kumo-danger",
216 strokeWeight: 1,
217 });
218 });
219
220 it("should parse dashed border style", () => {
221 expect(parseTailwindClasses("border-dashed")).toEqual({
222 hasBorder: true,
223 borderStyle: "dashed",
224 dashPattern: [4, 4],
225 });
226 });
227
228 it("should parse ring as border", () => {
229 expect(parseTailwindClasses("ring")).toEqual({ hasBorder: true });
230 expect(parseTailwindClasses("ring-kumo-line")).toEqual({
231 hasBorder: true,
232 strokeVariable: "color-kumo-line",
233 });
234 });
235
236 it("should parse default border width (1px)", () => {
237 expect(parseTailwindClasses("border")).toEqual({
238 hasBorder: true,
239 strokeWeight: 1,
240 });
241 });
242
243 it("should parse border width from border-N classes", () => {
244 expect(parseTailwindClasses("border-2")).toEqual({
245 hasBorder: true,
246 strokeWeight: 2,
247 });
248 expect(parseTailwindClasses("border-4")).toEqual({
249 hasBorder: true,
250 strokeWeight: 4,
251 });
252 expect(parseTailwindClasses("border-8")).toEqual({
253 hasBorder: true,
254 strokeWeight: 8,
255 });
256 });
257
258 it("should parse dashPattern from border-dashed", () => {
259 expect(parseTailwindClasses("border-dashed")).toEqual({
260 hasBorder: true,
261 borderStyle: "dashed",
262 dashPattern: [4, 4],
263 });
264 });
265
266 it("should parse border with width, style, and color together", () => {
267 expect(
268 parseTailwindClasses("border-2 border-dashed border-kumo-danger"),
269 ).toEqual({
270 hasBorder: true,
271 strokeWeight: 2,
272 borderStyle: "dashed",
273 dashPattern: [4, 4],
274 strokeVariable: "color-kumo-danger",
275 });
276 });
277 });
278
279 describe("Combined Classes", () => {
280 it("should parse multiple classes together", () => {
281 const result = parseTailwindClasses(
282 "h-9 px-3 py-1 gap-2 rounded-lg text-base bg-kumo-brand text-white border border-kumo-line",
283 );
284 expect(result).toEqual({
285 height: 36,
286 paddingX: 12,
287 paddingY: 4,
288 gap: 8,
289 borderRadius: 8,
290 fontSize: 14, // Kumo theme: --text-base: 14px
291 fillVariable: "color-kumo-brand",
292 textVariable: null,
293 isWhiteText: true,
294 hasBorder: true,
295 strokeVariable: "color-kumo-line",
296 strokeWeight: 1,
297 });
298 });
299
300 it("should parse button-like classes", () => {
301 const result = parseTailwindClasses(
302 "flex items-center h-10 px-4 rounded-md bg-kumo-control text-kumo-default ring ring-kumo-line",
303 );
304 expect(result).toEqual({
305 height: 40,
306 paddingX: 16,
307 borderRadius: 6,
308 fillVariable: "color-kumo-control",
309 textVariable: "text-color-kumo-default",
310 hasBorder: true,
311 strokeVariable: "color-kumo-line",
312 });
313 });
314
315 it("should parse badge-like classes", () => {
316 const result = parseTailwindClasses(
317 "h-5 px-1.5 gap-1 rounded text-xs bg-kumo-info/20 text-kumo-link border-dashed",
318 );
319 expect(result).toEqual({
320 height: 20,
321 paddingX: 6,
322 gap: 4,
323 borderRadius: 4,
324 fontSize: 12,
325 fillVariable: "color-kumo-info/20",
326 fillOpacity: 0.2,
327 textVariable: "text-color-kumo-link",
328 hasBorder: true,
329 borderStyle: "dashed",
330 dashPattern: [4, 4],
331 });
332 });
333 });
334
335 describe("State Variants", () => {
336 it("should parse hover state classes", () => {
337 const result = parseTailwindClasses("hover:bg-kumo-brand");
338 expect(result.states).toBeDefined();
339 expect(result.states?.hover).toEqual({
340 fillVariable: "color-kumo-brand",
341 });
342 });
343
344 it("should parse disabled state with opacity", () => {
345 const result = parseTailwindClasses("disabled:opacity-50");
346 // Note: opacity-50 is not currently parsed by the parser
347 // This is expected - opacity utility is not in the parser yet
348 // Since nothing is parsed, no states object is created
349 expect(result.states).toBeUndefined();
350 });
351
352 it("should parse focus state classes", () => {
353 const result = parseTailwindClasses("focus:ring-kumo-ring");
354 expect(result.states).toBeDefined();
355 // Note: ring-kumo-ring is parsed as strokeVariable and hasBorder
356 expect(result.states?.focus).toEqual({
357 hasBorder: true,
358 strokeVariable: "color-kumo-ring",
359 });
360 });
361
362 it("should parse important classes even with colon-like syntax", () => {
363 expect(parseTailwindClasses("!text-white")).toEqual({
364 textVariable: null,
365 isWhiteText: true,
366 });
367 });
368
369 it("should parse both base classes and state variants", () => {
370 const result = parseTailwindClasses(
371 "bg-kumo-brand hover:bg-kumo-control text-white focus:ring-kumo-ring",
372 );
373 expect(result).toEqual({
374 fillVariable: "color-kumo-brand",
375 textVariable: null,
376 isWhiteText: true,
377 states: {
378 hover: {
379 fillVariable: "color-kumo-control",
380 },
381 focus: {
382 hasBorder: true,
383 strokeVariable: "color-kumo-ring",
384 },
385 },
386 });
387 });
388
389 it("should parse multiple state variants", () => {
390 const result = parseTailwindClasses(
391 "hover:bg-kumo-brand focus:bg-kumo-control active:bg-kumo-danger disabled:bg-kumo-base pressed:bg-kumo-info",
392 );
393 expect(result.states).toEqual({
394 hover: { fillVariable: "color-kumo-brand" },
395 focus: { fillVariable: "color-kumo-control" },
396 active: { fillVariable: "color-kumo-danger" },
397 disabled: { fillVariable: "color-kumo-base" },
398 pressed: { fillVariable: "color-kumo-info" },
399 });
400 });
401
402 it("should parse state variants with opacity modifiers", () => {
403 const result = parseTailwindClasses("hover:bg-kumo-brand/70");
404 expect(result.states?.hover).toEqual({
405 fillVariable: "color-kumo-brand/70",
406 fillOpacity: 0.7,
407 });
408 });
409
410 it("should parse state variants with text colors", () => {
411 const result = parseTailwindClasses(
412 "hover:text-kumo-link focus:text-kumo-danger",
413 );
414 expect(result.states).toEqual({
415 hover: { textVariable: "text-color-kumo-link" },
416 focus: { textVariable: "text-color-kumo-danger" },
417 });
418 });
419
420 it("should skip unknown colon-prefixed classes", () => {
421 const result = parseTailwindClasses("sm:bg-kumo-brand lg:text-white");
422 expect(result).toEqual({});
423 });
424 });
425
426 describe("Edge Cases", () => {
427 it("should handle empty string", () => {
428 expect(parseTailwindClasses("")).toEqual({});
429 });
430
431 it("should handle only whitespace", () => {
432 expect(parseTailwindClasses(" ")).toEqual({});
433 });
434
435 it("should handle single class", () => {
436 expect(parseTailwindClasses("bg-kumo-brand")).toEqual({
437 fillVariable: "color-kumo-brand",
438 });
439 });
440
441 it("should handle extra whitespace between classes", () => {
442 const result = parseTailwindClasses("h-9 px-3 py-1 gap-2");
443 expect(result).toEqual({
444 height: 36,
445 paddingX: 12,
446 paddingY: 4,
447 gap: 8,
448 });
449 });
450
451 it("should handle unknown classes gracefully", () => {
452 const result = parseTailwindClasses(
453 "unknown-class flex items-center bg-kumo-brand",
454 );
455 expect(result).toEqual({
456 fillVariable: "color-kumo-brand",
457 });
458 });
459
460 it("should handle malformed opacity syntax", () => {
461 // Invalid opacity format should not match
462 expect(parseTailwindClasses("bg-primary/")).toEqual({});
463 expect(parseTailwindClasses("bg-primary/abc")).toEqual({});
464 });
465
466 it("should handle classes with numbers that don't match patterns", () => {
467 expect(parseTailwindClasses("z-10")).toEqual({});
468 expect(parseTailwindClasses("order-1")).toEqual({});
469 });
470
471 it("should not parse text color classes that are actually font sizes", () => {
472 const result = parseTailwindClasses("text-lg text-kumo-default");
473 expect(result).toEqual({
474 fontSize: 16, // Kumo theme: --text-lg: 16px
475 textVariable: "text-color-kumo-default",
476 });
477 });
478 });
479
480 describe("Opacity Handling", () => {
481 it("should parse opacity for various background color classes", () => {
482 expect(parseTailwindClasses("bg-kumo-danger/10")).toEqual({
483 fillVariable: "color-kumo-danger/10",
484 fillOpacity: 0.1,
485 });
486 expect(parseTailwindClasses("bg-kumo-warning/30")).toEqual({
487 fillVariable: "color-kumo-warning/30",
488 fillOpacity: 0.3,
489 });
490 expect(parseTailwindClasses("bg-kumo-base/90")).toEqual({
491 fillVariable: "color-kumo-base/90",
492 fillOpacity: 0.9,
493 });
494 });
495
496 it("should parse opacity with zero", () => {
497 expect(parseTailwindClasses("bg-kumo-brand/0")).toEqual({
498 fillVariable: "color-kumo-brand/0",
499 fillOpacity: 0,
500 });
501 });
502
503 it("should parse full opacity", () => {
504 expect(parseTailwindClasses("bg-kumo-brand/100")).toEqual({
505 fillVariable: "color-kumo-brand/100",
506 fillOpacity: 1,
507 });
508 });
509
510 it("should handle multi-digit opacity values", () => {
511 expect(parseTailwindClasses("bg-kumo-control/75")).toEqual({
512 fillVariable: "color-kumo-control/75",
513 fillOpacity: 0.75,
514 });
515 });
516
517 it("should parse common opacity values correctly", () => {
518 expect(parseTailwindClasses("bg-kumo-brand/20")).toEqual({
519 fillVariable: "color-kumo-brand/20",
520 fillOpacity: 0.2,
521 });
522 expect(parseTailwindClasses("bg-kumo-brand/50")).toEqual({
523 fillVariable: "color-kumo-brand/50",
524 fillOpacity: 0.5,
525 });
526 expect(parseTailwindClasses("bg-kumo-brand/70")).toEqual({
527 fillVariable: "color-kumo-brand/70",
528 fillOpacity: 0.7,
529 });
530 expect(parseTailwindClasses("bg-kumo-brand/80")).toEqual({
531 fillVariable: "color-kumo-brand/80",
532 fillOpacity: 0.8,
533 });
534 });
535
536 it("should parse text color opacity modifiers", () => {
537 expect(parseTailwindClasses("text-kumo-default/50")).toEqual({
538 textVariable: "text-color-kumo-default/50",
539 textOpacity: 0.5,
540 });
541 expect(parseTailwindClasses("text-kumo-danger/80")).toEqual({
542 textVariable: "text-color-kumo-danger/80",
543 textOpacity: 0.8,
544 });
545 expect(parseTailwindClasses("text-kumo-link/30")).toEqual({
546 textVariable: "text-color-kumo-link/30",
547 textOpacity: 0.3,
548 });
549 });
550
551 it("should parse important text color opacity modifiers", () => {
552 expect(parseTailwindClasses("!text-kumo-default/50")).toEqual({
553 textVariable: "text-color-kumo-default/50",
554 textOpacity: 0.5,
555 });
556 expect(parseTailwindClasses("!text-kumo-danger/70")).toEqual({
557 textVariable: "text-color-kumo-danger/70",
558 textOpacity: 0.7,
559 });
560 });
561
562 it("should parse border color opacity modifiers", () => {
563 expect(parseTailwindClasses("border border-kumo-danger/50")).toEqual({
564 hasBorder: true,
565 strokeWeight: 1,
566 strokeVariable: "color-kumo-danger/50",
567 strokeOpacity: 0.5,
568 });
569 expect(parseTailwindClasses("border border-kumo-brand/30")).toEqual({
570 hasBorder: true,
571 strokeWeight: 1,
572 strokeVariable: "color-kumo-brand/30",
573 strokeOpacity: 0.3,
574 });
575 });
576
577 it("should parse ring color opacity modifiers", () => {
578 expect(parseTailwindClasses("ring-kumo-ring/50")).toEqual({
579 hasBorder: true,
580 strokeVariable: "color-kumo-ring/50",
581 strokeOpacity: 0.5,
582 });
583 expect(parseTailwindClasses("ring-kumo-danger/70")).toEqual({
584 hasBorder: true,
585 strokeVariable: "color-kumo-danger/70",
586 strokeOpacity: 0.7,
587 });
588 });
589
590 it("should handle opacity in combined classes", () => {
591 const result = parseTailwindClasses(
592 "bg-kumo-brand/70 text-white/90 border border-kumo-danger/50",
593 );
594 expect(result).toEqual({
595 fillVariable: "color-kumo-brand/70",
596 fillOpacity: 0.7,
597 textVariable: null,
598 textOpacity: 0.9,
599 isWhiteText: true,
600 hasBorder: true,
601 strokeWeight: 1,
602 strokeVariable: "color-kumo-danger/50",
603 strokeOpacity: 0.5,
604 });
605 });
606 });
607
608 describe("Color Variable Resolution", () => {
609 it("should resolve background to fill variable", () => {
610 expect(parseTailwindClasses("bg-kumo-fill")).toEqual({
611 fillVariable: "color-kumo-fill",
612 });
613 expect(parseTailwindClasses("bg-kumo-tint")).toEqual({
614 fillVariable: "color-kumo-tint",
615 });
616 expect(parseTailwindClasses("bg-kumo-control")).toEqual({
617 fillVariable: "color-kumo-control",
618 });
619 });
620
621 it("should resolve text colors to text variables", () => {
622 expect(parseTailwindClasses("text-kumo-warning")).toEqual({
623 textVariable: "text-color-kumo-warning",
624 });
625 });
626
627 it("should resolve border colors to stroke variables", () => {
628 expect(parseTailwindClasses("border border-kumo-fill")).toEqual({
629 hasBorder: true,
630 strokeVariable: "color-kumo-fill",
631 strokeWeight: 1,
632 });
633 expect(parseTailwindClasses("border border-kumo-brand")).toEqual({
634 hasBorder: true,
635 strokeVariable: "color-kumo-brand",
636 strokeWeight: 1,
637 });
638 });
639
640 it("should handle null variable mapping for special cases", () => {
641 expect(parseTailwindClasses("bg-transparent")).toEqual({
642 fillVariable: null,
643 });
644 expect(parseTailwindClasses("text-white")).toEqual({
645 textVariable: null,
646 isWhiteText: true,
647 });
648 });
649 });
650
651 describe("Arbitrary Value Parsing", () => {
652 describe("Width", () => {
653 it("should parse width with px unit", () => {
654 expect(parseTailwindClasses("w-[350px]")).toEqual({
655 width: 350,
656 });
657 expect(parseTailwindClasses("w-[100px]")).toEqual({
658 width: 100,
659 });
660 });
661
662 it("should parse width with rem unit", () => {
663 expect(parseTailwindClasses("w-[32rem]")).toEqual({
664 width: 512, // 32 * 16
665 });
666 expect(parseTailwindClasses("w-[20rem]")).toEqual({
667 width: 320, // 20 * 16
668 });
669 });
670
671 it("should parse width with em unit", () => {
672 expect(parseTailwindClasses("w-[16em]")).toEqual({
673 width: 256, // 16 * 16
674 });
675 });
676
677 it("should parse width with decimal values", () => {
678 expect(parseTailwindClasses("w-[21.875rem]")).toEqual({
679 width: 350, // 21.875 * 16
680 });
681 expect(parseTailwindClasses("w-[12.5px]")).toEqual({
682 width: 12.5,
683 });
684 });
685
686 it("should parse width without unit (defaults to px)", () => {
687 expect(parseTailwindClasses("w-[250]")).toEqual({
688 width: 250,
689 });
690 });
691 });
692
693 describe("Height", () => {
694 it("should parse height with px unit", () => {
695 expect(parseTailwindClasses("h-[100px]")).toEqual({
696 height: 100,
697 });
698 });
699
700 it("should parse height with rem unit", () => {
701 expect(parseTailwindClasses("h-[2.5rem]")).toEqual({
702 height: 40, // 2.5 * 16
703 });
704 expect(parseTailwindClasses("h-[10rem]")).toEqual({
705 height: 160, // 10 * 16
706 });
707 });
708
709 it("should parse height with em unit", () => {
710 expect(parseTailwindClasses("h-[3em]")).toEqual({
711 height: 48, // 3 * 16
712 });
713 });
714
715 it("should parse height without unit (defaults to px)", () => {
716 expect(parseTailwindClasses("h-[50]")).toEqual({
717 height: 50,
718 });
719 });
720 });
721
722 describe("Min Width", () => {
723 it("should parse min-width with px unit", () => {
724 expect(parseTailwindClasses("min-w-[200px]")).toEqual({
725 minWidth: 200,
726 });
727 });
728
729 it("should parse min-width with rem unit", () => {
730 expect(parseTailwindClasses("min-w-[32rem]")).toEqual({
731 minWidth: 512, // 32 * 16
732 });
733 });
734
735 it("should parse min-width with em unit", () => {
736 expect(parseTailwindClasses("min-w-[10em]")).toEqual({
737 minWidth: 160, // 10 * 16
738 });
739 });
740 });
741
742 describe("Min Height", () => {
743 it("should parse min-height with px unit", () => {
744 expect(parseTailwindClasses("min-h-[100px]")).toEqual({
745 minHeight: 100,
746 });
747 });
748
749 it("should parse min-height with rem unit", () => {
750 expect(parseTailwindClasses("min-h-[5rem]")).toEqual({
751 minHeight: 80, // 5 * 16
752 });
753 });
754 });
755
756 describe("Max Width", () => {
757 it("should parse max-width with px unit", () => {
758 expect(parseTailwindClasses("max-w-[800px]")).toEqual({
759 maxWidth: 800,
760 });
761 });
762
763 it("should parse max-width with rem unit", () => {
764 expect(parseTailwindClasses("max-w-[64rem]")).toEqual({
765 maxWidth: 1024, // 64 * 16
766 });
767 });
768 });
769
770 describe("Max Height", () => {
771 it("should parse max-height with px unit", () => {
772 expect(parseTailwindClasses("max-h-[500px]")).toEqual({
773 maxHeight: 500,
774 });
775 });
776
777 it("should parse max-height with rem unit", () => {
778 expect(parseTailwindClasses("max-h-[30rem]")).toEqual({
779 maxHeight: 480, // 30 * 16
780 });
781 });
782 });
783
784 describe("Combined with other classes", () => {
785 it("should parse arbitrary values with standard classes", () => {
786 const result = parseTailwindClasses(
787 "w-[350px] h-[2.5rem] px-4 rounded-lg bg-kumo-brand",
788 );
789 expect(result).toEqual({
790 width: 350,
791 height: 40, // 2.5 * 16
792 paddingX: 16,
793 borderRadius: 8,
794 fillVariable: "color-kumo-brand",
795 });
796 });
797
798 it("should handle multiple arbitrary values", () => {
799 const result = parseTailwindClasses(
800 "min-w-[200px] max-w-[800px] min-h-[100px] max-h-[600px]",
801 );
802 expect(result).toEqual({
803 minWidth: 200,
804 maxWidth: 800,
805 minHeight: 100,
806 maxHeight: 600,
807 });
808 });
809
810 it("should prioritize arbitrary height over standard height", () => {
811 // If both h-9 and h-[100px] are present, the last one wins
812 const result1 = parseTailwindClasses("h-9 h-[100px]");
813 expect(result1).toEqual({
814 height: 100,
815 });
816
817 const result2 = parseTailwindClasses("h-[100px] h-9");
818 expect(result2).toEqual({
819 height: 36, // h-9 wins
820 });
821 });
822 });
823
824 describe("Edge cases", () => {
825 it("should handle invalid arbitrary value syntax gracefully", () => {
826 expect(parseTailwindClasses("w-[invalid]")).toEqual({});
827 expect(parseTailwindClasses("w-[]")).toEqual({});
828 expect(parseTailwindClasses("w-[px]")).toEqual({});
829 });
830
831 it("should handle unsupported properties gracefully", () => {
832 // These should not match the pattern
833 expect(parseTailwindClasses("p-[16px]")).toEqual({});
834 expect(parseTailwindClasses("m-[20px]")).toEqual({});
835 expect(parseTailwindClasses("gap-[8px]")).toEqual({});
836 });
837
838 it("should handle malformed brackets", () => {
839 expect(parseTailwindClasses("w-[350px")).toEqual({});
840 expect(parseTailwindClasses("w-350px]")).toEqual({});
841 expect(parseTailwindClasses("w-350px")).toEqual({});
842 });
843
844 it("should handle zero values", () => {
845 expect(parseTailwindClasses("w-[0px]")).toEqual({
846 width: 0,
847 });
848 expect(parseTailwindClasses("h-[0]")).toEqual({
849 height: 0,
850 });
851 });
852
853 it("should handle large values", () => {
854 expect(parseTailwindClasses("w-[9999px]")).toEqual({
855 width: 9999,
856 });
857 expect(parseTailwindClasses("max-w-[100rem]")).toEqual({
858 maxWidth: 1600, // 100 * 16
859 });
860 });
861 });
862 });
863});
864
865describe("parseBaseStyles", () => {
866 it("should be an alias for parseTailwindClasses", () => {
867 const classes = "h-9 px-3 bg-kumo-brand text-white";
868 expect(parseBaseStyles(classes)).toEqual(parseTailwindClasses(classes));
869 });
870
871 it("should parse complex base styles string", () => {
872 const baseStyles =
873 "flex items-center font-medium h-9 px-3 gap-2 rounded-lg text-base bg-kumo-control text-kumo-default ring ring-kumo-line";
874 const result = parseBaseStyles(baseStyles);
875 expect(result).toEqual({
876 height: 36,
877 paddingX: 12,
878 gap: 8,
879 borderRadius: 8,
880 fontSize: 14, // Kumo theme: --text-base: 14px
881 fontWeight: 500, // font-medium
882 fillVariable: "color-kumo-control",
883 textVariable: "text-color-kumo-default",
884 hasBorder: true,
885 strokeVariable: "color-kumo-line",
886 });
887 });
888
889 it("should handle empty base styles", () => {
890 expect(parseBaseStyles("")).toEqual({});
891 });
892});
893