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

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/README.md CHANGED
@@ -1,32 +1,11 @@
1
1
  # @kbach/react
2
2
 
3
- A utility-first styling engine for React, powered by a real Rust/WASM core.
4
- Kbach aims for full [Tailwind CSS v4](https://tailwindcss.com) feature
5
- parity — grid, transforms, filters, gradients, animations, container
6
- queries, arbitrary values, and a parameterized variant system (`has-*`,
7
- `data-*`, `aria-*`, `not-*`, generalized `group-*`/`peer-*`, and more) — with
8
- its own 22-family + metals color palette as the one deliberate difference.
9
-
10
- > **Beta.** The API is close to stable but may still change before `1.0.0`.
11
- > Feedback and issues are very welcome.
12
-
13
- ## Why Kbach
14
-
15
- - **A real engine, not a lookup table.** Every class is resolved by the same
16
- Rust/WASM core at build time (via the Vite plugin) and at runtime (via
17
- `kb()`), so build-time and runtime output can never drift apart.
18
- - **Static by default.** The Vite plugin scans your source and writes a real,
19
- plain `kbach.css` file — no runtime style injection needed, no
20
- flash-of-unstyled-content, no client-side CSS-in-JS overhead.
21
- - **Dark mode that works without a provider.** Toggle/set/subscribe to dark
22
- mode from anywhere — a plain function, an event handler, a component that
23
- never wants React Context — with an optional `<ThemeProvider>` as a thin
24
- wrapper around the exact same store.
25
- - **Composable by nature.** Utility class strings are just strings — combine
26
- them with plain arrays, ternaries, or template literals. No bespoke
27
- composition API required.
28
-
29
- ## Installation
3
+ A utility-first styling engine for React, powered by a Rust/WASM core.
4
+ Tailwind CSS v4 feature parity, plus Kbach's own color palette.
5
+
6
+ > Beta — API may still change before 1.0.0.
7
+
8
+ ## Install
30
9
 
31
10
  ```sh
32
11
  npm install @kbach/react
@@ -34,9 +13,6 @@ npm install @kbach/react
34
13
 
35
14
  ## Quick start
36
15
 
37
- Add the Vite plugin so your utility classes are resolved to real CSS at
38
- build time:
39
-
40
16
  ```ts
41
17
  // vite.config.ts
42
18
  import { defineConfig } from 'vite';
@@ -48,9 +24,6 @@ export default defineConfig({
48
24
  });
49
25
  ```
50
26
 
51
- Create `src/kbach.css` with the markers the plugin writes between, and
52
- import it once:
53
-
54
27
  ```css
55
28
  /* src/kbach.css */
56
29
  /* kbach:start */
@@ -69,8 +42,6 @@ initKbach().then(() => {
69
42
  });
70
43
  ```
71
44
 
72
- Then just use utility classes:
73
-
74
45
  ```tsx
75
46
  <div className="flex items-center gap-4 bg-blue-6 text-white p-4 rounded-lg">
76
47
  Hello Kbach
@@ -79,40 +50,20 @@ Then just use utility classes:
79
50
 
80
51
  ## Dark mode
81
52
 
82
- Works standalone, with zero provider:
83
-
84
53
  ```tsx
85
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react';
54
+ import { useTheme } from '@kbach/react';
86
55
 
87
56
  function ThemeToggle() {
88
- const isDark = useGlobalDarkMode();
89
- return <button onClick={toggleGlobalDarkMode}>{isDark ? 'Dark' : 'Light'}</button>;
90
- }
91
- ```
92
-
93
- Or opt into the optional `<ThemeProvider>` / `useTheme()` pair — both read
94
- and write the exact same global store, so they always stay in sync with the
95
- standalone functions above:
96
-
97
- ```tsx
98
- import { ThemeProvider, useTheme } from '@kbach/react';
99
-
100
- function Toggle() {
101
57
  const { mode, isDark, setMode, toggle } = useTheme();
102
58
  return <button onClick={toggle}>{mode}</button>;
103
59
  }
104
-
105
- <ThemeProvider defaultMode="system">
106
- <Toggle />
107
- </ThemeProvider>;
108
60
  ```
109
61
 
110
- ## Theming
62
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
63
+ optional, for seeding a startup default. Outside React (plain functions,
64
+ event handlers), use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
111
65
 
112
- Customize the color palette, spacing scale, breakpoints, and dark-mode
113
- strategy via `setTheme()` (call once, before your first render) — the same
114
- theme object is used by both the runtime and the Vite plugin, so the two
115
- never fall out of sync:
66
+ ## Theming
116
67
 
117
68
  ```ts
118
69
  import { setTheme, defaultTheme } from '@kbach/react';
@@ -123,12 +74,10 @@ setTheme({
123
74
  });
124
75
  ```
125
76
 
126
- ## Runtime & dynamic classes
77
+ ## Dynamic classes
127
78
 
128
- Every class name that appears literally in your source is resolved at build
129
- time. For a class name assembled from truly dynamic data (e.g. user input)
130
- with no static occurrence anywhere in your source, call `kb()` — it resolves
131
- and injects the CSS at runtime and returns the class name unchanged:
79
+ Classes that appear literally in your source are resolved at build time.
80
+ For classes assembled from truly dynamic data, use `kb()`:
132
81
 
133
82
  ```ts
134
83
  import { kb } from '@kbach/react';
@@ -136,9 +85,37 @@ import { kb } from '@kbach/react';
136
85
  const className = kb(`bg-${userColor}-6`);
137
86
  ```
138
87
 
139
- If you're using the static build (the default with the Vite plugin), prefer
140
- a `safelist` entry in the plugin config for known-but-dynamic values instead
141
- — it keeps everything static with zero runtime cost.
88
+ Prefer a `safelist` entry in the plugin config when the possible values are
89
+ known ahead of time — keeps everything static.
90
+
91
+ ## Reading colors as values
92
+
93
+ For anywhere you need a real color, not a `className` (an SVG `fill`, a
94
+ chart library prop):
95
+
96
+ ```tsx
97
+ import { useColors } from '@kbach/react';
98
+
99
+ const colors = useColors();
100
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
101
+ colors.blue['6/50']; // same, at 50% opacity
102
+ colors.surface; // 'rgb(var(--kb-color-surface))' if defined as light/dark
103
+ ```
104
+
105
+ Any color — a shade-family entry like `blue-6` or a standalone name like
106
+ `surface` — can be defined in the theme as a plain string (static) or a
107
+ `{ light, dark }` pair, fully manual:
108
+
109
+ ```ts
110
+ setTheme({
111
+ ...defaultTheme,
112
+ colors: { ...defaultTheme.colors, surface: { light: '#f9fafb', dark: '#111827' } },
113
+ });
114
+ ```
115
+
116
+ A plain color always resolves to its literal value. A `{ light, dark }`
117
+ color resolves to a live CSS variable instead of a fixed hex — it stays
118
+ correct across theme changes with zero re-render.
142
119
 
143
120
  ## License
144
121
 
@@ -284,14 +284,15 @@ var defaultTheme = {
284
284
  // Real Tailwind's default spacing scale in full — every step is exactly
285
285
  // `n * 4px` (Tailwind's base spacing unit), including the fractional
286
286
  // `.5` steps (`1.5` -> 6px, `2.5` -> 10px, `3.5` -> 14px) and the "px"
287
- // literal (a bare 1px, for `p-px`-style hairline spacing). Previously
288
- // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
289
- // the real scale — so anything else (`p-3`, `w-5`, `border-2`, `gap-10`,
290
- // ...) silently failed to resolve (an unknown spacing key returns `None`
291
- // the same way an unknown class does, with no error). "0" itself is
292
- // still needed explicitly (not derivable as "any number * 4") for the
293
- // same reason it always was: `sticky top-0` needs an actual "0" key to
294
- // resolve at all, not just fall through as unresolvable.
287
+ // literal (a bare 1px, for `p-px`-style hairline spacing). This table is
288
+ // only the "usual stops", though — the Rust/JS engines' shared
289
+ // `spacing_px`/`spacingPx` helper falls back to Tailwind v4's own live
290
+ // `n * 4px` formula for any bare numeric step NOT listed here (`p-0.25`,
291
+ // `gap-1.75`, `w-13`, ...), so this table exists to name the common
292
+ // values and give a theme customization something to override, not to
293
+ // enumerate every resolvable step. "px" is the one exception that must
294
+ // stay listed explicitly — it isn't a number, so the formula can't
295
+ // derive it.
295
296
  spacing: {
296
297
  "0": 0,
297
298
  "px": 1,
@@ -336,7 +337,21 @@ var defaultTheme = {
336
337
  xl: 1280,
337
338
  "2xl": 1536
338
339
  },
339
- darkMode: "attribute"
340
+ // Byte-for-byte the same three stacks
341
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
342
+ // falls back to — kept here too (rather than leaving this empty and
343
+ // relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
344
+ // is a real, readable value for anything that wants to extend rather
345
+ // than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
346
+ fontFamily: {
347
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
348
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
349
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
350
+ },
351
+ darkMode: "attribute",
352
+ // Empty by default, matching real Tailwind's own defaults (no forced
353
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
354
+ container: {}
340
355
  };
341
356
  var activeTheme = defaultTheme;
342
357
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -0,0 +1,115 @@
1
+ import {
2
+ defaultTheme,
3
+ setTheme
4
+ } from "./chunk-N5HCQIBB.mjs";
5
+
6
+ // src/colorVariables.ts
7
+ function parseHexRgb(hex) {
8
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
9
+ if (!m) return null;
10
+ let h = m[1];
11
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
12
+ const n = parseInt(h, 16);
13
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
14
+ }
15
+ function wrapDarkDecls(strategy, decls) {
16
+ switch (strategy) {
17
+ case "class":
18
+ return `.dark { ${decls} }`;
19
+ case "media":
20
+ return `@media (prefers-color-scheme: dark) { :root { ${decls} } }`;
21
+ case "attribute":
22
+ default:
23
+ return `[data-theme="dark"] { ${decls} }`;
24
+ }
25
+ }
26
+ var STYLE_ID = "kbach-colors";
27
+ var injectedForTheme = null;
28
+ function ensureColorVariablesInjected(theme) {
29
+ if (typeof document === "undefined") return;
30
+ if (injectedForTheme === theme) return;
31
+ injectedForTheme = theme;
32
+ const lightDecls = [];
33
+ const darkDecls = [];
34
+ for (const [name, value] of Object.entries(theme.colors)) {
35
+ if (typeof value === "string") continue;
36
+ const light = parseHexRgb(value.light);
37
+ const dark = parseHexRgb(value.dark);
38
+ if (!light || !dark) continue;
39
+ lightDecls.push(`--kb-color-${name}: ${light.join(",")};`);
40
+ darkDecls.push(`--kb-color-${name}: ${dark.join(",")};`);
41
+ }
42
+ let el = document.getElementById(STYLE_ID);
43
+ if (lightDecls.length === 0) {
44
+ el?.remove();
45
+ return;
46
+ }
47
+ if (!el) {
48
+ el = document.createElement("style");
49
+ el.id = STYLE_ID;
50
+ document.head.insertBefore(el, document.head.firstChild);
51
+ }
52
+ el.textContent = `:root { ${lightDecls.join(" ")} }
53
+ ${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
54
+ }
55
+
56
+ // src/config.ts
57
+ function applyOpacityToHex(hex, opacityText) {
58
+ const rgb = parseHexRgb(hex);
59
+ if (!rgb) return hex;
60
+ const a = Number(opacityText) / 100;
61
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
62
+ return `rgba(${rgb.join(",")},${clamped})`;
63
+ }
64
+ function resolveColorEntry(value, lookup) {
65
+ if (typeof value !== "string") return value;
66
+ const slash = value.indexOf("/");
67
+ const name = slash === -1 ? value : value.slice(0, slash);
68
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
69
+ const base = lookup(name);
70
+ if (base === void 0) return value;
71
+ if (opacity === void 0) return base;
72
+ return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
73
+ }
74
+ function resolveColors(extend, existing) {
75
+ const resolved = {};
76
+ const lookup = (name) => resolved[name] ?? existing[name];
77
+ for (const [name, value] of Object.entries(extend)) {
78
+ resolved[name] = resolveColorEntry(value, lookup);
79
+ }
80
+ return resolved;
81
+ }
82
+ function resolveKbachConfig(config) {
83
+ let theme = { ...defaultTheme, ...config.theme };
84
+ if (config.extend?.colors) {
85
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
86
+ }
87
+ if (config.extend?.spacing) {
88
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
89
+ }
90
+ if (config.extend?.screens) {
91
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
92
+ }
93
+ if (config.extend?.fontFamily) {
94
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
95
+ }
96
+ if (config.extend?.container) {
97
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
98
+ }
99
+ if (config.darkMode) {
100
+ theme = { ...theme, darkMode: config.darkMode };
101
+ }
102
+ return theme;
103
+ }
104
+ function applyKbachConfig(config) {
105
+ const theme = resolveKbachConfig(config);
106
+ setTheme(theme);
107
+ return theme;
108
+ }
109
+
110
+ export {
111
+ parseHexRgb,
112
+ ensureColorVariablesInjected,
113
+ resolveKbachConfig,
114
+ applyKbachConfig
115
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  kb
3
- } from "./chunk-ROZXCQSM.mjs";
3
+ } from "./chunk-ZQJKL2MV.mjs";
4
4
 
5
5
  // src/jsx-runtime.tsx
6
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -2,7 +2,7 @@ import {
2
2
  BASE_RESET,
3
3
  RESET_STYLE_ID,
4
4
  getThemeJson
5
- } from "./chunk-FOQWQDF4.mjs";
5
+ } from "./chunk-N5HCQIBB.mjs";
6
6
 
7
7
  // src/wasmLoader.ts
8
8
  import init, { generate_css as wasmGenerateCss } from "@kbach/core-engine";
@@ -0,0 +1,105 @@
1
+ /** A plain hex color, or a mode-aware pair auto-expanded into a light/dark class pair by the Rust engine. */
2
+ type ColorEntry = string | {
3
+ light: string;
4
+ dark: string;
5
+ };
6
+ type DarkModeStrategy = 'attribute' | 'class' | 'media';
7
+ /**
8
+ * Real Tailwind's `theme.container` config, mirroring
9
+ * `packages/core-engine/src/theme.rs`'s `ContainerConfig` exactly (same
10
+ * field names, `camelCase` on both sides via serde). `padding` is one
11
+ * uniform value applied at every breakpoint (including below the smallest
12
+ * one) rather than real Tailwind's optional per-breakpoint object form —
13
+ * the common case in practice, and simpler to reason about; a
14
+ * per-breakpoint padding map is a natural follow-up if ever needed.
15
+ */
16
+ interface ContainerConfig {
17
+ center?: boolean;
18
+ padding?: string;
19
+ }
20
+ interface ThemeConfig {
21
+ /** e.g. "blue-6" -> "#2563eb", or "surface" -> { light, dark } */
22
+ colors: Record<string, ColorEntry>;
23
+ /** e.g. "4" -> 16 (px) */
24
+ spacing: Record<string, number>;
25
+ /** e.g. "sm" -> 640 (min-width px) */
26
+ screens: Record<string, number>;
27
+ /**
28
+ * e.g. "sans" -> "ui-sans-serif, system-ui, ...". Optional — the Rust
29
+ * engine's own `font_family_value` already carries a hardcoded fallback
30
+ * for "sans"/"serif"/"mono" (see its own doc comment) for when a theme
31
+ * omits this field (or a name within it) entirely, matching this field's
32
+ * `#[serde(default)]` on the Rust side. `defaultTheme` below still
33
+ * populates all three explicitly, matching the Rust fallback strings
34
+ * byte-for-byte, so a caller reading `defaultTheme.fontFamily.sans`
35
+ * (e.g. to extend rather than replace it) gets the real value rather
36
+ * than needing to already know the Rust-side fallback text.
37
+ */
38
+ fontFamily?: Record<string, string>;
39
+ darkMode: DarkModeStrategy;
40
+ /** Optional, matching `ContainerConfig`'s own `#[serde(default)]` on the Rust side — an omitted theme.container behaves exactly like `{}`. */
41
+ container?: ContainerConfig;
42
+ }
43
+ declare const defaultTheme: ThemeConfig;
44
+ declare function setTheme(theme: ThemeConfig): void;
45
+ declare function getTheme(): ThemeConfig;
46
+
47
+ /**
48
+ * A `kbach.config.js`-style theme customization, in one plain object — same
49
+ * `theme`/`extend`/`darkMode` shape real Tailwind's own config file uses,
50
+ * simplified from old-kbach's version (no plugins, no color alias chains
51
+ * with cycle detection, no live-update-listener store): a config object
52
+ * goes in, a resolved `ThemeConfig` comes out, via one PURE function
53
+ * (`resolveKbachConfig`) — same input always produces the same output, so
54
+ * nothing needs to stay "in sync" the way old-kbach's Babel-injected config
55
+ * singleton did. Call it identically from a Node.js build script
56
+ * (`vite.config.ts`) and from browser/app code (`applyKbachConfig`) and
57
+ * both get the exact same theme, no synchronization mechanism required.
58
+ */
59
+ interface KbachConfig {
60
+ /** Applied on top of `defaultTheme`; `undefined` means "keep the default". */
61
+ darkMode?: DarkModeStrategy;
62
+ /**
63
+ * REPLACES the named section of `defaultTheme` entirely (e.g. supplying
64
+ * `theme.colors` drops the rest of the default palette) — use `extend`
65
+ * instead to add to the defaults rather than replace them. `container`
66
+ * isn't here (only under `extend` below) — it has no "defaults to
67
+ * replace" the way colors/spacing/screens/fontFamily do, since real
68
+ * Tailwind's own container config is inherently additive (center/padding
69
+ * on top of the always-present breakpoint ladder), so there's no
70
+ * meaningful distinction between "replace" and "extend" for it.
71
+ */
72
+ theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
73
+ /** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
74
+ extend?: {
75
+ /**
76
+ * A value is either a literal color (a hex string, or `{ light, dark }`
77
+ * — same mode-aware shape `useColors()`/`useTheme()` already support)
78
+ * or a REFERENCE to another color name, optionally with an opacity
79
+ * suffix. Two things can be referenced:
80
+ * - A BUILT-IN palette color, always available regardless of ordering
81
+ * (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`).
82
+ * - A custom color defined EARLIER in this same object
83
+ * (`{ brand: '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft`
84
+ * resolves to `brand`'s own value at 30% opacity, computed once here
85
+ * rather than needing a `colors.alpha()` call at every use site).
86
+ * A string that doesn't match any known color name (built-in or
87
+ * already-defined-above) is treated as a literal CSS value instead
88
+ * (`accent: 'red'` — REAL CSS `red`, since there's no color named "red"
89
+ * to alias), never an error either way.
90
+ */
91
+ colors?: Record<string, ColorEntry | string>;
92
+ spacing?: Record<string, number>;
93
+ screens?: Record<string, number>;
94
+ /** Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` — adds to (or overrides one of) `defaultTheme.fontFamily`'s three names. */
95
+ fontFamily?: Record<string, string>;
96
+ /** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
97
+ container?: ContainerConfig;
98
+ };
99
+ }
100
+ /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
101
+ 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). */
103
+ declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
104
+
105
+ 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 };
@@ -0,0 +1,105 @@
1
+ /** A plain hex color, or a mode-aware pair auto-expanded into a light/dark class pair by the Rust engine. */
2
+ type ColorEntry = string | {
3
+ light: string;
4
+ dark: string;
5
+ };
6
+ type DarkModeStrategy = 'attribute' | 'class' | 'media';
7
+ /**
8
+ * Real Tailwind's `theme.container` config, mirroring
9
+ * `packages/core-engine/src/theme.rs`'s `ContainerConfig` exactly (same
10
+ * field names, `camelCase` on both sides via serde). `padding` is one
11
+ * uniform value applied at every breakpoint (including below the smallest
12
+ * one) rather than real Tailwind's optional per-breakpoint object form —
13
+ * the common case in practice, and simpler to reason about; a
14
+ * per-breakpoint padding map is a natural follow-up if ever needed.
15
+ */
16
+ interface ContainerConfig {
17
+ center?: boolean;
18
+ padding?: string;
19
+ }
20
+ interface ThemeConfig {
21
+ /** e.g. "blue-6" -> "#2563eb", or "surface" -> { light, dark } */
22
+ colors: Record<string, ColorEntry>;
23
+ /** e.g. "4" -> 16 (px) */
24
+ spacing: Record<string, number>;
25
+ /** e.g. "sm" -> 640 (min-width px) */
26
+ screens: Record<string, number>;
27
+ /**
28
+ * e.g. "sans" -> "ui-sans-serif, system-ui, ...". Optional — the Rust
29
+ * engine's own `font_family_value` already carries a hardcoded fallback
30
+ * for "sans"/"serif"/"mono" (see its own doc comment) for when a theme
31
+ * omits this field (or a name within it) entirely, matching this field's
32
+ * `#[serde(default)]` on the Rust side. `defaultTheme` below still
33
+ * populates all three explicitly, matching the Rust fallback strings
34
+ * byte-for-byte, so a caller reading `defaultTheme.fontFamily.sans`
35
+ * (e.g. to extend rather than replace it) gets the real value rather
36
+ * than needing to already know the Rust-side fallback text.
37
+ */
38
+ fontFamily?: Record<string, string>;
39
+ darkMode: DarkModeStrategy;
40
+ /** Optional, matching `ContainerConfig`'s own `#[serde(default)]` on the Rust side — an omitted theme.container behaves exactly like `{}`. */
41
+ container?: ContainerConfig;
42
+ }
43
+ declare const defaultTheme: ThemeConfig;
44
+ declare function setTheme(theme: ThemeConfig): void;
45
+ declare function getTheme(): ThemeConfig;
46
+
47
+ /**
48
+ * A `kbach.config.js`-style theme customization, in one plain object — same
49
+ * `theme`/`extend`/`darkMode` shape real Tailwind's own config file uses,
50
+ * simplified from old-kbach's version (no plugins, no color alias chains
51
+ * with cycle detection, no live-update-listener store): a config object
52
+ * goes in, a resolved `ThemeConfig` comes out, via one PURE function
53
+ * (`resolveKbachConfig`) — same input always produces the same output, so
54
+ * nothing needs to stay "in sync" the way old-kbach's Babel-injected config
55
+ * singleton did. Call it identically from a Node.js build script
56
+ * (`vite.config.ts`) and from browser/app code (`applyKbachConfig`) and
57
+ * both get the exact same theme, no synchronization mechanism required.
58
+ */
59
+ interface KbachConfig {
60
+ /** Applied on top of `defaultTheme`; `undefined` means "keep the default". */
61
+ darkMode?: DarkModeStrategy;
62
+ /**
63
+ * REPLACES the named section of `defaultTheme` entirely (e.g. supplying
64
+ * `theme.colors` drops the rest of the default palette) — use `extend`
65
+ * instead to add to the defaults rather than replace them. `container`
66
+ * isn't here (only under `extend` below) — it has no "defaults to
67
+ * replace" the way colors/spacing/screens/fontFamily do, since real
68
+ * Tailwind's own container config is inherently additive (center/padding
69
+ * on top of the always-present breakpoint ladder), so there's no
70
+ * meaningful distinction between "replace" and "extend" for it.
71
+ */
72
+ theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
73
+ /** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
74
+ extend?: {
75
+ /**
76
+ * A value is either a literal color (a hex string, or `{ light, dark }`
77
+ * — same mode-aware shape `useColors()`/`useTheme()` already support)
78
+ * or a REFERENCE to another color name, optionally with an opacity
79
+ * suffix. Two things can be referenced:
80
+ * - A BUILT-IN palette color, always available regardless of ordering
81
+ * (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`).
82
+ * - A custom color defined EARLIER in this same object
83
+ * (`{ brand: '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft`
84
+ * resolves to `brand`'s own value at 30% opacity, computed once here
85
+ * rather than needing a `colors.alpha()` call at every use site).
86
+ * A string that doesn't match any known color name (built-in or
87
+ * already-defined-above) is treated as a literal CSS value instead
88
+ * (`accent: 'red'` — REAL CSS `red`, since there's no color named "red"
89
+ * to alias), never an error either way.
90
+ */
91
+ colors?: Record<string, ColorEntry | string>;
92
+ spacing?: Record<string, number>;
93
+ screens?: Record<string, number>;
94
+ /** Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` — adds to (or overrides one of) `defaultTheme.fontFamily`'s three names. */
95
+ fontFamily?: Record<string, string>;
96
+ /** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
97
+ container?: ContainerConfig;
98
+ };
99
+ }
100
+ /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
101
+ 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). */
103
+ declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
104
+
105
+ 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 };