@kbach/react 1.0.0-beta.0 → 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);
@@ -357,21 +359,54 @@ var defaultTheme = {
357
359
  ...PALETTE,
358
360
  surface: { light: "#f9fafb", dark: "#111827" }
359
361
  },
362
+ // Real Tailwind's default spacing scale in full — every step is exactly
363
+ // `n * 4px` (Tailwind's base spacing unit), including the fractional
364
+ // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
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.
360
374
  spacing: {
361
- // "0" is always a hardcoded zero in real Tailwind's own scale too,
362
- // regardless of the base spacing unit — needed for `p-0`/`m-0`/
363
- // `top-0`/`inset-0`/etc., and specifically for `sticky top-0` (a
364
- // sticky element needs an explicit offset to have any stick point at
365
- // all; without one it behaves just like `relative`, no different from
366
- // omitting `top-0` outright). Confirmed missing by hand: this
367
- // sandbox's own header used `sticky top-0`, which silently never
368
- // resolved (no "0" spacing key existed) until this was added.
369
375
  "0": 0,
376
+ "px": 1,
377
+ "0.5": 2,
370
378
  "1": 4,
379
+ "1.5": 6,
371
380
  "2": 8,
381
+ "2.5": 10,
382
+ "3": 12,
383
+ "3.5": 14,
372
384
  "4": 16,
385
+ "5": 20,
373
386
  "6": 24,
374
- "8": 32
387
+ "7": 28,
388
+ "8": 32,
389
+ "9": 36,
390
+ "10": 40,
391
+ "11": 44,
392
+ "12": 48,
393
+ "14": 56,
394
+ "16": 64,
395
+ "20": 80,
396
+ "24": 96,
397
+ "28": 112,
398
+ "32": 128,
399
+ "36": 144,
400
+ "40": 160,
401
+ "44": 176,
402
+ "48": 192,
403
+ "52": 208,
404
+ "56": 224,
405
+ "60": 240,
406
+ "64": 256,
407
+ "72": 288,
408
+ "80": 320,
409
+ "96": 384
375
410
  },
376
411
  screens: {
377
412
  sm: 640,
@@ -380,7 +415,33 @@ var defaultTheme = {
380
415
  xl: 1280,
381
416
  "2xl": 1536
382
417
  },
383
- 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: {}
384
445
  };
385
446
  var activeTheme = defaultTheme;
386
447
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -545,6 +606,113 @@ function KbachReset() {
545
606
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { id: RESET_STYLE_ID, children: BASE_RESET });
546
607
  }
547
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
+
548
716
  // src/darkModeStore.ts
549
717
  var STORAGE_KEY = "kbach-theme";
550
718
  var listeners = /* @__PURE__ */ new Set();
@@ -632,64 +800,111 @@ function seedDefaultMode(fallback) {
632
800
  notify();
633
801
  }
634
802
 
635
- // src/useGlobalDarkMode.ts
636
- var import_react = require("react");
637
- var getServerSnapshot = () => false;
638
- function useGlobalDarkMode() {
639
- return (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, getServerSnapshot);
640
- }
641
-
642
- // src/ThemeProvider.tsx
643
- var import_react3 = require("react");
644
-
645
- // src/ThemeContext.tsx
646
- var import_react2 = require("react");
647
- var ThemeContext = (0, import_react2.createContext)(null);
803
+ // src/useTheme.ts
648
804
  function useTheme() {
649
- const ctx = (0, import_react2.useContext)(ThemeContext);
650
- if (!ctx) {
651
- throw new Error(
652
- '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.'
653
- );
654
- }
655
- 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
+ );
656
811
  }
657
812
 
658
813
  // src/ThemeProvider.tsx
814
+ var import_react2 = require("react");
659
815
  var import_jsx_runtime2 = require("react/jsx-runtime");
660
816
  var mountedProviderCount = 0;
661
817
  var warnedMultipleProviders = false;
662
818
  function ThemeProvider({ children, defaultMode = "system" }) {
663
- (0, import_react3.useEffect)(() => {
819
+ (0, import_react2.useEffect)(() => {
664
820
  mountedProviderCount++;
665
- if (mountedProviderCount > 1 && !warnedMultipleProviders) {
821
+ if (mountedProviderCount > 1 && !warnedMultipleProviders && process.env.NODE_ENV !== "production") {
666
822
  warnedMultipleProviders = true;
667
823
  console.warn(
668
- "[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."
669
825
  );
670
826
  }
671
827
  return () => {
672
828
  mountedProviderCount--;
673
829
  };
674
830
  }, []);
675
- const seeded = (0, import_react3.useRef)(false);
676
- (0, import_react3.useEffect)(() => {
831
+ const seeded = (0, import_react2.useRef)(false);
832
+ (0, import_react2.useEffect)(() => {
677
833
  if (seeded.current) return;
678
834
  seeded.current = true;
679
835
  seedDefaultMode(defaultMode);
680
836
  }, []);
681
- const isDark2 = (0, import_react3.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
682
- const mode2 = (0, import_react3.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => defaultMode);
683
- const value = (0, import_react3.useMemo)(
684
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
685
- [mode2, isDark2]
686
- );
687
- 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);
688
902
  }
689
903
  // Annotate the CommonJS export names for ESM import in node:
690
904
  0 && (module.exports = {
691
905
  KbachReset,
692
906
  ThemeProvider,
907
+ applyKbachConfig,
693
908
  defaultTheme,
694
909
  disableRuntimeCSS,
695
910
  getGlobalDarkMode,
@@ -699,10 +914,11 @@ function ThemeProvider({ children, defaultMode = "system" }) {
699
914
  isKbachReady,
700
915
  isRuntimeCSSDisabled,
701
916
  kb,
917
+ resolveKbachConfig,
702
918
  setGlobalThemeMode,
703
919
  setTheme,
704
920
  subscribeGlobalDarkMode,
705
921
  toggleGlobalDarkMode,
706
- useGlobalDarkMode,
922
+ useColors,
707
923
  useTheme
708
924
  });
package/dist/index.mjs CHANGED
@@ -4,14 +4,20 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-G3RJJNSY.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-AFY36PRX.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
  };
@@ -334,21 +334,54 @@ var defaultTheme = {
334
334
  ...PALETTE,
335
335
  surface: { light: "#f9fafb", dark: "#111827" }
336
336
  },
337
+ // Real Tailwind's default spacing scale in full — every step is exactly
338
+ // `n * 4px` (Tailwind's base spacing unit), including the fractional
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). 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.
337
349
  spacing: {
338
- // "0" is always a hardcoded zero in real Tailwind's own scale too,
339
- // regardless of the base spacing unit — needed for `p-0`/`m-0`/
340
- // `top-0`/`inset-0`/etc., and specifically for `sticky top-0` (a
341
- // sticky element needs an explicit offset to have any stick point at
342
- // all; without one it behaves just like `relative`, no different from
343
- // omitting `top-0` outright). Confirmed missing by hand: this
344
- // sandbox's own header used `sticky top-0`, which silently never
345
- // resolved (no "0" spacing key existed) until this was added.
346
350
  "0": 0,
351
+ "px": 1,
352
+ "0.5": 2,
347
353
  "1": 4,
354
+ "1.5": 6,
348
355
  "2": 8,
356
+ "2.5": 10,
357
+ "3": 12,
358
+ "3.5": 14,
349
359
  "4": 16,
360
+ "5": 20,
350
361
  "6": 24,
351
- "8": 32
362
+ "7": 28,
363
+ "8": 32,
364
+ "9": 36,
365
+ "10": 40,
366
+ "11": 44,
367
+ "12": 48,
368
+ "14": 56,
369
+ "16": 64,
370
+ "20": 80,
371
+ "24": 96,
372
+ "28": 112,
373
+ "32": 128,
374
+ "36": 144,
375
+ "40": 160,
376
+ "44": 176,
377
+ "48": 192,
378
+ "52": 208,
379
+ "56": 224,
380
+ "60": 240,
381
+ "64": 256,
382
+ "72": 288,
383
+ "80": 320,
384
+ "96": 384
352
385
  },
353
386
  screens: {
354
387
  sm: 640,
@@ -357,7 +390,33 @@ var defaultTheme = {
357
390
  xl: 1280,
358
391
  "2xl": 1536
359
392
  },
360
- 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: {}
361
420
  };
362
421
  var activeThemeJson = JSON.stringify(defaultTheme);
363
422
  function getThemeJson() {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-VBPQAIZL.mjs";
6
- import "./chunk-G3RJJNSY.mjs";
7
- import "./chunk-AFY36PRX.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) {