@kbach/react 0.6.49 → 0.6.51
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/KBACH.md +1044 -0
- package/README.md +110 -6
- package/dist/core/index.d.ts +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/native.d.ts +314 -0
- package/dist/native.js +99 -0
- package/kbach-react.md +27 -1
- package/package.json +18 -2
- package/src/native/babel/index.js +30 -0
- package/src/native/babel-plugin/index.js +605 -0
- package/src/native/babel-plugin/index.test.ts +86 -0
package/README.md
CHANGED
|
@@ -38,7 +38,8 @@ That's the only setting needed — Vite, Next.js, and React Router all read it.
|
|
|
38
38
|
|---|---|
|
|
39
39
|
| Vite, React Router library mode, CRA, other Vite-based | **[Static CSS setup](#static-css-setup)** (recommended) — zero runtime cost, catches typos at build time. [Runtime setup](#runtime-setup) is there if you'd rather skip the plugin for now. |
|
|
40
40
|
| React Router, framework mode | **[Static CSS setup](#static-css-setup)** — and skip `@vitejs/plugin-react`, see note in that section |
|
|
41
|
-
| Next.js | **[
|
|
41
|
+
| Next.js | **[Next.js setup](#nextjs-setup)** — Runtime setup, plus one App Router-specific detail |
|
|
42
|
+
| React Native, Expo | **[React Native / Expo setup](#react-native--expo-setup)** — different setup entirely (Babel preset, not the JSX runtime step above) |
|
|
42
43
|
|
|
43
44
|
## Static CSS setup
|
|
44
45
|
|
|
@@ -98,7 +99,7 @@ export default defineConfig({ plugins: [kbach(), reactRouter()] });
|
|
|
98
99
|
|
|
99
100
|
## Runtime setup
|
|
100
101
|
|
|
101
|
-
Client-side CSS injection — works with any bundler (Vite, webpack, Turbopack, Metro-for-web, …), no build plugin.
|
|
102
|
+
Client-side CSS injection — works with any bundler (Vite, webpack, Turbopack, Metro-for-web, …), no build plugin. Next.js always uses this (see [Next.js setup](#nextjs-setup) below for the one extra detail), or use it on Vite if you'd rather not wire up the plugin yet. This is the whole setup:
|
|
102
103
|
|
|
103
104
|
```jsx
|
|
104
105
|
import { ThemeProvider, KbachReset } from '@kbach/react';
|
|
@@ -113,12 +114,115 @@ export default function Root() {
|
|
|
113
114
|
}
|
|
114
115
|
```
|
|
115
116
|
|
|
116
|
-
That's it — done. `<KbachReset />` renders the base reset (see [CSS resets](#css-resets)) as real markup instead of waiting on client JS — matters most for SSR, where it avoids a flash of unstyled browser defaults before hydration.
|
|
117
|
+
That's it — done. `<KbachReset />` renders the base reset (see [CSS resets](#css-resets)) as real markup instead of waiting on client JS — matters most for SSR, where it avoids a flash of unstyled browser defaults before hydration.
|
|
117
118
|
|
|
118
119
|
Using a custom `kbach.config.js`? Pass it to `ThemeProvider` too — see [Wiring the config in](#wiring-the-config-in--required-for-both-setups).
|
|
119
120
|
|
|
120
121
|
Don't also set up Static CSS above in the same app — pick one.
|
|
121
122
|
|
|
123
|
+
## Next.js setup
|
|
124
|
+
|
|
125
|
+
Next.js is always [Runtime setup](#runtime-setup) above — Static CSS doesn't apply (webpack/Turbopack, not Vite). The `tsconfig.json` step from [Setup](#setup) applies as-is; SWC reads `jsxImportSource` the same way Vite does.
|
|
126
|
+
|
|
127
|
+
The one Next.js-specific detail: render `<KbachReset />` once in the root App Router `layout.tsx` (inside `<head>`, or right after `<ThemeProvider>` opens) so the Server Component HTML has the base reset without waiting on hydration:
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
// app/layout.tsx
|
|
131
|
+
import { ThemeProvider, KbachReset } from '@kbach/react';
|
|
132
|
+
|
|
133
|
+
export default function RootLayout({ children }) {
|
|
134
|
+
return (
|
|
135
|
+
<html lang="en">
|
|
136
|
+
<body>
|
|
137
|
+
<ThemeProvider defaultMode="system">
|
|
138
|
+
<KbachReset />
|
|
139
|
+
{children}
|
|
140
|
+
</ThemeProvider>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Without `<KbachReset />` there, expect a flash of raw browser defaults (native button border, arrow-less `<select>`, etc.) on first paint until hydration completes. Utility classes beyond the base reset still wait on hydration either way — a known limitation of the runtime-only path (Static CSS isn't available for webpack/Turbopack), not a per-project bug.
|
|
148
|
+
|
|
149
|
+
`@kbach/react`'s compiled output ships its own `"use client"` directive, so App Router Server Components can use `className`, `styled()`, hooks, `<ThemeProvider>`, and `<KbachReset>` directly — no manual `'use client'` wrapper needed anywhere in your own components.
|
|
150
|
+
|
|
151
|
+
## React Native / Expo setup
|
|
152
|
+
|
|
153
|
+
Same `npm install @kbach/react` — no separate package. Everything below (API, modifiers, color system) is the same import as web; only setup differs.
|
|
154
|
+
|
|
155
|
+
**1. babel.config.js:**
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
module.exports = function (api) {
|
|
159
|
+
api.cache(true);
|
|
160
|
+
return {
|
|
161
|
+
presets: [
|
|
162
|
+
'babel-preset-expo',
|
|
163
|
+
'@kbach/react/babel',
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Or the one-liner helper: `const { createKbachConfig } = require('@kbach/react/native'); module.exports = createKbachConfig();` — identical result. Merging into an existing config: `withKbachBabel({ presets: [...] })`, also from `@kbach/react/native`. After changing this file, clear the Metro cache: `npx expo start --clear`.
|
|
170
|
+
|
|
171
|
+
**2. Wrap your app:**
|
|
172
|
+
|
|
173
|
+
```jsx
|
|
174
|
+
import { ThemeProvider } from '@kbach/react/native';
|
|
175
|
+
|
|
176
|
+
export default function App() {
|
|
177
|
+
return (
|
|
178
|
+
<ThemeProvider defaultMode="system">
|
|
179
|
+
<AppContent />
|
|
180
|
+
</ThemeProvider>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
This is a native-aware `ThemeProvider` — reads `useColorScheme()`/`useWindowDimensions()` automatically, no extra props needed. Import it from `@kbach/react/native`, not the plain `ThemeProvider` from `@kbach/react` — that one has no automatic RN wiring (colors scheme/window width would need to be passed in by hand).
|
|
186
|
+
|
|
187
|
+
### Platform differences
|
|
188
|
+
|
|
189
|
+
A handful of utilities are native-only or web-only:
|
|
190
|
+
|
|
191
|
+
| | |
|
|
192
|
+
|---|---|
|
|
193
|
+
| Native-only | `tint-{color}` (Image/icon tinting), `perspective-{n}`, `backface-hidden`, `text-shadow`/`text-shadow-lg` |
|
|
194
|
+
| Web-only, ignored on native (no warning) | `caret-*` `accent-*` `stroke-*` `fill-*` `touch-*` `float-*` `clear-*` `line-clamp-*` `scroll-*` `animate-*` `transition` `filter` `backdrop-filter` `print:` `before:` `after:` `selection:` `first-letter:` `first-line:` `marker:` `landscape:` `portrait:` `motion-reduce:` `motion-safe:` `contrast-more:` `contrast-less:` `rtl:` `ltr:` `grid` `grid-cols-*` `ring-offset-*` `outline-*` `cursor-*` `bg-gradient-*` |
|
|
195
|
+
|
|
196
|
+
`ring`/`ring-{n}`/`ring-{color}` is a partial exception — RN has no box-shadow, so it falls back to `borderWidth`/`borderColor`, which *does* affect layout and shares properties with `border-*` (whichever class comes last wins if you combine both).
|
|
197
|
+
|
|
198
|
+
CSS inheritance doesn't exist in React Native — apply font utilities to each `Text`, or define a styled component once: `const Body = styled(Text, 'font-sans text-gray-10 dark:text-white');`.
|
|
199
|
+
|
|
200
|
+
### Expo Web / React Native Web
|
|
201
|
+
|
|
202
|
+
In a browser (Expo Web, Metro web), `@kbach/react` switches to the same CSS-class strategy as plain web automatically:
|
|
203
|
+
|
|
204
|
+
- RN components substitute to HTML: `View`/`ScrollView`→`div`, `Text`→`span`, `TextInput`→`input`/`textarea`, `Image`→`img`, `Pressable`/`TouchableOpacity`→`div[role=button]`
|
|
205
|
+
- RN-only props (`onChangeText`, `source`, `secureTextEntry`, …) map to HTML equivalents
|
|
206
|
+
- Register more: `registerWebElement(Animated.View, 'div')`
|
|
207
|
+
- Recommended: use the Vite plugin same as [Static CSS setup](#static-css-setup) above — `import { kbach } from '@kbach/react/vite'` — and import `kbach.css` in your entry file, for zero runtime cost on the web target too
|
|
208
|
+
- Not using the Vite plugin (the common case for Expo/Metro web, which has no Vite build step)? Render `<KbachReset />` once near your root — e.g. Expo Router's root `app/_layout.tsx`, inside `<ThemeProvider>`:
|
|
209
|
+
|
|
210
|
+
```jsx
|
|
211
|
+
import { KbachReset } from '@kbach/react';
|
|
212
|
+
import { ThemeProvider } from '@kbach/react/native';
|
|
213
|
+
|
|
214
|
+
export default function RootLayout() {
|
|
215
|
+
return (
|
|
216
|
+
<ThemeProvider defaultMode="system">
|
|
217
|
+
<KbachReset />
|
|
218
|
+
<Slot />
|
|
219
|
+
</ThemeProvider>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This ships the base reset as real markup instead of relying solely on the runtime injector.
|
|
225
|
+
|
|
122
226
|
## Dark mode
|
|
123
227
|
|
|
124
228
|
`<ThemeProvider>` powers every `dark:` class — detects OS color scheme, persists the user's choice, re-renders on change.
|
|
@@ -135,7 +239,7 @@ Don't also set up Static CSS above in the same app — pick one.
|
|
|
135
239
|
| Prop | Type | Default | Description |
|
|
136
240
|
|---|---|---|---|
|
|
137
241
|
| `defaultMode` | `'light' \| 'dark' \| 'system'` | `'system'` | Starting mode |
|
|
138
|
-
| `disablePersistence` | `boolean` | `false` | Skip saving to `localStorage` |
|
|
242
|
+
| `disablePersistence` | `boolean` | `false` | Skip saving to `localStorage` (web) / `AsyncStorage` (native) |
|
|
139
243
|
| `config` | `FrameworkConfig` | global config | Scope a different config to this subtree |
|
|
140
244
|
|
|
141
245
|
`darkMode` in `kbach.config.js` picks the matching strategy: `'attribute'` (default), `'class'`, or `'media'` (system-only). Toggle it with `useTheme()`'s `toggle()`/`setMode()` — see [API](#api).
|
|
@@ -364,6 +468,6 @@ Skipping the runtime one is an easy mistake under Static CSS setup specifically
|
|
|
364
468
|
|
|
365
469
|
## Full reference
|
|
366
470
|
|
|
367
|
-
[kbach-react.md](./kbach-react.md) — complete utility list, every modifier, all config options.
|
|
471
|
+
[kbach-react.md](./kbach-react.md) — complete utility list, every modifier, all config options, covers web and React Native/Expo.
|
|
368
472
|
|
|
369
|
-
|
|
473
|
+
`@kbach/native` still exists on npm but is now just a deprecated compatibility shim re-exporting this package — install `@kbach/react` directly for new projects.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -233,7 +233,7 @@ declare function expandModeAwareColorClasses(classString: string, colors: ThemeC
|
|
|
233
233
|
/**
|
|
234
234
|
* Generates the KbachCustomColors/KbachCustomSpacing module-augmentation
|
|
235
235
|
* source for every color/spacing key `extend.theme` added beyond the built-in
|
|
236
|
-
* defaults. Called by both the Vite plugin and
|
|
236
|
+
* defaults. Called by both the Vite plugin and the Babel plugin (@kbach/react/babel-plugin) —
|
|
237
237
|
* kept here, shared, so "which colors count as custom" and "flat string vs.
|
|
238
238
|
* ColorScale" can't drift between the two.
|
|
239
239
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import React__default, { ReactNode, ComponentType, ForwardRefExoticComponent } from 'react';
|
|
2
|
+
import React__default, { Context, ReactNode, ComponentType, ForwardRefExoticComponent } from 'react';
|
|
3
3
|
import * as __core from './core';
|
|
4
4
|
import { ThemeMode, ResolvedConfig, DefaultColorName, defaultColors, ThemeColors, DefaultSpacingKey, ThemeSpacing, FrameworkConfig, StyleValue, ResolvedStyle } from './core';
|
|
5
5
|
export { DefaultColorName, DefaultSpacingKey, FrameworkConfig, ParsedClass, PluginAPI, ResolvedConfig, ResolvedStyle, StyleValue, ThemeConfig, ThemeMode, buildConfig, clearCache, defaultColors, defaultTheme, disableRuntimeCSS, flatten, generateKbachTypesDts, getConfig, initConfig, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, updateConfig } from './core';
|
|
@@ -19,7 +19,10 @@ interface ThemeContextValue {
|
|
|
19
19
|
/** The fully resolved framework config (theme values, darkMode strategy, etc.) */
|
|
20
20
|
config: ResolvedConfig;
|
|
21
21
|
}
|
|
22
|
-
declare
|
|
22
|
+
declare global {
|
|
23
|
+
var __kbachThemeContext: Context<ThemeContextValue | null> | undefined;
|
|
24
|
+
}
|
|
25
|
+
declare const ThemeContext: Context<ThemeContextValue | null>;
|
|
23
26
|
declare function useTheme(): ThemeContextValue;
|
|
24
27
|
declare function useIsDark(): boolean;
|
|
25
28
|
|
|
@@ -134,7 +137,7 @@ interface ThemeProviderProps {
|
|
|
134
137
|
* System color scheme for native `defaultMode="system"`.
|
|
135
138
|
*
|
|
136
139
|
* Pass the value of `useColorScheme()` from `react-native`. When importing
|
|
137
|
-
* `ThemeProvider` from `@kbach/native` this is handled automatically.
|
|
140
|
+
* `ThemeProvider` from `@kbach/react/native` this is handled automatically.
|
|
138
141
|
*
|
|
139
142
|
* @example
|
|
140
143
|
* ```tsx
|
|
@@ -147,7 +150,7 @@ interface ThemeProviderProps {
|
|
|
147
150
|
/**
|
|
148
151
|
* Current window/screen width in pixels for responsive breakpoints.
|
|
149
152
|
* On web this is read from `window.innerWidth` automatically.
|
|
150
|
-
* When importing `ThemeProvider` from `@kbach/native` this is provided
|
|
153
|
+
* When importing `ThemeProvider` from `@kbach/react/native` this is provided
|
|
151
154
|
* automatically from `useWindowDimensions()`.
|
|
152
155
|
*/
|
|
153
156
|
windowWidth?: number;
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
71
71
|
|
|
72
72
|
// src/context.tsx
|
|
73
73
|
var import_react = require("react");
|
|
74
|
-
var ThemeContext = (0, import_react.createContext)(null);
|
|
74
|
+
var ThemeContext = globalThis.__kbachThemeContext ?? (globalThis.__kbachThemeContext = (0, import_react.createContext)(null));
|
|
75
75
|
function useTheme() {
|
|
76
76
|
const ctx = (0, import_react.useContext)(ThemeContext);
|
|
77
77
|
if (!ctx) {
|
package/dist/index.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
|
|
53
53
|
// src/context.tsx
|
|
54
54
|
import { createContext, useContext } from "react";
|
|
55
|
-
var ThemeContext = createContext(null);
|
|
55
|
+
var ThemeContext = globalThis.__kbachThemeContext ?? (globalThis.__kbachThemeContext = createContext(null));
|
|
56
56
|
function useTheme() {
|
|
57
57
|
const ctx = useContext(ThemeContext);
|
|
58
58
|
if (!ctx) {
|
package/dist/native.d.ts
ADDED
|
@@ -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/react/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/react/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/react/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/react/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/react' 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/react" and
|
|
222
|
+
* "@kbach/react/native" consumers — require('@kbach/react') 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/react")
|
|
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/react/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/react/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/react/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/react/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/react/babel',
|
|
301
|
+
* ],
|
|
302
|
+
* };
|
|
303
|
+
* };
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* Do NOT pass `jsxImportSource: '@kbach/react'` 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/react");
|
|
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/react/babel", { configFile, attributes, debug }]]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createKbachConfig(options = {}) {
|
|
82
|
+
return {
|
|
83
|
+
presets: [
|
|
84
|
+
"babel-preset-expo",
|
|
85
|
+
["@kbach/react/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
|
+
});
|