@kbach/react 1.0.0-beta.2 → 1.0.0-beta.20

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.
@@ -20,122 +20,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/vite-plugin/index.ts
21
21
  var vite_plugin_exports = {};
22
22
  __export(vite_plugin_exports, {
23
- kbach: () => kbach
23
+ CSS_SYNC_DEBOUNCE_MS: () => CSS_SYNC_DEBOUNCE_MS,
24
+ DEFAULT_SCAN_DIRS: () => DEFAULT_SCAN_DIRS,
25
+ findMainCSSFile: () => findMainCSSFile,
26
+ kbach: () => kbach,
27
+ loadConfigFile: () => loadConfigFile,
28
+ resolveEffectiveTheme: () => resolveEffectiveTheme
24
29
  });
25
30
  module.exports = __toCommonJS(vite_plugin_exports);
26
- var import_fs2 = require("fs");
27
- var import_path3 = require("path");
31
+ var import_fs3 = require("fs");
32
+ var import_path4 = require("path");
33
+ var import_url = require("url");
28
34
  var import_module = require("module");
29
35
 
30
- // src/vite-plugin/wasmNode.ts
31
- var import_node = require("@kbach/core-engine/node");
32
- function generateCssForToken(token, themeJson) {
33
- return JSON.parse((0, import_node.generate_css)(token, themeJson));
34
- }
35
-
36
- // src/vite-plugin/scan.ts
37
- var import_fs = require("fs");
38
- var import_path = require("path");
39
- function splitClassTokens(str) {
40
- return str.split(/\s+/).filter(Boolean);
41
- }
42
- var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
43
- function pushTokens(str, into) {
44
- for (const tok of splitClassTokens(str)) {
45
- if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
46
- }
47
- }
48
- function pushTemplateLiteralBody(body, into) {
49
- const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
50
- let im;
51
- while ((im = interpolationRe.exec(body)) !== null) {
52
- pushClassLikeStrings(im[0], into);
53
- }
54
- pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, () => "$"), into);
55
- }
56
- function pushClassLikeStrings(text, into) {
57
- const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
58
- let qm;
59
- while ((qm = quotedStringRe.exec(text)) !== null) {
60
- const [, quote, content] = qm;
61
- if (quote === "`") pushTemplateLiteralBody(content, into);
62
- else pushTokens(content, into);
63
- }
64
- }
65
- var JSX_TAG_RE = /<([a-z][a-zA-Z0-9]*)[\s/>]/g;
66
- function scanUsedTags(code) {
67
- const tags = /* @__PURE__ */ new Set();
68
- let m;
69
- JSX_TAG_RE.lastIndex = 0;
70
- while ((m = JSX_TAG_RE.exec(code)) !== null) tags.add(m[1]);
71
- return tags;
72
- }
73
- function extractClassStrings(code) {
74
- const found = /* @__PURE__ */ new Set();
75
- const simpleRe = /(?:className|kb)=(["'])((?:(?!\1).)*)\1/g;
76
- let m;
77
- while ((m = simpleRe.exec(code)) !== null) pushTokens(m[2], found);
78
- const jsxExprRe = /(?:className|kb)=\{/g;
79
- while ((m = jsxExprRe.exec(code)) !== null) {
80
- let depth = 1;
81
- let i = m.index + m[0].length;
82
- let block = "";
83
- while (i < code.length && depth > 0) {
84
- const ch = code[i];
85
- if (ch === "{") depth++;
86
- else if (ch === "}") {
87
- if (--depth === 0) break;
88
- }
89
- block += ch;
90
- i++;
91
- }
92
- jsxExprRe.lastIndex = i + 1;
93
- pushClassLikeStrings(block, found);
94
- }
95
- const classComposerCallRe = /(?:clsx|cn|classnames|cx|kb)\(/g;
96
- while ((m = classComposerCallRe.exec(code)) !== null) {
97
- let depth = 1;
98
- let i = m.index + m[0].length;
99
- let block = "";
100
- while (i < code.length && depth > 0) {
101
- const ch = code[i];
102
- if (ch === "(") depth++;
103
- else if (ch === ")") {
104
- if (--depth === 0) break;
105
- }
106
- block += ch;
107
- i++;
108
- }
109
- classComposerCallRe.lastIndex = i + 1;
110
- pushClassLikeStrings(block, found);
111
- }
112
- const templateRe = /`([^`]{1,2000})`/g;
113
- while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
114
- return [...found];
115
- }
116
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
117
- var MAX_SCAN_DEPTH = 10;
118
- var DEFAULT_FILE_MATCH = /\.(tsx?|jsx?)$/;
119
- function scanDir(dir, onFile, depth = 0, fileMatch = DEFAULT_FILE_MATCH) {
120
- if (depth > MAX_SCAN_DEPTH) return;
121
- let entries;
122
- try {
123
- entries = (0, import_fs.readdirSync)(dir);
124
- } catch {
125
- return;
126
- }
127
- for (const entry of entries) {
128
- if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
129
- const full = (0, import_path.join)(dir, entry);
130
- try {
131
- const st = (0, import_fs.statSync)(full);
132
- if (st.isDirectory()) scanDir(full, onFile, depth + 1, fileMatch);
133
- else if (fileMatch.test(entry)) onFile(full, (0, import_fs.readFileSync)(full, "utf-8"));
134
- } catch {
135
- }
136
- }
137
- }
138
-
139
36
  // src/vite-plugin/cssVars.ts
140
37
  function hexToRgbTriplet(hex) {
141
38
  const h = hex.replace("#", "");
@@ -388,18 +285,16 @@ function formatKbachCSS(tokenRules, theme, usedTags) {
388
285
  const spacingMap = buildSpacingVarMap(theme, compact);
389
286
  const { text, declarations } = applyVarMap(compact, colorMap, spacingMap);
390
287
  const lines = ["/* Generated by Kbach \u2014 do not edit */", "", buildResetCSS(usedTags)];
391
- if (declarations.size > 0) {
392
- lines.push("", ":root {");
393
- for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
394
- lines.push("}");
395
- }
288
+ lines.push("", ":root {", ` --kb-dark-mode: ${theme.darkMode};`);
289
+ for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
290
+ lines.push("}");
396
291
  lines.push("", prettifyCSS(text));
397
292
  return lines.join("\n");
398
293
  }
399
- function writeKbachToFile(filePath, css, readFile2, writeFile2) {
294
+ function writeKbachToFile(filePath, css, readFile, writeFile) {
400
295
  let existing = "";
401
296
  try {
402
- existing = readFile2(filePath);
297
+ existing = readFile(filePath);
403
298
  } catch {
404
299
  }
405
300
  const block = `${KBACH_START}
@@ -411,734 +306,10 @@ ${KBACH_END}`;
411
306
 
412
307
  ${block}` : block;
413
308
  if (next === existing) return false;
414
- writeFile2(filePath, next);
309
+ writeFile(filePath, next);
415
310
  return true;
416
311
  }
417
312
 
418
- // src/vite-plugin/unknownClassWarnings.ts
419
- var import_path2 = require("path");
420
- var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
421
- var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
422
- var useColor = !!process.stdout?.isTTY && !process.env.NO_COLOR;
423
- var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
424
- var green = (s) => paint("32", s);
425
- var blue = (s) => paint("34", s);
426
- var white = (s) => paint("97", s);
427
- var gray = (s) => paint("90", s);
428
- var bold = (s) => paint("1", s);
429
- var ARROW = () => green("\u2192");
430
- var TAG = () => bold(green("[kbach]"));
431
- var highlight = (s) => bold(white(s));
432
- function findLineCol(code, needle) {
433
- const index = code.indexOf(needle);
434
- if (index === -1) return null;
435
- let line = 1;
436
- let lastNewline = -1;
437
- for (let i = 0; i < index; i++) {
438
- if (code.charCodeAt(i) === 10) {
439
- line++;
440
- lastNewline = i;
441
- }
442
- }
443
- return { line, column: index - lastNewline };
444
- }
445
- function scanCssFileInto(code, into) {
446
- const re = new RegExp(CSS_CLASS_SELECTOR_RE.source, "g");
447
- let m;
448
- while ((m = re.exec(code)) !== null) into.add(m[1]);
449
- }
450
- function scanProjectCssSelectors(root, includeDirs) {
451
- const projectCssClasses = /* @__PURE__ */ new Set();
452
- for (const dir of includeDirs) {
453
- scanDir((0, import_path2.join)(root, dir), (_filePath, code) => scanCssFileInto(code, projectCssClasses), 0, CSS_FILE_RE);
454
- }
455
- return projectCssClasses;
456
- }
457
- function stripModifierPrefix(token) {
458
- return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
459
- }
460
- var MARKER_CLASSES = /* @__PURE__ */ new Set(["group", "peer"]);
461
- function warnIfUnknownClass(token, filePath, code, isResolvable, projectCssClasses, warnedTokens) {
462
- if (isResolvable || warnedTokens.has(token)) return;
463
- const base = stripModifierPrefix(token);
464
- if (!base || projectCssClasses.has(base) || MARKER_CLASSES.has(base)) return;
465
- warnedTokens.add(token);
466
- const pos = findLineCol(code, token);
467
- const location = pos ? `${blue(filePath)}${green(`:${pos.line}:${pos.column}`)}` : blue(filePath);
468
- console.warn(
469
- `${ARROW()} ${TAG()} ${gray("Unknown class")} ${highlight(token)} ${gray("\u2014 no Kbach utility or project CSS rule matches it. Typo?")}
470
- ${bold("at")} ${location}`
471
- );
472
- }
473
-
474
- // src/vite-plugin/classNameHints.ts
475
- var STRUCTURAL_TOKENS = [
476
- // layout.rs
477
- "flex",
478
- "flex-row",
479
- "flex-col",
480
- "flex-row-reverse",
481
- "flex-col-reverse",
482
- "flex-wrap",
483
- "flex-wrap-reverse",
484
- "flex-nowrap",
485
- "flex-1",
486
- "flex-auto",
487
- "flex-initial",
488
- "flex-none",
489
- "flex-grow",
490
- "flex-grow-0",
491
- "flex-shrink",
492
- "flex-shrink-0",
493
- "grow",
494
- "grow-0",
495
- "shrink",
496
- "shrink-0",
497
- "grid",
498
- "block",
499
- "inline-block",
500
- "inline",
501
- "inline-flex",
502
- "inline-grid",
503
- "contents",
504
- "flow-root",
505
- "hidden",
506
- "items-center",
507
- "items-start",
508
- "items-end",
509
- "items-baseline",
510
- "items-stretch",
511
- "justify-center",
512
- "justify-start",
513
- "justify-end",
514
- "justify-between",
515
- "justify-around",
516
- "justify-evenly",
517
- "content-start",
518
- "content-end",
519
- "content-center",
520
- "content-between",
521
- "content-around",
522
- "content-evenly",
523
- "content-stretch",
524
- "self-auto",
525
- "self-center",
526
- "self-start",
527
- "self-end",
528
- "self-stretch",
529
- "self-baseline",
530
- "overflow-hidden",
531
- "overflow-auto",
532
- "overflow-scroll",
533
- "overflow-visible",
534
- "static",
535
- "relative",
536
- "absolute",
537
- "fixed",
538
- "sticky",
539
- "aspect-square",
540
- "aspect-video",
541
- "w-full",
542
- "h-full",
543
- "min-w-full",
544
- "max-w-full",
545
- "min-h-full",
546
- "max-h-full",
547
- "w-screen",
548
- "h-screen",
549
- "min-w-screen",
550
- "max-w-screen",
551
- "min-h-screen",
552
- "max-h-screen",
553
- "w-auto",
554
- "h-auto",
555
- "m-auto",
556
- "mx-auto",
557
- "my-auto",
558
- // grid.rs — web-only (see the module's own doc comment for why)
559
- "grid-cols-none",
560
- "grid-rows-none",
561
- "grid-flow-row",
562
- "grid-flow-col",
563
- "grid-flow-dense",
564
- "grid-flow-row-dense",
565
- "grid-flow-col-dense",
566
- "auto-cols-auto",
567
- "auto-cols-min",
568
- "auto-cols-max",
569
- "auto-cols-fr",
570
- "auto-rows-auto",
571
- "auto-rows-min",
572
- "auto-rows-max",
573
- "auto-rows-fr",
574
- "col-span-full",
575
- "row-span-full",
576
- "col-start-auto",
577
- "col-end-auto",
578
- "row-start-auto",
579
- "row-end-auto",
580
- "col",
581
- "col-auto",
582
- "row",
583
- "row-auto",
584
- "place-items-start",
585
- "place-items-end",
586
- "place-items-center",
587
- "place-items-stretch",
588
- "place-items-baseline",
589
- "place-content-start",
590
- "place-content-end",
591
- "place-content-center",
592
- "place-content-stretch",
593
- "place-content-between",
594
- "place-content-around",
595
- "place-content-evenly",
596
- "place-content-baseline",
597
- "place-self-auto",
598
- "place-self-start",
599
- "place-self-end",
600
- "place-self-center",
601
- "place-self-stretch",
602
- "justify-items-start",
603
- "justify-items-end",
604
- "justify-items-center",
605
- "justify-items-stretch",
606
- "justify-self-auto",
607
- "justify-self-start",
608
- "justify-self-end",
609
- "justify-self-center",
610
- "justify-self-stretch",
611
- // transform.rs — web-only (see the module's own doc comment for why)
612
- "transform-none",
613
- "origin-center",
614
- "origin-top",
615
- "origin-top-right",
616
- "origin-right",
617
- "origin-bottom-right",
618
- "origin-bottom",
619
- "origin-bottom-left",
620
- "origin-left",
621
- "origin-top-left",
622
- // filters.rs — web-only (see the module's own doc comment for why)
623
- "blur",
624
- "blur-sm",
625
- "blur-md",
626
- "blur-lg",
627
- "blur-xl",
628
- "blur-2xl",
629
- "blur-3xl",
630
- "blur-none",
631
- "grayscale",
632
- "grayscale-0",
633
- "invert",
634
- "invert-0",
635
- "sepia",
636
- "sepia-0",
637
- "drop-shadow",
638
- "drop-shadow-sm",
639
- "drop-shadow-md",
640
- "drop-shadow-lg",
641
- "drop-shadow-xl",
642
- "drop-shadow-2xl",
643
- "drop-shadow-none",
644
- "filter-none",
645
- "backdrop-filter-none",
646
- "backdrop-blur",
647
- "backdrop-blur-sm",
648
- "backdrop-blur-md",
649
- "backdrop-blur-lg",
650
- "backdrop-blur-xl",
651
- "backdrop-blur-2xl",
652
- "backdrop-blur-3xl",
653
- "backdrop-blur-none",
654
- "backdrop-grayscale",
655
- "backdrop-grayscale-0",
656
- "backdrop-invert",
657
- "backdrop-invert-0",
658
- "backdrop-sepia",
659
- "backdrop-sepia-0",
660
- // border.rs
661
- "border",
662
- "border-t",
663
- "border-r",
664
- "border-b",
665
- "border-l",
666
- "rounded",
667
- "rounded-none",
668
- "rounded-sm",
669
- "rounded-md",
670
- "rounded-lg",
671
- "rounded-xl",
672
- "rounded-2xl",
673
- "rounded-3xl",
674
- "rounded-full",
675
- "ring",
676
- "ring-0",
677
- "ring-1",
678
- "ring-2",
679
- "ring-4",
680
- "ring-8",
681
- "ring-inset",
682
- "ring-offset-0",
683
- "ring-offset-1",
684
- "ring-offset-2",
685
- "ring-offset-4",
686
- "ring-offset-8",
687
- "outline",
688
- "outline-none",
689
- "border-solid",
690
- "border-dashed",
691
- "border-dotted",
692
- "border-none",
693
- // typography.rs
694
- "font-thin",
695
- "font-normal",
696
- "font-medium",
697
- "font-semibold",
698
- "font-bold",
699
- "font-extrabold",
700
- "uppercase",
701
- "lowercase",
702
- "capitalize",
703
- "underline",
704
- "line-through",
705
- "no-underline",
706
- "truncate",
707
- "text-xs",
708
- "text-sm",
709
- "text-base",
710
- "text-lg",
711
- "text-xl",
712
- "text-2xl",
713
- "text-3xl",
714
- "text-4xl",
715
- "text-left",
716
- "text-center",
717
- "text-right",
718
- "text-justify",
719
- // typography.rs — Phase 20 (typography completeness)
720
- "italic",
721
- "not-italic",
722
- "leading-none",
723
- "leading-tight",
724
- "leading-snug",
725
- "leading-normal",
726
- "leading-relaxed",
727
- "leading-loose",
728
- "tracking-tighter",
729
- "tracking-tight",
730
- "tracking-normal",
731
- "tracking-wide",
732
- "tracking-wider",
733
- "tracking-widest",
734
- "decoration-auto",
735
- "decoration-from-font",
736
- "decoration-solid",
737
- "decoration-dashed",
738
- "decoration-dotted",
739
- "decoration-double",
740
- "decoration-wavy",
741
- "underline-offset-auto",
742
- "underline-offset-0",
743
- "underline-offset-1",
744
- "underline-offset-2",
745
- "underline-offset-4",
746
- "underline-offset-8",
747
- "text-wrap",
748
- "text-nowrap",
749
- "text-balance",
750
- "text-pretty",
751
- "whitespace-normal",
752
- "whitespace-nowrap",
753
- "whitespace-pre",
754
- "whitespace-pre-wrap",
755
- "whitespace-pre-line",
756
- "break-normal",
757
- "break-words",
758
- "break-all",
759
- "align-baseline",
760
- "align-top",
761
- "align-middle",
762
- "align-bottom",
763
- "align-text-top",
764
- "align-text-bottom",
765
- "align-sub",
766
- "align-super",
767
- "list-none",
768
- "list-disc",
769
- "list-decimal",
770
- "list-inside",
771
- "list-outside",
772
- "hyphens-none",
773
- "hyphens-manual",
774
- "hyphens-auto",
775
- // effects.rs
776
- "shadow",
777
- "shadow-sm",
778
- "shadow-md",
779
- "shadow-lg",
780
- "shadow-xl",
781
- "shadow-2xl",
782
- "shadow-inner",
783
- "shadow-none",
784
- "transition",
785
- "transition-none",
786
- "ease-linear",
787
- "ease-in",
788
- "ease-out",
789
- "ease-in-out",
790
- "cursor-pointer",
791
- "cursor-default",
792
- "cursor-not-allowed",
793
- "cursor-wait",
794
- "select-none",
795
- "select-text",
796
- "select-all",
797
- "pointer-events-none",
798
- "pointer-events-auto",
799
- // effects.rs — Phase 22 (effects completeness)
800
- "text-shadow",
801
- "text-shadow-sm",
802
- "text-shadow-lg",
803
- "text-shadow-none",
804
- "mix-blend-normal",
805
- "mix-blend-multiply",
806
- "mix-blend-screen",
807
- "mix-blend-overlay",
808
- "mix-blend-darken",
809
- "mix-blend-lighten",
810
- "mix-blend-color-dodge",
811
- "mix-blend-color-burn",
812
- "mix-blend-hard-light",
813
- "mix-blend-soft-light",
814
- "mix-blend-difference",
815
- "mix-blend-exclusion",
816
- "mix-blend-hue",
817
- "mix-blend-saturation",
818
- "mix-blend-color",
819
- "mix-blend-luminosity",
820
- "mix-blend-plus-lighter",
821
- "bg-blend-normal",
822
- "bg-blend-multiply",
823
- "bg-blend-screen",
824
- "bg-blend-overlay",
825
- "bg-blend-darken",
826
- "bg-blend-lighten",
827
- "bg-blend-color-dodge",
828
- "bg-blend-color-burn",
829
- "bg-blend-hard-light",
830
- "bg-blend-soft-light",
831
- "bg-blend-difference",
832
- "bg-blend-exclusion",
833
- "bg-blend-hue",
834
- "bg-blend-saturation",
835
- "bg-blend-color",
836
- "bg-blend-luminosity",
837
- "bg-blend-plus-lighter",
838
- "animate-spin",
839
- "animate-ping",
840
- "animate-pulse",
841
- "animate-bounce",
842
- "animate-none",
843
- // interactivity.rs — Phase 23 (interactivity & sizing rest)
844
- "resize",
845
- "resize-none",
846
- "resize-y",
847
- "resize-x",
848
- "touch-auto",
849
- "touch-none",
850
- "touch-pan-x",
851
- "touch-pan-left",
852
- "touch-pan-right",
853
- "touch-pan-y",
854
- "touch-pan-up",
855
- "touch-pan-down",
856
- "touch-pinch-zoom",
857
- "touch-manipulation",
858
- "will-change-auto",
859
- "will-change-scroll",
860
- "will-change-contents",
861
- "will-change-transform",
862
- "sr-only",
863
- "not-sr-only",
864
- "border-collapse",
865
- "border-separate",
866
- "table-auto",
867
- "table-fixed",
868
- "caption-top",
869
- "caption-bottom",
870
- "stroke-width-0",
871
- "stroke-width-1",
872
- "stroke-width-2",
873
- // spacing.rs — Phase 23 (size-* shorthand + fraction-based width/height)
874
- "size-full",
875
- "size-auto",
876
- "size-px",
877
- "w-1/2",
878
- "w-1/3",
879
- "w-2/3",
880
- "w-1/4",
881
- "w-2/4",
882
- "w-3/4",
883
- "w-1/5",
884
- "w-2/5",
885
- "w-3/5",
886
- "w-4/5",
887
- "w-1/6",
888
- "w-5/6",
889
- "w-1/12",
890
- "w-5/12",
891
- "w-7/12",
892
- "w-11/12",
893
- "h-1/2",
894
- "h-1/3",
895
- "h-2/3",
896
- "h-1/4",
897
- "h-2/4",
898
- "h-3/4",
899
- "h-1/5",
900
- "h-2/5",
901
- "h-3/5",
902
- "h-4/5",
903
- "h-1/6",
904
- "h-5/6",
905
- // scroll.rs — Phase 23 (scroll-behavior/-margin/-padding, scroll-snap)
906
- "scroll-auto",
907
- "scroll-smooth",
908
- "snap-start",
909
- "snap-end",
910
- "snap-center",
911
- "snap-align-none",
912
- "snap-normal",
913
- "snap-always",
914
- "snap-none",
915
- "snap-x",
916
- "snap-y",
917
- "snap-both",
918
- "snap-mandatory",
919
- "snap-proximity",
920
- // divide.rs
921
- "divide-x",
922
- "divide-y",
923
- // background.rs — web-only (see the module's own doc comment for why)
924
- "bg-top",
925
- "bg-bottom",
926
- "bg-left",
927
- "bg-right",
928
- "bg-center",
929
- "bg-left-top",
930
- "bg-left-bottom",
931
- "bg-right-top",
932
- "bg-right-bottom",
933
- "bg-auto",
934
- "bg-cover",
935
- "bg-contain",
936
- "bg-repeat",
937
- "bg-no-repeat",
938
- "bg-repeat-x",
939
- "bg-repeat-y",
940
- "bg-repeat-round",
941
- "bg-repeat-space",
942
- "bg-fixed",
943
- "bg-local",
944
- "bg-scroll",
945
- "bg-clip-border",
946
- "bg-clip-padding",
947
- "bg-clip-content",
948
- "bg-clip-text",
949
- "bg-origin-border",
950
- "bg-origin-padding",
951
- "bg-origin-content",
952
- "bg-none",
953
- "bg-linear-to-t",
954
- "bg-linear-to-tr",
955
- "bg-linear-to-r",
956
- "bg-linear-to-br",
957
- "bg-linear-to-b",
958
- "bg-linear-to-bl",
959
- "bg-linear-to-l",
960
- "bg-linear-to-tl",
961
- "bg-radial",
962
- "bg-conic",
963
- // Ancestor/sibling-state marker classes — carry no styles of their own,
964
- // just a selector target for group-hover:/peer-hover:/etc. below (see
965
- // unknownClassWarnings.ts's MARKER_CLASSES for why they need special
966
- // handling there too).
967
- "group",
968
- "peer",
969
- // registry.rs modifiers — bare tokens only, not composed with a
970
- // following utility (see this module's doc header / the Phase 15 plan).
971
- "hover:",
972
- "focus:",
973
- "focus-visible:",
974
- "focus-within:",
975
- "active:",
976
- "visited:",
977
- "disabled:",
978
- "checked:",
979
- "group-hover:",
980
- "group-focus:",
981
- "peer-hover:",
982
- "peer-focus:",
983
- "peer-checked:",
984
- "dark:",
985
- "motion-safe:",
986
- "motion-reduce:",
987
- "print:",
988
- "sm:",
989
- "md:",
990
- "lg:",
991
- "xl:",
992
- "2xl:",
993
- // registry.rs modifiers — Phase 24 (expanded variant system), the
994
- // FIXED/static additions only. Deliberately excluded: every genuinely
995
- // parameterized modifier (has-[...]/group-has-[...]/peer-has-[...]/
996
- // data-[...]/aria-[...]/not-[...]/min-[...]/max-[...], and the
997
- // generalized group-<pseudo>/peer-<pseudo> beyond the five hardcoded
998
- // combos above) — these have no finite enumerable set (their whole point
999
- // is arbitrary user-supplied content), so there's nothing meaningful to
1000
- // suggest beyond the bare prefix, same reasoning as every other
1001
- // arbitrary-value exclusion in this file. `not-hover:`/`not-disabled:`/
1002
- // etc. (wrapping a KNOWN static modifier) are excluded for the same
1003
- // reason — enumerable in principle, but a full cross-product with every
1004
- // modifier above would be more noise than signal for an editor hint list.
1005
- "first:",
1006
- "last:",
1007
- "only:",
1008
- "odd:",
1009
- "even:",
1010
- "open:",
1011
- "inert:",
1012
- "aria-checked:",
1013
- "aria-disabled:",
1014
- "aria-expanded:",
1015
- "aria-hidden:",
1016
- "aria-pressed:",
1017
- "aria-readonly:",
1018
- "aria-required:",
1019
- "aria-selected:",
1020
- "aria-busy:",
1021
- // Phase 25 (container queries & arbitrary properties). "@container" is a
1022
- // real UTILITY (container-type: inline-size), not a modifier — included
1023
- // as a bare token like "flex"/"hidden" above, not with the trailing ":"
1024
- // the modifier tokens below get. "@sm:"/"@md:"/"@lg:"/"@xl:"/"@2xl:" are
1025
- // container-responsive modifiers (registry.rs's `is_container_responsive`),
1026
- // and "starting:" wraps in @starting-style. Deliberately excluded, same
1027
- // "no finite enumerable set" reasoning as Phase 24's exclusions:
1028
- // "@min-[...]:"/"@max-[...]:" (arbitrary container breakpoints) and every
1029
- // `[property:value]` arbitrary property — both are open-ended by design.
1030
- "@container",
1031
- "@sm:",
1032
- "@md:",
1033
- "@lg:",
1034
- "@xl:",
1035
- "@2xl:",
1036
- "starting:"
1037
- ];
1038
- var COLOR_PREFIXES = [
1039
- "bg-",
1040
- "text-",
1041
- "border-",
1042
- "divide-",
1043
- "decoration-",
1044
- "from-",
1045
- "via-",
1046
- "to-",
1047
- "text-shadow-",
1048
- "ring-",
1049
- "ring-offset-",
1050
- "caret-",
1051
- "accent-",
1052
- "fill-",
1053
- "stroke-"
1054
- ];
1055
- var SPACING_PREFIXES = [
1056
- "p-",
1057
- "px-",
1058
- "py-",
1059
- "pt-",
1060
- "pr-",
1061
- "pb-",
1062
- "pl-",
1063
- "m-",
1064
- "mx-",
1065
- "my-",
1066
- "mt-",
1067
- "mr-",
1068
- "mb-",
1069
- "ml-",
1070
- "gap-",
1071
- "gap-x-",
1072
- "gap-y-",
1073
- "w-",
1074
- "h-",
1075
- "min-w-",
1076
- "max-w-",
1077
- "min-h-",
1078
- "max-h-",
1079
- "size-",
1080
- "top-",
1081
- "right-",
1082
- "bottom-",
1083
- "left-",
1084
- "inset-",
1085
- "space-x-",
1086
- "space-y-",
1087
- "indent-",
1088
- "scroll-m-",
1089
- "scroll-mx-",
1090
- "scroll-my-",
1091
- "scroll-mt-",
1092
- "scroll-mr-",
1093
- "scroll-mb-",
1094
- "scroll-ml-",
1095
- "scroll-p-",
1096
- "scroll-px-",
1097
- "scroll-py-",
1098
- "scroll-pt-",
1099
- "scroll-pr-",
1100
- "scroll-pb-",
1101
- "scroll-pl-"
1102
- ];
1103
- var SIZE_PREFIXES = ["w-", "h-", "min-w-", "max-w-", "min-h-", "max-h-", "size-"];
1104
- var NAMED_SIZES = ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
1105
- function buildClassTokens(theme) {
1106
- const tokens = new Set(STRUCTURAL_TOKENS);
1107
- for (const colorKey of Object.keys(theme.colors)) {
1108
- for (const prefix of COLOR_PREFIXES) tokens.add(`${prefix}${colorKey}`);
1109
- }
1110
- for (const spacingKey of Object.keys(theme.spacing)) {
1111
- for (const prefix of SPACING_PREFIXES) tokens.add(`${prefix}${spacingKey}`);
1112
- }
1113
- for (const size of NAMED_SIZES) {
1114
- for (const prefix of SIZE_PREFIXES) tokens.add(`${prefix}${size}`);
1115
- }
1116
- return Array.from(tokens).sort();
1117
- }
1118
- function buildClassNameHintsDts(tokens) {
1119
- const literals = tokens.map((t) => ` | '${t}'`).join("\n");
1120
- return [
1121
- "// Generated by Kbach \u2014 do not edit",
1122
- "// Editor autocomplete for className \u2014 see packages/react/src/vite-plugin/classNameHints.ts",
1123
- "",
1124
- "type KbachClassToken =",
1125
- literals,
1126
- ";",
1127
- "",
1128
- "export type KbachClassName = KbachClassToken | `${string} ${KbachClassToken}` | (string & {});",
1129
- "",
1130
- "declare module 'react' {",
1131
- " interface HTMLAttributes<T> {",
1132
- " className?: KbachClassName | undefined;",
1133
- " }",
1134
- " interface SVGAttributes<T> {",
1135
- " className?: KbachClassName | undefined;",
1136
- " }",
1137
- "}",
1138
- ""
1139
- ].join("\n");
1140
- }
1141
-
1142
313
  // src/generatedPalette.ts
1143
314
  var PALETTE = {
1144
315
  "amber-1": "#fffbeb",
@@ -1489,12 +660,39 @@ var defaultTheme = {
1489
660
  serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
1490
661
  mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
1491
662
  },
1492
- darkMode: "attribute",
663
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
664
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
665
+ // using only static `className` strings gets working `dark:` classes
666
+ // out of the box, with no runtime import required to write `data-theme`/
667
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
668
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
669
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
670
+ // for JS consumers, but flipping it has no visible effect on `dark:`
671
+ // classes, since the browser's media query only ever reflects the real
672
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
673
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
674
+ // as an app wants a real in-app toggle.
675
+ darkMode: "media",
1493
676
  // Empty by default, matching real Tailwind's own defaults (no forced
1494
677
  // centering/padding) — see `ContainerConfig`'s own doc comment.
1495
678
  container: {}
1496
679
  };
680
+ var activeTheme = defaultTheme;
1497
681
  var activeThemeJson = JSON.stringify(defaultTheme);
682
+ function setTheme(theme) {
683
+ activeTheme = theme;
684
+ activeThemeJson = JSON.stringify(theme);
685
+ }
686
+ function getTheme() {
687
+ return activeTheme;
688
+ }
689
+ var DARK_MODE_STRATEGIES = /* @__PURE__ */ new Set(["attribute", "class", "media"]);
690
+ function detectDarkModeStrategyFromDom() {
691
+ if (typeof document === "undefined") return;
692
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--kb-dark-mode").trim();
693
+ if (!DARK_MODE_STRATEGIES.has(raw) || raw === activeTheme.darkMode) return;
694
+ setTheme({ ...activeTheme, darkMode: raw });
695
+ }
1498
696
 
1499
697
  // src/colorVariables.ts
1500
698
  function parseHexRgb(hex) {
@@ -1506,6 +704,61 @@ function parseHexRgb(hex) {
1506
704
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
1507
705
  }
1508
706
 
707
+ // src/darkModeStore.ts
708
+ var STORAGE_KEY = "kbach-theme";
709
+ var listeners = /* @__PURE__ */ new Set();
710
+ function systemPrefersDark() {
711
+ if (typeof window === "undefined" || !window.matchMedia) return false;
712
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
713
+ }
714
+ function storage() {
715
+ return typeof window === "undefined" ? null : window.localStorage;
716
+ }
717
+ function readPersistedMode() {
718
+ const ls = storage();
719
+ if (!ls) return null;
720
+ try {
721
+ const raw = ls.getItem(STORAGE_KEY);
722
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
723
+ } catch {
724
+ return null;
725
+ }
726
+ }
727
+ function resolveIsDark(m) {
728
+ return m === "system" ? systemPrefersDark() : m === "dark";
729
+ }
730
+ function applyToDom(dark) {
731
+ if (typeof document === "undefined") return;
732
+ const root = document.documentElement;
733
+ const strategy = getTheme().darkMode;
734
+ root.classList.toggle("dark", strategy === "class" && dark);
735
+ root.classList.toggle("light", strategy === "class" && !dark);
736
+ if (strategy === "attribute") {
737
+ root.setAttribute("data-theme", dark ? "dark" : "light");
738
+ } else {
739
+ root.removeAttribute("data-theme");
740
+ }
741
+ }
742
+ detectDarkModeStrategyFromDom();
743
+ var persistedMode = readPersistedMode();
744
+ var mode = persistedMode ?? "system";
745
+ var isDark = resolveIsDark(mode);
746
+ applyToDom(isDark);
747
+ if (typeof window !== "undefined" && window.matchMedia) {
748
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
749
+ const onSystemChange = (event) => {
750
+ if (mode !== "system") return;
751
+ isDark = event.matches;
752
+ applyToDom(isDark);
753
+ notify();
754
+ };
755
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
756
+ else mq.addListener(onSystemChange);
757
+ }
758
+ function notify() {
759
+ for (const listener of listeners) listener();
760
+ }
761
+
1509
762
  // src/config.ts
1510
763
  function applyOpacityToHex(hex, opacityText) {
1511
764
  const rgb = parseHexRgb(hex);
@@ -1525,11 +778,29 @@ function resolveColorEntry(value, lookup) {
1525
778
  return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
1526
779
  }
1527
780
  function resolveColors(extend, existing) {
781
+ const { dark: darkExtend, ...lightExtend } = extend;
1528
782
  const resolved = {};
1529
783
  const lookup = (name) => resolved[name] ?? existing[name];
1530
- for (const [name, value] of Object.entries(extend)) {
784
+ for (const [name, value] of Object.entries(lightExtend)) {
785
+ if (typeof value === "object" && value !== null && !("light" in value) && !("dark" in value)) continue;
1531
786
  resolved[name] = resolveColorEntry(value, lookup);
1532
787
  }
788
+ if (darkExtend) {
789
+ const resolvedDark = {};
790
+ const lookupDark = (name) => {
791
+ const found = resolvedDark[name] ?? existing[name];
792
+ return typeof found === "string" ? found : void 0;
793
+ };
794
+ for (const [name, value] of Object.entries(darkExtend)) {
795
+ const entry = resolveColorEntry(value, lookupDark);
796
+ resolvedDark[name] = typeof entry === "string" ? entry : entry.dark;
797
+ }
798
+ for (const [name, darkValue] of Object.entries(resolvedDark)) {
799
+ const lightEntry = resolved[name];
800
+ if (lightEntry === void 0 || typeof lightEntry !== "string") continue;
801
+ resolved[name] = { light: lightEntry, dark: darkValue };
802
+ }
803
+ }
1533
804
  return resolved;
1534
805
  }
1535
806
  function resolveKbachConfig(config) {
@@ -1555,35 +826,272 @@ function resolveKbachConfig(config) {
1555
826
  return theme;
1556
827
  }
1557
828
 
1558
- // src/vite-plugin/index.ts
1559
- var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1560
- function normPath(p) {
1561
- const fwd = p.replace(/\\/g, "/");
1562
- return process.platform === "win32" ? fwd.toLowerCase() : fwd;
829
+ // src/staticCss/engine.ts
830
+ var import_fs2 = require("fs");
831
+ var import_path3 = require("path");
832
+
833
+ // src/vite-plugin/wasmNode.ts
834
+ var import_node = require("@kbach/core-engine/node");
835
+ function generateCssForToken(token, themeJson) {
836
+ return JSON.parse((0, import_node.generate_css)(token, themeJson));
1563
837
  }
1564
- var SAFELIST_KEY = "\0kbach-safelist";
1565
- function readFile(path) {
1566
- return (0, import_fs2.readFileSync)(path, "utf-8");
838
+
839
+ // src/vite-plugin/scan.ts
840
+ var import_fs = require("fs");
841
+ var import_path = require("path");
842
+ function stripComments(code) {
843
+ let out = "";
844
+ let i = 0;
845
+ const n = code.length;
846
+ while (i < n) {
847
+ const two = code[i] + (code[i + 1] ?? "");
848
+ if (two === "//") {
849
+ i += 2;
850
+ while (i < n && code[i] !== "\n") i++;
851
+ continue;
852
+ }
853
+ if (two === "/*") {
854
+ i += 2;
855
+ while (i < n && code[i] + (code[i + 1] ?? "") !== "*/") i++;
856
+ i += 2;
857
+ continue;
858
+ }
859
+ const ch = code[i];
860
+ if (ch === '"' || ch === "'") {
861
+ out += ch;
862
+ i++;
863
+ while (i < n && code[i] !== ch) {
864
+ if (code[i] === "\\") {
865
+ out += code[i] + (code[i + 1] ?? "");
866
+ i += 2;
867
+ continue;
868
+ }
869
+ out += code[i];
870
+ i++;
871
+ }
872
+ out += code[i] ?? "";
873
+ i++;
874
+ continue;
875
+ }
876
+ if (ch === "`") {
877
+ out += ch;
878
+ i++;
879
+ let interpolationDepth = 0;
880
+ while (i < n) {
881
+ if (interpolationDepth === 0 && code[i] === "`") {
882
+ out += code[i];
883
+ i++;
884
+ break;
885
+ }
886
+ if (interpolationDepth === 0 && code[i] === "\\") {
887
+ out += code[i] + (code[i + 1] ?? "");
888
+ i += 2;
889
+ continue;
890
+ }
891
+ if (interpolationDepth === 0 && code[i] + (code[i + 1] ?? "") === "${") {
892
+ out += "${";
893
+ i += 2;
894
+ interpolationDepth = 1;
895
+ continue;
896
+ }
897
+ if (interpolationDepth > 0) {
898
+ if (code[i] === "{") interpolationDepth++;
899
+ else if (code[i] === "}") {
900
+ interpolationDepth--;
901
+ if (interpolationDepth === 0) {
902
+ out += "}";
903
+ i++;
904
+ continue;
905
+ }
906
+ }
907
+ }
908
+ out += code[i];
909
+ i++;
910
+ }
911
+ continue;
912
+ }
913
+ out += ch;
914
+ i++;
915
+ }
916
+ return out;
1567
917
  }
1568
- function writeFile(path, content) {
1569
- (0, import_fs2.writeFileSync)(path, content, "utf-8");
918
+ function splitClassTokens(str) {
919
+ return str.split(/\s+/).filter(Boolean);
1570
920
  }
1571
- function writeFileEnsuringDir(path, content) {
1572
- (0, import_fs2.mkdirSync)((0, import_path3.dirname)(path), { recursive: true });
1573
- (0, import_fs2.writeFileSync)(path, content, "utf-8");
921
+ var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
922
+ function pushTokens(str, into) {
923
+ for (const tok of splitClassTokens(str)) {
924
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
925
+ }
1574
926
  }
1575
- function loadConfigFile(root) {
1576
- const path = (0, import_path3.join)(root, "kbach.config.js");
1577
- if (!(0, import_fs2.existsSync)(path)) return void 0;
1578
- const mod = (0, import_module.createRequire)(root)(path);
1579
- return mod.default ?? mod;
927
+ function pushTemplateLiteralBody(body, into) {
928
+ const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
929
+ let im;
930
+ while ((im = interpolationRe.exec(body)) !== null) {
931
+ pushClassLikeStrings(im[0], into);
932
+ }
933
+ pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, () => "$"), into);
1580
934
  }
1581
- function kbach(options = {}) {
1582
- let theme = options.theme ?? (options.config ? resolveKbachConfig(options.config) : defaultTheme);
1583
- let themeJson = JSON.stringify(theme);
1584
- const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1585
- const safelist = options.safelist ?? [];
1586
- let root = process.cwd();
935
+ function pushClassLikeStrings(text, into) {
936
+ const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
937
+ let qm;
938
+ while ((qm = quotedStringRe.exec(text)) !== null) {
939
+ const [, quote, content] = qm;
940
+ if (quote === "`") pushTemplateLiteralBody(content, into);
941
+ else pushTokens(content, into);
942
+ }
943
+ }
944
+ var JSX_TAG_RE = /<([a-z][a-zA-Z0-9]*)[\s/>]/g;
945
+ function scanUsedTags(code) {
946
+ const cleaned = stripComments(code);
947
+ const tags = /* @__PURE__ */ new Set();
948
+ let m;
949
+ JSX_TAG_RE.lastIndex = 0;
950
+ while ((m = JSX_TAG_RE.exec(cleaned)) !== null) tags.add(m[1]);
951
+ return tags;
952
+ }
953
+ function extractClassStrings(rawCode) {
954
+ const code = stripComments(rawCode);
955
+ const found = /* @__PURE__ */ new Set();
956
+ const simpleRe = /(?:className|kb)=(["'])((?:(?!\1).)*)\1/g;
957
+ let m;
958
+ while ((m = simpleRe.exec(code)) !== null) pushTokens(m[2], found);
959
+ const jsxExprRe = /(?:className|kb)=\{/g;
960
+ while ((m = jsxExprRe.exec(code)) !== null) {
961
+ let depth = 1;
962
+ let i = m.index + m[0].length;
963
+ let block = "";
964
+ while (i < code.length && depth > 0) {
965
+ const ch = code[i];
966
+ if (ch === "{") depth++;
967
+ else if (ch === "}") {
968
+ if (--depth === 0) break;
969
+ }
970
+ block += ch;
971
+ i++;
972
+ }
973
+ jsxExprRe.lastIndex = i + 1;
974
+ pushClassLikeStrings(block, found);
975
+ }
976
+ const classComposerCallRe = /(?:clsx|cn|classnames|cx|kb)\(/g;
977
+ while ((m = classComposerCallRe.exec(code)) !== null) {
978
+ let depth = 1;
979
+ let i = m.index + m[0].length;
980
+ let block = "";
981
+ while (i < code.length && depth > 0) {
982
+ const ch = code[i];
983
+ if (ch === "(") depth++;
984
+ else if (ch === ")") {
985
+ if (--depth === 0) break;
986
+ }
987
+ block += ch;
988
+ i++;
989
+ }
990
+ classComposerCallRe.lastIndex = i + 1;
991
+ pushClassLikeStrings(block, found);
992
+ }
993
+ const templateRe = /`([^`]{1,2000})`/g;
994
+ while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
995
+ return [...found];
996
+ }
997
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
998
+ var MAX_SCAN_DEPTH = 10;
999
+ var DEFAULT_FILE_MATCH = /\.(tsx?|jsx?)$/;
1000
+ function scanDir(dir, onFile, depth = 0, fileMatch = DEFAULT_FILE_MATCH) {
1001
+ if (depth > MAX_SCAN_DEPTH) return;
1002
+ let entries;
1003
+ try {
1004
+ entries = (0, import_fs.readdirSync)(dir);
1005
+ } catch {
1006
+ return;
1007
+ }
1008
+ for (const entry of entries) {
1009
+ if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
1010
+ const full = (0, import_path.join)(dir, entry);
1011
+ try {
1012
+ const st = (0, import_fs.statSync)(full);
1013
+ if (st.isDirectory()) scanDir(full, onFile, depth + 1, fileMatch);
1014
+ else if (fileMatch.test(entry)) onFile(full, (0, import_fs.readFileSync)(full, "utf-8"));
1015
+ } catch {
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ // src/vite-plugin/unknownClassWarnings.ts
1021
+ var import_path2 = require("path");
1022
+
1023
+ // src/terminalWarn.ts
1024
+ var useColor = !!process.stdout?.isTTY && !process.env.NO_COLOR;
1025
+ var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
1026
+ var green = (s) => paint("32", s);
1027
+ var blue = (s) => paint("34", s);
1028
+ var white = (s) => paint("97", s);
1029
+ var gray = (s) => paint("90", s);
1030
+ var bold = (s) => paint("1", s);
1031
+ var highlight = (s) => bold(white(s));
1032
+ function kbachTag() {
1033
+ return `${green("\u2192")} ${bold(green("[Kbach]"))}`;
1034
+ }
1035
+
1036
+ // src/vite-plugin/unknownClassWarnings.ts
1037
+ var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
1038
+ var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
1039
+ function findLineCol(code, needle) {
1040
+ const index = code.indexOf(needle);
1041
+ if (index === -1) return null;
1042
+ let line = 1;
1043
+ let lastNewline = -1;
1044
+ for (let i = 0; i < index; i++) {
1045
+ if (code.charCodeAt(i) === 10) {
1046
+ line++;
1047
+ lastNewline = i;
1048
+ }
1049
+ }
1050
+ return { line, column: index - lastNewline };
1051
+ }
1052
+ function scanCssFileInto(code, into) {
1053
+ const re = new RegExp(CSS_CLASS_SELECTOR_RE.source, "g");
1054
+ let m;
1055
+ while ((m = re.exec(code)) !== null) into.add(m[1]);
1056
+ }
1057
+ function scanProjectCssSelectors(root, includeDirs) {
1058
+ const projectCssClasses = /* @__PURE__ */ new Set();
1059
+ for (const dir of includeDirs) {
1060
+ scanDir((0, import_path2.join)(root, dir), (_filePath, code) => scanCssFileInto(code, projectCssClasses), 0, CSS_FILE_RE);
1061
+ }
1062
+ return projectCssClasses;
1063
+ }
1064
+ function stripModifierPrefix(token) {
1065
+ return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
1066
+ }
1067
+ var MARKER_CLASSES = /* @__PURE__ */ new Set(["group", "peer"]);
1068
+ function isMarkerClass(base) {
1069
+ if (MARKER_CLASSES.has(base)) return true;
1070
+ const slashIdx = base.lastIndexOf("/");
1071
+ if (slashIdx === -1) return false;
1072
+ return MARKER_CLASSES.has(base.slice(0, slashIdx)) && slashIdx < base.length - 1;
1073
+ }
1074
+ function warnIfUnknownClass(token, filePath, code, isResolvable, projectCssClasses, warnedTokens) {
1075
+ if (isResolvable || warnedTokens.has(token)) return;
1076
+ const base = stripModifierPrefix(token);
1077
+ if (!base || projectCssClasses.has(base) || isMarkerClass(base)) return;
1078
+ warnedTokens.add(token);
1079
+ const pos = findLineCol(code, token);
1080
+ const location = pos ? `${blue(filePath)}${green(`:${pos.line}:${pos.column}`)}` : blue(filePath);
1081
+ console.warn(
1082
+ `${kbachTag()} ${gray("Unknown class")} ${highlight(token)} ${gray("\u2014 no Kbach utility or project CSS rule matches it. Typo?")}
1083
+ ${bold("at")} ${location}`
1084
+ );
1085
+ }
1086
+
1087
+ // src/staticCss/engine.ts
1088
+ function normPath(p) {
1089
+ const fwd = p.replace(/\\/g, "/");
1090
+ return process.platform === "win32" ? fwd.toLowerCase() : fwd;
1091
+ }
1092
+ var SAFELIST_KEY = "\0kbach-safelist";
1093
+ function createKbachStaticCssEngine(options) {
1094
+ const { root, theme, themeJson, includeDirs, safelist } = options;
1587
1095
  const fileTokens = /* @__PURE__ */ new Map();
1588
1096
  const tokenRules = /* @__PURE__ */ new Map();
1589
1097
  const fileTags = /* @__PURE__ */ new Map();
@@ -1607,6 +1115,10 @@ function kbach(options = {}) {
1607
1115
  fileTokens.set(normPath(filePath), tokens);
1608
1116
  fileTags.set(normPath(filePath), scanUsedTags(code));
1609
1117
  }
1118
+ function removeFile(filePath) {
1119
+ fileTokens.delete(normPath(filePath));
1120
+ fileTags.delete(normPath(filePath));
1121
+ }
1610
1122
  function processSafelist() {
1611
1123
  if (safelist.length === 0) return;
1612
1124
  const tokens = new Set(safelist);
@@ -1630,50 +1142,121 @@ function kbach(options = {}) {
1630
1142
  const rules = tokenRules.get(tok);
1631
1143
  if (rules) view.set(tok, rules);
1632
1144
  }
1633
- return formatKbachCSS(view, theme, activeTags());
1634
- }
1635
- function mainCSSFile() {
1636
- return (0, import_path3.join)(root, "src", "kbach.css");
1637
- }
1638
- function classNameHintsFile() {
1639
- return (0, import_path3.join)(root, ".kbach", "kbach-classnames.d.ts");
1640
- }
1641
- function writeClassNameHints() {
1642
- const dts = buildClassNameHintsDts(buildClassTokens(theme));
1643
- writeFileEnsuringDir(classNameHintsFile(), dts);
1644
- }
1645
- function syncMainCSSFile(server) {
1646
- const active = activeTokens();
1647
1145
  for (const tok of tokenRules.keys()) {
1648
1146
  if (!active.has(tok)) tokenRules.delete(tok);
1649
1147
  }
1650
- const changed = writeKbachToFile(mainCSSFile(), generateCSS(), readFile, writeFile);
1651
- if (changed && server) server.watcher.emit("change", mainCSSFile());
1148
+ return formatKbachCSS(view, theme, activeTags());
1652
1149
  }
1653
1150
  function initialScan() {
1654
1151
  for (const dir of includeDirs) {
1655
1152
  scanDir((0, import_path3.join)(root, dir), processFile);
1656
1153
  }
1657
1154
  }
1155
+ function scanProjectCssSelectorsOnce() {
1156
+ projectCssClasses = scanProjectCssSelectors(root, includeDirs);
1157
+ }
1158
+ return { processFile, removeFile, initialScan, processSafelist, generateCSS, scanProjectCssSelectorsOnce };
1159
+ }
1160
+ function readSourceFile(path) {
1161
+ return (0, import_fs2.readFileSync)(path, "utf-8");
1162
+ }
1163
+
1164
+ // src/vite-plugin/index.ts
1165
+ function findMainCSSFile(root, includeDirs, explicitCssFile) {
1166
+ if (explicitCssFile) return (0, import_path4.join)(root, explicitCssFile);
1167
+ const searchDirs = [...includeDirs, "."];
1168
+ for (const dir of searchDirs) {
1169
+ const candidate = (0, import_path4.join)(root, dir, "kbach.css");
1170
+ if (!(0, import_fs3.existsSync)(candidate)) continue;
1171
+ try {
1172
+ const content = (0, import_fs3.readFileSync)(candidate, "utf-8");
1173
+ if (content.includes(KBACH_START) && content.includes(KBACH_END)) return candidate;
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ for (const dir of includeDirs) {
1178
+ const full = (0, import_path4.join)(root, dir);
1179
+ try {
1180
+ if ((0, import_fs3.statSync)(full).isDirectory()) return (0, import_path4.join)(full, "kbach.css");
1181
+ } catch {
1182
+ }
1183
+ }
1184
+ return (0, import_path4.join)(root, includeDirs[0] ?? "src", "kbach.css");
1185
+ }
1186
+ var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1187
+ var CSS_SYNC_DEBOUNCE_MS = 50;
1188
+ function writeFileEnsuringDir(path, content) {
1189
+ (0, import_fs3.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
1190
+ (0, import_fs3.writeFileSync)(path, content, "utf-8");
1191
+ }
1192
+ function isEsmRequireFailure(err) {
1193
+ if (!(err instanceof Error)) return false;
1194
+ return err.code === "ERR_REQUIRE_ESM" || /is not defined in ES module scope/.test(err.message);
1195
+ }
1196
+ async function importConfigModule(path) {
1197
+ const mod = await import((0, import_url.pathToFileURL)(path).href);
1198
+ return mod.default ?? mod;
1199
+ }
1200
+ function requireConfigModule(root, path) {
1201
+ const mod = (0, import_module.createRequire)(root)(path);
1202
+ return mod.default ?? mod;
1203
+ }
1204
+ async function loadConfigFile(root) {
1205
+ const cjsPath = (0, import_path4.join)(root, "kbach.config.cjs");
1206
+ if ((0, import_fs3.existsSync)(cjsPath)) return requireConfigModule(root, cjsPath);
1207
+ const mjsPath = (0, import_path4.join)(root, "kbach.config.mjs");
1208
+ if ((0, import_fs3.existsSync)(mjsPath)) return importConfigModule(mjsPath);
1209
+ const jsPath = (0, import_path4.join)(root, "kbach.config.js");
1210
+ if (!(0, import_fs3.existsSync)(jsPath)) return void 0;
1211
+ try {
1212
+ return requireConfigModule(root, jsPath);
1213
+ } catch (err) {
1214
+ if (!isEsmRequireFailure(err)) throw err;
1215
+ return importConfigModule(jsPath);
1216
+ }
1217
+ }
1218
+ async function resolveEffectiveTheme(root, options) {
1219
+ if (options.theme) return options.theme;
1220
+ if (options.config) return resolveKbachConfig(options.config);
1221
+ const discovered = await loadConfigFile(root);
1222
+ return discovered ? resolveKbachConfig(discovered) : defaultTheme;
1223
+ }
1224
+ function kbach(options = {}) {
1225
+ const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1226
+ const safelist = options.safelist ?? [];
1227
+ let root = process.cwd();
1228
+ let theme = defaultTheme;
1229
+ let engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1230
+ let mainCSSFilePath = (0, import_path4.join)(root, "src", "kbach.css");
1231
+ function mainCSSFile() {
1232
+ return mainCSSFilePath;
1233
+ }
1234
+ function syncMainCSSFile(server) {
1235
+ const changed = writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1236
+ if (changed && server) server.watcher.emit("change", mainCSSFile());
1237
+ }
1238
+ let syncTimer = null;
1239
+ function scheduleSync(server) {
1240
+ if (syncTimer) clearTimeout(syncTimer);
1241
+ syncTimer = setTimeout(() => {
1242
+ syncTimer = null;
1243
+ syncMainCSSFile(server);
1244
+ }, CSS_SYNC_DEBOUNCE_MS);
1245
+ }
1658
1246
  return {
1659
1247
  name: "kbach",
1660
1248
  enforce: "pre",
1661
- configResolved(resolved) {
1249
+ async configResolved(resolved) {
1662
1250
  root = resolved.root;
1663
- if (!options.theme && !options.config) {
1664
- const discovered = loadConfigFile(root);
1665
- if (discovered) {
1666
- theme = resolveKbachConfig(discovered);
1667
- themeJson = JSON.stringify(theme);
1668
- }
1669
- }
1251
+ theme = await resolveEffectiveTheme(root, options);
1252
+ engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1253
+ mainCSSFilePath = findMainCSSFile(root, includeDirs, options.cssFile);
1670
1254
  },
1671
1255
  buildStart() {
1672
- projectCssClasses = scanProjectCssSelectors(root, includeDirs);
1673
- initialScan();
1674
- processSafelist();
1675
- writeKbachToFile(mainCSSFile(), generateCSS(), readFile, writeFile);
1676
- writeClassNameHints();
1256
+ engine.scanProjectCssSelectorsOnce();
1257
+ engine.initialScan();
1258
+ engine.processSafelist();
1259
+ writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1677
1260
  },
1678
1261
  // handleHotUpdate covers edits; add/unlink (create/delete) land on the
1679
1262
  // raw watcher instead — Vite's handleHotUpdate hook only fires for
@@ -1683,17 +1266,15 @@ function kbach(options = {}) {
1683
1266
  if (file.includes("node_modules")) return;
1684
1267
  if (!/\.(tsx?|jsx?)$/.test(file)) return;
1685
1268
  if (isUnlink) {
1686
- fileTokens.delete(normPath(file));
1687
- fileTags.delete(normPath(file));
1269
+ engine.removeFile(file);
1688
1270
  } else {
1689
1271
  try {
1690
- processFile(file, readFile(file));
1272
+ engine.processFile(file, readSourceFile(file));
1691
1273
  } catch {
1692
- fileTokens.delete(normPath(file));
1693
- fileTags.delete(normPath(file));
1274
+ engine.removeFile(file);
1694
1275
  }
1695
1276
  }
1696
- syncMainCSSFile(server);
1277
+ scheduleSync(server);
1697
1278
  };
1698
1279
  server.watcher.on("add", (file) => onAddOrUnlink(file, false));
1699
1280
  server.watcher.on("unlink", (file) => onAddOrUnlink(file, true));
@@ -1702,16 +1283,20 @@ function kbach(options = {}) {
1702
1283
  if (file.includes("node_modules")) return;
1703
1284
  if (!/\.(tsx?|jsx?)$/.test(file)) return;
1704
1285
  try {
1705
- processFile(file, readFile(file));
1286
+ engine.processFile(file, readSourceFile(file));
1706
1287
  } catch {
1707
- fileTokens.delete(normPath(file));
1708
- fileTags.delete(normPath(file));
1288
+ engine.removeFile(file);
1709
1289
  }
1710
- syncMainCSSFile(server);
1290
+ scheduleSync(server);
1711
1291
  }
1712
1292
  };
1713
1293
  }
1714
1294
  // Annotate the CommonJS export names for ESM import in node:
1715
1295
  0 && (module.exports = {
1716
- kbach
1296
+ CSS_SYNC_DEBOUNCE_MS,
1297
+ DEFAULT_SCAN_DIRS,
1298
+ findMainCSSFile,
1299
+ kbach,
1300
+ loadConfigFile,
1301
+ resolveEffectiveTheme
1717
1302
  });