@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.
- package/README.md +253 -87
- package/android/build.gradle +36 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/kbachnative/KbachModule.kt +83 -0
- package/android/src/main/java/com/kbachnative/KbachPackage.kt +54 -0
- package/android/src/main/jniLibs/arm64-v8a/libkbach_core_engine.so +0 -0
- package/android/src/main/jniLibs/x86_64/libkbach_core_engine.so +0 -0
- package/dist/chunk-AG7ZQ2K4.mjs +552 -0
- package/dist/chunk-OS7EPBH5.mjs +40 -0
- package/dist/chunk-TS2EEVOE.mjs +107 -0
- package/dist/darkModeStore.d.ts +58 -0
- package/dist/darkModeStore.js +112 -0
- package/dist/dynamicTokens.d.ts +36 -0
- package/dist/dynamicTokens.js +75 -0
- package/dist/index.d.ts +53 -102
- package/dist/index.js +1721 -385
- package/dist/index.web.d.ts +18 -0
- package/dist/index.web.js +436 -0
- package/dist/index.web.mjs +210 -0
- package/dist/jsx-dev-runtime.d.ts +3 -12
- package/dist/jsx-dev-runtime.js +1738 -343
- package/dist/jsx-dev-runtime.web.d.ts +10 -0
- package/dist/jsx-dev-runtime.web.js +270 -0
- package/dist/jsx-dev-runtime.web.mjs +29 -0
- package/dist/jsx-runtime.d.ts +3 -23
- package/dist/jsx-runtime.js +1729 -335
- package/dist/jsx-runtime.web.d.ts +7 -0
- package/dist/jsx-runtime.web.js +252 -0
- package/dist/{jsx-runtime.mjs → jsx-runtime.web.mjs} +2 -2
- package/dist/theme.d.ts +54 -0
- package/dist/{chunk-YHF3QLRL.mjs → theme.js} +94 -72
- package/dist/useDynamicToken-Dr15c88t.d.ts +160 -0
- package/package.json +62 -14
- package/react-native.config.js +29 -0
- package/specs/NativeKbachModule.ts +31 -0
- package/src/babel-plugin.js +74 -2
- package/dist/chunk-SVMYVXY6.mjs +0 -44
- package/dist/index.d.mts +0 -148
- package/dist/index.mjs +0 -80
- package/dist/jsx-dev-runtime.d.mts +0 -19
- package/dist/jsx-dev-runtime.mjs +0 -20
- 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
|
|
5
|
+
resolved to a `StyleSheet`-ready style object instead of injected CSS.
|
|
6
6
|
|
|
7
|
-
>
|
|
8
|
-
>
|
|
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
|
-
##
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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'],
|
|
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
|
|
67
|
-
npx expo run:android
|
|
50
|
+
npx expo prebuild
|
|
51
|
+
npx expo run:android
|
|
68
52
|
```
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
58
|
+
### Expo Web
|
|
101
59
|
|
|
102
|
-
Works
|
|
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
|
-
|
|
72
|
+
const [mounted, setMounted] = useState(false);
|
|
73
|
+
useEffect(() => setMounted(true), []);
|
|
106
74
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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 {
|
|
163
|
+
import { useTheme } from '@kbach/react-native';
|
|
118
164
|
|
|
119
|
-
function
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
Binary file
|
|
Binary file
|