@kbach/react-native 1.0.0-beta.5 → 1.0.0-beta.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.
Files changed (42) hide show
  1. package/AGENTS.md +361 -0
  2. package/README.md +150 -137
  3. package/android/src/main/jniLibs/arm64-v8a/libkbach_core_engine.so +0 -0
  4. package/android/src/main/jniLibs/x86_64/libkbach_core_engine.so +0 -0
  5. package/dist/chunk-CD5WFFIQ.mjs +40 -0
  6. package/dist/chunk-IJAXJVB4.mjs +566 -0
  7. package/dist/chunk-KHQ2BWYH.mjs +112 -0
  8. package/dist/clsx-BLLSnqAa.d.ts +277 -0
  9. package/dist/darkModeStore.d.ts +73 -0
  10. package/dist/darkModeStore.js +118 -0
  11. package/dist/dynamicTokens.d.ts +53 -0
  12. package/dist/dynamicTokens.js +75 -0
  13. package/dist/index.d.ts +92 -151
  14. package/dist/index.js +2119 -393
  15. package/dist/index.web.d.ts +101 -4
  16. package/dist/index.web.js +438 -401
  17. package/dist/index.web.mjs +268 -14
  18. package/dist/jsx-dev-runtime.js +2238 -332
  19. package/dist/jsx-dev-runtime.web.js +207 -361
  20. package/dist/jsx-dev-runtime.web.mjs +19 -7
  21. package/dist/jsx-runtime.js +2238 -332
  22. package/dist/jsx-runtime.web.js +191 -353
  23. package/dist/jsx-runtime.web.mjs +3 -6
  24. package/dist/theme.d.ts +79 -0
  25. package/dist/{chunk-AEVOM2NB.mjs → theme.js} +89 -62
  26. package/package.json +44 -14
  27. package/src/babel-plugin.js +144 -2
  28. package/dist/chunk-4KCCEMEN.mjs +0 -66
  29. package/dist/chunk-DLIXI5OW.mjs +0 -53
  30. package/dist/chunk-FG5E4NLJ.mjs +0 -18
  31. package/dist/chunk-KSDFO34H.mjs +0 -32
  32. package/dist/chunk-OJQAJLYT.mjs +0 -14
  33. package/dist/chunk-ZUE6SCTE.mjs +0 -14
  34. package/dist/index.d.mts +0 -161
  35. package/dist/index.mjs +0 -32
  36. package/dist/index.web.d.mts +0 -7
  37. package/dist/jsx-dev-runtime.d.mts +0 -10
  38. package/dist/jsx-dev-runtime.mjs +0 -17
  39. package/dist/jsx-dev-runtime.web.d.mts +0 -10
  40. package/dist/jsx-runtime.d.mts +0 -7
  41. package/dist/jsx-runtime.mjs +0 -14
  42. package/dist/jsx-runtime.web.d.mts +0 -7
package/AGENTS.md ADDED
@@ -0,0 +1,361 @@
1
+ # `@kbach/react-native` — reference for AI assistants
2
+
3
+ The React Native binding of [Kbach](https://github.com/semsakadanupol/kbach)
4
+ — the same Tailwind-v4-shaped class-name vocabulary as `@kbach/react`,
5
+ resolved to a **`StyleSheet`-ready style object at render time** via a
6
+ shared Rust core (JNI on Android, WASM on Expo Web, a pure-JS fallback on
7
+ Expo Go and iOS). This file covers `@kbach/react-native` only; for the
8
+ cross-package overview, `kbach.config.js` reference, the color palette,
9
+ and how the underlying engine is built, see the repo root's
10
+ [AGENTS.md](https://github.com/semsakadanupol/kbach/blob/main/AGENTS.md).
11
+
12
+ ---
13
+
14
+ ## 1. Install + setup
15
+
16
+ ```sh
17
+ npx @kbach/cli init
18
+ ```
19
+
20
+ Automates everything below — installs `@kbach/react-native`, patches (or
21
+ creates) `babel.config.js` via a format-preserving codemod (handles both
22
+ Expo's function-export form and React Native CLI's plain object-export
23
+ form), and on React Native CLI offers to run the native rebuild right
24
+ there. Safe to re-run at any time (idempotent — reports "already has the
25
+ kbach babel plugin" rather than duplicating the entry). See
26
+ [`packages/cli/AGENTS.md`](../cli/AGENTS.md) for exactly what it detects
27
+ and changes, and what it does when the codemod can't safely apply
28
+ (prints the same manual snippet shown below, rather than guessing).
29
+
30
+ The manual steps below are both the CLI's own fallback and the accurate
31
+ reference for what it actually does — start here if you're wiring this
32
+ up by hand, or the CLI reported it couldn't:
33
+
34
+ ```sh
35
+ npm install @kbach/react-native@beta
36
+ ```
37
+
38
+ The babel plugin is **required on every target, including Expo Go** — it
39
+ injects the per-file `@jsxImportSource @kbach/react-native` pragma
40
+ (babel-plugin.js) that routes `className` through this package's
41
+ jsx-runtime instead of `react/jsx-runtime`; nothing else makes
42
+ interception happen. Confirmed as a real, reported doc bug: the table
43
+ below used to say "None" for Expo Go/Expo Web, and a from-scratch repro
44
+ (fresh Expo project, README's own example, zero babel config, Expo Go on
45
+ a real emulator) showed `className` doing nothing at all — no styling, no
46
+ warning, no error — exactly as this mechanism predicts. What differs per
47
+ target is what happens *after* the plugin is in place:
48
+
49
+ | Target | Setup |
50
+ | :-- | :-- |
51
+ | **Expo Go** | Babel plugin. A pure-JS fallback engine loads automatically after that (reduced utility coverage) — no other setup. |
52
+ | **Expo Web** / `react-native-web` | Babel plugin. Uses the same WASM CSS engine as `@kbach/react` after that — no other setup. |
53
+ | **Expo dev client / prebuild / EAS** | Babel plugin, then `npx expo prebuild && npx expo run:android`. Full native engine. |
54
+ | **React Native CLI** | Babel plugin, then restart Metro with `--reset-cache` and rebuild the Android app. |
55
+
56
+ Babel plugin — added to the existing `babel.config.js` (keep the function
57
+ form Expo generates; only add the `plugins` entry):
58
+
59
+ ```js
60
+ module.exports = function (api) {
61
+ api.cache(true);
62
+ return {
63
+ presets: ['babel-preset-expo'],
64
+ plugins: ['@kbach/react-native/babel-plugin'],
65
+ };
66
+ };
67
+ ```
68
+
69
+ React Native CLI uses `presets: ['module:@react-native/babel-preset']` and
70
+ a plain object export.
71
+
72
+ **Native is Android-only** (no iOS bridge yet) — on iOS, every build
73
+ (dev client, CLI, EAS) runs the same reduced-coverage JS-fallback engine
74
+ as Expo Go, not the full native engine (see §3's engine table).
75
+
76
+ ## 2. What the babel plugin does
77
+
78
+ Per user source file (never `node_modules`, never Metro/Expo virtual
79
+ modules, never `kbach.config.js` itself):
80
+
81
+ 1. Prepends a `// @jsxImportSource @kbach/react-native` comment so the JSX
82
+ transform routes `className` through this package's jsx-runtime.
83
+ 2. Prepends `require('@kbach/react-native').applyKbachConfig(require('<abs
84
+ path>/kbach.config.js'))` if that file exists at the project root — so
85
+ the config is applied before the app's first render with **no import or
86
+ call in app code**.
87
+
88
+ A dev-server restart is required after changing `kbach.config.js` or the
89
+ babel config — for the babel config specifically, a plain restart isn't
90
+ reliably enough (Metro's transform cache doesn't always invalidate on a
91
+ babel config change on its own); use `--reset-cache` (`npx react-native
92
+ start --reset-cache` / `npx expo start --clear`).
93
+
94
+ ## 3. How resolution works
95
+
96
+ `className` on a host element (`View`, `Text`, `Pressable`, …) is
97
+ intercepted by the jsx-runtime and resolved to a style object. One of three
98
+ engines does the work, chosen automatically:
99
+
100
+ | Runtime | Engine | Mechanism |
101
+ | :-- | :-- | :-- |
102
+ | Android dev-client / CLI build | **Rust via JNI** (`KbachModule` TurboModule, prebuilt `.so`) | `resolveStyle(classString, themeJson, colorScheme, pressed, width)` → JSON style object. |
103
+ | Expo Web / `react-native-web` | **Rust via WASM** (`generate_css_attr`) | Emits real CSS rules into a `<style>` tag; element gets `dataSet={{ kb }}` and rules target `[data-kb~="…"]`. Full modifier set. |
104
+ | Expo Go | **Pure-JS fallback** (`jsEngine/`) | A partial TS port of the native resolver. No custom native module can load in Expo Go. |
105
+ | iOS — ANY build (dev-client, CLI, EAS) | **Pure-JS fallback** (`jsEngine/`) | Same reduced-coverage path as Expo Go, since there's no iOS native module at all yet (`TurboModuleRegistry.get('KbachModule')` returns `null` on iOS, so `resolveStyle` falls through to the JS engine — see nativeBridge.ts). Not specific to Expo Go; a real iOS dev-client/CLI build is on this row too. |
106
+
107
+ Resolution is memoized per `(classString, pressed, colorScheme, width,
108
+ theme)`.
109
+
110
+ ## 4. Modifiers on native
111
+
112
+ Native has no selector/cascade engine. Two groups:
113
+
114
+ **Resolved in the engine from live parameters:**
115
+ - `dark:` — current OS/app color scheme.
116
+ - `sm:` `md:` `lg:` `xl:` `2xl:` — window width vs `theme.screens`.
117
+ - `min-[500px]:` / `max-[30rem]:` — arbitrary width breakpoints (constant
118
+ px/rem only).
119
+ - `active:` — `Pressable` press state (via its style-callback).
120
+
121
+ **Resolved in `jsxRuntimeCore.ts` from React props, before the engine sees
122
+ the string:**
123
+ - `hover:` — `onHoverIn`/`onHoverOut` — only fires for pointer-capable
124
+ input (mouse/trackpad); inert on pure touch.
125
+ - `focus:` — `onFocus`/`onBlur`.
126
+ - `disabled:` — the element's own `disabled` prop.
127
+ - `data-[key=value]:` / `data-[key]:` / `aria-[…]:` / static `aria-*`
128
+ shortcuts — the element's own `data-*` / `aria-*` props.
129
+
130
+ A satisfied prop-based modifier is rewritten to an internal `_kbon` marker
131
+ (one per satisfied modifier) so it still contributes to specificity.
132
+
133
+ **Prop-based modifiers never inherit from an ancestor.** `disabled:` /
134
+ `aria-[…]:` / `data-[…]:` read `hostRest` off THAT exact element's own
135
+ props (`propBasedModifierState` in jsxRuntimeCore.ts) — there is no
136
+ `group-disabled:`-style cascade on native to inherit one through. Confirmed
137
+ as a real footgun (reported after two independent hits building a demo
138
+ app): `<Pressable disabled><Text className="disabled:text-gray-5">` does
139
+ nothing at all, silently — `Text` never received a `disabled` prop, so
140
+ `propBasedModifierState` returns `null` for it (no opinion), the modifier
141
+ is treated as never-satisfied, and nothing warns. The prop has to be
142
+ repeated on whichever element actually carries the modifier:
143
+ `<Pressable disabled><Text disabled className="disabled:text-gray-5">`.
144
+
145
+ **Not supported** (parse without error, never apply): `group-*`, `peer-*`,
146
+ `has-[…]`, container queries, `*:` / `**:`, `before:` / `after:` and other
147
+ pseudo-elements.
148
+
149
+ **Specificity / class order.** When two classes set the same property, the
150
+ one with more satisfied modifiers wins **regardless of source order** —
151
+ `dark:bg-black bg-white` and `bg-white dark:bg-black` both resolve to black
152
+ in dark mode, matching web. Equal specificity falls back to last-token-wins.
153
+ `transform-none` is an exception: it clears all transforms above it in
154
+ source order regardless of specificity.
155
+
156
+ Chaining composes and is order-independent (`dark:sm:` ≡ `sm:dark:`); every
157
+ named condition must hold.
158
+
159
+ ## 5. Expo Go coverage gap
160
+
161
+ The JS fallback covers: layout, spacing, border/radius, plain colors,
162
+ font-weight, text-transform/decoration, line-height, letter-spacing,
163
+ shadows, and the RN-representable transforms (`translate-x/y`,
164
+ `scale`/`scale-x/y`, `rotate`/`rotate-x/y/z`, `skew-x/y`, `transform-none`,
165
+ `backface-*`).
166
+
167
+ Not covered: grid, filters, gradients, DOM-only typography, and transforms
168
+ with no RN equivalent (`translate-z`, `scale-z`, `perspective*`, `origin`,
169
+ `transform-gpu/cpu`). Same scope native itself excludes. Typo warnings are
170
+ also unavailable in Expo Go (no full-vocabulary resolver to check against).
171
+
172
+ This is also exactly what iOS gets in every build type (§3) — the coverage
173
+ gap isn't Expo-Go-specific, it's "whatever the JS-fallback engine covers."
174
+
175
+ ## 6. Dark mode
176
+
177
+ Zero setup on all targets. On native `dark:` is resolved from a live
178
+ parameter every render — no build step to keep in sync. Expo Web defaults
179
+ to the `'attribute'` strategy.
180
+
181
+ ```tsx
182
+ import { useTheme } from '@kbach/react-native';
183
+ const { mode, isDark, setMode, toggle } = useTheme();
184
+ ```
185
+
186
+ No provider required. `<ThemeProvider>` optionally seeds a startup
187
+ `defaultMode` and enables persistence:
188
+
189
+ ```tsx
190
+ import AsyncStorage from '@react-native-async-storage/async-storage';
191
+ <ThemeProvider persist={AsyncStorage}><App /></ThemeProvider>;
192
+ ```
193
+
194
+ Mount at most one — dark mode is one global store, not scoped per
195
+ provider, so a second mounted instance logs a dev-only warning (they'd
196
+ fight over the same `defaultMode`/`persist` seed).
197
+
198
+ `persist` takes any `{ getItem, setItem }` async store. The persisted mode
199
+ is read once on mount and wins over `defaultMode`; later changes are
200
+ written back. This package never imports a storage library itself.
201
+
202
+ Outside React: `getGlobalDarkMode()` / `toggleGlobalDarkMode()` /
203
+ `setGlobalThemeMode()`.
204
+
205
+ ## 7. `calc()` / `min()` / `max()` / `clamp()`
206
+
207
+ - **Constant** px/rem/bare-number expressions are reduced at resolve time:
208
+ `p-[calc(16px+8px)]` → `padding: 24`. No spaces or underscores are
209
+ required around operators; `calc(16px+8px)` is fine.
210
+ - **Viewport-relative** (`calc(10vw+8px)`): no such unit on native — the
211
+ declaration is dropped and a dev-only `console.warn` explains why.
212
+ - **Percentage-relative `width`/`height`** (`w-[calc(100%-3rem)]`,
213
+ `w-[min(50%,20rem)]`, `w-[clamp(16rem,50%,32rem)]`): supported via a
214
+ one-frame measure-then-snap — renders at `100%`, measures via
215
+ `onLayout`, then corrects to the computed number; re-measures on layout
216
+ changes. Narrow shapes only: `calc()` = one percent term + one constant
217
+ term; `min`/`max` args each a plain percent OR a constant expression (not
218
+ mixed); `clamp` exactly three args; `width`/`height` only.
219
+ - **Literal spaces are illegal inside `[...]`** on any Tailwind-family
220
+ engine (a space splits the class token). Use underscores if you want them
221
+ for readability: `w-[calc(100%_-_3rem)]`.
222
+
223
+ ## 8. Dynamic tokens (native counterpart to CSS custom properties)
224
+
225
+ ```tsx
226
+ import { setDynamicToken, useDynamicToken } from '@kbach/react-native';
227
+
228
+ setDynamicToken('sidebar-width', '240px');
229
+ <View className="w-[var(--sidebar-width)]" />;
230
+ const w = useDynamicToken('sidebar-width'); // '240px' | undefined, reactive
231
+ ```
232
+
233
+ On web `setDynamicToken` writes a real CSS custom property (browser
234
+ repaints, no re-render). On native the token value is substituted into the
235
+ class string before each resolve, and subscribed elements re-render on
236
+ change. `var(--x)` in a `className` already works unaided on Expo Web.
237
+
238
+ **Call `setDynamicToken` synchronously from a render body, not from module
239
+ top-level.** It calls `notify()` synchronously (dynamicTokens.ts), which
240
+ synchronously re-renders every already-mounted `ReactiveElement` subscribed
241
+ via `useSyncExternalStore` — safe when nothing is subscribed yet, a real
242
+ hazard once something is. Module-top-level code runs whenever Metro's
243
+ inline-requires first evaluates that module, which is NOT a fixed point in
244
+ the app's lifecycle — it can land in the middle of React rendering some
245
+ unrelated component elsewhere in the tree, which is exactly the "Cannot
246
+ update a component while rendering a different component" class of bug:
247
+
248
+ ```tsx
249
+ // Bad — races Metro inline-requires; can fire mid-render of an unrelated component.
250
+ setDynamicToken('accent', '#8b5cf6');
251
+
252
+ // Good — call synchronously in your ROOT component's own render, before any
253
+ // consumer mounts; guard so it only ever fires once (unguarded, every
254
+ // re-render would call notify() again, wastefully re-rendering every
255
+ // subscribed element for no value change).
256
+ function RootLayout({ children }) {
257
+ if (getDynamicToken('accent') === undefined) {
258
+ setDynamicToken('accent', '#8b5cf6');
259
+ }
260
+ return <ThemeProvider>{children}</ThemeProvider>;
261
+ }
262
+ ```
263
+
264
+ ## 9. Reading colors as values
265
+
266
+ ```tsx
267
+ import { useColors } from '@kbach/react-native';
268
+
269
+ const colors = useColors();
270
+ colors.blue[6]; // '#2563eb'
271
+ colors.blue['6/50']; // 50% opacity
272
+ colors.alpha(colors.blue[6], 30);
273
+ colors.get('brand'); // a custom color, typed as plain `string`
274
+ ```
275
+
276
+ `colors.blue[6]` returns the same value in both light and dark mode
277
+ unless `blue-6` itself is defined in the theme as a `{ light, dark }` pair
278
+ (§5.1 of the root AGENTS.md) — define a color as mode-aware in
279
+ `kbach.config.js` if you want it to differ by mode.
280
+
281
+ `colors.brand` (a custom flat color) is typed `string | ColorScale`
282
+ because the same object also serves shade families and TS can't tell them
283
+ apart statically — use `colors.get(name)` when you need a guaranteed
284
+ `string`.
285
+
286
+ ## 10. Unknown-class warnings
287
+
288
+ On a native dev-client/CLI build, a class matching no Kbach utility on any
289
+ platform emits a one-time `console.warn` (dev only). A real utility native
290
+ just doesn't support yet (`grid-cols-3`, `blur`, …) stays silent — that's a
291
+ documented platform gap, not a typo. Not available in Expo Go.
292
+
293
+ A malformed or misused value fails the same safe way — a deduped, dev-only
294
+ `console.warn` naming the offending class and the fix, never a crash or
295
+ silently-wrong output. Confirmed across CSS-injection attempts in an
296
+ arbitrary value, invalid `calc()` operands, and double-mounted providers.
297
+
298
+ ## 11. Monorepo / workspace installs
299
+
300
+ If `@kbach/react-native` is a workspace/hoisted dependency, Metro can end
301
+ up with **two physical copies of `react-native` / `react`** — different
302
+ files resolve to different module instances, and a dark-mode toggle can
303
+ silently do nothing. Force a single instance in your app's
304
+ `metro.config.js` via `resolver.resolveRequest`, pointing `react-native`
305
+ and `react` at `projectRoot/node_modules/...`. A plain non-monorepo
306
+ `npm install` never hits this.
307
+
308
+ ## 12. Exports
309
+
310
+ `setTheme`, `getTheme`, `defaultTheme`, `resolveStyle`, `resolveKbachConfig`,
311
+ `applyKbachConfig`, `useTheme`, `ThemeProvider`, `useColors`,
312
+ `setDynamicToken`, `getDynamicToken`, `deleteDynamicToken`,
313
+ `subscribeDynamicTokens`, `useDynamicToken`, `clsx`, `getGlobalDarkMode`,
314
+ `getGlobalThemeMode`, `setGlobalThemeMode`, `toggleGlobalDarkMode`,
315
+ `subscribeGlobalDarkMode`. Types: `ThemeConfig`, `ColorEntry`,
316
+ `ContainerConfig`, `StyleObject`, `KbachConfig`, `ThemeState`,
317
+ `ThemeProviderProps`, `ThemeStorageAdapter`, `ThemeMode`, `ColorsAPI`,
318
+ `ColorScale`, `ClassValue`.
319
+
320
+ Subpath exports: `@kbach/react-native/babel-plugin`,
321
+ `@kbach/react-native/jsx-runtime`, `@kbach/react-native/jsx-dev-runtime`.
322
+
323
+ ## 13. Common failure modes
324
+
325
+ - **`bg-<custom>` renders nothing** — the color is mode-aware
326
+ (`{light,dark}` / `dark:` block) and the app is on a stale
327
+ `@kbach/react-native` (`< 1.0.0-beta.36`). Update.
328
+ - **A `dark:` variant loses to a plain class** — stale
329
+ `@kbach/react-native` (`< 1.0.0-beta.37`); class-order independence
330
+ landed there. Update.
331
+ - **`useColors().brand` type error** — assign via `colors.get('brand')`
332
+ (`< beta.36` has no `.get`).
333
+ - **Dark toggle does nothing in a monorepo** — duplicate `react-native` /
334
+ `react` instances; add the `metro.config.js` `resolveRequest` override
335
+ (§11).
336
+ - **Rust change not reflected on a real Android build** — stale prebuilt
337
+ `.so`; the maintainer must re-run `build:android` and republish.
338
+ - **`kbach.config.js` "does nothing" / Metro bundling error** — an
339
+ unquoted hyphenated key (`surface-dim:` instead of `'surface-dim':`).
340
+ Quote it.
341
+ - **TypeScript error on `className`** (`Property 'className' does not
342
+ exist on type ... ViewProps ...`) — fixed in `>= 1.0.0-beta.46`; update.
343
+ - **`className` does absolutely nothing — no styling, no warning, no
344
+ error, on any target including Expo Go** — the babel plugin (§1) is
345
+ missing. It's required on every target, not just Android/dev-client
346
+ builds; without it nothing routes `className` through this package's
347
+ jsx-runtime at all, so RN just silently ignores the unrecognized prop.
348
+ - **`ERROR [Error: Unable to find node on an unmounted component.]` on
349
+ Expo Fast Refresh, especially right after editing `kbach.config.js`** —
350
+ stale `@kbach/react-native` (`< 1.0.0-beta.51`). Root cause: the babel
351
+ plugin (§1) injects `applyKbachConfig(require('kbach.config.js'))` into
352
+ *every* user file, so editing that config forces Metro to re-execute the
353
+ entire app (every file requires it), not just the screen you're on;
354
+ before beta.51, `setTheme` (`theme.ts`) handed every one of those
355
+ redundant re-applications a brand-new theme object even when the config
356
+ content hadn't changed, invalidating every `useColors()` memo and
357
+ reactive-element check across the whole tree simultaneously, right as
358
+ Fast Refresh was already mid-teardown — worsening this normally-rare
359
+ React Native Fast Refresh race. `setTheme` is idempotent by content
360
+ since beta.51 (compares against the theme's own JSON serialization
361
+ before replacing it); update.