@kbach/react 1.0.0-beta.0 → 1.0.0-beta.10

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,71 +24,208 @@ 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
- ```css
55
- /* src/kbach.css */
56
- /* kbach:start */
57
- /* kbach:end */
58
- ```
27
+ That's it — the plugin finds or creates your `kbach.css` on its own,
28
+ wherever your source actually lives (`src/`, `app/`, ...; see "Where does
29
+ `kbach.css` go?" below if you want to know exactly where or control it).
30
+ Import it once from your entry point and render normally:
59
31
 
60
32
  ```tsx
61
33
  // main.tsx
62
- import { initKbach } from '@kbach/react';
63
34
  import { createRoot } from 'react-dom/client';
64
35
  import App from './App';
65
36
  import './kbach.css';
66
37
 
67
- initKbach().then(() => {
68
- createRoot(document.getElementById('root')!).render(<App />);
69
- });
38
+ createRoot(document.getElementById('root')!).render(<App />);
70
39
  ```
71
40
 
72
- Then just use utility classes:
73
-
74
41
  ```tsx
75
42
  <div className="flex items-center gap-4 bg-blue-6 text-white p-4 rounded-lg">
76
43
  Hello Kbach
77
44
  </div>
78
45
  ```
79
46
 
80
- ## Dark mode
47
+ No `initKbach()`/async step before your first render: `className` here is
48
+ a plain string prop, resolved entirely by the CSS `@kbach/react/vite`
49
+ already generated at build time, same as any other static stylesheet —
50
+ nothing here calls into the WASM engine at render time, so there's nothing
51
+ to await.
52
+
53
+ ### Where does `kbach.css` go?
54
+
55
+ You don't need to think about this for the common case — skip to
56
+ [React Router](#react-router)/[Next.js](#nextjs) if that's what brought you
57
+ here. For everyone else: the plugin looks for a file already containing
58
+ `/* kbach:start */`/`/* kbach:end */` markers under each of your `include`
59
+ directories (`src`, `app`, `pages`, `components` by default) and the
60
+ project root, in that order, and keeps using whichever one it finds. If
61
+ none exists yet, it creates `<first-existing-include-dir>/kbach.css` for
62
+ you, markers included — you never have to hand-author that file. Pass
63
+ `cssFile` to `kbach()` only if you want to pin the location explicitly
64
+ (a path outside every `include` dir, or more than one candidate present at
65
+ once).
66
+
67
+ `initKbach()` (still exported from `@kbach/react`) is only needed for two
68
+ opt-in features that genuinely do call into the Rust/WASM engine at
69
+ runtime: the dynamic [`kb()`](#dynamic-classes) function, and the optional
70
+ `@kbach/react/jsx-runtime` (a `jsxImportSource` override that intercepts
71
+ *every* element's `className` automatically instead of relying on the
72
+ static build). If you use either, `await initKbach()` once before your
73
+ first render, same as before.
74
+
75
+ ## React Router
76
+
77
+ React Router is a client-side routing library with no build-tool coupling
78
+ of its own, so it layers on top of the exact same Vite + `kbach.css` setup
79
+ either way — but which "either way" depends on which of React Router's two
80
+ genuinely different modes you're using.
81
+
82
+ **Library mode** (you call `createRoot` yourself — `createBrowserRouter` +
83
+ `<RouterProvider>`, or the classic `<BrowserRouter>` component): needs
84
+ nothing beyond the Quick Start above.
81
85
 
82
- Works standalone, with zero provider:
86
+ ```tsx
87
+ // main.tsx
88
+ import { createRoot } from 'react-dom/client';
89
+ import { RouterProvider, createBrowserRouter } from 'react-router';
90
+ import './kbach.css';
91
+ import Home from './routes/home';
92
+ import About from './routes/about';
93
+
94
+ const router = createBrowserRouter([
95
+ { path: '/', Component: Home },
96
+ { path: '/about', Component: About },
97
+ ]);
98
+
99
+ createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />);
100
+ ```
101
+
102
+ **Framework mode** (`npx create-react-router@latest` — file-based routing
103
+ under `app/`, its own Vite plugin, SSR by default):
104
+
105
+ ```ts
106
+ // vite.config.ts
107
+ import { reactRouter } from '@react-router/dev/vite';
108
+ import { kbach } from '@kbach/react/vite';
109
+ import { defineConfig } from 'vite';
110
+
111
+ export default defineConfig({
112
+ plugins: [kbach(), reactRouter()],
113
+ });
114
+ ```
115
+
116
+ Same `kbach()` call as everywhere else — its `app/`-vs-`src/` convention
117
+ is exactly what auto-detection (above) exists for, so there's nothing
118
+ React-Router-specific to configure. The one real gotcha is unrelated to
119
+ Kbach at all: **don't call `createRoot` yourself in `app/root.tsx`** — the
120
+ framework's own generated client/server entry points already do this;
121
+ `root.tsx` only ever *exports* a `Layout`/default component. Adding your
122
+ own `createRoot(...).render(...)` at that file's top level runs during SSR
123
+ too, where `document` doesn't exist, and crashes the build immediately.
124
+ `app/root.tsx` needs only:
83
125
 
84
126
  ```tsx
85
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react';
127
+ import './kbach.css';
128
+ export default function App() {
129
+ return <Outlet />;
130
+ }
131
+ ```
86
132
 
87
- function ThemeToggle() {
88
- const isDark = useGlobalDarkMode();
89
- return <button onClick={toggleGlobalDarkMode}>{isDark ? 'Dark' : 'Light'}</button>;
133
+ Either mode: every route's own components use `className` exactly like any
134
+ other component — routing has no effect on how Kbach resolves classes,
135
+ since resolution already happened at build time, before routing even
136
+ enters the picture.
137
+
138
+ ## Next.js
139
+
140
+ Next.js uses webpack/Turbopack, not Vite, so `@kbach/react/vite` doesn't
141
+ apply — use `@kbach/react/postcss` instead, which does the exact same
142
+ build-time class-string scanning and CSS generation (they share the same
143
+ underlying engine), just wired into PostCSS, which both of Next.js's
144
+ bundlers already run for you with zero extra config.
145
+
146
+ ```js
147
+ // postcss.config.js
148
+ module.exports = {
149
+ plugins: {
150
+ '@kbach/react/postcss': {},
151
+ },
152
+ };
153
+ ```
154
+
155
+ ```css
156
+ /* app/globals.css (App Router) or styles/globals.css (Pages Router) */
157
+ /* kbach:start */
158
+ /* kbach:end */
159
+ ```
160
+
161
+ ```tsx
162
+ // app/layout.tsx
163
+ import './globals.css';
164
+
165
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
166
+ return (
167
+ <html lang="en">
168
+ <body>{children}</body>
169
+ </html>
170
+ );
90
171
  }
91
172
  ```
92
173
 
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:
174
+ Plain `className="..."` strings on Server Components work with zero extra
175
+ setup — the CSS already exists at build time, same as any other static
176
+ stylesheet. Hook-based exports (`useTheme`, `useColors`, `<ThemeProvider>`)
177
+ already carry their own `'use client'` directive, so importing them into a
178
+ Client Component just works; importing one into a Server Component is a
179
+ compile-time error the same way importing `useState` into one would be —
180
+ that's React's own rule, not a Kbach limitation.
181
+
182
+ `root`/`theme`/`config`/`include`/`safelist` options all match
183
+ `@kbach/react/vite`'s exactly (see `KbachPluginOptions`, above) — `root`
184
+ additionally defaults to `process.cwd()` rather than a bundler-resolved
185
+ project root, since PostCSS has no equivalent to Vite's own resolved
186
+ `root` to read; pass it explicitly if your `postcss.config.js` doesn't
187
+ live at your project root (e.g. a monorepo app).
188
+
189
+ ## Dark mode
190
+
191
+ By default, `dark:` classes follow the OS `prefers-color-scheme` setting via
192
+ plain CSS — zero config, zero JS required, works even if your app never
193
+ imports anything from `@kbach/react`'s runtime:
96
194
 
97
195
  ```tsx
98
- import { ThemeProvider, useTheme } from '@kbach/react';
196
+ <div className="bg-white dark:bg-neutral-12">...</div>
197
+ ```
198
+
199
+ This default (`darkMode: 'media'`) can't be overridden by an in-app toggle —
200
+ `@media (prefers-color-scheme)` only ever reflects the real OS setting. To
201
+ add a manual light/dark switcher, opt into `darkMode: 'class'` or
202
+ `'attribute'` (writes `.dark`/`data-theme` onto `<html>` instead) via
203
+ `kbach.config.js` or the plugin's `config`/`theme` option:
204
+
205
+ ```js
206
+ // kbach.config.js
207
+ export default { darkMode: 'attribute' };
208
+ ```
99
209
 
100
- function Toggle() {
210
+ Then:
211
+
212
+ ```tsx
213
+ import { useTheme } from '@kbach/react';
214
+
215
+ function ThemeToggle() {
101
216
  const { mode, isDark, setMode, toggle } = useTheme();
102
217
  return <button onClick={toggle}>{mode}</button>;
103
218
  }
104
-
105
- <ThemeProvider defaultMode="system">
106
- <Toggle />
107
- </ThemeProvider>;
108
219
  ```
109
220
 
110
- ## Theming
221
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
222
+ optional, for seeding a startup default. Outside React (plain functions,
223
+ event handlers), use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
224
+ Note: `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
225
+ under the default `'media'` strategy too — they just won't visibly affect
226
+ `dark:` classes unless you've switched to `'class'`/`'attribute'`.
111
227
 
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:
228
+ ## Theming
116
229
 
117
230
  ```ts
118
231
  import { setTheme, defaultTheme } from '@kbach/react';
@@ -123,22 +236,52 @@ setTheme({
123
236
  });
124
237
  ```
125
238
 
126
- ## Runtime & dynamic classes
239
+ ## Dynamic classes
127
240
 
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:
241
+ Classes that appear literally in your source are resolved at build time.
242
+ For classes assembled from truly dynamic data, use `kb()` — this is the
243
+ one thing in this package that genuinely calls into the WASM engine at
244
+ runtime, so `await initKbach()` once at startup before the first `kb()`
245
+ call (or before rendering, if you can't guarantee it runs first otherwise):
132
246
 
133
247
  ```ts
134
- import { kb } from '@kbach/react';
248
+ import { initKbach, kb } from '@kbach/react';
135
249
 
250
+ await initKbach();
136
251
  const className = kb(`bg-${userColor}-6`);
137
252
  ```
138
253
 
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.
254
+ Prefer a `safelist` entry in the plugin config when the possible values are
255
+ known ahead of time — keeps everything static, no `initKbach()` needed.
256
+
257
+ ## Reading colors as values
258
+
259
+ For anywhere you need a real color, not a `className` (an SVG `fill`, a
260
+ chart library prop):
261
+
262
+ ```tsx
263
+ import { useColors } from '@kbach/react';
264
+
265
+ const colors = useColors();
266
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
267
+ colors.blue['6/50']; // same, at 50% opacity
268
+ colors.surface; // 'rgb(var(--kb-color-surface))' if defined as light/dark
269
+ ```
270
+
271
+ Any color — a shade-family entry like `blue-6` or a standalone name like
272
+ `surface` — can be defined in the theme as a plain string (static) or a
273
+ `{ light, dark }` pair, fully manual:
274
+
275
+ ```ts
276
+ setTheme({
277
+ ...defaultTheme,
278
+ colors: { ...defaultTheme.colors, surface: { light: '#f9fafb', dark: '#111827' } },
279
+ });
280
+ ```
281
+
282
+ A plain color always resolves to its literal value. A `{ light, dark }`
283
+ color resolves to a live CSS variable instead of a fixed hex — it stays
284
+ correct across theme changes with zero re-render.
142
285
 
143
286
  ## License
144
287
 
@@ -0,0 +1,115 @@
1
+ import {
2
+ defaultTheme,
3
+ setTheme
4
+ } from "./chunk-EOSN5CYG.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
+ };
@@ -281,21 +281,54 @@ var defaultTheme = {
281
281
  ...PALETTE,
282
282
  surface: { light: "#f9fafb", dark: "#111827" }
283
283
  },
284
+ // Real Tailwind's default spacing scale in full — every step is exactly
285
+ // `n * 4px` (Tailwind's base spacing unit), including the fractional
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). 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.
284
296
  spacing: {
285
- // "0" is always a hardcoded zero in real Tailwind's own scale too,
286
- // regardless of the base spacing unit — needed for `p-0`/`m-0`/
287
- // `top-0`/`inset-0`/etc., and specifically for `sticky top-0` (a
288
- // sticky element needs an explicit offset to have any stick point at
289
- // all; without one it behaves just like `relative`, no different from
290
- // omitting `top-0` outright). Confirmed missing by hand: this
291
- // sandbox's own header used `sticky top-0`, which silently never
292
- // resolved (no "0" spacing key existed) until this was added.
293
297
  "0": 0,
298
+ "px": 1,
299
+ "0.5": 2,
294
300
  "1": 4,
301
+ "1.5": 6,
295
302
  "2": 8,
303
+ "2.5": 10,
304
+ "3": 12,
305
+ "3.5": 14,
296
306
  "4": 16,
307
+ "5": 20,
297
308
  "6": 24,
298
- "8": 32
309
+ "7": 28,
310
+ "8": 32,
311
+ "9": 36,
312
+ "10": 40,
313
+ "11": 44,
314
+ "12": 48,
315
+ "14": 56,
316
+ "16": 64,
317
+ "20": 80,
318
+ "24": 96,
319
+ "28": 112,
320
+ "32": 128,
321
+ "36": 144,
322
+ "40": 160,
323
+ "44": 176,
324
+ "48": 192,
325
+ "52": 208,
326
+ "56": 224,
327
+ "60": 240,
328
+ "64": 256,
329
+ "72": 288,
330
+ "80": 320,
331
+ "96": 384
299
332
  },
300
333
  screens: {
301
334
  sm: 640,
@@ -304,7 +337,33 @@ var defaultTheme = {
304
337
  xl: 1280,
305
338
  "2xl": 1536
306
339
  },
307
- 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
+ // Matches real Tailwind's own default (`darkMode: 'media'`): pure
352
+ // `@media (prefers-color-scheme: dark)`, works with zero JS — a project
353
+ // using only static `className` strings gets working `dark:` classes
354
+ // out of the box, with no runtime import required to write `data-theme`/
355
+ // `.dark` onto `<html>` (see darkModeStore.ts's `applyToDom`). The
356
+ // tradeoff: `'media'` can't be overridden by a manual light/dark toggle —
357
+ // `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
358
+ // for JS consumers, but flipping it has no visible effect on `dark:`
359
+ // classes, since the browser's media query only ever reflects the real
360
+ // OS setting. Opt into `darkMode: 'class'` or `'attribute'` explicitly
361
+ // (via `kbach.config.js` or the `config`/`theme` plugin option) as soon
362
+ // as an app wants a real in-app toggle.
363
+ darkMode: "media",
364
+ // Empty by default, matching real Tailwind's own defaults (no forced
365
+ // centering/padding) — see `ContainerConfig`'s own doc comment.
366
+ container: {}
308
367
  };
309
368
  var activeTheme = defaultTheme;
310
369
  var activeThemeJson = JSON.stringify(defaultTheme);