@kbach/react 1.0.0-beta.1 → 1.0.0-beta.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.
@@ -20,120 +20,18 @@ 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");
28
-
29
- // src/vite-plugin/wasmNode.ts
30
- var import_node = require("@kbach/core-engine/node");
31
- function generateCssForToken(token, themeJson) {
32
- return JSON.parse((0, import_node.generate_css)(token, themeJson));
33
- }
34
-
35
- // src/vite-plugin/scan.ts
36
- var import_fs = require("fs");
37
- var import_path = require("path");
38
- function splitClassTokens(str) {
39
- return str.split(/\s+/).filter(Boolean);
40
- }
41
- var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
42
- function pushTokens(str, into) {
43
- for (const tok of splitClassTokens(str)) {
44
- if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
45
- }
46
- }
47
- function pushTemplateLiteralBody(body, into) {
48
- const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
49
- let im;
50
- while ((im = interpolationRe.exec(body)) !== null) {
51
- pushClassLikeStrings(im[0], into);
52
- }
53
- pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, () => "$"), into);
54
- }
55
- function pushClassLikeStrings(text, into) {
56
- const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
57
- let qm;
58
- while ((qm = quotedStringRe.exec(text)) !== null) {
59
- const [, quote, content] = qm;
60
- if (quote === "`") pushTemplateLiteralBody(content, into);
61
- else pushTokens(content, into);
62
- }
63
- }
64
- var JSX_TAG_RE = /<([a-z][a-zA-Z0-9]*)[\s/>]/g;
65
- function scanUsedTags(code) {
66
- const tags = /* @__PURE__ */ new Set();
67
- let m;
68
- JSX_TAG_RE.lastIndex = 0;
69
- while ((m = JSX_TAG_RE.exec(code)) !== null) tags.add(m[1]);
70
- return tags;
71
- }
72
- function extractClassStrings(code) {
73
- const found = /* @__PURE__ */ new Set();
74
- const simpleRe = /(?:className|kb)=(["'])((?:(?!\1).)*)\1/g;
75
- let m;
76
- while ((m = simpleRe.exec(code)) !== null) pushTokens(m[2], found);
77
- const jsxExprRe = /(?:className|kb)=\{/g;
78
- while ((m = jsxExprRe.exec(code)) !== null) {
79
- let depth = 1;
80
- let i = m.index + m[0].length;
81
- let block = "";
82
- while (i < code.length && depth > 0) {
83
- const ch = code[i];
84
- if (ch === "{") depth++;
85
- else if (ch === "}") {
86
- if (--depth === 0) break;
87
- }
88
- block += ch;
89
- i++;
90
- }
91
- jsxExprRe.lastIndex = i + 1;
92
- pushClassLikeStrings(block, found);
93
- }
94
- const classComposerCallRe = /(?:clsx|cn|classnames|cx|kb)\(/g;
95
- while ((m = classComposerCallRe.exec(code)) !== null) {
96
- let depth = 1;
97
- let i = m.index + m[0].length;
98
- let block = "";
99
- while (i < code.length && depth > 0) {
100
- const ch = code[i];
101
- if (ch === "(") depth++;
102
- else if (ch === ")") {
103
- if (--depth === 0) break;
104
- }
105
- block += ch;
106
- i++;
107
- }
108
- classComposerCallRe.lastIndex = i + 1;
109
- pushClassLikeStrings(block, found);
110
- }
111
- const templateRe = /`([^`]{1,2000})`/g;
112
- while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
113
- return [...found];
114
- }
115
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
116
- var MAX_SCAN_DEPTH = 10;
117
- var DEFAULT_FILE_MATCH = /\.(tsx?|jsx?)$/;
118
- function scanDir(dir, onFile, depth = 0, fileMatch = DEFAULT_FILE_MATCH) {
119
- if (depth > MAX_SCAN_DEPTH) return;
120
- let entries;
121
- try {
122
- entries = (0, import_fs.readdirSync)(dir);
123
- } catch {
124
- return;
125
- }
126
- for (const entry of entries) {
127
- if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
128
- const full = (0, import_path.join)(dir, entry);
129
- try {
130
- const st = (0, import_fs.statSync)(full);
131
- if (st.isDirectory()) scanDir(full, onFile, depth + 1, fileMatch);
132
- else if (fileMatch.test(entry)) onFile(full, (0, import_fs.readFileSync)(full, "utf-8"));
133
- } catch {
134
- }
135
- }
136
- }
31
+ var import_fs3 = require("fs");
32
+ var import_path4 = require("path");
33
+ var import_url = require("url");
34
+ var import_module = require("module");
137
35
 
138
36
  // src/vite-plugin/cssVars.ts
139
37
  function hexToRgbTriplet(hex) {
@@ -395,10 +293,10 @@ function formatKbachCSS(tokenRules, theme, usedTags) {
395
293
  lines.push("", prettifyCSS(text));
396
294
  return lines.join("\n");
397
295
  }
398
- function writeKbachToFile(filePath, css, readFile2, writeFile2) {
296
+ function writeKbachToFile(filePath, css, readFile, writeFile) {
399
297
  let existing = "";
400
298
  try {
401
- existing = readFile2(filePath);
299
+ existing = readFile(filePath);
402
300
  } catch {
403
301
  }
404
302
  const block = `${KBACH_START}
@@ -410,66 +308,10 @@ ${KBACH_END}`;
410
308
 
411
309
  ${block}` : block;
412
310
  if (next === existing) return false;
413
- writeFile2(filePath, next);
311
+ writeFile(filePath, next);
414
312
  return true;
415
313
  }
416
314
 
417
- // src/vite-plugin/unknownClassWarnings.ts
418
- var import_path2 = require("path");
419
- var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
420
- var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
421
- var useColor = !!process.stdout?.isTTY && !process.env.NO_COLOR;
422
- var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
423
- var green = (s) => paint("32", s);
424
- var blue = (s) => paint("34", s);
425
- var white = (s) => paint("97", s);
426
- var gray = (s) => paint("90", s);
427
- var bold = (s) => paint("1", s);
428
- var ARROW = () => green("\u2192");
429
- var TAG = () => bold(green("[kbach]"));
430
- var highlight = (s) => bold(white(s));
431
- function findLineCol(code, needle) {
432
- const index = code.indexOf(needle);
433
- if (index === -1) return null;
434
- let line = 1;
435
- let lastNewline = -1;
436
- for (let i = 0; i < index; i++) {
437
- if (code.charCodeAt(i) === 10) {
438
- line++;
439
- lastNewline = i;
440
- }
441
- }
442
- return { line, column: index - lastNewline };
443
- }
444
- function scanCssFileInto(code, into) {
445
- const re = new RegExp(CSS_CLASS_SELECTOR_RE.source, "g");
446
- let m;
447
- while ((m = re.exec(code)) !== null) into.add(m[1]);
448
- }
449
- function scanProjectCssSelectors(root, includeDirs) {
450
- const projectCssClasses = /* @__PURE__ */ new Set();
451
- for (const dir of includeDirs) {
452
- scanDir((0, import_path2.join)(root, dir), (_filePath, code) => scanCssFileInto(code, projectCssClasses), 0, CSS_FILE_RE);
453
- }
454
- return projectCssClasses;
455
- }
456
- function stripModifierPrefix(token) {
457
- return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
458
- }
459
- var MARKER_CLASSES = /* @__PURE__ */ new Set(["group", "peer"]);
460
- function warnIfUnknownClass(token, filePath, code, isResolvable, projectCssClasses, warnedTokens) {
461
- if (isResolvable || warnedTokens.has(token)) return;
462
- const base = stripModifierPrefix(token);
463
- if (!base || projectCssClasses.has(base) || MARKER_CLASSES.has(base)) return;
464
- warnedTokens.add(token);
465
- const pos = findLineCol(code, token);
466
- const location = pos ? `${blue(filePath)}${green(`:${pos.line}:${pos.column}`)}` : blue(filePath);
467
- console.warn(
468
- `${ARROW()} ${TAG()} ${gray("Unknown class")} ${highlight(token)} ${gray("\u2014 no Kbach utility or project CSS rule matches it. Typo?")}
469
- ${bold("at")} ${location}`
470
- );
471
- }
472
-
473
315
  // src/vite-plugin/classNameHints.ts
474
316
  var STRUCTURAL_TOKENS = [
475
317
  // layout.rs
@@ -1017,6 +859,41 @@ var STRUCTURAL_TOKENS = [
1017
859
  "aria-required:",
1018
860
  "aria-selected:",
1019
861
  "aria-busy:",
862
+ // registry.rs modifiers, added later — same "fixed/static, finite" bar
863
+ // as the Phase 24 block above. Structural -of-type/empty, form-state and
864
+ // validation pseudo-classes, other native pseudo-classes with no aria-*
865
+ // counterpart, a pseudo-element, pointer media variants, and the
866
+ // direct-children/all-descendants variants. "supports-[...]:"/"in-*:" are
867
+ // deliberately excluded here for the same "no finite enumerable set"
868
+ // reason as has-[...]:/group-<pseudo>: above.
869
+ "first-of-type:",
870
+ "last-of-type:",
871
+ "only-of-type:",
872
+ "empty:",
873
+ "enabled:",
874
+ "read-only:",
875
+ "required:",
876
+ "optional:",
877
+ "valid:",
878
+ "invalid:",
879
+ "in-range:",
880
+ "out-of-range:",
881
+ "default:",
882
+ "indeterminate:",
883
+ "placeholder-shown:",
884
+ "autofill:",
885
+ "user-valid:",
886
+ "user-invalid:",
887
+ "target:",
888
+ "fullscreen:",
889
+ "popover-open:",
890
+ "details-content:",
891
+ "pointer-coarse:",
892
+ "pointer-fine:",
893
+ "any-pointer-coarse:",
894
+ "any-pointer-fine:",
895
+ "*:",
896
+ "**:",
1020
897
  // Phase 25 (container queries & arbitrary properties). "@container" is a
1021
898
  // real UTILITY (container-type: inline-size), not a modifier — included
1022
899
  // as a bare token like "flex"/"hidden" above, not with the trailing ":"
@@ -1424,14 +1301,15 @@ var defaultTheme = {
1424
1301
  // Real Tailwind's default spacing scale in full — every step is exactly
1425
1302
  // `n * 4px` (Tailwind's base spacing unit), including the fractional
1426
1303
  // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
1427
- // literal (a bare 1px, for `p-px`-style hairline spacing). Previously
1428
- // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
1429
- // the real scale — so anything else (`p-3`, `w-5`, `border-2`, `gap-10`,
1430
- // ...) silently failed to resolve (an unknown spacing key returns `None`
1431
- // the same way an unknown class does, with no error). "0" itself is
1432
- // still needed explicitly (not derivable as "any number * 4") for the
1433
- // same reason it always was: `sticky top-0` needs an actual "0" key to
1434
- // resolve at all, not just fall through as unresolvable.
1304
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
1305
+ // only the "usual stops", though — the Rust/JS engines' shared
1306
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
1307
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
1308
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
1309
+ // values and give a theme customization something to override, not to
1310
+ // enumerate every resolvable step. "px" is the one exception that must
1311
+ // stay listed explicitly — it isn't a number, so the formula can't
1312
+ // derive it.
1435
1313
  spacing: {
1436
1314
  "0": 0,
1437
1315
  "px": 1,
@@ -1476,33 +1354,350 @@ var defaultTheme = {
1476
1354
  xl: 1280,
1477
1355
  "2xl": 1536
1478
1356
  },
1479
- darkMode: "attribute"
1357
+ // Byte-for-byte the same three stacks
1358
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
1359
+ // falls back to — kept here too (rather than leaving this empty and
1360
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
1361
+ // is a real, readable value for anything that wants to extend rather
1362
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
1363
+ fontFamily: {
1364
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
1365
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
1366
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
1367
+ },
1368
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
1369
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
1370
+ // using only static `className` strings gets working `dark:` classes
1371
+ // out of the box, with no runtime import required to write `data-theme`/
1372
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
1373
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
1374
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
1375
+ // for JS consumers, but flipping it has no visible effect on `dark:`
1376
+ // classes, since the browser's media query only ever reflects the real
1377
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
1378
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
1379
+ // as an app wants a real in-app toggle.
1380
+ darkMode: "media",
1381
+ // Empty by default, matching real Tailwind's own defaults (no forced
1382
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
1383
+ container: {}
1480
1384
  };
1481
1385
  var activeThemeJson = JSON.stringify(defaultTheme);
1482
1386
 
1483
- // src/vite-plugin/index.ts
1484
- var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1387
+ // src/colorVariables.ts
1388
+ function parseHexRgb(hex) {
1389
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
1390
+ if (!m) return null;
1391
+ let h = m[1];
1392
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
1393
+ const n = parseInt(h, 16);
1394
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1395
+ }
1396
+
1397
+ // src/config.ts
1398
+ function applyOpacityToHex(hex, opacityText) {
1399
+ const rgb = parseHexRgb(hex);
1400
+ if (!rgb) return hex;
1401
+ const a = Number(opacityText) / 100;
1402
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
1403
+ return `rgba(${rgb.join(",")},${clamped})`;
1404
+ }
1405
+ function resolveColorEntry(value, lookup) {
1406
+ if (typeof value !== "string") return value;
1407
+ const slash = value.indexOf("/");
1408
+ const name = slash === -1 ? value : value.slice(0, slash);
1409
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
1410
+ const base = lookup(name);
1411
+ if (base === void 0) return value;
1412
+ if (opacity === void 0) return base;
1413
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
1414
+ }
1415
+ function resolveColors(extend, existing) {
1416
+ const resolved = {};
1417
+ const lookup = (name) => resolved[name] ?? existing[name];
1418
+ for (const [name, value] of Object.entries(extend)) {
1419
+ resolved[name] = resolveColorEntry(value, lookup);
1420
+ }
1421
+ return resolved;
1422
+ }
1423
+ function resolveKbachConfig(config) {
1424
+ let theme = { ...defaultTheme, ...config.theme };
1425
+ if (config.extend?.colors) {
1426
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
1427
+ }
1428
+ if (config.extend?.spacing) {
1429
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
1430
+ }
1431
+ if (config.extend?.screens) {
1432
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
1433
+ }
1434
+ if (config.extend?.fontFamily) {
1435
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
1436
+ }
1437
+ if (config.extend?.container) {
1438
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
1439
+ }
1440
+ if (config.darkMode) {
1441
+ theme = { ...theme, darkMode: config.darkMode };
1442
+ }
1443
+ return theme;
1444
+ }
1445
+
1446
+ // src/staticCss/engine.ts
1447
+ var import_fs2 = require("fs");
1448
+ var import_path3 = require("path");
1449
+
1450
+ // src/vite-plugin/wasmNode.ts
1451
+ var import_node = require("@kbach/core-engine/node");
1452
+ function generateCssForToken(token, themeJson) {
1453
+ return JSON.parse((0, import_node.generate_css)(token, themeJson));
1454
+ }
1455
+
1456
+ // src/vite-plugin/scan.ts
1457
+ var import_fs = require("fs");
1458
+ var import_path = require("path");
1459
+ function stripComments(code) {
1460
+ let out = "";
1461
+ let i = 0;
1462
+ const n = code.length;
1463
+ while (i < n) {
1464
+ const two = code[i] + (code[i + 1] ?? "");
1465
+ if (two === "//") {
1466
+ i += 2;
1467
+ while (i < n && code[i] !== "\n") i++;
1468
+ continue;
1469
+ }
1470
+ if (two === "/*") {
1471
+ i += 2;
1472
+ while (i < n && code[i] + (code[i + 1] ?? "") !== "*/") i++;
1473
+ i += 2;
1474
+ continue;
1475
+ }
1476
+ const ch = code[i];
1477
+ if (ch === '"' || ch === "'") {
1478
+ out += ch;
1479
+ i++;
1480
+ while (i < n && code[i] !== ch) {
1481
+ if (code[i] === "\\") {
1482
+ out += code[i] + (code[i + 1] ?? "");
1483
+ i += 2;
1484
+ continue;
1485
+ }
1486
+ out += code[i];
1487
+ i++;
1488
+ }
1489
+ out += code[i] ?? "";
1490
+ i++;
1491
+ continue;
1492
+ }
1493
+ if (ch === "`") {
1494
+ out += ch;
1495
+ i++;
1496
+ let interpolationDepth = 0;
1497
+ while (i < n) {
1498
+ if (interpolationDepth === 0 && code[i] === "`") {
1499
+ out += code[i];
1500
+ i++;
1501
+ break;
1502
+ }
1503
+ if (interpolationDepth === 0 && code[i] === "\\") {
1504
+ out += code[i] + (code[i + 1] ?? "");
1505
+ i += 2;
1506
+ continue;
1507
+ }
1508
+ if (interpolationDepth === 0 && code[i] + (code[i + 1] ?? "") === "${") {
1509
+ out += "${";
1510
+ i += 2;
1511
+ interpolationDepth = 1;
1512
+ continue;
1513
+ }
1514
+ if (interpolationDepth > 0) {
1515
+ if (code[i] === "{") interpolationDepth++;
1516
+ else if (code[i] === "}") {
1517
+ interpolationDepth--;
1518
+ if (interpolationDepth === 0) {
1519
+ out += "}";
1520
+ i++;
1521
+ continue;
1522
+ }
1523
+ }
1524
+ }
1525
+ out += code[i];
1526
+ i++;
1527
+ }
1528
+ continue;
1529
+ }
1530
+ out += ch;
1531
+ i++;
1532
+ }
1533
+ return out;
1534
+ }
1535
+ function splitClassTokens(str) {
1536
+ return str.split(/\s+/).filter(Boolean);
1537
+ }
1538
+ var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
1539
+ function pushTokens(str, into) {
1540
+ for (const tok of splitClassTokens(str)) {
1541
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
1542
+ }
1543
+ }
1544
+ function pushTemplateLiteralBody(body, into) {
1545
+ const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
1546
+ let im;
1547
+ while ((im = interpolationRe.exec(body)) !== null) {
1548
+ pushClassLikeStrings(im[0], into);
1549
+ }
1550
+ pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, () => "$"), into);
1551
+ }
1552
+ function pushClassLikeStrings(text, into) {
1553
+ const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
1554
+ let qm;
1555
+ while ((qm = quotedStringRe.exec(text)) !== null) {
1556
+ const [, quote, content] = qm;
1557
+ if (quote === "`") pushTemplateLiteralBody(content, into);
1558
+ else pushTokens(content, into);
1559
+ }
1560
+ }
1561
+ var JSX_TAG_RE = /<([a-z][a-zA-Z0-9]*)[\s/>]/g;
1562
+ function scanUsedTags(code) {
1563
+ const cleaned = stripComments(code);
1564
+ const tags = /* @__PURE__ */ new Set();
1565
+ let m;
1566
+ JSX_TAG_RE.lastIndex = 0;
1567
+ while ((m = JSX_TAG_RE.exec(cleaned)) !== null) tags.add(m[1]);
1568
+ return tags;
1569
+ }
1570
+ function extractClassStrings(rawCode) {
1571
+ const code = stripComments(rawCode);
1572
+ const found = /* @__PURE__ */ new Set();
1573
+ const simpleRe = /(?:className|kb)=(["'])((?:(?!\1).)*)\1/g;
1574
+ let m;
1575
+ while ((m = simpleRe.exec(code)) !== null) pushTokens(m[2], found);
1576
+ const jsxExprRe = /(?:className|kb)=\{/g;
1577
+ while ((m = jsxExprRe.exec(code)) !== null) {
1578
+ let depth = 1;
1579
+ let i = m.index + m[0].length;
1580
+ let block = "";
1581
+ while (i < code.length && depth > 0) {
1582
+ const ch = code[i];
1583
+ if (ch === "{") depth++;
1584
+ else if (ch === "}") {
1585
+ if (--depth === 0) break;
1586
+ }
1587
+ block += ch;
1588
+ i++;
1589
+ }
1590
+ jsxExprRe.lastIndex = i + 1;
1591
+ pushClassLikeStrings(block, found);
1592
+ }
1593
+ const classComposerCallRe = /(?:clsx|cn|classnames|cx|kb)\(/g;
1594
+ while ((m = classComposerCallRe.exec(code)) !== null) {
1595
+ let depth = 1;
1596
+ let i = m.index + m[0].length;
1597
+ let block = "";
1598
+ while (i < code.length && depth > 0) {
1599
+ const ch = code[i];
1600
+ if (ch === "(") depth++;
1601
+ else if (ch === ")") {
1602
+ if (--depth === 0) break;
1603
+ }
1604
+ block += ch;
1605
+ i++;
1606
+ }
1607
+ classComposerCallRe.lastIndex = i + 1;
1608
+ pushClassLikeStrings(block, found);
1609
+ }
1610
+ const templateRe = /`([^`]{1,2000})`/g;
1611
+ while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
1612
+ return [...found];
1613
+ }
1614
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
1615
+ var MAX_SCAN_DEPTH = 10;
1616
+ var DEFAULT_FILE_MATCH = /\.(tsx?|jsx?)$/;
1617
+ function scanDir(dir, onFile, depth = 0, fileMatch = DEFAULT_FILE_MATCH) {
1618
+ if (depth > MAX_SCAN_DEPTH) return;
1619
+ let entries;
1620
+ try {
1621
+ entries = (0, import_fs.readdirSync)(dir);
1622
+ } catch {
1623
+ return;
1624
+ }
1625
+ for (const entry of entries) {
1626
+ if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
1627
+ const full = (0, import_path.join)(dir, entry);
1628
+ try {
1629
+ const st = (0, import_fs.statSync)(full);
1630
+ if (st.isDirectory()) scanDir(full, onFile, depth + 1, fileMatch);
1631
+ else if (fileMatch.test(entry)) onFile(full, (0, import_fs.readFileSync)(full, "utf-8"));
1632
+ } catch {
1633
+ }
1634
+ }
1635
+ }
1636
+
1637
+ // src/vite-plugin/unknownClassWarnings.ts
1638
+ var import_path2 = require("path");
1639
+ var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
1640
+ var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
1641
+ var useColor = !!process.stdout?.isTTY && !process.env.NO_COLOR;
1642
+ var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
1643
+ var green = (s) => paint("32", s);
1644
+ var blue = (s) => paint("34", s);
1645
+ var white = (s) => paint("97", s);
1646
+ var gray = (s) => paint("90", s);
1647
+ var bold = (s) => paint("1", s);
1648
+ var ARROW = () => green("\u2192");
1649
+ var TAG = () => bold(green("[kbach]"));
1650
+ var highlight = (s) => bold(white(s));
1651
+ function findLineCol(code, needle) {
1652
+ const index = code.indexOf(needle);
1653
+ if (index === -1) return null;
1654
+ let line = 1;
1655
+ let lastNewline = -1;
1656
+ for (let i = 0; i < index; i++) {
1657
+ if (code.charCodeAt(i) === 10) {
1658
+ line++;
1659
+ lastNewline = i;
1660
+ }
1661
+ }
1662
+ return { line, column: index - lastNewline };
1663
+ }
1664
+ function scanCssFileInto(code, into) {
1665
+ const re = new RegExp(CSS_CLASS_SELECTOR_RE.source, "g");
1666
+ let m;
1667
+ while ((m = re.exec(code)) !== null) into.add(m[1]);
1668
+ }
1669
+ function scanProjectCssSelectors(root, includeDirs) {
1670
+ const projectCssClasses = /* @__PURE__ */ new Set();
1671
+ for (const dir of includeDirs) {
1672
+ scanDir((0, import_path2.join)(root, dir), (_filePath, code) => scanCssFileInto(code, projectCssClasses), 0, CSS_FILE_RE);
1673
+ }
1674
+ return projectCssClasses;
1675
+ }
1676
+ function stripModifierPrefix(token) {
1677
+ return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
1678
+ }
1679
+ var MARKER_CLASSES = /* @__PURE__ */ new Set(["group", "peer"]);
1680
+ function warnIfUnknownClass(token, filePath, code, isResolvable, projectCssClasses, warnedTokens) {
1681
+ if (isResolvable || warnedTokens.has(token)) return;
1682
+ const base = stripModifierPrefix(token);
1683
+ if (!base || projectCssClasses.has(base) || MARKER_CLASSES.has(base)) return;
1684
+ warnedTokens.add(token);
1685
+ const pos = findLineCol(code, token);
1686
+ const location = pos ? `${blue(filePath)}${green(`:${pos.line}:${pos.column}`)}` : blue(filePath);
1687
+ console.warn(
1688
+ `${ARROW()} ${TAG()} ${gray("Unknown class")} ${highlight(token)} ${gray("\u2014 no Kbach utility or project CSS rule matches it. Typo?")}
1689
+ ${bold("at")} ${location}`
1690
+ );
1691
+ }
1692
+
1693
+ // src/staticCss/engine.ts
1485
1694
  function normPath(p) {
1486
1695
  const fwd = p.replace(/\\/g, "/");
1487
1696
  return process.platform === "win32" ? fwd.toLowerCase() : fwd;
1488
1697
  }
1489
1698
  var SAFELIST_KEY = "\0kbach-safelist";
1490
- function readFile(path) {
1491
- return (0, import_fs2.readFileSync)(path, "utf-8");
1492
- }
1493
- function writeFile(path, content) {
1494
- (0, import_fs2.writeFileSync)(path, content, "utf-8");
1495
- }
1496
- function writeFileEnsuringDir(path, content) {
1497
- (0, import_fs2.mkdirSync)((0, import_path3.dirname)(path), { recursive: true });
1498
- (0, import_fs2.writeFileSync)(path, content, "utf-8");
1499
- }
1500
- function kbach(options = {}) {
1501
- const theme = options.theme ?? defaultTheme;
1502
- const themeJson = JSON.stringify(theme);
1503
- const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1504
- const safelist = options.safelist ?? [];
1505
- let root = process.cwd();
1699
+ function createKbachStaticCssEngine(options) {
1700
+ const { root, theme, themeJson, includeDirs, safelist } = options;
1506
1701
  const fileTokens = /* @__PURE__ */ new Map();
1507
1702
  const tokenRules = /* @__PURE__ */ new Map();
1508
1703
  const fileTags = /* @__PURE__ */ new Map();
@@ -1526,6 +1721,10 @@ function kbach(options = {}) {
1526
1721
  fileTokens.set(normPath(filePath), tokens);
1527
1722
  fileTags.set(normPath(filePath), scanUsedTags(code));
1528
1723
  }
1724
+ function removeFile(filePath) {
1725
+ fileTokens.delete(normPath(filePath));
1726
+ fileTags.delete(normPath(filePath));
1727
+ }
1529
1728
  function processSafelist() {
1530
1729
  if (safelist.length === 0) return;
1531
1730
  const tokens = new Set(safelist);
@@ -1549,42 +1748,128 @@ function kbach(options = {}) {
1549
1748
  const rules = tokenRules.get(tok);
1550
1749
  if (rules) view.set(tok, rules);
1551
1750
  }
1751
+ for (const tok of tokenRules.keys()) {
1752
+ if (!active.has(tok)) tokenRules.delete(tok);
1753
+ }
1552
1754
  return formatKbachCSS(view, theme, activeTags());
1553
1755
  }
1756
+ function initialScan() {
1757
+ for (const dir of includeDirs) {
1758
+ scanDir((0, import_path3.join)(root, dir), processFile);
1759
+ }
1760
+ }
1761
+ function scanProjectCssSelectorsOnce() {
1762
+ projectCssClasses = scanProjectCssSelectors(root, includeDirs);
1763
+ }
1764
+ return { processFile, removeFile, initialScan, processSafelist, generateCSS, scanProjectCssSelectorsOnce };
1765
+ }
1766
+ function readSourceFile(path) {
1767
+ return (0, import_fs2.readFileSync)(path, "utf-8");
1768
+ }
1769
+
1770
+ // src/vite-plugin/index.ts
1771
+ function findMainCSSFile(root, includeDirs, explicitCssFile) {
1772
+ if (explicitCssFile) return (0, import_path4.join)(root, explicitCssFile);
1773
+ const searchDirs = [...includeDirs, "."];
1774
+ for (const dir of searchDirs) {
1775
+ const candidate = (0, import_path4.join)(root, dir, "kbach.css");
1776
+ if (!(0, import_fs3.existsSync)(candidate)) continue;
1777
+ try {
1778
+ const content = (0, import_fs3.readFileSync)(candidate, "utf-8");
1779
+ if (content.includes(KBACH_START) && content.includes(KBACH_END)) return candidate;
1780
+ } catch {
1781
+ }
1782
+ }
1783
+ for (const dir of includeDirs) {
1784
+ const full = (0, import_path4.join)(root, dir);
1785
+ try {
1786
+ if ((0, import_fs3.statSync)(full).isDirectory()) return (0, import_path4.join)(full, "kbach.css");
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ return (0, import_path4.join)(root, includeDirs[0] ?? "src", "kbach.css");
1791
+ }
1792
+ var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1793
+ var CSS_SYNC_DEBOUNCE_MS = 50;
1794
+ function writeFileEnsuringDir(path, content) {
1795
+ (0, import_fs3.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
1796
+ (0, import_fs3.writeFileSync)(path, content, "utf-8");
1797
+ }
1798
+ function isEsmRequireFailure(err) {
1799
+ if (!(err instanceof Error)) return false;
1800
+ return err.code === "ERR_REQUIRE_ESM" || /is not defined in ES module scope/.test(err.message);
1801
+ }
1802
+ async function importConfigModule(path) {
1803
+ const mod = await import((0, import_url.pathToFileURL)(path).href);
1804
+ return mod.default ?? mod;
1805
+ }
1806
+ function requireConfigModule(root, path) {
1807
+ const mod = (0, import_module.createRequire)(root)(path);
1808
+ return mod.default ?? mod;
1809
+ }
1810
+ async function loadConfigFile(root) {
1811
+ const cjsPath = (0, import_path4.join)(root, "kbach.config.cjs");
1812
+ if ((0, import_fs3.existsSync)(cjsPath)) return requireConfigModule(root, cjsPath);
1813
+ const mjsPath = (0, import_path4.join)(root, "kbach.config.mjs");
1814
+ if ((0, import_fs3.existsSync)(mjsPath)) return importConfigModule(mjsPath);
1815
+ const jsPath = (0, import_path4.join)(root, "kbach.config.js");
1816
+ if (!(0, import_fs3.existsSync)(jsPath)) return void 0;
1817
+ try {
1818
+ return requireConfigModule(root, jsPath);
1819
+ } catch (err) {
1820
+ if (!isEsmRequireFailure(err)) throw err;
1821
+ return importConfigModule(jsPath);
1822
+ }
1823
+ }
1824
+ async function resolveEffectiveTheme(root, options) {
1825
+ if (options.theme) return options.theme;
1826
+ if (options.config) return resolveKbachConfig(options.config);
1827
+ const discovered = await loadConfigFile(root);
1828
+ return discovered ? resolveKbachConfig(discovered) : defaultTheme;
1829
+ }
1830
+ function kbach(options = {}) {
1831
+ const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1832
+ const safelist = options.safelist ?? [];
1833
+ let root = process.cwd();
1834
+ let theme = defaultTheme;
1835
+ let engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1836
+ let mainCSSFilePath = (0, import_path4.join)(root, "src", "kbach.css");
1554
1837
  function mainCSSFile() {
1555
- return (0, import_path3.join)(root, "src", "kbach.css");
1838
+ return mainCSSFilePath;
1556
1839
  }
1557
1840
  function classNameHintsFile() {
1558
- return (0, import_path3.join)(root, ".kbach", "kbach-classnames.d.ts");
1841
+ return (0, import_path4.join)(root, ".kbach", "kbach-classnames.d.ts");
1559
1842
  }
1560
1843
  function writeClassNameHints() {
1561
1844
  const dts = buildClassNameHintsDts(buildClassTokens(theme));
1562
1845
  writeFileEnsuringDir(classNameHintsFile(), dts);
1563
1846
  }
1564
1847
  function syncMainCSSFile(server) {
1565
- const active = activeTokens();
1566
- for (const tok of tokenRules.keys()) {
1567
- if (!active.has(tok)) tokenRules.delete(tok);
1568
- }
1569
- const changed = writeKbachToFile(mainCSSFile(), generateCSS(), readFile, writeFile);
1848
+ const changed = writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1570
1849
  if (changed && server) server.watcher.emit("change", mainCSSFile());
1571
1850
  }
1572
- function initialScan() {
1573
- for (const dir of includeDirs) {
1574
- scanDir((0, import_path3.join)(root, dir), processFile);
1575
- }
1851
+ let syncTimer = null;
1852
+ function scheduleSync(server) {
1853
+ if (syncTimer) clearTimeout(syncTimer);
1854
+ syncTimer = setTimeout(() => {
1855
+ syncTimer = null;
1856
+ syncMainCSSFile(server);
1857
+ }, CSS_SYNC_DEBOUNCE_MS);
1576
1858
  }
1577
1859
  return {
1578
1860
  name: "kbach",
1579
1861
  enforce: "pre",
1580
- configResolved(resolved) {
1862
+ async configResolved(resolved) {
1581
1863
  root = resolved.root;
1864
+ theme = await resolveEffectiveTheme(root, options);
1865
+ engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1866
+ mainCSSFilePath = findMainCSSFile(root, includeDirs, options.cssFile);
1582
1867
  },
1583
1868
  buildStart() {
1584
- projectCssClasses = scanProjectCssSelectors(root, includeDirs);
1585
- initialScan();
1586
- processSafelist();
1587
- writeKbachToFile(mainCSSFile(), generateCSS(), readFile, writeFile);
1869
+ engine.scanProjectCssSelectorsOnce();
1870
+ engine.initialScan();
1871
+ engine.processSafelist();
1872
+ writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1588
1873
  writeClassNameHints();
1589
1874
  },
1590
1875
  // handleHotUpdate covers edits; add/unlink (create/delete) land on the
@@ -1595,17 +1880,15 @@ function kbach(options = {}) {
1595
1880
  if (file.includes("node_modules")) return;
1596
1881
  if (!/\.(tsx?|jsx?)$/.test(file)) return;
1597
1882
  if (isUnlink) {
1598
- fileTokens.delete(normPath(file));
1599
- fileTags.delete(normPath(file));
1883
+ engine.removeFile(file);
1600
1884
  } else {
1601
1885
  try {
1602
- processFile(file, readFile(file));
1886
+ engine.processFile(file, readSourceFile(file));
1603
1887
  } catch {
1604
- fileTokens.delete(normPath(file));
1605
- fileTags.delete(normPath(file));
1888
+ engine.removeFile(file);
1606
1889
  }
1607
1890
  }
1608
- syncMainCSSFile(server);
1891
+ scheduleSync(server);
1609
1892
  };
1610
1893
  server.watcher.on("add", (file) => onAddOrUnlink(file, false));
1611
1894
  server.watcher.on("unlink", (file) => onAddOrUnlink(file, true));
@@ -1614,16 +1897,20 @@ function kbach(options = {}) {
1614
1897
  if (file.includes("node_modules")) return;
1615
1898
  if (!/\.(tsx?|jsx?)$/.test(file)) return;
1616
1899
  try {
1617
- processFile(file, readFile(file));
1900
+ engine.processFile(file, readSourceFile(file));
1618
1901
  } catch {
1619
- fileTokens.delete(normPath(file));
1620
- fileTags.delete(normPath(file));
1902
+ engine.removeFile(file);
1621
1903
  }
1622
- syncMainCSSFile(server);
1904
+ scheduleSync(server);
1623
1905
  }
1624
1906
  };
1625
1907
  }
1626
1908
  // Annotate the CommonJS export names for ESM import in node:
1627
1909
  0 && (module.exports = {
1628
- kbach
1910
+ CSS_SYNC_DEBOUNCE_MS,
1911
+ DEFAULT_SCAN_DIRS,
1912
+ findMainCSSFile,
1913
+ kbach,
1914
+ loadConfigFile,
1915
+ resolveEffectiveTheme
1629
1916
  });