@kbach/react 1.0.0-beta.0 → 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);
@@ -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,21 @@ 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
+ 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: {}
384
433
  };
385
434
  var activeTheme = defaultTheme;
386
435
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -545,6 +594,113 @@ function KbachReset() {
545
594
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { id: RESET_STYLE_ID, children: BASE_RESET });
546
595
  }
547
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
+
548
704
  // src/darkModeStore.ts
549
705
  var STORAGE_KEY = "kbach-theme";
550
706
  var listeners = /* @__PURE__ */ new Set();
@@ -632,64 +788,111 @@ function seedDefaultMode(fallback) {
632
788
  notify();
633
789
  }
634
790
 
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);
791
+ // src/useTheme.ts
648
792
  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;
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
+ );
656
799
  }
657
800
 
658
801
  // src/ThemeProvider.tsx
802
+ var import_react2 = require("react");
659
803
  var import_jsx_runtime2 = require("react/jsx-runtime");
660
804
  var mountedProviderCount = 0;
661
805
  var warnedMultipleProviders = false;
662
806
  function ThemeProvider({ children, defaultMode = "system" }) {
663
- (0, import_react3.useEffect)(() => {
807
+ (0, import_react2.useEffect)(() => {
664
808
  mountedProviderCount++;
665
809
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
666
810
  warnedMultipleProviders = true;
667
811
  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."
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."
669
813
  );
670
814
  }
671
815
  return () => {
672
816
  mountedProviderCount--;
673
817
  };
674
818
  }, []);
675
- const seeded = (0, import_react3.useRef)(false);
676
- (0, import_react3.useEffect)(() => {
819
+ const seeded = (0, import_react2.useRef)(false);
820
+ (0, import_react2.useEffect)(() => {
677
821
  if (seeded.current) return;
678
822
  seeded.current = true;
679
823
  seedDefaultMode(defaultMode);
680
824
  }, []);
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 });
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);
688
890
  }
689
891
  // Annotate the CommonJS export names for ESM import in node:
690
892
  0 && (module.exports = {
691
893
  KbachReset,
692
894
  ThemeProvider,
895
+ applyKbachConfig,
693
896
  defaultTheme,
694
897
  disableRuntimeCSS,
695
898
  getGlobalDarkMode,
@@ -699,10 +902,11 @@ function ThemeProvider({ children, defaultMode = "system" }) {
699
902
  isKbachReady,
700
903
  isRuntimeCSSDisabled,
701
904
  kb,
905
+ resolveKbachConfig,
702
906
  setGlobalThemeMode,
703
907
  setTheme,
704
908
  subscribeGlobalDarkMode,
705
909
  toggleGlobalDarkMode,
706
- useGlobalDarkMode,
910
+ useColors,
707
911
  useTheme
708
912
  });