@kbach/react 1.0.0-beta.12 → 1.0.0-beta.14

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
@@ -199,31 +199,24 @@ imports anything from `@kbach/react`'s runtime:
199
199
  This default (`darkMode: 'media'`) can't be overridden by an in-app toggle —
200
200
  `@media (prefers-color-scheme)` only ever reflects the real OS setting. To
201
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.
202
+ `'attribute'` (writes `.dark`/`data-theme` onto `<html>` instead):
216
203
 
217
204
  ```js
218
- // kbach.config.js — build-time, read by the Vite/PostCSS plugin
205
+ // kbach.config.js — a plain object, auto-discovered by the plugin at your
206
+ // project root, no import needed in vite.config.ts/postcss.config.js
219
207
  export default { darkMode: 'attribute' };
220
208
  ```
221
209
 
222
- ```tsx
223
- // app entry (e.g. root.tsx / main.tsx) — runtime, same value as above
224
- import { applyKbachConfig, useTheme } from '@kbach/react';
210
+ ```ts
211
+ // vite.config.ts — no `config`/`theme` option needed
212
+ import { kbach } from '@kbach/react/vite';
225
213
 
226
- applyKbachConfig({ darkMode: 'attribute' }); // call once, before first render
214
+ export default defineConfig({ plugins: [kbach(), react()] });
215
+ ```
216
+
217
+ ```tsx
218
+ // app entry — no applyKbachConfig() call needed either
219
+ import { useTheme } from '@kbach/react';
227
220
 
228
221
  function ThemeToggle() {
229
222
  const { mode, isDark, setMode, toggle } = useTheme();
@@ -231,13 +224,34 @@ function ThemeToggle() {
231
224
  }
232
225
  ```
233
226
 
227
+ That's the whole setup — one file, no runtime call. The generated CSS
228
+ carries the strategy it was built with (as a `--kb-dark-mode` custom
229
+ property), and `useTheme()`'s store reads it back from the DOM at startup,
230
+ so build-time and runtime always agree automatically.
231
+
232
+ If you're not using the static plugin at all (a pure runtime `kb()` setup,
233
+ with no build step generating `dark:` CSS ahead of time), there's nothing
234
+ for the runtime to auto-detect — call `applyKbachConfig({ darkMode: '...' })`
235
+ yourself instead, once, before your first render:
236
+
237
+ ```tsx
238
+ import { applyKbachConfig, useTheme } from '@kbach/react';
239
+
240
+ applyKbachConfig({ darkMode: 'attribute' });
241
+ ```
242
+
243
+ An explicit `applyKbachConfig()` call always overrides whatever was
244
+ auto-detected, if you ever need one — e.g. switching strategy at runtime for
245
+ some reason. Keep the value in one shared `kbach.config.js` and import it on
246
+ both sides rather than typing the same literal twice, if you do reach for it.
247
+
234
248
  Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
235
249
  optional, for seeding a startup default. Outside React (plain functions,
236
250
  event handlers), use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
237
251
  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'`.
252
+ under the default `'media'` strategy too (and even without any of the above)
253
+ — they just won't visibly affect `dark:` classes unless the runtime has also
254
+ learned about `'class'`/`'attribute'`, one way or another.
241
255
 
242
256
  ## Theming
243
257
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  kb
3
- } from "./chunk-ROKZPEHA.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";
@@ -79,9 +79,23 @@ function ensureResetInjected() {
79
79
  el.textContent = BASE_RESET;
80
80
  document.head.insertBefore(el, document.head.firstChild);
81
81
  }
82
+ var KB_CACHE_MAX = 500;
83
+ var kbCache = /* @__PURE__ */ new Map();
84
+ var cachedForThemeJson = null;
82
85
  function kb(classString) {
83
- const json = generateCss(classString, getThemeJson());
84
- const { className, rules } = JSON.parse(json);
86
+ const themeJson = getThemeJson();
87
+ if (themeJson !== cachedForThemeJson) {
88
+ kbCache.clear();
89
+ cachedForThemeJson = themeJson;
90
+ }
91
+ let result = kbCache.get(classString);
92
+ if (result === void 0) {
93
+ const json = generateCss(classString, themeJson);
94
+ result = JSON.parse(json);
95
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
96
+ kbCache.set(classString, result);
97
+ }
98
+ const { className, rules } = result;
85
99
  if (!runtimeCSSDisabled) {
86
100
  ensureResetInjected();
87
101
  for (const { rule, order } of rules) {
@@ -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,7 +1,101 @@
1
1
  import {
2
2
  defaultTheme,
3
+ detectDarkModeStrategyFromDom,
4
+ getTheme,
3
5
  setTheme
4
- } from "./chunk-EOSN5CYG.mjs";
6
+ } from "./chunk-OVKVZA7B.mjs";
7
+
8
+ // src/darkModeStore.ts
9
+ var STORAGE_KEY = "kbach-theme";
10
+ var listeners = /* @__PURE__ */ new Set();
11
+ function systemPrefersDark() {
12
+ if (typeof window === "undefined" || !window.matchMedia) return false;
13
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
14
+ }
15
+ function storage() {
16
+ return typeof window === "undefined" ? null : window.localStorage;
17
+ }
18
+ function readPersistedMode() {
19
+ const ls = storage();
20
+ if (!ls) return null;
21
+ try {
22
+ const raw = ls.getItem(STORAGE_KEY);
23
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function resolveIsDark(m) {
29
+ return m === "system" ? systemPrefersDark() : m === "dark";
30
+ }
31
+ function applyToDom(dark) {
32
+ if (typeof document === "undefined") return;
33
+ const root = document.documentElement;
34
+ const strategy = getTheme().darkMode;
35
+ root.classList.toggle("dark", strategy === "class" && dark);
36
+ root.classList.toggle("light", strategy === "class" && !dark);
37
+ if (strategy === "attribute") {
38
+ root.setAttribute("data-theme", dark ? "dark" : "light");
39
+ } else {
40
+ root.removeAttribute("data-theme");
41
+ }
42
+ }
43
+ detectDarkModeStrategyFromDom();
44
+ var persistedMode = readPersistedMode();
45
+ var hadPersistedMode = persistedMode !== null;
46
+ var mode = persistedMode ?? "system";
47
+ var isDark = resolveIsDark(mode);
48
+ applyToDom(isDark);
49
+ if (typeof window !== "undefined" && window.matchMedia) {
50
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
51
+ const onSystemChange = (event) => {
52
+ if (mode !== "system") return;
53
+ isDark = event.matches;
54
+ applyToDom(isDark);
55
+ notify();
56
+ };
57
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
58
+ else mq.addListener(onSystemChange);
59
+ }
60
+ function notify() {
61
+ for (const listener of listeners) listener();
62
+ }
63
+ function resyncDomWithActiveTheme() {
64
+ applyToDom(isDark);
65
+ }
66
+ function getGlobalDarkMode() {
67
+ return isDark;
68
+ }
69
+ function getGlobalThemeMode() {
70
+ return mode;
71
+ }
72
+ function setGlobalThemeMode(nextMode) {
73
+ mode = nextMode;
74
+ isDark = resolveIsDark(nextMode);
75
+ applyToDom(isDark);
76
+ const ls = storage();
77
+ if (ls) {
78
+ try {
79
+ ls.setItem(STORAGE_KEY, nextMode);
80
+ } catch {
81
+ }
82
+ }
83
+ notify();
84
+ }
85
+ function toggleGlobalDarkMode() {
86
+ setGlobalThemeMode(isDark ? "light" : "dark");
87
+ }
88
+ function subscribeGlobalDarkMode(listener) {
89
+ listeners.add(listener);
90
+ return () => listeners.delete(listener);
91
+ }
92
+ function seedDefaultMode(fallback) {
93
+ if (hadPersistedMode || mode === fallback) return;
94
+ mode = fallback;
95
+ isDark = resolveIsDark(fallback);
96
+ applyToDom(isDark);
97
+ notify();
98
+ }
5
99
 
6
100
  // src/colorVariables.ts
7
101
  function parseHexRgb(hex) {
@@ -104,12 +198,19 @@ function resolveKbachConfig(config) {
104
198
  function applyKbachConfig(config) {
105
199
  const theme = resolveKbachConfig(config);
106
200
  setTheme(theme);
201
+ resyncDomWithActiveTheme();
107
202
  return theme;
108
203
  }
109
204
 
110
205
  export {
111
206
  parseHexRgb,
112
207
  ensureColorVariablesInjected,
208
+ getGlobalDarkMode,
209
+ getGlobalThemeMode,
210
+ setGlobalThemeMode,
211
+ toggleGlobalDarkMode,
212
+ subscribeGlobalDarkMode,
213
+ seedDefaultMode,
113
214
  resolveKbachConfig,
114
215
  applyKbachConfig
115
216
  };
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  resolveKbachConfig
3
- } from "./chunk-4Y2QXNJY.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
  }
@@ -99,7 +99,18 @@ interface KbachConfig {
99
99
  }
100
100
  /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
101
101
  declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
102
- /** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. Returns the resolved theme (e.g. to also hand to the Vite plugin's own `theme` option). */
102
+ /**
103
+ * `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point
104
+ * from app code. Returns the resolved theme (e.g. to also hand to the Vite
105
+ * plugin's own `theme` option).
106
+ *
107
+ * Also re-syncs the DOM to the CURRENT dark-mode state right after
108
+ * `setTheme()` — see `resyncDomWithActiveTheme`'s own doc comment for why
109
+ * this is required, not optional: without it, an app that configures
110
+ * `darkMode: 'attribute'`/`'class'` here can silently never apply its
111
+ * `dark:` classes on initial load (even though `useTheme().isDark` reads
112
+ * correctly) until some later, unrelated dark-mode event happens to fire.
113
+ */
103
114
  declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
104
115
 
105
116
  export { type ColorEntry as C, type DarkModeStrategy as D, type KbachConfig as K, type ThemeConfig as T, type ContainerConfig as a, applyKbachConfig as b, defaultTheme as d, getTheme as g, resolveKbachConfig as r, setTheme as s };
@@ -99,7 +99,18 @@ interface KbachConfig {
99
99
  }
100
100
  /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
101
101
  declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
102
- /** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. Returns the resolved theme (e.g. to also hand to the Vite plugin's own `theme` option). */
102
+ /**
103
+ * `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point
104
+ * from app code. Returns the resolved theme (e.g. to also hand to the Vite
105
+ * plugin's own `theme` option).
106
+ *
107
+ * Also re-syncs the DOM to the CURRENT dark-mode state right after
108
+ * `setTheme()` — see `resyncDomWithActiveTheme`'s own doc comment for why
109
+ * this is required, not optional: without it, an app that configures
110
+ * `darkMode: 'attribute'`/`'class'` here can silently never apply its
111
+ * `dark:` classes on initial load (even though `useTheme().isDark` reads
112
+ * correctly) until some later, unrelated dark-mode event happens to fire.
113
+ */
103
114
  declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
104
115
 
105
116
  export { type ColorEntry as C, type DarkModeStrategy as D, type KbachConfig as K, type ThemeConfig as T, type ContainerConfig as a, applyKbachConfig as b, defaultTheme as d, getTheme as g, resolveKbachConfig as r, setTheme as s };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-BQ_b-qxF.mjs';
1
+ export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-B00tH6vN.mjs';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-BQ_b-qxF.js';
1
+ export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-B00tH6vN.js';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
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";
@@ -587,9 +594,23 @@ function ensureResetInjected() {
587
594
  el.textContent = BASE_RESET;
588
595
  document.head.insertBefore(el, document.head.firstChild);
589
596
  }
597
+ var KB_CACHE_MAX = 500;
598
+ var kbCache = /* @__PURE__ */ new Map();
599
+ var cachedForThemeJson = null;
590
600
  function kb(classString) {
591
- const json = generateCss(classString, getThemeJson());
592
- const { className, rules } = JSON.parse(json);
601
+ const themeJson = getThemeJson();
602
+ if (themeJson !== cachedForThemeJson) {
603
+ kbCache.clear();
604
+ cachedForThemeJson = themeJson;
605
+ }
606
+ let result = kbCache.get(classString);
607
+ if (result === void 0) {
608
+ const json = generateCss(classString, themeJson);
609
+ result = JSON.parse(json);
610
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
611
+ kbCache.set(classString, result);
612
+ }
613
+ const { className, rules } = result;
593
614
  if (!runtimeCSSDisabled) {
594
615
  ensureResetInjected();
595
616
  for (const { rule, order } of rules) {
@@ -656,63 +677,6 @@ function ensureColorVariablesInjected(theme) {
656
677
  ${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
657
678
  }
658
679
 
659
- // src/config.ts
660
- function applyOpacityToHex(hex, opacityText) {
661
- const rgb = parseHexRgb(hex);
662
- if (!rgb) return hex;
663
- const a = Number(opacityText) / 100;
664
- const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
665
- return `rgba(${rgb.join(",")},${clamped})`;
666
- }
667
- function resolveColorEntry(value, lookup) {
668
- if (typeof value !== "string") return value;
669
- const slash = value.indexOf("/");
670
- const name = slash === -1 ? value : value.slice(0, slash);
671
- const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
672
- const base = lookup(name);
673
- if (base === void 0) return value;
674
- if (opacity === void 0) return base;
675
- return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
676
- }
677
- function resolveColors(extend, existing) {
678
- const resolved = {};
679
- const lookup = (name) => resolved[name] ?? existing[name];
680
- for (const [name, value] of Object.entries(extend)) {
681
- resolved[name] = resolveColorEntry(value, lookup);
682
- }
683
- return resolved;
684
- }
685
- function resolveKbachConfig(config) {
686
- let theme = { ...defaultTheme, ...config.theme };
687
- if (config.extend?.colors) {
688
- theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
689
- }
690
- if (config.extend?.spacing) {
691
- theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
692
- }
693
- if (config.extend?.screens) {
694
- theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
695
- }
696
- if (config.extend?.fontFamily) {
697
- theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
698
- }
699
- if (config.extend?.container) {
700
- theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
701
- }
702
- if (config.darkMode) {
703
- theme = { ...theme, darkMode: config.darkMode };
704
- }
705
- return theme;
706
- }
707
- function applyKbachConfig(config) {
708
- const theme = resolveKbachConfig(config);
709
- setTheme(theme);
710
- return theme;
711
- }
712
-
713
- // src/useTheme.ts
714
- var import_react = require("react");
715
-
716
680
  // src/darkModeStore.ts
717
681
  var STORAGE_KEY = "kbach-theme";
718
682
  var listeners = /* @__PURE__ */ new Set();
@@ -740,13 +704,15 @@ function applyToDom(dark) {
740
704
  if (typeof document === "undefined") return;
741
705
  const root = document.documentElement;
742
706
  const strategy = getTheme().darkMode;
743
- if (strategy === "class") {
744
- root.classList.toggle("dark", dark);
745
- root.classList.toggle("light", !dark);
746
- } else if (strategy === "attribute") {
707
+ root.classList.toggle("dark", strategy === "class" && dark);
708
+ root.classList.toggle("light", strategy === "class" && !dark);
709
+ if (strategy === "attribute") {
747
710
  root.setAttribute("data-theme", dark ? "dark" : "light");
711
+ } else {
712
+ root.removeAttribute("data-theme");
748
713
  }
749
714
  }
715
+ detectDarkModeStrategyFromDom();
750
716
  var persistedMode = readPersistedMode();
751
717
  var hadPersistedMode = persistedMode !== null;
752
718
  var mode = persistedMode ?? "system";
@@ -766,6 +732,9 @@ if (typeof window !== "undefined" && window.matchMedia) {
766
732
  function notify() {
767
733
  for (const listener of listeners) listener();
768
734
  }
735
+ function resyncDomWithActiveTheme() {
736
+ applyToDom(isDark);
737
+ }
769
738
  function getGlobalDarkMode() {
770
739
  return isDark;
771
740
  }
@@ -800,7 +769,63 @@ function seedDefaultMode(fallback) {
800
769
  notify();
801
770
  }
802
771
 
772
+ // src/config.ts
773
+ function applyOpacityToHex(hex, opacityText) {
774
+ const rgb = parseHexRgb(hex);
775
+ if (!rgb) return hex;
776
+ const a = Number(opacityText) / 100;
777
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
778
+ return `rgba(${rgb.join(",")},${clamped})`;
779
+ }
780
+ function resolveColorEntry(value, lookup) {
781
+ if (typeof value !== "string") return value;
782
+ const slash = value.indexOf("/");
783
+ const name = slash === -1 ? value : value.slice(0, slash);
784
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
785
+ const base = lookup(name);
786
+ if (base === void 0) return value;
787
+ if (opacity === void 0) return base;
788
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
789
+ }
790
+ function resolveColors(extend, existing) {
791
+ const resolved = {};
792
+ const lookup = (name) => resolved[name] ?? existing[name];
793
+ for (const [name, value] of Object.entries(extend)) {
794
+ resolved[name] = resolveColorEntry(value, lookup);
795
+ }
796
+ return resolved;
797
+ }
798
+ function resolveKbachConfig(config) {
799
+ let theme = { ...defaultTheme, ...config.theme };
800
+ if (config.extend?.colors) {
801
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
802
+ }
803
+ if (config.extend?.spacing) {
804
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
805
+ }
806
+ if (config.extend?.screens) {
807
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
808
+ }
809
+ if (config.extend?.fontFamily) {
810
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
811
+ }
812
+ if (config.extend?.container) {
813
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
814
+ }
815
+ if (config.darkMode) {
816
+ theme = { ...theme, darkMode: config.darkMode };
817
+ }
818
+ return theme;
819
+ }
820
+ function applyKbachConfig(config) {
821
+ const theme = resolveKbachConfig(config);
822
+ setTheme(theme);
823
+ resyncDomWithActiveTheme();
824
+ return theme;
825
+ }
826
+
803
827
  // src/useTheme.ts
828
+ var import_react = require("react");
804
829
  function useTheme() {
805
830
  const isDark2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
806
831
  const mode2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
@@ -838,6 +863,7 @@ function ThemeProvider({ children, defaultMode = "system" }) {
838
863
  }
839
864
 
840
865
  // src/useColors.ts
866
+ var import_react3 = require("react");
841
867
  function applyOpacity(color, opacity) {
842
868
  const a = Math.max(0, Math.min(1, opacity / 100));
843
869
  const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
@@ -898,7 +924,7 @@ function useColors() {
898
924
  const { isDark: isDark2 } = useTheme();
899
925
  const theme = getTheme();
900
926
  ensureColorVariablesInjected(theme);
901
- return makeColorsProxy(theme, isDark2);
927
+ return (0, import_react3.useMemo)(() => makeColorsProxy(theme, isDark2), [theme, isDark2]);
902
928
  }
903
929
  // Annotate the CommonJS export names for ESM import in node:
904
930
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -4,20 +4,26 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-ROKZPEHA.mjs";
7
+ } from "./chunk-KJY3W4GF.mjs";
8
8
  import {
9
9
  applyKbachConfig,
10
10
  ensureColorVariablesInjected,
11
+ getGlobalDarkMode,
12
+ getGlobalThemeMode,
11
13
  parseHexRgb,
12
- resolveKbachConfig
13
- } from "./chunk-4Y2QXNJY.mjs";
14
+ resolveKbachConfig,
15
+ seedDefaultMode,
16
+ setGlobalThemeMode,
17
+ subscribeGlobalDarkMode,
18
+ toggleGlobalDarkMode
19
+ } from "./chunk-OWWJ2PME.mjs";
14
20
  import {
15
21
  BASE_RESET,
16
22
  RESET_STYLE_ID,
17
23
  defaultTheme,
18
24
  getTheme,
19
25
  setTheme
20
- } from "./chunk-EOSN5CYG.mjs";
26
+ } from "./chunk-OVKVZA7B.mjs";
21
27
 
22
28
  // src/KbachReset.tsx
23
29
  import { jsx } from "react/jsx-runtime";
@@ -28,101 +34,12 @@ function KbachReset() {
28
34
 
29
35
  // src/useTheme.ts
30
36
  import { useMemo, useSyncExternalStore } from "react";
31
-
32
- // src/darkModeStore.ts
33
- var STORAGE_KEY = "kbach-theme";
34
- var listeners = /* @__PURE__ */ new Set();
35
- function systemPrefersDark() {
36
- if (typeof window === "undefined" || !window.matchMedia) return false;
37
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
38
- }
39
- function storage() {
40
- return typeof window === "undefined" ? null : window.localStorage;
41
- }
42
- function readPersistedMode() {
43
- const ls = storage();
44
- if (!ls) return null;
45
- try {
46
- const raw = ls.getItem(STORAGE_KEY);
47
- return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
48
- } catch {
49
- return null;
50
- }
51
- }
52
- function resolveIsDark(m) {
53
- return m === "system" ? systemPrefersDark() : m === "dark";
54
- }
55
- function applyToDom(dark) {
56
- if (typeof document === "undefined") return;
57
- const root = document.documentElement;
58
- const strategy = getTheme().darkMode;
59
- if (strategy === "class") {
60
- root.classList.toggle("dark", dark);
61
- root.classList.toggle("light", !dark);
62
- } else if (strategy === "attribute") {
63
- root.setAttribute("data-theme", dark ? "dark" : "light");
64
- }
65
- }
66
- var persistedMode = readPersistedMode();
67
- var hadPersistedMode = persistedMode !== null;
68
- var mode = persistedMode ?? "system";
69
- var isDark = resolveIsDark(mode);
70
- applyToDom(isDark);
71
- if (typeof window !== "undefined" && window.matchMedia) {
72
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
73
- const onSystemChange = (event) => {
74
- if (mode !== "system") return;
75
- isDark = event.matches;
76
- applyToDom(isDark);
77
- notify();
78
- };
79
- if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
80
- else mq.addListener(onSystemChange);
81
- }
82
- function notify() {
83
- for (const listener of listeners) listener();
84
- }
85
- function getGlobalDarkMode() {
86
- return isDark;
87
- }
88
- function getGlobalThemeMode() {
89
- return mode;
90
- }
91
- function setGlobalThemeMode(nextMode) {
92
- mode = nextMode;
93
- isDark = resolveIsDark(nextMode);
94
- applyToDom(isDark);
95
- const ls = storage();
96
- if (ls) {
97
- try {
98
- ls.setItem(STORAGE_KEY, nextMode);
99
- } catch {
100
- }
101
- }
102
- notify();
103
- }
104
- function toggleGlobalDarkMode() {
105
- setGlobalThemeMode(isDark ? "light" : "dark");
106
- }
107
- function subscribeGlobalDarkMode(listener) {
108
- listeners.add(listener);
109
- return () => listeners.delete(listener);
110
- }
111
- function seedDefaultMode(fallback) {
112
- if (hadPersistedMode || mode === fallback) return;
113
- mode = fallback;
114
- isDark = resolveIsDark(fallback);
115
- applyToDom(isDark);
116
- notify();
117
- }
118
-
119
- // src/useTheme.ts
120
37
  function useTheme() {
121
- const isDark2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
122
- const mode2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
38
+ const isDark = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
39
+ const mode = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
123
40
  return useMemo(
124
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
125
- [mode2, isDark2]
41
+ () => ({ mode, isDark, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
42
+ [mode, isDark]
126
43
  );
127
44
  }
128
45
 
@@ -154,6 +71,7 @@ function ThemeProvider({ children, defaultMode = "system" }) {
154
71
  }
155
72
 
156
73
  // src/useColors.ts
74
+ import { useMemo as useMemo2 } from "react";
157
75
  function applyOpacity(color, opacity) {
158
76
  const a = Math.max(0, Math.min(1, opacity / 100));
159
77
  const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
@@ -166,14 +84,14 @@ function applyOpacity(color, opacity) {
166
84
  if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
167
85
  return color;
168
86
  }
169
- function resolveEntry(name, entry, isDark2) {
87
+ function resolveEntry(name, entry, isDark) {
170
88
  if (typeof entry === "string") return entry;
171
89
  const light = parseHexRgb(entry.light);
172
90
  const dark = parseHexRgb(entry.dark);
173
91
  if (light && dark) return `rgb(var(--kb-color-${name}))`;
174
- return isDark2 ? entry.dark : entry.light;
92
+ return isDark ? entry.dark : entry.light;
175
93
  }
176
- function makeColorsProxy(theme, isDark2) {
94
+ function makeColorsProxy(theme, isDark) {
177
95
  const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
178
96
  const cache = /* @__PURE__ */ new Map();
179
97
  function resolveNamed(key) {
@@ -186,7 +104,7 @@ function makeColorsProxy(theme, isDark2) {
186
104
  }
187
105
  const entry = theme.colors[name];
188
106
  if (entry === void 0) return void 0;
189
- const value = resolveEntry(name, entry, isDark2);
107
+ const value = resolveEntry(name, entry, isDark);
190
108
  return opacity === void 0 ? value : applyOpacity(value, opacity);
191
109
  }
192
110
  function makeFamilyProxy(family) {
@@ -211,10 +129,10 @@ function makeColorsProxy(theme, isDark2) {
211
129
  });
212
130
  }
213
131
  function useColors() {
214
- const { isDark: isDark2 } = useTheme();
132
+ const { isDark } = useTheme();
215
133
  const theme = getTheme();
216
134
  ensureColorVariablesInjected(theme);
217
- return makeColorsProxy(theme, isDark2);
135
+ return useMemo2(() => makeColorsProxy(theme, isDark), [theme, isDark]);
218
136
  }
219
137
  export {
220
138
  KbachReset,
@@ -548,9 +548,23 @@ function ensureResetInjected() {
548
548
  el.textContent = BASE_RESET;
549
549
  document.head.insertBefore(el, document.head.firstChild);
550
550
  }
551
+ var KB_CACHE_MAX = 500;
552
+ var kbCache = /* @__PURE__ */ new Map();
553
+ var cachedForThemeJson = null;
551
554
  function kb(classString) {
552
- const json = generateCss(classString, getThemeJson());
553
- const { className, rules } = JSON.parse(json);
555
+ const themeJson = getThemeJson();
556
+ if (themeJson !== cachedForThemeJson) {
557
+ kbCache.clear();
558
+ cachedForThemeJson = themeJson;
559
+ }
560
+ let result = kbCache.get(classString);
561
+ if (result === void 0) {
562
+ const json = generateCss(classString, themeJson);
563
+ result = JSON.parse(json);
564
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
565
+ kbCache.set(classString, result);
566
+ }
567
+ const { className, rules } = result;
554
568
  if (!runtimeCSSDisabled) {
555
569
  ensureResetInjected();
556
570
  for (const { rule, order } of rules) {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-L6B4DTLD.mjs";
6
- import "./chunk-ROKZPEHA.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) {
@@ -547,9 +547,23 @@ function ensureResetInjected() {
547
547
  el.textContent = BASE_RESET;
548
548
  document.head.insertBefore(el, document.head.firstChild);
549
549
  }
550
+ var KB_CACHE_MAX = 500;
551
+ var kbCache = /* @__PURE__ */ new Map();
552
+ var cachedForThemeJson = null;
550
553
  function kb(classString) {
551
- const json = generateCss(classString, getThemeJson());
552
- const { className, rules } = JSON.parse(json);
554
+ const themeJson = getThemeJson();
555
+ if (themeJson !== cachedForThemeJson) {
556
+ kbCache.clear();
557
+ cachedForThemeJson = themeJson;
558
+ }
559
+ let result = kbCache.get(classString);
560
+ if (result === void 0) {
561
+ const json = generateCss(classString, themeJson);
562
+ result = JSON.parse(json);
563
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
564
+ kbCache.set(classString, result);
565
+ }
566
+ const { className, rules } = result;
553
567
  if (!runtimeCSSDisabled) {
554
568
  ensureResetInjected();
555
569
  for (const { rule, order } of rules) {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-L6B4DTLD.mjs";
6
- import "./chunk-ROKZPEHA.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,
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'postcss';
2
- import { T as ThemeConfig, K as KbachConfig } from './config-BQ_b-qxF.mjs';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-B00tH6vN.mjs';
3
3
 
4
4
  interface KbachPostcssPluginOptions {
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'postcss';
2
- import { T as ThemeConfig, K as KbachConfig } from './config-BQ_b-qxF.js';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-B00tH6vN.js';
3
3
 
4
4
  interface KbachPostcssPluginOptions {
5
5
  /**
@@ -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
  }
@@ -988,7 +986,22 @@ var defaultTheme = {
988
986
  // centering/padding) — see `ContainerConfig`'s own doc comment.
989
987
  container: {}
990
988
  };
989
+ var activeTheme = defaultTheme;
991
990
  var activeThemeJson = JSON.stringify(defaultTheme);
991
+ function setTheme(theme) {
992
+ activeTheme = theme;
993
+ activeThemeJson = JSON.stringify(theme);
994
+ }
995
+ function getTheme() {
996
+ return activeTheme;
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
+ }
992
1005
 
993
1006
  // src/colorVariables.ts
994
1007
  function parseHexRgb(hex) {
@@ -1000,6 +1013,61 @@ function parseHexRgb(hex) {
1000
1013
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
1001
1014
  }
1002
1015
 
1016
+ // src/darkModeStore.ts
1017
+ var STORAGE_KEY = "kbach-theme";
1018
+ var listeners = /* @__PURE__ */ new Set();
1019
+ function systemPrefersDark() {
1020
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1021
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
1022
+ }
1023
+ function storage() {
1024
+ return typeof window === "undefined" ? null : window.localStorage;
1025
+ }
1026
+ function readPersistedMode() {
1027
+ const ls = storage();
1028
+ if (!ls) return null;
1029
+ try {
1030
+ const raw = ls.getItem(STORAGE_KEY);
1031
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
1032
+ } catch {
1033
+ return null;
1034
+ }
1035
+ }
1036
+ function resolveIsDark(m) {
1037
+ return m === "system" ? systemPrefersDark() : m === "dark";
1038
+ }
1039
+ function applyToDom(dark) {
1040
+ if (typeof document === "undefined") return;
1041
+ const root = document.documentElement;
1042
+ const strategy = getTheme().darkMode;
1043
+ root.classList.toggle("dark", strategy === "class" && dark);
1044
+ root.classList.toggle("light", strategy === "class" && !dark);
1045
+ if (strategy === "attribute") {
1046
+ root.setAttribute("data-theme", dark ? "dark" : "light");
1047
+ } else {
1048
+ root.removeAttribute("data-theme");
1049
+ }
1050
+ }
1051
+ detectDarkModeStrategyFromDom();
1052
+ var persistedMode = readPersistedMode();
1053
+ var mode = persistedMode ?? "system";
1054
+ var isDark = resolveIsDark(mode);
1055
+ applyToDom(isDark);
1056
+ if (typeof window !== "undefined" && window.matchMedia) {
1057
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
1058
+ const onSystemChange = (event) => {
1059
+ if (mode !== "system") return;
1060
+ isDark = event.matches;
1061
+ applyToDom(isDark);
1062
+ notify();
1063
+ };
1064
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
1065
+ else mq.addListener(onSystemChange);
1066
+ }
1067
+ function notify() {
1068
+ for (const listener of listeners) listener();
1069
+ }
1070
+
1003
1071
  // src/config.ts
1004
1072
  function applyOpacityToHex(hex, opacityText) {
1005
1073
  const rgb = parseHexRgb(hex);
@@ -2,9 +2,9 @@ import {
2
2
  DEFAULT_SCAN_DIRS,
3
3
  createKbachStaticCssEngine,
4
4
  resolveEffectiveTheme
5
- } from "./chunk-J2M6QWJW.mjs";
6
- import "./chunk-4Y2QXNJY.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";
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
- import { T as ThemeConfig, K as KbachConfig } from './config-BQ_b-qxF.mjs';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-B00tH6vN.mjs';
3
3
 
4
4
  interface KbachPluginOptions {
5
5
  /** Defaults to the same defaultTheme the runtime uses — pass your own to keep both in sync. */
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
- import { T as ThemeConfig, K as KbachConfig } from './config-BQ_b-qxF.js';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-B00tH6vN.js';
3
3
 
4
4
  interface KbachPluginOptions {
5
5
  /** Defaults to the same defaultTheme the runtime uses — pass your own to keep both in sync. */
@@ -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
  }
@@ -1382,7 +1380,22 @@ var defaultTheme = {
1382
1380
  // centering/padding) — see `ContainerConfig`'s own doc comment.
1383
1381
  container: {}
1384
1382
  };
1383
+ var activeTheme = defaultTheme;
1385
1384
  var activeThemeJson = JSON.stringify(defaultTheme);
1385
+ function setTheme(theme) {
1386
+ activeTheme = theme;
1387
+ activeThemeJson = JSON.stringify(theme);
1388
+ }
1389
+ function getTheme() {
1390
+ return activeTheme;
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
+ }
1386
1399
 
1387
1400
  // src/colorVariables.ts
1388
1401
  function parseHexRgb(hex) {
@@ -1394,6 +1407,61 @@ function parseHexRgb(hex) {
1394
1407
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
1395
1408
  }
1396
1409
 
1410
+ // src/darkModeStore.ts
1411
+ var STORAGE_KEY = "kbach-theme";
1412
+ var listeners = /* @__PURE__ */ new Set();
1413
+ function systemPrefersDark() {
1414
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1415
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
1416
+ }
1417
+ function storage() {
1418
+ return typeof window === "undefined" ? null : window.localStorage;
1419
+ }
1420
+ function readPersistedMode() {
1421
+ const ls = storage();
1422
+ if (!ls) return null;
1423
+ try {
1424
+ const raw = ls.getItem(STORAGE_KEY);
1425
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
1426
+ } catch {
1427
+ return null;
1428
+ }
1429
+ }
1430
+ function resolveIsDark(m) {
1431
+ return m === "system" ? systemPrefersDark() : m === "dark";
1432
+ }
1433
+ function applyToDom(dark) {
1434
+ if (typeof document === "undefined") return;
1435
+ const root = document.documentElement;
1436
+ const strategy = getTheme().darkMode;
1437
+ root.classList.toggle("dark", strategy === "class" && dark);
1438
+ root.classList.toggle("light", strategy === "class" && !dark);
1439
+ if (strategy === "attribute") {
1440
+ root.setAttribute("data-theme", dark ? "dark" : "light");
1441
+ } else {
1442
+ root.removeAttribute("data-theme");
1443
+ }
1444
+ }
1445
+ detectDarkModeStrategyFromDom();
1446
+ var persistedMode = readPersistedMode();
1447
+ var mode = persistedMode ?? "system";
1448
+ var isDark = resolveIsDark(mode);
1449
+ applyToDom(isDark);
1450
+ if (typeof window !== "undefined" && window.matchMedia) {
1451
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
1452
+ const onSystemChange = (event) => {
1453
+ if (mode !== "system") return;
1454
+ isDark = event.matches;
1455
+ applyToDom(isDark);
1456
+ notify();
1457
+ };
1458
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
1459
+ else mq.addListener(onSystemChange);
1460
+ }
1461
+ function notify() {
1462
+ for (const listener of listeners) listener();
1463
+ }
1464
+
1397
1465
  // src/config.ts
1398
1466
  function applyOpacityToHex(hex, opacityText) {
1399
1467
  const rgb = parseHexRgb(hex);
@@ -5,9 +5,9 @@ import {
5
5
  kbach,
6
6
  loadConfigFile,
7
7
  resolveEffectiveTheme
8
- } from "./chunk-J2M6QWJW.mjs";
9
- import "./chunk-4Y2QXNJY.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.12",
3
+ "version": "1.0.0-beta.14",
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",
@@ -62,7 +62,7 @@
62
62
  "generate:colors": "node ../core-engine/scripts/generate-palette.mjs src/generatedPalette.ts"
63
63
  },
64
64
  "dependencies": {
65
- "@kbach/core-engine": "1.0.0-beta.7"
65
+ "@kbach/core-engine": "1.0.0-beta.8"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/react": "^19.2.0",