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

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.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { T as ThemeConfig, d as defaultTheme, g as getTheme, s as setTheme } from './theme-BKAb78KP.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-BQ_b-qxF.mjs';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -61,16 +61,23 @@ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
61
61
  declare function toggleGlobalDarkMode(): void;
62
62
  declare function subscribeGlobalDarkMode(listener: () => void): () => void;
63
63
 
64
+ interface ThemeState {
65
+ mode: ThemeMode;
66
+ isDark: boolean;
67
+ setMode: (mode: ThemeMode) => void;
68
+ toggle: () => void;
69
+ }
64
70
  /**
65
- * Reactive read of the global dark-mode store — works with or without a
66
- * `<ThemeProvider>` mounted anywhere in the tree, since it subscribes to
67
- * `darkModeStore.ts` directly. `useSyncExternalStore`'s SSR snapshot always
68
- * returns `false` (light), matching `applyToDom`'s own server no-op — the
69
- * server and the client's very first render agree, so there's no
70
- * hydration-mismatch warning; the real value (if different) applies on the
71
- * client immediately after.
71
+ * The one hook for reading and writing Kbach's dark-mode state. Reads
72
+ * `darkModeStore.ts`'s global store directly via `useSyncExternalStore` —
73
+ * that module-level store, not React Context, is the actual source of
74
+ * truth (every provider used to just mirror it into a Context value), so
75
+ * this works from ANY component, with or without a `<ThemeProvider>`
76
+ * mounted anywhere in the tree. `<ThemeProvider>` still exists, but purely
77
+ * as an optional convenience for seeding an app's initial `defaultMode` —
78
+ * see its own doc comment.
72
79
  */
73
- declare function useGlobalDarkMode(): boolean;
80
+ declare function useTheme(): ThemeState;
74
81
 
75
82
  interface ThemeProviderProps {
76
83
  children: ReactNode;
@@ -82,36 +89,60 @@ interface ThemeProviderProps {
82
89
  defaultMode?: ThemeMode;
83
90
  }
84
91
  /**
85
- * Optional React Context wrapper around `darkModeStore.ts`'s single global
86
- * store — port of old-kbach's `ThemeProvider` (`packages/ui/src/
87
- * ThemeProvider.tsx`), same "provider is a thin wrapper around a global
88
- * store, not the source of truth itself" shape, simplified to this
89
- * package's web-only scope (no native `useColorScheme()`/`windowWidth`
90
- * branch, no `disablePersistence` prop — persistence is always on, since
91
- * `darkModeStore.ts` already guards every `localStorage` access for
92
- * environments where it's unavailable).
92
+ * Optional — `useTheme()` reads `darkModeStore.ts`'s global store directly
93
+ * and works anywhere without this mounted at all. The only thing this
94
+ * component does is seed that store's initial `defaultMode` once, for apps
95
+ * that want to declare a startup default in JSX rather than calling
96
+ * `setGlobalThemeMode()` imperatively before their first render. It
97
+ * renders `children` as-is — no Context.Provider, since there's no value
98
+ * left to distribute; `useTheme()` never reads from Context.
93
99
  *
94
- * Because the store is global, MULTIPLE mounted providers share the exact
95
- * same `mode`/`isDark` state rather than each getting their own scoped
96
- * value — mounting more than one is very likely a mistake (each fighting
97
- * over the same state via a different `defaultMode`), so this warns once
98
- * in dev, matching old-kbach's identical guard.
100
+ * Because the store is global, MULTIPLE mounted providers would each try
101
+ * to seed the exact same store rather than getting their own scoped
102
+ * state — mounting more than one is very likely a mistake, so this warns
103
+ * once in dev.
99
104
  */
100
105
  declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
101
106
 
102
- interface ThemeContextValue {
103
- mode: ThemeMode;
104
- isDark: boolean;
105
- setMode: (mode: ThemeMode) => void;
106
- toggle: () => void;
107
+ interface ColorScale {
108
+ /** `colors.blue[6]` -> raw value */
109
+ readonly [shade: number]: string;
110
+ /** `colors.blue['6/50']` -> shade 6 at 50% opacity */
111
+ readonly [key: string]: string;
107
112
  }
113
+ type ColorsAPI = {
114
+ /** `colors.white`, `colors.blue` (a shade family), `colors.surface`, ... */
115
+ readonly [name: string]: string | ColorScale;
116
+ } & {
117
+ /**
118
+ * Applies an opacity (0–100) to any color string this hook can return —
119
+ * a hex, `rgb()`/`rgba()`, or one of this hook's own
120
+ * `rgb(var(--kb-color-x))` mode-aware references.
121
+ */
122
+ readonly alpha: (color: string, opacity?: number) => string;
123
+ };
108
124
  /**
109
- * Requires a `<ThemeProvider>` ancestor — throws otherwise, since a
110
- * component that destructures `{ mode, setMode }` genuinely needs Context
111
- * (unlike a component that just wants the dark/light boolean, which should
112
- * reach for `useGlobalDarkMode()` instead and never needs a provider at
113
- * all).
125
+ * Resolved theme colors as real values — for anywhere you need an actual
126
+ * color (an SVG `fill`, a canvas context, a chart library prop), not a
127
+ * `className`. Ported from old-kbach's `useColors()`
128
+ * (`old-kbach/packages/ui/src/useColors.ts`), adapted to this engine's flat
129
+ * `"blue-6"`-keyed palette (`colors.blue[6]` looks up `"blue-6"`).
130
+ *
131
+ * Any color — a shade-family entry like `"blue-6"` or a standalone name
132
+ * like `"surface"` — can be defined in the theme as either a plain string
133
+ * (static, same in both modes) or a `{ light, dark }` pair — fully manual,
134
+ * no automatic shade-shifting or other assumption about what a "dark
135
+ * variant" should be; see `resolveEntry`'s own doc comment for how a
136
+ * `{ light, dark }` entry resolves.
137
+ *
138
+ * ```ts
139
+ * const colors = useColors();
140
+ * colors.blue[6]; // "#3b82f6" — static, same value in both modes
141
+ * colors.blue['6/50']; // same, at 50% opacity
142
+ * colors.surface; // "rgb(var(--kb-color-surface))" if defined as { light, dark }
143
+ * colors.alpha(colors.surface, 50); // "rgba(var(--kb-color-surface), 0.5)"
144
+ * ```
114
145
  */
115
- declare function useTheme(): ThemeContextValue;
146
+ declare function useColors(): ColorsAPI;
116
147
 
117
- export { KbachReset, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, disableRuntimeCSS, getGlobalDarkMode, getGlobalThemeMode, initKbach, isKbachReady, isRuntimeCSSDisabled, kb, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode, useGlobalDarkMode, useTheme };
148
+ export { type ColorScale, type ColorsAPI, KbachReset, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeState, disableRuntimeCSS, getGlobalDarkMode, getGlobalThemeMode, initKbach, isKbachReady, isRuntimeCSSDisabled, kb, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode, useColors, useTheme };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { T as ThemeConfig, d as defaultTheme, g as getTheme, s as setTheme } from './theme-BKAb78KP.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-BQ_b-qxF.js';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -61,16 +61,23 @@ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
61
61
  declare function toggleGlobalDarkMode(): void;
62
62
  declare function subscribeGlobalDarkMode(listener: () => void): () => void;
63
63
 
64
+ interface ThemeState {
65
+ mode: ThemeMode;
66
+ isDark: boolean;
67
+ setMode: (mode: ThemeMode) => void;
68
+ toggle: () => void;
69
+ }
64
70
  /**
65
- * Reactive read of the global dark-mode store — works with or without a
66
- * `<ThemeProvider>` mounted anywhere in the tree, since it subscribes to
67
- * `darkModeStore.ts` directly. `useSyncExternalStore`'s SSR snapshot always
68
- * returns `false` (light), matching `applyToDom`'s own server no-op — the
69
- * server and the client's very first render agree, so there's no
70
- * hydration-mismatch warning; the real value (if different) applies on the
71
- * client immediately after.
71
+ * The one hook for reading and writing Kbach's dark-mode state. Reads
72
+ * `darkModeStore.ts`'s global store directly via `useSyncExternalStore` —
73
+ * that module-level store, not React Context, is the actual source of
74
+ * truth (every provider used to just mirror it into a Context value), so
75
+ * this works from ANY component, with or without a `<ThemeProvider>`
76
+ * mounted anywhere in the tree. `<ThemeProvider>` still exists, but purely
77
+ * as an optional convenience for seeding an app's initial `defaultMode` —
78
+ * see its own doc comment.
72
79
  */
73
- declare function useGlobalDarkMode(): boolean;
80
+ declare function useTheme(): ThemeState;
74
81
 
75
82
  interface ThemeProviderProps {
76
83
  children: ReactNode;
@@ -82,36 +89,60 @@ interface ThemeProviderProps {
82
89
  defaultMode?: ThemeMode;
83
90
  }
84
91
  /**
85
- * Optional React Context wrapper around `darkModeStore.ts`'s single global
86
- * store — port of old-kbach's `ThemeProvider` (`packages/ui/src/
87
- * ThemeProvider.tsx`), same "provider is a thin wrapper around a global
88
- * store, not the source of truth itself" shape, simplified to this
89
- * package's web-only scope (no native `useColorScheme()`/`windowWidth`
90
- * branch, no `disablePersistence` prop — persistence is always on, since
91
- * `darkModeStore.ts` already guards every `localStorage` access for
92
- * environments where it's unavailable).
92
+ * Optional — `useTheme()` reads `darkModeStore.ts`'s global store directly
93
+ * and works anywhere without this mounted at all. The only thing this
94
+ * component does is seed that store's initial `defaultMode` once, for apps
95
+ * that want to declare a startup default in JSX rather than calling
96
+ * `setGlobalThemeMode()` imperatively before their first render. It
97
+ * renders `children` as-is — no Context.Provider, since there's no value
98
+ * left to distribute; `useTheme()` never reads from Context.
93
99
  *
94
- * Because the store is global, MULTIPLE mounted providers share the exact
95
- * same `mode`/`isDark` state rather than each getting their own scoped
96
- * value — mounting more than one is very likely a mistake (each fighting
97
- * over the same state via a different `defaultMode`), so this warns once
98
- * in dev, matching old-kbach's identical guard.
100
+ * Because the store is global, MULTIPLE mounted providers would each try
101
+ * to seed the exact same store rather than getting their own scoped
102
+ * state — mounting more than one is very likely a mistake, so this warns
103
+ * once in dev.
99
104
  */
100
105
  declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
101
106
 
102
- interface ThemeContextValue {
103
- mode: ThemeMode;
104
- isDark: boolean;
105
- setMode: (mode: ThemeMode) => void;
106
- toggle: () => void;
107
+ interface ColorScale {
108
+ /** `colors.blue[6]` -> raw value */
109
+ readonly [shade: number]: string;
110
+ /** `colors.blue['6/50']` -> shade 6 at 50% opacity */
111
+ readonly [key: string]: string;
107
112
  }
113
+ type ColorsAPI = {
114
+ /** `colors.white`, `colors.blue` (a shade family), `colors.surface`, ... */
115
+ readonly [name: string]: string | ColorScale;
116
+ } & {
117
+ /**
118
+ * Applies an opacity (0–100) to any color string this hook can return —
119
+ * a hex, `rgb()`/`rgba()`, or one of this hook's own
120
+ * `rgb(var(--kb-color-x))` mode-aware references.
121
+ */
122
+ readonly alpha: (color: string, opacity?: number) => string;
123
+ };
108
124
  /**
109
- * Requires a `<ThemeProvider>` ancestor — throws otherwise, since a
110
- * component that destructures `{ mode, setMode }` genuinely needs Context
111
- * (unlike a component that just wants the dark/light boolean, which should
112
- * reach for `useGlobalDarkMode()` instead and never needs a provider at
113
- * all).
125
+ * Resolved theme colors as real values — for anywhere you need an actual
126
+ * color (an SVG `fill`, a canvas context, a chart library prop), not a
127
+ * `className`. Ported from old-kbach's `useColors()`
128
+ * (`old-kbach/packages/ui/src/useColors.ts`), adapted to this engine's flat
129
+ * `"blue-6"`-keyed palette (`colors.blue[6]` looks up `"blue-6"`).
130
+ *
131
+ * Any color — a shade-family entry like `"blue-6"` or a standalone name
132
+ * like `"surface"` — can be defined in the theme as either a plain string
133
+ * (static, same in both modes) or a `{ light, dark }` pair — fully manual,
134
+ * no automatic shade-shifting or other assumption about what a "dark
135
+ * variant" should be; see `resolveEntry`'s own doc comment for how a
136
+ * `{ light, dark }` entry resolves.
137
+ *
138
+ * ```ts
139
+ * const colors = useColors();
140
+ * colors.blue[6]; // "#3b82f6" — static, same value in both modes
141
+ * colors.blue['6/50']; // same, at 50% opacity
142
+ * colors.surface; // "rgb(var(--kb-color-surface))" if defined as { light, dark }
143
+ * colors.alpha(colors.surface, 50); // "rgba(var(--kb-color-surface), 0.5)"
144
+ * ```
114
145
  */
115
- declare function useTheme(): ThemeContextValue;
146
+ declare function useColors(): ColorsAPI;
116
147
 
117
- export { KbachReset, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, disableRuntimeCSS, getGlobalDarkMode, getGlobalThemeMode, initKbach, isKbachReady, isRuntimeCSSDisabled, kb, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode, useGlobalDarkMode, useTheme };
148
+ export { type ColorScale, type ColorsAPI, KbachReset, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeState, disableRuntimeCSS, getGlobalDarkMode, getGlobalThemeMode, initKbach, isKbachReady, isRuntimeCSSDisabled, kb, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode, useColors, useTheme };
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,21 @@ 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
+ darkMode: "attribute",
430
+ // Empty by default, matching real Tailwind's own defaults (no forced
431
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
432
+ container: {}
416
433
  };
417
434
  var activeTheme = defaultTheme;
418
435
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -577,6 +594,113 @@ function KbachReset() {
577
594
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { id: RESET_STYLE_ID, children: BASE_RESET });
578
595
  }
579
596
 
597
+ // src/colorVariables.ts
598
+ function parseHexRgb(hex) {
599
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
600
+ if (!m) return null;
601
+ let h = m[1];
602
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
603
+ const n = parseInt(h, 16);
604
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
605
+ }
606
+ function wrapDarkDecls(strategy, decls) {
607
+ switch (strategy) {
608
+ case "class":
609
+ return `.dark { ${decls} }`;
610
+ case "media":
611
+ return `@media (prefers-color-scheme: dark) { :root { ${decls} } }`;
612
+ case "attribute":
613
+ default:
614
+ return `[data-theme="dark"] { ${decls} }`;
615
+ }
616
+ }
617
+ var STYLE_ID = "kbach-colors";
618
+ var injectedForTheme = null;
619
+ function ensureColorVariablesInjected(theme) {
620
+ if (typeof document === "undefined") return;
621
+ if (injectedForTheme === theme) return;
622
+ injectedForTheme = theme;
623
+ const lightDecls = [];
624
+ const darkDecls = [];
625
+ for (const [name, value] of Object.entries(theme.colors)) {
626
+ if (typeof value === "string") continue;
627
+ const light = parseHexRgb(value.light);
628
+ const dark = parseHexRgb(value.dark);
629
+ if (!light || !dark) continue;
630
+ lightDecls.push(`--kb-color-${name}: ${light.join(",")};`);
631
+ darkDecls.push(`--kb-color-${name}: ${dark.join(",")};`);
632
+ }
633
+ let el = document.getElementById(STYLE_ID);
634
+ if (lightDecls.length === 0) {
635
+ el?.remove();
636
+ return;
637
+ }
638
+ if (!el) {
639
+ el = document.createElement("style");
640
+ el.id = STYLE_ID;
641
+ document.head.insertBefore(el, document.head.firstChild);
642
+ }
643
+ el.textContent = `:root { ${lightDecls.join(" ")} }
644
+ ${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
645
+ }
646
+
647
+ // src/config.ts
648
+ function applyOpacityToHex(hex, opacityText) {
649
+ const rgb = parseHexRgb(hex);
650
+ if (!rgb) return hex;
651
+ const a = Number(opacityText) / 100;
652
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
653
+ return `rgba(${rgb.join(",")},${clamped})`;
654
+ }
655
+ function resolveColorEntry(value, lookup) {
656
+ if (typeof value !== "string") return value;
657
+ const slash = value.indexOf("/");
658
+ const name = slash === -1 ? value : value.slice(0, slash);
659
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
660
+ const base = lookup(name);
661
+ if (base === void 0) return value;
662
+ if (opacity === void 0) return base;
663
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
664
+ }
665
+ function resolveColors(extend, existing) {
666
+ const resolved = {};
667
+ const lookup = (name) => resolved[name] ?? existing[name];
668
+ for (const [name, value] of Object.entries(extend)) {
669
+ resolved[name] = resolveColorEntry(value, lookup);
670
+ }
671
+ return resolved;
672
+ }
673
+ function resolveKbachConfig(config) {
674
+ let theme = { ...defaultTheme, ...config.theme };
675
+ if (config.extend?.colors) {
676
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
677
+ }
678
+ if (config.extend?.spacing) {
679
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
680
+ }
681
+ if (config.extend?.screens) {
682
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
683
+ }
684
+ if (config.extend?.fontFamily) {
685
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
686
+ }
687
+ if (config.extend?.container) {
688
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
689
+ }
690
+ if (config.darkMode) {
691
+ theme = { ...theme, darkMode: config.darkMode };
692
+ }
693
+ return theme;
694
+ }
695
+ function applyKbachConfig(config) {
696
+ const theme = resolveKbachConfig(config);
697
+ setTheme(theme);
698
+ return theme;
699
+ }
700
+
701
+ // src/useTheme.ts
702
+ var import_react = require("react");
703
+
580
704
  // src/darkModeStore.ts
581
705
  var STORAGE_KEY = "kbach-theme";
582
706
  var listeners = /* @__PURE__ */ new Set();
@@ -664,64 +788,111 @@ function seedDefaultMode(fallback) {
664
788
  notify();
665
789
  }
666
790
 
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);
791
+ // src/useTheme.ts
680
792
  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;
793
+ const isDark2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
794
+ const mode2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
795
+ return (0, import_react.useMemo)(
796
+ () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
797
+ [mode2, isDark2]
798
+ );
688
799
  }
689
800
 
690
801
  // src/ThemeProvider.tsx
802
+ var import_react2 = require("react");
691
803
  var import_jsx_runtime2 = require("react/jsx-runtime");
692
804
  var mountedProviderCount = 0;
693
805
  var warnedMultipleProviders = false;
694
806
  function ThemeProvider({ children, defaultMode = "system" }) {
695
- (0, import_react3.useEffect)(() => {
807
+ (0, import_react2.useEffect)(() => {
696
808
  mountedProviderCount++;
697
809
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
698
810
  warnedMultipleProviders = true;
699
811
  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."
812
+ "[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
813
  );
702
814
  }
703
815
  return () => {
704
816
  mountedProviderCount--;
705
817
  };
706
818
  }, []);
707
- const seeded = (0, import_react3.useRef)(false);
708
- (0, import_react3.useEffect)(() => {
819
+ const seeded = (0, import_react2.useRef)(false);
820
+ (0, import_react2.useEffect)(() => {
709
821
  if (seeded.current) return;
710
822
  seeded.current = true;
711
823
  seedDefaultMode(defaultMode);
712
824
  }, []);
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 });
825
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
826
+ }
827
+
828
+ // src/useColors.ts
829
+ function applyOpacity(color, opacity) {
830
+ const a = Math.max(0, Math.min(1, opacity / 100));
831
+ const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
832
+ if (varMatch) return `rgba(var(${varMatch[1]}), ${a})`;
833
+ if (color.startsWith("#")) {
834
+ const rgb = parseHexRgb(color);
835
+ return rgb ? `rgba(${rgb.join(",")},${a})` : color;
836
+ }
837
+ if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${a})`);
838
+ if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
839
+ return color;
840
+ }
841
+ function resolveEntry(name, entry, isDark2) {
842
+ if (typeof entry === "string") return entry;
843
+ const light = parseHexRgb(entry.light);
844
+ const dark = parseHexRgb(entry.dark);
845
+ if (light && dark) return `rgb(var(--kb-color-${name}))`;
846
+ return isDark2 ? entry.dark : entry.light;
847
+ }
848
+ function makeColorsProxy(theme, isDark2) {
849
+ const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
850
+ const cache = /* @__PURE__ */ new Map();
851
+ function resolveNamed(key) {
852
+ let name = key;
853
+ let opacity;
854
+ if (key.includes("/")) {
855
+ const slash = key.indexOf("/");
856
+ name = key.slice(0, slash);
857
+ opacity = Number(key.slice(slash + 1));
858
+ }
859
+ const entry = theme.colors[name];
860
+ if (entry === void 0) return void 0;
861
+ const value = resolveEntry(name, entry, isDark2);
862
+ return opacity === void 0 ? value : applyOpacity(value, opacity);
863
+ }
864
+ function makeFamilyProxy(family) {
865
+ return new Proxy({}, {
866
+ get(_, prop) {
867
+ const key = String(prop);
868
+ if (key === "then") return void 0;
869
+ return resolveNamed(`${family}-${key}`);
870
+ }
871
+ });
872
+ }
873
+ return new Proxy({ alpha }, {
874
+ get(_, prop) {
875
+ const key = String(prop);
876
+ if (key === "then") return void 0;
877
+ if (key === "alpha") return alpha;
878
+ const direct = resolveNamed(key);
879
+ if (direct !== void 0) return direct;
880
+ if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
881
+ return cache.get(key);
882
+ }
883
+ });
884
+ }
885
+ function useColors() {
886
+ const { isDark: isDark2 } = useTheme();
887
+ const theme = getTheme();
888
+ ensureColorVariablesInjected(theme);
889
+ return makeColorsProxy(theme, isDark2);
720
890
  }
721
891
  // Annotate the CommonJS export names for ESM import in node:
722
892
  0 && (module.exports = {
723
893
  KbachReset,
724
894
  ThemeProvider,
895
+ applyKbachConfig,
725
896
  defaultTheme,
726
897
  disableRuntimeCSS,
727
898
  getGlobalDarkMode,
@@ -731,10 +902,11 @@ function ThemeProvider({ children, defaultMode = "system" }) {
731
902
  isKbachReady,
732
903
  isRuntimeCSSDisabled,
733
904
  kb,
905
+ resolveKbachConfig,
734
906
  setGlobalThemeMode,
735
907
  setTheme,
736
908
  subscribeGlobalDarkMode,
737
909
  toggleGlobalDarkMode,
738
- useGlobalDarkMode,
910
+ useColors,
739
911
  useTheme
740
912
  });