@kbach/react 1.0.0-beta.2 → 1.0.0-beta.20
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 +204 -16
- package/dist/chunk-43FOLRQA.mjs +234 -0
- package/dist/{chunk-PAJ6KHDB.mjs → chunk-5EXGGZMA.mjs} +1 -1
- package/dist/{chunk-ZQJKL2MV.mjs → chunk-BJEOK2VK.mjs} +18 -4
- package/dist/chunk-MRBB55KV.mjs +687 -0
- package/dist/{chunk-N5HCQIBB.mjs → chunk-OVKVZA7B.mjs} +21 -1
- package/dist/{config-BQ_b-qxF.d.mts → config-N74_lKa0.d.mts} +33 -2
- package/dist/{config-BQ_b-qxF.d.ts → config-N74_lKa0.d.ts} +33 -2
- package/dist/index.d.mts +67 -2
- package/dist/index.d.ts +67 -2
- package/dist/index.js +157 -69
- package/dist/index.mjs +55 -106
- package/dist/jsx-dev-runtime.js +30 -4
- package/dist/jsx-dev-runtime.mjs +3 -3
- package/dist/jsx-runtime.js +30 -4
- 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 +1250 -0
- package/dist/postcss-plugin.mjs +84 -0
- package/dist/vite-plugin.d.mts +94 -6
- package/dist/vite-plugin.d.ts +94 -6
- package/dist/vite-plugin.js +495 -910
- package/dist/vite-plugin.mjs +15 -1198
- package/package.json +26 -4
- package/dist/chunk-NKR3PSWO.mjs +0 -115
package/README.md
CHANGED
|
@@ -24,22 +24,18 @@ export default defineConfig({
|
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
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:
|
|
32
31
|
|
|
33
32
|
```tsx
|
|
34
33
|
// main.tsx
|
|
35
|
-
import { initKbach } from '@kbach/react';
|
|
36
34
|
import { createRoot } from 'react-dom/client';
|
|
37
35
|
import App from './App';
|
|
38
36
|
import './kbach.css';
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
createRoot(document.getElementById('root')!).render(<App />);
|
|
42
|
-
});
|
|
38
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
43
39
|
```
|
|
44
40
|
|
|
45
41
|
```tsx
|
|
@@ -48,20 +44,208 @@ initKbach().then(() => {
|
|
|
48
44
|
</div>
|
|
49
45
|
```
|
|
50
46
|
|
|
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
|
+
]);
|
|
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:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import './kbach.css';
|
|
128
|
+
export default function App() {
|
|
129
|
+
return <Outlet />;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
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
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
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
|
+
|
|
51
189
|
## Dark mode
|
|
52
190
|
|
|
191
|
+
**Following the OS setting — zero setup.** `dark:` classes already work,
|
|
192
|
+
via plain CSS, with nothing to configure:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<div className="bg-white dark:bg-neutral-12">...</div>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**A manual light/dark toggle — one file, no other setup.** `@media
|
|
199
|
+
(prefers-color-scheme)` (the default above) can't be flipped by a button —
|
|
200
|
+
it only ever reflects the OS. For a real toggle, add one file:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// kbach.config.js — at your project root
|
|
204
|
+
export default { darkMode: 'attribute' };
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
That's it. Don't pass anything to `kbach()`/`'@kbach/react/postcss'` and
|
|
208
|
+
don't call anything at app startup — both sides pick this up on their own
|
|
209
|
+
(the plugin auto-discovers the file; the runtime reads the strategy back
|
|
210
|
+
out of the CSS the plugin already generated). Then just use `useTheme()`:
|
|
211
|
+
|
|
53
212
|
```tsx
|
|
54
213
|
import { useTheme } from '@kbach/react';
|
|
55
214
|
|
|
56
215
|
function ThemeToggle() {
|
|
57
|
-
const { mode,
|
|
216
|
+
const { mode, toggle } = useTheme();
|
|
58
217
|
return <button onClick={toggle}>{mode}</button>;
|
|
59
218
|
}
|
|
60
219
|
```
|
|
61
220
|
|
|
62
|
-
Works anywhere, no provider
|
|
63
|
-
optional, for seeding a startup default.
|
|
64
|
-
|
|
221
|
+
Works anywhere in the tree, no provider required. `<ThemeProvider
|
|
222
|
+
defaultMode="dark">` is optional, only for seeding a startup default.
|
|
223
|
+
Outside React, use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` directly.
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary>Not using the static plugin, or need to set the strategy explicitly?</summary>
|
|
227
|
+
|
|
228
|
+
There's nothing for the runtime to auto-detect if no build step ever
|
|
229
|
+
generates `dark:` CSS (a pure runtime `kb()`-only setup) — call
|
|
230
|
+
`applyKbachConfig()` yourself once, before your first render, instead:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
import { applyKbachConfig } from '@kbach/react';
|
|
234
|
+
|
|
235
|
+
applyKbachConfig({ darkMode: 'attribute' });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
An explicit call like this always wins over whatever the CSS auto-detect
|
|
239
|
+
found, if the two ever disagree. If you do reach for it alongside the
|
|
240
|
+
plugin, import the same `kbach.config.js` rather than retyping the value —
|
|
241
|
+
`import kbachConfig from '../kbach.config.js'; applyKbachConfig(kbachConfig);`.
|
|
242
|
+
|
|
243
|
+
`useTheme()`/`toggleGlobalDarkMode()` still track `isDark` correctly under
|
|
244
|
+
the default `'media'` strategy too, with none of the above — they just
|
|
245
|
+
won't visibly affect `dark:` classes until the runtime knows about
|
|
246
|
+
`'class'`/`'attribute'` one way or another.
|
|
247
|
+
|
|
248
|
+
</details>
|
|
65
249
|
|
|
66
250
|
## Theming
|
|
67
251
|
|
|
@@ -77,16 +261,20 @@ setTheme({
|
|
|
77
261
|
## Dynamic classes
|
|
78
262
|
|
|
79
263
|
Classes that appear literally in your source are resolved at build time.
|
|
80
|
-
For classes assembled from truly dynamic data, use `kb()
|
|
264
|
+
For classes assembled from truly dynamic data, use `kb()` — this is the
|
|
265
|
+
one thing in this package that genuinely calls into the WASM engine at
|
|
266
|
+
runtime, so `await initKbach()` once at startup before the first `kb()`
|
|
267
|
+
call (or before rendering, if you can't guarantee it runs first otherwise):
|
|
81
268
|
|
|
82
269
|
```ts
|
|
83
|
-
import { kb } from '@kbach/react';
|
|
270
|
+
import { initKbach, kb } from '@kbach/react';
|
|
84
271
|
|
|
272
|
+
await initKbach();
|
|
85
273
|
const className = kb(`bg-${userColor}-6`);
|
|
86
274
|
```
|
|
87
275
|
|
|
88
276
|
Prefer a `safelist` entry in the plugin config when the possible values are
|
|
89
|
-
known ahead of time — keeps everything static.
|
|
277
|
+
known ahead of time — keeps everything static, no `initKbach()` needed.
|
|
90
278
|
|
|
91
279
|
## Reading colors as values
|
|
92
280
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultTheme,
|
|
3
|
+
detectDarkModeStrategyFromDom,
|
|
4
|
+
getTheme,
|
|
5
|
+
setTheme
|
|
6
|
+
} from "./chunk-OVKVZA7B.mjs";
|
|
7
|
+
|
|
8
|
+
// src/darkModeStore.ts
|
|
9
|
+
var STORAGE_KEY = "kbach-theme";
|
|
10
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
11
|
+
function systemPrefersDark() {
|
|
12
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
13
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
14
|
+
}
|
|
15
|
+
function storage() {
|
|
16
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
17
|
+
}
|
|
18
|
+
function readPersistedMode() {
|
|
19
|
+
const ls = storage();
|
|
20
|
+
if (!ls) return null;
|
|
21
|
+
try {
|
|
22
|
+
const raw = ls.getItem(STORAGE_KEY);
|
|
23
|
+
return raw === "light" || raw === "dark" || raw === "system" ? raw : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveIsDark(m) {
|
|
29
|
+
return m === "system" ? systemPrefersDark() : m === "dark";
|
|
30
|
+
}
|
|
31
|
+
function applyToDom(dark) {
|
|
32
|
+
if (typeof document === "undefined") return;
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
const strategy = getTheme().darkMode;
|
|
35
|
+
root.classList.toggle("dark", strategy === "class" && dark);
|
|
36
|
+
root.classList.toggle("light", strategy === "class" && !dark);
|
|
37
|
+
if (strategy === "attribute") {
|
|
38
|
+
root.setAttribute("data-theme", dark ? "dark" : "light");
|
|
39
|
+
} else {
|
|
40
|
+
root.removeAttribute("data-theme");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
detectDarkModeStrategyFromDom();
|
|
44
|
+
var persistedMode = readPersistedMode();
|
|
45
|
+
var hadPersistedMode = persistedMode !== null;
|
|
46
|
+
var mode = persistedMode ?? "system";
|
|
47
|
+
var isDark = resolveIsDark(mode);
|
|
48
|
+
applyToDom(isDark);
|
|
49
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
50
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
51
|
+
const onSystemChange = (event) => {
|
|
52
|
+
if (mode !== "system") return;
|
|
53
|
+
isDark = event.matches;
|
|
54
|
+
applyToDom(isDark);
|
|
55
|
+
notify();
|
|
56
|
+
};
|
|
57
|
+
if (mq.addEventListener) mq.addEventListener("change", onSystemChange);
|
|
58
|
+
else mq.addListener(onSystemChange);
|
|
59
|
+
}
|
|
60
|
+
function notify() {
|
|
61
|
+
for (const listener of listeners) listener();
|
|
62
|
+
}
|
|
63
|
+
function resyncDomWithActiveTheme() {
|
|
64
|
+
applyToDom(isDark);
|
|
65
|
+
}
|
|
66
|
+
function getGlobalDarkMode() {
|
|
67
|
+
return isDark;
|
|
68
|
+
}
|
|
69
|
+
function getGlobalThemeMode() {
|
|
70
|
+
return mode;
|
|
71
|
+
}
|
|
72
|
+
function setGlobalThemeMode(nextMode) {
|
|
73
|
+
mode = nextMode;
|
|
74
|
+
isDark = resolveIsDark(nextMode);
|
|
75
|
+
applyToDom(isDark);
|
|
76
|
+
const ls = storage();
|
|
77
|
+
if (ls) {
|
|
78
|
+
try {
|
|
79
|
+
ls.setItem(STORAGE_KEY, nextMode);
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
notify();
|
|
84
|
+
}
|
|
85
|
+
function toggleGlobalDarkMode() {
|
|
86
|
+
setGlobalThemeMode(isDark ? "light" : "dark");
|
|
87
|
+
}
|
|
88
|
+
function subscribeGlobalDarkMode(listener) {
|
|
89
|
+
listeners.add(listener);
|
|
90
|
+
return () => listeners.delete(listener);
|
|
91
|
+
}
|
|
92
|
+
function seedDefaultMode(fallback) {
|
|
93
|
+
if (hadPersistedMode || mode === fallback) return;
|
|
94
|
+
mode = fallback;
|
|
95
|
+
isDark = resolveIsDark(fallback);
|
|
96
|
+
applyToDom(isDark);
|
|
97
|
+
notify();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/colorVariables.ts
|
|
101
|
+
function parseHexRgb(hex) {
|
|
102
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
103
|
+
if (!m) return null;
|
|
104
|
+
let h = m[1];
|
|
105
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
106
|
+
const n = parseInt(h, 16);
|
|
107
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
108
|
+
}
|
|
109
|
+
function wrapDarkDecls(strategy, decls) {
|
|
110
|
+
switch (strategy) {
|
|
111
|
+
case "class":
|
|
112
|
+
return `.dark { ${decls} }`;
|
|
113
|
+
case "media":
|
|
114
|
+
return `@media (prefers-color-scheme: dark) { :root { ${decls} } }`;
|
|
115
|
+
case "attribute":
|
|
116
|
+
default:
|
|
117
|
+
return `[data-theme="dark"] { ${decls} }`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
var STYLE_ID = "kbach-colors";
|
|
121
|
+
var injectedForTheme = null;
|
|
122
|
+
function ensureColorVariablesInjected(theme) {
|
|
123
|
+
if (typeof document === "undefined") return;
|
|
124
|
+
if (injectedForTheme === theme) return;
|
|
125
|
+
injectedForTheme = theme;
|
|
126
|
+
const lightDecls = [];
|
|
127
|
+
const darkDecls = [];
|
|
128
|
+
for (const [name, value] of Object.entries(theme.colors)) {
|
|
129
|
+
if (typeof value === "string") continue;
|
|
130
|
+
const light = parseHexRgb(value.light);
|
|
131
|
+
const dark = parseHexRgb(value.dark);
|
|
132
|
+
if (!light || !dark) continue;
|
|
133
|
+
lightDecls.push(`--kb-color-${name}: ${light.join(",")};`);
|
|
134
|
+
darkDecls.push(`--kb-color-${name}: ${dark.join(",")};`);
|
|
135
|
+
}
|
|
136
|
+
let el = document.getElementById(STYLE_ID);
|
|
137
|
+
if (lightDecls.length === 0) {
|
|
138
|
+
el?.remove();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!el) {
|
|
142
|
+
el = document.createElement("style");
|
|
143
|
+
el.id = STYLE_ID;
|
|
144
|
+
document.head.insertBefore(el, document.head.firstChild);
|
|
145
|
+
}
|
|
146
|
+
el.textContent = `:root { ${lightDecls.join(" ")} }
|
|
147
|
+
${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/config.ts
|
|
151
|
+
function applyOpacityToHex(hex, opacityText) {
|
|
152
|
+
const rgb = parseHexRgb(hex);
|
|
153
|
+
if (!rgb) return hex;
|
|
154
|
+
const a = Number(opacityText) / 100;
|
|
155
|
+
const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
|
|
156
|
+
return `rgba(${rgb.join(",")},${clamped})`;
|
|
157
|
+
}
|
|
158
|
+
function resolveColorEntry(value, lookup) {
|
|
159
|
+
if (typeof value !== "string") return value;
|
|
160
|
+
const slash = value.indexOf("/");
|
|
161
|
+
const name = slash === -1 ? value : value.slice(0, slash);
|
|
162
|
+
const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
|
|
163
|
+
const base = lookup(name);
|
|
164
|
+
if (base === void 0) return value;
|
|
165
|
+
if (opacity === void 0) return base;
|
|
166
|
+
return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
|
|
167
|
+
}
|
|
168
|
+
function resolveColors(extend, existing) {
|
|
169
|
+
const { dark: darkExtend, ...lightExtend } = extend;
|
|
170
|
+
const resolved = {};
|
|
171
|
+
const lookup = (name) => resolved[name] ?? existing[name];
|
|
172
|
+
for (const [name, value] of Object.entries(lightExtend)) {
|
|
173
|
+
if (typeof value === "object" && value !== null && !("light" in value) && !("dark" in value)) continue;
|
|
174
|
+
resolved[name] = resolveColorEntry(value, lookup);
|
|
175
|
+
}
|
|
176
|
+
if (darkExtend) {
|
|
177
|
+
const resolvedDark = {};
|
|
178
|
+
const lookupDark = (name) => {
|
|
179
|
+
const found = resolvedDark[name] ?? existing[name];
|
|
180
|
+
return typeof found === "string" ? found : void 0;
|
|
181
|
+
};
|
|
182
|
+
for (const [name, value] of Object.entries(darkExtend)) {
|
|
183
|
+
const entry = resolveColorEntry(value, lookupDark);
|
|
184
|
+
resolvedDark[name] = typeof entry === "string" ? entry : entry.dark;
|
|
185
|
+
}
|
|
186
|
+
for (const [name, darkValue] of Object.entries(resolvedDark)) {
|
|
187
|
+
const lightEntry = resolved[name];
|
|
188
|
+
if (lightEntry === void 0 || typeof lightEntry !== "string") continue;
|
|
189
|
+
resolved[name] = { light: lightEntry, dark: darkValue };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return resolved;
|
|
193
|
+
}
|
|
194
|
+
function resolveKbachConfig(config) {
|
|
195
|
+
let theme = { ...defaultTheme, ...config.theme };
|
|
196
|
+
if (config.extend?.colors) {
|
|
197
|
+
theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
|
|
198
|
+
}
|
|
199
|
+
if (config.extend?.spacing) {
|
|
200
|
+
theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
|
|
201
|
+
}
|
|
202
|
+
if (config.extend?.screens) {
|
|
203
|
+
theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
|
|
204
|
+
}
|
|
205
|
+
if (config.extend?.fontFamily) {
|
|
206
|
+
theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
|
|
207
|
+
}
|
|
208
|
+
if (config.extend?.container) {
|
|
209
|
+
theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
|
|
210
|
+
}
|
|
211
|
+
if (config.darkMode) {
|
|
212
|
+
theme = { ...theme, darkMode: config.darkMode };
|
|
213
|
+
}
|
|
214
|
+
return theme;
|
|
215
|
+
}
|
|
216
|
+
function applyKbachConfig(config) {
|
|
217
|
+
const theme = resolveKbachConfig(config);
|
|
218
|
+
setTheme(theme);
|
|
219
|
+
resyncDomWithActiveTheme();
|
|
220
|
+
return theme;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
parseHexRgb,
|
|
225
|
+
ensureColorVariablesInjected,
|
|
226
|
+
getGlobalDarkMode,
|
|
227
|
+
getGlobalThemeMode,
|
|
228
|
+
setGlobalThemeMode,
|
|
229
|
+
toggleGlobalDarkMode,
|
|
230
|
+
subscribeGlobalDarkMode,
|
|
231
|
+
seedDefaultMode,
|
|
232
|
+
resolveKbachConfig,
|
|
233
|
+
applyKbachConfig
|
|
234
|
+
};
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
BASE_RESET,
|
|
3
3
|
RESET_STYLE_ID,
|
|
4
4
|
getThemeJson
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OVKVZA7B.mjs";
|
|
6
6
|
|
|
7
7
|
// src/wasmLoader.ts
|
|
8
8
|
import init, { generate_css as wasmGenerateCss } from "@kbach/core-engine";
|
|
@@ -22,7 +22,7 @@ function isKbachReady() {
|
|
|
22
22
|
function generateCss(classString, themeJson) {
|
|
23
23
|
if (!ready) {
|
|
24
24
|
throw new Error(
|
|
25
|
-
"[
|
|
25
|
+
"[Kbach] generateCss() was called before initKbach() resolved. Call `await initKbach()` once at startup (e.g. before rendering your app) before using kb()."
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
return wasmGenerateCss(classString, themeJson);
|
|
@@ -79,9 +79,23 @@ function ensureResetInjected() {
|
|
|
79
79
|
el.textContent = BASE_RESET;
|
|
80
80
|
document.head.insertBefore(el, document.head.firstChild);
|
|
81
81
|
}
|
|
82
|
+
var KB_CACHE_MAX = 500;
|
|
83
|
+
var kbCache = /* @__PURE__ */ new Map();
|
|
84
|
+
var cachedForThemeJson = null;
|
|
82
85
|
function kb(classString) {
|
|
83
|
-
const
|
|
84
|
-
|
|
86
|
+
const themeJson = getThemeJson();
|
|
87
|
+
if (themeJson !== cachedForThemeJson) {
|
|
88
|
+
kbCache.clear();
|
|
89
|
+
cachedForThemeJson = themeJson;
|
|
90
|
+
}
|
|
91
|
+
let result = kbCache.get(classString);
|
|
92
|
+
if (result === void 0) {
|
|
93
|
+
const json = generateCss(classString, themeJson);
|
|
94
|
+
result = JSON.parse(json);
|
|
95
|
+
if (kbCache.size >= KB_CACHE_MAX) kbCache.clear();
|
|
96
|
+
kbCache.set(classString, result);
|
|
97
|
+
}
|
|
98
|
+
const { className, rules } = result;
|
|
85
99
|
if (!runtimeCSSDisabled) {
|
|
86
100
|
ensureResetInjected();
|
|
87
101
|
for (const { rule, order } of rules) {
|