@kbach/react 1.0.0-beta.0 → 1.0.0-beta.2
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 +45 -68
- package/dist/{chunk-AFY36PRX.mjs → chunk-N5HCQIBB.mjs} +57 -10
- package/dist/chunk-NKR3PSWO.mjs +115 -0
- package/dist/{chunk-VBPQAIZL.mjs → chunk-PAJ6KHDB.mjs} +1 -1
- package/dist/{chunk-G3RJJNSY.mjs → chunk-ZQJKL2MV.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 +247 -43
- package/dist/index.mjs +89 -32
- package/dist/jsx-dev-runtime.js +57 -10
- package/dist/jsx-dev-runtime.mjs +3 -3
- package/dist/jsx-runtime.js +57 -10
- package/dist/jsx-runtime.mjs +3 -3
- package/dist/vite-plugin.d.mts +15 -1
- package/dist/vite-plugin.d.ts +15 -1
- package/dist/vite-plugin.js +132 -12
- package/dist/vite-plugin.mjs +21 -4
- package/package.json +2 -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,9 +24,6 @@ export default defineConfig({
|
|
|
48
24
|
});
|
|
49
25
|
```
|
|
50
26
|
|
|
51
|
-
Create `src/kbach.css` with the markers the plugin writes between, and
|
|
52
|
-
import it once:
|
|
53
|
-
|
|
54
27
|
```css
|
|
55
28
|
/* src/kbach.css */
|
|
56
29
|
/* kbach:start */
|
|
@@ -69,8 +42,6 @@ initKbach().then(() => {
|
|
|
69
42
|
});
|
|
70
43
|
```
|
|
71
44
|
|
|
72
|
-
Then just use utility classes:
|
|
73
|
-
|
|
74
45
|
```tsx
|
|
75
46
|
<div className="flex items-center gap-4 bg-blue-6 text-white p-4 rounded-lg">
|
|
76
47
|
Hello Kbach
|
|
@@ -79,40 +50,20 @@ Then just use utility classes:
|
|
|
79
50
|
|
|
80
51
|
## Dark mode
|
|
81
52
|
|
|
82
|
-
Works standalone, with zero provider:
|
|
83
|
-
|
|
84
53
|
```tsx
|
|
85
|
-
import {
|
|
54
|
+
import { useTheme } from '@kbach/react';
|
|
86
55
|
|
|
87
56
|
function ThemeToggle() {
|
|
88
|
-
const isDark = useGlobalDarkMode();
|
|
89
|
-
return <button onClick={toggleGlobalDarkMode}>{isDark ? 'Dark' : 'Light'}</button>;
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Or opt into the optional `<ThemeProvider>` / `useTheme()` pair — both read
|
|
94
|
-
and write the exact same global store, so they always stay in sync with the
|
|
95
|
-
standalone functions above:
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
import { ThemeProvider, useTheme } from '@kbach/react';
|
|
99
|
-
|
|
100
|
-
function Toggle() {
|
|
101
57
|
const { mode, isDark, setMode, toggle } = useTheme();
|
|
102
58
|
return <button onClick={toggle}>{mode}</button>;
|
|
103
59
|
}
|
|
104
|
-
|
|
105
|
-
<ThemeProvider defaultMode="system">
|
|
106
|
-
<Toggle />
|
|
107
|
-
</ThemeProvider>;
|
|
108
60
|
```
|
|
109
61
|
|
|
110
|
-
|
|
62
|
+
Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
|
|
63
|
+
optional, for seeding a startup default. Outside React (plain functions,
|
|
64
|
+
event handlers), use `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
|
|
111
65
|
|
|
112
|
-
|
|
113
|
-
strategy via `setTheme()` (call once, before your first render) — the same
|
|
114
|
-
theme object is used by both the runtime and the Vite plugin, so the two
|
|
115
|
-
never fall out of sync:
|
|
66
|
+
## Theming
|
|
116
67
|
|
|
117
68
|
```ts
|
|
118
69
|
import { setTheme, defaultTheme } from '@kbach/react';
|
|
@@ -123,12 +74,10 @@ setTheme({
|
|
|
123
74
|
});
|
|
124
75
|
```
|
|
125
76
|
|
|
126
|
-
##
|
|
77
|
+
## Dynamic classes
|
|
127
78
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
with no static occurrence anywhere in your source, call `kb()` — it resolves
|
|
131
|
-
and injects the CSS at runtime and returns the class name unchanged:
|
|
79
|
+
Classes that appear literally in your source are resolved at build time.
|
|
80
|
+
For classes assembled from truly dynamic data, use `kb()`:
|
|
132
81
|
|
|
133
82
|
```ts
|
|
134
83
|
import { kb } from '@kbach/react';
|
|
@@ -136,9 +85,37 @@ import { kb } from '@kbach/react';
|
|
|
136
85
|
const className = kb(`bg-${userColor}-6`);
|
|
137
86
|
```
|
|
138
87
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
88
|
+
Prefer a `safelist` entry in the plugin config when the possible values are
|
|
89
|
+
known ahead of time — keeps everything static.
|
|
90
|
+
|
|
91
|
+
## Reading colors as values
|
|
92
|
+
|
|
93
|
+
For anywhere you need a real color, not a `className` (an SVG `fill`, a
|
|
94
|
+
chart library prop):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { useColors } from '@kbach/react';
|
|
98
|
+
|
|
99
|
+
const colors = useColors();
|
|
100
|
+
colors.blue[6]; // '#2563eb' — static, same value in both modes
|
|
101
|
+
colors.blue['6/50']; // same, at 50% opacity
|
|
102
|
+
colors.surface; // 'rgb(var(--kb-color-surface))' if defined as light/dark
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Any color — a shade-family entry like `blue-6` or a standalone name like
|
|
106
|
+
`surface` — can be defined in the theme as a plain string (static) or a
|
|
107
|
+
`{ light, dark }` pair, fully manual:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
setTheme({
|
|
111
|
+
...defaultTheme,
|
|
112
|
+
colors: { ...defaultTheme.colors, surface: { light: '#f9fafb', dark: '#111827' } },
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A plain color always resolves to its literal value. A `{ light, dark }`
|
|
117
|
+
color resolves to a live CSS variable instead of a fixed hex — it stays
|
|
118
|
+
correct across theme changes with zero re-render.
|
|
142
119
|
|
|
143
120
|
## License
|
|
144
121
|
|
|
@@ -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
|
-
"
|
|
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,21 @@ var defaultTheme = {
|
|
|
304
337
|
xl: 1280,
|
|
305
338
|
"2xl": 1536
|
|
306
339
|
},
|
|
307
|
-
|
|
340
|
+
// Byte-for-byte the same three stacks
|
|
341
|
+
// packages/core-engine/src/resolvers/typography.rs's `font_family_value`
|
|
342
|
+
// falls back to — kept here too (rather than leaving this empty and
|
|
343
|
+
// relying purely on the Rust-side fallback) so `defaultTheme.fontFamily.sans`
|
|
344
|
+
// is a real, readable value for anything that wants to extend rather
|
|
345
|
+
// than replace it (see `KbachConfig.extend.fontFamily` in config.ts).
|
|
346
|
+
fontFamily: {
|
|
347
|
+
sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
|
348
|
+
serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
|
349
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
|
350
|
+
},
|
|
351
|
+
darkMode: "attribute",
|
|
352
|
+
// Empty by default, matching real Tailwind's own defaults (no forced
|
|
353
|
+
// centering/padding) — see `ContainerConfig`'s own doc comment.
|
|
354
|
+
container: {}
|
|
308
355
|
};
|
|
309
356
|
var activeTheme = defaultTheme;
|
|
310
357
|
var activeThemeJson = JSON.stringify(defaultTheme);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultTheme,
|
|
3
|
+
setTheme
|
|
4
|
+
} from "./chunk-N5HCQIBB.mjs";
|
|
5
|
+
|
|
6
|
+
// src/colorVariables.ts
|
|
7
|
+
function parseHexRgb(hex) {
|
|
8
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
9
|
+
if (!m) return null;
|
|
10
|
+
let h = m[1];
|
|
11
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
12
|
+
const n = parseInt(h, 16);
|
|
13
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
14
|
+
}
|
|
15
|
+
function wrapDarkDecls(strategy, decls) {
|
|
16
|
+
switch (strategy) {
|
|
17
|
+
case "class":
|
|
18
|
+
return `.dark { ${decls} }`;
|
|
19
|
+
case "media":
|
|
20
|
+
return `@media (prefers-color-scheme: dark) { :root { ${decls} } }`;
|
|
21
|
+
case "attribute":
|
|
22
|
+
default:
|
|
23
|
+
return `[data-theme="dark"] { ${decls} }`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var STYLE_ID = "kbach-colors";
|
|
27
|
+
var injectedForTheme = null;
|
|
28
|
+
function ensureColorVariablesInjected(theme) {
|
|
29
|
+
if (typeof document === "undefined") return;
|
|
30
|
+
if (injectedForTheme === theme) return;
|
|
31
|
+
injectedForTheme = theme;
|
|
32
|
+
const lightDecls = [];
|
|
33
|
+
const darkDecls = [];
|
|
34
|
+
for (const [name, value] of Object.entries(theme.colors)) {
|
|
35
|
+
if (typeof value === "string") continue;
|
|
36
|
+
const light = parseHexRgb(value.light);
|
|
37
|
+
const dark = parseHexRgb(value.dark);
|
|
38
|
+
if (!light || !dark) continue;
|
|
39
|
+
lightDecls.push(`--kb-color-${name}: ${light.join(",")};`);
|
|
40
|
+
darkDecls.push(`--kb-color-${name}: ${dark.join(",")};`);
|
|
41
|
+
}
|
|
42
|
+
let el = document.getElementById(STYLE_ID);
|
|
43
|
+
if (lightDecls.length === 0) {
|
|
44
|
+
el?.remove();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!el) {
|
|
48
|
+
el = document.createElement("style");
|
|
49
|
+
el.id = STYLE_ID;
|
|
50
|
+
document.head.insertBefore(el, document.head.firstChild);
|
|
51
|
+
}
|
|
52
|
+
el.textContent = `:root { ${lightDecls.join(" ")} }
|
|
53
|
+
${wrapDarkDecls(theme.darkMode, darkDecls.join(" "))}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/config.ts
|
|
57
|
+
function applyOpacityToHex(hex, opacityText) {
|
|
58
|
+
const rgb = parseHexRgb(hex);
|
|
59
|
+
if (!rgb) return hex;
|
|
60
|
+
const a = Number(opacityText) / 100;
|
|
61
|
+
const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
|
|
62
|
+
return `rgba(${rgb.join(",")},${clamped})`;
|
|
63
|
+
}
|
|
64
|
+
function resolveColorEntry(value, lookup) {
|
|
65
|
+
if (typeof value !== "string") return value;
|
|
66
|
+
const slash = value.indexOf("/");
|
|
67
|
+
const name = slash === -1 ? value : value.slice(0, slash);
|
|
68
|
+
const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
|
|
69
|
+
const base = lookup(name);
|
|
70
|
+
if (base === void 0) return value;
|
|
71
|
+
if (opacity === void 0) return base;
|
|
72
|
+
return typeof base === "string" ? applyOpacityToHex(base, opacity) : { light: applyOpacityToHex(base.light, opacity), dark: applyOpacityToHex(base.dark, opacity) };
|
|
73
|
+
}
|
|
74
|
+
function resolveColors(extend, existing) {
|
|
75
|
+
const resolved = {};
|
|
76
|
+
const lookup = (name) => resolved[name] ?? existing[name];
|
|
77
|
+
for (const [name, value] of Object.entries(extend)) {
|
|
78
|
+
resolved[name] = resolveColorEntry(value, lookup);
|
|
79
|
+
}
|
|
80
|
+
return resolved;
|
|
81
|
+
}
|
|
82
|
+
function resolveKbachConfig(config) {
|
|
83
|
+
let theme = { ...defaultTheme, ...config.theme };
|
|
84
|
+
if (config.extend?.colors) {
|
|
85
|
+
theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
|
|
86
|
+
}
|
|
87
|
+
if (config.extend?.spacing) {
|
|
88
|
+
theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
|
|
89
|
+
}
|
|
90
|
+
if (config.extend?.screens) {
|
|
91
|
+
theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
|
|
92
|
+
}
|
|
93
|
+
if (config.extend?.fontFamily) {
|
|
94
|
+
theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
|
|
95
|
+
}
|
|
96
|
+
if (config.extend?.container) {
|
|
97
|
+
theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
|
|
98
|
+
}
|
|
99
|
+
if (config.darkMode) {
|
|
100
|
+
theme = { ...theme, darkMode: config.darkMode };
|
|
101
|
+
}
|
|
102
|
+
return theme;
|
|
103
|
+
}
|
|
104
|
+
function applyKbachConfig(config) {
|
|
105
|
+
const theme = resolveKbachConfig(config);
|
|
106
|
+
setTheme(theme);
|
|
107
|
+
return theme;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
parseHexRgb,
|
|
112
|
+
ensureColorVariablesInjected,
|
|
113
|
+
resolveKbachConfig,
|
|
114
|
+
applyKbachConfig
|
|
115
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** A plain hex color, or a mode-aware pair auto-expanded into a light/dark class pair by the Rust engine. */
|
|
2
|
+
type ColorEntry = string | {
|
|
3
|
+
light: string;
|
|
4
|
+
dark: string;
|
|
5
|
+
};
|
|
6
|
+
type DarkModeStrategy = 'attribute' | 'class' | 'media';
|
|
7
|
+
/**
|
|
8
|
+
* Real Tailwind's `theme.container` config, mirroring
|
|
9
|
+
* `packages/core-engine/src/theme.rs`'s `ContainerConfig` exactly (same
|
|
10
|
+
* field names, `camelCase` on both sides via serde). `padding` is one
|
|
11
|
+
* uniform value applied at every breakpoint (including below the smallest
|
|
12
|
+
* one) rather than real Tailwind's optional per-breakpoint object form —
|
|
13
|
+
* the common case in practice, and simpler to reason about; a
|
|
14
|
+
* per-breakpoint padding map is a natural follow-up if ever needed.
|
|
15
|
+
*/
|
|
16
|
+
interface ContainerConfig {
|
|
17
|
+
center?: boolean;
|
|
18
|
+
padding?: string;
|
|
19
|
+
}
|
|
20
|
+
interface ThemeConfig {
|
|
21
|
+
/** e.g. "blue-6" -> "#2563eb", or "surface" -> { light, dark } */
|
|
22
|
+
colors: Record<string, ColorEntry>;
|
|
23
|
+
/** e.g. "4" -> 16 (px) */
|
|
24
|
+
spacing: Record<string, number>;
|
|
25
|
+
/** e.g. "sm" -> 640 (min-width px) */
|
|
26
|
+
screens: Record<string, number>;
|
|
27
|
+
/**
|
|
28
|
+
* e.g. "sans" -> "ui-sans-serif, system-ui, ...". Optional — the Rust
|
|
29
|
+
* engine's own `font_family_value` already carries a hardcoded fallback
|
|
30
|
+
* for "sans"/"serif"/"mono" (see its own doc comment) for when a theme
|
|
31
|
+
* omits this field (or a name within it) entirely, matching this field's
|
|
32
|
+
* `#[serde(default)]` on the Rust side. `defaultTheme` below still
|
|
33
|
+
* populates all three explicitly, matching the Rust fallback strings
|
|
34
|
+
* byte-for-byte, so a caller reading `defaultTheme.fontFamily.sans`
|
|
35
|
+
* (e.g. to extend rather than replace it) gets the real value rather
|
|
36
|
+
* than needing to already know the Rust-side fallback text.
|
|
37
|
+
*/
|
|
38
|
+
fontFamily?: Record<string, string>;
|
|
39
|
+
darkMode: DarkModeStrategy;
|
|
40
|
+
/** Optional, matching `ContainerConfig`'s own `#[serde(default)]` on the Rust side — an omitted theme.container behaves exactly like `{}`. */
|
|
41
|
+
container?: ContainerConfig;
|
|
42
|
+
}
|
|
43
|
+
declare const defaultTheme: ThemeConfig;
|
|
44
|
+
declare function setTheme(theme: ThemeConfig): void;
|
|
45
|
+
declare function getTheme(): ThemeConfig;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A `kbach.config.js`-style theme customization, in one plain object — same
|
|
49
|
+
* `theme`/`extend`/`darkMode` shape real Tailwind's own config file uses,
|
|
50
|
+
* simplified from old-kbach's version (no plugins, no color alias chains
|
|
51
|
+
* with cycle detection, no live-update-listener store): a config object
|
|
52
|
+
* goes in, a resolved `ThemeConfig` comes out, via one PURE function
|
|
53
|
+
* (`resolveKbachConfig`) — same input always produces the same output, so
|
|
54
|
+
* nothing needs to stay "in sync" the way old-kbach's Babel-injected config
|
|
55
|
+
* singleton did. Call it identically from a Node.js build script
|
|
56
|
+
* (`vite.config.ts`) and from browser/app code (`applyKbachConfig`) and
|
|
57
|
+
* both get the exact same theme, no synchronization mechanism required.
|
|
58
|
+
*/
|
|
59
|
+
interface KbachConfig {
|
|
60
|
+
/** Applied on top of `defaultTheme`; `undefined` means "keep the default". */
|
|
61
|
+
darkMode?: DarkModeStrategy;
|
|
62
|
+
/**
|
|
63
|
+
* REPLACES the named section of `defaultTheme` entirely (e.g. supplying
|
|
64
|
+
* `theme.colors` drops the rest of the default palette) — use `extend`
|
|
65
|
+
* instead to add to the defaults rather than replace them. `container`
|
|
66
|
+
* isn't here (only under `extend` below) — it has no "defaults to
|
|
67
|
+
* replace" the way colors/spacing/screens/fontFamily do, since real
|
|
68
|
+
* Tailwind's own container config is inherently additive (center/padding
|
|
69
|
+
* on top of the always-present breakpoint ladder), so there's no
|
|
70
|
+
* meaningful distinction between "replace" and "extend" for it.
|
|
71
|
+
*/
|
|
72
|
+
theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
|
|
73
|
+
/** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
|
|
74
|
+
extend?: {
|
|
75
|
+
/**
|
|
76
|
+
* A value is either a literal color (a hex string, or `{ light, dark }`
|
|
77
|
+
* — same mode-aware shape `useColors()`/`useTheme()` already support)
|
|
78
|
+
* or a REFERENCE to another color name, optionally with an opacity
|
|
79
|
+
* suffix. Two things can be referenced:
|
|
80
|
+
* - A BUILT-IN palette color, always available regardless of ordering
|
|
81
|
+
* (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`).
|
|
82
|
+
* - A custom color defined EARLIER in this same object
|
|
83
|
+
* (`{ brand: '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft`
|
|
84
|
+
* resolves to `brand`'s own value at 30% opacity, computed once here
|
|
85
|
+
* rather than needing a `colors.alpha()` call at every use site).
|
|
86
|
+
* A string that doesn't match any known color name (built-in or
|
|
87
|
+
* already-defined-above) is treated as a literal CSS value instead
|
|
88
|
+
* (`accent: 'red'` — REAL CSS `red`, since there's no color named "red"
|
|
89
|
+
* to alias), never an error either way.
|
|
90
|
+
*/
|
|
91
|
+
colors?: Record<string, ColorEntry | string>;
|
|
92
|
+
spacing?: Record<string, number>;
|
|
93
|
+
screens?: Record<string, number>;
|
|
94
|
+
/** Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` — adds to (or overrides one of) `defaultTheme.fontFamily`'s three names. */
|
|
95
|
+
fontFamily?: Record<string, string>;
|
|
96
|
+
/** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
|
|
97
|
+
container?: ContainerConfig;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
|
|
101
|
+
declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
|
|
102
|
+
/** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. Returns the resolved theme (e.g. to also hand to the Vite plugin's own `theme` option). */
|
|
103
|
+
declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
|
|
104
|
+
|
|
105
|
+
export { type ColorEntry as C, type DarkModeStrategy as D, type KbachConfig as K, type ThemeConfig as T, type ContainerConfig as a, applyKbachConfig as b, defaultTheme as d, getTheme as g, resolveKbachConfig as r, setTheme as s };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** A plain hex color, or a mode-aware pair auto-expanded into a light/dark class pair by the Rust engine. */
|
|
2
|
+
type ColorEntry = string | {
|
|
3
|
+
light: string;
|
|
4
|
+
dark: string;
|
|
5
|
+
};
|
|
6
|
+
type DarkModeStrategy = 'attribute' | 'class' | 'media';
|
|
7
|
+
/**
|
|
8
|
+
* Real Tailwind's `theme.container` config, mirroring
|
|
9
|
+
* `packages/core-engine/src/theme.rs`'s `ContainerConfig` exactly (same
|
|
10
|
+
* field names, `camelCase` on both sides via serde). `padding` is one
|
|
11
|
+
* uniform value applied at every breakpoint (including below the smallest
|
|
12
|
+
* one) rather than real Tailwind's optional per-breakpoint object form —
|
|
13
|
+
* the common case in practice, and simpler to reason about; a
|
|
14
|
+
* per-breakpoint padding map is a natural follow-up if ever needed.
|
|
15
|
+
*/
|
|
16
|
+
interface ContainerConfig {
|
|
17
|
+
center?: boolean;
|
|
18
|
+
padding?: string;
|
|
19
|
+
}
|
|
20
|
+
interface ThemeConfig {
|
|
21
|
+
/** e.g. "blue-6" -> "#2563eb", or "surface" -> { light, dark } */
|
|
22
|
+
colors: Record<string, ColorEntry>;
|
|
23
|
+
/** e.g. "4" -> 16 (px) */
|
|
24
|
+
spacing: Record<string, number>;
|
|
25
|
+
/** e.g. "sm" -> 640 (min-width px) */
|
|
26
|
+
screens: Record<string, number>;
|
|
27
|
+
/**
|
|
28
|
+
* e.g. "sans" -> "ui-sans-serif, system-ui, ...". Optional — the Rust
|
|
29
|
+
* engine's own `font_family_value` already carries a hardcoded fallback
|
|
30
|
+
* for "sans"/"serif"/"mono" (see its own doc comment) for when a theme
|
|
31
|
+
* omits this field (or a name within it) entirely, matching this field's
|
|
32
|
+
* `#[serde(default)]` on the Rust side. `defaultTheme` below still
|
|
33
|
+
* populates all three explicitly, matching the Rust fallback strings
|
|
34
|
+
* byte-for-byte, so a caller reading `defaultTheme.fontFamily.sans`
|
|
35
|
+
* (e.g. to extend rather than replace it) gets the real value rather
|
|
36
|
+
* than needing to already know the Rust-side fallback text.
|
|
37
|
+
*/
|
|
38
|
+
fontFamily?: Record<string, string>;
|
|
39
|
+
darkMode: DarkModeStrategy;
|
|
40
|
+
/** Optional, matching `ContainerConfig`'s own `#[serde(default)]` on the Rust side — an omitted theme.container behaves exactly like `{}`. */
|
|
41
|
+
container?: ContainerConfig;
|
|
42
|
+
}
|
|
43
|
+
declare const defaultTheme: ThemeConfig;
|
|
44
|
+
declare function setTheme(theme: ThemeConfig): void;
|
|
45
|
+
declare function getTheme(): ThemeConfig;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A `kbach.config.js`-style theme customization, in one plain object — same
|
|
49
|
+
* `theme`/`extend`/`darkMode` shape real Tailwind's own config file uses,
|
|
50
|
+
* simplified from old-kbach's version (no plugins, no color alias chains
|
|
51
|
+
* with cycle detection, no live-update-listener store): a config object
|
|
52
|
+
* goes in, a resolved `ThemeConfig` comes out, via one PURE function
|
|
53
|
+
* (`resolveKbachConfig`) — same input always produces the same output, so
|
|
54
|
+
* nothing needs to stay "in sync" the way old-kbach's Babel-injected config
|
|
55
|
+
* singleton did. Call it identically from a Node.js build script
|
|
56
|
+
* (`vite.config.ts`) and from browser/app code (`applyKbachConfig`) and
|
|
57
|
+
* both get the exact same theme, no synchronization mechanism required.
|
|
58
|
+
*/
|
|
59
|
+
interface KbachConfig {
|
|
60
|
+
/** Applied on top of `defaultTheme`; `undefined` means "keep the default". */
|
|
61
|
+
darkMode?: DarkModeStrategy;
|
|
62
|
+
/**
|
|
63
|
+
* REPLACES the named section of `defaultTheme` entirely (e.g. supplying
|
|
64
|
+
* `theme.colors` drops the rest of the default palette) — use `extend`
|
|
65
|
+
* instead to add to the defaults rather than replace them. `container`
|
|
66
|
+
* isn't here (only under `extend` below) — it has no "defaults to
|
|
67
|
+
* replace" the way colors/spacing/screens/fontFamily do, since real
|
|
68
|
+
* Tailwind's own container config is inherently additive (center/padding
|
|
69
|
+
* on top of the always-present breakpoint ladder), so there's no
|
|
70
|
+
* meaningful distinction between "replace" and "extend" for it.
|
|
71
|
+
*/
|
|
72
|
+
theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
|
|
73
|
+
/** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
|
|
74
|
+
extend?: {
|
|
75
|
+
/**
|
|
76
|
+
* A value is either a literal color (a hex string, or `{ light, dark }`
|
|
77
|
+
* — same mode-aware shape `useColors()`/`useTheme()` already support)
|
|
78
|
+
* or a REFERENCE to another color name, optionally with an opacity
|
|
79
|
+
* suffix. Two things can be referenced:
|
|
80
|
+
* - A BUILT-IN palette color, always available regardless of ordering
|
|
81
|
+
* (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`).
|
|
82
|
+
* - A custom color defined EARLIER in this same object
|
|
83
|
+
* (`{ brand: '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft`
|
|
84
|
+
* resolves to `brand`'s own value at 30% opacity, computed once here
|
|
85
|
+
* rather than needing a `colors.alpha()` call at every use site).
|
|
86
|
+
* A string that doesn't match any known color name (built-in or
|
|
87
|
+
* already-defined-above) is treated as a literal CSS value instead
|
|
88
|
+
* (`accent: 'red'` — REAL CSS `red`, since there's no color named "red"
|
|
89
|
+
* to alias), never an error either way.
|
|
90
|
+
*/
|
|
91
|
+
colors?: Record<string, ColorEntry | string>;
|
|
92
|
+
spacing?: Record<string, number>;
|
|
93
|
+
screens?: Record<string, number>;
|
|
94
|
+
/** Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` — adds to (or overrides one of) `defaultTheme.fontFamily`'s three names. */
|
|
95
|
+
fontFamily?: Record<string, string>;
|
|
96
|
+
/** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
|
|
97
|
+
container?: ContainerConfig;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
|
|
101
|
+
declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
|
|
102
|
+
/** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. Returns the resolved theme (e.g. to also hand to the Vite plugin's own `theme` option). */
|
|
103
|
+
declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
|
|
104
|
+
|
|
105
|
+
export { type ColorEntry as C, type DarkModeStrategy as D, type KbachConfig as K, type ThemeConfig as T, type ContainerConfig as a, applyKbachConfig as b, defaultTheme as d, getTheme as g, resolveKbachConfig as r, setTheme as s };
|