@kbach/react 1.0.0-beta.23 → 1.0.0-beta.25

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.
package/AGENTS.md ADDED
@@ -0,0 +1,129 @@
1
+ # `@kbach/react` — reference for AI assistants
2
+
3
+ The web binding of [Kbach](https://github.com/semsakadanupol/kbach) — the
4
+ same Tailwind-v4-shaped class-name vocabulary as `@kbach/react-native`,
5
+ resolved at **build time** into a real CSS file via a shared Rust/WASM
6
+ core. This file covers `@kbach/react` only; for the cross-package
7
+ overview, `kbach.config.js` reference, the color palette, and how the
8
+ underlying engine is built, see the repo root's
9
+ [AGENTS.md](https://github.com/semsakadanupol/kbach/blob/main/AGENTS.md).
10
+
11
+ ---
12
+
13
+ ## 1. Install + wire up the build
14
+
15
+ ```sh
16
+ npm install @kbach/react@beta
17
+ ```
18
+
19
+ **Vite:**
20
+
21
+ ```ts
22
+ // vite.config.ts
23
+ import { defineConfig } from 'vite';
24
+ import react from '@vitejs/plugin-react';
25
+ import { kbach } from '@kbach/react/vite';
26
+
27
+ export default defineConfig({ plugins: [kbach(), react()] });
28
+ ```
29
+
30
+ **Next.js / any PostCSS bundler:**
31
+
32
+ ```js
33
+ // postcss.config.js
34
+ module.exports = { plugins: { '@kbach/react/postcss': {} } };
35
+ ```
36
+
37
+ ```css
38
+ /* globals.css — the plugin fills between these markers */
39
+ /* kbach:start */
40
+ /* kbach:end */
41
+ ```
42
+
43
+ Then import the generated CSS once from your entry point
44
+ (`import './kbach.css'` for Vite; `import './globals.css'` for PostCSS) and
45
+ write `className="..."` normally.
46
+
47
+ ## 2. How resolution works
48
+
49
+ 1. At build time the plugin walks your `include` dirs (`src`, `app`,
50
+ `pages`, `components` by default), extracts every string that looks like
51
+ a class list, and hands each to the WASM engine.
52
+ 2. The engine parses each class → emits CSS rules (with the correct
53
+ `@media` / selector wrappers for modifiers).
54
+ 3. Rules are written between the `/* kbach:start */` / `/* kbach:end */`
55
+ markers of a `kbach.css` file. The plugin finds an existing marked file
56
+ under your source dirs or the project root, else creates one; pass
57
+ `cssFile` to pin the path.
58
+ 4. At runtime `className` is a plain string prop — **nothing calls the
59
+ engine**, the CSS already exists. No async init.
60
+
61
+ ## 3. Runtime pieces (opt-in, these DO call the engine)
62
+
63
+ - **`kb(dynamicClassString)`** — resolve a class list assembled from
64
+ runtime data (`kb(\`bg-${color}-6\`)`). Requires `await initKbach()` once
65
+ before first use. Prefer a `safelist: [...]` plugin entry when the
66
+ possible values are known ahead of time (stays fully static).
67
+ - **`@kbach/react/jsx-runtime`** — a `jsxImportSource` override that
68
+ intercepts *every* element's `className` at runtime instead of relying on
69
+ the static scan. Also needs `await initKbach()`.
70
+
71
+ ## 4. Dark mode
72
+
73
+ `dark:` classes work through plain CSS with zero setup, following the OS
74
+ (`@media (prefers-color-scheme)`).
75
+
76
+ For a **manual toggle**, add one line to `kbach.config.js`:
77
+
78
+ ```js
79
+ export default { darkMode: 'attribute' }; // or 'class'
80
+ ```
81
+
82
+ Nothing else — the plugin auto-discovers the file, and the runtime reads
83
+ the strategy back out of the generated CSS. Then:
84
+
85
+ ```tsx
86
+ import { useTheme } from '@kbach/react';
87
+ const { mode, isDark, toggle, setMode } = useTheme();
88
+ ```
89
+
90
+ No provider needed (`useTheme` reads a global store). `<ThemeProvider
91
+ defaultMode="dark">` only seeds a startup default. Outside React:
92
+ `getGlobalDarkMode()` / `toggleGlobalDarkMode()` / `setGlobalThemeMode()`.
93
+
94
+ Pure-runtime (`kb()`-only, no build step): call `applyKbachConfig({
95
+ darkMode: 'attribute' })` yourself once before first render.
96
+
97
+ ## 5. Plugin options (`KbachPluginOptions`)
98
+
99
+ `root`, `theme` (a resolved `ThemeConfig`), `config` (a `KbachConfig`, same
100
+ shape as `kbach.config.js` — see the root AGENTS.md's config reference),
101
+ `include` (dirs to scan), `safelist` (classes to always emit), `cssFile`
102
+ (explicit output path). PostCSS: `root` defaults to `process.cwd()`; pass
103
+ it if `postcss.config.js` isn't at the project root.
104
+
105
+ ## 6. Exports
106
+
107
+ `initKbach`, `isKbachReady`, `kb`, `disableRuntimeCSS`,
108
+ `isRuntimeCSSDisabled`, `setTheme`, `getTheme`, `defaultTheme`,
109
+ `KbachReset`, `resolveKbachConfig`, `applyKbachConfig`, `useTheme`,
110
+ `ThemeProvider`, `useColors`, `clsx`, `getGlobalDarkMode`,
111
+ `getGlobalThemeMode`, `setGlobalThemeMode`, `toggleGlobalDarkMode`,
112
+ `subscribeGlobalDarkMode`. Types: `ThemeConfig`, `ColorEntry`,
113
+ `ContainerConfig`, `DarkModeStrategy`, `KbachConfig`, `ThemeState`,
114
+ `ThemeProviderProps`, `ThemeMode`, `ColorsAPI`, `ColorScale`, `ClassValue`.
115
+
116
+ ## 7. Common failure modes
117
+
118
+ - **Build fails after adding/editing `kbach.config.js`** — an unquoted
119
+ hyphenated object key (`surface-dim:` instead of `'surface-dim':`) is a
120
+ syntax error; `loadConfigFile` (vite-plugin/index.ts) re-throws it
121
+ rather than silently falling back to defaults, so the whole build fails
122
+ loudly. Quote the key.
123
+ - **A dynamic `kb(...)` call returns nothing / throws** — `initKbach()`
124
+ hasn't resolved yet. It's async; await it once before the first `kb()`
125
+ call, or prefer a static `safelist` entry when the value set is known
126
+ ahead of time.
127
+ - **A class works in one file but not another** — it's outside every
128
+ `include` dir the plugin scans (default: `src`, `app`, `pages`,
129
+ `components`). Add the directory via the plugin's `include` option.
@@ -299,11 +299,28 @@ function splitClassTokens(str) {
299
299
  return str.split(/\s+/).filter(Boolean);
300
300
  }
301
301
  var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
302
+ function hasUnbracketedParen(token) {
303
+ let bracketDepth = 0;
304
+ for (const ch of token) {
305
+ if (ch === "[") bracketDepth++;
306
+ else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
307
+ else if ((ch === "(" || ch === ")") && bracketDepth === 0) return true;
308
+ }
309
+ return false;
310
+ }
302
311
  function pushTokens(str, into) {
303
312
  for (const tok of splitClassTokens(str)) {
304
- if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
313
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok) && !hasUnbracketedParen(tok)) into.add(tok);
305
314
  }
306
315
  }
316
+ var JSX_CHILD_CONTEXT_WINDOW = 40;
317
+ var JSX_CHILD_BEFORE_RE = />\s*\{\s*$/;
318
+ var JSX_CHILD_AFTER_RE = /^\s*\}\s*</;
319
+ function isJsxExpressionChildTemplateLiteral(code, matchStart, matchEnd) {
320
+ const before = code.slice(Math.max(0, matchStart - JSX_CHILD_CONTEXT_WINDOW), matchStart);
321
+ const after = code.slice(matchEnd, matchEnd + JSX_CHILD_CONTEXT_WINDOW);
322
+ return JSX_CHILD_BEFORE_RE.test(before) && JSX_CHILD_AFTER_RE.test(after);
323
+ }
307
324
  function pushTemplateLiteralBody(body, into) {
308
325
  const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
309
326
  let im;
@@ -374,7 +391,10 @@ function extractClassStrings(rawCode) {
374
391
  if (depth === 0) pushClassLikeStrings(block, found);
375
392
  }
376
393
  const templateRe = /`([^`]{1,2000})`/g;
377
- while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
394
+ while ((m = templateRe.exec(code)) !== null) {
395
+ if (isJsxExpressionChildTemplateLiteral(code, m.index, templateRe.lastIndex)) continue;
396
+ pushTemplateLiteralBody(m[1], found);
397
+ }
378
398
  return [...found];
379
399
  }
380
400
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
@@ -128,11 +128,28 @@ function splitClassTokens(str) {
128
128
  return str.split(/\s+/).filter(Boolean);
129
129
  }
130
130
  var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
131
+ function hasUnbracketedParen(token) {
132
+ let bracketDepth = 0;
133
+ for (const ch of token) {
134
+ if (ch === "[") bracketDepth++;
135
+ else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
136
+ else if ((ch === "(" || ch === ")") && bracketDepth === 0) return true;
137
+ }
138
+ return false;
139
+ }
131
140
  function pushTokens(str, into) {
132
141
  for (const tok of splitClassTokens(str)) {
133
- if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
142
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok) && !hasUnbracketedParen(tok)) into.add(tok);
134
143
  }
135
144
  }
145
+ var JSX_CHILD_CONTEXT_WINDOW = 40;
146
+ var JSX_CHILD_BEFORE_RE = />\s*\{\s*$/;
147
+ var JSX_CHILD_AFTER_RE = /^\s*\}\s*</;
148
+ function isJsxExpressionChildTemplateLiteral(code, matchStart, matchEnd) {
149
+ const before = code.slice(Math.max(0, matchStart - JSX_CHILD_CONTEXT_WINDOW), matchStart);
150
+ const after = code.slice(matchEnd, matchEnd + JSX_CHILD_CONTEXT_WINDOW);
151
+ return JSX_CHILD_BEFORE_RE.test(before) && JSX_CHILD_AFTER_RE.test(after);
152
+ }
136
153
  function pushTemplateLiteralBody(body, into) {
137
154
  const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
138
155
  let im;
@@ -203,7 +220,10 @@ function extractClassStrings(rawCode) {
203
220
  if (depth === 0) pushClassLikeStrings(block, found);
204
221
  }
205
222
  const templateRe = /`([^`]{1,2000})`/g;
206
- while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
223
+ while ((m = templateRe.exec(code)) !== null) {
224
+ if (isJsxExpressionChildTemplateLiteral(code, m.index, templateRe.lastIndex)) continue;
225
+ pushTemplateLiteralBody(m[1], found);
226
+ }
207
227
  return [...found];
208
228
  }
209
229
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
@@ -1251,3 +1271,5 @@ function kbachPostcss(options = {}) {
1251
1271
  };
1252
1272
  }
1253
1273
  kbachPostcss.postcss = true;
1274
+
1275
+ module.exports = module.exports.default;
@@ -6,7 +6,7 @@ import {
6
6
  highlight,
7
7
  kbachTag,
8
8
  resolveEffectiveTheme
9
- } from "./chunk-QYTXXF22.mjs";
9
+ } from "./chunk-KTRJZDVE.mjs";
10
10
  import "./chunk-43FOLRQA.mjs";
11
11
  import "./chunk-OVKVZA7B.mjs";
12
12
 
@@ -919,11 +919,28 @@ function splitClassTokens(str) {
919
919
  return str.split(/\s+/).filter(Boolean);
920
920
  }
921
921
  var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
922
+ function hasUnbracketedParen(token) {
923
+ let bracketDepth = 0;
924
+ for (const ch of token) {
925
+ if (ch === "[") bracketDepth++;
926
+ else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
927
+ else if ((ch === "(" || ch === ")") && bracketDepth === 0) return true;
928
+ }
929
+ return false;
930
+ }
922
931
  function pushTokens(str, into) {
923
932
  for (const tok of splitClassTokens(str)) {
924
- if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
933
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok) && !hasUnbracketedParen(tok)) into.add(tok);
925
934
  }
926
935
  }
936
+ var JSX_CHILD_CONTEXT_WINDOW = 40;
937
+ var JSX_CHILD_BEFORE_RE = />\s*\{\s*$/;
938
+ var JSX_CHILD_AFTER_RE = /^\s*\}\s*</;
939
+ function isJsxExpressionChildTemplateLiteral(code, matchStart, matchEnd) {
940
+ const before = code.slice(Math.max(0, matchStart - JSX_CHILD_CONTEXT_WINDOW), matchStart);
941
+ const after = code.slice(matchEnd, matchEnd + JSX_CHILD_CONTEXT_WINDOW);
942
+ return JSX_CHILD_BEFORE_RE.test(before) && JSX_CHILD_AFTER_RE.test(after);
943
+ }
927
944
  function pushTemplateLiteralBody(body, into) {
928
945
  const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
929
946
  let im;
@@ -994,7 +1011,10 @@ function extractClassStrings(rawCode) {
994
1011
  if (depth === 0) pushClassLikeStrings(block, found);
995
1012
  }
996
1013
  const templateRe = /`([^`]{1,2000})`/g;
997
- while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
1014
+ while ((m = templateRe.exec(code)) !== null) {
1015
+ if (isJsxExpressionChildTemplateLiteral(code, m.index, templateRe.lastIndex)) continue;
1016
+ pushTemplateLiteralBody(m[1], found);
1017
+ }
998
1018
  return [...found];
999
1019
  }
1000
1020
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".output", ".git", ".svn", ".cache", "coverage"]);
@@ -5,7 +5,7 @@ import {
5
5
  kbach,
6
6
  loadConfigFile,
7
7
  resolveEffectiveTheme
8
- } from "./chunk-QYTXXF22.mjs";
8
+ } from "./chunk-KTRJZDVE.mjs";
9
9
  import "./chunk-43FOLRQA.mjs";
10
10
  import "./chunk-OVKVZA7B.mjs";
11
11
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "1.0.0-beta.23",
3
+ "version": "1.0.0-beta.25",
4
4
  "description": "A utility-first styling engine for React, powered by a real Rust/WASM core — Tailwind CSS v4 feature parity with Kbach's own color palette.",
5
5
  "keywords": [
6
6
  "css",
@@ -25,7 +25,8 @@
25
25
  "access": "public"
26
26
  },
27
27
  "files": [
28
- "dist"
28
+ "dist",
29
+ "AGENTS.md"
29
30
  ],
30
31
  "source": "./src/index.ts",
31
32
  "main": "./dist/index.js",
@@ -64,7 +65,7 @@
64
65
  }
65
66
  },
66
67
  "scripts": {
67
- "build": "tsup",
68
+ "build": "tsup && node scripts/fix-postcss-cjs-interop.mjs",
68
69
  "dev": "tsup --watch",
69
70
  "test": "vitest run --passWithNoTests",
70
71
  "test:coverage": "vitest run --coverage --passWithNoTests",