@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 +230 -73
- package/dist/chunk-4Y2QXNJY.mjs +115 -0
- package/dist/{chunk-FOQWQDF4.mjs → chunk-EOSN5CYG.mjs} +36 -9
- package/dist/chunk-J2M6QWJW.mjs +1385 -0
- package/dist/{chunk-AD3G6RHY.mjs → chunk-L6B4DTLD.mjs} +1 -1
- package/dist/{chunk-ROZXCQSM.mjs → chunk-ROKZPEHA.mjs} +1 -1
- package/dist/config-BQ_b-qxF.d.mts +105 -0
- package/dist/config-BQ_b-qxF.d.ts +105 -0
- package/dist/index.d.mts +65 -34
- package/dist/index.d.ts +65 -34
- package/dist/index.js +227 -43
- package/dist/index.mjs +90 -33
- package/dist/jsx-dev-runtime.js +36 -9
- package/dist/jsx-dev-runtime.mjs +3 -3
- package/dist/jsx-runtime.js +36 -9
- package/dist/jsx-runtime.mjs +3 -3
- package/dist/postcss-plugin.d.mts +36 -0
- package/dist/postcss-plugin.d.ts +36 -0
- package/dist/postcss-plugin.js +1152 -0
- package/dist/postcss-plugin.mjs +79 -0
- package/dist/vite-plugin.d.mts +108 -6
- package/dist/vite-plugin.d.ts +108 -6
- package/dist/vite-plugin.js +512 -225
- package/dist/vite-plugin.mjs +15 -1181
- package/package.json +13 -2
- package/dist/theme-BKAb78KP.d.mts +0 -20
- package/dist/theme-BKAb78KP.d.ts +0 -20
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
127
|
+
import './kbach.css';
|
|
128
|
+
export default function App() {
|
|
129
|
+
return <Outlet />;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
86
132
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
223
|
+
// app entry (e.g. root.tsx / main.tsx) — runtime, same value as above
|
|
224
|
+
import { applyKbachConfig, useTheme } from '@kbach/react';
|
|
99
225
|
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
253
|
+
## Dynamic classes
|
|
127
254
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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).
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
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
|
-
|
|
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);
|