@kbach/react-native 1.0.0-beta.3 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,11 +4,12 @@ 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
5
  resolved to a real `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 (Android-only, no iOS bridge yet) resolves through a
8
+ > Kotlin/JNI TurboModule; Expo Web / react-native-web resolves through the
9
+ > same Rust engine compiled to WASM instead — both call the identical
10
+ > resolution logic, so class semantics stay identical everywhere. The
11
+ > native module ships prebuilt (see Setup below) — no Rust toolchain or
12
+ > manual native wiring required on any platform.
12
13
 
13
14
  ## Setup
14
15
 
@@ -46,13 +47,18 @@ one needs (a plain single-app install doesn't need any of that — it's
46
47
  only relevant if you're linking packages locally instead of installing
47
48
  from npm).
48
49
 
49
- ### Expo
50
+ ### Expo (native — Android)
50
51
 
51
- **Expo Go doesn't work** — it's a fixed pre-built binary with no way to
52
- load a custom native module like this one. You need a **dev client**
53
- 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:
52
+ **Expo Go can never work with this package, and that isn't specific to
53
+ Kbach.** Expo Go is a single fixed binary Expo pre-builds and ships
54
+ through the app stores, containing only the native code Expo bundled into
55
+ it at their own build time — there is no mechanism for *any* third-party
56
+ package to add native code to it at runtime, whether that's a raw
57
+ TurboModule (what this package uses) or even Expo's own Modules API. This
58
+ is exactly why the **dev client** concept exists: a custom build of your
59
+ app — built once via `expo prebuild`/`expo run:android`, functionally the
60
+ same "real native project" as the CLI path above — that *does* include
61
+ this module:
56
62
 
57
63
  ```js
58
64
  // babel.config.js
@@ -76,6 +82,25 @@ Architecture off, turn it on in `app.json`
76
82
  EAS Build works the same way — it runs the same `prebuild` step, so no
77
83
  extra configuration beyond the two steps above.
78
84
 
85
+ ### Expo Web
86
+
87
+ **Works — no extra setup, no separate package, no init call.** `expo start
88
+ --web` (and EAS's web builds) render through `react-native-web` —
89
+ `View`/`Text`/etc. become real DOM elements with real CSS, not a
90
+ Kotlin/JNI bridge, so on web this package resolves classes through the
91
+ same Rust engine [`@kbach/react`](https://www.npmjs.com/package/@kbach/react)
92
+ uses — WASM instead of JNI — automatically. Metro picks the right
93
+ implementation per platform on its own via this package's `"browser"`
94
+ export condition; your app code and JSX never change. The WASM module
95
+ initializes itself eagerly (synchronously, at import time — no `await`
96
+ needed before your first render, unlike a from-scratch WASM setup).
97
+
98
+ The only thing this doesn't cover on web is an iOS-parity feature gap —
99
+ everything web-side resolves through the exact same
100
+ [`resolve_style.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolve_style.rs)
101
+ logic real native uses, so see this README's "What this is" section above
102
+ for what's covered.
103
+
79
104
  ## What this is
80
105
 
81
106
  - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
@@ -0,0 +1,66 @@
1
+ import {
2
+ getGlobalDarkMode,
3
+ getGlobalThemeMode,
4
+ seedDefaultMode,
5
+ setGlobalThemeMode,
6
+ subscribeGlobalDarkMode,
7
+ toggleGlobalDarkMode
8
+ } from "./chunk-AEVOM2NB.mjs";
9
+
10
+ // src/useGlobalDarkMode.ts
11
+ import { useSyncExternalStore } from "react";
12
+ function useGlobalDarkMode() {
13
+ return useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode);
14
+ }
15
+
16
+ // src/ThemeContext.tsx
17
+ import { createContext, useContext } from "react";
18
+ var ThemeContext = createContext(null);
19
+ function useTheme() {
20
+ const ctx = useContext(ThemeContext);
21
+ if (!ctx) {
22
+ throw new Error(
23
+ 'useTheme() must be called within a <ThemeProvider>. For dark-mode state without a provider, use useGlobalDarkMode() (read) or toggleGlobalDarkMode()/setGlobalThemeMode() (write) from "@kbach/react-native" instead.'
24
+ );
25
+ }
26
+ return ctx;
27
+ }
28
+
29
+ // src/ThemeProvider.tsx
30
+ import { useEffect, useMemo, useRef, useSyncExternalStore as useSyncExternalStore2 } from "react";
31
+ import { jsx } from "react/jsx-runtime";
32
+ var mountedProviderCount = 0;
33
+ var warnedMultipleProviders = false;
34
+ function ThemeProvider({ children, defaultMode = "system" }) {
35
+ useEffect(() => {
36
+ mountedProviderCount++;
37
+ if (mountedProviderCount > 1 && !warnedMultipleProviders) {
38
+ warnedMultipleProviders = true;
39
+ console.warn(
40
+ "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store (the same one useGlobalDarkMode()/toggleGlobalDarkMode() read and write), not scoped per-provider, so they will fight over the same state instead of each getting their own."
41
+ );
42
+ }
43
+ return () => {
44
+ mountedProviderCount--;
45
+ };
46
+ }, []);
47
+ const seeded = useRef(false);
48
+ useEffect(() => {
49
+ if (seeded.current) return;
50
+ seeded.current = true;
51
+ seedDefaultMode(defaultMode);
52
+ }, []);
53
+ const isDark = useSyncExternalStore2(subscribeGlobalDarkMode, getGlobalDarkMode);
54
+ const mode = useSyncExternalStore2(subscribeGlobalDarkMode, getGlobalThemeMode);
55
+ const value = useMemo(
56
+ () => ({ mode, isDark, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
57
+ [mode, isDark]
58
+ );
59
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
60
+ }
61
+
62
+ export {
63
+ useGlobalDarkMode,
64
+ useTheme,
65
+ ThemeProvider
66
+ };
@@ -357,25 +357,15 @@ function seedDefaultMode(fallback) {
357
357
  notify();
358
358
  }
359
359
 
360
- // src/nativeBridge.ts
361
- import { Dimensions, TurboModuleRegistry } from "react-native";
362
- function resolveStyle(classString, pressed = false) {
363
- const KbachModule = TurboModuleRegistry.getEnforcing("KbachModule");
364
- const colorScheme = getGlobalDarkMode() ? "dark" : "light";
365
- const width = Dimensions.get("window").width;
366
- const json = KbachModule.resolveStyle(classString, getThemeJson(), colorScheme, pressed, width);
367
- return JSON.parse(json);
368
- }
369
-
370
360
  export {
371
361
  defaultTheme,
372
362
  setTheme,
373
363
  getTheme,
364
+ getThemeJson,
374
365
  getGlobalDarkMode,
375
366
  getGlobalThemeMode,
376
367
  setGlobalThemeMode,
377
368
  toggleGlobalDarkMode,
378
369
  subscribeGlobalDarkMode,
379
- seedDefaultMode,
380
- resolveStyle
370
+ seedDefaultMode
381
371
  };
@@ -1,15 +1,11 @@
1
- import {
2
- resolveStyle
3
- } from "./chunk-7KUNKZBM.mjs";
4
-
5
- // src/jsx-runtime.tsx
1
+ // src/jsxRuntimeCore.ts
6
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
3
  import { Fragment } from "react";
8
4
  import { Pressable } from "react-native";
9
5
  function makeElement(isStaticChildren, type, props, key) {
10
6
  return (isStaticChildren ? _jsxs : _jsx)(type, props, key);
11
7
  }
12
- function resolvedStyleFor(classStrRaw, userStyle, pressed) {
8
+ function resolvedStyleFor(resolveStyle, classStrRaw, userStyle, pressed) {
13
9
  const resolved = resolveStyle(classStrRaw, pressed);
14
10
  if (userStyle === void 0) {
15
11
  return resolved;
@@ -19,7 +15,7 @@ function resolvedStyleFor(classStrRaw, userStyle, pressed) {
19
15
  }
20
16
  return [resolved, userStyle];
21
17
  }
22
- function processElement(type, rawProps, key, isStaticChildren) {
18
+ function processElement(resolveStyle, type, rawProps, key, isStaticChildren) {
23
19
  if (type === null || typeof type === "symbol" || !rawProps) {
24
20
  return makeElement(isStaticChildren, type, rawProps ?? {}, key);
25
21
  }
@@ -27,18 +23,31 @@ function processElement(type, rawProps, key, isStaticChildren) {
27
23
  if (typeof classStrRaw !== "string") {
28
24
  return makeElement(isStaticChildren, type, rawProps, key);
29
25
  }
30
- const finalStyle = type === Pressable ? (state) => resolvedStyleFor(classStrRaw, userStyle, state.pressed) : resolvedStyleFor(classStrRaw, userStyle, false);
26
+ const finalStyle = type === Pressable ? (state) => resolvedStyleFor(resolveStyle, classStrRaw, userStyle, state.pressed) : resolvedStyleFor(resolveStyle, classStrRaw, userStyle, false);
31
27
  return makeElement(isStaticChildren, type, { ...rest, style: finalStyle }, key);
32
28
  }
33
- function jsx(type, props, key) {
34
- return processElement(type, props, key, false);
29
+ function createJsxFunctions(resolveStyle) {
30
+ function jsx(type, props, key) {
31
+ return processElement(resolveStyle, type, props, key, false);
32
+ }
33
+ function jsxs(type, props, key) {
34
+ return processElement(resolveStyle, type, props, key, true);
35
+ }
36
+ return { jsx, jsxs };
35
37
  }
36
- function jsxs(type, props, key) {
37
- return processElement(type, props, key, true);
38
+ function createJsxDEV(jsx, jsxs) {
39
+ return function jsxDEV(type, props, key, isStaticChildren, source, self) {
40
+ const element = (isStaticChildren ? jsxs : jsx)(type, props, key);
41
+ if (source && element && typeof element === "object" && Object.isExtensible(element)) {
42
+ element._source = source;
43
+ element._self = self;
44
+ }
45
+ return element;
46
+ };
38
47
  }
39
48
 
40
49
  export {
41
50
  Fragment,
42
- jsx,
43
- jsxs
51
+ createJsxFunctions,
52
+ createJsxDEV
44
53
  };
@@ -0,0 +1,18 @@
1
+ import {
2
+ getGlobalDarkMode,
3
+ getThemeJson
4
+ } from "./chunk-AEVOM2NB.mjs";
5
+
6
+ // src/nativeBridge.ts
7
+ import { Dimensions, TurboModuleRegistry } from "react-native";
8
+ function resolveStyle(classString, pressed = false) {
9
+ const KbachModule = TurboModuleRegistry.getEnforcing("KbachModule");
10
+ const colorScheme = getGlobalDarkMode() ? "dark" : "light";
11
+ const width = Dimensions.get("window").width;
12
+ const json = KbachModule.resolveStyle(classString, getThemeJson(), colorScheme, pressed, width);
13
+ return JSON.parse(json);
14
+ }
15
+
16
+ export {
17
+ resolveStyle
18
+ };