@kbach/react-native 1.0.0-beta.7 → 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,34 +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; Expo Web / react-native-web resolves through the
9
- > same Rust engine compiled to WASM instead — both call the identical
10
- > resolution logic, so class semantics stay identical everywhere. The
11
- > native module ships prebuilt (see Setup below) — no Rust toolchain or
12
- > 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.
13
9
 
14
- ## Setup
10
+ ## Install
15
11
 
16
12
  ```sh
17
13
  npm install @kbach/react-native
18
14
  ```
19
15
 
20
- One package, nothing else to install — the Kotlin TurboModule and its
21
- prebuilt native `.so` ship inside this same package's own `android/`
22
- folder. Android's autolinking discovers it automatically the moment it's
23
- in `node_modules`; no manual `MainApplication.kt` edit needed. Works the
24
- same way whether your app was created with the React Native CLI or with
25
- Expo — **except Expo Go**, which can never load this (or any) custom
26
- native module; see below.
27
-
28
16
  ### React Native CLI
29
17
 
30
- Add the Babel plugin so your app's JSX routes through Kbach's JSX runtime
31
- instead of `react/jsx-runtime`:
32
-
33
18
  ```js
34
19
  // babel.config.js
35
20
  module.exports = {
@@ -38,188 +23,104 @@ module.exports = {
38
23
  };
39
24
  ```
40
25
 
41
- Then rebuild the Android app (`npm run android`, or a clean Gradle build if
42
- you're re-running one that's already built) so Gradle picks up the new
43
- native module. See `apps/native-sandbox` in the
44
- [repo](https://github.com/semsakadanupol/kbach) for a full working
45
- reference project, including the Metro config a monorepo setup like this
46
- one needs (a plain single-app install doesn't need any of that — it's
47
- only relevant if you're linking packages locally instead of installing
48
- from npm).
49
-
50
- ### Expo (native — Android)
51
-
52
- **Expo Go can never work with this package, and that isn't specific to
53
- Kbach.** Expo Go is a single fixed binary Expo pre-builds and ships
54
- through the app stores, containing only the native code Expo bundled into
55
- it at their own build time — there is no mechanism for *any* third-party
56
- package to add native code to it at runtime, whether that's a raw
57
- TurboModule (what this package uses) or even Expo's own Modules API. This
58
- is exactly why the **dev client** concept exists: a custom build of your
59
- app — built once via `expo prebuild`/`expo run:android`, functionally the
60
- same "real native project" as the CLI path above — that *does* include
61
- this module:
26
+ Rebuild the Android app afterward so Gradle picks up the native module.
27
+
28
+ ### Expo Go
29
+
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:
62
40
 
63
41
  ```js
64
42
  // babel.config.js
65
43
  module.exports = {
66
- presets: ['babel-preset-expo'], // not '@react-native/babel-preset'
44
+ presets: ['babel-preset-expo'],
67
45
  plugins: ['@kbach/react-native/babel-plugin'],
68
46
  };
69
47
  ```
70
48
 
71
49
  ```sh
72
- npx expo prebuild # generates android/ (and ios/, which this package doesn't support yet)
73
- npx expo run:android # builds and installs a dev client with the native module included
50
+ npx expo prebuild
51
+ npx expo run:android
74
52
  ```
75
53
 
76
- Also requires the New Architecture enabled (Expo's default in current SDK
77
- versions) — the native module is a TurboModule, which doesn't exist under
78
- the legacy bridge at all. If you're on an older SDK/config with the New
79
- Architecture off, turn it on in `app.json`
80
- (`"newArchEnabled": true` under `expo`) before prebuilding.
81
-
82
- EAS Build works the same way — it runs the same `prebuild` step, so no
83
- extra configuration beyond the two steps above.
84
-
85
- See `apps/expo-sandbox` in the [repo](https://github.com/semsakadanupol/kbach)
86
- for a full working reference project covering both this section and Expo
87
- Web below — including the `metro.config.js` a monorepo setup like this one
88
- 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.
89
57
 
90
58
  ### Expo Web
91
59
 
92
- **Works — no extra setup, no separate package, no init call.** `expo start
93
- --web` (and EAS's web builds) render through `react-native-web` —
94
- `View`/`Text`/etc. become real DOM elements with real CSS, not a
95
- Kotlin/JNI bridge, so on web this package resolves classes through the
96
- same Rust engine [`@kbach/react`](https://www.npmjs.com/package/@kbach/react)
97
- uses — WASM instead of JNI — automatically. Metro picks the right
98
- implementation per platform on its own via this package's `"browser"`
99
- (client bundles) and `"node"` (Expo Router's static-rendering/SSR bundles
100
- — a genuinely separate resolution axis from `platform: 'web'`, see
101
- `nativeBridge.web.ts`'s own doc comment) export conditions; your app code
102
- and JSX never change. The WASM module initializes itself eagerly
103
- (synchronously, at import time — no `await` needed before your first
104
- render, unlike a from-scratch WASM setup).
105
-
106
- The only thing this doesn't cover on web is an iOS-parity feature gap —
107
- everything web-side resolves through the exact same
108
- [`resolve_style.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolve_style.rs)
109
- logic real native uses, so see this README's "What this is" section above
110
- for what's covered.
111
-
112
- #### Expo Router static rendering: `dark:`/responsive on the FIRST paint
113
-
114
- If you use [static rendering](https://docs.expo.dev/router/reference/static-rendering/)
115
- (`web.output: "static"` — Expo Router's SSR/pre-render feature), any
116
- `dark:`/`sm:`/etc. class on content that's part of the **initial,
117
- pre-rendered HTML** resolves against whatever the **server** (Node.js, no
118
- real display/viewport) reports — not the visiting browser's actual OS
119
- preference or window size. React's hydration then adopts that
120
- server-rendered markup as-is rather than silently overwriting it with the
121
- client's own (correct) values, because a `style`-attribute-only difference
122
- doesn't fail hydration the way a text-content mismatch does. Concretely: a
123
- user with their OS in dark mode can see a `dark:` class resolve to its
124
- *light* value on first paint, with no error or warning anywhere.
125
-
126
- **This isn't a Kbach bug** — it's the same fundamental constraint every
127
- OS-preference-based styling system has under SSR (Next.js's own dark-mode
128
- docs describe the identical problem and fix). The standard fix applies
129
- here unchanged: don't resolve preference-dependent classes on
130
- initially-server-rendered content at all; defer them to after your app has
131
- 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:
132
69
 
133
70
  ```tsx
134
- import { useEffect, useState } from 'react';
135
- import { Text, View } from 'react-native';
136
-
137
- function ThemedScreen() {
138
- const [mounted, setMounted] = useState(false);
139
- useEffect(() => setMounted(true), []);
140
-
141
- // Neutral on the server AND on the client's first paint (matches exactly,
142
- // no hydration mismatch) — only becomes preference-aware once mounted.
143
- return (
144
- <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'}>
145
- <Text>Hello</Text>
146
- </View>
147
- );
148
- }
149
- ```
71
+ const [mounted, setMounted] = useState(false);
72
+ useEffect(() => setMounted(true), []);
150
73
 
151
- Content that only ever renders client-side (not part of the statically
152
- pre-rendered route) is unaffected — this only matters for what's visible
153
- before your first `useEffect` runs.
154
-
155
- ## What this is
156
-
157
- - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
158
- `className`/`kb` props on every element and calls into the native/WASM
159
- bridge (whichever platform you're on) to resolve them to a style object,
160
- synchronously, on every render.
161
- - A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
162
- app's own files (never `node_modules`) through that JSX runtime via a
163
- per-file `@jsxImportSource` pragma.
164
- - `setTheme()`/`getTheme()` for customizing the color palette and spacing
165
- scale — the same `ThemeConfig` shape `@kbach/react` uses, kept as an
166
- independent type here so a native-only project isn't forced to depend on
167
- a web-oriented package.
168
- - `shadow-*` resolves to real native shadows — RN's discrete
169
- `shadowColor`/`shadowOffset`/`shadowOpacity`/`shadowRadius` props plus
170
- Android's `elevation` (both emitted together; `elevation` is what
171
- actually renders a shadow on Android — the `shadow*` props are iOS-only
172
- in RN itself, ready for whenever this package gets an iOS bridge),
173
- derived from the same named tiers (`sm`/`DEFAULT`/`md`/`lg`/`xl`/`2xl`)
174
- `@kbach/react`'s CSS `box-shadow` scale uses. `shadow-inner` and
175
- arbitrary `shadow-[...]` values aren't supported — RN's native shadow
176
- props can't express an inset shadow, and an arbitrary value is a raw CSS
177
- string with no mechanical translation into discrete native props.
178
- - Responsive breakpoints (`sm:`/`md:`/`lg:`/`xl:`/`2xl:`) — gated live
179
- against the current window width (`Dimensions.get('window').width`),
180
- same mobile-first "min-width and up" semantics as web, and freely
181
- combinable with `dark:`/`active:` (e.g. `dark:md:bg-blue-8`). Call RN's
182
- own `useWindowDimensions()` somewhere in your tree to re-render on
183
- rotation/resize — same role `useColorScheme()` plays for `dark:` below.
74
+ <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
75
+ ```
184
76
 
185
77
  ## Dark mode
186
78
 
187
- Works standalone, with zero provider — same architecture as `@kbach/react`:
188
-
189
79
  ```tsx
190
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react-native';
80
+ import { useTheme } from '@kbach/react-native';
191
81
 
192
82
  function ThemeToggle() {
193
- const isDark = useGlobalDarkMode();
194
- 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>;
195
85
  }
196
86
  ```
197
87
 
198
- Or the optional `<ThemeProvider>` / `useTheme()` pair, reading and writing
199
- 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:
200
100
 
201
101
  ```tsx
202
- import { ThemeProvider, useTheme } from '@kbach/react-native';
102
+ import { useColors } from '@kbach/react-native';
203
103
 
204
- function Toggle() {
205
- const { mode, isDark, setMode, toggle } = useTheme();
206
- return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
207
- }
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:
208
112
 
209
- <ThemeProvider defaultMode="system">
210
- <Toggle />
211
- </ThemeProvider>;
113
+ ```ts
114
+ setTheme({
115
+ ...defaultTheme,
116
+ colors: { ...defaultTheme.colors, brand: { light: '#2563eb', dark: '#93c5fd' } },
117
+ });
212
118
  ```
213
119
 
214
- By default the mode follows the OS `Appearance` setting live until you
215
- explicitly override it — and **isn't persisted across app launches**.
216
- There's no React-Native-builtin equivalent of `localStorage`, and adding
217
- one would mean forcing a real native dependency (e.g.
218
- `@react-native-async-storage/async-storage`) on every consumer, even ones
219
- that don't want it. If you want persistence, read your own storage at
220
- startup and call `setGlobalThemeMode()` with the result before your first
221
- render — the same "seed once" shape `<ThemeProvider defaultMode>` itself
222
- 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.
223
124
 
224
125
  ## License
225
126
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveStyle
3
- } from "./chunk-FG5E4NLJ.mjs";
3
+ } from "./chunk-SY5OAY2C.mjs";
4
4
  import {
5
5
  createJsxFunctions
6
6
  } from "./chunk-DLIXI5OW.mjs";