@kbach/react 1.0.0-beta.13 → 1.0.0-beta.15

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/README.md CHANGED
@@ -188,56 +188,64 @@ live at your project root (e.g. a monorepo app).
188
188
 
189
189
  ## Dark mode
190
190
 
191
- By default, `dark:` classes follow the OS `prefers-color-scheme` setting via
192
- plain CSS — zero config, zero JS required, works even if your app never
193
- imports anything from `@kbach/react`'s runtime:
191
+ **Following the OS setting — zero setup.** `dark:` classes already work,
192
+ via plain CSS, with nothing to configure:
194
193
 
195
194
  ```tsx
196
195
  <div className="bg-white dark:bg-neutral-12">...</div>
197
196
  ```
198
197
 
199
- This default (`darkMode: 'media'`) can't be overridden by an in-app toggle —
200
- `@media (prefers-color-scheme)` only ever reflects the real OS setting. To
201
- add a manual light/dark switcher, opt into `darkMode: 'class'` or
202
- `'attribute'` (writes `.dark`/`data-theme` onto `<html>` instead) — this
203
- needs setting in **two places**, since they solve two different problems:
204
-
205
- 1. **Build-time**, via `kbach.config.js` or the plugin's `config`/`theme`
206
- option — controls which selector the generated CSS for `dark:` classes
207
- compiles to (`[data-theme="dark"] .dark\:bg-neutral-12` vs
208
- `.dark .dark\:bg-neutral-12` vs a plain `@media` query).
209
- 2. **Runtime**, via `applyKbachConfig()` — controls what `useTheme()`'s
210
- `toggle()`/`setMode()` actually write to the DOM. This is a *separate*
211
- module from the build step above (it has to be: the plugin runs in
212
- Node during your build, this runs in the browser) — setting only the
213
- plugin option compiles the right CSS but leaves the toggle a silent
214
- no-op, since the runtime still thinks the strategy is the `'media'`
215
- default and has nothing to write to the DOM for it.
198
+ **A manual light/dark toggle — one file, no other setup.** `@media
199
+ (prefers-color-scheme)` (the default above) can't be flipped by a button —
200
+ it only ever reflects the OS. For a real toggle, add one file:
216
201
 
217
202
  ```js
218
- // kbach.config.js — build-time, read by the Vite/PostCSS plugin
203
+ // kbach.config.js — at your project root
219
204
  export default { darkMode: 'attribute' };
220
205
  ```
221
206
 
222
- ```tsx
223
- // app entry (e.g. root.tsx / main.tsx) — runtime, same value as above
224
- import { applyKbachConfig, useTheme } from '@kbach/react';
207
+ That's it. Don't pass anything to `kbach()`/`'@kbach/react/postcss'` and
208
+ don't call anything at app startup — both sides pick this up on their own
209
+ (the plugin auto-discovers the file; the runtime reads the strategy back
210
+ out of the CSS the plugin already generated). Then just use `useTheme()`:
225
211
 
226
- applyKbachConfig({ darkMode: 'attribute' }); // call once, before first render
212
+ ```tsx
213
+ import { useTheme } from '@kbach/react';
227
214
 
228
215
  function ThemeToggle() {
229
- const { mode, isDark, setMode, toggle } = useTheme();
216
+ const { mode, toggle } = useTheme();
230
217
  return <button onClick={toggle}>{mode}</button>;
231
218
  }
232
219
  ```
233
220
 
234
- Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
235
- optional, for seeding a startup default. Outside React (plain functions,
236
- event handlers), use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
237
- Note: `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
238
- under the default `'media'` strategy too (and even without ever calling
239
- `applyKbachConfig()`) — they just won't visibly affect `dark:` classes
240
- unless the runtime has also been told about `'class'`/`'attribute'`.
221
+ Works anywhere in the tree, no provider required. `<ThemeProvider
222
+ defaultMode="dark">` is optional, only for seeding a startup default.
223
+ Outside React, use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` directly.
224
+
225
+ <details>
226
+ <summary>Not using the static plugin, or need to set the strategy explicitly?</summary>
227
+
228
+ There's nothing for the runtime to auto-detect if no build step ever
229
+ generates `dark:` CSS (a pure runtime `kb()`-only setup) — call
230
+ `applyKbachConfig()` yourself once, before your first render, instead:
231
+
232
+ ```tsx
233
+ import { applyKbachConfig } from '@kbach/react';
234
+
235
+ applyKbachConfig({ darkMode: 'attribute' });
236
+ ```
237
+
238
+ An explicit call like this always wins over whatever the CSS auto-detect
239
+ found, if the two ever disagree. If you do reach for it alongside the
240
+ plugin, import the same `kbach.config.js` rather than retyping the value —
241
+ `import kbachConfig from '../kbach.config.js'; applyKbachConfig(kbachConfig);`.
242
+
243
+ `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly under
244
+ the default `'media'` strategy too, with none of the above — they just
245
+ won't visibly affect `dark:` classes until the runtime knows about
246
+ `'class'`/`'attribute'` one way or another.
247
+
248
+ </details>
241
249
 
242
250
  ## Theming
243
251
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  kb
3
- } from "./chunk-U4FGKMNH.mjs";
3
+ } from "./chunk-KJY3W4GF.mjs";
4
4
 
5
5
  // src/jsx-runtime.tsx
6
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -2,7 +2,7 @@ import {
2
2
  BASE_RESET,
3
3
  RESET_STYLE_ID,
4
4
  getThemeJson
5
- } from "./chunk-EOSN5CYG.mjs";
5
+ } from "./chunk-OVKVZA7B.mjs";
6
6
 
7
7
  // src/wasmLoader.ts
8
8
  import init, { generate_css as wasmGenerateCss } from "@kbach/core-engine";
@@ -377,6 +377,13 @@ function getTheme() {
377
377
  function getThemeJson() {
378
378
  return activeThemeJson;
379
379
  }
380
+ var DARK_MODE_STRATEGIES = /* @__PURE__ */ new Set(["attribute", "class", "media"]);
381
+ function detectDarkModeStrategyFromDom() {
382
+ if (typeof document === "undefined") return;
383
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--kb-dark-mode").trim();
384
+ if (!DARK_MODE_STRATEGIES.has(raw) || raw === activeTheme.darkMode) return;
385
+ setTheme({ ...activeTheme, darkMode: raw });
386
+ }
380
387
 
381
388
  // src/reset.ts
382
389
  var RESET_STYLE_ID = "kbach-reset";
@@ -467,6 +474,7 @@ export {
467
474
  setTheme,
468
475
  getTheme,
469
476
  getThemeJson,
477
+ detectDarkModeStrategyFromDom,
470
478
  RESET_STYLE_ID,
471
479
  BASE_RESET,
472
480
  buildResetCSS
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  defaultTheme,
3
+ detectDarkModeStrategyFromDom,
3
4
  getTheme,
4
5
  setTheme
5
- } from "./chunk-EOSN5CYG.mjs";
6
+ } from "./chunk-OVKVZA7B.mjs";
6
7
 
7
8
  // src/darkModeStore.ts
8
9
  var STORAGE_KEY = "kbach-theme";
@@ -31,13 +32,15 @@ function applyToDom(dark) {
31
32
  if (typeof document === "undefined") return;
32
33
  const root = document.documentElement;
33
34
  const strategy = getTheme().darkMode;
34
- if (strategy === "class") {
35
- root.classList.toggle("dark", dark);
36
- root.classList.toggle("light", !dark);
37
- } else if (strategy === "attribute") {
35
+ root.classList.toggle("dark", strategy === "class" && dark);
36
+ root.classList.toggle("light", strategy === "class" && !dark);
37
+ if (strategy === "attribute") {
38
38
  root.setAttribute("data-theme", dark ? "dark" : "light");
39
+ } else {
40
+ root.removeAttribute("data-theme");
39
41
  }
40
42
  }
43
+ detectDarkModeStrategyFromDom();
41
44
  var persistedMode = readPersistedMode();
42
45
  var hadPersistedMode = persistedMode !== null;
43
46
  var mode = persistedMode ?? "system";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  resolveKbachConfig
3
- } from "./chunk-77PKNQ2K.mjs";
3
+ } from "./chunk-OWWJ2PME.mjs";
4
4
  import {
5
5
  buildResetCSS,
6
6
  defaultTheme
7
- } from "./chunk-EOSN5CYG.mjs";
7
+ } from "./chunk-OVKVZA7B.mjs";
8
8
 
9
9
  // src/vite-plugin/index.ts
10
10
  import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync, statSync as statSync2 } from "fs";
@@ -181,11 +181,9 @@ function formatKbachCSS(tokenRules, theme, usedTags) {
181
181
  const spacingMap = buildSpacingVarMap(theme, compact);
182
182
  const { text, declarations } = applyVarMap(compact, colorMap, spacingMap);
183
183
  const lines = ["/* Generated by Kbach \u2014 do not edit */", "", buildResetCSS(usedTags)];
184
- if (declarations.size > 0) {
185
- lines.push("", ":root {");
186
- for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
187
- lines.push("}");
188
- }
184
+ lines.push("", ":root {", ` --kb-dark-mode: ${theme.darkMode};`);
185
+ for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
186
+ lines.push("}");
189
187
  lines.push("", prettifyCSS(text));
190
188
  return lines.join("\n");
191
189
  }
package/dist/index.js CHANGED
@@ -455,6 +455,13 @@ function getTheme() {
455
455
  function getThemeJson() {
456
456
  return activeThemeJson;
457
457
  }
458
+ var DARK_MODE_STRATEGIES = /* @__PURE__ */ new Set(["attribute", "class", "media"]);
459
+ function detectDarkModeStrategyFromDom() {
460
+ if (typeof document === "undefined") return;
461
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--kb-dark-mode").trim();
462
+ if (!DARK_MODE_STRATEGIES.has(raw) || raw === activeTheme.darkMode) return;
463
+ setTheme({ ...activeTheme, darkMode: raw });
464
+ }
458
465
 
459
466
  // src/reset.ts
460
467
  var RESET_STYLE_ID = "kbach-reset";
@@ -697,13 +704,15 @@ function applyToDom(dark) {
697
704
  if (typeof document === "undefined") return;
698
705
  const root = document.documentElement;
699
706
  const strategy = getTheme().darkMode;
700
- if (strategy === "class") {
701
- root.classList.toggle("dark", dark);
702
- root.classList.toggle("light", !dark);
703
- } else if (strategy === "attribute") {
707
+ root.classList.toggle("dark", strategy === "class" && dark);
708
+ root.classList.toggle("light", strategy === "class" && !dark);
709
+ if (strategy === "attribute") {
704
710
  root.setAttribute("data-theme", dark ? "dark" : "light");
711
+ } else {
712
+ root.removeAttribute("data-theme");
705
713
  }
706
714
  }
715
+ detectDarkModeStrategyFromDom();
707
716
  var persistedMode = readPersistedMode();
708
717
  var hadPersistedMode = persistedMode !== null;
709
718
  var mode = persistedMode ?? "system";
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-U4FGKMNH.mjs";
7
+ } from "./chunk-KJY3W4GF.mjs";
8
8
  import {
9
9
  applyKbachConfig,
10
10
  ensureColorVariablesInjected,
@@ -16,14 +16,14 @@ import {
16
16
  setGlobalThemeMode,
17
17
  subscribeGlobalDarkMode,
18
18
  toggleGlobalDarkMode
19
- } from "./chunk-77PKNQ2K.mjs";
19
+ } from "./chunk-OWWJ2PME.mjs";
20
20
  import {
21
21
  BASE_RESET,
22
22
  RESET_STYLE_ID,
23
23
  defaultTheme,
24
24
  getTheme,
25
25
  setTheme
26
- } from "./chunk-EOSN5CYG.mjs";
26
+ } from "./chunk-OVKVZA7B.mjs";
27
27
 
28
28
  // src/KbachReset.tsx
29
29
  import { jsx } from "react/jsx-runtime";
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-7G7TMQL6.mjs";
6
- import "./chunk-U4FGKMNH.mjs";
7
- import "./chunk-EOSN5CYG.mjs";
5
+ } from "./chunk-D7OC2AE7.mjs";
6
+ import "./chunk-KJY3W4GF.mjs";
7
+ import "./chunk-OVKVZA7B.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-7G7TMQL6.mjs";
6
- import "./chunk-U4FGKMNH.mjs";
7
- import "./chunk-EOSN5CYG.mjs";
5
+ } from "./chunk-D7OC2AE7.mjs";
6
+ import "./chunk-KJY3W4GF.mjs";
7
+ import "./chunk-OVKVZA7B.mjs";
8
8
  export {
9
9
  Fragment,
10
10
  jsx,
@@ -476,11 +476,9 @@ function formatKbachCSS(tokenRules, theme, usedTags) {
476
476
  const spacingMap = buildSpacingVarMap(theme, compact);
477
477
  const { text, declarations } = applyVarMap(compact, colorMap, spacingMap);
478
478
  const lines = ["/* Generated by Kbach \u2014 do not edit */", "", buildResetCSS(usedTags)];
479
- if (declarations.size > 0) {
480
- lines.push("", ":root {");
481
- for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
482
- lines.push("}");
483
- }
479
+ lines.push("", ":root {", ` --kb-dark-mode: ${theme.darkMode};`);
480
+ for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
481
+ lines.push("}");
484
482
  lines.push("", prettifyCSS(text));
485
483
  return lines.join("\n");
486
484
  }
@@ -990,9 +988,20 @@ var defaultTheme = {
990
988
  };
991
989
  var activeTheme = defaultTheme;
992
990
  var activeThemeJson = JSON.stringify(defaultTheme);
991
+ function setTheme(theme) {
992
+ activeTheme = theme;
993
+ activeThemeJson = JSON.stringify(theme);
994
+ }
993
995
  function getTheme() {
994
996
  return activeTheme;
995
997
  }
998
+ var DARK_MODE_STRATEGIES = /* @__PURE__ */ new Set(["attribute", "class", "media"]);
999
+ function detectDarkModeStrategyFromDom() {
1000
+ if (typeof document === "undefined") return;
1001
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--kb-dark-mode").trim();
1002
+ if (!DARK_MODE_STRATEGIES.has(raw) || raw === activeTheme.darkMode) return;
1003
+ setTheme({ ...activeTheme, darkMode: raw });
1004
+ }
996
1005
 
997
1006
  // src/colorVariables.ts
998
1007
  function parseHexRgb(hex) {
@@ -1031,13 +1040,15 @@ function applyToDom(dark) {
1031
1040
  if (typeof document === "undefined") return;
1032
1041
  const root = document.documentElement;
1033
1042
  const strategy = getTheme().darkMode;
1034
- if (strategy === "class") {
1035
- root.classList.toggle("dark", dark);
1036
- root.classList.toggle("light", !dark);
1037
- } else if (strategy === "attribute") {
1043
+ root.classList.toggle("dark", strategy === "class" && dark);
1044
+ root.classList.toggle("light", strategy === "class" && !dark);
1045
+ if (strategy === "attribute") {
1038
1046
  root.setAttribute("data-theme", dark ? "dark" : "light");
1047
+ } else {
1048
+ root.removeAttribute("data-theme");
1039
1049
  }
1040
1050
  }
1051
+ detectDarkModeStrategyFromDom();
1041
1052
  var persistedMode = readPersistedMode();
1042
1053
  var mode = persistedMode ?? "system";
1043
1054
  var isDark = resolveIsDark(mode);
@@ -2,9 +2,9 @@ import {
2
2
  DEFAULT_SCAN_DIRS,
3
3
  createKbachStaticCssEngine,
4
4
  resolveEffectiveTheme
5
- } from "./chunk-FJUZ2ZYT.mjs";
6
- import "./chunk-77PKNQ2K.mjs";
7
- import "./chunk-EOSN5CYG.mjs";
5
+ } from "./chunk-XV2ZLURB.mjs";
6
+ import "./chunk-OWWJ2PME.mjs";
7
+ import "./chunk-OVKVZA7B.mjs";
8
8
 
9
9
  // src/postcss-plugin/index.ts
10
10
  import postcss from "postcss";
@@ -285,11 +285,9 @@ function formatKbachCSS(tokenRules, theme, usedTags) {
285
285
  const spacingMap = buildSpacingVarMap(theme, compact);
286
286
  const { text, declarations } = applyVarMap(compact, colorMap, spacingMap);
287
287
  const lines = ["/* Generated by Kbach \u2014 do not edit */", "", buildResetCSS(usedTags)];
288
- if (declarations.size > 0) {
289
- lines.push("", ":root {");
290
- for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
291
- lines.push("}");
292
- }
288
+ lines.push("", ":root {", ` --kb-dark-mode: ${theme.darkMode};`);
289
+ for (const [varName, val] of declarations) lines.push(` ${varName}: ${val};`);
290
+ lines.push("}");
293
291
  lines.push("", prettifyCSS(text));
294
292
  return lines.join("\n");
295
293
  }
@@ -1384,9 +1382,20 @@ var defaultTheme = {
1384
1382
  };
1385
1383
  var activeTheme = defaultTheme;
1386
1384
  var activeThemeJson = JSON.stringify(defaultTheme);
1385
+ function setTheme(theme) {
1386
+ activeTheme = theme;
1387
+ activeThemeJson = JSON.stringify(theme);
1388
+ }
1387
1389
  function getTheme() {
1388
1390
  return activeTheme;
1389
1391
  }
1392
+ var DARK_MODE_STRATEGIES = /* @__PURE__ */ new Set(["attribute", "class", "media"]);
1393
+ function detectDarkModeStrategyFromDom() {
1394
+ if (typeof document === "undefined") return;
1395
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--kb-dark-mode").trim();
1396
+ if (!DARK_MODE_STRATEGIES.has(raw) || raw === activeTheme.darkMode) return;
1397
+ setTheme({ ...activeTheme, darkMode: raw });
1398
+ }
1390
1399
 
1391
1400
  // src/colorVariables.ts
1392
1401
  function parseHexRgb(hex) {
@@ -1425,13 +1434,15 @@ function applyToDom(dark) {
1425
1434
  if (typeof document === "undefined") return;
1426
1435
  const root = document.documentElement;
1427
1436
  const strategy = getTheme().darkMode;
1428
- if (strategy === "class") {
1429
- root.classList.toggle("dark", dark);
1430
- root.classList.toggle("light", !dark);
1431
- } else if (strategy === "attribute") {
1437
+ root.classList.toggle("dark", strategy === "class" && dark);
1438
+ root.classList.toggle("light", strategy === "class" && !dark);
1439
+ if (strategy === "attribute") {
1432
1440
  root.setAttribute("data-theme", dark ? "dark" : "light");
1441
+ } else {
1442
+ root.removeAttribute("data-theme");
1433
1443
  }
1434
1444
  }
1445
+ detectDarkModeStrategyFromDom();
1435
1446
  var persistedMode = readPersistedMode();
1436
1447
  var mode = persistedMode ?? "system";
1437
1448
  var isDark = resolveIsDark(mode);
@@ -5,9 +5,9 @@ import {
5
5
  kbach,
6
6
  loadConfigFile,
7
7
  resolveEffectiveTheme
8
- } from "./chunk-FJUZ2ZYT.mjs";
9
- import "./chunk-77PKNQ2K.mjs";
10
- import "./chunk-EOSN5CYG.mjs";
8
+ } from "./chunk-XV2ZLURB.mjs";
9
+ import "./chunk-OWWJ2PME.mjs";
10
+ import "./chunk-OVKVZA7B.mjs";
11
11
  export {
12
12
  CSS_SYNC_DEBOUNCE_MS,
13
13
  DEFAULT_SCAN_DIRS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "1.0.0-beta.13",
3
+ "version": "1.0.0-beta.15",
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": ["css", "react", "tailwind", "utility-first", "wasm", "styling"],
6
6
  "license": "MIT",