@kbach/ui 0.1.0-beta.0

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.
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import {
3
+ Fragment,
4
+ _invalidateDefaultFontCache,
5
+ jsx,
6
+ jsxs
7
+ } from "./chunk-UE54W6ZG.mjs";
8
+ import {
9
+ registerWebElement
10
+ } from "./chunk-BPCFICND.mjs";
11
+ export {
12
+ Fragment,
13
+ _invalidateDefaultFontCache,
14
+ jsx,
15
+ jsxs,
16
+ registerWebElement
17
+ };
@@ -0,0 +1,314 @@
1
+ import React, { ReactNode } from 'react';
2
+
3
+ /**
4
+ * Unified modifier registry — single source of truth for ALL modifier behavior.
5
+ *
6
+ * Adding a new modifier requires editing ONLY this file:
7
+ * 1. Add an entry to BUILTIN_MODIFIERS with its CSS and JS behavior.
8
+ * 2. Done — parser, resolver, CSS generator, and JSX runtime all derive
9
+ * their behavior from this data automatically.
10
+ *
11
+ * Plugin authors can register custom modifiers via registerModifier().
12
+ */
13
+ interface ModifierDef {
14
+ /**
15
+ * Cascade priority for CSS rule ORDER — NOT specificity. Two rules that
16
+ * differ only by modifier (e.g. `.hover\:bg-blue-6:hover` and
17
+ * `.focus\:bg-red-6:focus`) have equal CSS specificity, so when both
18
+ * conditions are true at once (hovering AND focused), the winner is
19
+ * whichever rule appears LATER in the stylesheet — CSS's normal same-
20
+ * specificity tiebreak. Without a fixed priority, "later" would depend on
21
+ * encounter order (whichever class the app happens to render/scan first),
22
+ * making the winner effectively random and inconsistent across reloads/
23
+ * builds. `order` fixes that: rules are emitted/injected sorted by this
24
+ * value (ascending — higher wins ties), regardless of source order, so
25
+ * e.g. `disabled:` always beats `hover:` on the same element no matter
26
+ * which one was written first in the className or rendered first in the
27
+ * app. Omit for the default (0). See getModifierOrder() below.
28
+ */
29
+ order?: number;
30
+ /** CSS pseudo-class or pseudo-element appended to the selector (e.g. ':hover', '::before') */
31
+ pseudo?: string;
32
+ /** Ancestor selector prefix INCLUDING trailing space (e.g. '.group:hover ', '.peer:focus ~ ') */
33
+ ancestorSelector?: string;
34
+ /** Directionality attribute selector prefix INCLUDING trailing space (e.g. '[dir="rtl"] ') */
35
+ dirSelector?: string;
36
+ /** @media query body WITHOUT the '@media ' prefix (e.g. 'print', '(orientation: landscape)') */
37
+ mediaQuery?: string;
38
+ /** Dark/light mode scheme — triggers the configured darkMode strategy in CSS output */
39
+ darkScheme?: 'dark' | 'light';
40
+ /** True for responsive modifiers — wraps in @media (min-width: theme.screens[name]) */
41
+ isResponsive?: boolean;
42
+ /**
43
+ * Forces !important on all declarations in the generated CSS rule.
44
+ * Applied automatically for structural / ancestor / media modifiers that must
45
+ * win over base inline styles.
46
+ */
47
+ forcesImportant?: boolean;
48
+ /**
49
+ * How the JSX runtime routes this modifier:
50
+ * 'interactive' — managed by InteractiveWrapper (hover, focus, pressed, …)
51
+ * 'mode' — managed by DarkWrapper (dark, light, not-dark, …)
52
+ * 'responsive' — managed by DarkWrapper (sm, md, lg, xl, 2xl)
53
+ * 'css-only' — CSS injection only; matchModifier always returns false
54
+ */
55
+ jsBehavior: 'interactive' | 'mode' | 'responsive' | 'css-only';
56
+ /**
57
+ * Evaluates whether this modifier's condition is met at runtime.
58
+ * Omit for 'css-only' modifiers — they never apply as inline styles.
59
+ */
60
+ jsMatch?: (isDark: boolean, state: Record<string, boolean | undefined>, breakpoints: Set<string>) => boolean;
61
+ }
62
+
63
+ type ThemeMode = 'light' | 'dark' | 'system';
64
+ interface StyleValue {
65
+ [key: string]: string | number | undefined | null | StyleValue | StyleValue[];
66
+ }
67
+ /**
68
+ * A color value is either a plain string (hex/rgb/alias-to-another-color-name)
69
+ * or a mode-aware pair — resolved to `light` or `dark` per the active theme
70
+ * mode wherever it's actually used (className resolution, useColors()).
71
+ */
72
+ type ColorValue = string | {
73
+ light: string;
74
+ dark: string;
75
+ };
76
+ type ColorShades = Record<string, ColorValue>;
77
+ type ThemeColors = Record<string, ColorValue | ColorShades>;
78
+ type ThemeSpacing = Record<string, number | string>;
79
+ interface ThemeConfig {
80
+ colors: ThemeColors;
81
+ spacing: ThemeSpacing;
82
+ fontSize: Record<string, number | string>;
83
+ fontFamily: Record<string, string | string[]>;
84
+ fontWeight: Record<string, string | number>;
85
+ borderRadius: Record<string, number | string>;
86
+ borderWidth: Record<string, number>;
87
+ opacity: Record<string, number>;
88
+ lineHeight: Record<string, number | string>;
89
+ letterSpacing: Record<string, number | string>;
90
+ zIndex: Record<string, number | string>;
91
+ flex: Record<string, number | string>;
92
+ shadow: Record<string, StyleValue>;
93
+ screens: Record<string, string | number>;
94
+ /**
95
+ * Custom @keyframes, web only. Each key is a keyframe name, its value maps
96
+ * percentage/from/to selectors to a plain CSS declaration object (camelCase
97
+ * properties, same shape as an inline style object):
98
+ * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } }
99
+ * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out].
100
+ */
101
+ keyframes: Record<string, Record<string, StyleValue>>;
102
+ /**
103
+ * Named animation shorthands built on `keyframes` above, referenced via
104
+ * animate-{name} (e.g. animate-wiggle):
105
+ * animation: { wiggle: 'wiggle 1s ease-in-out infinite' }
106
+ * The first word must match a `keyframes` key so its @keyframes rule can be
107
+ * injected alongside the animation — a name with no matching keyframes entry
108
+ * still sets the `animation` CSS property, it just won't animate anything.
109
+ */
110
+ animation: Record<string, string>;
111
+ [key: string]: unknown;
112
+ }
113
+ /**
114
+ * 'class' — toggles .dark class on <html>
115
+ * 'media' — uses prefers-color-scheme media query
116
+ * 'attribute' — uses data-theme="dark" attribute on <html>
117
+ */
118
+ type DarkMode = 'attribute' | 'class' | 'media';
119
+ interface PluginAPI {
120
+ addUtility(name: string, styles: StyleValue): void;
121
+ /**
122
+ * Register a custom variant.
123
+ *
124
+ * Pass a CSS selector string for simple cases — it is automatically
125
+ * converted into a ModifierDef that generates correct CSS rules:
126
+ * addVariant('hocus', ':hover, :focus') // pseudo
127
+ * addVariant('supports-grid', '@media (display: grid)') // media
128
+ * addVariant('dark-green', '.dark-green') // ancestor selector
129
+ *
130
+ * Pass a full ModifierDef object for advanced control (e.g. JS-trackable
131
+ * interactive variants with custom jsMatch logic).
132
+ */
133
+ addVariant(name: string, selectorOrDef: string | ModifierDef): void;
134
+ theme(path: string, defaultValue?: unknown): unknown;
135
+ e(className: string): string;
136
+ }
137
+ interface FrameworkConfig {
138
+ darkMode?: DarkMode;
139
+ theme?: Partial<ThemeConfig>;
140
+ /** Additive theme extension — accepts either `extend.theme.X` or `extend.X` directly. */
141
+ extend?: {
142
+ theme?: Partial<ThemeConfig>;
143
+ } & Partial<ThemeConfig>;
144
+ plugins?: Array<(api: PluginAPI) => void>;
145
+ content?: string[];
146
+ }
147
+
148
+ interface ThemeProviderProps {
149
+ children: ReactNode;
150
+ /** Initial mode. Falls back to persisted value, then 'system'. */
151
+ defaultMode?: ThemeMode;
152
+ /**
153
+ * System color scheme for native `defaultMode="system"`.
154
+ *
155
+ * Pass the value of `useColorScheme()` from `react-native`. When importing
156
+ * `ThemeProvider` from `@kbach/ui/native` this is handled automatically.
157
+ *
158
+ * @example
159
+ * ```tsx
160
+ * import { useColorScheme } from 'react-native';
161
+ * const colorScheme = useColorScheme();
162
+ * <ThemeProvider defaultMode="system" colorScheme={colorScheme}>…</ThemeProvider>
163
+ * ```
164
+ */
165
+ colorScheme?: 'light' | 'dark' | null;
166
+ /**
167
+ * Current window/screen width in pixels for responsive breakpoints.
168
+ * On web this is read from `window.innerWidth` automatically.
169
+ * When importing `ThemeProvider` from `@kbach/ui/native` this is provided
170
+ * automatically from `useWindowDimensions()`.
171
+ */
172
+ windowWidth?: number;
173
+ /** Override the config (useful for per-tree config). Defaults to global getConfig(). */
174
+ config?: FrameworkConfig;
175
+ /** Disable persistence to localStorage */
176
+ disablePersistence?: boolean;
177
+ }
178
+
179
+ /**
180
+ * Native-aware ThemeProvider. Wraps the base ThemeProvider and automatically
181
+ * passes the system color scheme from React Native's useColorScheme() hook.
182
+ *
183
+ * This fixes Android startup dark mode detection: Appearance.getColorScheme()
184
+ * can cache null if the device was already dark at launch (the appearanceChanged
185
+ * event only fires on *changes*). useColorScheme() called here with a proper
186
+ * hook name ensures the React Compiler and all linters handle it correctly, and
187
+ * useSyncExternalStore inside the hook properly subscribes to Appearance events.
188
+ *
189
+ * Exported as `ThemeProvider` from @kbach/ui/native — no API change for
190
+ * existing @kbach/native users (that package now re-exports this).
191
+ *
192
+ * `react-native` is required here lazily (inside the function body) rather
193
+ * than via a top-level `import`. native/index.ts bundles this file together
194
+ * with setup.ts's Node-only helpers (createKbachConfig, withKbach,
195
+ * withKbachBabel), which babel.config.js loads by calling
196
+ * `require('@kbach/ui/native')` in a plain Node.js process — no Metro, no
197
+ * Babel/Flow transform for react-native's own source. A top-level import
198
+ * would make Node eagerly require the real `react-native` package just to
199
+ * read createKbachConfig off the module, which crashes immediately
200
+ * (react-native's entry point isn't valid plain-Node JS). A require() inside
201
+ * the function body only ever runs when NativeThemeProvider actually renders
202
+ * — i.e. inside the real Metro/Hermes runtime, where require() is always
203
+ * available and react-native loads fine.
204
+ *
205
+ * This is also why the "./native" export has no separate ESM entry: tsup/
206
+ * esbuild can't emit a real `require()` inside ESM output — it rewrites it to
207
+ * a `__require` shim (`typeof require !== "undefined" ? require : …`). That
208
+ * shim still resolves to Metro's real require function at runtime, but
209
+ * Metro's bundler only registers a module's dependencies by statically
210
+ * finding literal `require("name")` calls in its source — `__require(...)`
211
+ * doesn't match, so "react-native" is never added to the compiled module's
212
+ * dependency map and Metro throws "Requiring unknown module" at runtime. The
213
+ * CJS build's plain `require('react-native')` call doesn't have this
214
+ * problem, so both "import" and "require" conditions point at dist/native.js.
215
+ *
216
+ * '@kbach/ui' itself is required the same lazy way, for a different
217
+ * reason: this file is bundled into its own dist/native.js (see
218
+ * tsup.config.ts), separate from dist/index.js/.mjs. A top-level `import`
219
+ * would make esbuild inline a SECOND, independent copy of
220
+ * ThemeProvider.tsx/context.tsx (its own createContext() call) into
221
+ * dist/native.js, splitting ThemeContext between "@kbach/ui" and
222
+ * "@kbach/ui/native" consumers — require('@kbach/ui') instead resolves
223
+ * through Node/npm workspaces' self-reference and tsup's default of treating
224
+ * anything outside the entry's own source tree as external, reaching the
225
+ * exact same dist/index.js instance every other consumer gets (verified by
226
+ * building and grepping dist/native.js for a literal require("@kbach/ui")
227
+ * rather than an inlined copy). `typeof import(...)` for the type would hit
228
+ * the same self-reference resolution tsup's DTS step can't handle during its
229
+ * own package's build (unlike esbuild's JS bundling, which resolves it
230
+ * fine) — so the type comes from the relative ThemeProviderProps import
231
+ * above instead, and ThemeProvider itself is cast to match.
232
+ */
233
+ declare function NativeThemeProvider(props: ThemeProviderProps): React.JSX.Element;
234
+
235
+ /**
236
+ * Metro and Babel setup helpers for React Native / Expo projects.
237
+ *
238
+ * All three functions are meant to be called from Node.js config files
239
+ * (metro.config.js, babel.config.js). They are safe to import in React
240
+ * Native bundles but will never execute there.
241
+ */
242
+ interface KbachOptions {
243
+ /** Path to kbach.config.js, relative to project root. Default: 'kbach.config.js' */
244
+ configFile?: string;
245
+ /** JSX attribute names to transform at build time. Default: ['kb', 'className'] */
246
+ attributes?: string[];
247
+ /** Log transformed class strings to the Metro console. Default: false */
248
+ debug?: boolean;
249
+ }
250
+ /**
251
+ * Inject the Kbach Babel plugin into a Metro transformer config.
252
+ *
253
+ * metro.config.js (Expo):
254
+ * ```js
255
+ * const { getDefaultConfig } = require('expo/metro-config');
256
+ * const { withKbach } = require('@kbach/ui/native');
257
+ * const config = getDefaultConfig(__dirname);
258
+ * module.exports = withKbach(config);
259
+ * ```
260
+ *
261
+ * metro.config.js (bare React Native):
262
+ * ```js
263
+ * const { getDefaultConfig } = require('@react-native/metro-config');
264
+ * const { withKbach } = require('@kbach/ui/native');
265
+ * const config = getDefaultConfig(__dirname);
266
+ * module.exports = withKbach(config);
267
+ * ```
268
+ */
269
+ declare function withKbach(metroConfig: Record<string, any>, _options?: KbachOptions): Record<string, any>;
270
+ /**
271
+ * Add the Kbach preset to an existing Babel config.
272
+ * Use this when you have a custom babel.config.js and want to keep it.
273
+ *
274
+ * babel.config.js:
275
+ * ```js
276
+ * const { withKbachBabel } = require('@kbach/ui/native');
277
+ * module.exports = withKbachBabel({
278
+ * presets: ['babel-preset-expo'],
279
+ * });
280
+ * ```
281
+ */
282
+ declare function withKbachBabel(babelConfig: Record<string, any>, options?: KbachOptions): Record<string, any>;
283
+ /**
284
+ * Generate a complete Babel config for Expo projects.
285
+ * This is the recommended one-liner for new projects.
286
+ *
287
+ * babel.config.js:
288
+ * ```js
289
+ * const { createKbachConfig } = require('@kbach/ui/native');
290
+ * module.exports = createKbachConfig();
291
+ * ```
292
+ *
293
+ * Or written manually (identical to NativeWind's config shape):
294
+ * ```js
295
+ * module.exports = function(api) {
296
+ * api.cache(true);
297
+ * return {
298
+ * presets: [
299
+ * 'babel-preset-expo',
300
+ * '@kbach/ui/babel',
301
+ * ],
302
+ * };
303
+ * };
304
+ * ```
305
+ *
306
+ * Do NOT pass `jsxImportSource: '@kbach/ui'` to babel-preset-expo here —
307
+ * that sets the default JSX pragma for every file Metro transforms, including
308
+ * node_modules and react-native's own internals, which breaks them. See the
309
+ * comment in withKbachBabel above for why the per-file pragma comment the
310
+ * Kbach babel plugin injects is the only place that should apply.
311
+ */
312
+ declare function createKbachConfig(options?: KbachOptions): Record<string, unknown>;
313
+
314
+ export { type KbachOptions, NativeThemeProvider as ThemeProvider, createKbachConfig, withKbach, withKbachBabel };
package/dist/native.js ADDED
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/native/index.ts
22
+ var native_exports = {};
23
+ __export(native_exports, {
24
+ ThemeProvider: () => NativeThemeProvider,
25
+ createKbachConfig: () => createKbachConfig,
26
+ withKbach: () => withKbach,
27
+ withKbachBabel: () => withKbachBabel
28
+ });
29
+ module.exports = __toCommonJS(native_exports);
30
+
31
+ // src/native/NativeThemeProvider.tsx
32
+ var import_jsx_runtime = require("react/jsx-runtime");
33
+ function NativeThemeProvider(props) {
34
+ const { useColorScheme, useWindowDimensions } = require("react-native");
35
+ const { ThemeProvider } = require("@kbach/ui");
36
+ const raw = useColorScheme();
37
+ const { width } = useWindowDimensions();
38
+ const systemColorScheme = raw === "dark" ? "dark" : raw === "light" ? "light" : null;
39
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
40
+ ThemeProvider,
41
+ {
42
+ ...props,
43
+ colorScheme: props.colorScheme ?? systemColorScheme,
44
+ windowWidth: props.windowWidth ?? width
45
+ }
46
+ );
47
+ }
48
+
49
+ // src/native/setup.ts
50
+ function warn(message) {
51
+ const proc = process;
52
+ const useColor = !!proc.stdout?.isTTY && !proc.env.NO_COLOR;
53
+ const paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
54
+ console.warn(`${paint("1", paint("35", "[kbach]"))} ${paint("33", message)}`);
55
+ }
56
+ function withKbach(metroConfig, _options = {}) {
57
+ return metroConfig;
58
+ }
59
+ function withKbachBabel(babelConfig, options = {}) {
60
+ const {
61
+ configFile = "kbach.config.js",
62
+ attributes = ["kb", "className"],
63
+ debug = false
64
+ } = options;
65
+ const presets = (babelConfig.presets ?? []).map((preset) => {
66
+ const [name, presetOpts = {}] = Array.isArray(preset) ? preset : [preset, {}];
67
+ if (typeof name === "string" && (name.includes("babel-preset-expo") || name.includes("preset-react"))) {
68
+ const opts = presetOpts;
69
+ if (opts.jsxRuntime === "classic") {
70
+ warn(`Forcing jsxRuntime "automatic" on ${name} (was "classic") \u2014 required for Kbach`);
71
+ }
72
+ return [name, { ...opts, jsxRuntime: "automatic" }];
73
+ }
74
+ return preset;
75
+ });
76
+ return {
77
+ ...babelConfig,
78
+ presets: [...presets, ["@kbach/ui/babel", { configFile, attributes, debug }]]
79
+ };
80
+ }
81
+ function createKbachConfig(options = {}) {
82
+ return {
83
+ presets: [
84
+ "babel-preset-expo",
85
+ ["@kbach/ui/babel", {
86
+ configFile: options.configFile ?? "kbach.config.js",
87
+ attributes: options.attributes ?? ["kb", "className"],
88
+ debug: options.debug ?? false
89
+ }]
90
+ ]
91
+ };
92
+ }
93
+ // Annotate the CommonJS export names for ESM import in node:
94
+ 0 && (module.exports = {
95
+ ThemeProvider,
96
+ createKbachConfig,
97
+ withKbach,
98
+ withKbachBabel
99
+ });
@@ -0,0 +1,155 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /**
4
+ * Unified modifier registry — single source of truth for ALL modifier behavior.
5
+ *
6
+ * Adding a new modifier requires editing ONLY this file:
7
+ * 1. Add an entry to BUILTIN_MODIFIERS with its CSS and JS behavior.
8
+ * 2. Done — parser, resolver, CSS generator, and JSX runtime all derive
9
+ * their behavior from this data automatically.
10
+ *
11
+ * Plugin authors can register custom modifiers via registerModifier().
12
+ */
13
+ interface ModifierDef {
14
+ /**
15
+ * Cascade priority for CSS rule ORDER — NOT specificity. Two rules that
16
+ * differ only by modifier (e.g. `.hover\:bg-blue-6:hover` and
17
+ * `.focus\:bg-red-6:focus`) have equal CSS specificity, so when both
18
+ * conditions are true at once (hovering AND focused), the winner is
19
+ * whichever rule appears LATER in the stylesheet — CSS's normal same-
20
+ * specificity tiebreak. Without a fixed priority, "later" would depend on
21
+ * encounter order (whichever class the app happens to render/scan first),
22
+ * making the winner effectively random and inconsistent across reloads/
23
+ * builds. `order` fixes that: rules are emitted/injected sorted by this
24
+ * value (ascending — higher wins ties), regardless of source order, so
25
+ * e.g. `disabled:` always beats `hover:` on the same element no matter
26
+ * which one was written first in the className or rendered first in the
27
+ * app. Omit for the default (0). See getModifierOrder() below.
28
+ */
29
+ order?: number;
30
+ /** CSS pseudo-class or pseudo-element appended to the selector (e.g. ':hover', '::before') */
31
+ pseudo?: string;
32
+ /** Ancestor selector prefix INCLUDING trailing space (e.g. '.group:hover ', '.peer:focus ~ ') */
33
+ ancestorSelector?: string;
34
+ /** Directionality attribute selector prefix INCLUDING trailing space (e.g. '[dir="rtl"] ') */
35
+ dirSelector?: string;
36
+ /** @media query body WITHOUT the '@media ' prefix (e.g. 'print', '(orientation: landscape)') */
37
+ mediaQuery?: string;
38
+ /** Dark/light mode scheme — triggers the configured darkMode strategy in CSS output */
39
+ darkScheme?: 'dark' | 'light';
40
+ /** True for responsive modifiers — wraps in @media (min-width: theme.screens[name]) */
41
+ isResponsive?: boolean;
42
+ /**
43
+ * Forces !important on all declarations in the generated CSS rule.
44
+ * Applied automatically for structural / ancestor / media modifiers that must
45
+ * win over base inline styles.
46
+ */
47
+ forcesImportant?: boolean;
48
+ /**
49
+ * How the JSX runtime routes this modifier:
50
+ * 'interactive' — managed by InteractiveWrapper (hover, focus, pressed, …)
51
+ * 'mode' — managed by DarkWrapper (dark, light, not-dark, …)
52
+ * 'responsive' — managed by DarkWrapper (sm, md, lg, xl, 2xl)
53
+ * 'css-only' — CSS injection only; matchModifier always returns false
54
+ */
55
+ jsBehavior: 'interactive' | 'mode' | 'responsive' | 'css-only';
56
+ /**
57
+ * Evaluates whether this modifier's condition is met at runtime.
58
+ * Omit for 'css-only' modifiers — they never apply as inline styles.
59
+ */
60
+ jsMatch?: (isDark: boolean, state: Record<string, boolean | undefined>, breakpoints: Set<string>) => boolean;
61
+ }
62
+
63
+ interface StyleValue {
64
+ [key: string]: string | number | undefined | null | StyleValue | StyleValue[];
65
+ }
66
+ /**
67
+ * A color value is either a plain string (hex/rgb/alias-to-another-color-name)
68
+ * or a mode-aware pair — resolved to `light` or `dark` per the active theme
69
+ * mode wherever it's actually used (className resolution, useColors()).
70
+ */
71
+ type ColorValue = string | {
72
+ light: string;
73
+ dark: string;
74
+ };
75
+ type ColorShades = Record<string, ColorValue>;
76
+ type ThemeColors = Record<string, ColorValue | ColorShades>;
77
+ type ThemeSpacing = Record<string, number | string>;
78
+ interface ThemeConfig {
79
+ colors: ThemeColors;
80
+ spacing: ThemeSpacing;
81
+ fontSize: Record<string, number | string>;
82
+ fontFamily: Record<string, string | string[]>;
83
+ fontWeight: Record<string, string | number>;
84
+ borderRadius: Record<string, number | string>;
85
+ borderWidth: Record<string, number>;
86
+ opacity: Record<string, number>;
87
+ lineHeight: Record<string, number | string>;
88
+ letterSpacing: Record<string, number | string>;
89
+ zIndex: Record<string, number | string>;
90
+ flex: Record<string, number | string>;
91
+ shadow: Record<string, StyleValue>;
92
+ screens: Record<string, string | number>;
93
+ /**
94
+ * Custom @keyframes, web only. Each key is a keyframe name, its value maps
95
+ * percentage/from/to selectors to a plain CSS declaration object (camelCase
96
+ * properties, same shape as an inline style object):
97
+ * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } }
98
+ * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out].
99
+ */
100
+ keyframes: Record<string, Record<string, StyleValue>>;
101
+ /**
102
+ * Named animation shorthands built on `keyframes` above, referenced via
103
+ * animate-{name} (e.g. animate-wiggle):
104
+ * animation: { wiggle: 'wiggle 1s ease-in-out infinite' }
105
+ * The first word must match a `keyframes` key so its @keyframes rule can be
106
+ * injected alongside the animation — a name with no matching keyframes entry
107
+ * still sets the `animation` CSS property, it just won't animate anything.
108
+ */
109
+ animation: Record<string, string>;
110
+ [key: string]: unknown;
111
+ }
112
+ /**
113
+ * 'class' — toggles .dark class on <html>
114
+ * 'media' — uses prefers-color-scheme media query
115
+ * 'attribute' — uses data-theme="dark" attribute on <html>
116
+ */
117
+ type DarkMode = 'attribute' | 'class' | 'media';
118
+ interface PluginAPI {
119
+ addUtility(name: string, styles: StyleValue): void;
120
+ /**
121
+ * Register a custom variant.
122
+ *
123
+ * Pass a CSS selector string for simple cases — it is automatically
124
+ * converted into a ModifierDef that generates correct CSS rules:
125
+ * addVariant('hocus', ':hover, :focus') // pseudo
126
+ * addVariant('supports-grid', '@media (display: grid)') // media
127
+ * addVariant('dark-green', '.dark-green') // ancestor selector
128
+ *
129
+ * Pass a full ModifierDef object for advanced control (e.g. JS-trackable
130
+ * interactive variants with custom jsMatch logic).
131
+ */
132
+ addVariant(name: string, selectorOrDef: string | ModifierDef): void;
133
+ theme(path: string, defaultValue?: unknown): unknown;
134
+ e(className: string): string;
135
+ }
136
+ interface FrameworkConfig {
137
+ darkMode?: DarkMode;
138
+ theme?: Partial<ThemeConfig>;
139
+ /** Additive theme extension — accepts either `extend.theme.X` or `extend.X` directly. */
140
+ extend?: {
141
+ theme?: Partial<ThemeConfig>;
142
+ } & Partial<ThemeConfig>;
143
+ plugins?: Array<(api: PluginAPI) => void>;
144
+ content?: string[];
145
+ }
146
+
147
+ declare function formatKbachCSS(tokenCSS: Map<string, string>, theme: ThemeConfig, responsiveRe: RegExp): string;
148
+ interface KbachPluginOptions {
149
+ framework?: FrameworkConfig;
150
+ /** Directories to scan for class strings (relative to Vite root). Defaults to common source dirs. */
151
+ include?: string[];
152
+ }
153
+ declare function kbach(userConfigOrOptions?: FrameworkConfig | KbachPluginOptions): Plugin;
154
+
155
+ export { type KbachPluginOptions, formatKbachCSS, kbach };