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

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,222 @@ 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.
85
+
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
+ ]);
81
98
 
82
- Works standalone, with zero provider:
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:
194
+
195
+ ```tsx
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) — this
203
+ needs setting in **two places**, since they solve two different problems:
204
+
205
+ 1. **Build-time**, via `kbach.config.js` or the plugin's `config`/`theme`
206
+ option — controls which selector the generated CSS for `dark:` classes
207
+ compiles to (`[data-theme="dark"] .dark\:bg-neutral-12` vs
208
+ `.dark .dark\:bg-neutral-12` vs a plain `@media` query).
209
+ 2. **Runtime**, via `applyKbachConfig()` — controls what `useTheme()`'s
210
+ `toggle()`/`setMode()` actually write to the DOM. This is a *separate*
211
+ module from the build step above (it has to be: the plugin runs in
212
+ Node during your build, this runs in the browser) — setting only the
213
+ plugin option compiles the right CSS but leaves the toggle a silent
214
+ no-op, since the runtime still thinks the strategy is the `'media'`
215
+ default and has nothing to write to the DOM for it.
216
+
217
+ ```js
218
+ // kbach.config.js — build-time, read by the Vite/PostCSS plugin
219
+ export default { darkMode: 'attribute' };
220
+ ```
96
221
 
97
222
  ```tsx
98
- import { ThemeProvider, useTheme } from '@kbach/react';
223
+ // app entry (e.g. root.tsx / main.tsx) — runtime, same value as above
224
+ import { applyKbachConfig, useTheme } from '@kbach/react';
99
225
 
100
- function Toggle() {
226
+ applyKbachConfig({ darkMode: 'attribute' }); // call once, before first render
227
+
228
+ function ThemeToggle() {
101
229
  const { mode, isDark, setMode, toggle } = useTheme();
102
230
  return <button onClick={toggle}>{mode}</button>;
103
231
  }
104
-
105
- <ThemeProvider defaultMode="system">
106
- <Toggle />
107
- </ThemeProvider>;
108
232
  ```
109
233
 
110
- ## Theming
234
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
235
+ optional, for seeding a startup default. Outside React (plain functions,
236
+ event handlers), use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
237
+ Note: `useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly
238
+ under the default `'media'` strategy too (and even without ever calling
239
+ `applyKbachConfig()`) — they just won't visibly affect `dark:` classes
240
+ unless the runtime has also been told about `'class'`/`'attribute'`.
111
241
 
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:
242
+ ## Theming
116
243
 
117
244
  ```ts
118
245
  import { setTheme, defaultTheme } from '@kbach/react';
@@ -123,22 +250,52 @@ setTheme({
123
250
  });
124
251
  ```
125
252
 
126
- ## Runtime & dynamic classes
253
+ ## Dynamic classes
127
254
 
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:
255
+ Classes that appear literally in your source are resolved at build time.
256
+ For classes assembled from truly dynamic data, use `kb()` — this is the
257
+ one thing in this package that genuinely calls into the WASM engine at
258
+ runtime, so `await initKbach()` once at startup before the first `kb()`
259
+ call (or before rendering, if you can't guarantee it runs first otherwise):
132
260
 
133
261
  ```ts
134
- import { kb } from '@kbach/react';
262
+ import { initKbach, kb } from '@kbach/react';
135
263
 
264
+ await initKbach();
136
265
  const className = kb(`bg-${userColor}-6`);
137
266
  ```
138
267
 
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.
268
+ Prefer a `safelist` entry in the plugin config when the possible values are
269
+ known ahead of time — keeps everything static, no `initKbach()` needed.
270
+
271
+ ## Reading colors as values
272
+
273
+ For anywhere you need a real color, not a `className` (an SVG `fill`, a
274
+ chart library prop):
275
+
276
+ ```tsx
277
+ import { useColors } from '@kbach/react';
278
+
279
+ const colors = useColors();
280
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
281
+ colors.blue['6/50']; // same, at 50% opacity
282
+ colors.surface; // 'rgb(var(--kb-color-surface))' if defined as light/dark
283
+ ```
284
+
285
+ Any color — a shade-family entry like `blue-6` or a standalone name like
286
+ `surface` — can be defined in the theme as a plain string (static) or a
287
+ `{ light, dark }` pair, fully manual:
288
+
289
+ ```ts
290
+ setTheme({
291
+ ...defaultTheme,
292
+ colors: { ...defaultTheme.colors, surface: { light: '#f9fafb', dark: '#111827' } },
293
+ });
294
+ ```
295
+
296
+ A plain color always resolves to its literal value. A `{ light, dark }`
297
+ color resolves to a live CSS variable instead of a fixed hex — it stays
298
+ correct across theme changes with zero re-render.
142
299
 
143
300
  ## License
144
301
 
@@ -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
+ };
@@ -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,33 @@ 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
+ // 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: {}
340
367
  };
341
368
  var activeTheme = defaultTheme;
342
369
  var activeThemeJson = JSON.stringify(defaultTheme);