@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.mjs CHANGED
@@ -4,14 +4,20 @@ import {
4
4
  isKbachReady,
5
5
  isRuntimeCSSDisabled,
6
6
  kb
7
- } from "./chunk-G3RJJNSY.mjs";
7
+ } from "./chunk-ZQJKL2MV.mjs";
8
+ import {
9
+ applyKbachConfig,
10
+ ensureColorVariablesInjected,
11
+ parseHexRgb,
12
+ resolveKbachConfig
13
+ } from "./chunk-NKR3PSWO.mjs";
8
14
  import {
9
15
  BASE_RESET,
10
16
  RESET_STYLE_ID,
11
17
  defaultTheme,
12
18
  getTheme,
13
19
  setTheme
14
- } from "./chunk-AFY36PRX.mjs";
20
+ } from "./chunk-N5HCQIBB.mjs";
15
21
 
16
22
  // src/KbachReset.tsx
17
23
  import { jsx } from "react/jsx-runtime";
@@ -20,6 +26,9 @@ function KbachReset() {
20
26
  return /* @__PURE__ */ jsx("style", { id: RESET_STYLE_ID, children: BASE_RESET });
21
27
  }
22
28
 
29
+ // src/useTheme.ts
30
+ import { useMemo, useSyncExternalStore } from "react";
31
+
23
32
  // src/darkModeStore.ts
24
33
  var STORAGE_KEY = "kbach-theme";
25
34
  var listeners = /* @__PURE__ */ new Set();
@@ -107,31 +116,19 @@ function seedDefaultMode(fallback) {
107
116
  notify();
108
117
  }
109
118
 
110
- // src/useGlobalDarkMode.ts
111
- import { useSyncExternalStore } from "react";
112
- var getServerSnapshot = () => false;
113
- function useGlobalDarkMode() {
114
- return useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, getServerSnapshot);
115
- }
116
-
117
- // src/ThemeProvider.tsx
118
- import { useEffect, useMemo, useRef, useSyncExternalStore as useSyncExternalStore2 } from "react";
119
-
120
- // src/ThemeContext.tsx
121
- import { createContext, useContext } from "react";
122
- var ThemeContext = createContext(null);
119
+ // src/useTheme.ts
123
120
  function useTheme() {
124
- const ctx = useContext(ThemeContext);
125
- if (!ctx) {
126
- throw new Error(
127
- 'useTheme() must be called within a <ThemeProvider>. For dark-mode state without a provider, use useGlobalDarkMode() (read) or toggleGlobalDarkMode()/setGlobalThemeMode() (write) from "@kbach/react" instead.'
128
- );
129
- }
130
- return ctx;
121
+ const isDark2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
122
+ const mode2 = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode, () => "system");
123
+ return useMemo(
124
+ () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
125
+ [mode2, isDark2]
126
+ );
131
127
  }
132
128
 
133
129
  // src/ThemeProvider.tsx
134
- import { jsx as jsx2 } from "react/jsx-runtime";
130
+ import { useEffect, useRef } from "react";
131
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
135
132
  var mountedProviderCount = 0;
136
133
  var warnedMultipleProviders = false;
137
134
  function ThemeProvider({ children, defaultMode = "system" }) {
@@ -140,7 +137,7 @@ function ThemeProvider({ children, defaultMode = "system" }) {
140
137
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
141
138
  warnedMultipleProviders = true;
142
139
  console.warn(
143
- "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store (the same one useGlobalDarkMode()/toggleGlobalDarkMode() read and write), not scoped per-provider, so they will fight over the same state instead of each getting their own."
140
+ "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store useTheme() reads everywhere, not scoped per-provider, so their defaultMode seeds will fight over the same state."
144
141
  );
145
142
  }
146
143
  return () => {
@@ -153,17 +150,76 @@ function ThemeProvider({ children, defaultMode = "system" }) {
153
150
  seeded.current = true;
154
151
  seedDefaultMode(defaultMode);
155
152
  }, []);
156
- const isDark2 = useSyncExternalStore2(subscribeGlobalDarkMode, getGlobalDarkMode, () => false);
157
- const mode2 = useSyncExternalStore2(subscribeGlobalDarkMode, getGlobalThemeMode, () => defaultMode);
158
- const value = useMemo(
159
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
160
- [mode2, isDark2]
161
- );
162
- return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children });
153
+ return /* @__PURE__ */ jsx2(Fragment, { children });
154
+ }
155
+
156
+ // src/useColors.ts
157
+ function applyOpacity(color, opacity) {
158
+ const a = Math.max(0, Math.min(1, opacity / 100));
159
+ const varMatch = /^rgb\(var\((--[\w-]+)\)\)$/.exec(color);
160
+ if (varMatch) return `rgba(var(${varMatch[1]}), ${a})`;
161
+ if (color.startsWith("#")) {
162
+ const rgb = parseHexRgb(color);
163
+ return rgb ? `rgba(${rgb.join(",")},${a})` : color;
164
+ }
165
+ if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${a})`);
166
+ if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
167
+ return color;
168
+ }
169
+ function resolveEntry(name, entry, isDark2) {
170
+ if (typeof entry === "string") return entry;
171
+ const light = parseHexRgb(entry.light);
172
+ const dark = parseHexRgb(entry.dark);
173
+ if (light && dark) return `rgb(var(--kb-color-${name}))`;
174
+ return isDark2 ? entry.dark : entry.light;
175
+ }
176
+ function makeColorsProxy(theme, isDark2) {
177
+ const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
178
+ const cache = /* @__PURE__ */ new Map();
179
+ function resolveNamed(key) {
180
+ let name = key;
181
+ let opacity;
182
+ if (key.includes("/")) {
183
+ const slash = key.indexOf("/");
184
+ name = key.slice(0, slash);
185
+ opacity = Number(key.slice(slash + 1));
186
+ }
187
+ const entry = theme.colors[name];
188
+ if (entry === void 0) return void 0;
189
+ const value = resolveEntry(name, entry, isDark2);
190
+ return opacity === void 0 ? value : applyOpacity(value, opacity);
191
+ }
192
+ function makeFamilyProxy(family) {
193
+ return new Proxy({}, {
194
+ get(_, prop) {
195
+ const key = String(prop);
196
+ if (key === "then") return void 0;
197
+ return resolveNamed(`${family}-${key}`);
198
+ }
199
+ });
200
+ }
201
+ return new Proxy({ alpha }, {
202
+ get(_, prop) {
203
+ const key = String(prop);
204
+ if (key === "then") return void 0;
205
+ if (key === "alpha") return alpha;
206
+ const direct = resolveNamed(key);
207
+ if (direct !== void 0) return direct;
208
+ if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
209
+ return cache.get(key);
210
+ }
211
+ });
212
+ }
213
+ function useColors() {
214
+ const { isDark: isDark2 } = useTheme();
215
+ const theme = getTheme();
216
+ ensureColorVariablesInjected(theme);
217
+ return makeColorsProxy(theme, isDark2);
163
218
  }
164
219
  export {
165
220
  KbachReset,
166
221
  ThemeProvider,
222
+ applyKbachConfig,
167
223
  defaultTheme,
168
224
  disableRuntimeCSS,
169
225
  getGlobalDarkMode,
@@ -173,10 +229,11 @@ export {
173
229
  isKbachReady,
174
230
  isRuntimeCSSDisabled,
175
231
  kb,
232
+ resolveKbachConfig,
176
233
  setGlobalThemeMode,
177
234
  setTheme,
178
235
  subscribeGlobalDarkMode,
179
236
  toggleGlobalDarkMode,
180
- useGlobalDarkMode,
237
+ useColors,
181
238
  useTheme
182
239
  };
@@ -334,21 +334,54 @@ var defaultTheme = {
334
334
  ...PALETTE,
335
335
  surface: { light: "#f9fafb", dark: "#111827" }
336
336
  },
337
+ // Real Tailwind's default spacing scale in full — every step is exactly
338
+ // `n * 4px` (Tailwind's base spacing unit), including the fractional
339
+ // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
340
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
341
+ // only the "usual stops", though — the Rust/JS engines' shared
342
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
343
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
344
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
345
+ // values and give a theme customization something to override, not to
346
+ // enumerate every resolvable step. "px" is the one exception that must
347
+ // stay listed explicitly — it isn't a number, so the formula can't
348
+ // derive it.
337
349
  spacing: {
338
- // "0" is always a hardcoded zero in real Tailwind's own scale too,
339
- // regardless of the base spacing unit — needed for `p-0`/`m-0`/
340
- // `top-0`/`inset-0`/etc., and specifically for `sticky top-0` (a
341
- // sticky element needs an explicit offset to have any stick point at
342
- // all; without one it behaves just like `relative`, no different from
343
- // omitting `top-0` outright). Confirmed missing by hand: this
344
- // sandbox's own header used `sticky top-0`, which silently never
345
- // resolved (no "0" spacing key existed) until this was added.
346
350
  "0": 0,
351
+ "px": 1,
352
+ "0.5": 2,
347
353
  "1": 4,
354
+ "1.5": 6,
348
355
  "2": 8,
356
+ "2.5": 10,
357
+ "3": 12,
358
+ "3.5": 14,
349
359
  "4": 16,
360
+ "5": 20,
350
361
  "6": 24,
351
- "8": 32
362
+ "7": 28,
363
+ "8": 32,
364
+ "9": 36,
365
+ "10": 40,
366
+ "11": 44,
367
+ "12": 48,
368
+ "14": 56,
369
+ "16": 64,
370
+ "20": 80,
371
+ "24": 96,
372
+ "28": 112,
373
+ "32": 128,
374
+ "36": 144,
375
+ "40": 160,
376
+ "44": 176,
377
+ "48": 192,
378
+ "52": 208,
379
+ "56": 224,
380
+ "60": 240,
381
+ "64": 256,
382
+ "72": 288,
383
+ "80": 320,
384
+ "96": 384
352
385
  },
353
386
  screens: {
354
387
  sm: 640,
@@ -357,7 +390,21 @@ var defaultTheme = {
357
390
  xl: 1280,
358
391
  "2xl": 1536
359
392
  },
360
- darkMode: "attribute"
393
+ // Byte-for-byte the same three stacks
394
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
395
+ // falls back to — kept here too (rather than leaving this empty and
396
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
397
+ // is a real, readable value for anything that wants to extend rather
398
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
399
+ fontFamily: {
400
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
401
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
402
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
403
+ },
404
+ darkMode: "attribute",
405
+ // Empty by default, matching real Tailwind's own defaults (no forced
406
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
407
+ container: {}
361
408
  };
362
409
  var activeThemeJson = JSON.stringify(defaultTheme);
363
410
  function getThemeJson() {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-VBPQAIZL.mjs";
6
- import "./chunk-G3RJJNSY.mjs";
7
- import "./chunk-AFY36PRX.mjs";
5
+ } from "./chunk-PAJ6KHDB.mjs";
6
+ import "./chunk-ZQJKL2MV.mjs";
7
+ import "./chunk-N5HCQIBB.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -333,21 +333,54 @@ var defaultTheme = {
333
333
  ...PALETTE,
334
334
  surface: { light: "#f9fafb", dark: "#111827" }
335
335
  },
336
+ // Real Tailwind's default spacing scale in full — every step is exactly
337
+ // `n * 4px` (Tailwind's base spacing unit), including the fractional
338
+ // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
339
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
340
+ // only the "usual stops", though — the Rust/JS engines' shared
341
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
342
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
343
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
344
+ // values and give a theme customization something to override, not to
345
+ // enumerate every resolvable step. "px" is the one exception that must
346
+ // stay listed explicitly — it isn't a number, so the formula can't
347
+ // derive it.
336
348
  spacing: {
337
- // "0" is always a hardcoded zero in real Tailwind's own scale too,
338
- // regardless of the base spacing unit — needed for `p-0`/`m-0`/
339
- // `top-0`/`inset-0`/etc., and specifically for `sticky top-0` (a
340
- // sticky element needs an explicit offset to have any stick point at
341
- // all; without one it behaves just like `relative`, no different from
342
- // omitting `top-0` outright). Confirmed missing by hand: this
343
- // sandbox's own header used `sticky top-0`, which silently never
344
- // resolved (no "0" spacing key existed) until this was added.
345
349
  "0": 0,
350
+ "px": 1,
351
+ "0.5": 2,
346
352
  "1": 4,
353
+ "1.5": 6,
347
354
  "2": 8,
355
+ "2.5": 10,
356
+ "3": 12,
357
+ "3.5": 14,
348
358
  "4": 16,
359
+ "5": 20,
349
360
  "6": 24,
350
- "8": 32
361
+ "7": 28,
362
+ "8": 32,
363
+ "9": 36,
364
+ "10": 40,
365
+ "11": 44,
366
+ "12": 48,
367
+ "14": 56,
368
+ "16": 64,
369
+ "20": 80,
370
+ "24": 96,
371
+ "28": 112,
372
+ "32": 128,
373
+ "36": 144,
374
+ "40": 160,
375
+ "44": 176,
376
+ "48": 192,
377
+ "52": 208,
378
+ "56": 224,
379
+ "60": 240,
380
+ "64": 256,
381
+ "72": 288,
382
+ "80": 320,
383
+ "96": 384
351
384
  },
352
385
  screens: {
353
386
  sm: 640,
@@ -356,7 +389,21 @@ var defaultTheme = {
356
389
  xl: 1280,
357
390
  "2xl": 1536
358
391
  },
359
- darkMode: "attribute"
392
+ // Byte-for-byte the same three stacks
393
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
394
+ // falls back to — kept here too (rather than leaving this empty and
395
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
396
+ // is a real, readable value for anything that wants to extend rather
397
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
398
+ fontFamily: {
399
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
400
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
401
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
402
+ },
403
+ darkMode: "attribute",
404
+ // Empty by default, matching real Tailwind's own defaults (no forced
405
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
406
+ container: {}
360
407
  };
361
408
  var activeThemeJson = JSON.stringify(defaultTheme);
362
409
  function getThemeJson() {
@@ -2,9 +2,9 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-VBPQAIZL.mjs";
6
- import "./chunk-G3RJJNSY.mjs";
7
- import "./chunk-AFY36PRX.mjs";
5
+ } from "./chunk-PAJ6KHDB.mjs";
6
+ import "./chunk-ZQJKL2MV.mjs";
7
+ import "./chunk-N5HCQIBB.mjs";
8
8
  export {
9
9
  Fragment,
10
10
  jsx,
@@ -1,9 +1,23 @@
1
1
  import { Plugin } from 'vite';
2
- import { T as ThemeConfig } from './theme-BKAb78KP.mjs';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-BQ_b-qxF.mjs';
3
3
 
4
4
  interface KbachPluginOptions {
5
5
  /** Defaults to the same defaultTheme the runtime uses — pass your own to keep both in sync. */
6
6
  theme?: ThemeConfig;
7
+ /**
8
+ * A `kbach.config.js`-style config object, resolved the same way
9
+ * `applyKbachConfig()` resolves it for the runtime — import the SAME
10
+ * config file here and in your app's own `applyKbachConfig()` call, and
11
+ * build-time CSS generation and runtime resolution always agree, with no
12
+ * synchronization step needed (`resolveKbachConfig` is a pure function:
13
+ * same input, same output, in Node.js here and in the browser there).
14
+ * Ignored if `theme` is also supplied. Only needed for an explicit
15
+ * override, though — if BOTH this and `theme` are omitted, `kbach()`
16
+ * auto-discovers a `kbach.config.js` at the project root on its own (see
17
+ * `loadConfigFile`'s own doc comment), the same zero-config convenience
18
+ * `tailwind.config.js` gets from Tailwind's own PostCSS plugin.
19
+ */
20
+ config?: KbachConfig;
7
21
  /** Directories to scan for class strings (relative to Vite root). */
8
22
  include?: string[];
9
23
  /**
@@ -1,9 +1,23 @@
1
1
  import { Plugin } from 'vite';
2
- import { T as ThemeConfig } from './theme-BKAb78KP.js';
2
+ import { T as ThemeConfig, K as KbachConfig } from './config-BQ_b-qxF.js';
3
3
 
4
4
  interface KbachPluginOptions {
5
5
  /** Defaults to the same defaultTheme the runtime uses — pass your own to keep both in sync. */
6
6
  theme?: ThemeConfig;
7
+ /**
8
+ * A `kbach.config.js`-style config object, resolved the same way
9
+ * `applyKbachConfig()` resolves it for the runtime — import the SAME
10
+ * config file here and in your app's own `applyKbachConfig()` call, and
11
+ * build-time CSS generation and runtime resolution always agree, with no
12
+ * synchronization step needed (`resolveKbachConfig` is a pure function:
13
+ * same input, same output, in Node.js here and in the browser there).
14
+ * Ignored if `theme` is also supplied. Only needed for an explicit
15
+ * override, though — if BOTH this and `theme` are omitted, `kbach()`
16
+ * auto-discovers a `kbach.config.js` at the project root on its own (see
17
+ * `loadConfigFile`'s own doc comment), the same zero-config convenience
18
+ * `tailwind.config.js` gets from Tailwind's own PostCSS plugin.
19
+ */
20
+ config?: KbachConfig;
7
21
  /** Directories to scan for class strings (relative to Vite root). */
8
22
  include?: string[];
9
23
  /**
@@ -25,6 +25,7 @@ __export(vite_plugin_exports, {
25
25
  module.exports = __toCommonJS(vite_plugin_exports);
26
26
  var import_fs2 = require("fs");
27
27
  var import_path3 = require("path");
28
+ var import_module = require("module");
28
29
 
29
30
  // src/vite-plugin/wasmNode.ts
30
31
  var import_node = require("@kbach/core-engine/node");
@@ -1421,21 +1422,54 @@ var defaultTheme = {
1421
1422
  ...PALETTE,
1422
1423
  surface: { light: "#f9fafb", dark: "#111827" }
1423
1424
  },
1425
+ // Real Tailwind's default spacing scale in full — every step is exactly
1426
+ // `n * 4px` (Tailwind's base spacing unit), including the fractional
1427
+ // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
1428
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
1429
+ // only the "usual stops", though — the Rust/JS engines' shared
1430
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
1431
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
1432
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
1433
+ // values and give a theme customization something to override, not to
1434
+ // enumerate every resolvable step. "px" is the one exception that must
1435
+ // stay listed explicitly — it isn't a number, so the formula can't
1436
+ // derive it.
1424
1437
  spacing: {
1425
- // "0" is always a hardcoded zero in real Tailwind's own scale too,
1426
- // regardless of the base spacing unit — needed for `p-0`/`m-0`/
1427
- // `top-0`/`inset-0`/etc., and specifically for `sticky top-0` (a
1428
- // sticky element needs an explicit offset to have any stick point at
1429
- // all; without one it behaves just like `relative`, no different from
1430
- // omitting `top-0` outright). Confirmed missing by hand: this
1431
- // sandbox's own header used `sticky top-0`, which silently never
1432
- // resolved (no "0" spacing key existed) until this was added.
1433
1438
  "0": 0,
1439
+ "px": 1,
1440
+ "0.5": 2,
1434
1441
  "1": 4,
1442
+ "1.5": 6,
1435
1443
  "2": 8,
1444
+ "2.5": 10,
1445
+ "3": 12,
1446
+ "3.5": 14,
1436
1447
  "4": 16,
1448
+ "5": 20,
1437
1449
  "6": 24,
1438
- "8": 32
1450
+ "7": 28,
1451
+ "8": 32,
1452
+ "9": 36,
1453
+ "10": 40,
1454
+ "11": 44,
1455
+ "12": 48,
1456
+ "14": 56,
1457
+ "16": 64,
1458
+ "20": 80,
1459
+ "24": 96,
1460
+ "28": 112,
1461
+ "32": 128,
1462
+ "36": 144,
1463
+ "40": 160,
1464
+ "44": 176,
1465
+ "48": 192,
1466
+ "52": 208,
1467
+ "56": 224,
1468
+ "60": 240,
1469
+ "64": 256,
1470
+ "72": 288,
1471
+ "80": 320,
1472
+ "96": 384
1439
1473
  },
1440
1474
  screens: {
1441
1475
  sm: 640,
@@ -1444,10 +1478,83 @@ var defaultTheme = {
1444
1478
  xl: 1280,
1445
1479
  "2xl": 1536
1446
1480
  },
1447
- darkMode: "attribute"
1481
+ // Byte-for-byte the same three stacks
1482
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
1483
+ // falls back to — kept here too (rather than leaving this empty and
1484
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
1485
+ // is a real, readable value for anything that wants to extend rather
1486
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
1487
+ fontFamily: {
1488
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
1489
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
1490
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
1491
+ },
1492
+ darkMode: "attribute",
1493
+ // Empty by default, matching real Tailwind's own defaults (no forced
1494
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
1495
+ container: {}
1448
1496
  };
1449
1497
  var activeThemeJson = JSON.stringify(defaultTheme);
1450
1498
 
1499
+ // src/colorVariables.ts
1500
+ function parseHexRgb(hex) {
1501
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
1502
+ if (!m) return null;
1503
+ let h = m[1];
1504
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
1505
+ const n = parseInt(h, 16);
1506
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1507
+ }
1508
+
1509
+ // src/config.ts
1510
+ function applyOpacityToHex(hex, opacityText) {
1511
+ const rgb = parseHexRgb(hex);
1512
+ if (!rgb) return hex;
1513
+ const a = Number(opacityText) / 100;
1514
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
1515
+ return `rgba(${rgb.join(",")},${clamped})`;
1516
+ }
1517
+ function resolveColorEntry(value, lookup) {
1518
+ if (typeof value !== "string") return value;
1519
+ const slash = value.indexOf("/");
1520
+ const name = slash === -1 ? value : value.slice(0, slash);
1521
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
1522
+ const base = lookup(name);
1523
+ if (base === void 0) return value;
1524
+ if (opacity === void 0) return base;
1525
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
1526
+ }
1527
+ function resolveColors(extend, existing) {
1528
+ const resolved = {};
1529
+ const lookup = (name) => resolved[name] ?? existing[name];
1530
+ for (const [name, value] of Object.entries(extend)) {
1531
+ resolved[name] = resolveColorEntry(value, lookup);
1532
+ }
1533
+ return resolved;
1534
+ }
1535
+ function resolveKbachConfig(config) {
1536
+ let theme = { ...defaultTheme, ...config.theme };
1537
+ if (config.extend?.colors) {
1538
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
1539
+ }
1540
+ if (config.extend?.spacing) {
1541
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
1542
+ }
1543
+ if (config.extend?.screens) {
1544
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
1545
+ }
1546
+ if (config.extend?.fontFamily) {
1547
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
1548
+ }
1549
+ if (config.extend?.container) {
1550
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
1551
+ }
1552
+ if (config.darkMode) {
1553
+ theme = { ...theme, darkMode: config.darkMode };
1554
+ }
1555
+ return theme;
1556
+ }
1557
+
1451
1558
  // src/vite-plugin/index.ts
1452
1559
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1453
1560
  function normPath(p) {
@@ -1465,9 +1572,15 @@ function writeFileEnsuringDir(path, content) {
1465
1572
  (0, import_fs2.mkdirSync)((0, import_path3.dirname)(path), { recursive: true });
1466
1573
  (0, import_fs2.writeFileSync)(path, content, "utf-8");
1467
1574
  }
1575
+ function loadConfigFile(root) {
1576
+ const path = (0, import_path3.join)(root, "kbach.config.js");
1577
+ if (!(0, import_fs2.existsSync)(path)) return void 0;
1578
+ const mod = (0, import_module.createRequire)(root)(path);
1579
+ return mod.default ?? mod;
1580
+ }
1468
1581
  function kbach(options = {}) {
1469
- const theme = options.theme ?? defaultTheme;
1470
- const themeJson = JSON.stringify(theme);
1582
+ let theme = options.theme ?? (options.config ? resolveKbachConfig(options.config) : defaultTheme);
1583
+ let themeJson = JSON.stringify(theme);
1471
1584
  const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1472
1585
  const safelist = options.safelist ?? [];
1473
1586
  let root = process.cwd();
@@ -1547,6 +1660,13 @@ function kbach(options = {}) {
1547
1660
  enforce: "pre",
1548
1661
  configResolved(resolved) {
1549
1662
  root = resolved.root;
1663
+ if (!options.theme && !options.config) {
1664
+ const discovered = loadConfigFile(root);
1665
+ if (discovered) {
1666
+ theme = resolveKbachConfig(discovered);
1667
+ themeJson = JSON.stringify(theme);
1668
+ }
1669
+ }
1550
1670
  },
1551
1671
  buildStart() {
1552
1672
  projectCssClasses = scanProjectCssSelectors(root, includeDirs);
@@ -1,11 +1,15 @@
1
+ import {
2
+ resolveKbachConfig
3
+ } from "./chunk-NKR3PSWO.mjs";
1
4
  import {
2
5
  buildResetCSS,
3
6
  defaultTheme
4
- } from "./chunk-AFY36PRX.mjs";
7
+ } from "./chunk-N5HCQIBB.mjs";
5
8
 
6
9
  // src/vite-plugin/index.ts
7
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
10
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync } from "fs";
8
11
  import { join as join3, dirname } from "path";
12
+ import { createRequire } from "module";
9
13
 
10
14
  // src/vite-plugin/wasmNode.ts
11
15
  import { generate_css } from "@kbach/core-engine/node";
@@ -1053,9 +1057,15 @@ function writeFileEnsuringDir(path, content) {
1053
1057
  mkdirSync(dirname(path), { recursive: true });
1054
1058
  writeFileSync(path, content, "utf-8");
1055
1059
  }
1060
+ function loadConfigFile(root) {
1061
+ const path = join3(root, "kbach.config.js");
1062
+ if (!existsSync(path)) return void 0;
1063
+ const mod = createRequire(root)(path);
1064
+ return mod.default ?? mod;
1065
+ }
1056
1066
  function kbach(options = {}) {
1057
- const theme = options.theme ?? defaultTheme;
1058
- const themeJson = JSON.stringify(theme);
1067
+ let theme = options.theme ?? (options.config ? resolveKbachConfig(options.config) : defaultTheme);
1068
+ let themeJson = JSON.stringify(theme);
1059
1069
  const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1060
1070
  const safelist = options.safelist ?? [];
1061
1071
  let root = process.cwd();
@@ -1135,6 +1145,13 @@ function kbach(options = {}) {
1135
1145
  enforce: "pre",
1136
1146
  configResolved(resolved) {
1137
1147
  root = resolved.root;
1148
+ if (!options.theme && !options.config) {
1149
+ const discovered = loadConfigFile(root);
1150
+ if (discovered) {
1151
+ theme = resolveKbachConfig(discovered);
1152
+ themeJson = JSON.stringify(theme);
1153
+ }
1154
+ }
1138
1155
  },
1139
1156
  buildStart() {
1140
1157
  projectCssClasses = scanProjectCssSelectors(root, includeDirs);