@kbach/react-native 1.0.0-beta.8 → 1.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,37 +2,19 @@
2
2
 
3
3
  Kbach's utility-class styling engine for React Native — the same class
4
4
  names as [`@kbach/react`](https://www.npmjs.com/package/@kbach/react),
5
- resolved to a real `StyleSheet`-ready style object instead of injected CSS.
5
+ resolved to a `StyleSheet`-ready style object instead of injected CSS.
6
6
 
7
- > **Beta.** Native (Android-only, no iOS bridge yet) resolves through a
8
- > Kotlin/JNI TurboModule to an inline style object; Expo Web /
9
- > react-native-web resolves through the same Rust engine compiled to WASM,
10
- > but to real generated CSS instead (full modifier support, including
11
- > `hover:`/`focus:`/etc. — see "Expo Web" below); Expo Go — which can load
12
- > neither a TurboModule nor WASM — resolves through a third, pure-JS
13
- > fallback engine with reduced utility coverage (see "Expo Go" below). The
14
- > native module ships prebuilt (see Setup below) — no Rust toolchain or
15
- > manual native wiring required on any platform.
7
+ > Beta. Native is Android-only (no iOS bridge yet). Ships a prebuilt native
8
+ > module — no Rust toolchain or manual native wiring required.
16
9
 
17
- ## Setup
10
+ ## Install
18
11
 
19
12
  ```sh
20
13
  npm install @kbach/react-native
21
14
  ```
22
15
 
23
- One package, nothing else to install — the Kotlin TurboModule and its
24
- prebuilt native `.so` ship inside this same package's own `android/`
25
- folder. Android's autolinking discovers it automatically the moment it's
26
- in `node_modules`; no manual `MainApplication.kt` edit needed. Works the
27
- same way whether your app was created with the React Native CLI or with
28
- Expo, **including Expo Go** — see its own section below for the scope
29
- tradeoff that involves.
30
-
31
16
  ### React Native CLI
32
17
 
33
- Add the Babel plugin so your app's JSX routes through Kbach's JSX runtime
34
- instead of `react/jsx-runtime`:
35
-
36
18
  ```js
37
19
  // babel.config.js
38
20
  module.exports = {
@@ -41,254 +23,104 @@ module.exports = {
41
23
  };
42
24
  ```
43
25
 
44
- Then rebuild the Android app (`npm run android`, or a clean Gradle build if
45
- you're re-running one that's already built) so Gradle picks up the new
46
- native module. See `apps/native-sandbox` in the
47
- [repo](https://github.com/semsakadanupol/kbach) for a full working
48
- reference project, including the Metro config a monorepo setup like this
49
- one needs (a plain single-app install doesn't need any of that — it's
50
- only relevant if you're linking packages locally instead of installing
51
- from npm).
26
+ Rebuild the Android app afterward so Gradle picks up the native module.
52
27
 
53
28
  ### Expo Go
54
29
 
55
- **Works — no prebuild, no dev client, no extra setup.** Expo Go is a
56
- single fixed binary Expo pre-builds and ships through the app stores,
57
- containing only the native code Expo bundled into it at their own build
58
- time — there is no mechanism for *any* third-party package to add native
59
- code to it at runtime, whether that's a raw TurboModule (what this
60
- package's Android/Expo-dev-client path uses) or even Expo's own Modules
61
- API. So instead of trying to load one, this package detects that the
62
- TurboModule isn't registered (`TurboModuleRegistry.get('KbachModule')`
63
- returns `null` rather than throwing) and transparently falls back to a
64
- **third resolution engine**: a pure-JavaScript reimplementation of the
65
- same class-resolution logic, ported by hand from
66
- [`resolve_style.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolve_style.rs)
67
- and its resolvers (see
68
- [`src/jsEngine`](https://github.com/semsakadanupol/kbach/blob/main/packages/react-native/src/jsEngine)
69
- in the repo) — no WASM (Hermes has no `WebAssembly` API at all) and no
70
- native code, so it can run anywhere plain JS runs, Expo Go included. A
71
- real dev-client or CLI build always has the TurboModule registered, so
72
- this fallback activates ONLY inside Expo Go itself; every other
73
- environment is completely unaffected.
74
-
75
- The tradeoff: the JS engine covers layout, spacing, border/radius,
76
- plain-hex colors (`bg-*`/`text-*`, no opacity composition), font-weight,
77
- text-transform (`uppercase`/`lowercase`/`capitalize`), text-decoration-line
78
- (`underline`/`line-through`/`no-underline`), `italic`/`not-italic`, numeric
79
- line-height and arbitrary letter-spacing, and `shadow-*` — the same
80
- native-representable subset
81
- `resolve_utility_native` (the Rust engine's own native dispatcher) covers,
82
- minus a WASM/JNI dependency. It does **not** cover anything that subset
83
- already excludes on native (grid, transforms, filters, gradients, DOM-only
84
- typography like `truncate`/`decoration-*`/`indent-*`, interactivity/scroll
85
- utilities) — those were never available outside a dev client/CLI build,
86
- Expo Go doesn't change that. `dark:`/`active:`/`sm:`-`2xl:` modifiers work
87
- identically to the native/WASM engines (same gating logic, just inlined
88
- rather than routed through a shared registry). If you hit a class that
89
- silently doesn't apply only in Expo Go, check whether it resolves under a
90
- dev client first — if it does, it's outside this fallback's ported scope,
91
- not a bug in the class itself.
92
-
93
- ### Expo (dev client / prebuild — Android)
94
-
95
- A dev client gets you the *full* native engine (the same one the CLI path
96
- above uses) instead of the JS fallback above — worth it once you need
97
- something outside that fallback's scope. Built once via `expo prebuild`/
98
- `expo run:android`, functionally the same "real native project" as the CLI
99
- path above:
30
+ Works out of the box — no prebuild, no dev client, nothing to configure.
31
+ Expo Go can't load third-party native code, so this falls back to a pure-JS
32
+ engine automatically. Covers layout, spacing, border/radius, plain colors,
33
+ font-weight, text-transform/decoration, line-height, letter-spacing, and
34
+ shadows. Doesn't cover grid, transforms, filters, gradients, or DOM-only
35
+ typography — same scope native itself excludes.
36
+
37
+ ### Expo (dev client / prebuild)
38
+
39
+ Gets you the full native engine instead of the JS fallback:
100
40
 
101
41
  ```js
102
42
  // babel.config.js
103
43
  module.exports = {
104
- presets: ['babel-preset-expo'], // not '@react-native/babel-preset'
44
+ presets: ['babel-preset-expo'],
105
45
  plugins: ['@kbach/react-native/babel-plugin'],
106
46
  };
107
47
  ```
108
48
 
109
49
  ```sh
110
- npx expo prebuild # generates android/ (and ios/, which this package doesn't support yet)
111
- npx expo run:android # builds and installs a dev client with the native module included
50
+ npx expo prebuild
51
+ npx expo run:android
112
52
  ```
113
53
 
114
- Also requires the New Architecture enabled (Expo's default in current SDK
115
- versions) — the native module is a TurboModule, which doesn't exist under
116
- the legacy bridge at all. If you're on an older SDK/config with the New
117
- Architecture off, turn it on in `app.json`
118
- (`"newArchEnabled": true` under `expo`) before prebuilding.
119
-
120
- EAS Build works the same way — it runs the same `prebuild` step, so no
121
- extra configuration beyond the two steps above.
122
-
123
- See `apps/expo-sandbox` in the [repo](https://github.com/semsakadanupol/kbach)
124
- for a full working reference project covering both this section and Expo
125
- Web below — including the `metro.config.js` a monorepo setup like this one
126
- needs (a plain single-app install doesn't need it).
54
+ Requires the New Architecture enabled (`"newArchEnabled": true` in
55
+ `app.json`, on by default in current Expo SDKs). EAS Build works the same
56
+ way, no extra config.
127
57
 
128
58
  ### Expo Web
129
59
 
130
- **Works — no extra setup, no separate package, no init call.** `expo start
131
- --web` (and EAS's web builds) render through `react-native-web` —
132
- `View`/`Text`/etc. become real DOM elements with real CSS, not a
133
- Kotlin/JNI bridge — so on web this package resolves classes through the
134
- same real-CSS engine
135
- [`@kbach/react`](https://www.npmjs.com/package/@kbach/react) uses on the
136
- DOM, not the inline-style-object engine native/Expo Go use. Metro picks
137
- the right implementation per platform on its own via this package's
138
- `"browser"` (client bundles) and `"node"` (Expo Router's static-rendering/
139
- SSR bundles — a genuinely separate resolution axis from `platform: 'web'`,
140
- see `nativeBridge.web.ts`'s own doc comment) export conditions; your app
141
- code and JSX never change. The WASM module initializes itself eagerly
142
- (synchronously, at import time — no `await` needed before your first
143
- render, unlike a from-scratch WASM setup).
144
-
145
- **Full modifier support, including `hover:`/`focus:`/`group-hover:`/
146
- `has-[...]`/container queries/etc.** — every modifier
147
- [`registry.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/registry.rs)
148
- knows, not just the `dark:`/`active:`/responsive subset native/Expo Go
149
- gate via a live JS parameter. This is possible specifically *because* Expo
150
- Web is real DOM: real CSS selectors (`:hover`, `:focus`, `@media`,
151
- `@container`, ...) work exactly the way they do for `@kbach/react`, no
152
- polyfilling needed. The one wrinkle: `react-native-web`'s `View`/`Text`/
153
- `Pressable` don't forward a `className` prop to the DOM at all (checked
154
- against its own `forwardedProps` whitelist), so this can't render a
155
- literal `className` the way `@kbach/react` does on plain DOM. What it uses
156
- instead: RN's `dataSet` prop (which RNW DOES forward, as real `data-*`
157
- attributes) carries the resolved class text into a `data-kb="..."`
158
- attribute, and the generated CSS targets it with `[data-kb~="..."]`
159
- attribute selectors instead of `.class` selectors — functionally
160
- identical to a real `className`, just reached through RN's own prop
161
- system instead. This also means `active:` is now REAL CSS `:active` (the
162
- browser's own press-state detection), not a `Pressable`-specific style-
163
- function simulation the way native's `active:` still is — one fewer
164
- special case, and closer to actual Tailwind's own semantics.
165
-
166
- Everything else about this matches `@kbach/react`'s own web engine
167
- exactly — same
168
- [`resolve_utility`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolvers/mod.rs)
169
- dispatch (not the reduced `resolve_utility_native` subset), so grid,
170
- transforms, filters, gradients, and DOM-only typography (`truncate`,
171
- `decoration-*`, etc.) all resolve on Expo Web even though they don't on
172
- native or Expo Go — see this README's "What this is" section for what's
173
- covered elsewhere.
174
-
175
- #### Expo Router static rendering: `dark:`/responsive on the FIRST paint
176
-
177
- If you use [static rendering](https://docs.expo.dev/router/reference/static-rendering/)
178
- (`web.output: "static"` — Expo Router's SSR/pre-render feature), any
179
- `dark:`/`sm:`/etc. class on content that's part of the **initial,
180
- pre-rendered HTML** resolves against whatever the **server** (Node.js, no
181
- real display/viewport) reports — not the visiting browser's actual OS
182
- preference or window size. React's hydration then adopts that
183
- server-rendered markup as-is rather than silently overwriting it with the
184
- client's own (correct) values, because a `style`-attribute-only difference
185
- doesn't fail hydration the way a text-content mismatch does. Concretely: a
186
- user with their OS in dark mode can see a `dark:` class resolve to its
187
- *light* value on first paint, with no error or warning anywhere.
188
-
189
- **This isn't a Kbach bug** — it's the same fundamental constraint every
190
- OS-preference-based styling system has under SSR (Next.js's own dark-mode
191
- docs describe the identical problem and fix). The standard fix applies
192
- here unchanged: don't resolve preference-dependent classes on
193
- initially-server-rendered content at all; defer them to after your app has
194
- mounted client-side, e.g.:
60
+ Works out of the box — resolves through the same real-CSS engine
61
+ `@kbach/react` uses on the DOM, with full modifier support (`hover:`,
62
+ `focus:`, `group-hover:`, container queries, etc.), not just the
63
+ `dark:`/`active:`/responsive subset native supports.
64
+
65
+ **Static rendering (SSR) caveat:** `dark:`/responsive classes on content
66
+ that's part of the initial pre-rendered HTML resolve against the *server*,
67
+ not the visiting browser — so a user in dark mode can briefly see the light
68
+ version on first paint. Defer preference-dependent classes until mounted:
195
69
 
196
70
  ```tsx
197
- import { useEffect, useState } from 'react';
198
- import { Text, View } from 'react-native';
199
-
200
- function ThemedScreen() {
201
- const [mounted, setMounted] = useState(false);
202
- useEffect(() => setMounted(true), []);
203
-
204
- // Neutral on the server AND on the client's first paint (matches exactly,
205
- // no hydration mismatch) — only becomes preference-aware once mounted.
206
- return (
207
- <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'}>
208
- <Text>Hello</Text>
209
- </View>
210
- );
211
- }
212
- ```
71
+ const [mounted, setMounted] = useState(false);
72
+ useEffect(() => setMounted(true), []);
213
73
 
214
- Content that only ever renders client-side (not part of the statically
215
- pre-rendered route) is unaffected — this only matters for what's visible
216
- before your first `useEffect` runs.
217
-
218
- ## What this is
219
-
220
- - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
221
- `className`/`kb` props on every element and calls into the native, WASM,
222
- or pure-JS bridge (whichever platform you're on — see the Setup section
223
- above), synchronously, on every render. On native and Expo Go this
224
- resolves to an inline `style` object; on Expo Web it resolves to real
225
- injected CSS plus a `dataSet={{ kb: '...' }}` prop instead — see "Expo
226
- Web" below for why the two differ.
227
- - A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
228
- app's own files (never `node_modules`) through that JSX runtime via a
229
- per-file `@jsxImportSource` pragma.
230
- - `setTheme()`/`getTheme()` for customizing the color palette and spacing
231
- scale — the same `ThemeConfig` shape `@kbach/react` uses, kept as an
232
- independent type here so a native-only project isn't forced to depend on
233
- a web-oriented package.
234
- - `shadow-*` resolves to real native shadows — RN's discrete
235
- `shadowColor`/`shadowOffset`/`shadowOpacity`/`shadowRadius` props plus
236
- Android's `elevation` (both emitted together; `elevation` is what
237
- actually renders a shadow on Android — the `shadow*` props are iOS-only
238
- in RN itself, ready for whenever this package gets an iOS bridge),
239
- derived from the same named tiers (`sm`/`DEFAULT`/`md`/`lg`/`xl`/`2xl`)
240
- `@kbach/react`'s CSS `box-shadow` scale uses. `shadow-inner` and
241
- arbitrary `shadow-[...]` values aren't supported — RN's native shadow
242
- props can't express an inset shadow, and an arbitrary value is a raw CSS
243
- string with no mechanical translation into discrete native props.
244
- - Responsive breakpoints (`sm:`/`md:`/`lg:`/`xl:`/`2xl:`) — same
245
- mobile-first "min-width and up" semantics as web, freely combinable with
246
- `dark:`/`active:` (e.g. `dark:md:bg-blue-8`). On native/Expo Go, gated
247
- live against the current window width (`Dimensions.get('window').width`)
248
- — call RN's own `useWindowDimensions()` somewhere in your tree to
249
- re-render on rotation/resize, same role `useColorScheme()` plays for
250
- `dark:` below. On Expo Web this is real `@media (min-width: ...)` CSS
251
- instead (see "Expo Web" above) — the browser evaluates it continuously
252
- on its own, so no `useWindowDimensions()` call is needed there at all.
74
+ <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
75
+ ```
253
76
 
254
77
  ## Dark mode
255
78
 
256
- Works standalone, with zero provider — same architecture as `@kbach/react`:
257
-
258
79
  ```tsx
259
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react-native';
80
+ import { useTheme } from '@kbach/react-native';
260
81
 
261
82
  function ThemeToggle() {
262
- const isDark = useGlobalDarkMode();
263
- return <Pressable onPress={toggleGlobalDarkMode}><Text>{isDark ? 'Dark' : 'Light'}</Text></Pressable>;
83
+ const { mode, isDark, setMode, toggle } = useTheme();
84
+ return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
264
85
  }
265
86
  ```
266
87
 
267
- Or the optional `<ThemeProvider>` / `useTheme()` pair, reading and writing
268
- the exact same global store:
88
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
89
+ optional, for seeding a startup default. Outside React, use
90
+ `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
91
+
92
+ Follows the OS setting live until overridden, and isn't persisted across
93
+ app launches (no built-in storage dependency). To persist, read your own
94
+ storage at startup and call `setGlobalThemeMode()` before your first render.
95
+
96
+ ## Reading colors as values
97
+
98
+ For anywhere you need a real color, not a `className` — a chart prop, a
99
+ native shadow color:
269
100
 
270
101
  ```tsx
271
- import { ThemeProvider, useTheme } from '@kbach/react-native';
102
+ import { useColors } from '@kbach/react-native';
272
103
 
273
- function Toggle() {
274
- const { mode, isDark, setMode, toggle } = useTheme();
275
- return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
276
- }
104
+ const colors = useColors();
105
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
106
+ colors.blue['6/50']; // same, at 50% opacity
107
+ ```
108
+
109
+ Any color can be defined in the theme as a plain string (static) or a
110
+ `{ light, dark }` pair — fully manual, resolved per the current dark-mode
111
+ state:
277
112
 
278
- <ThemeProvider defaultMode="system">
279
- <Toggle />
280
- </ThemeProvider>;
113
+ ```ts
114
+ setTheme({
115
+ ...defaultTheme,
116
+ colors: { ...defaultTheme.colors, brand: { light: '#2563eb', dark: '#93c5fd' } },
117
+ });
281
118
  ```
282
119
 
283
- By default the mode follows the OS `Appearance` setting live until you
284
- explicitly override it — and **isn't persisted across app launches**.
285
- There's no React-Native-builtin equivalent of `localStorage`, and adding
286
- one would mean forcing a real native dependency (e.g.
287
- `@react-native-async-storage/async-storage`) on every consumer, even ones
288
- that don't want it. If you want persistence, read your own storage at
289
- startup and call `setGlobalThemeMode()` with the result before your first
290
- render — the same "seed once" shape `<ThemeProvider defaultMode>` itself
291
- uses internally.
120
+ A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
121
+ dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
122
+ that's the same visual weight relative to its own background, no manual
123
+ `colors.blue[isDark ? 7 : 6]` needed.
292
124
 
293
125
  ## License
294
126
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveStyle
3
- } from "./chunk-JR3BLUAV.mjs";
3
+ } from "./chunk-SY5OAY2C.mjs";
4
4
  import {
5
5
  createJsxFunctions
6
6
  } from "./chunk-DLIXI5OW.mjs";