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

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.
@@ -87,8 +87,28 @@ interface KbachConfig {
87
87
  * already-defined-above) is treated as a literal CSS value instead
88
88
  * (`accent: 'red'` — REAL CSS `red`, since there's no color named "red"
89
89
  * to alias), never an error either way.
90
+ *
91
+ * `dark` is a reserved key, not a color name — a SECOND way to write a
92
+ * mode-aware color, as a group instead of per-color: `{ surface:
93
+ * 'gray-2', card: 'white', dark: { surface: 'gray-11', card: 'gray-9' } }`
94
+ * makes `surface`/`card` mode-aware, while a name never mentioned under
95
+ * `dark` (like a hypothetical always-the-same `brand` here) stays a
96
+ * plain, mode-independent color. Equivalent to writing `surface: {
97
+ * light: 'gray-2', dark: 'gray-11' }` directly — pick whichever reads
98
+ * better for your config: per-color pairs when only a couple of colors
99
+ * are mode-aware, the grouped block when most of them are and writing
100
+ * "light"/"dark" twice per line would be repetitive. A name under
101
+ * `dark` with no matching top-level (light) entry is skipped — mode-
102
+ * aware colors need both sides written out. References inside `dark`
103
+ * resolve against the built-in palette and OTHER entries within `dark`
104
+ * itself (not the light-side custom colors, and not a light-side color
105
+ * already written as a `{ light, dark }` pair) — self-contained, the
106
+ * same way the light side already resolves independently of it.
90
107
  */
91
- colors?: Record<string, ColorEntry | string>;
108
+ colors?: {
109
+ [colorName: string]: ColorEntry | string | Record<string, string> | undefined;
110
+ dark?: Record<string, string>;
111
+ };
92
112
  spacing?: Record<string, number>;
93
113
  screens?: Record<string, number>;
94
114
  /** Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` — adds to (or overrides one of) `defaultTheme.fontFamily`'s three names. */
@@ -99,7 +119,18 @@ interface KbachConfig {
99
119
  }
100
120
  /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
101
121
  declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
102
- /** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. Returns the resolved theme (e.g. to also hand to the Vite plugin's own `theme` option). */
122
+ /**
123
+ * `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point
124
+ * from app code. Returns the resolved theme (e.g. to also hand to the Vite
125
+ * plugin's own `theme` option).
126
+ *
127
+ * Also re-syncs the DOM to the CURRENT dark-mode state right after
128
+ * `setTheme()` — see `resyncDomWithActiveTheme`'s own doc comment for why
129
+ * this is required, not optional: without it, an app that configures
130
+ * `darkMode: 'attribute'`/`'class'` here can silently never apply its
131
+ * `dark:` classes on initial load (even though `useTheme().isDark` reads
132
+ * correctly) until some later, unrelated dark-mode event happens to fire.
133
+ */
103
134
  declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
104
135
 
105
136
  export { type ColorEntry as C, type DarkModeStrategy as D, type KbachConfig as K, type ThemeConfig as T, type ContainerConfig as a, applyKbachConfig as b, defaultTheme as d, getTheme as g, resolveKbachConfig as r, setTheme as s };
@@ -87,8 +87,28 @@ interface KbachConfig {
87
87
  * already-defined-above) is treated as a literal CSS value instead
88
88
  * (`accent: 'red'` — REAL CSS `red`, since there's no color named "red"
89
89
  * to alias), never an error either way.
90
+ *
91
+ * `dark` is a reserved key, not a color name — a SECOND way to write a
92
+ * mode-aware color, as a group instead of per-color: `{ surface:
93
+ * 'gray-2', card: 'white', dark: { surface: 'gray-11', card: 'gray-9' } }`
94
+ * makes `surface`/`card` mode-aware, while a name never mentioned under
95
+ * `dark` (like a hypothetical always-the-same `brand` here) stays a
96
+ * plain, mode-independent color. Equivalent to writing `surface: {
97
+ * light: 'gray-2', dark: 'gray-11' }` directly — pick whichever reads
98
+ * better for your config: per-color pairs when only a couple of colors
99
+ * are mode-aware, the grouped block when most of them are and writing
100
+ * "light"/"dark" twice per line would be repetitive. A name under
101
+ * `dark` with no matching top-level (light) entry is skipped — mode-
102
+ * aware colors need both sides written out. References inside `dark`
103
+ * resolve against the built-in palette and OTHER entries within `dark`
104
+ * itself (not the light-side custom colors, and not a light-side color
105
+ * already written as a `{ light, dark }` pair) — self-contained, the
106
+ * same way the light side already resolves independently of it.
90
107
  */
91
- colors?: Record<string, ColorEntry | string>;
108
+ colors?: {
109
+ [colorName: string]: ColorEntry | string | Record<string, string> | undefined;
110
+ dark?: Record<string, string>;
111
+ };
92
112
  spacing?: Record<string, number>;
93
113
  screens?: Record<string, number>;
94
114
  /** Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` — adds to (or overrides one of) `defaultTheme.fontFamily`'s three names. */
@@ -99,7 +119,18 @@ interface KbachConfig {
99
119
  }
100
120
  /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
101
121
  declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
102
- /** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. Returns the resolved theme (e.g. to also hand to the Vite plugin's own `theme` option). */
122
+ /**
123
+ * `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point
124
+ * from app code. Returns the resolved theme (e.g. to also hand to the Vite
125
+ * plugin's own `theme` option).
126
+ *
127
+ * Also re-syncs the DOM to the CURRENT dark-mode state right after
128
+ * `setTheme()` — see `resyncDomWithActiveTheme`'s own doc comment for why
129
+ * this is required, not optional: without it, an app that configures
130
+ * `darkMode: 'attribute'`/`'class'` here can silently never apply its
131
+ * `dark:` classes on initial load (even though `useTheme().isDark` reads
132
+ * correctly) until some later, unrelated dark-mode event happens to fire.
133
+ */
103
134
  declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
104
135
 
105
136
  export { type ColorEntry as C, type DarkModeStrategy as D, type KbachConfig as K, type ThemeConfig as T, type ContainerConfig as a, applyKbachConfig as b, defaultTheme as d, getTheme as g, resolveKbachConfig as r, setTheme as s };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-BQ_b-qxF.mjs';
1
+ export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-N74_lKa0.mjs';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -120,6 +120,22 @@ type ColorsAPI = {
120
120
  * `rgb(var(--kb-color-x))` mode-aware references.
121
121
  */
122
122
  readonly alpha: (color: string, opacity?: number) => string;
123
+ /**
124
+ * Same lookup dot/bracket access does (`colors.get('brand')` ===
125
+ * `colors.brand`), typed as plain `string` instead of `string |
126
+ * ColorScale` — use this whenever the result goes somewhere that expects
127
+ * a real `string` and hits a type error otherwise. The wider union on
128
+ * every OTHER property here isn't a bug: a flat custom color (`surface`)
129
+ * and a shade-family prefix (`blue`) are served by the exact same
130
+ * object, and TypeScript has no way to statically know which one any
131
+ * given key names — `kbach.config.js`'s colors are plain runtime JS, not
132
+ * a type declaration this package could inspect ahead of time. A name
133
+ * with no matching theme entry falls back to the name itself,
134
+ * unresolved — the same "not a known color name -> literal value"
135
+ * convention `kbach.config.js`'s own color aliasing already uses (see
136
+ * `config.ts`'s `resolveColorEntry`), never an error.
137
+ */
138
+ readonly get: (name: string) => string;
123
139
  };
124
140
  /**
125
141
  * Resolved theme colors as real values — for anywhere you need an actual
@@ -141,8 +157,57 @@ type ColorsAPI = {
141
157
  * colors.blue['6/50']; // same, at 50% opacity
142
158
  * colors.surface; // "rgb(var(--kb-color-surface))" if defined as { light, dark }
143
159
  * colors.alpha(colors.surface, 50); // "rgba(var(--kb-color-surface), 0.5)"
160
+ * colors.get('surface'); // same value as colors.surface, typed as plain `string`
144
161
  * ```
145
162
  */
146
163
  declare function useColors(): ColorsAPI;
147
164
 
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 };
165
+ /**
166
+ * A tiny class-name composer — combine strings, arrays, and objects (with
167
+ * boolean values) into one space-separated className, skipping anything
168
+ * falsy. Matches the widely-used `clsx` npm package's public API and
169
+ * behavior exactly (a from-scratch reimplementation, not a fork or a thin
170
+ * wrapper around it) — this package ships its own copy so using it adds
171
+ * zero runtime dependencies, and so it works identically whether or not
172
+ * `clsx`/`classnames` happens to already be installed.
173
+ *
174
+ * Kbach class strings never NEED this — a Kbach `className` is just a
175
+ * plain string, composable with plain JS (`"bg-" + color + "-6"`,
176
+ * `[base, active && "opacity-100"].join(" ")`, see this package's own
177
+ * README) with no special API required. This exists purely as an
178
+ * ergonomic convenience for the common "several conditional classes on one
179
+ * element" shape, for anyone who'd rather write it declaratively:
180
+ *
181
+ * ```ts
182
+ * clsx("px-4 py-2", isActive && "bg-blue-6", { "opacity-50": disabled });
183
+ * // "px-4 py-2 bg-blue-6" (when isActive is true and disabled is false)
184
+ * ```
185
+ *
186
+ * The static build-time scanner (vite-plugin/postcss-plugin) already
187
+ * recognizes `clsx(...)` call sites (alongside `cn`/`classnames`/`cx`/
188
+ * `kb`) and extracts every string literal inside them on its own — using
189
+ * this function needs no extra plugin configuration to keep working with
190
+ * the static CSS build.
191
+ *
192
+ * Identical to `@kbach/react-native`'s own `clsx.ts` — deliberately
193
+ * duplicated rather than factored into a shared package, since this is
194
+ * genuinely all there is to it and a whole extra published package for one
195
+ * ~20-line function would be more ceremony than the thing itself.
196
+ */
197
+ /** Anything `clsx` accepts as one argument — recursively, for arrays. */
198
+ type ClassValue = ClassArray | ClassDictionary | string | number | bigint | boolean | null | undefined;
199
+ /** A key is kept (as a literal class name) only when its value is truthy — `{ "opacity-50": disabled }`. */
200
+ interface ClassDictionary {
201
+ [className: string]: unknown;
202
+ }
203
+ type ClassArray = ClassValue[];
204
+ /**
205
+ * Joins every truthy piece across all arguments with a single space,
206
+ * skipping falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`)
207
+ * entirely — they contribute nothing, not even an extra space. Arrays are
208
+ * flattened (recursively, to any depth); a plain object keeps only the
209
+ * keys whose value is truthy.
210
+ */
211
+ declare function clsx(...inputs: ClassValue[]): string;
212
+
213
+ export { type ClassArray, type ClassDictionary, type ClassValue, type ColorScale, type ColorsAPI, KbachReset, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeState, clsx, disableRuntimeCSS, getGlobalDarkMode, getGlobalThemeMode, initKbach, isKbachReady, isRuntimeCSSDisabled, kb, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode, useColors, useTheme };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-BQ_b-qxF.js';
1
+ export { C as ColorEntry, a as ContainerConfig, D as DarkModeStrategy, K as KbachConfig, T as ThemeConfig, b as applyKbachConfig, d as defaultTheme, g as getTheme, r as resolveKbachConfig, s as setTheme } from './config-N74_lKa0.js';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -120,6 +120,22 @@ type ColorsAPI = {
120
120
  * `rgb(var(--kb-color-x))` mode-aware references.
121
121
  */
122
122
  readonly alpha: (color: string, opacity?: number) => string;
123
+ /**
124
+ * Same lookup dot/bracket access does (`colors.get('brand')` ===
125
+ * `colors.brand`), typed as plain `string` instead of `string |
126
+ * ColorScale` — use this whenever the result goes somewhere that expects
127
+ * a real `string` and hits a type error otherwise. The wider union on
128
+ * every OTHER property here isn't a bug: a flat custom color (`surface`)
129
+ * and a shade-family prefix (`blue`) are served by the exact same
130
+ * object, and TypeScript has no way to statically know which one any
131
+ * given key names — `kbach.config.js`'s colors are plain runtime JS, not
132
+ * a type declaration this package could inspect ahead of time. A name
133
+ * with no matching theme entry falls back to the name itself,
134
+ * unresolved — the same "not a known color name -> literal value"
135
+ * convention `kbach.config.js`'s own color aliasing already uses (see
136
+ * `config.ts`'s `resolveColorEntry`), never an error.
137
+ */
138
+ readonly get: (name: string) => string;
123
139
  };
124
140
  /**
125
141
  * Resolved theme colors as real values — for anywhere you need an actual
@@ -141,8 +157,57 @@ type ColorsAPI = {
141
157
  * colors.blue['6/50']; // same, at 50% opacity
142
158
  * colors.surface; // "rgb(var(--kb-color-surface))" if defined as { light, dark }
143
159
  * colors.alpha(colors.surface, 50); // "rgba(var(--kb-color-surface), 0.5)"
160
+ * colors.get('surface'); // same value as colors.surface, typed as plain `string`
144
161
  * ```
145
162
  */
146
163
  declare function useColors(): ColorsAPI;
147
164
 
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 };
165
+ /**
166
+ * A tiny class-name composer — combine strings, arrays, and objects (with
167
+ * boolean values) into one space-separated className, skipping anything
168
+ * falsy. Matches the widely-used `clsx` npm package's public API and
169
+ * behavior exactly (a from-scratch reimplementation, not a fork or a thin
170
+ * wrapper around it) — this package ships its own copy so using it adds
171
+ * zero runtime dependencies, and so it works identically whether or not
172
+ * `clsx`/`classnames` happens to already be installed.
173
+ *
174
+ * Kbach class strings never NEED this — a Kbach `className` is just a
175
+ * plain string, composable with plain JS (`"bg-" + color + "-6"`,
176
+ * `[base, active && "opacity-100"].join(" ")`, see this package's own
177
+ * README) with no special API required. This exists purely as an
178
+ * ergonomic convenience for the common "several conditional classes on one
179
+ * element" shape, for anyone who'd rather write it declaratively:
180
+ *
181
+ * ```ts
182
+ * clsx("px-4 py-2", isActive && "bg-blue-6", { "opacity-50": disabled });
183
+ * // "px-4 py-2 bg-blue-6" (when isActive is true and disabled is false)
184
+ * ```
185
+ *
186
+ * The static build-time scanner (vite-plugin/postcss-plugin) already
187
+ * recognizes `clsx(...)` call sites (alongside `cn`/`classnames`/`cx`/
188
+ * `kb`) and extracts every string literal inside them on its own — using
189
+ * this function needs no extra plugin configuration to keep working with
190
+ * the static CSS build.
191
+ *
192
+ * Identical to `@kbach/react-native`'s own `clsx.ts` — deliberately
193
+ * duplicated rather than factored into a shared package, since this is
194
+ * genuinely all there is to it and a whole extra published package for one
195
+ * ~20-line function would be more ceremony than the thing itself.
196
+ */
197
+ /** Anything `clsx` accepts as one argument — recursively, for arrays. */
198
+ type ClassValue = ClassArray | ClassDictionary | string | number | bigint | boolean | null | undefined;
199
+ /** A key is kept (as a literal class name) only when its value is truthy — `{ "opacity-50": disabled }`. */
200
+ interface ClassDictionary {
201
+ [className: string]: unknown;
202
+ }
203
+ type ClassArray = ClassValue[];
204
+ /**
205
+ * Joins every truthy piece across all arguments with a single space,
206
+ * skipping falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`)
207
+ * entirely — they contribute nothing, not even an extra space. Arrays are
208
+ * flattened (recursively, to any depth); a plain object keeps only the
209
+ * keys whose value is truthy.
210
+ */
211
+ declare function clsx(...inputs: ClassValue[]): string;
212
+
213
+ export { type ClassArray, type ClassDictionary, type ClassValue, type ColorScale, type ColorsAPI, KbachReset, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeState, clsx, disableRuntimeCSS, getGlobalDarkMode, getGlobalThemeMode, initKbach, isKbachReady, isRuntimeCSSDisabled, kb, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode, useColors, useTheme };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ __export(src_exports, {
33
33
  KbachReset: () => KbachReset,
34
34
  ThemeProvider: () => ThemeProvider,
35
35
  applyKbachConfig: () => applyKbachConfig,
36
+ clsx: () => clsx,
36
37
  defaultTheme: () => defaultTheme,
37
38
  disableRuntimeCSS: () => disableRuntimeCSS,
38
39
  getGlobalDarkMode: () => getGlobalDarkMode,
@@ -70,7 +71,7 @@ function isKbachReady() {
70
71
  function generateCss(classString, themeJson) {
71
72
  if (!ready) {
72
73
  throw new Error(
73
- "[kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
74
+ "[Kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
74
75
  );
75
76
  }
76
77
  return (0, import_core_engine.generate_css)(classString, themeJson);
@@ -426,7 +427,19 @@ var defaultTheme = {
426
427
  serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
427
428
  mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
428
429
  },
429
- darkMode: "attribute",
430
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
431
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
432
+ // using only static `className` strings gets working `dark:` classes
433
+ // out of the box, with no runtime import required to write `data-theme`/
434
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
435
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
436
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
437
+ // for JS consumers, but flipping it has no visible effect on `dark:`
438
+ // classes, since the browser's media query only ever reflects the real
439
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
440
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
441
+ // as an app wants a real in-app toggle.
442
+ darkMode: "media",
430
443
  // Empty by default, matching real Tailwind's own defaults (no forced
431
444
  // centering/padding) — see `ContainerConfig`'s own doc comment.
432
445
  container: {}
@@ -443,6 +456,13 @@ function getTheme() {
443
456
  function getThemeJson() {
444
457
  return activeThemeJson;
445
458
  }
459
+ var DARK_MODE_STRATEGIES = /* @__PURE__ */ new Set(["attribute", "class", "media"]);
460
+ function detectDarkModeStrategyFromDom() {
461
+ if (typeof document === "undefined") return;
462
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--kb-dark-mode").trim();
463
+ if (!DARK_MODE_STRATEGIES.has(raw) || raw === activeTheme.darkMode) return;
464
+ setTheme({ ...activeTheme, darkMode: raw });
465
+ }
446
466
 
447
467
  // src/reset.ts
448
468
  var RESET_STYLE_ID = "kbach-reset";
@@ -575,9 +595,23 @@ function ensureResetInjected() {
575
595
  el.textContent = BASE_RESET;
576
596
  document.head.insertBefore(el, document.head.firstChild);
577
597
  }
598
+ var KB_CACHE_MAX = 500;
599
+ var kbCache = /* @__PURE__ */ new Map();
600
+ var cachedForThemeJson = null;
578
601
  function kb(classString) {
579
- const json = generateCss(classString, getThemeJson());
580
- const { className, rules } = JSON.parse(json);
602
+ const themeJson = getThemeJson();
603
+ if (themeJson !== cachedForThemeJson) {
604
+ kbCache.clear();
605
+ cachedForThemeJson = themeJson;
606
+ }
607
+ let result = kbCache.get(classString);
608
+ if (result === void 0) {
609
+ const json = generateCss(classString, themeJson);
610
+ result = JSON.parse(json);
611
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
612
+ kbCache.set(classString, result);
613
+ }
614
+ const { className, rules } = result;
581
615
  if (!runtimeCSSDisabled) {
582
616
  ensureResetInjected();
583
617
  for (const { rule, order } of rules) {
@@ -644,63 +678,6 @@ function ensureColorVariablesInjected(theme) {
644
678
  ${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
645
679
  }
646
680
 
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
-
704
681
  // src/darkModeStore.ts
705
682
  var STORAGE_KEY = "kbach-theme";
706
683
  var listeners = /* @__PURE__ */ new Set();
@@ -728,13 +705,15 @@ function applyToDom(dark) {
728
705
  if (typeof document === "undefined") return;
729
706
  const root = document.documentElement;
730
707
  const strategy = getTheme().darkMode;
731
- if (strategy === "class") {
732
- root.classList.toggle("dark", dark);
733
- root.classList.toggle("light", !dark);
734
- } else if (strategy === "attribute") {
708
+ root.classList.toggle("dark", strategy === "class" && dark);
709
+ root.classList.toggle("light", strategy === "class" && !dark);
710
+ if (strategy === "attribute") {
735
711
  root.setAttribute("data-theme", dark ? "dark" : "light");
712
+ } else {
713
+ root.removeAttribute("data-theme");
736
714
  }
737
715
  }
716
+ detectDarkModeStrategyFromDom();
738
717
  var persistedMode = readPersistedMode();
739
718
  var hadPersistedMode = persistedMode !== null;
740
719
  var mode = persistedMode ?? "system";
@@ -754,6 +733,9 @@ if (typeof window !== "undefined" && window.matchMedia) {
754
733
  function notify() {
755
734
  for (const listener of listeners) listener();
756
735
  }
736
+ function resyncDomWithActiveTheme() {
737
+ applyToDom(isDark);
738
+ }
757
739
  function getGlobalDarkMode() {
758
740
  return isDark;
759
741
  }
@@ -788,7 +770,81 @@ function seedDefaultMode(fallback) {
788
770
  notify();
789
771
  }
790
772
 
773
+ // src/config.ts
774
+ function applyOpacityToHex(hex, opacityText) {
775
+ const rgb = parseHexRgb(hex);
776
+ if (!rgb) return hex;
777
+ const a = Number(opacityText) / 100;
778
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
779
+ return `rgba(${rgb.join(",")},${clamped})`;
780
+ }
781
+ function resolveColorEntry(value, lookup) {
782
+ if (typeof value !== "string") return value;
783
+ const slash = value.indexOf("/");
784
+ const name = slash === -1 ? value : value.slice(0, slash);
785
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
786
+ const base = lookup(name);
787
+ if (base === void 0) return value;
788
+ if (opacity === void 0) return base;
789
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
790
+ }
791
+ function resolveColors(extend, existing) {
792
+ const { dark: darkExtend, ...lightExtend } = extend;
793
+ const resolved = {};
794
+ const lookup = (name) => resolved[name] ?? existing[name];
795
+ for (const [name, value] of Object.entries(lightExtend)) {
796
+ if (typeof value === "object" && value !== null && !("light" in value) && !("dark" in value)) continue;
797
+ resolved[name] = resolveColorEntry(value, lookup);
798
+ }
799
+ if (darkExtend) {
800
+ const resolvedDark = {};
801
+ const lookupDark = (name) => {
802
+ const found = resolvedDark[name] ?? existing[name];
803
+ return typeof found === "string" ? found : void 0;
804
+ };
805
+ for (const [name, value] of Object.entries(darkExtend)) {
806
+ const entry = resolveColorEntry(value, lookupDark);
807
+ resolvedDark[name] = typeof entry === "string" ? entry : entry.dark;
808
+ }
809
+ for (const [name, darkValue] of Object.entries(resolvedDark)) {
810
+ const lightEntry = resolved[name];
811
+ if (lightEntry === void 0 || typeof lightEntry !== "string") continue;
812
+ resolved[name] = { light: lightEntry, dark: darkValue };
813
+ }
814
+ }
815
+ return resolved;
816
+ }
817
+ function resolveKbachConfig(config) {
818
+ let theme = { ...defaultTheme, ...config.theme };
819
+ if (config.extend?.colors) {
820
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
821
+ }
822
+ if (config.extend?.spacing) {
823
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
824
+ }
825
+ if (config.extend?.screens) {
826
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
827
+ }
828
+ if (config.extend?.fontFamily) {
829
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
830
+ }
831
+ if (config.extend?.container) {
832
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
833
+ }
834
+ if (config.darkMode) {
835
+ theme = { ...theme, darkMode: config.darkMode };
836
+ }
837
+ return theme;
838
+ }
839
+ function applyKbachConfig(config) {
840
+ const theme = resolveKbachConfig(config);
841
+ setTheme(theme);
842
+ resyncDomWithActiveTheme();
843
+ return theme;
844
+ }
845
+
791
846
  // src/useTheme.ts
847
+ var import_react = require("react");
792
848
  function useTheme() {
793
849
  const isDark2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
794
850
  const mode2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
@@ -806,10 +862,12 @@ var warnedMultipleProviders = false;
806
862
  function ThemeProvider({ children, defaultMode = "system" }) {
807
863
  (0, import_react2.useEffect)(() => {
808
864
  mountedProviderCount++;
809
- if (mountedProviderCount > 1 && !warnedMultipleProviders) {
865
+ if (mountedProviderCount > 1 && !warnedMultipleProviders && process.env.NODE_ENV !== "production") {
810
866
  warnedMultipleProviders = true;
811
867
  console.warn(
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."
868
+ "%c[Kbach]%c Multiple <ThemeProvider> instances are mounted at once.\nDark mode is one global store \u2014 useTheme() reads it everywhere, not scoped per provider \u2014 so their defaultMode seeds will fight over the same state.\nMount exactly one <ThemeProvider>, as high in the tree as convenient.",
869
+ "background:#7c3aed;color:#fff;font-weight:600;padding:1px 6px;border-radius:3px",
870
+ "font-weight:400"
813
871
  );
814
872
  }
815
873
  return () => {
@@ -826,6 +884,7 @@ function ThemeProvider({ children, defaultMode = "system" }) {
826
884
  }
827
885
 
828
886
  // src/useColors.ts
887
+ var import_react3 = require("react");
829
888
  function applyOpacity(color, opacity) {
830
889
  const a = Math.max(0, Math.min(1, opacity / 100));
831
890
  const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
@@ -870,11 +929,13 @@ function makeColorsProxy(theme, isDark2) {
870
929
  }
871
930
  });
872
931
  }
873
- return new Proxy({ alpha }, {
932
+ const get = (name) => resolveNamed(name) ?? name;
933
+ return new Proxy({ alpha, get }, {
874
934
  get(_, prop) {
875
935
  const key = String(prop);
876
936
  if (key === "then") return void 0;
877
937
  if (key === "alpha") return alpha;
938
+ if (key === "get") return get;
878
939
  const direct = resolveNamed(key);
879
940
  if (direct !== void 0) return direct;
880
941
  if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
@@ -886,13 +947,40 @@ function useColors() {
886
947
  const { isDark: isDark2 } = useTheme();
887
948
  const theme = getTheme();
888
949
  ensureColorVariablesInjected(theme);
889
- return makeColorsProxy(theme, isDark2);
950
+ return (0, import_react3.useMemo)(() => makeColorsProxy(theme, isDark2), [theme, isDark2]);
951
+ }
952
+
953
+ // src/clsx.ts
954
+ function clsx(...inputs) {
955
+ let out = "";
956
+ for (const input of inputs) {
957
+ const piece = resolveClassValue(input);
958
+ if (piece) out = out ? `${out} ${piece}` : piece;
959
+ }
960
+ return out;
961
+ }
962
+ function resolveClassValue(value) {
963
+ if (!value) return "";
964
+ const type = typeof value;
965
+ if (type === "string" || type === "number" || type === "bigint") return String(value);
966
+ if (Array.isArray(value)) return clsx(...value);
967
+ if (type === "object") {
968
+ let out = "";
969
+ for (const key in value) {
970
+ if (Object.prototype.hasOwnProperty.call(value, key) && value[key]) {
971
+ out = out ? `${out} ${key}` : key;
972
+ }
973
+ }
974
+ return out;
975
+ }
976
+ return "";
890
977
  }
891
978
  // Annotate the CommonJS export names for ESM import in node:
892
979
  0 && (module.exports = {
893
980
  KbachReset,
894
981
  ThemeProvider,
895
982
  applyKbachConfig,
983
+ clsx,
896
984
  defaultTheme,
897
985
  disableRuntimeCSS,
898
986
  getGlobalDarkMode,