@kbach/react-native 1.0.0-beta.2 → 1.0.0-beta.21

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/README.md +253 -87
  2. package/android/build.gradle +36 -0
  3. package/android/src/main/AndroidManifest.xml +14 -0
  4. package/android/src/main/java/com/kbachnative/KbachModule.kt +83 -0
  5. package/android/src/main/java/com/kbachnative/KbachPackage.kt +54 -0
  6. package/android/src/main/jniLibs/arm64-v8a/libkbach_core_engine.so +0 -0
  7. package/android/src/main/jniLibs/x86_64/libkbach_core_engine.so +0 -0
  8. package/dist/chunk-AG7ZQ2K4.mjs +552 -0
  9. package/dist/chunk-OS7EPBH5.mjs +40 -0
  10. package/dist/chunk-TS2EEVOE.mjs +107 -0
  11. package/dist/darkModeStore.d.ts +58 -0
  12. package/dist/darkModeStore.js +112 -0
  13. package/dist/dynamicTokens.d.ts +36 -0
  14. package/dist/dynamicTokens.js +75 -0
  15. package/dist/index.d.ts +53 -102
  16. package/dist/index.js +1721 -385
  17. package/dist/index.web.d.ts +18 -0
  18. package/dist/index.web.js +436 -0
  19. package/dist/index.web.mjs +210 -0
  20. package/dist/jsx-dev-runtime.d.ts +3 -12
  21. package/dist/jsx-dev-runtime.js +1738 -343
  22. package/dist/jsx-dev-runtime.web.d.ts +10 -0
  23. package/dist/jsx-dev-runtime.web.js +270 -0
  24. package/dist/jsx-dev-runtime.web.mjs +29 -0
  25. package/dist/jsx-runtime.d.ts +3 -23
  26. package/dist/jsx-runtime.js +1729 -335
  27. package/dist/jsx-runtime.web.d.ts +7 -0
  28. package/dist/jsx-runtime.web.js +252 -0
  29. package/dist/{jsx-runtime.mjs → jsx-runtime.web.mjs} +2 -2
  30. package/dist/theme.d.ts +54 -0
  31. package/dist/{chunk-YHF3QLRL.mjs → theme.js} +94 -72
  32. package/dist/useDynamicToken-Dr15c88t.d.ts +160 -0
  33. package/package.json +62 -14
  34. package/react-native.config.js +29 -0
  35. package/specs/NativeKbachModule.ts +31 -0
  36. package/src/babel-plugin.js +74 -2
  37. package/dist/chunk-SVMYVXY6.mjs +0 -44
  38. package/dist/index.d.mts +0 -148
  39. package/dist/index.mjs +0 -80
  40. package/dist/jsx-dev-runtime.d.mts +0 -19
  41. package/dist/jsx-dev-runtime.mjs +0 -20
  42. package/dist/jsx-runtime.d.mts +0 -27
package/README.md CHANGED
@@ -2,33 +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, Android-only.** Resolution runs through a Kotlin/JNI TurboModule
8
- > calling the same Rust core `@kbach/react` uses, so class semantics stay
9
- > identical across web and native. The native module ships prebuilt (see
10
- > Setup below) — no Rust toolchain or manual native wiring required — but
11
- > there's no iOS bridge yet.
7
+ > Beta. Native is Android-only (no iOS bridge yet). Ships a prebuilt native
8
+ > module — no Rust toolchain or manual native wiring required.
12
9
 
13
- ## Setup
10
+ ## Install
14
11
 
15
12
  ```sh
16
13
  npm install @kbach/react-native
17
14
  ```
18
15
 
19
- That pulls in [`@kbach/android`](https://www.npmjs.com/package/@kbach/android)
20
- too — the actual native module — as a real dependency. Android's
21
- autolinking discovers it automatically the moment it's in `node_modules`;
22
- no manual `MainApplication.kt` edit needed. Works the same way whether
23
- your app was created with the React Native CLI or with Expo — **except
24
- Expo Go**, which can never load this (or any) custom native module; see
25
- below.
26
-
27
16
  ### React Native CLI
28
17
 
29
- Add the Babel plugin so your app's JSX routes through Kbach's JSX runtime
30
- instead of `react/jsx-runtime`:
31
-
32
18
  ```js
33
19
  // babel.config.js
34
20
  module.exports = {
@@ -37,104 +23,284 @@ module.exports = {
37
23
  };
38
24
  ```
39
25
 
40
- Then rebuild the Android app (`npm run android`, or a clean Gradle build if
41
- you're re-running one that's already built) so Gradle picks up the new
42
- native module. See `apps/native-sandbox` in the
43
- [repo](https://github.com/semsakadanupol/kbach) for a full working
44
- reference project, including the Metro config a monorepo setup like this
45
- one needs (a plain single-app install doesn't need any of that — it's
46
- only relevant if you're linking packages locally instead of installing
47
- from npm).
26
+ Rebuild the Android app afterward so Gradle picks up the native module.
27
+
28
+ ### Expo Go
48
29
 
49
- ### Expo
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.
50
36
 
51
- **Expo Go doesn't work** — it's a fixed pre-built binary with no way to
52
- load a custom native module like `@kbach/android`. You need a **dev
53
- client** instead (a custom build of your app with this module baked in) —
54
- functionally the same "real native project" setup as the CLI path above,
55
- just generated and driven through Expo's own tooling:
37
+ ### Expo (dev client / prebuild)
38
+
39
+ Gets you the full native engine instead of the JS fallback:
56
40
 
57
41
  ```js
58
42
  // babel.config.js
59
43
  module.exports = {
60
- presets: ['babel-preset-expo'], // not '@react-native/babel-preset'
44
+ presets: ['babel-preset-expo'],
61
45
  plugins: ['@kbach/react-native/babel-plugin'],
62
46
  };
63
47
  ```
64
48
 
65
49
  ```sh
66
- npx expo prebuild # generates android/ (and ios/, which this package doesn't support yet)
67
- npx expo run:android # builds and installs a dev client with the native module included
50
+ npx expo prebuild
51
+ npx expo run:android
68
52
  ```
69
53
 
70
- Also requires the New Architecture enabled (Expo's default in current SDK
71
- versions) — `@kbach/android` is a TurboModule, which doesn't exist under
72
- the legacy bridge at all. If you're on an older SDK/config with the New
73
- Architecture off, turn it on in `app.json`
74
- (`"newArchEnabled": true` under `expo`) before prebuilding.
75
-
76
- EAS Build works the same way — it runs the same `prebuild` step, so no
77
- extra configuration beyond the two steps above.
78
-
79
- ## What this is
80
-
81
- - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
82
- `className`/`kb` props on every element and calls into the native bridge
83
- to resolve them to a style object, synchronously, on every render.
84
- - A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
85
- app's own files (never `node_modules`) through that JSX runtime via a
86
- per-file `@jsxImportSource` pragma.
87
- - `setTheme()`/`getTheme()` for customizing the color palette and spacing
88
- scale — the same `ThemeConfig` shape `@kbach/react` uses, kept as an
89
- independent type here so a native-only project isn't forced to depend on
90
- a web-oriented package.
91
- - `shadow-*` resolves to real native shadows — iOS's discrete
92
- `shadowColor`/`shadowOffset`/`shadowOpacity`/`shadowRadius` and Android's
93
- `elevation`, both derived from the same named tiers (`sm`/`DEFAULT`/`md`/
94
- `lg`/`xl`/`2xl`) `@kbach/react`'s CSS `box-shadow` scale uses — one class
95
- produces a real shadow on both platforms. `shadow-inner` and arbitrary
96
- `shadow-[...]` values aren't supported — neither platform's native shadow
97
- API can express an inset shadow, and an arbitrary value is a raw CSS
98
- string with no mechanical translation into discrete native props.
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.
99
57
 
100
- ## Dark mode
58
+ ### Expo Web
101
59
 
102
- Works standalone, with zero provider — same architecture as `@kbach/react`:
60
+ Works out of the box — resolves through the same real-CSS engine
61
+ `@kbach/react` uses on the DOM, with the FULL modifier set (`group-hover:`,
62
+ `peer-hover:`, container queries, `has-[...]:`, etc.) — everything real
63
+ Tailwind supports. Native supports a smaller, explicitly-scoped subset; see
64
+ "Modifiers on native" below for exactly what that covers.
65
+
66
+ **Static rendering (SSR) caveat:** `dark:`/responsive classes on content
67
+ that's part of the initial pre-rendered HTML resolve against the *server*,
68
+ not the visiting browser — so a user in dark mode can briefly see the light
69
+ version on first paint. Defer preference-dependent classes until mounted:
103
70
 
104
71
  ```tsx
105
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react-native';
72
+ const [mounted, setMounted] = useState(false);
73
+ useEffect(() => setMounted(true), []);
106
74
 
107
- function ThemeToggle() {
108
- const isDark = useGlobalDarkMode();
109
- return <Pressable onPress={toggleGlobalDarkMode}><Text>{isDark ? 'Dark' : 'Light'}</Text></Pressable>;
110
- }
75
+ <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
76
+ ```
77
+
78
+ ### Monorepo installs (npm/yarn/pnpm workspaces)
79
+
80
+ If your app lives in a monorepo where `@kbach/react-native` is a workspace
81
+ package (a `file:`/`link:` dependency, or hoisted rather than nested), watch
82
+ for **two physical copies of `react-native`/`react`** ending up in your
83
+ dependency tree — one this package would otherwise resolve up to from its
84
+ own directory, and a different one your app's own files resolve to. Metro
85
+ walks `node_modules` hierarchically per-file, so it can silently hand two
86
+ different files two different copies of the *same* module, each with its
87
+ own separate module state.
88
+
89
+ This isn't hypothetical: it's the root cause of a real dark-mode bug this
90
+ package shipped and fixed — `useColorScheme()`/`Appearance` in one copy
91
+ never saw changes made through the other, so an explicit theme toggle
92
+ appeared to do nothing. The fix is a Metro config override that forces a
93
+ single instance, **required in your app**, not something this package can
94
+ do on your behalf from inside `node_modules`:
95
+
96
+ ```js
97
+ // metro.config.js
98
+ const path = require('path');
99
+ // React Native CLI: const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
100
+ // Expo: const { getDefaultConfig } = require('expo/metro-config');
101
+
102
+ const projectRoot = __dirname;
103
+ const workspaceRoot = path.resolve(projectRoot, '../..'); // -> your monorepo root
104
+
105
+ const config = getDefaultConfig(projectRoot);
106
+ config.watchFolders = [workspaceRoot];
107
+
108
+ const forcedSingleInstance = {
109
+ 'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
110
+ react: path.resolve(projectRoot, 'node_modules/react'),
111
+ };
112
+ const defaultResolveRequest = config.resolver.resolveRequest;
113
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
114
+ if (Object.prototype.hasOwnProperty.call(forcedSingleInstance, moduleName)) {
115
+ return { type: 'sourceFile', filePath: require.resolve(forcedSingleInstance[moduleName]) };
116
+ }
117
+ return defaultResolveRequest
118
+ ? defaultResolveRequest(context, moduleName, platform)
119
+ : context.resolveRequest(context, moduleName, platform);
120
+ };
121
+
122
+ module.exports = config; // (React Native CLI: mergeConfig(getDefaultConfig(projectRoot), config))
111
123
  ```
112
124
 
113
- Or the optional `<ThemeProvider>` / `useTheme()` pair, reading and writing
114
- the exact same global store:
125
+ A single, non-monorepo `npm install` never hits this — only relevant if your
126
+ app and `@kbach/react-native` share a workspace root.
127
+
128
+ ## Modifiers on native
129
+
130
+ Native has no CSS selector/cascade engine, so only a fixed, explicitly
131
+ supported set of modifiers actually applies — everything else (`group-*:`,
132
+ `peer-*:`, `has-[...]:`, `aria-*:`, `data-*:`, container queries, ...)
133
+ parses without error but never takes effect. Supported today:
134
+
135
+ - **`dark:`** — reacts to the OS/app color scheme, see "Dark mode" below.
136
+ - **`sm:`/`md:`/`lg:`/`xl:`/`2xl:`** — reacts to `useWindowDimensions()`
137
+ crossing the matching `theme.screens` breakpoint (rotation/resize/fold).
138
+ - **`active:`** — `Pressable`'s own press state, via its style-callback API.
139
+ - **`hover:`** — tracked via `onHoverIn`/`onHoverOut`, which RN only ever
140
+ fires on a platform with real pointer input (iOS 13+/Android/macOS/
141
+ Windows via a mouse/trackpad/Apple Pencil hover) — inert (never fires) on
142
+ a touch-only device, the same way real Tailwind's `hover:` already
143
+ degrades on a touch-only *browser*. Composing it onto any host type is
144
+ harmless even where it never fires.
145
+ - **`focus:`** — tracked via `onFocus`/`onBlur` (keyboard/external-keyboard/
146
+ TV-remote/tab navigation, and `TextInput` focus).
147
+ - **`disabled:`** — reads the element's own `disabled` prop directly; no
148
+ event tracking needed, since a prop change already re-renders normally.
149
+ - Any combination of the above chained on one class (`sm:dark:bg-blue-8`,
150
+ `hover:focus:opacity-100`) requires every named condition to hold at once
151
+ — chain order never matters (`dark:sm:` and `sm:dark:` are equivalent).
152
+
153
+ `group-hover:`/`peer-hover:` and friends are NOT implemented on native —
154
+ doing so needs a React Context bridge (propagating an ancestor's hover/
155
+ press/focus state down to descendants, since there's no `.group`/`.peer`
156
+ selector relationship to lean on) that doesn't exist today. If you need
157
+ group/peer-style coordination on native right now, lift the shared state
158
+ into a parent component yourself and pass it down as an explicit prop.
159
+
160
+ ## Dark mode
115
161
 
116
162
  ```tsx
117
- import { ThemeProvider, useTheme } from '@kbach/react-native';
163
+ import { useTheme } from '@kbach/react-native';
118
164
 
119
- function Toggle() {
165
+ function ThemeToggle() {
120
166
  const { mode, isDark, setMode, toggle } = useTheme();
121
167
  return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
122
168
  }
169
+ ```
170
+
171
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
172
+ optional, for seeding a startup default. Outside React, use
173
+ `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
174
+
175
+ Follows the OS setting live until overridden, and isn't persisted across
176
+ app launches (no built-in storage dependency). To persist, read your own
177
+ storage at startup and call `setGlobalThemeMode()` before your first render.
178
+
179
+ ## Reading colors as values
180
+
181
+ For anywhere you need a real color, not a `className` — a chart prop, a
182
+ native shadow color:
183
+
184
+ ```tsx
185
+ import { useColors } from '@kbach/react-native';
123
186
 
124
- <ThemeProvider defaultMode="system">
125
- <Toggle />
126
- </ThemeProvider>;
187
+ const colors = useColors();
188
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
189
+ colors.blue['6/50']; // same, at 50% opacity
190
+ ```
191
+
192
+ Any color can be defined in the theme as a plain string (static) or a
193
+ `{ light, dark }` pair — fully manual, resolved per the current dark-mode
194
+ state:
195
+
196
+ ```ts
197
+ setTheme({
198
+ ...defaultTheme,
199
+ colors: { ...defaultTheme.colors, brand: { light: '#2563eb', dark: '#93c5fd' } },
200
+ });
201
+ ```
202
+
203
+ A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
204
+ dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
205
+ that's the same visual weight relative to its own background, no manual
206
+ `colors.blue[isDark ? 7 : 6]` needed.
207
+
208
+ ## Arbitrary math: `calc()`, `clamp()`, `min()`, `max()`
209
+
210
+ ```tsx
211
+ <View className="p-[calc(16px+8px)]" /> // -> padding: 24
212
+ <View className="w-[clamp(1rem,2rem,3rem)]" /> // -> width: 32
213
+ ```
214
+
215
+ Resolves at **build/resolve time**, not runtime — every operand must be a
216
+ constant `px`/`rem` length (or a bare number). This works because native has
217
+ no CSS engine to evaluate a real `calc()` string at paint time, so the only
218
+ way to support it at all is to compute the answer ourselves.
219
+
220
+ A **viewport-relative** expression (`calc(10vw+8px)`) can't be reduced this
221
+ way — no such unit exists on native at all. It already works fine on web
222
+ (real CSS); on native the declaration is dropped and a `console.warn`
223
+ explains why (dev builds only).
224
+
225
+ **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`)
226
+ IS supported, differently: since native has no way to know "100% of what"
227
+ ahead of time, this measures the element's own real layout instead —
228
+
229
+ ```tsx
230
+ <View className="w-[calc(100%_-_3rem)]" />
231
+ ```
232
+
233
+ Renders once at a plain `100%` (a real RN percentage, resolved correctly by
234
+ RN's own layout engine against the parent) purely to trigger a layout
235
+ event, then immediately corrects to the actual computed number once
236
+ measured — a genuine one-frame measure-then-snap, not instant math like the
237
+ constant-only case above, and it keeps re-measuring on every layout (screen
238
+ rotation, parent resize, ...) rather than freezing after the first value.
239
+ Scoped to exactly one percentage term plus one constant term on `width`/
240
+ `height` only — `calc(50%-50%)`, other properties, or anything with `*`/`/`
241
+ falls through to the same drop-and-warn behavior as before.
242
+
243
+ **Arbitrary values can't contain literal spaces** — this is a pre-existing
244
+ rule (real Tailwind has the same one), not specific to `calc()`: a space
245
+ inside `[...]` is indistinguishable from the whitespace that separates
246
+ class tokens in the first place, so `w-[calc(100% - 3rem)]` silently splits
247
+ into three broken tokens before `calc()` is ever even recognized. Write
248
+ `w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
249
+ unknown-class warning below is for: it'll tell you when this happens.
250
+
251
+ ## Unknown-class warnings (typo detection)
252
+
253
+ ```tsx
254
+ <View className="flexx-center" /> // real Kbach classes never have to guess — a typo warns
255
+ ```
256
+
257
+ ```
258
+ Kbach: "flexx-center" doesn't match any known Kbach utility — typo? (skipped) (from className "flexx-center")
259
+ ```
260
+
261
+ On native/dev-client builds, a class that doesn't match *any* Kbach
262
+ utility on *any* platform gets a `console.warn` (dev builds only, printed
263
+ once per unique message) instead of silently doing nothing — the same
264
+ signal `@kbach/react`'s Vite plugin already gives you at build time on
265
+ web, just at resolve time here instead. A real utility this engine simply
266
+ doesn't support on native yet (`grid-cols-3`, `scale-150`, `blur`, ...)
267
+ never warns — that's an intentional, documented platform gap (see
268
+ `resolve_style.rs`'s own doc comment), not a typo, so it stays silent the
269
+ same way it always has.
270
+
271
+ Not available in Expo Go's pure-JS fallback engine today — that engine has
272
+ no web-shaped resolver to check "is this a real Kbach utility at all"
273
+ against, only the (smaller) native-supported subset, so it can't
274
+ distinguish a genuine typo from a real-but-native-unsupported class
275
+ without a much larger port. A real dev-client/CLI build always gets this
276
+ check.
277
+
278
+ ## Dynamic tokens
279
+
280
+ The native counterpart to a real CSS custom property — `var(--x)` already
281
+ works in a `className` on Expo Web with zero help from this package (real
282
+ CSS, resolved by the browser). This gives native the same className syntax:
283
+
284
+ ```tsx
285
+ import { setDynamicToken, useDynamicToken } from '@kbach/react-native';
286
+
287
+ setDynamicToken('sidebar-width', '240px'); // updates every subscribed element, on both platforms
288
+ <View className="w-[var(--sidebar-width)]" />
289
+
290
+ // Reading the raw value in JS, reactively:
291
+ function Sidebar() {
292
+ const width = useDynamicToken('sidebar-width'); // '240px' | undefined
293
+ }
127
294
  ```
128
295
 
129
- By default the mode follows the OS `Appearance` setting live until you
130
- explicitly override it — and **isn't persisted across app launches**.
131
- There's no React-Native-builtin equivalent of `localStorage`, and adding
132
- one would mean forcing a real native dependency (e.g.
133
- `@react-native-async-storage/async-storage`) on every consumer, even ones
134
- that don't want it. If you want persistence, read your own storage at
135
- startup and call `setGlobalThemeMode()` with the result before your first
136
- render — the same "seed once" shape `<ThemeProvider defaultMode>` itself
137
- uses internally.
296
+ On web, `setDynamicToken` writes the real CSS custom property to the DOM —
297
+ every element using it repaints instantly via the browser's own CSS engine,
298
+ no React re-render involved. On native, the className is substituted with
299
+ the token's current value before every resolve, and an element referencing
300
+ a token re-renders/repaints on its own when that token changes (same
301
+ subscription mechanism `dark:` and `sm:`/`md:`/... already use — see
302
+ `jsxRuntimeCore.ts`'s own doc comments). An unregistered token name falls
303
+ through to the same native invalid-value warning above.
138
304
 
139
305
  ## License
140
306
 
@@ -0,0 +1,36 @@
1
+ apply plugin: "com.android.library"
2
+ apply plugin: "org.jetbrains.kotlin.android"
3
+ apply plugin: "com.facebook.react"
4
+
5
+ def getExtOrDefault(name) {
6
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : null
7
+ }
8
+
9
+ android {
10
+ // NOT "com.kbachnative" — that's the app module's own namespace (Phase 5
11
+ // deliberately made the app's package match KbachModule's Kotlin package
12
+ // for simpler JNI symbol names), and AGP requires unique namespaces per
13
+ // module. This is purely a Gradle build-identity value, separate from
14
+ // the actual `package com.kbachnative` declared in the .kt files below —
15
+ // JNI symbol resolution depends on that Kotlin package, not this.
16
+ namespace "com.kbachnative.nativemodule"
17
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
18
+
19
+ defaultConfig {
20
+ minSdkVersion getExtOrDefault("minSdkVersion")
21
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
22
+ }
23
+ }
24
+
25
+ repositories {
26
+ google()
27
+ maven {
28
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm.
29
+ url "$rootDir/../node_modules/react-native/android"
30
+ }
31
+ mavenCentral()
32
+ }
33
+
34
+ dependencies {
35
+ implementation "com.facebook.react:react-native:+"
36
+ }
@@ -0,0 +1,14 @@
1
+ <!--
2
+ Deliberately NO `package="..."` attribute here, even though this
3
+ module's Kotlin package (com.kbachnative, see KbachModule.kt) differs
4
+ from its Gradle build.gradle `namespace` (com.kbachnative.nativemodule)
5
+ — adding one here to help React Native's autolinking guess the right
6
+ Kotlin package (see react-native.config.js at this package's root, which
7
+ fixes that instead) breaks the actual Gradle build on AGP 9+: it now
8
+ hard-errors ("Setting the namespace via the package attribute in the
9
+ source AndroidManifest.xml is no longer supported") whenever the
10
+ manifest's package doesn't match the namespace exactly. If you're
11
+ tempted to add one back to fix an autolinking symbol-resolution error,
12
+ fix react-native.config.js's packageImportPath/packageInstance instead.
13
+ -->
14
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,83 @@
1
+ package com.kbachnative
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+
5
+ /**
6
+ * Bridges to the same Rust core engine every other Kbach platform uses
7
+ * (packages/core-engine) — via JNI here instead of WASM. The native
8
+ * `generateCss`/`resolveStyle` externs below (in the companion object) must
9
+ * match `Java_com_kbachnative_KbachModule_generateCss`/`resolveStyle`'s
10
+ * signatures exactly (packages/core-engine/src/jni_bridge.rs): STATIC
11
+ * native methods (hence @JvmStatic in a companion object, not instance
12
+ * methods — the Rust side takes JClass, not JObject, as its second
13
+ * parameter).
14
+ *
15
+ * TurboModule since Phase 13 (was the legacy bridge's
16
+ * `isBlockingSynchronousMethod` support directly on `ReactContextBaseJavaModule`
17
+ * before) — extends the Codegen-generated `NativeKbachModuleSpec`
18
+ * (packages/react-native/specs/NativeKbachModule.ts -> this package's own
19
+ * codegenConfig in package.json, discovered via autolinking ->
20
+ * android/build/generated/source/codegen/java/com/kbachnative/NativeKbachModuleSpec.java),
21
+ * which already provides `getName()` and declares `generateCss`/`resolveStyle`
22
+ * as abstract `@ReactMethod(isBlockingSynchronousMethod = true)` methods —
23
+ * Codegen still routes a synchronously-returning TurboModule method through
24
+ * that same Android bridge mechanism; the real gain here is JSI-backed
25
+ * dispatch + build-time type checking against the JS spec, not the removal
26
+ * of `isBlockingSynchronousMethod` as a concept.
27
+ *
28
+ * The companion's JNI externs are named `nativeGenerateCss`/`nativeResolveStyle`
29
+ * — NOT the same names as the spec's `generateCss`/`resolveStyle` overrides.
30
+ * Tried keeping identical names first (disambiguated via an explicit
31
+ * `Companion.generateCss(...)` qualifier) since that would've meant zero
32
+ * Rust changes, but that fails to compile: `@JvmStatic` duplicates each
33
+ * companion function directly onto the outer class for Java-interop
34
+ * purposes, which collides at the JVM bytecode level with the identically-
35
+ * named, identically-shaped instance override ("platform declaration
36
+ * clash"), regardless of how the Kotlin-source call site is qualified. The
37
+ * rename requires matching `Java_com_kbachnative_KbachModule_nativeGenerateCss`/
38
+ * `nativeResolveStyle` `#[no_mangle]` export names in
39
+ * packages/core-engine/src/jni_bridge.rs — JNI's implicit symbol linking
40
+ * binds by the extern's own method name, not by whatever calls it.
41
+ *
42
+ * Moved here from apps/native-sandbox/android/app in Phase 13 — Android
43
+ * autolinking only discovers native modules from npm dependencies with
44
+ * their own `android/` folder, never from native code declared directly
45
+ * inside the app itself, so `KbachModule` had to become a real (locally
46
+ * `file:`-linked) package to be found by `TurboModuleRegistry.getEnforcing`.
47
+ * Originally that "real package" was a separate `@kbach/android` npm
48
+ * package; later folded directly into `@kbach/react-native` (this
49
+ * package's own `android/` folder, sibling to its `src/`) once it became
50
+ * clear the same autolinking requirement is satisfied just as well by a
51
+ * SINGLE npm package containing both JS and native code — the same layout
52
+ * react-native-safe-area-context itself uses — so consumers only ever run
53
+ * one `npm install @kbach/react-native`, not two.
54
+ */
55
+ class KbachModule(reactContext: ReactApplicationContext) : NativeKbachModuleSpec(reactContext) {
56
+
57
+ companion object {
58
+ init {
59
+ System.loadLibrary("kbach_core_engine")
60
+ }
61
+
62
+ @JvmStatic
63
+ external fun nativeGenerateCss(classString: String, themeJson: String): String
64
+
65
+ @JvmStatic
66
+ external fun nativeResolveStyle(classString: String, themeJson: String, colorScheme: String, pressed: Boolean, width: Double): String
67
+ }
68
+
69
+ override fun generateCss(classString: String, themeJson: String): String {
70
+ return nativeGenerateCss(classString, themeJson)
71
+ }
72
+
73
+ /**
74
+ * `colorScheme` ("light"/"dark", from Appearance.getColorScheme() on the
75
+ * JS side) gates whether `dark:` declarations are applied; `pressed`
76
+ * (only ever true when the caller is RN's Pressable) gates `active:`;
77
+ * `width` (the JS side's current `Dimensions.get('window').width`) gates
78
+ * `sm:`/`md:`/`lg:`/`xl:`/`2xl:` against the theme's `screens` scale.
79
+ */
80
+ override fun resolveStyle(classString: String, themeJson: String, colorScheme: String, pressed: Boolean, width: Double): String {
81
+ return nativeResolveStyle(classString, themeJson, colorScheme, pressed, width)
82
+ }
83
+ }
@@ -0,0 +1,54 @@
1
+ package com.kbachnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ /**
11
+ * `BaseReactPackage` (not plain `ReactPackage`) since Phase 13's TurboModule
12
+ * migration — a plain `ReactPackage`'s `createNativeModules()`-provided
13
+ * modules stay visible to the legacy `NativeModules` interop but are never
14
+ * consulted by `TurboModuleRegistry`'s resolution path, which only walks
15
+ * packages that can answer `getModule(name, ...)` directly. Modeled on
16
+ * react-native-safe-area-context's own `SafeAreaContextPackage.kt` (a real,
17
+ * New-Architecture-compatible package already in this project's
18
+ * dependencies) rather than reflection over a `@ReactModule` annotation —
19
+ * KbachModule doesn't have one, and there's only ever this one module, so
20
+ * building its `ReactModuleInfo` directly is simpler than adding an
21
+ * annotation just to read it back via reflection.
22
+ *
23
+ * Auto-discovered via Android autolinking, not manually registered in
24
+ * apps/native-sandbox's MainApplication.kt — same as SafeAreaContextPackage
25
+ * already was, and the entire reason this module became a real npm package
26
+ * with its own `android/` folder (this one — `@kbach/react-native` itself,
27
+ * see KbachModule.kt's own doc comment) in Phase 13 rather than living
28
+ * inside the app.
29
+ */
30
+ class KbachPackage : BaseReactPackage() {
31
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
32
+ return if (name == NativeKbachModuleSpec.NAME) KbachModule(reactContext) else null
33
+ }
34
+
35
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
36
+ return ReactModuleInfoProvider {
37
+ mapOf(
38
+ NativeKbachModuleSpec.NAME to
39
+ ReactModuleInfo(
40
+ NativeKbachModuleSpec.NAME,
41
+ KbachModule::class.java.name,
42
+ false, // canOverrideExistingModule
43
+ false, // needsEagerInit
44
+ false, // isCxxModule
45
+ true, // isTurboModule
46
+ )
47
+ )
48
+ }
49
+ }
50
+
51
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
52
+ return emptyList()
53
+ }
54
+ }