@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.
package/dist/index.mjs CHANGED
@@ -4,20 +4,26 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-ZQJKL2MV.mjs";
7
+ } from "./chunk-BJEOK2VK.mjs";
8
8
  import {
9
9
  applyKbachConfig,
10
10
  ensureColorVariablesInjected,
11
+ getGlobalDarkMode,
12
+ getGlobalThemeMode,
11
13
  parseHexRgb,
12
- resolveKbachConfig
13
- } from "./chunk-NKR3PSWO.mjs";
14
+ resolveKbachConfig,
15
+ seedDefaultMode,
16
+ setGlobalThemeMode,
17
+ subscribeGlobalDarkMode,
18
+ toggleGlobalDarkMode
19
+ } from "./chunk-43FOLRQA.mjs";
14
20
  import {
15
21
  BASE_RESET,
16
22
  RESET_STYLE_ID,
17
23
  defaultTheme,
18
24
  getTheme,
19
25
  setTheme
20
- } from "./chunk-N5HCQIBB.mjs";
26
+ } from "./chunk-OVKVZA7B.mjs";
21
27
 
22
28
  // src/KbachReset.tsx
23
29
  import { jsx } from "react/jsx-runtime";
@@ -28,101 +34,12 @@ function KbachReset() {
28
34
 
29
35
  // src/useTheme.ts
30
36
  import { useMemo, useSyncExternalStore } from "react";
31
-
32
- // src/darkModeStore.ts
33
- var STORAGE_KEY = "kbach-theme";
34
- var listeners = /* @__PURE__ */ new Set();
35
- function systemPrefersDark() {
36
- if (typeof window === "undefined" || !window.matchMedia) return false;
37
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
38
- }
39
- function storage() {
40
- return typeof window === "undefined" ? null : window.localStorage;
41
- }
42
- function readPersistedMode() {
43
- const ls = storage();
44
- if (!ls) return null;
45
- try {
46
- const raw = ls.getItem(STORAGE_KEY);
47
- return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
48
- } catch {
49
- return null;
50
- }
51
- }
52
- function resolveIsDark(m) {
53
- return m === "system" ? systemPrefersDark() : m === "dark";
54
- }
55
- function applyToDom(dark) {
56
- if (typeof document === "undefined") return;
57
- const root = document.documentElement;
58
- const strategy = getTheme().darkMode;
59
- if (strategy === "class") {
60
- root.classList.toggle("dark", dark);
61
- root.classList.toggle("light", !dark);
62
- } else if (strategy === "attribute") {
63
- root.setAttribute("data-theme", dark ? "dark" : "light");
64
- }
65
- }
66
- var persistedMode = readPersistedMode();
67
- var hadPersistedMode = persistedMode !== null;
68
- var mode = persistedMode ?? "system";
69
- var isDark = resolveIsDark(mode);
70
- applyToDom(isDark);
71
- if (typeof window !== "undefined" && window.matchMedia) {
72
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
73
- const onSystemChange = (event) => {
74
- if (mode !== "system") return;
75
- isDark = event.matches;
76
- applyToDom(isDark);
77
- notify();
78
- };
79
- if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
80
- else mq.addListener(onSystemChange);
81
- }
82
- function notify() {
83
- for (const listener of listeners) listener();
84
- }
85
- function getGlobalDarkMode() {
86
- return isDark;
87
- }
88
- function getGlobalThemeMode() {
89
- return mode;
90
- }
91
- function setGlobalThemeMode(nextMode) {
92
- mode = nextMode;
93
- isDark = resolveIsDark(nextMode);
94
- applyToDom(isDark);
95
- const ls = storage();
96
- if (ls) {
97
- try {
98
- ls.setItem(STORAGE_KEY, nextMode);
99
- } catch {
100
- }
101
- }
102
- notify();
103
- }
104
- function toggleGlobalDarkMode() {
105
- setGlobalThemeMode(isDark ? "light" : "dark");
106
- }
107
- function subscribeGlobalDarkMode(listener) {
108
- listeners.add(listener);
109
- return () => listeners.delete(listener);
110
- }
111
- function seedDefaultMode(fallback) {
112
- if (hadPersistedMode || mode === fallback) return;
113
- mode = fallback;
114
- isDark = resolveIsDark(fallback);
115
- applyToDom(isDark);
116
- notify();
117
- }
118
-
119
- // src/useTheme.ts
120
37
  function useTheme() {
121
- const isDark2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
122
- const mode2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
38
+ const isDark = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
39
+ const mode = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
123
40
  return useMemo(
124
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
125
- [mode2, isDark2]
41
+ () => ({ mode, isDark, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
42
+ [mode, isDark]
126
43
  );
127
44
  }
128
45
 
@@ -134,10 +51,12 @@ var warnedMultipleProviders = false;
134
51
  function ThemeProvider({ children, defaultMode = "system" }) {
135
52
  useEffect(() => {
136
53
  mountedProviderCount++;
137
- if (mountedProviderCount > 1 && !warnedMultipleProviders) {
54
+ if (mountedProviderCount > 1 && !warnedMultipleProviders && process.env.NODE_ENV !== "production") {
138
55
  warnedMultipleProviders = true;
139
56
  console.warn(
140
- "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store useTheme() reads everywhere, not scoped per-provider, so their defaultMode seeds will fight over the same state."
57
+ "%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.",
58
+ "background:#7c3aed;color:#fff;font-weight:600;padding:1px 6px;border-radius:3px",
59
+ "font-weight:400"
141
60
  );
142
61
  }
143
62
  return () => {
@@ -154,6 +73,7 @@ function ThemeProvider({ children, defaultMode = "system" }) {
154
73
  }
155
74
 
156
75
  // src/useColors.ts
76
+ import { useMemo as useMemo2 } from "react";
157
77
  function applyOpacity(color, opacity) {
158
78
  const a = Math.max(0, Math.min(1, opacity / 100));
159
79
  const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
@@ -166,14 +86,14 @@ function applyOpacity(color, opacity) {
166
86
  if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
167
87
  return color;
168
88
  }
169
- function resolveEntry(name, entry, isDark2) {
89
+ function resolveEntry(name, entry, isDark) {
170
90
  if (typeof entry === "string") return entry;
171
91
  const light = parseHexRgb(entry.light);
172
92
  const dark = parseHexRgb(entry.dark);
173
93
  if (light && dark) return `rgb(var(--kb-color-${name}))`;
174
- return isDark2 ? entry.dark : entry.light;
94
+ return isDark ? entry.dark : entry.light;
175
95
  }
176
- function makeColorsProxy(theme, isDark2) {
96
+ function makeColorsProxy(theme, isDark) {
177
97
  const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
178
98
  const cache = /* @__PURE__ */ new Map();
179
99
  function resolveNamed(key) {
@@ -186,7 +106,7 @@ function makeColorsProxy(theme, isDark2) {
186
106
  }
187
107
  const entry = theme.colors[name];
188
108
  if (entry === void 0) return void 0;
189
- const value = resolveEntry(name, entry, isDark2);
109
+ const value = resolveEntry(name, entry, isDark);
190
110
  return opacity === void 0 ? value : applyOpacity(value, opacity);
191
111
  }
192
112
  function makeFamilyProxy(family) {
@@ -198,11 +118,13 @@ function makeColorsProxy(theme, isDark2) {
198
118
  }
199
119
  });
200
120
  }
201
- return new Proxy({ alpha }, {
121
+ const get = (name) => resolveNamed(name) ?? name;
122
+ return new Proxy({ alpha, get }, {
202
123
  get(_, prop) {
203
124
  const key = String(prop);
204
125
  if (key === "then") return void 0;
205
126
  if (key === "alpha") return alpha;
127
+ if (key === "get") return get;
206
128
  const direct = resolveNamed(key);
207
129
  if (direct !== void 0) return direct;
208
130
  if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
@@ -211,15 +133,42 @@ function makeColorsProxy(theme, isDark2) {
211
133
  });
212
134
  }
213
135
  function useColors() {
214
- const { isDark: isDark2 } = useTheme();
136
+ const { isDark } = useTheme();
215
137
  const theme = getTheme();
216
138
  ensureColorVariablesInjected(theme);
217
- return makeColorsProxy(theme, isDark2);
139
+ return useMemo2(() => makeColorsProxy(theme, isDark), [theme, isDark]);
140
+ }
141
+
142
+ // src/clsx.ts
143
+ function clsx(...inputs) {
144
+ let out = "";
145
+ for (const input of inputs) {
146
+ const piece = resolveClassValue(input);
147
+ if (piece) out = out ? `${out} ${piece}` : piece;
148
+ }
149
+ return out;
150
+ }
151
+ function resolveClassValue(value) {
152
+ if (!value) return "";
153
+ const type = typeof value;
154
+ if (type === "string" || type === "number" || type === "bigint") return String(value);
155
+ if (Array.isArray(value)) return clsx(...value);
156
+ if (type === "object") {
157
+ let out = "";
158
+ for (const key in value) {
159
+ if (Object.prototype.hasOwnProperty.call(value, key) && value[key]) {
160
+ out = out ? `${out} ${key}` : key;
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+ return "";
218
166
  }
219
167
  export {
220
168
  KbachReset,
221
169
  ThemeProvider,
222
170
  applyKbachConfig,
171
+ clsx,
223
172
  defaultTheme,
224
173
  disableRuntimeCSS,
225
174
  getGlobalDarkMode,
@@ -45,7 +45,7 @@ var ready = false;
45
45
  function generateCss(classString, themeJson) {
46
46
  if (!ready) {
47
47
  throw new Error(
48
- "[kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
48
+ "[Kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
49
49
  );
50
50
  }
51
51
  return (0, import_core_engine.generate_css)(classString, themeJson);
@@ -401,7 +401,19 @@ var defaultTheme = {
401
401
  serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
402
402
  mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
403
403
  },
404
- darkMode: "attribute",
404
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
405
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
406
+ // using only static `className` strings gets working `dark:` classes
407
+ // out of the box, with no runtime import required to write `data-theme`/
408
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
409
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
410
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
411
+ // for JS consumers, but flipping it has no visible effect on `dark:`
412
+ // classes, since the browser's media query only ever reflects the real
413
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
414
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
415
+ // as an app wants a real in-app toggle.
416
+ darkMode: "media",
405
417
  // Empty by default, matching real Tailwind's own defaults (no forced
406
418
  // centering/padding) — see `ContainerConfig`'s own doc comment.
407
419
  container: {}
@@ -536,9 +548,23 @@ function ensureResetInjected() {
536
548
  el.textContent = BASE_RESET;
537
549
  document.head.insertBefore(el, document.head.firstChild);
538
550
  }
551
+ var KB_CACHE_MAX = 500;
552
+ var kbCache = /* @__PURE__ */ new Map();
553
+ var cachedForThemeJson = null;
539
554
  function kb(classString) {
540
- const json = generateCss(classString, getThemeJson());
541
- const { className, rules } = JSON.parse(json);
555
+ const themeJson = getThemeJson();
556
+ if (themeJson !== cachedForThemeJson) {
557
+ kbCache.clear();
558
+ cachedForThemeJson = themeJson;
559
+ }
560
+ let result = kbCache.get(classString);
561
+ if (result === void 0) {
562
+ const json = generateCss(classString, themeJson);
563
+ result = JSON.parse(json);
564
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
565
+ kbCache.set(classString, result);
566
+ }
567
+ const { className, rules } = result;
542
568
  if (!runtimeCSSDisabled) {
543
569
  ensureResetInjected();
544
570
  for (const { rule, order } of rules) {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-PAJ6KHDB.mjs";
6
- import "./chunk-ZQJKL2MV.mjs";
7
- import "./chunk-N5HCQIBB.mjs";
5
+ } from "./chunk-5EXGGZMA.mjs";
6
+ import "./chunk-BJEOK2VK.mjs";
7
+ import "./chunk-OVKVZA7B.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -44,7 +44,7 @@ var ready = false;
44
44
  function generateCss(classString, themeJson) {
45
45
  if (!ready) {
46
46
  throw new Error(
47
- "[kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
47
+ "[Kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
48
48
  );
49
49
  }
50
50
  return (0, import_core_engine.generate_css)(classString, themeJson);
@@ -400,7 +400,19 @@ var defaultTheme = {
400
400
  serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
401
401
  mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
402
402
  },
403
- darkMode: "attribute",
403
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
404
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
405
+ // using only static `className` strings gets working `dark:` classes
406
+ // out of the box, with no runtime import required to write `data-theme`/
407
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
408
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
409
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
410
+ // for JS consumers, but flipping it has no visible effect on `dark:`
411
+ // classes, since the browser's media query only ever reflects the real
412
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
413
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
414
+ // as an app wants a real in-app toggle.
415
+ darkMode: "media",
404
416
  // Empty by default, matching real Tailwind's own defaults (no forced
405
417
  // centering/padding) — see `ContainerConfig`'s own doc comment.
406
418
  container: {}
@@ -535,9 +547,23 @@ function ensureResetInjected() {
535
547
  el.textContent = BASE_RESET;
536
548
  document.head.insertBefore(el, document.head.firstChild);
537
549
  }
550
+ var KB_CACHE_MAX = 500;
551
+ var kbCache = /* @__PURE__ */ new Map();
552
+ var cachedForThemeJson = null;
538
553
  function kb(classString) {
539
- const json = generateCss(classString, getThemeJson());
540
- const { className, rules } = JSON.parse(json);
554
+ const themeJson = getThemeJson();
555
+ if (themeJson !== cachedForThemeJson) {
556
+ kbCache.clear();
557
+ cachedForThemeJson = themeJson;
558
+ }
559
+ let result = kbCache.get(classString);
560
+ if (result === void 0) {
561
+ const json = generateCss(classString, themeJson);
562
+ result = JSON.parse(json);
563
+ if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
564
+ kbCache.set(classString, result);
565
+ }
566
+ const { className, rules } = result;
541
567
  if (!runtimeCSSDisabled) {
542
568
  ensureResetInjected();
543
569
  for (const { rule, order } of rules) {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-PAJ6KHDB.mjs";
6
- import "./chunk-ZQJKL2MV.mjs";
7
- import "./chunk-N5HCQIBB.mjs";
5
+ } from "./chunk-5EXGGZMA.mjs";
6
+ import "./chunk-BJEOK2VK.mjs";
7
+ import "./chunk-OVKVZA7B.mjs";
8
8
  export {
9
9
  Fragment,
10
10
  jsx,
@@ -0,0 +1,36 @@
1
+ import { Plugin } from 'postcss';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-N74_lKa0.mjs';
3
+
4
+ interface KbachPostcssPluginOptions {
5
+ /**
6
+ * Project root to scan from. Defaults to `process.cwd()` — correct for
7
+ * the common case (PostCSS/Next.js run from the project root), but pass
8
+ * this explicitly if your `postcss.config.js` lives somewhere else, or
9
+ * from a monorepo app whose cwd differs from where `src`/`app` actually
10
+ * live.
11
+ */
12
+ root?: string;
13
+ /** Same as `KbachPluginOptions.theme` (vite-plugin/index.ts) — see its own doc comment. */
14
+ theme?: ThemeConfig;
15
+ /** Same as `KbachPluginOptions.config`. */
16
+ config?: KbachConfig;
17
+ /** Directories to scan for class strings, relative to `root`. Defaults to the same set the Vite plugin uses. */
18
+ include?: string[];
19
+ /** Same as `KbachPluginOptions.safelist`. */
20
+ safelist?: string[];
21
+ }
22
+ /**
23
+ * `postcss.config.js`:
24
+ * ```js
25
+ * module.exports = { plugins: { '@kbach/react/postcss': {} } };
26
+ * ```
27
+ * Processes whichever CSS file carries the `kbach:start`/`kbach:end`
28
+ * markers (conventionally `app/globals.css`/`src/kbach.css` — the same
29
+ * file you'd `@import`/reference like any other global stylesheet).
30
+ */
31
+ declare function kbachPostcss(options?: KbachPostcssPluginOptions): Plugin;
32
+ declare namespace kbachPostcss {
33
+ var postcss: boolean;
34
+ }
35
+
36
+ export { type KbachPostcssPluginOptions, kbachPostcss as default };
@@ -0,0 +1,36 @@
1
+ import { Plugin } from 'postcss';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-N74_lKa0.js';
3
+
4
+ interface KbachPostcssPluginOptions {
5
+ /**
6
+ * Project root to scan from. Defaults to `process.cwd()` — correct for
7
+ * the common case (PostCSS/Next.js run from the project root), but pass
8
+ * this explicitly if your `postcss.config.js` lives somewhere else, or
9
+ * from a monorepo app whose cwd differs from where `src`/`app` actually
10
+ * live.
11
+ */
12
+ root?: string;
13
+ /** Same as `KbachPluginOptions.theme` (vite-plugin/index.ts) — see its own doc comment. */
14
+ theme?: ThemeConfig;
15
+ /** Same as `KbachPluginOptions.config`. */
16
+ config?: KbachConfig;
17
+ /** Directories to scan for class strings, relative to `root`. Defaults to the same set the Vite plugin uses. */
18
+ include?: string[];
19
+ /** Same as `KbachPluginOptions.safelist`. */
20
+ safelist?: string[];
21
+ }
22
+ /**
23
+ * `postcss.config.js`:
24
+ * ```js
25
+ * module.exports = { plugins: { '@kbach/react/postcss': {} } };
26
+ * ```
27
+ * Processes whichever CSS file carries the `kbach:start`/`kbach:end`
28
+ * markers (conventionally `app/globals.css`/`src/kbach.css` — the same
29
+ * file you'd `@import`/reference like any other global stylesheet).
30
+ */
31
+ declare function kbachPostcss(options?: KbachPostcssPluginOptions): Plugin;
32
+ declare namespace kbachPostcss {
33
+ var postcss: boolean;
34
+ }
35
+
36
+ export { type KbachPostcssPluginOptions, kbachPostcss as default };