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

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 +220 -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-45GOGUHJ.mjs +107 -0
  9. package/dist/chunk-MJWWZJMH.mjs +40 -0
  10. package/dist/chunk-OGFB3OXS.mjs +552 -0
  11. package/dist/darkModeStore.d.ts +58 -0
  12. package/dist/darkModeStore.js +112 -0
  13. package/dist/dynamicTokens.d.ts +35 -0
  14. package/dist/dynamicTokens.js +75 -0
  15. package/dist/index.d.ts +53 -102
  16. package/dist/index.js +1578 -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 +1565 -344
  22. package/dist/jsx-dev-runtime.web.d.ts +10 -0
  23. package/dist/jsx-dev-runtime.web.js +271 -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 +1556 -336
  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,251 @@ 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
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.
48
36
 
49
- ### Expo
37
+ ### Expo (dev client / prebuild)
50
38
 
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:
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
59
+
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.
101
64
 
102
- Works standalone, with zero provider — same architecture as `@kbach/react`:
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:
103
69
 
104
70
  ```tsx
105
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react-native';
71
+ const [mounted, setMounted] = useState(false);
72
+ useEffect(() => setMounted(true), []);
106
73
 
107
- function ThemeToggle() {
108
- const isDark = useGlobalDarkMode();
109
- return <Pressable onPress={toggleGlobalDarkMode}><Text>{isDark ? 'Dark' : 'Light'}</Text></Pressable>;
110
- }
74
+ <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
75
+ ```
76
+
77
+ ### Monorepo installs (npm/yarn/pnpm workspaces)
78
+
79
+ If your app lives in a monorepo where `@kbach/react-native` is a workspace
80
+ package (a `file:`/`link:` dependency, or hoisted rather than nested), watch
81
+ for **two physical copies of `react-native`/`react`** ending up in your
82
+ dependency tree — one this package would otherwise resolve up to from its
83
+ own directory, and a different one your app's own files resolve to. Metro
84
+ walks `node_modules` hierarchically per-file, so it can silently hand two
85
+ different files two different copies of the *same* module, each with its
86
+ own separate module state.
87
+
88
+ This isn't hypothetical: it's the root cause of a real dark-mode bug this
89
+ package shipped and fixed — `useColorScheme()`/`Appearance` in one copy
90
+ never saw changes made through the other, so an explicit theme toggle
91
+ appeared to do nothing. The fix is a Metro config override that forces a
92
+ single instance, **required in your app**, not something this package can
93
+ do on your behalf from inside `node_modules`:
94
+
95
+ ```js
96
+ // metro.config.js
97
+ const path = require('path');
98
+ // React Native CLI: const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
99
+ // Expo: const { getDefaultConfig } = require('expo/metro-config');
100
+
101
+ const projectRoot = __dirname;
102
+ const workspaceRoot = path.resolve(projectRoot, '../..'); // -> your monorepo root
103
+
104
+ const config = getDefaultConfig(projectRoot);
105
+ config.watchFolders = [workspaceRoot];
106
+
107
+ const forcedSingleInstance = {
108
+ 'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
109
+ react: path.resolve(projectRoot, 'node_modules/react'),
110
+ };
111
+ const defaultResolveRequest = config.resolver.resolveRequest;
112
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
113
+ if (Object.prototype.hasOwnProperty.call(forcedSingleInstance, moduleName)) {
114
+ return { type: 'sourceFile', filePath: require.resolve(forcedSingleInstance[moduleName]) };
115
+ }
116
+ return defaultResolveRequest
117
+ ? defaultResolveRequest(context, moduleName, platform)
118
+ : context.resolveRequest(context, moduleName, platform);
119
+ };
120
+
121
+ module.exports = config; // (React Native CLI: mergeConfig(getDefaultConfig(projectRoot), config))
111
122
  ```
112
123
 
113
- Or the optional `<ThemeProvider>` / `useTheme()` pair, reading and writing
114
- the exact same global store:
124
+ A single, non-monorepo `npm install` never hits this — only relevant if your
125
+ app and `@kbach/react-native` share a workspace root.
126
+
127
+ ## Dark mode
115
128
 
116
129
  ```tsx
117
- import { ThemeProvider, useTheme } from '@kbach/react-native';
130
+ import { useTheme } from '@kbach/react-native';
118
131
 
119
- function Toggle() {
132
+ function ThemeToggle() {
120
133
  const { mode, isDark, setMode, toggle } = useTheme();
121
134
  return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
122
135
  }
136
+ ```
137
+
138
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
139
+ optional, for seeding a startup default. Outside React, use
140
+ `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
141
+
142
+ Follows the OS setting live until overridden, and isn't persisted across
143
+ app launches (no built-in storage dependency). To persist, read your own
144
+ storage at startup and call `setGlobalThemeMode()` before your first render.
145
+
146
+ ## Reading colors as values
147
+
148
+ For anywhere you need a real color, not a `className` — a chart prop, a
149
+ native shadow color:
150
+
151
+ ```tsx
152
+ import { useColors } from '@kbach/react-native';
153
+
154
+ const colors = useColors();
155
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
156
+ colors.blue['6/50']; // same, at 50% opacity
157
+ ```
123
158
 
124
- <ThemeProvider defaultMode="system">
125
- <Toggle />
126
- </ThemeProvider>;
159
+ Any color can be defined in the theme as a plain string (static) or a
160
+ `{ light, dark }` pair — fully manual, resolved per the current dark-mode
161
+ state:
162
+
163
+ ```ts
164
+ setTheme({
165
+ ...defaultTheme,
166
+ colors: { ...defaultTheme.colors, brand: { light: '#2563eb', dark: '#93c5fd' } },
167
+ });
168
+ ```
169
+
170
+ A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
171
+ dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
172
+ that's the same visual weight relative to its own background, no manual
173
+ `colors.blue[isDark ? 7 : 6]` needed.
174
+
175
+ ## Arbitrary math: `calc()`, `clamp()`, `min()`, `max()`
176
+
177
+ ```tsx
178
+ <View className="p-[calc(16px+8px)]" /> // -> padding: 24
179
+ <View className="w-[clamp(1rem,2rem,3rem)]" /> // -> width: 32
180
+ ```
181
+
182
+ Resolves at **build/resolve time**, not runtime — every operand must be a
183
+ constant `px`/`rem` length (or a bare number). This works because native has
184
+ no CSS engine to evaluate a real `calc()` string at paint time, so the only
185
+ way to support it at all is to compute the answer ourselves.
186
+
187
+ A **viewport-relative** expression (`calc(10vw+8px)`) can't be reduced this
188
+ way — no such unit exists on native at all. It already works fine on web
189
+ (real CSS); on native the declaration is dropped and a `console.warn`
190
+ explains why (dev builds only).
191
+
192
+ **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`)
193
+ IS supported, differently: since native has no way to know "100% of what"
194
+ ahead of time, this measures the element's own real layout instead —
195
+
196
+ ```tsx
197
+ <View className="w-[calc(100%_-_3rem)]" />
198
+ ```
199
+
200
+ Renders once at a plain `100%` (a real RN percentage, resolved correctly by
201
+ RN's own layout engine against the parent) purely to trigger a layout
202
+ event, then immediately corrects to the actual computed number once
203
+ measured — a genuine one-frame measure-then-snap, not instant math like the
204
+ constant-only case above, and it keeps re-measuring on every layout (screen
205
+ rotation, parent resize, ...) rather than freezing after the first value.
206
+ Scoped to exactly one percentage term plus one constant term on `width`/
207
+ `height` only — `calc(50%-50%)`, other properties, or anything with `*`/`/`
208
+ falls through to the same drop-and-warn behavior as before.
209
+
210
+ **Arbitrary values can't contain literal spaces** — this is a pre-existing
211
+ rule (real Tailwind has the same one), not specific to `calc()`: a space
212
+ inside `[...]` is indistinguishable from the whitespace that separates
213
+ class tokens in the first place, so `w-[calc(100% - 3rem)]` silently splits
214
+ into three broken tokens before `calc()` is ever even recognized. Write
215
+ `w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
216
+ unknown-class warning below is for: it'll tell you when this happens.
217
+
218
+ ## Unknown-class warnings (typo detection)
219
+
220
+ ```tsx
221
+ <View className="flexx-center" /> // real Kbach classes never have to guess — a typo warns
222
+ ```
223
+
224
+ ```
225
+ Kbach: "flexx-center" doesn't match any known Kbach utility — typo? (skipped) (from className "flexx-center")
226
+ ```
227
+
228
+ On native/dev-client builds, a class that doesn't match *any* Kbach
229
+ utility on *any* platform gets a `console.warn` (dev builds only, printed
230
+ once per unique message) instead of silently doing nothing — the same
231
+ signal `@kbach/react`'s Vite plugin already gives you at build time on
232
+ web, just at resolve time here instead. A real utility this engine simply
233
+ doesn't support on native yet (`grid-cols-3`, `scale-150`, `blur`, ...)
234
+ never warns — that's an intentional, documented platform gap (see
235
+ `resolve_style.rs`'s own doc comment), not a typo, so it stays silent the
236
+ same way it always has.
237
+
238
+ Not available in Expo Go's pure-JS fallback engine today — that engine has
239
+ no web-shaped resolver to check "is this a real Kbach utility at all"
240
+ against, only the (smaller) native-supported subset, so it can't
241
+ distinguish a genuine typo from a real-but-native-unsupported class
242
+ without a much larger port. A real dev-client/CLI build always gets this
243
+ check.
244
+
245
+ ## Dynamic tokens
246
+
247
+ The native counterpart to a real CSS custom property — `var(--x)` already
248
+ works in a `className` on Expo Web with zero help from this package (real
249
+ CSS, resolved by the browser). This gives native the same className syntax:
250
+
251
+ ```tsx
252
+ import { setDynamicToken, useDynamicToken } from '@kbach/react-native';
253
+
254
+ setDynamicToken('sidebar-width', '240px'); // updates every subscribed element, on both platforms
255
+ <View className="w-[var(--sidebar-width)]" />
256
+
257
+ // Reading the raw value in JS, reactively:
258
+ function Sidebar() {
259
+ const width = useDynamicToken('sidebar-width'); // '240px' | undefined
260
+ }
127
261
  ```
128
262
 
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.
263
+ On web, `setDynamicToken` writes the real CSS custom property to the DOM —
264
+ every element using it repaints instantly via the browser's own CSS engine,
265
+ no React re-render involved. On native, the className is substituted with
266
+ the token's current value before every resolve, and an element referencing
267
+ a token re-renders/repaints on its own when that specific token changes
268
+ (same `key`-remount mechanism `dark:` and `sm:`/`md:`/... already use — see
269
+ `jsxRuntimeCore.ts`'s own doc comments). An unregistered token name falls
270
+ through to the same native invalid-value warning above.
138
271
 
139
272
  ## License
140
273
 
@@ -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
+ }
@@ -0,0 +1,107 @@
1
+ import {
2
+ getTheme
3
+ } from "./chunk-OGFB3OXS.mjs";
4
+
5
+ // src/darkModeStore.ts
6
+ import { Appearance } from "react-native";
7
+ var listeners = /* @__PURE__ */ new Set();
8
+ function systemPrefersDark() {
9
+ return Appearance.getColorScheme() === "dark";
10
+ }
11
+ function resolveIsDark(m) {
12
+ return m === "system" ? systemPrefersDark() : m === "dark";
13
+ }
14
+ function applyToDom(dark) {
15
+ if (typeof document === "undefined") return;
16
+ const root = document.documentElement;
17
+ const strategy = getTheme().darkMode;
18
+ if (strategy === "class") {
19
+ root.classList.toggle("dark", dark);
20
+ root.classList.toggle("light", !dark);
21
+ } else if (strategy === "attribute") {
22
+ root.setAttribute("data-theme", dark ? "dark" : "light");
23
+ }
24
+ }
25
+ var mode = "system";
26
+ var isDark = resolveIsDark(mode);
27
+ applyToDom(isDark);
28
+ var hasExplicitChoice = false;
29
+ var subscription = Appearance.addChangeListener(({ colorScheme }) => {
30
+ if (mode !== "system") return;
31
+ isDark = colorScheme === "dark";
32
+ applyToDom(isDark);
33
+ notify();
34
+ });
35
+ function notify() {
36
+ for (const listener of listeners) listener();
37
+ }
38
+ function getGlobalDarkMode() {
39
+ return isDark;
40
+ }
41
+ function getGlobalThemeMode() {
42
+ return mode;
43
+ }
44
+ function setGlobalThemeMode(nextMode) {
45
+ mode = nextMode;
46
+ isDark = resolveIsDark(nextMode);
47
+ hasExplicitChoice = true;
48
+ applyToDom(isDark);
49
+ notify();
50
+ }
51
+ function toggleGlobalDarkMode() {
52
+ setGlobalThemeMode(isDark ? "light" : "dark");
53
+ }
54
+ function subscribeGlobalDarkMode(listener) {
55
+ listeners.add(listener);
56
+ return () => listeners.delete(listener);
57
+ }
58
+ function seedDefaultMode(fallback) {
59
+ if (hasExplicitChoice || mode === fallback) return;
60
+ mode = fallback;
61
+ isDark = resolveIsDark(fallback);
62
+ applyToDom(isDark);
63
+ notify();
64
+ }
65
+
66
+ // src/dynamicTokens.ts
67
+ var tokens = /* @__PURE__ */ new Map();
68
+ var listeners2 = /* @__PURE__ */ new Set();
69
+ var version = 0;
70
+ function applyToDom2(name, value) {
71
+ if (typeof document === "undefined") return;
72
+ document.documentElement.style.setProperty(`--${name}`, value);
73
+ }
74
+ function notify2() {
75
+ version++;
76
+ for (const listener of listeners2) listener();
77
+ }
78
+ function setDynamicToken(name, value) {
79
+ tokens.set(name, value);
80
+ applyToDom2(name, value);
81
+ notify2();
82
+ }
83
+ function getDynamicToken(name) {
84
+ return tokens.get(name);
85
+ }
86
+ function deleteDynamicToken(name) {
87
+ if (!tokens.delete(name)) return;
88
+ if (typeof document !== "undefined") document.documentElement.style.removeProperty(`--${name}`);
89
+ notify2();
90
+ }
91
+ function subscribeDynamicTokens(listener) {
92
+ listeners2.add(listener);
93
+ return () => listeners2.delete(listener);
94
+ }
95
+
96
+ export {
97
+ getGlobalDarkMode,
98
+ getGlobalThemeMode,
99
+ setGlobalThemeMode,
100
+ toggleGlobalDarkMode,
101
+ subscribeGlobalDarkMode,
102
+ seedDefaultMode,
103
+ setDynamicToken,
104
+ getDynamicToken,
105
+ deleteDynamicToken,
106
+ subscribeDynamicTokens
107
+ };