@kbach/react-native 1.0.0-beta.0

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 ADDED
@@ -0,0 +1,78 @@
1
+ # @kbach/react-native
2
+
3
+ Kbach's utility-class styling engine for React Native — the same class
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.
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 — but that TurboModule currently has to be
10
+ > wired into your app's native Android project by hand (see
11
+ > `apps/native-sandbox` in the [repo](https://github.com/semsakadanupol/kbach)
12
+ > for a full working reference integration). There's no iOS bridge yet, and
13
+ > no prebuilt native module distribution — expect to do real native-side
14
+ > setup, not a drop-in `npm install`.
15
+
16
+ ## What this is
17
+
18
+ - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
19
+ `className`/`kb` props on every element and calls into the native bridge
20
+ to resolve them to a style object, synchronously, on every render.
21
+ - A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
22
+ app's own files (never `node_modules`) through that JSX runtime via a
23
+ per-file `@jsxImportSource` pragma.
24
+ - `setTheme()`/`getTheme()` for customizing the color palette and spacing
25
+ scale — the same `ThemeConfig` shape `@kbach/react` uses, kept as an
26
+ independent type here so a native-only project isn't forced to depend on
27
+ a web-oriented package.
28
+ - `shadow-*` resolves to real native shadows — iOS's discrete
29
+ `shadowColor`/`shadowOffset`/`shadowOpacity`/`shadowRadius` and Android's
30
+ `elevation`, both derived from the same named tiers (`sm`/`DEFAULT`/`md`/
31
+ `lg`/`xl`/`2xl`) `@kbach/react`'s CSS `box-shadow` scale uses — one class
32
+ produces a real shadow on both platforms. `shadow-inner` and arbitrary
33
+ `shadow-[...]` values aren't supported — neither platform's native shadow
34
+ API can express an inset shadow, and an arbitrary value is a raw CSS
35
+ string with no mechanical translation into discrete native props.
36
+
37
+ ## Dark mode
38
+
39
+ Works standalone, with zero provider — same architecture as `@kbach/react`:
40
+
41
+ ```tsx
42
+ import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react-native';
43
+
44
+ function ThemeToggle() {
45
+ const isDark = useGlobalDarkMode();
46
+ return <Pressable onPress={toggleGlobalDarkMode}><Text>{isDark ? 'Dark' : 'Light'}</Text></Pressable>;
47
+ }
48
+ ```
49
+
50
+ Or the optional `<ThemeProvider>` / `useTheme()` pair, reading and writing
51
+ the exact same global store:
52
+
53
+ ```tsx
54
+ import { ThemeProvider, useTheme } from '@kbach/react-native';
55
+
56
+ function Toggle() {
57
+ const { mode, isDark, setMode, toggle } = useTheme();
58
+ return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
59
+ }
60
+
61
+ <ThemeProvider defaultMode="system">
62
+ <Toggle />
63
+ </ThemeProvider>;
64
+ ```
65
+
66
+ By default the mode follows the OS `Appearance` setting live until you
67
+ explicitly override it — and **isn't persisted across app launches**.
68
+ There's no React-Native-builtin equivalent of `localStorage`, and adding
69
+ one would mean forcing a real native dependency (e.g.
70
+ `@react-native-async-storage/async-storage`) on every consumer, even ones
71
+ that don't want it. If you want persistence, read your own storage at
72
+ startup and call `setGlobalThemeMode()` with the result before your first
73
+ render — the same "seed once" shape `<ThemeProvider defaultMode>` itself
74
+ uses internally.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,44 @@
1
+ import {
2
+ resolveStyle
3
+ } from "./chunk-YHF3QLRL.mjs";
4
+
5
+ // src/jsx-runtime.tsx
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ import { Fragment } from "react";
8
+ import { Pressable } from "react-native";
9
+ function makeElement(isStaticChildren, type, props, key) {
10
+ return (isStaticChildren ? _jsxs : _jsx)(type, props, key);
11
+ }
12
+ function resolvedStyleFor(classStrRaw, userStyle, pressed) {
13
+ const resolved = resolveStyle(classStrRaw, pressed);
14
+ if (userStyle === void 0) {
15
+ return resolved;
16
+ }
17
+ if (typeof userStyle === "function") {
18
+ return [resolved, userStyle({ pressed })];
19
+ }
20
+ return [resolved, userStyle];
21
+ }
22
+ function processElement(type, rawProps, key, isStaticChildren) {
23
+ if (type === null || typeof type === "symbol" || !rawProps) {
24
+ return makeElement(isStaticChildren, type, rawProps ?? {}, key);
25
+ }
26
+ const { className: classStrRaw, style: userStyle, ...rest } = rawProps;
27
+ if (typeof classStrRaw !== "string") {
28
+ return makeElement(isStaticChildren, type, rawProps, key);
29
+ }
30
+ const finalStyle = type === Pressable ? (state) => resolvedStyleFor(classStrRaw, userStyle, state.pressed) : resolvedStyleFor(classStrRaw, userStyle, false);
31
+ return makeElement(isStaticChildren, type, { ...rest, style: finalStyle }, key);
32
+ }
33
+ function jsx(type, props, key) {
34
+ return processElement(type, props, key, false);
35
+ }
36
+ function jsxs(type, props, key) {
37
+ return processElement(type, props, key, true);
38
+ }
39
+
40
+ export {
41
+ Fragment,
42
+ jsx,
43
+ jsxs
44
+ };
@@ -0,0 +1,374 @@
1
+ // src/generatedPalette.ts
2
+ var PALETTE = {
3
+ "amber-1": "#fffbeb",
4
+ "amber-10": "#78350f",
5
+ "amber-11": "#451a03",
6
+ "amber-12": "#291002",
7
+ "amber-2": "#fef3c7",
8
+ "amber-3": "#fde68a",
9
+ "amber-4": "#fcd34d",
10
+ "amber-5": "#fbbf24",
11
+ "amber-6": "#f59e0b",
12
+ "amber-7": "#d97706",
13
+ "amber-8": "#b45309",
14
+ "amber-9": "#92400e",
15
+ "black": "#000000",
16
+ "blue-1": "#eff6ff",
17
+ "blue-10": "#1e3a8a",
18
+ "blue-11": "#172554",
19
+ "blue-12": "#0d1633",
20
+ "blue-2": "#dbeafe",
21
+ "blue-3": "#bfdbfe",
22
+ "blue-4": "#93c5fd",
23
+ "blue-5": "#60a5fa",
24
+ "blue-6": "#3b82f6",
25
+ "blue-7": "#2563eb",
26
+ "blue-8": "#1d4ed8",
27
+ "blue-9": "#1e40af",
28
+ "bronze": "#cd7f32",
29
+ "coal": "#1c1c1c",
30
+ "copper": "#b87333",
31
+ "current": "currentColor",
32
+ "cyan-1": "#ecfeff",
33
+ "cyan-10": "#164e63",
34
+ "cyan-11": "#083344",
35
+ "cyan-12": "#041e28",
36
+ "cyan-2": "#cffafe",
37
+ "cyan-3": "#a5f3fc",
38
+ "cyan-4": "#67e8f9",
39
+ "cyan-5": "#22d3ee",
40
+ "cyan-6": "#06b6d4",
41
+ "cyan-7": "#0891b2",
42
+ "cyan-8": "#0e7490",
43
+ "cyan-9": "#155e75",
44
+ "emerald-1": "#ecfdf5",
45
+ "emerald-10": "#064e3b",
46
+ "emerald-11": "#022c22",
47
+ "emerald-12": "#011a14",
48
+ "emerald-2": "#d1fae5",
49
+ "emerald-3": "#a7f3d0",
50
+ "emerald-4": "#6ee7b7",
51
+ "emerald-5": "#34d399",
52
+ "emerald-6": "#10b981",
53
+ "emerald-7": "#059669",
54
+ "emerald-8": "#047857",
55
+ "emerald-9": "#065f46",
56
+ "fuchsia-1": "#fdf4ff",
57
+ "fuchsia-10": "#701a75",
58
+ "fuchsia-11": "#4a044e",
59
+ "fuchsia-12": "#2d022f",
60
+ "fuchsia-2": "#fae8ff",
61
+ "fuchsia-3": "#f5d0fe",
62
+ "fuchsia-4": "#f0abfc",
63
+ "fuchsia-5": "#e879f9",
64
+ "fuchsia-6": "#d946ef",
65
+ "fuchsia-7": "#c026d3",
66
+ "fuchsia-8": "#a21caf",
67
+ "fuchsia-9": "#86198f",
68
+ "gold": "#d4af37",
69
+ "gray-1": "#f9fafb",
70
+ "gray-10": "#111827",
71
+ "gray-11": "#030712",
72
+ "gray-12": "#020409",
73
+ "gray-2": "#f3f4f6",
74
+ "gray-3": "#e5e7eb",
75
+ "gray-4": "#d1d5db",
76
+ "gray-5": "#9ca3af",
77
+ "gray-6": "#6b7280",
78
+ "gray-7": "#4b5563",
79
+ "gray-8": "#374151",
80
+ "gray-9": "#1f2937",
81
+ "green-1": "#f0fdf4",
82
+ "green-10": "#14532d",
83
+ "green-11": "#052e16",
84
+ "green-12": "#031b0d",
85
+ "green-2": "#dcfce7",
86
+ "green-3": "#bbf7d0",
87
+ "green-4": "#86efac",
88
+ "green-5": "#4ade80",
89
+ "green-6": "#22c55e",
90
+ "green-7": "#16a34a",
91
+ "green-8": "#15803d",
92
+ "green-9": "#166534",
93
+ "indigo-1": "#eef2ff",
94
+ "indigo-10": "#312e81",
95
+ "indigo-11": "#1e1b4b",
96
+ "indigo-12": "#12102d",
97
+ "indigo-2": "#e0e7ff",
98
+ "indigo-3": "#c7d2fe",
99
+ "indigo-4": "#a5b4fc",
100
+ "indigo-5": "#818cf8",
101
+ "indigo-6": "#6366f1",
102
+ "indigo-7": "#4f46e5",
103
+ "indigo-8": "#4338ca",
104
+ "indigo-9": "#3730a3",
105
+ "lime-1": "#f7fee7",
106
+ "lime-10": "#365314",
107
+ "lime-11": "#1a2e05",
108
+ "lime-12": "#0f1b03",
109
+ "lime-2": "#ecfccb",
110
+ "lime-3": "#d9f99d",
111
+ "lime-4": "#bef264",
112
+ "lime-5": "#a3e635",
113
+ "lime-6": "#84cc16",
114
+ "lime-7": "#65a30d",
115
+ "lime-8": "#4d7c0f",
116
+ "lime-9": "#3f6212",
117
+ "neutral-1": "#fafafa",
118
+ "neutral-10": "#171717",
119
+ "neutral-11": "#0a0a0a",
120
+ "neutral-12": "#050505",
121
+ "neutral-2": "#f5f5f5",
122
+ "neutral-3": "#e5e5e5",
123
+ "neutral-4": "#d4d4d4",
124
+ "neutral-5": "#a3a3a3",
125
+ "neutral-6": "#737373",
126
+ "neutral-7": "#525252",
127
+ "neutral-8": "#404040",
128
+ "neutral-9": "#262626",
129
+ "orange-1": "#fff7ed",
130
+ "orange-10": "#7c2d12",
131
+ "orange-11": "#431407",
132
+ "orange-12": "#270c04",
133
+ "orange-2": "#ffedd5",
134
+ "orange-3": "#fed7aa",
135
+ "orange-4": "#fdba74",
136
+ "orange-5": "#fb923c",
137
+ "orange-6": "#f97316",
138
+ "orange-7": "#ea580c",
139
+ "orange-8": "#c2410c",
140
+ "orange-9": "#9a3412",
141
+ "pink-1": "#fdf2f8",
142
+ "pink-10": "#831843",
143
+ "pink-11": "#500724",
144
+ "pink-12": "#300415",
145
+ "pink-2": "#fce7f3",
146
+ "pink-3": "#fbcfe8",
147
+ "pink-4": "#f9a8d4",
148
+ "pink-5": "#f472b6",
149
+ "pink-6": "#ec4899",
150
+ "pink-7": "#db2777",
151
+ "pink-8": "#be185d",
152
+ "pink-9": "#9d174d",
153
+ "purple-1": "#faf5ff",
154
+ "purple-10": "#581c87",
155
+ "purple-11": "#3b0764",
156
+ "purple-12": "#23043c",
157
+ "purple-2": "#f3e8ff",
158
+ "purple-3": "#e9d5ff",
159
+ "purple-4": "#d8b4fe",
160
+ "purple-5": "#c084fc",
161
+ "purple-6": "#a855f7",
162
+ "purple-7": "#9333ea",
163
+ "purple-8": "#7e22ce",
164
+ "purple-9": "#6b21a8",
165
+ "red-1": "#fef2f2",
166
+ "red-10": "#7f1d1d",
167
+ "red-11": "#450a0a",
168
+ "red-12": "#280606",
169
+ "red-2": "#fee2e2",
170
+ "red-3": "#fecaca",
171
+ "red-4": "#fca5a5",
172
+ "red-5": "#f87171",
173
+ "red-6": "#ef4444",
174
+ "red-7": "#dc2626",
175
+ "red-8": "#b91c1c",
176
+ "red-9": "#991b1b",
177
+ "rose-1": "#fff1f2",
178
+ "rose-10": "#881337",
179
+ "rose-11": "#4c0519",
180
+ "rose-12": "#2d030e",
181
+ "rose-2": "#ffe4e6",
182
+ "rose-3": "#fecdd3",
183
+ "rose-4": "#fda4af",
184
+ "rose-5": "#fb7185",
185
+ "rose-6": "#f43f5e",
186
+ "rose-7": "#e11d48",
187
+ "rose-8": "#be123c",
188
+ "rose-9": "#9f1239",
189
+ "silver": "#c0c0c0",
190
+ "sky-1": "#f0f9ff",
191
+ "sky-10": "#0c4a6e",
192
+ "sky-11": "#082f49",
193
+ "sky-12": "#041b2b",
194
+ "sky-2": "#e0f2fe",
195
+ "sky-3": "#bae6fd",
196
+ "sky-4": "#7dd3fc",
197
+ "sky-5": "#38bdf8",
198
+ "sky-6": "#0ea5e9",
199
+ "sky-7": "#0284c7",
200
+ "sky-8": "#0369a1",
201
+ "sky-9": "#075985",
202
+ "slate-1": "#f8fafc",
203
+ "slate-10": "#0f172a",
204
+ "slate-11": "#020617",
205
+ "slate-12": "#01020a",
206
+ "slate-2": "#f1f5f9",
207
+ "slate-3": "#e2e8f0",
208
+ "slate-4": "#cbd5e1",
209
+ "slate-5": "#94a3b8",
210
+ "slate-6": "#64748b",
211
+ "slate-7": "#475569",
212
+ "slate-8": "#334155",
213
+ "slate-9": "#1e293b",
214
+ "stone-1": "#fafaf9",
215
+ "stone-10": "#1c1917",
216
+ "stone-11": "#0c0a09",
217
+ "stone-12": "#070605",
218
+ "stone-2": "#f5f5f4",
219
+ "stone-3": "#e7e5e4",
220
+ "stone-4": "#d6d3d1",
221
+ "stone-5": "#a8a29e",
222
+ "stone-6": "#78716c",
223
+ "stone-7": "#57534e",
224
+ "stone-8": "#44403c",
225
+ "stone-9": "#292524",
226
+ "teal-1": "#f0fdfa",
227
+ "teal-10": "#134e4a",
228
+ "teal-11": "#042f2e",
229
+ "teal-12": "#021c1b",
230
+ "teal-2": "#ccfbf1",
231
+ "teal-3": "#99f6e4",
232
+ "teal-4": "#5eead4",
233
+ "teal-5": "#2dd4bf",
234
+ "teal-6": "#14b8a6",
235
+ "teal-7": "#0d9488",
236
+ "teal-8": "#0f766e",
237
+ "teal-9": "#115e59",
238
+ "transparent": "transparent",
239
+ "violet-1": "#f5f3ff",
240
+ "violet-10": "#4c1d95",
241
+ "violet-11": "#2e1065",
242
+ "violet-12": "#1c0a3d",
243
+ "violet-2": "#ede9fe",
244
+ "violet-3": "#ddd6fe",
245
+ "violet-4": "#c4b5fd",
246
+ "violet-5": "#a78bfa",
247
+ "violet-6": "#8b5cf6",
248
+ "violet-7": "#7c3aed",
249
+ "violet-8": "#6d28d9",
250
+ "violet-9": "#5b21b6",
251
+ "white": "#ffffff",
252
+ "yellow-1": "#fefce8",
253
+ "yellow-10": "#713f12",
254
+ "yellow-11": "#422006",
255
+ "yellow-12": "#271304",
256
+ "yellow-2": "#fef9c3",
257
+ "yellow-3": "#fef08a",
258
+ "yellow-4": "#fde047",
259
+ "yellow-5": "#facc15",
260
+ "yellow-6": "#eab308",
261
+ "yellow-7": "#ca8a04",
262
+ "yellow-8": "#a16207",
263
+ "yellow-9": "#854d0e",
264
+ "zinc-1": "#fafafa",
265
+ "zinc-10": "#18181b",
266
+ "zinc-11": "#09090b",
267
+ "zinc-12": "#050506",
268
+ "zinc-2": "#f4f4f5",
269
+ "zinc-3": "#e4e4e7",
270
+ "zinc-4": "#d4d4d8",
271
+ "zinc-5": "#a1a1aa",
272
+ "zinc-6": "#71717a",
273
+ "zinc-7": "#52525b",
274
+ "zinc-8": "#3f3f46",
275
+ "zinc-9": "#27272a"
276
+ };
277
+
278
+ // src/theme.ts
279
+ var defaultTheme = {
280
+ colors: PALETTE,
281
+ spacing: {
282
+ // "0" is always a hardcoded zero in real Tailwind's own scale too,
283
+ // regardless of the base spacing unit — needed for `p-0`/`m-0`/etc.
284
+ // Mirrors the identical fix in @kbach/react's theme.ts.
285
+ "0": 0,
286
+ "1": 4,
287
+ "2": 8,
288
+ "4": 16,
289
+ "6": 24,
290
+ "8": 32
291
+ },
292
+ screens: {},
293
+ darkMode: "attribute"
294
+ };
295
+ var activeTheme = defaultTheme;
296
+ var activeThemeJson = JSON.stringify(defaultTheme);
297
+ function setTheme(theme) {
298
+ activeTheme = theme;
299
+ activeThemeJson = JSON.stringify(theme);
300
+ }
301
+ function getTheme() {
302
+ return activeTheme;
303
+ }
304
+ function getThemeJson() {
305
+ return activeThemeJson;
306
+ }
307
+
308
+ // src/darkModeStore.ts
309
+ import { Appearance } from "react-native";
310
+ var listeners = /* @__PURE__ */ new Set();
311
+ function systemPrefersDark() {
312
+ return Appearance.getColorScheme() === "dark";
313
+ }
314
+ function resolveIsDark(m) {
315
+ return m === "system" ? systemPrefersDark() : m === "dark";
316
+ }
317
+ var mode = "system";
318
+ var isDark = resolveIsDark(mode);
319
+ var hasExplicitChoice = false;
320
+ var subscription = Appearance.addChangeListener(({ colorScheme }) => {
321
+ if (mode !== "system") return;
322
+ isDark = colorScheme === "dark";
323
+ notify();
324
+ });
325
+ function notify() {
326
+ for (const listener of listeners) listener();
327
+ }
328
+ function getGlobalDarkMode() {
329
+ return isDark;
330
+ }
331
+ function getGlobalThemeMode() {
332
+ return mode;
333
+ }
334
+ function setGlobalThemeMode(nextMode) {
335
+ mode = nextMode;
336
+ isDark = resolveIsDark(nextMode);
337
+ hasExplicitChoice = true;
338
+ notify();
339
+ }
340
+ function toggleGlobalDarkMode() {
341
+ setGlobalThemeMode(isDark ? "light" : "dark");
342
+ }
343
+ function subscribeGlobalDarkMode(listener) {
344
+ listeners.add(listener);
345
+ return () => listeners.delete(listener);
346
+ }
347
+ function seedDefaultMode(fallback) {
348
+ if (hasExplicitChoice || mode === fallback) return;
349
+ mode = fallback;
350
+ isDark = resolveIsDark(fallback);
351
+ notify();
352
+ }
353
+
354
+ // src/nativeBridge.ts
355
+ import { TurboModuleRegistry } from "react-native";
356
+ function resolveStyle(classString, pressed = false) {
357
+ const KbachModule = TurboModuleRegistry.getEnforcing("KbachModule");
358
+ const colorScheme = getGlobalDarkMode() ? "dark" : "light";
359
+ const json = KbachModule.resolveStyle(classString, getThemeJson(), colorScheme, pressed);
360
+ return JSON.parse(json);
361
+ }
362
+
363
+ export {
364
+ defaultTheme,
365
+ setTheme,
366
+ getTheme,
367
+ getGlobalDarkMode,
368
+ getGlobalThemeMode,
369
+ setGlobalThemeMode,
370
+ toggleGlobalDarkMode,
371
+ subscribeGlobalDarkMode,
372
+ seedDefaultMode,
373
+ resolveStyle
374
+ };
@@ -0,0 +1,148 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface ThemeConfig {
5
+ /** Plain hex only, this pass — e.g. "blue-6" -> "#2563eb" */
6
+ colors: Record<string, string>;
7
+ spacing: Record<string, number>;
8
+ screens: Record<string, number>;
9
+ darkMode: 'attribute' | 'class' | 'media';
10
+ }
11
+ declare const defaultTheme: ThemeConfig;
12
+ declare function setTheme(theme: ThemeConfig): void;
13
+ declare function getTheme(): ThemeConfig;
14
+
15
+ interface StyleObject {
16
+ [key: string]: string | number;
17
+ }
18
+ /**
19
+ * Calls the Rust engine via the KbachModule TurboModule (see
20
+ * apps/native-sandbox/android/.../KbachModule.kt and
21
+ * packages/core-engine/src/resolve_style.rs) and returns a plain style
22
+ * object ready for RN's `style` prop. Synchronous — no async init step,
23
+ * unlike the web/WASM path, since JSI-backed TurboModule methods are
24
+ * available as soon as the module is loaded.
25
+ *
26
+ * Reads `getGlobalDarkMode()` (darkModeStore.ts) fresh on every call (no
27
+ * caching) so `dark:` resolves correctly whenever this runs. That store —
28
+ * not a direct `Appearance.getColorScheme()` read, which this function
29
+ * used prior to `<ThemeProvider>` support — is what makes an explicit
30
+ * `setGlobalThemeMode('dark')`/`toggle()` override actually affect
31
+ * rendering; the store itself falls back to tracking `Appearance` live
32
+ * whenever the mode is `'system'` (its own default), so nothing regresses
33
+ * for apps that never touch the override API at all. This function itself
34
+ * has no subscription of its own, so nothing re-renders just because the
35
+ * store's value changed — call `useGlobalDarkMode()` (or consume
36
+ * `useTheme()`'s `isDark`) somewhere in the tree for that, NOT React
37
+ * Native's own `useColorScheme()`, which only reflects the raw OS setting
38
+ * and would miss an explicit override entirely.
39
+ *
40
+ * Phase 11 originally shipped with live in-place theme switching unverified
41
+ * (a `uiMode` toggle triggered a re-render but `Appearance.getColorScheme()`
42
+ * kept returning a stale value). Root-caused in Phase 12 while debugging an
43
+ * unrelated `active:` failure: `apps/native-sandbox` has its own separate
44
+ * `node_modules/react-native` (excluded from the npm workspace since Phase
45
+ * 5), while `packages/react-native` resolved `react-native` hierarchically
46
+ * up to a *different* copy at the repo root — two physical module
47
+ * instances, so a `useColorScheme()` subscription and an
48
+ * `Appearance.getColorScheme()` read were silently talking to two separate
49
+ * copies of the Appearance module. Fixed by forcing a single instance via
50
+ * `resolver.resolveRequest` in apps/native-sandbox/metro.config.js; live
51
+ * theme switching (and `active:`, the bug that surfaced this) both verified
52
+ * working after the fix — see Phase 12. Still relevant here: darkModeStore.ts
53
+ * itself calls `Appearance.addChangeListener`, so the same dual-instance
54
+ * failure mode would silently break the 'system' fallback path too if that
55
+ * metro.config.js fix ever regressed.
56
+ *
57
+ * `pressed` gates the `active:` modifier — unlike `colorScheme`, there's no
58
+ * system-level getter for "is this element currently pressed"; only RN's
59
+ * `Pressable` ever knows this, so it's passed in directly by the caller
60
+ * (jsx-runtime, from Pressable's own style-function callback) rather than
61
+ * queried here. Defaults to `false` for every other element type.
62
+ */
63
+ declare function resolveStyle(classString: string, pressed?: boolean): StyleObject;
64
+
65
+ /**
66
+ * Native port of @kbach/react's darkModeStore.ts — same architecture (a
67
+ * single module-level store is the real source of truth;
68
+ * ThemeProvider/useTheme are a thin, OPTIONAL wrapper around it; every
69
+ * function here works standalone with no provider mounted anywhere), with
70
+ * two real platform differences from the web version:
71
+ *
72
+ * 1. No persistence. There's no React-Native-builtin equivalent of
73
+ * localStorage — adding one would mean forcing a real native dependency
74
+ * (e.g. @react-native-async-storage/async-storage) on every consumer,
75
+ * even ones that don't want it. The mode always starts at 'system' on a
76
+ * fresh app launch. If you want persistence, read your own storage at
77
+ * startup (before your first render) and call setGlobalThemeMode() with
78
+ * the result — same "seed once, no built-in write-back" shape as
79
+ * seedDefaultMode() below, just driven by your own storage instead of
80
+ * one this package picks for you.
81
+ * 2. No DOM to "apply" the resolved mode to. On web, applyToDom() flips a
82
+ * class/attribute the generated CSS's `dark:` selectors match against.
83
+ * On native there's no CSS at all — nativeBridge.ts's resolveStyle()
84
+ * reads getGlobalDarkMode() directly on every call instead of querying
85
+ * Appearance.getColorScheme() itself, which is what actually makes an
86
+ * explicit override affect rendering (previously that function always
87
+ * read the raw OS setting, so it had no way to reflect a user choice —
88
+ * see nativeBridge.ts's own comment on this for why that's the one
89
+ * genuinely fixed bug this change introduces, not just a new feature).
90
+ */
91
+ type ThemeMode = 'light' | 'dark' | 'system';
92
+ declare function getGlobalDarkMode(): boolean;
93
+ declare function getGlobalThemeMode(): ThemeMode;
94
+ /** Explicit user choice — takes priority over any later `<ThemeProvider defaultMode>` seed (see `seedDefaultMode` below). Not persisted; see this module's own doc comment. */
95
+ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
96
+ declare function toggleGlobalDarkMode(): void;
97
+ declare function subscribeGlobalDarkMode(listener: () => void): () => void;
98
+
99
+ /**
100
+ * Reactive read of the global dark-mode store — works with or without a
101
+ * `<ThemeProvider>` mounted anywhere in the tree, since it subscribes to
102
+ * `darkModeStore.ts` directly. No server-snapshot argument (unlike
103
+ * @kbach/react's web version) — React Native has no SSR concept at all,
104
+ * so there's no third argument for `useSyncExternalStore` to need.
105
+ */
106
+ declare function useGlobalDarkMode(): boolean;
107
+
108
+ interface ThemeProviderProps {
109
+ children: ReactNode;
110
+ /**
111
+ * Applied once, on this provider's first mount, but ONLY if nothing has
112
+ * made an explicit mode choice yet (see
113
+ * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
114
+ * wins over a component's own default. Unlike @kbach/react's web
115
+ * version, there's no persisted value to check here — see
116
+ * darkModeStore.ts's own doc comment on why this package doesn't persist
117
+ * the mode across app launches.
118
+ */
119
+ defaultMode?: ThemeMode;
120
+ }
121
+ /**
122
+ * Native port of @kbach/react's `ThemeProvider` — a thin, OPTIONAL React
123
+ * Context wrapper around `darkModeStore.ts`'s single global store, not the
124
+ * source of truth itself. Because the store is global, MULTIPLE mounted
125
+ * providers share the exact same `mode`/`isDark` state rather than each
126
+ * getting their own scoped value — mounting more than one is very likely a
127
+ * mistake (each fighting over the same state via a different
128
+ * `defaultMode`), so this warns once in dev, matching the web version's
129
+ * identical guard.
130
+ */
131
+ declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
132
+
133
+ interface ThemeContextValue {
134
+ mode: ThemeMode;
135
+ isDark: boolean;
136
+ setMode: (mode: ThemeMode) => void;
137
+ toggle: () => void;
138
+ }
139
+ /**
140
+ * Requires a `<ThemeProvider>` ancestor — throws otherwise, since a
141
+ * component that destructures `{ mode, setMode }` genuinely needs Context
142
+ * (unlike a component that just wants the dark/light boolean, which should
143
+ * reach for `useGlobalDarkMode()` instead and never needs a provider at
144
+ * all).
145
+ */
146
+ declare function useTheme(): ThemeContextValue;
147
+
148
+ export { type StyleObject, type ThemeConfig, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, defaultTheme, getGlobalDarkMode, getGlobalThemeMode, getTheme, resolveStyle, setGlobalThemeMode, setTheme, subscribeGlobalDarkMode, toggleGlobalDarkMode, useGlobalDarkMode, useTheme };