import React, { ReactNode } from 'react'; /** * Unified modifier registry — single source of truth for ALL modifier behavior. * * Adding a new modifier requires editing ONLY this file: * 1. Add an entry to BUILTIN_MODIFIERS with its CSS and JS behavior. * 2. Done — parser, resolver, CSS generator, and JSX runtime all derive * their behavior from this data automatically. * * Plugin authors can register custom modifiers via registerModifier(). */ interface ModifierDef { /** * Cascade priority for CSS rule ORDER — NOT specificity. Two rules that * differ only by modifier (e.g. `.hover\:bg-blue-6:hover` and * `.focus\:bg-red-6:focus`) have equal CSS specificity, so when both * conditions are true at once (hovering AND focused), the winner is * whichever rule appears LATER in the stylesheet — CSS's normal same- * specificity tiebreak. Without a fixed priority, "later" would depend on * encounter order (whichever class the app happens to render/scan first), * making the winner effectively random and inconsistent across reloads/ * builds. `order` fixes that: rules are emitted/injected sorted by this * value (ascending — higher wins ties), regardless of source order, so * e.g. `disabled:` always beats `hover:` on the same element no matter * which one was written first in the className or rendered first in the * app. Omit for the default (0). See getModifierOrder() below. */ order?: number; /** CSS pseudo-class or pseudo-element appended to the selector (e.g. ':hover', '::before') */ pseudo?: string; /** Ancestor selector prefix INCLUDING trailing space (e.g. '.group:hover ', '.peer:focus ~ ') */ ancestorSelector?: string; /** Directionality attribute selector prefix INCLUDING trailing space (e.g. '[dir="rtl"] ') */ dirSelector?: string; /** @media query body WITHOUT the '@media ' prefix (e.g. 'print', '(orientation: landscape)') */ mediaQuery?: string; /** Dark/light mode scheme — triggers the configured darkMode strategy in CSS output */ darkScheme?: 'dark' | 'light'; /** True for responsive modifiers — wraps in @media (min-width: theme.screens[name]) */ isResponsive?: boolean; /** * Forces !important on all declarations in the generated CSS rule. * Applied automatically for structural / ancestor / media modifiers that must * win over base inline styles. */ forcesImportant?: boolean; /** * How the JSX runtime routes this modifier: * 'interactive' — managed by InteractiveWrapper (hover, focus, pressed, …) * 'mode' — managed by DarkWrapper (dark, light, not-dark, …) * 'responsive' — managed by DarkWrapper (sm, md, lg, xl, 2xl) * 'css-only' — CSS injection only; matchModifier always returns false */ jsBehavior: 'interactive' | 'mode' | 'responsive' | 'css-only'; /** * Evaluates whether this modifier's condition is met at runtime. * Omit for 'css-only' modifiers — they never apply as inline styles. */ jsMatch?: (isDark: boolean, state: Record, breakpoints: Set) => boolean; } type ThemeMode = 'light' | 'dark' | 'system'; interface StyleValue { [key: string]: string | number | undefined | null | StyleValue | StyleValue[]; } /** * A color value is either a plain string (hex/rgb/alias-to-another-color-name) * or a mode-aware pair — resolved to `light` or `dark` per the active theme * mode wherever it's actually used (className resolution, useColors()). */ type ColorValue = string | { light: string; dark: string; }; type ColorShades = Record; type ThemeColors = Record; type ThemeSpacing = Record; interface ThemeConfig { colors: ThemeColors; spacing: ThemeSpacing; fontSize: Record; fontFamily: Record; fontWeight: Record; borderRadius: Record; borderWidth: Record; opacity: Record; lineHeight: Record; letterSpacing: Record; zIndex: Record; flex: Record; shadow: Record; screens: Record; /** * Custom @keyframes, web only. Each key is a keyframe name, its value maps * percentage/from/to selectors to a plain CSS declaration object (camelCase * properties, same shape as an inline style object): * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } } * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out]. */ keyframes: Record>; /** * Named animation shorthands built on `keyframes` above, referenced via * animate-{name} (e.g. animate-wiggle): * animation: { wiggle: 'wiggle 1s ease-in-out infinite' } * The first word must match a `keyframes` key so its @keyframes rule can be * injected alongside the animation — a name with no matching keyframes entry * still sets the `animation` CSS property, it just won't animate anything. */ animation: Record; [key: string]: unknown; } /** * 'class' — toggles .dark class on * 'media' — uses prefers-color-scheme media query * 'attribute' — uses data-theme="dark" attribute on */ type DarkMode = 'attribute' | 'class' | 'media'; interface PluginAPI { addUtility(name: string, styles: StyleValue): void; /** * Register a custom variant. * * Pass a CSS selector string for simple cases — it is automatically * converted into a ModifierDef that generates correct CSS rules: * addVariant('hocus', ':hover, :focus') // pseudo * addVariant('supports-grid', '@media (display: grid)') // media * addVariant('dark-green', '.dark-green') // ancestor selector * * Pass a full ModifierDef object for advanced control (e.g. JS-trackable * interactive variants with custom jsMatch logic). */ addVariant(name: string, selectorOrDef: string | ModifierDef): void; theme(path: string, defaultValue?: unknown): unknown; e(className: string): string; } interface FrameworkConfig { darkMode?: DarkMode; theme?: Partial; /** Additive theme extension — accepts either `extend.theme.X` or `extend.X` directly. */ extend?: { theme?: Partial; } & Partial; plugins?: Array<(api: PluginAPI) => void>; content?: string[]; } interface ThemeProviderProps { children: ReactNode; /** Initial mode. Falls back to persisted value, then 'system'. */ defaultMode?: ThemeMode; /** * System color scheme for native `defaultMode="system"`. * * Pass the value of `useColorScheme()` from `react-native`. When importing * `ThemeProvider` from `@kbach/react/native` this is handled automatically. * * @example * ```tsx * import { useColorScheme } from 'react-native'; * const colorScheme = useColorScheme(); * * ``` */ colorScheme?: 'light' | 'dark' | null; /** * Current window/screen width in pixels for responsive breakpoints. * On web this is read from `window.innerWidth` automatically. * When importing `ThemeProvider` from `@kbach/react/native` this is provided * automatically from `useWindowDimensions()`. */ windowWidth?: number; /** Override the config (useful for per-tree config). Defaults to global getConfig(). */ config?: FrameworkConfig; /** Disable persistence to localStorage */ disablePersistence?: boolean; } /** * Native-aware ThemeProvider. Wraps the base ThemeProvider and automatically * passes the system color scheme from React Native's useColorScheme() hook. * * This fixes Android startup dark mode detection: Appearance.getColorScheme() * can cache null if the device was already dark at launch (the appearanceChanged * event only fires on *changes*). useColorScheme() called here with a proper * hook name ensures the React Compiler and all linters handle it correctly, and * useSyncExternalStore inside the hook properly subscribes to Appearance events. * * Exported as `ThemeProvider` from @kbach/react/native — no API change for * existing @kbach/native users (that package now re-exports this). * * `react-native` is required here lazily (inside the function body) rather * than via a top-level `import`. native/index.ts bundles this file together * with setup.ts's Node-only helpers (createKbachConfig, withKbach, * withKbachBabel), which babel.config.js loads by calling * `require('@kbach/react/native')` in a plain Node.js process — no Metro, no * Babel/Flow transform for react-native's own source. A top-level import * would make Node eagerly require the real `react-native` package just to * read createKbachConfig off the module, which crashes immediately * (react-native's entry point isn't valid plain-Node JS). A require() inside * the function body only ever runs when NativeThemeProvider actually renders * — i.e. inside the real Metro/Hermes runtime, where require() is always * available and react-native loads fine. * * This is also why the "./native" export has no separate ESM entry: tsup/ * esbuild can't emit a real `require()` inside ESM output — it rewrites it to * a `__require` shim (`typeof require !== "undefined" ? require : …`). That * shim still resolves to Metro's real require function at runtime, but * Metro's bundler only registers a module's dependencies by statically * finding literal `require("name")` calls in its source — `__require(...)` * doesn't match, so "react-native" is never added to the compiled module's * dependency map and Metro throws "Requiring unknown module" at runtime. The * CJS build's plain `require('react-native')` call doesn't have this * problem, so both "import" and "require" conditions point at dist/native.js. * * '@kbach/react' itself is required the same lazy way, for a different * reason: this file is bundled into its own dist/native.js (see * tsup.config.ts), separate from dist/index.js/.mjs. A top-level `import` * would make esbuild inline a SECOND, independent copy of * ThemeProvider.tsx/context.tsx (its own createContext() call) into * dist/native.js, splitting ThemeContext between "@kbach/react" and * "@kbach/react/native" consumers — require('@kbach/react') instead resolves * through Node/npm workspaces' self-reference and tsup's default of treating * anything outside the entry's own source tree as external, reaching the * exact same dist/index.js instance every other consumer gets (verified by * building and grepping dist/native.js for a literal require("@kbach/react") * rather than an inlined copy). `typeof import(...)` for the type would hit * the same self-reference resolution tsup's DTS step can't handle during its * own package's build (unlike esbuild's JS bundling, which resolves it * fine) — so the type comes from the relative ThemeProviderProps import * above instead, and ThemeProvider itself is cast to match. */ declare function NativeThemeProvider(props: ThemeProviderProps): React.JSX.Element; /** * Metro and Babel setup helpers for React Native / Expo projects. * * All three functions are meant to be called from Node.js config files * (metro.config.js, babel.config.js). They are safe to import in React * Native bundles but will never execute there. */ interface KbachOptions { /** Path to kbach.config.js, relative to project root. Default: 'kbach.config.js' */ configFile?: string; /** JSX attribute names to transform at build time. Default: ['kb', 'className'] */ attributes?: string[]; /** Log transformed class strings to the Metro console. Default: false */ debug?: boolean; } /** * Inject the Kbach Babel plugin into a Metro transformer config. * * metro.config.js (Expo): * ```js * const { getDefaultConfig } = require('expo/metro-config'); * const { withKbach } = require('@kbach/react/native'); * const config = getDefaultConfig(__dirname); * module.exports = withKbach(config); * ``` * * metro.config.js (bare React Native): * ```js * const { getDefaultConfig } = require('@react-native/metro-config'); * const { withKbach } = require('@kbach/react/native'); * const config = getDefaultConfig(__dirname); * module.exports = withKbach(config); * ``` */ declare function withKbach(metroConfig: Record, _options?: KbachOptions): Record; /** * Add the Kbach preset to an existing Babel config. * Use this when you have a custom babel.config.js and want to keep it. * * babel.config.js: * ```js * const { withKbachBabel } = require('@kbach/react/native'); * module.exports = withKbachBabel({ * presets: ['babel-preset-expo'], * }); * ``` */ declare function withKbachBabel(babelConfig: Record, options?: KbachOptions): Record; /** * Generate a complete Babel config for Expo projects. * This is the recommended one-liner for new projects. * * babel.config.js: * ```js * const { createKbachConfig } = require('@kbach/react/native'); * module.exports = createKbachConfig(); * ``` * * Or written manually (identical to NativeWind's config shape): * ```js * module.exports = function(api) { * api.cache(true); * return { * presets: [ * 'babel-preset-expo', * '@kbach/react/babel', * ], * }; * }; * ``` * * Do NOT pass `jsxImportSource: '@kbach/react'` to babel-preset-expo here — * that sets the default JSX pragma for every file Metro transforms, including * node_modules and react-native's own internals, which breaks them. See the * comment in withKbachBabel above for why the per-file pragma comment the * Kbach babel plugin injects is the only place that should apply. */ declare function createKbachConfig(options?: KbachOptions): Record; export { type KbachOptions, NativeThemeProvider as ThemeProvider, createKbachConfig, withKbach, withKbachBabel };