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

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.
@@ -1,8 +1,99 @@
1
1
  import {
2
2
  defaultTheme,
3
+ getTheme,
3
4
  setTheme
4
5
  } from "./chunk-EOSN5CYG.mjs";
5
6
 
7
+ // src/darkModeStore.ts
8
+ var STORAGE_KEY = "kbach-theme";
9
+ var listeners = /* @__PURE__ */ new Set();
10
+ function systemPrefersDark() {
11
+ if (typeof window === "undefined" || !window.matchMedia) return false;
12
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
13
+ }
14
+ function storage() {
15
+ return typeof window === "undefined" ? null : window.localStorage;
16
+ }
17
+ function readPersistedMode() {
18
+ const ls = storage();
19
+ if (!ls) return null;
20
+ try {
21
+ const raw = ls.getItem(STORAGE_KEY);
22
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ function resolveIsDark(m) {
28
+ return m === "system" ? systemPrefersDark() : m === "dark";
29
+ }
30
+ function applyToDom(dark) {
31
+ if (typeof document === "undefined") return;
32
+ const root = document.documentElement;
33
+ 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") {
38
+ root.setAttribute("data-theme", dark ? "dark" : "light");
39
+ }
40
+ }
41
+ var persistedMode = readPersistedMode();
42
+ var hadPersistedMode = persistedMode !== null;
43
+ var mode = persistedMode ?? "system";
44
+ var isDark = resolveIsDark(mode);
45
+ applyToDom(isDark);
46
+ if (typeof window !== "undefined" && window.matchMedia) {
47
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
48
+ const onSystemChange = (event) => {
49
+ if (mode !== "system") return;
50
+ isDark = event.matches;
51
+ applyToDom(isDark);
52
+ notify();
53
+ };
54
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
55
+ else mq.addListener(onSystemChange);
56
+ }
57
+ function notify() {
58
+ for (const listener of listeners) listener();
59
+ }
60
+ function resyncDomWithActiveTheme() {
61
+ applyToDom(isDark);
62
+ }
63
+ function getGlobalDarkMode() {
64
+ return isDark;
65
+ }
66
+ function getGlobalThemeMode() {
67
+ return mode;
68
+ }
69
+ function setGlobalThemeMode(nextMode) {
70
+ mode = nextMode;
71
+ isDark = resolveIsDark(nextMode);
72
+ applyToDom(isDark);
73
+ const ls = storage();
74
+ if (ls) {
75
+ try {
76
+ ls.setItem(STORAGE_KEY, nextMode);
77
+ } catch {
78
+ }
79
+ }
80
+ notify();
81
+ }
82
+ function toggleGlobalDarkMode() {
83
+ setGlobalThemeMode(isDark ? "light" : "dark");
84
+ }
85
+ function subscribeGlobalDarkMode(listener) {
86
+ listeners.add(listener);
87
+ return () => listeners.delete(listener);
88
+ }
89
+ function seedDefaultMode(fallback) {
90
+ if (hadPersistedMode || mode === fallback) return;
91
+ mode = fallback;
92
+ isDark = resolveIsDark(fallback);
93
+ applyToDom(isDark);
94
+ notify();
95
+ }
96
+
6
97
  // src/colorVariables.ts
7
98
  function parseHexRgb(hex) {
8
99
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
@@ -104,12 +195,19 @@ function resolveKbachConfig(config) {
104
195
  function applyKbachConfig(config) {
105
196
  const theme = resolveKbachConfig(config);
106
197
  setTheme(theme);
198
+ resyncDomWithActiveTheme();
107
199
  return theme;
108
200
  }
109
201
 
110
202
  export {
111
203
  parseHexRgb,
112
204
  ensureColorVariablesInjected,
205
+ getGlobalDarkMode,
206
+ getGlobalThemeMode,
207
+ setGlobalThemeMode,
208
+ toggleGlobalDarkMode,
209
+ subscribeGlobalDarkMode,
210
+ seedDefaultMode,
113
211
  resolveKbachConfig,
114
212
  applyKbachConfig
115
213
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  kb
3
- } from "./chunk-ROKZPEHA.mjs";
3
+ } from "./chunk-U4FGKMNH.mjs";
4
4
 
5
5
  // src/jsx-runtime.tsx
6
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveKbachConfig
3
- } from "./chunk-4Y2QXNJY.mjs";
3
+ } from "./chunk-77PKNQ2K.mjs";
4
4
  import {
5
5
  buildResetCSS,
6
6
  defaultTheme
@@ -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) {
@@ -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
@@ -587,9 +587,23 @@ function ensureResetInjected() {
587
587
  el.textContent = BASE_RESET;
588
588
  document.head.insertBefore(el, document.head.firstChild);
589
589
  }
590
+ var KB_CACHE_MAX = 500;
591
+ var kbCache = /* @__PURE__ */ new Map();
592
+ var cachedForThemeJson = null;
590
593
  function kb(classString) {
591
- const json = generateCss(classString, getThemeJson());
592
- const { className, rules } = JSON.parse(json);
594
+ const themeJson = getThemeJson();
595
+ if (themeJson !== cachedForThemeJson) {
596
+ kbCache.clear();
597
+ cachedForThemeJson = themeJson;
598
+ }
599
+ let result = kbCache.get(classString);
600
+ if (result === void 0) {
601
+ const json = generateCss(classString, themeJson);
602
+ result = JSON.parse(json);
603
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
604
+ kbCache.set(classString, result);
605
+ }
606
+ const { className, rules } = result;
593
607
  if (!runtimeCSSDisabled) {
594
608
  ensureResetInjected();
595
609
  for (const { rule, order } of rules) {
@@ -656,63 +670,6 @@ function ensureColorVariablesInjected(theme) {
656
670
  ${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
657
671
  }
658
672
 
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
673
  // src/darkModeStore.ts
717
674
  var STORAGE_KEY = "kbach-theme";
718
675
  var listeners = /* @__PURE__ */ new Set();
@@ -766,6 +723,9 @@ if (typeof window !== "undefined" && window.matchMedia) {
766
723
  function notify() {
767
724
  for (const listener of listeners) listener();
768
725
  }
726
+ function resyncDomWithActiveTheme() {
727
+ applyToDom(isDark);
728
+ }
769
729
  function getGlobalDarkMode() {
770
730
  return isDark;
771
731
  }
@@ -800,7 +760,63 @@ function seedDefaultMode(fallback) {
800
760
  notify();
801
761
  }
802
762
 
763
+ // src/config.ts
764
+ function applyOpacityToHex(hex, opacityText) {
765
+ const rgb = parseHexRgb(hex);
766
+ if (!rgb) return hex;
767
+ const a = Number(opacityText) / 100;
768
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
769
+ return `rgba(${rgb.join(",")},${clamped})`;
770
+ }
771
+ function resolveColorEntry(value, lookup) {
772
+ if (typeof value !== "string") return value;
773
+ const slash = value.indexOf("/");
774
+ const name = slash === -1 ? value : value.slice(0, slash);
775
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
776
+ const base = lookup(name);
777
+ if (base === void 0) return value;
778
+ if (opacity === void 0) return base;
779
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
780
+ }
781
+ function resolveColors(extend, existing) {
782
+ const resolved = {};
783
+ const lookup = (name) => resolved[name] ?? existing[name];
784
+ for (const [name, value] of Object.entries(extend)) {
785
+ resolved[name] = resolveColorEntry(value, lookup);
786
+ }
787
+ return resolved;
788
+ }
789
+ function resolveKbachConfig(config) {
790
+ let theme = { ...defaultTheme, ...config.theme };
791
+ if (config.extend?.colors) {
792
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
793
+ }
794
+ if (config.extend?.spacing) {
795
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
796
+ }
797
+ if (config.extend?.screens) {
798
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
799
+ }
800
+ if (config.extend?.fontFamily) {
801
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
802
+ }
803
+ if (config.extend?.container) {
804
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
805
+ }
806
+ if (config.darkMode) {
807
+ theme = { ...theme, darkMode: config.darkMode };
808
+ }
809
+ return theme;
810
+ }
811
+ function applyKbachConfig(config) {
812
+ const theme = resolveKbachConfig(config);
813
+ setTheme(theme);
814
+ resyncDomWithActiveTheme();
815
+ return theme;
816
+ }
817
+
803
818
  // src/useTheme.ts
819
+ var import_react = require("react");
804
820
  function useTheme() {
805
821
  const isDark2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
806
822
  const mode2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
@@ -838,6 +854,7 @@ function ThemeProvider({ children, defaultMode = "system" }) {
838
854
  }
839
855
 
840
856
  // src/useColors.ts
857
+ var import_react3 = require("react");
841
858
  function applyOpacity(color, opacity) {
842
859
  const a = Math.max(0, Math.min(1, opacity / 100));
843
860
  const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
@@ -898,7 +915,7 @@ function useColors() {
898
915
  const { isDark: isDark2 } = useTheme();
899
916
  const theme = getTheme();
900
917
  ensureColorVariablesInjected(theme);
901
- return makeColorsProxy(theme, isDark2);
918
+ return (0, import_react3.useMemo)(() => makeColorsProxy(theme, isDark2), [theme, isDark2]);
902
919
  }
903
920
  // Annotate the CommonJS export names for ESM import in node:
904
921
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -4,13 +4,19 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-ROKZPEHA.mjs";
7
+ } from "./chunk-U4FGKMNH.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-77PKNQ2K.mjs";
14
20
  import {
15
21
  BASE_RESET,
16
22
  RESET_STYLE_ID,
@@ -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,8 +2,8 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-L6B4DTLD.mjs";
6
- import "./chunk-ROKZPEHA.mjs";
5
+ } from "./chunk-7G7TMQL6.mjs";
6
+ import "./chunk-U4FGKMNH.mjs";
7
7
  import "./chunk-EOSN5CYG.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
@@ -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,8 +2,8 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-L6B4DTLD.mjs";
6
- import "./chunk-ROKZPEHA.mjs";
5
+ } from "./chunk-7G7TMQL6.mjs";
6
+ import "./chunk-U4FGKMNH.mjs";
7
7
  import "./chunk-EOSN5CYG.mjs";
8
8
  export {
9
9
  Fragment,
@@ -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
  /**
@@ -988,7 +988,11 @@ var defaultTheme = {
988
988
  // centering/padding) — see `ContainerConfig`'s own doc comment.
989
989
  container: {}
990
990
  };
991
+ var activeTheme = defaultTheme;
991
992
  var activeThemeJson = JSON.stringify(defaultTheme);
993
+ function getTheme() {
994
+ return activeTheme;
995
+ }
992
996
 
993
997
  // src/colorVariables.ts
994
998
  function parseHexRgb(hex) {
@@ -1000,6 +1004,59 @@ function parseHexRgb(hex) {
1000
1004
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
1001
1005
  }
1002
1006
 
1007
+ // src/darkModeStore.ts
1008
+ var STORAGE_KEY = "kbach-theme";
1009
+ var listeners = /* @__PURE__ */ new Set();
1010
+ function systemPrefersDark() {
1011
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1012
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
1013
+ }
1014
+ function storage() {
1015
+ return typeof window === "undefined" ? null : window.localStorage;
1016
+ }
1017
+ function readPersistedMode() {
1018
+ const ls = storage();
1019
+ if (!ls) return null;
1020
+ try {
1021
+ const raw = ls.getItem(STORAGE_KEY);
1022
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
1023
+ } catch {
1024
+ return null;
1025
+ }
1026
+ }
1027
+ function resolveIsDark(m) {
1028
+ return m === "system" ? systemPrefersDark() : m === "dark";
1029
+ }
1030
+ function applyToDom(dark) {
1031
+ if (typeof document === "undefined") return;
1032
+ const root = document.documentElement;
1033
+ 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") {
1038
+ root.setAttribute("data-theme", dark ? "dark" : "light");
1039
+ }
1040
+ }
1041
+ var persistedMode = readPersistedMode();
1042
+ var mode = persistedMode ?? "system";
1043
+ var isDark = resolveIsDark(mode);
1044
+ applyToDom(isDark);
1045
+ if (typeof window !== "undefined" && window.matchMedia) {
1046
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
1047
+ const onSystemChange = (event) => {
1048
+ if (mode !== "system") return;
1049
+ isDark = event.matches;
1050
+ applyToDom(isDark);
1051
+ notify();
1052
+ };
1053
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
1054
+ else mq.addListener(onSystemChange);
1055
+ }
1056
+ function notify() {
1057
+ for (const listener of listeners) listener();
1058
+ }
1059
+
1003
1060
  // src/config.ts
1004
1061
  function applyOpacityToHex(hex, opacityText) {
1005
1062
  const rgb = parseHexRgb(hex);
@@ -2,8 +2,8 @@ import {
2
2
  DEFAULT_SCAN_DIRS,
3
3
  createKbachStaticCssEngine,
4
4
  resolveEffectiveTheme
5
- } from "./chunk-J2M6QWJW.mjs";
6
- import "./chunk-4Y2QXNJY.mjs";
5
+ } from "./chunk-FJUZ2ZYT.mjs";
6
+ import "./chunk-77PKNQ2K.mjs";
7
7
  import "./chunk-EOSN5CYG.mjs";
8
8
 
9
9
  // src/postcss-plugin/index.ts
@@ -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. */
@@ -1382,7 +1382,11 @@ var defaultTheme = {
1382
1382
  // centering/padding) — see `ContainerConfig`'s own doc comment.
1383
1383
  container: {}
1384
1384
  };
1385
+ var activeTheme = defaultTheme;
1385
1386
  var activeThemeJson = JSON.stringify(defaultTheme);
1387
+ function getTheme() {
1388
+ return activeTheme;
1389
+ }
1386
1390
 
1387
1391
  // src/colorVariables.ts
1388
1392
  function parseHexRgb(hex) {
@@ -1394,6 +1398,59 @@ function parseHexRgb(hex) {
1394
1398
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
1395
1399
  }
1396
1400
 
1401
+ // src/darkModeStore.ts
1402
+ var STORAGE_KEY = "kbach-theme";
1403
+ var listeners = /* @__PURE__ */ new Set();
1404
+ function systemPrefersDark() {
1405
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1406
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
1407
+ }
1408
+ function storage() {
1409
+ return typeof window === "undefined" ? null : window.localStorage;
1410
+ }
1411
+ function readPersistedMode() {
1412
+ const ls = storage();
1413
+ if (!ls) return null;
1414
+ try {
1415
+ const raw = ls.getItem(STORAGE_KEY);
1416
+ return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
1417
+ } catch {
1418
+ return null;
1419
+ }
1420
+ }
1421
+ function resolveIsDark(m) {
1422
+ return m === "system" ? systemPrefersDark() : m === "dark";
1423
+ }
1424
+ function applyToDom(dark) {
1425
+ if (typeof document === "undefined") return;
1426
+ const root = document.documentElement;
1427
+ 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") {
1432
+ root.setAttribute("data-theme", dark ? "dark" : "light");
1433
+ }
1434
+ }
1435
+ var persistedMode = readPersistedMode();
1436
+ var mode = persistedMode ?? "system";
1437
+ var isDark = resolveIsDark(mode);
1438
+ applyToDom(isDark);
1439
+ if (typeof window !== "undefined" && window.matchMedia) {
1440
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
1441
+ const onSystemChange = (event) => {
1442
+ if (mode !== "system") return;
1443
+ isDark = event.matches;
1444
+ applyToDom(isDark);
1445
+ notify();
1446
+ };
1447
+ if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
1448
+ else mq.addListener(onSystemChange);
1449
+ }
1450
+ function notify() {
1451
+ for (const listener of listeners) listener();
1452
+ }
1453
+
1397
1454
  // src/config.ts
1398
1455
  function applyOpacityToHex(hex, opacityText) {
1399
1456
  const rgb = parseHexRgb(hex);
@@ -5,8 +5,8 @@ import {
5
5
  kbach,
6
6
  loadConfigFile,
7
7
  resolveEffectiveTheme
8
- } from "./chunk-J2M6QWJW.mjs";
9
- import "./chunk-4Y2QXNJY.mjs";
8
+ } from "./chunk-FJUZ2ZYT.mjs";
9
+ import "./chunk-77PKNQ2K.mjs";
10
10
  import "./chunk-EOSN5CYG.mjs";
11
11
  export {
12
12
  CSS_SYNC_DEBOUNCE_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "1.0.0-beta.11",
3
+ "version": "1.0.0-beta.13",
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.6"
65
+ "@kbach/core-engine": "1.0.0-beta.8"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/react": "^19.2.0",