@kbach/react 1.0.0-beta.1 → 1.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  KbachReset: () => KbachReset,
34
34
  ThemeProvider: () => ThemeProvider,
35
+ applyKbachConfig: () => applyKbachConfig,
35
36
  defaultTheme: () => defaultTheme,
36
37
  disableRuntimeCSS: () => disableRuntimeCSS,
37
38
  getGlobalDarkMode: () => getGlobalDarkMode,
@@ -41,11 +42,12 @@ __export(src_exports, {
41
42
  isKbachReady: () => isKbachReady,
42
43
  isRuntimeCSSDisabled: () => isRuntimeCSSDisabled,
43
44
  kb: () => kb,
45
+ resolveKbachConfig: () => resolveKbachConfig,
44
46
  setGlobalThemeMode: () => setGlobalThemeMode,
45
47
  setTheme: () => setTheme,
46
48
  subscribeGlobalDarkMode: () => subscribeGlobalDarkMode,
47
49
  toggleGlobalDarkMode: () => toggleGlobalDarkMode,
48
- useGlobalDarkMode: () => useGlobalDarkMode,
50
+ useColors: () => useColors,
49
51
  useTheme: () => useTheme
50
52
  });
51
53
  module.exports = __toCommonJS(src_exports);
@@ -360,14 +362,15 @@ var defaultTheme = {
360
362
  // Real Tailwind's default spacing scale in full — every step is exactly
361
363
  // `n * 4px` (Tailwind's base spacing unit), including the fractional
362
364
  // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
363
- // literal (a bare 1px, for `p-px`-style hairline spacing). Previously
364
- // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
365
- // the real scale — so anything else (`p-3`, `w-5`, `border-2`, `gap-10`,
366
- // ...) silently failed to resolve (an unknown spacing key returns `None`
367
- // the same way an unknown class does, with no error). "0" itself is
368
- // still needed explicitly (not derivable as "any number * 4") for the
369
- // same reason it always was: `sticky top-0` needs an actual "0" key to
370
- // resolve at all, not just fall through as unresolvable.
365
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
366
+ // only the "usual stops", though — the Rust/JS engines' shared
367
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
368
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
369
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
370
+ // values and give a theme customization something to override, not to
371
+ // enumerate every resolvable step. "px" is the one exception that must
372
+ // stay listed explicitly — it isn't a number, so the formula can't
373
+ // derive it.
371
374
  spacing: {
372
375
  "0": 0,
373
376
  "px": 1,
@@ -412,7 +415,33 @@ var defaultTheme = {
412
415
  xl: 1280,
413
416
  "2xl": 1536
414
417
  },
415
- darkMode: "attribute"
418
+ // Byte-for-byte the same three stacks
419
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
420
+ // falls back to — kept here too (rather than leaving this empty and
421
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
422
+ // is a real, readable value for anything that wants to extend rather
423
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
424
+ fontFamily: {
425
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
426
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
427
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
428
+ },
429
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
430
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
431
+ // using only static `className` strings gets working `dark:` classes
432
+ // out of the box, with no runtime import required to write `data-theme`/
433
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
434
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
435
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
436
+ // for JS consumers, but flipping it has no visible effect on `dark:`
437
+ // classes, since the browser's media query only ever reflects the real
438
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
439
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
440
+ // as an app wants a real in-app toggle.
441
+ darkMode: "media",
442
+ // Empty by default, matching real Tailwind's own defaults (no forced
443
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
444
+ container: {}
416
445
  };
417
446
  var activeTheme = defaultTheme;
418
447
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -577,6 +606,113 @@ function KbachReset() {
577
606
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { id: RESET_STYLE_ID, children: BASE_RESET });
578
607
  }
579
608
 
609
+ // src/colorVariables.ts
610
+ function parseHexRgb(hex) {
611
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
612
+ if (!m) return null;
613
+ let h = m[1];
614
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
615
+ const n = parseInt(h, 16);
616
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
617
+ }
618
+ function wrapDarkDecls(strategy, decls) {
619
+ switch (strategy) {
620
+ case "class":
621
+ return `.dark { ${decls} }`;
622
+ case "media":
623
+ return `@media (prefers-color-scheme: dark) { :root { ${decls} } }`;
624
+ case "attribute":
625
+ default:
626
+ return `[data-theme="dark"] { ${decls} }`;
627
+ }
628
+ }
629
+ var STYLE_ID = "kbach-colors";
630
+ var injectedForTheme = null;
631
+ function ensureColorVariablesInjected(theme) {
632
+ if (typeof document === "undefined") return;
633
+ if (injectedForTheme === theme) return;
634
+ injectedForTheme = theme;
635
+ const lightDecls = [];
636
+ const darkDecls = [];
637
+ for (const [name, value] of Object.entries(theme.colors)) {
638
+ if (typeof value === "string") continue;
639
+ const light = parseHexRgb(value.light);
640
+ const dark = parseHexRgb(value.dark);
641
+ if (!light || !dark) continue;
642
+ lightDecls.push(`--kb-color-${name}: ${light.join(",")};`);
643
+ darkDecls.push(`--kb-color-${name}: ${dark.join(",")};`);
644
+ }
645
+ let el = document.getElementById(STYLE_ID);
646
+ if (lightDecls.length === 0) {
647
+ el?.remove();
648
+ return;
649
+ }
650
+ if (!el) {
651
+ el = document.createElement("style");
652
+ el.id = STYLE_ID;
653
+ document.head.insertBefore(el, document.head.firstChild);
654
+ }
655
+ el.textContent = `:root { ${lightDecls.join(" ")} }
656
+ ${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
657
+ }
658
+
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
+
580
716
  // src/darkModeStore.ts
581
717
  var STORAGE_KEY = "kbach-theme";
582
718
  var listeners = /* @__PURE__ */ new Set();
@@ -664,64 +800,111 @@ function seedDefaultMode(fallback) {
664
800
  notify();
665
801
  }
666
802
 
667
- // src/useGlobalDarkMode.ts
668
- var import_react = require("react");
669
- var getServerSnapshot = () => false;
670
- function useGlobalDarkMode() {
671
- return (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, getServerSnapshot);
672
- }
673
-
674
- // src/ThemeProvider.tsx
675
- var import_react3 = require("react");
676
-
677
- // src/ThemeContext.tsx
678
- var import_react2 = require("react");
679
- var ThemeContext = (0, import_react2.createContext)(null);
803
+ // src/useTheme.ts
680
804
  function useTheme() {
681
- const ctx = (0, import_react2.useContext)(ThemeContext);
682
- if (!ctx) {
683
- throw new Error(
684
- 'useTheme() must be called within a <ThemeProvider>. For dark-mode state without a provider, use useGlobalDarkMode() (read) or toggleGlobalDarkMode()/setGlobalThemeMode() (write) from "@kbach/react" instead.'
685
- );
686
- }
687
- return ctx;
805
+ const isDark2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
806
+ const mode2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
807
+ return (0, import_react.useMemo)(
808
+ () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
809
+ [mode2, isDark2]
810
+ );
688
811
  }
689
812
 
690
813
  // src/ThemeProvider.tsx
814
+ var import_react2 = require("react");
691
815
  var import_jsx_runtime2 = require("react/jsx-runtime");
692
816
  var mountedProviderCount = 0;
693
817
  var warnedMultipleProviders = false;
694
818
  function ThemeProvider({ children, defaultMode = "system" }) {
695
- (0, import_react3.useEffect)(() => {
819
+ (0, import_react2.useEffect)(() => {
696
820
  mountedProviderCount++;
697
- if (mountedProviderCount > 1 && !warnedMultipleProviders) {
821
+ if (mountedProviderCount > 1 && !warnedMultipleProviders && process.env.NODE_ENV !== "production") {
698
822
  warnedMultipleProviders = true;
699
823
  console.warn(
700
- "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store (the same one useGlobalDarkMode()/toggleGlobalDarkMode() read and write), not scoped per-provider, so they will fight over the same state instead of each getting their own."
824
+ "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store useTheme() reads everywhere, not scoped per-provider, so their defaultMode seeds will fight over the same state."
701
825
  );
702
826
  }
703
827
  return () => {
704
828
  mountedProviderCount--;
705
829
  };
706
830
  }, []);
707
- const seeded = (0, import_react3.useRef)(false);
708
- (0, import_react3.useEffect)(() => {
831
+ const seeded = (0, import_react2.useRef)(false);
832
+ (0, import_react2.useEffect)(() => {
709
833
  if (seeded.current) return;
710
834
  seeded.current = true;
711
835
  seedDefaultMode(defaultMode);
712
836
  }, []);
713
- const isDark2 = (0, import_react3.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
714
- const mode2 = (0, import_react3.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => defaultMode);
715
- const value = (0, import_react3.useMemo)(
716
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
717
- [mode2, isDark2]
718
- );
719
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ThemeContext.Provider, { value, children });
837
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
838
+ }
839
+
840
+ // src/useColors.ts
841
+ function applyOpacity(color, opacity) {
842
+ const a = Math.max(0, Math.min(1, opacity / 100));
843
+ const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
844
+ if (varMatch) return `rgba(var(${varMatch[1]}), ${a})`;
845
+ if (color.startsWith("#")) {
846
+ const rgb = parseHexRgb(color);
847
+ return rgb ? `rgba(${rgb.join(",")},${a})` : color;
848
+ }
849
+ if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${a})`);
850
+ if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
851
+ return color;
852
+ }
853
+ function resolveEntry(name, entry, isDark2) {
854
+ if (typeof entry === "string") return entry;
855
+ const light = parseHexRgb(entry.light);
856
+ const dark = parseHexRgb(entry.dark);
857
+ if (light && dark) return `rgb(var(--kb-color-${name}))`;
858
+ return isDark2 ? entry.dark : entry.light;
859
+ }
860
+ function makeColorsProxy(theme, isDark2) {
861
+ const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
862
+ const cache = /* @__PURE__ */ new Map();
863
+ function resolveNamed(key) {
864
+ let name = key;
865
+ let opacity;
866
+ if (key.includes("/")) {
867
+ const slash = key.indexOf("/");
868
+ name = key.slice(0, slash);
869
+ opacity = Number(key.slice(slash + 1));
870
+ }
871
+ const entry = theme.colors[name];
872
+ if (entry === void 0) return void 0;
873
+ const value = resolveEntry(name, entry, isDark2);
874
+ return opacity === void 0 ? value : applyOpacity(value, opacity);
875
+ }
876
+ function makeFamilyProxy(family) {
877
+ return new Proxy({}, {
878
+ get(_, prop) {
879
+ const key = String(prop);
880
+ if (key === "then") return void 0;
881
+ return resolveNamed(`${family}-${key}`);
882
+ }
883
+ });
884
+ }
885
+ return new Proxy({ alpha }, {
886
+ get(_, prop) {
887
+ const key = String(prop);
888
+ if (key === "then") return void 0;
889
+ if (key === "alpha") return alpha;
890
+ const direct = resolveNamed(key);
891
+ if (direct !== void 0) return direct;
892
+ if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
893
+ return cache.get(key);
894
+ }
895
+ });
896
+ }
897
+ function useColors() {
898
+ const { isDark: isDark2 } = useTheme();
899
+ const theme = getTheme();
900
+ ensureColorVariablesInjected(theme);
901
+ return makeColorsProxy(theme, isDark2);
720
902
  }
721
903
  // Annotate the CommonJS export names for ESM import in node:
722
904
  0 && (module.exports = {
723
905
  KbachReset,
724
906
  ThemeProvider,
907
+ applyKbachConfig,
725
908
  defaultTheme,
726
909
  disableRuntimeCSS,
727
910
  getGlobalDarkMode,
@@ -731,10 +914,11 @@ function ThemeProvider({ children, defaultMode = "system" }) {
731
914
  isKbachReady,
732
915
  isRuntimeCSSDisabled,
733
916
  kb,
917
+ resolveKbachConfig,
734
918
  setGlobalThemeMode,
735
919
  setTheme,
736
920
  subscribeGlobalDarkMode,
737
921
  toggleGlobalDarkMode,
738
- useGlobalDarkMode,
922
+ useColors,
739
923
  useTheme
740
924
  });
package/dist/index.mjs CHANGED
@@ -4,14 +4,20 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-ROZXCQSM.mjs";
7
+ } from "./chunk-ROKZPEHA.mjs";
8
+ import {
9
+ applyKbachConfig,
10
+ ensureColorVariablesInjected,
11
+ parseHexRgb,
12
+ resolveKbachConfig
13
+ } from "./chunk-4Y2QXNJY.mjs";
8
14
  import {
9
15
  BASE_RESET,
10
16
  RESET_STYLE_ID,
11
17
  defaultTheme,
12
18
  getTheme,
13
19
  setTheme
14
- } from "./chunk-FOQWQDF4.mjs";
20
+ } from "./chunk-EOSN5CYG.mjs";
15
21
 
16
22
  // src/KbachReset.tsx
17
23
  import { jsx } from "react/jsx-runtime";
@@ -20,6 +26,9 @@ function KbachReset() {
20
26
  return /* @__PURE__ */ jsx("style", { id: RESET_STYLE_ID, children: BASE_RESET });
21
27
  }
22
28
 
29
+ // src/useTheme.ts
30
+ import { useMemo, useSyncExternalStore } from "react";
31
+
23
32
  // src/darkModeStore.ts
24
33
  var STORAGE_KEY = "kbach-theme";
25
34
  var listeners = /* @__PURE__ */ new Set();
@@ -107,40 +116,28 @@ function seedDefaultMode(fallback) {
107
116
  notify();
108
117
  }
109
118
 
110
- // src/useGlobalDarkMode.ts
111
- import { useSyncExternalStore } from "react";
112
- var getServerSnapshot = () => false;
113
- function useGlobalDarkMode() {
114
- return useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, getServerSnapshot);
115
- }
116
-
117
- // src/ThemeProvider.tsx
118
- import { useEffect, useMemo, useRef, useSyncExternalStore as useSyncExternalStore2 } from "react";
119
-
120
- // src/ThemeContext.tsx
121
- import { createContext, useContext } from "react";
122
- var ThemeContext = createContext(null);
119
+ // src/useTheme.ts
123
120
  function useTheme() {
124
- const ctx = useContext(ThemeContext);
125
- if (!ctx) {
126
- throw new Error(
127
- 'useTheme() must be called within a <ThemeProvider>. For dark-mode state without a provider, use useGlobalDarkMode() (read) or toggleGlobalDarkMode()/setGlobalThemeMode() (write) from "@kbach/react" instead.'
128
- );
129
- }
130
- return ctx;
121
+ const isDark2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
122
+ const mode2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
123
+ return useMemo(
124
+ () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
125
+ [mode2, isDark2]
126
+ );
131
127
  }
132
128
 
133
129
  // src/ThemeProvider.tsx
134
- import { jsx as jsx2 } from "react/jsx-runtime";
130
+ import { useEffect, useRef } from "react";
131
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
135
132
  var mountedProviderCount = 0;
136
133
  var warnedMultipleProviders = false;
137
134
  function ThemeProvider({ children, defaultMode = "system" }) {
138
135
  useEffect(() => {
139
136
  mountedProviderCount++;
140
- if (mountedProviderCount > 1 && !warnedMultipleProviders) {
137
+ if (mountedProviderCount > 1 && !warnedMultipleProviders && process.env.NODE_ENV !== "production") {
141
138
  warnedMultipleProviders = true;
142
139
  console.warn(
143
- "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store (the same one useGlobalDarkMode()/toggleGlobalDarkMode() read and write), not scoped per-provider, so they will fight over the same state instead of each getting their own."
140
+ "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store useTheme() reads everywhere, not scoped per-provider, so their defaultMode seeds will fight over the same state."
144
141
  );
145
142
  }
146
143
  return () => {
@@ -153,17 +150,76 @@ function ThemeProvider({ children, defaultMode = "system" }) {
153
150
  seeded.current = true;
154
151
  seedDefaultMode(defaultMode);
155
152
  }, []);
156
- const isDark2 = useSyncExternalStore2(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
157
- const mode2 = useSyncExternalStore2(subscribeGlobalDarkMode, getGlobalThemeMode, () => defaultMode);
158
- const value = useMemo(
159
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
160
- [mode2, isDark2]
161
- );
162
- return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children });
153
+ return /* @__PURE__ */ jsx2(Fragment, { children });
154
+ }
155
+
156
+ // src/useColors.ts
157
+ function applyOpacity(color, opacity) {
158
+ const a = Math.max(0, Math.min(1, opacity / 100));
159
+ const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
160
+ if (varMatch) return `rgba(var(${varMatch[1]}), ${a})`;
161
+ if (color.startsWith("#")) {
162
+ const rgb = parseHexRgb(color);
163
+ return rgb ? `rgba(${rgb.join(",")},${a})` : color;
164
+ }
165
+ if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${a})`);
166
+ if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
167
+ return color;
168
+ }
169
+ function resolveEntry(name, entry, isDark2) {
170
+ if (typeof entry === "string") return entry;
171
+ const light = parseHexRgb(entry.light);
172
+ const dark = parseHexRgb(entry.dark);
173
+ if (light && dark) return `rgb(var(--kb-color-${name}))`;
174
+ return isDark2 ? entry.dark : entry.light;
175
+ }
176
+ function makeColorsProxy(theme, isDark2) {
177
+ const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
178
+ const cache = /* @__PURE__ */ new Map();
179
+ function resolveNamed(key) {
180
+ let name = key;
181
+ let opacity;
182
+ if (key.includes("/")) {
183
+ const slash = key.indexOf("/");
184
+ name = key.slice(0, slash);
185
+ opacity = Number(key.slice(slash + 1));
186
+ }
187
+ const entry = theme.colors[name];
188
+ if (entry === void 0) return void 0;
189
+ const value = resolveEntry(name, entry, isDark2);
190
+ return opacity === void 0 ? value : applyOpacity(value, opacity);
191
+ }
192
+ function makeFamilyProxy(family) {
193
+ return new Proxy({}, {
194
+ get(_, prop) {
195
+ const key = String(prop);
196
+ if (key === "then") return void 0;
197
+ return resolveNamed(`${family}-${key}`);
198
+ }
199
+ });
200
+ }
201
+ return new Proxy({ alpha }, {
202
+ get(_, prop) {
203
+ const key = String(prop);
204
+ if (key === "then") return void 0;
205
+ if (key === "alpha") return alpha;
206
+ const direct = resolveNamed(key);
207
+ if (direct !== void 0) return direct;
208
+ if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
209
+ return cache.get(key);
210
+ }
211
+ });
212
+ }
213
+ function useColors() {
214
+ const { isDark: isDark2 } = useTheme();
215
+ const theme = getTheme();
216
+ ensureColorVariablesInjected(theme);
217
+ return makeColorsProxy(theme, isDark2);
163
218
  }
164
219
  export {
165
220
  KbachReset,
166
221
  ThemeProvider,
222
+ applyKbachConfig,
167
223
  defaultTheme,
168
224
  disableRuntimeCSS,
169
225
  getGlobalDarkMode,
@@ -173,10 +229,11 @@ export {
173
229
  isKbachReady,
174
230
  isRuntimeCSSDisabled,
175
231
  kb,
232
+ resolveKbachConfig,
176
233
  setGlobalThemeMode,
177
234
  setTheme,
178
235
  subscribeGlobalDarkMode,
179
236
  toggleGlobalDarkMode,
180
- useGlobalDarkMode,
237
+ useColors,
181
238
  useTheme
182
239
  };
@@ -337,14 +337,15 @@ var defaultTheme = {
337
337
  // Real Tailwind's default spacing scale in full — every step is exactly
338
338
  // `n * 4px` (Tailwind's base spacing unit), including the fractional
339
339
  // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
340
- // literal (a bare 1px, for `p-px`-style hairline spacing). Previously
341
- // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
342
- // the real scale — so anything else (`p-3`, `w-5`, `border-2`, `gap-10`,
343
- // ...) silently failed to resolve (an unknown spacing key returns `None`
344
- // the same way an unknown class does, with no error). "0" itself is
345
- // still needed explicitly (not derivable as "any number * 4") for the
346
- // same reason it always was: `sticky top-0` needs an actual "0" key to
347
- // resolve at all, not just fall through as unresolvable.
340
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
341
+ // only the "usual stops", though — the Rust/JS engines' shared
342
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
343
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
344
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
345
+ // values and give a theme customization something to override, not to
346
+ // enumerate every resolvable step. "px" is the one exception that must
347
+ // stay listed explicitly — it isn't a number, so the formula can't
348
+ // derive it.
348
349
  spacing: {
349
350
  "0": 0,
350
351
  "px": 1,
@@ -389,7 +390,33 @@ var defaultTheme = {
389
390
  xl: 1280,
390
391
  "2xl": 1536
391
392
  },
392
- darkMode: "attribute"
393
+ // Byte-for-byte the same three stacks
394
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
395
+ // falls back to — kept here too (rather than leaving this empty and
396
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
397
+ // is a real, readable value for anything that wants to extend rather
398
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
399
+ fontFamily: {
400
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
401
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
402
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
403
+ },
404
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
405
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
406
+ // using only static `className` strings gets working `dark:` classes
407
+ // out of the box, with no runtime import required to write `data-theme`/
408
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
409
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
410
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
411
+ // for JS consumers, but flipping it has no visible effect on `dark:`
412
+ // classes, since the browser's media query only ever reflects the real
413
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
414
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
415
+ // as an app wants a real in-app toggle.
416
+ darkMode: "media",
417
+ // Empty by default, matching real Tailwind's own defaults (no forced
418
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
419
+ container: {}
393
420
  };
394
421
  var activeThemeJson = JSON.stringify(defaultTheme);
395
422
  function getThemeJson() {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-AD3G6RHY.mjs";
6
- import "./chunk-ROZXCQSM.mjs";
7
- import "./chunk-FOQWQDF4.mjs";
5
+ } from "./chunk-L6B4DTLD.mjs";
6
+ import "./chunk-ROKZPEHA.mjs";
7
+ import "./chunk-EOSN5CYG.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {