@kbach/react-native 1.0.0-beta.6 → 1.0.0-beta.8

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
@@ -5,9 +5,12 @@ 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
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
8
+ > Kotlin/JNI TurboModule to an inline style object; Expo Web /
9
+ > react-native-web resolves through the same Rust engine compiled to WASM,
10
+ > but to real generated CSS instead (full modifier support, including
11
+ > `hover:`/`focus:`/etc. — see "Expo Web" below); Expo Go — which can load
12
+ > neither a TurboModule nor WASM — resolves through a third, pure-JS
13
+ > fallback engine with reduced utility coverage (see "Expo Go" below). The
11
14
  > native module ships prebuilt (see Setup below) — no Rust toolchain or
12
15
  > manual native wiring required on any platform.
13
16
 
@@ -22,8 +25,8 @@ prebuilt native `.so` ship inside this same package's own `android/`
22
25
  folder. Android's autolinking discovers it automatically the moment it's
23
26
  in `node_modules`; no manual `MainApplication.kt` edit needed. Works the
24
27
  same way whether your app was created with the React Native CLI or with
25
- Expo — **except Expo Go**, which can never load this (or any) custom
26
- native module; see below.
28
+ Expo, **including Expo Go** — see its own section below for the scope
29
+ tradeoff that involves.
27
30
 
28
31
  ### React Native CLI
29
32
 
@@ -47,18 +50,53 @@ one needs (a plain single-app install doesn't need any of that — it's
47
50
  only relevant if you're linking packages locally instead of installing
48
51
  from npm).
49
52
 
50
- ### Expo (native — Android)
51
-
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:
53
+ ### Expo Go
54
+
55
+ **Works — no prebuild, no dev client, no extra setup.** Expo Go is a
56
+ single fixed binary Expo pre-builds and ships through the app stores,
57
+ containing only the native code Expo bundled into it at their own build
58
+ time — there is no mechanism for *any* third-party package to add native
59
+ code to it at runtime, whether that's a raw TurboModule (what this
60
+ package's Android/Expo-dev-client path uses) or even Expo's own Modules
61
+ API. So instead of trying to load one, this package detects that the
62
+ TurboModule isn't registered (`TurboModuleRegistry.get('KbachModule')`
63
+ returns `null` rather than throwing) and transparently falls back to a
64
+ **third resolution engine**: a pure-JavaScript reimplementation of the
65
+ same class-resolution logic, ported by hand from
66
+ [`resolve_style.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolve_style.rs)
67
+ and its resolvers (see
68
+ [`src/jsEngine`](https://github.com/semsakadanupol/kbach/blob/main/packages/react-native/src/jsEngine)
69
+ in the repo) — no WASM (Hermes has no `WebAssembly` API at all) and no
70
+ native code, so it can run anywhere plain JS runs, Expo Go included. A
71
+ real dev-client or CLI build always has the TurboModule registered, so
72
+ this fallback activates ONLY inside Expo Go itself; every other
73
+ environment is completely unaffected.
74
+
75
+ The tradeoff: the JS engine covers layout, spacing, border/radius,
76
+ plain-hex colors (`bg-*`/`text-*`, no opacity composition), font-weight,
77
+ text-transform (`uppercase`/`lowercase`/`capitalize`), text-decoration-line
78
+ (`underline`/`line-through`/`no-underline`), `italic`/`not-italic`, numeric
79
+ line-height and arbitrary letter-spacing, and `shadow-*` — the same
80
+ native-representable subset
81
+ `resolve_utility_native` (the Rust engine's own native dispatcher) covers,
82
+ minus a WASM/JNI dependency. It does **not** cover anything that subset
83
+ already excludes on native (grid, transforms, filters, gradients, DOM-only
84
+ typography like `truncate`/`decoration-*`/`indent-*`, interactivity/scroll
85
+ utilities) — those were never available outside a dev client/CLI build,
86
+ Expo Go doesn't change that. `dark:`/`active:`/`sm:`-`2xl:` modifiers work
87
+ identically to the native/WASM engines (same gating logic, just inlined
88
+ rather than routed through a shared registry). If you hit a class that
89
+ silently doesn't apply only in Expo Go, check whether it resolves under a
90
+ dev client first — if it does, it's outside this fallback's ported scope,
91
+ not a bug in the class itself.
92
+
93
+ ### Expo (dev client / prebuild — Android)
94
+
95
+ A dev client gets you the *full* native engine (the same one the CLI path
96
+ above uses) instead of the JS fallback above — worth it once you need
97
+ something outside that fallback's scope. Built once via `expo prebuild`/
98
+ `expo run:android`, functionally the same "real native project" as the CLI
99
+ path above:
62
100
 
63
101
  ```js
64
102
  // babel.config.js
@@ -92,26 +130,100 @@ needs (a plain single-app install doesn't need it).
92
130
  **Works — no extra setup, no separate package, no init call.** `expo start
93
131
  --web` (and EAS's web builds) render through `react-native-web` —
94
132
  `View`/`Text`/etc. become real DOM elements with real CSS, not a
95
- Kotlin/JNI bridge, so on web this package resolves classes through the
96
- same Rust engine [`@kbach/react`](https://www.npmjs.com/package/@kbach/react)
97
- uses — WASM instead of JNI — automatically. Metro picks the right
98
- implementation per platform on its own via this package's `"browser"`
99
- export condition; your app code and JSX never change. The WASM module
100
- initializes itself eagerly (synchronously, at import time — no `await`
101
- needed before your first render, unlike a from-scratch WASM setup).
102
-
103
- The only thing this doesn't cover on web is an iOS-parity feature gap —
104
- everything web-side resolves through the exact same
105
- [`resolve_style.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolve_style.rs)
106
- logic real native uses, so see this README's "What this is" section above
107
- for what's covered.
133
+ Kotlin/JNI bridge — so on web this package resolves classes through the
134
+ same real-CSS engine
135
+ [`@kbach/react`](https://www.npmjs.com/package/@kbach/react) uses on the
136
+ DOM, not the inline-style-object engine native/Expo Go use. Metro picks
137
+ the right implementation per platform on its own via this package's
138
+ `"browser"` (client bundles) and `"node"` (Expo Router's static-rendering/
139
+ SSR bundles — a genuinely separate resolution axis from `platform: 'web'`,
140
+ see `nativeBridge.web.ts`'s own doc comment) export conditions; your app
141
+ code and JSX never change. The WASM module initializes itself eagerly
142
+ (synchronously, at import time — no `await` needed before your first
143
+ render, unlike a from-scratch WASM setup).
144
+
145
+ **Full modifier support, including `hover:`/`focus:`/`group-hover:`/
146
+ `has-[...]`/container queries/etc.** — every modifier
147
+ [`registry.rs`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/registry.rs)
148
+ knows, not just the `dark:`/`active:`/responsive subset native/Expo Go
149
+ gate via a live JS parameter. This is possible specifically *because* Expo
150
+ Web is real DOM: real CSS selectors (`:hover`, `:focus`, `@media`,
151
+ `@container`, ...) work exactly the way they do for `@kbach/react`, no
152
+ polyfilling needed. The one wrinkle: `react-native-web`'s `View`/`Text`/
153
+ `Pressable` don't forward a `className` prop to the DOM at all (checked
154
+ against its own `forwardedProps` whitelist), so this can't render a
155
+ literal `className` the way `@kbach/react` does on plain DOM. What it uses
156
+ instead: RN's `dataSet` prop (which RNW DOES forward, as real `data-*`
157
+ attributes) carries the resolved class text into a `data-kb="..."`
158
+ attribute, and the generated CSS targets it with `[data-kb~="..."]`
159
+ attribute selectors instead of `.class` selectors — functionally
160
+ identical to a real `className`, just reached through RN's own prop
161
+ system instead. This also means `active:` is now REAL CSS `:active` (the
162
+ browser's own press-state detection), not a `Pressable`-specific style-
163
+ function simulation the way native's `active:` still is — one fewer
164
+ special case, and closer to actual Tailwind's own semantics.
165
+
166
+ Everything else about this matches `@kbach/react`'s own web engine
167
+ exactly — same
168
+ [`resolve_utility`](https://github.com/semsakadanupol/kbach/blob/main/packages/core-engine/src/resolvers/mod.rs)
169
+ dispatch (not the reduced `resolve_utility_native` subset), so grid,
170
+ transforms, filters, gradients, and DOM-only typography (`truncate`,
171
+ `decoration-*`, etc.) all resolve on Expo Web even though they don't on
172
+ native or Expo Go — see this README's "What this is" section for what's
173
+ covered elsewhere.
174
+
175
+ #### Expo Router static rendering: `dark:`/responsive on the FIRST paint
176
+
177
+ If you use [static rendering](https://docs.expo.dev/router/reference/static-rendering/)
178
+ (`web.output: "static"` — Expo Router's SSR/pre-render feature), any
179
+ `dark:`/`sm:`/etc. class on content that's part of the **initial,
180
+ pre-rendered HTML** resolves against whatever the **server** (Node.js, no
181
+ real display/viewport) reports — not the visiting browser's actual OS
182
+ preference or window size. React's hydration then adopts that
183
+ server-rendered markup as-is rather than silently overwriting it with the
184
+ client's own (correct) values, because a `style`-attribute-only difference
185
+ doesn't fail hydration the way a text-content mismatch does. Concretely: a
186
+ user with their OS in dark mode can see a `dark:` class resolve to its
187
+ *light* value on first paint, with no error or warning anywhere.
188
+
189
+ **This isn't a Kbach bug** — it's the same fundamental constraint every
190
+ OS-preference-based styling system has under SSR (Next.js's own dark-mode
191
+ docs describe the identical problem and fix). The standard fix applies
192
+ here unchanged: don't resolve preference-dependent classes on
193
+ initially-server-rendered content at all; defer them to after your app has
194
+ mounted client-side, e.g.:
195
+
196
+ ```tsx
197
+ import { useEffect, useState } from 'react';
198
+ import { Text, View } from 'react-native';
199
+
200
+ function ThemedScreen() {
201
+ const [mounted, setMounted] = useState(false);
202
+ useEffect(() => setMounted(true), []);
203
+
204
+ // Neutral on the server AND on the client's first paint (matches exactly,
205
+ // no hydration mismatch) — only becomes preference-aware once mounted.
206
+ return (
207
+ <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'}>
208
+ <Text>Hello</Text>
209
+ </View>
210
+ );
211
+ }
212
+ ```
213
+
214
+ Content that only ever renders client-side (not part of the statically
215
+ pre-rendered route) is unaffected — this only matters for what's visible
216
+ before your first `useEffect` runs.
108
217
 
109
218
  ## What this is
110
219
 
111
220
  - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
112
- `className`/`kb` props on every element and calls into the native/WASM
113
- bridge (whichever platform you're on) to resolve them to a style object,
114
- synchronously, on every render.
221
+ `className`/`kb` props on every element and calls into the native, WASM,
222
+ or pure-JS bridge (whichever platform you're on — see the Setup section
223
+ above), synchronously, on every render. On native and Expo Go this
224
+ resolves to an inline `style` object; on Expo Web it resolves to real
225
+ injected CSS plus a `dataSet={{ kb: '...' }}` prop instead — see "Expo
226
+ Web" below for why the two differ.
115
227
  - A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
116
228
  app's own files (never `node_modules`) through that JSX runtime via a
117
229
  per-file `@jsxImportSource` pragma.
@@ -129,12 +241,15 @@ for what's covered.
129
241
  arbitrary `shadow-[...]` values aren't supported — RN's native shadow
130
242
  props can't express an inset shadow, and an arbitrary value is a raw CSS
131
243
  string with no mechanical translation into discrete native props.
132
- - Responsive breakpoints (`sm:`/`md:`/`lg:`/`xl:`/`2xl:`) — gated live
133
- against the current window width (`Dimensions.get('window').width`),
134
- same mobile-first "min-width and up" semantics as web, and freely
135
- combinable with `dark:`/`active:` (e.g. `dark:md:bg-blue-8`). Call RN's
136
- own `useWindowDimensions()` somewhere in your tree to re-render on
137
- rotation/resize — same role `useColorScheme()` plays for `dark:` below.
244
+ - Responsive breakpoints (`sm:`/`md:`/`lg:`/`xl:`/`2xl:`) — same
245
+ mobile-first "min-width and up" semantics as web, freely combinable with
246
+ `dark:`/`active:` (e.g. `dark:md:bg-blue-8`). On native/Expo Go, gated
247
+ live against the current window width (`Dimensions.get('window').width`)
248
+ — call RN's own `useWindowDimensions()` somewhere in your tree to
249
+ re-render on rotation/resize, same role `useColorScheme()` plays for
250
+ `dark:` below. On Expo Web this is real `@media (min-width: ...)` CSS
251
+ instead (see "Expo Web" above) — the browser evaluates it continuously
252
+ on its own, so no `useWindowDimensions()` call is needed there at all.
138
253
 
139
254
  ## Dark mode
140
255
 
@@ -0,0 +1,98 @@
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
+ /**
16
+ * Native port of @kbach/react's darkModeStore.ts — same architecture (a
17
+ * single module-level store is the real source of truth;
18
+ * ThemeProvider/useTheme are a thin, OPTIONAL wrapper around it; every
19
+ * function here works standalone with no provider mounted anywhere), with
20
+ * two real platform differences from the web version:
21
+ *
22
+ * 1. No persistence. There's no React-Native-builtin equivalent of
23
+ * localStorage — adding one would mean forcing a real native dependency
24
+ * (e.g. @react-native-async-storage/async-storage) on every consumer,
25
+ * even ones that don't want it. The mode always starts at 'system' on a
26
+ * fresh app launch. If you want persistence, read your own storage at
27
+ * startup (before your first render) and call setGlobalThemeMode() with
28
+ * the result — same "seed once, no built-in write-back" shape as
29
+ * seedDefaultMode() below, just driven by your own storage instead of
30
+ * one this package picks for you.
31
+ * 2. No DOM to "apply" the resolved mode to. On web, applyToDom() flips a
32
+ * class/attribute the generated CSS's `dark:` selectors match against.
33
+ * On native there's no CSS at all — nativeBridge.ts's resolveStyle()
34
+ * reads getGlobalDarkMode() directly on every call instead of querying
35
+ * Appearance.getColorScheme() itself, which is what actually makes an
36
+ * explicit override affect rendering (previously that function always
37
+ * read the raw OS setting, so it had no way to reflect a user choice —
38
+ * see nativeBridge.ts's own comment on this for why that's the one
39
+ * genuinely fixed bug this change introduces, not just a new feature).
40
+ */
41
+ type ThemeMode = 'light' | 'dark' | 'system';
42
+ declare function getGlobalDarkMode(): boolean;
43
+ declare function getGlobalThemeMode(): ThemeMode;
44
+ /** Explicit user choice — takes priority over any later `<ThemeProvider defaultMode>` seed (see `seedDefaultMode` below). Not persisted; see this module's own doc comment. */
45
+ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
46
+ declare function toggleGlobalDarkMode(): void;
47
+ declare function subscribeGlobalDarkMode(listener: () => void): () => void;
48
+
49
+ /**
50
+ * Reactive read of the global dark-mode store — works with or without a
51
+ * `<ThemeProvider>` mounted anywhere in the tree, since it subscribes to
52
+ * `darkModeStore.ts` directly. No server-snapshot argument (unlike
53
+ * @kbach/react's web version) — React Native has no SSR concept at all,
54
+ * so there's no third argument for `useSyncExternalStore` to need.
55
+ */
56
+ declare function useGlobalDarkMode(): boolean;
57
+
58
+ interface ThemeProviderProps {
59
+ children: ReactNode;
60
+ /**
61
+ * Applied once, on this provider's first mount, but ONLY if nothing has
62
+ * made an explicit mode choice yet (see
63
+ * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
64
+ * wins over a component's own default. Unlike @kbach/react's web
65
+ * version, there's no persisted value to check here — see
66
+ * darkModeStore.ts's own doc comment on why this package doesn't persist
67
+ * the mode across app launches.
68
+ */
69
+ defaultMode?: ThemeMode;
70
+ }
71
+ /**
72
+ * Native port of @kbach/react's `ThemeProvider` — a thin, OPTIONAL React
73
+ * Context wrapper around `darkModeStore.ts`'s single global store, not the
74
+ * source of truth itself. Because the store is global, MULTIPLE mounted
75
+ * providers share the exact same `mode`/`isDark` state rather than each
76
+ * getting their own scoped value — mounting more than one is very likely a
77
+ * mistake (each fighting over the same state via a different
78
+ * `defaultMode`), so this warns once in dev, matching the web version's
79
+ * identical guard.
80
+ */
81
+ declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
82
+
83
+ interface ThemeContextValue {
84
+ mode: ThemeMode;
85
+ isDark: boolean;
86
+ setMode: (mode: ThemeMode) => void;
87
+ toggle: () => void;
88
+ }
89
+ /**
90
+ * Requires a `<ThemeProvider>` ancestor — throws otherwise, since a
91
+ * component that destructures `{ mode, setMode }` genuinely needs Context
92
+ * (unlike a component that just wants the dark/light boolean, which should
93
+ * reach for `useGlobalDarkMode()` instead and never needs a provider at
94
+ * all).
95
+ */
96
+ declare function useTheme(): ThemeContextValue;
97
+
98
+ export { type ThemeConfig as T, type ThemeContextValue as a, type ThemeMode as b, ThemeProvider as c, type ThemeProviderProps as d, defaultTheme as e, getGlobalThemeMode as f, getGlobalDarkMode as g, getTheme as h, setTheme as i, subscribeGlobalDarkMode as j, useTheme as k, setGlobalThemeMode as s, toggleGlobalDarkMode as t, useGlobalDarkMode as u };
@@ -0,0 +1,98 @@
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
+ /**
16
+ * Native port of @kbach/react's darkModeStore.ts — same architecture (a
17
+ * single module-level store is the real source of truth;
18
+ * ThemeProvider/useTheme are a thin, OPTIONAL wrapper around it; every
19
+ * function here works standalone with no provider mounted anywhere), with
20
+ * two real platform differences from the web version:
21
+ *
22
+ * 1. No persistence. There's no React-Native-builtin equivalent of
23
+ * localStorage — adding one would mean forcing a real native dependency
24
+ * (e.g. @react-native-async-storage/async-storage) on every consumer,
25
+ * even ones that don't want it. The mode always starts at 'system' on a
26
+ * fresh app launch. If you want persistence, read your own storage at
27
+ * startup (before your first render) and call setGlobalThemeMode() with
28
+ * the result — same "seed once, no built-in write-back" shape as
29
+ * seedDefaultMode() below, just driven by your own storage instead of
30
+ * one this package picks for you.
31
+ * 2. No DOM to "apply" the resolved mode to. On web, applyToDom() flips a
32
+ * class/attribute the generated CSS's `dark:` selectors match against.
33
+ * On native there's no CSS at all — nativeBridge.ts's resolveStyle()
34
+ * reads getGlobalDarkMode() directly on every call instead of querying
35
+ * Appearance.getColorScheme() itself, which is what actually makes an
36
+ * explicit override affect rendering (previously that function always
37
+ * read the raw OS setting, so it had no way to reflect a user choice —
38
+ * see nativeBridge.ts's own comment on this for why that's the one
39
+ * genuinely fixed bug this change introduces, not just a new feature).
40
+ */
41
+ type ThemeMode = 'light' | 'dark' | 'system';
42
+ declare function getGlobalDarkMode(): boolean;
43
+ declare function getGlobalThemeMode(): ThemeMode;
44
+ /** Explicit user choice — takes priority over any later `<ThemeProvider defaultMode>` seed (see `seedDefaultMode` below). Not persisted; see this module's own doc comment. */
45
+ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
46
+ declare function toggleGlobalDarkMode(): void;
47
+ declare function subscribeGlobalDarkMode(listener: () => void): () => void;
48
+
49
+ /**
50
+ * Reactive read of the global dark-mode store — works with or without a
51
+ * `<ThemeProvider>` mounted anywhere in the tree, since it subscribes to
52
+ * `darkModeStore.ts` directly. No server-snapshot argument (unlike
53
+ * @kbach/react's web version) — React Native has no SSR concept at all,
54
+ * so there's no third argument for `useSyncExternalStore` to need.
55
+ */
56
+ declare function useGlobalDarkMode(): boolean;
57
+
58
+ interface ThemeProviderProps {
59
+ children: ReactNode;
60
+ /**
61
+ * Applied once, on this provider's first mount, but ONLY if nothing has
62
+ * made an explicit mode choice yet (see
63
+ * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
64
+ * wins over a component's own default. Unlike @kbach/react's web
65
+ * version, there's no persisted value to check here — see
66
+ * darkModeStore.ts's own doc comment on why this package doesn't persist
67
+ * the mode across app launches.
68
+ */
69
+ defaultMode?: ThemeMode;
70
+ }
71
+ /**
72
+ * Native port of @kbach/react's `ThemeProvider` — a thin, OPTIONAL React
73
+ * Context wrapper around `darkModeStore.ts`'s single global store, not the
74
+ * source of truth itself. Because the store is global, MULTIPLE mounted
75
+ * providers share the exact same `mode`/`isDark` state rather than each
76
+ * getting their own scoped value — mounting more than one is very likely a
77
+ * mistake (each fighting over the same state via a different
78
+ * `defaultMode`), so this warns once in dev, matching the web version's
79
+ * identical guard.
80
+ */
81
+ declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
82
+
83
+ interface ThemeContextValue {
84
+ mode: ThemeMode;
85
+ isDark: boolean;
86
+ setMode: (mode: ThemeMode) => void;
87
+ toggle: () => void;
88
+ }
89
+ /**
90
+ * Requires a `<ThemeProvider>` ancestor — throws otherwise, since a
91
+ * component that destructures `{ mode, setMode }` genuinely needs Context
92
+ * (unlike a component that just wants the dark/light boolean, which should
93
+ * reach for `useGlobalDarkMode()` instead and never needs a provider at
94
+ * all).
95
+ */
96
+ declare function useTheme(): ThemeContextValue;
97
+
98
+ export { type ThemeConfig as T, type ThemeContextValue as a, type ThemeMode as b, ThemeProvider as c, type ThemeProviderProps as d, defaultTheme as e, getGlobalThemeMode as f, getGlobalDarkMode as g, getTheme as h, setTheme as i, subscribeGlobalDarkMode as j, useTheme as k, setGlobalThemeMode as s, toggleGlobalDarkMode as t, useGlobalDarkMode as u };
@@ -278,16 +278,49 @@ var PALETTE = {
278
278
  // src/theme.ts
279
279
  var defaultTheme = {
280
280
  colors: PALETTE,
281
+ // Real Tailwind's default spacing scale in full — every step is exactly
282
+ // `n * 4px`, including the fractional `.5` steps (`1.5` -> 6px, `2.5` ->
283
+ // 10px, `3.5` -> 14px) and the "px" literal (a bare 1px). Previously
284
+ // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
285
+ // the real scale — so anything else (`p-3`, `w-5`, `border-2`,
286
+ // `gap-10`, ...) silently failed to resolve. Mirrors @kbach/react's
287
+ // theme.ts exactly.
281
288
  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
289
  "0": 0,
290
+ "px": 1,
291
+ "0.5": 2,
286
292
  "1": 4,
293
+ "1.5": 6,
287
294
  "2": 8,
295
+ "2.5": 10,
296
+ "3": 12,
297
+ "3.5": 14,
288
298
  "4": 16,
299
+ "5": 20,
289
300
  "6": 24,
290
- "8": 32
301
+ "7": 28,
302
+ "8": 32,
303
+ "9": 36,
304
+ "10": 40,
305
+ "11": 44,
306
+ "12": 48,
307
+ "14": 56,
308
+ "16": 64,
309
+ "20": 80,
310
+ "24": 96,
311
+ "28": 112,
312
+ "32": 128,
313
+ "36": 144,
314
+ "40": 160,
315
+ "44": 176,
316
+ "48": 192,
317
+ "52": 208,
318
+ "56": 224,
319
+ "60": 240,
320
+ "64": 256,
321
+ "72": 288,
322
+ "80": 320,
323
+ "96": 384
291
324
  },
292
325
  screens: {
293
326
  sm: 640,
@@ -311,61 +344,9 @@ function getThemeJson() {
311
344
  return activeThemeJson;
312
345
  }
313
346
 
314
- // src/darkModeStore.ts
315
- import { Appearance } from "react-native";
316
- var listeners = /* @__PURE__ */ new Set();
317
- function systemPrefersDark() {
318
- return Appearance.getColorScheme() === "dark";
319
- }
320
- function resolveIsDark(m) {
321
- return m === "system" ? systemPrefersDark() : m === "dark";
322
- }
323
- var mode = "system";
324
- var isDark = resolveIsDark(mode);
325
- var hasExplicitChoice = false;
326
- var subscription = Appearance.addChangeListener(({ colorScheme }) => {
327
- if (mode !== "system") return;
328
- isDark = colorScheme === "dark";
329
- notify();
330
- });
331
- function notify() {
332
- for (const listener of listeners) listener();
333
- }
334
- function getGlobalDarkMode() {
335
- return isDark;
336
- }
337
- function getGlobalThemeMode() {
338
- return mode;
339
- }
340
- function setGlobalThemeMode(nextMode) {
341
- mode = nextMode;
342
- isDark = resolveIsDark(nextMode);
343
- hasExplicitChoice = true;
344
- notify();
345
- }
346
- function toggleGlobalDarkMode() {
347
- setGlobalThemeMode(isDark ? "light" : "dark");
348
- }
349
- function subscribeGlobalDarkMode(listener) {
350
- listeners.add(listener);
351
- return () => listeners.delete(listener);
352
- }
353
- function seedDefaultMode(fallback) {
354
- if (hasExplicitChoice || mode === fallback) return;
355
- mode = fallback;
356
- isDark = resolveIsDark(fallback);
357
- notify();
358
- }
359
-
360
347
  export {
361
348
  defaultTheme,
362
349
  setTheme,
363
350
  getTheme,
364
- getThemeJson,
365
- getGlobalDarkMode,
366
- getGlobalThemeMode,
367
- setGlobalThemeMode,
368
- toggleGlobalDarkMode,
369
- subscribeGlobalDarkMode,
370
- seedDefaultMode
351
+ getThemeJson
371
352
  };
@@ -5,7 +5,7 @@ import {
5
5
  setGlobalThemeMode,
6
6
  subscribeGlobalDarkMode,
7
7
  toggleGlobalDarkMode
8
- } from "./chunk-AEVOM2NB.mjs";
8
+ } from "./chunk-Z664AKW7.mjs";
9
9
 
10
10
  // src/useGlobalDarkMode.ts
11
11
  import { useSyncExternalStore } from "react";