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

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
@@ -2,32 +2,29 @@
2
2
 
3
3
  Kbach's utility-class styling engine for React Native — the same class
4
4
  names as [`@kbach/react`](https://www.npmjs.com/package/@kbach/react),
5
- resolved to a real `StyleSheet`-ready style object instead of injected CSS.
5
+ resolved to a `StyleSheet`-ready style object instead of injected CSS.
6
6
 
7
- > **Beta, Android-only.** Resolution runs through a Kotlin/JNI TurboModule
8
- > calling the same Rust core `@kbach/react` uses, so class semantics stay
9
- > identical across web and native. The native module ships prebuilt (see
10
- > Setup below) — no Rust toolchain or manual native wiring required — but
11
- > there's no iOS bridge yet.
7
+ > Beta. Native is Android-only (no iOS bridge yet). Ships a prebuilt native
8
+ > module — no Rust toolchain or manual native wiring required.
12
9
 
13
- ## Setup
10
+ ## Install
14
11
 
15
12
  ```sh
16
13
  npm install @kbach/react-native
17
14
  ```
18
15
 
19
- One package, nothing else to install — the Kotlin TurboModule and its
20
- prebuilt native `.so` ship inside this same package's own `android/`
21
- folder. Android's autolinking discovers it automatically the moment it's
22
- in `node_modules`; no manual `MainApplication.kt` edit needed. Works the
23
- same way whether your app was created with the React Native CLI or with
24
- Expo — **except Expo Go**, which can never load this (or any) custom
25
- native module; see below.
16
+ Then, pick the one section below that matches how you're actually running
17
+ the app — each is a genuinely different setup, not layered options:
26
18
 
27
- ### React Native CLI
19
+ - **React Native CLI** (`npx react-native ...`) → [React Native CLI](#react-native-cli)
20
+ - **Expo, using Expo Go** (`npx expo start`, scanning the QR code in the Expo
21
+ Go app) → [Expo Go](#expo-go) — nothing to configure at all
22
+ - **Expo, using a dev client or a bare/prebuilt project** (`npx expo run:android`,
23
+ or EAS Build) → [Expo (dev client / prebuild)](#expo-dev-client--prebuild)
24
+ - **Expo Web** (`npx expo start --web`, or any `react-native-web` target) →
25
+ [Expo Web](#expo-web) — also nothing to configure
28
26
 
29
- Add the Babel plugin so your app's JSX routes through Kbach's JSX runtime
30
- instead of `react/jsx-runtime`:
27
+ ### React Native CLI
31
28
 
32
29
  ```js
33
30
  // babel.config.js
@@ -37,112 +34,323 @@ module.exports = {
37
34
  };
38
35
  ```
39
36
 
40
- Then rebuild the Android app (`npm run android`, or a clean Gradle build if
41
- you're re-running one that's already built) so Gradle picks up the new
42
- native module. See `apps/native-sandbox` in the
43
- [repo](https://github.com/semsakadanupol/kbach) for a full working
44
- reference project, including the Metro config a monorepo setup like this
45
- one needs (a plain single-app install doesn't need any of that — it's
46
- only relevant if you're linking packages locally instead of installing
47
- from npm).
37
+ Rebuild the Android app afterward so Gradle picks up the native module.
38
+
39
+ ### Expo Go
48
40
 
49
- ### Expo
41
+ Works out of the box — no prebuild, no dev client, nothing to configure.
42
+ Expo Go can't load third-party native code, so this falls back to a pure-JS
43
+ engine automatically. Covers layout, spacing, border/radius, plain colors,
44
+ font-weight, text-transform/decoration, line-height, letter-spacing,
45
+ shadows, and transforms (`translate-x`/`translate-y`, `scale`/`scale-x`/
46
+ `scale-y`, `rotate`/`rotate-x`/`rotate-y`/`rotate-z`, `skew-x`/`skew-y`,
47
+ `transform-none`, `backface-visible`/`backface-hidden`). Doesn't cover grid,
48
+ filters, gradients, DOM-only typography, or the rest of the transform family
49
+ (`translate-z`/`scale-z`, `perspective`/`perspective-origin`/`origin`,
50
+ `transform-gpu`/`transform-cpu` — no RN equivalent for any of these) — same
51
+ scope native itself excludes.
50
52
 
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:
53
+ ### Expo (dev client / prebuild)
54
+
55
+ Gets you the full native engine instead of the JS fallback:
56
56
 
57
57
  ```js
58
58
  // babel.config.js
59
59
  module.exports = {
60
- presets: ['babel-preset-expo'], // not '@react-native/babel-preset'
60
+ presets: ['babel-preset-expo'],
61
61
  plugins: ['@kbach/react-native/babel-plugin'],
62
62
  };
63
63
  ```
64
64
 
65
65
  ```sh
66
- npx expo prebuild # generates android/ (and ios/, which this package doesn't support yet)
67
- npx expo run:android # builds and installs a dev client with the native module included
68
- ```
69
-
70
- Also requires the New Architecture enabled (Expo's default in current SDK
71
- versions) — the native module is a TurboModule, which doesn't exist under
72
- the legacy bridge at all. If you're on an older SDK/config with the New
73
- Architecture off, turn it on in `app.json`
74
- (`"newArchEnabled": true` under `expo`) before prebuilding.
75
-
76
- EAS Build works the same way — it runs the same `prebuild` step, so no
77
- extra configuration beyond the two steps above.
78
-
79
- ## What this is
80
-
81
- - A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
82
- `className`/`kb` props on every element and calls into the native bridge
83
- to resolve them to a style object, synchronously, on every render.
84
- - A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
85
- app's own files (never `node_modules`) through that JSX runtime via a
86
- per-file `@jsxImportSource` pragma.
87
- - `setTheme()`/`getTheme()` for customizing the color palette and spacing
88
- scale — the same `ThemeConfig` shape `@kbach/react` uses, kept as an
89
- independent type here so a native-only project isn't forced to depend on
90
- a web-oriented package.
91
- - `shadow-*` resolves to real native shadows — RN's discrete
92
- `shadowColor`/`shadowOffset`/`shadowOpacity`/`shadowRadius` props plus
93
- Android's `elevation` (both emitted together; `elevation` is what
94
- actually renders a shadow on Android — the `shadow*` props are iOS-only
95
- in RN itself, ready for whenever this package gets an iOS bridge),
96
- derived from the same named tiers (`sm`/`DEFAULT`/`md`/`lg`/`xl`/`2xl`)
97
- `@kbach/react`'s CSS `box-shadow` scale uses. `shadow-inner` and
98
- arbitrary `shadow-[...]` values aren't supported — RN's native shadow
99
- props can't express an inset shadow, and an arbitrary value is a raw CSS
100
- string with no mechanical translation into discrete native props.
101
- - Responsive breakpoints (`sm:`/`md:`/`lg:`/`xl:`/`2xl:`) — gated live
102
- against the current window width (`Dimensions.get('window').width`),
103
- same mobile-first "min-width and up" semantics as web, and freely
104
- combinable with `dark:`/`active:` (e.g. `dark:md:bg-blue-8`). Call RN's
105
- own `useWindowDimensions()` somewhere in your tree to re-render on
106
- rotation/resize — same role `useColorScheme()` plays for `dark:` below.
66
+ npx expo prebuild
67
+ npx expo run:android
68
+ ```
107
69
 
108
- ## Dark mode
70
+ Requires the New Architecture enabled (`"newArchEnabled": true` in
71
+ `app.json`, on by default in current Expo SDKs). EAS Build works the same
72
+ way, no extra config.
73
+
74
+ ### Expo Web
75
+
76
+ Works out of the box — resolves through the same real-CSS engine
77
+ `@kbach/react` uses on the DOM, with the FULL modifier set (`group-hover:`,
78
+ `peer-hover:`, container queries, `has-[...]:`, etc.) — everything real
79
+ Tailwind supports. Native supports a smaller, explicitly-scoped subset; see
80
+ "Modifiers on native" below for exactly what that covers.
109
81
 
110
- Works standalone, with zero provider — same architecture as `@kbach/react`:
82
+ **Static rendering (SSR) caveat:** `dark:`/responsive classes on content
83
+ that's part of the initial pre-rendered HTML resolve against the *server*,
84
+ not the visiting browser — so a user in dark mode can briefly see the light
85
+ version on first paint. Defer preference-dependent classes until mounted:
111
86
 
112
87
  ```tsx
113
- import { useGlobalDarkMode, toggleGlobalDarkMode } from '@kbach/react-native';
88
+ const [mounted, setMounted] = useState(false);
89
+ useEffect(() => setMounted(true), []);
114
90
 
115
- function ThemeToggle() {
116
- const isDark = useGlobalDarkMode();
117
- return <Pressable onPress={toggleGlobalDarkMode}><Text>{isDark ? 'Dark' : 'Light'}</Text></Pressable>;
118
- }
91
+ <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
92
+ ```
93
+
94
+ ### Monorepo installs (npm/yarn/pnpm workspaces)
95
+
96
+ If your app lives in a monorepo where `@kbach/react-native` is a workspace
97
+ package (a `file:`/`link:` dependency, or hoisted rather than nested), watch
98
+ for **two physical copies of `react-native`/`react`** ending up in your
99
+ dependency tree — one this package would otherwise resolve up to from its
100
+ own directory, and a different one your app's own files resolve to. Metro
101
+ walks `node_modules` hierarchically per-file, so it can silently hand two
102
+ different files two different copies of the *same* module, each with its
103
+ own separate module state.
104
+
105
+ This isn't hypothetical: it's the root cause of a real dark-mode bug this
106
+ package shipped and fixed — `useColorScheme()`/`Appearance` in one copy
107
+ never saw changes made through the other, so an explicit theme toggle
108
+ appeared to do nothing. The fix is a Metro config override that forces a
109
+ single instance, **required in your app**, not something this package can
110
+ do on your behalf from inside `node_modules`:
111
+
112
+ ```js
113
+ // metro.config.js
114
+ const path = require('path');
115
+ // React Native CLI: const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
116
+ // Expo: const { getDefaultConfig } = require('expo/metro-config');
117
+
118
+ const projectRoot = __dirname;
119
+ const workspaceRoot = path.resolve(projectRoot, '../..'); // -> your monorepo root
120
+
121
+ const config = getDefaultConfig(projectRoot);
122
+ config.watchFolders = [workspaceRoot];
123
+
124
+ const forcedSingleInstance = {
125
+ 'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
126
+ react: path.resolve(projectRoot, 'node_modules/react'),
127
+ };
128
+ const defaultResolveRequest = config.resolver.resolveRequest;
129
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
130
+ if (Object.prototype.hasOwnProperty.call(forcedSingleInstance, moduleName)) {
131
+ return { type: 'sourceFile', filePath: require.resolve(forcedSingleInstance[moduleName]) };
132
+ }
133
+ return defaultResolveRequest
134
+ ? defaultResolveRequest(context, moduleName, platform)
135
+ : context.resolveRequest(context, moduleName, platform);
136
+ };
137
+
138
+ module.exports = config; // (React Native CLI: mergeConfig(getDefaultConfig(projectRoot), config))
119
139
  ```
120
140
 
121
- Or the optional `<ThemeProvider>` / `useTheme()` pair, reading and writing
122
- the exact same global store:
141
+ A single, non-monorepo `npm install` never hits this — only relevant if your
142
+ app and `@kbach/react-native` share a workspace root.
143
+
144
+ ## Modifiers on native
145
+
146
+ Native has no CSS selector/cascade engine, so only a fixed, explicitly
147
+ supported set of modifiers actually applies — everything else (`group-*:`,
148
+ `peer-*:`, `has-[...]:`, `aria-*:`, `data-*:`, container queries, ...)
149
+ parses without error but never takes effect. Supported today:
150
+
151
+ - **`dark:`** — reacts to the OS/app color scheme, see "Dark mode" below.
152
+ - **`sm:`/`md:`/`lg:`/`xl:`/`2xl:`** — reacts to `useWindowDimensions()`
153
+ crossing the matching `theme.screens` breakpoint (rotation/resize/fold).
154
+ - **`active:`** — `Pressable`'s own press state, via its style-callback API.
155
+ - **`hover:`** — tracked via `onHoverIn`/`onHoverOut`, which RN only ever
156
+ fires on a platform with real pointer input (iOS 13+/Android/macOS/
157
+ Windows via a mouse/trackpad/Apple Pencil hover) — inert (never fires) on
158
+ a touch-only device, the same way real Tailwind's `hover:` already
159
+ degrades on a touch-only *browser*. Composing it onto any host type is
160
+ harmless even where it never fires.
161
+ - **`focus:`** — tracked via `onFocus`/`onBlur` (keyboard/external-keyboard/
162
+ TV-remote/tab navigation, and `TextInput` focus).
163
+ - **`disabled:`** — reads the element's own `disabled` prop directly; no
164
+ event tracking needed, since a prop change already re-renders normally.
165
+ - Any combination of the above chained on one class (`sm:dark:bg-blue-8`,
166
+ `hover:focus:opacity-100`) requires every named condition to hold at once
167
+ — chain order never matters (`dark:sm:` and `sm:dark:` are equivalent).
168
+
169
+ `group-hover:`/`peer-hover:` and friends are NOT implemented on native —
170
+ doing so needs a React Context bridge (propagating an ancestor's hover/
171
+ press/focus state down to descendants, since there's no `.group`/`.peer`
172
+ selector relationship to lean on) that doesn't exist today. If you need
173
+ group/peer-style coordination on native right now, lift the shared state
174
+ into a parent component yourself and pass it down as an explicit prop.
175
+
176
+ ## Dark mode
177
+
178
+ **Zero setup, on every platform this package supports** — real native
179
+ *and* Expo Web. Unlike [`@kbach/react`](https://www.npmjs.com/package/@kbach/react),
180
+ there's no build-time CSS step here to keep in sync with the runtime:
181
+ `dark:` is resolved fresh from a live parameter on native (no selectors at
182
+ all to generate ahead of time), and Expo Web's default strategy is already
183
+ `'attribute'` — a manual toggle just works, no config file, no
184
+ `applyKbachConfig()` call:
123
185
 
124
186
  ```tsx
125
- import { ThemeProvider, useTheme } from '@kbach/react-native';
187
+ import { useTheme } from '@kbach/react-native';
126
188
 
127
- function Toggle() {
189
+ function ThemeToggle() {
128
190
  const { mode, isDark, setMode, toggle } = useTheme();
129
191
  return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
130
192
  }
193
+ ```
194
+
195
+ Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
196
+ optional, for seeding a startup default. Outside React, use
197
+ `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
131
198
 
132
- <ThemeProvider defaultMode="system">
133
- <Toggle />
199
+ Follows the OS setting live until overridden, and isn't persisted across
200
+ app launches by default (no forced storage dependency). To persist, pass
201
+ any async key-value store exposing `getItem`/`setItem` as `<ThemeProvider
202
+ persist>` — `@react-native-async-storage/async-storage`'s default export
203
+ already matches this shape exactly:
204
+
205
+ ```tsx
206
+ import AsyncStorage from '@react-native-async-storage/async-storage';
207
+ import { ThemeProvider } from '@kbach/react-native';
208
+
209
+ <ThemeProvider persist={AsyncStorage}>
210
+ <App />
134
211
  </ThemeProvider>;
135
212
  ```
136
213
 
137
- By default the mode follows the OS `Appearance` setting live until you
138
- explicitly override it — and **isn't persisted across app launches**.
139
- There's no React-Native-builtin equivalent of `localStorage`, and adding
140
- one would mean forcing a real native dependency (e.g.
141
- `@react-native-async-storage/async-storage`) on every consumer, even ones
142
- that don't want it. If you want persistence, read your own storage at
143
- startup and call `setGlobalThemeMode()` with the result before your first
144
- render — the same "seed once" shape `<ThemeProvider defaultMode>` itself
145
- uses internally.
214
+ The persisted mode (if any) is read once on mount and wins over
215
+ `defaultMode`; every later explicit choice is written back automatically.
216
+ `false` by default — this package still never imports a storage library
217
+ itself, so nothing changes for apps that don't opt in. Prefer wiring your
218
+ own storage manually instead? `setGlobalThemeMode()` before your first
219
+ render still works exactly as before.
220
+
221
+ ## Reading colors as values
222
+
223
+ For anywhere you need a real color, not a `className` — a chart prop, a
224
+ native shadow color:
225
+
226
+ ```tsx
227
+ import { useColors } from '@kbach/react-native';
228
+
229
+ const colors = useColors();
230
+ colors.blue[6]; // '#2563eb' — static, same value in both modes
231
+ colors.blue['6/50']; // same, at 50% opacity
232
+ ```
233
+
234
+ Any color can be defined in the theme as a plain string (static) or a
235
+ `{ light, dark }` pair — fully manual, resolved per the current dark-mode
236
+ state:
237
+
238
+ ```ts
239
+ setTheme({
240
+ ...defaultTheme,
241
+ colors: { ...defaultTheme.colors, brand: { light: '#2563eb', dark: '#93c5fd' } },
242
+ });
243
+ ```
244
+
245
+ A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
246
+ dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
247
+ that's the same visual weight relative to its own background, no manual
248
+ `colors.blue[isDark ? 7 : 6]` needed.
249
+
250
+ ## Arbitrary math: `calc()`, `clamp()`, `min()`, `max()`
251
+
252
+ ```tsx
253
+ <View className="p-[calc(16px+8px)]" /> // -> padding: 24
254
+ <View className="w-[clamp(1rem,2rem,3rem)]" /> // -> width: 32
255
+ ```
256
+
257
+ Resolves at **build/resolve time**, not runtime — every operand must be a
258
+ constant `px`/`rem` length (or a bare number). This works because native has
259
+ no CSS engine to evaluate a real `calc()` string at paint time, so the only
260
+ way to support it at all is to compute the answer ourselves.
261
+
262
+ A **viewport-relative** expression (`calc(10vw+8px)`) can't be reduced this
263
+ way — no such unit exists on native at all. It already works fine on web
264
+ (real CSS); on native the declaration is dropped and a `console.warn`
265
+ explains why (dev builds only).
266
+
267
+ **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`,
268
+ `min(50%,20rem)`, `max(1rem,5%)`, `clamp(16rem,50%,32rem)`) IS supported,
269
+ differently: since native has no way to know "100% of what" ahead of time,
270
+ this measures the element's own real layout instead —
271
+
272
+ ```tsx
273
+ <View className="w-[calc(100%_-_3rem)]" />
274
+ <View className="w-[min(50%,20rem)]" /> // half the parent, capped at a fixed size
275
+ <View className="w-[clamp(16rem,50%,32rem)]" />
276
+ ```
277
+
278
+ Renders once at a plain `100%` (a real RN percentage, resolved correctly by
279
+ RN's own layout engine against the parent) purely to trigger a layout
280
+ event, then immediately corrects to the actual computed number once
281
+ measured — a genuine one-frame measure-then-snap, not instant math like the
282
+ constant-only case above, and it keeps re-measuring on every layout (screen
283
+ rotation, parent resize, ...) rather than freezing after the first value.
284
+ Scoped narrowly, same "reduce fully or bail" philosophy as the constant-only
285
+ case: `calc()` takes exactly one percentage term plus one constant term;
286
+ `min()`/`max()` take two or more comma-separated arguments, each EITHER a
287
+ plain percentage or a constant px/rem expression (not both mixed in one
288
+ argument — `min(50%-1rem,20rem)` is out of scope); `clamp()` takes exactly
289
+ three. `width`/`height` only in every case — other properties, or anything
290
+ that doesn't reduce fully, falls through to the same drop-and-warn behavior
291
+ as before.
292
+
293
+ **Arbitrary values can't contain literal spaces** — this is a pre-existing
294
+ rule (real Tailwind has the same one), not specific to `calc()`: a space
295
+ inside `[...]` is indistinguishable from the whitespace that separates
296
+ class tokens in the first place, so `w-[calc(100% - 3rem)]` silently splits
297
+ into three broken tokens before `calc()` is ever even recognized. Write
298
+ `w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
299
+ unknown-class warning below is for: it'll tell you when this happens.
300
+
301
+ ## Unknown-class warnings (typo detection)
302
+
303
+ ```tsx
304
+ <View className="flexx-center" /> // real Kbach classes never have to guess — a typo warns
305
+ ```
306
+
307
+ ```
308
+ Kbach: "flexx-center" doesn't match any known Kbach utility — typo? (skipped) (from className "flexx-center")
309
+ ```
310
+
311
+ On native/dev-client builds, a class that doesn't match *any* Kbach
312
+ utility on *any* platform gets a `console.warn` (dev builds only, printed
313
+ once per unique message) instead of silently doing nothing — the same
314
+ signal `@kbach/react`'s Vite plugin already gives you at build time on
315
+ web, just at resolve time here instead. A real utility this engine simply
316
+ doesn't support on native yet (`grid-cols-3`, `scale-150`, `blur`, ...)
317
+ never warns — that's an intentional, documented platform gap (see
318
+ `resolve_style.rs`'s own doc comment), not a typo, so it stays silent the
319
+ same way it always has.
320
+
321
+ Not available in Expo Go's pure-JS fallback engine today — that engine has
322
+ no web-shaped resolver to check "is this a real Kbach utility at all"
323
+ against, only the (smaller) native-supported subset, so it can't
324
+ distinguish a genuine typo from a real-but-native-unsupported class
325
+ without a much larger port. A real dev-client/CLI build always gets this
326
+ check.
327
+
328
+ ## Dynamic tokens
329
+
330
+ The native counterpart to a real CSS custom property — `var(--x)` already
331
+ works in a `className` on Expo Web with zero help from this package (real
332
+ CSS, resolved by the browser). This gives native the same className syntax:
333
+
334
+ ```tsx
335
+ import { setDynamicToken, useDynamicToken } from '@kbach/react-native';
336
+
337
+ setDynamicToken('sidebar-width', '240px'); // updates every subscribed element, on both platforms
338
+ <View className="w-[var(--sidebar-width)]" />
339
+
340
+ // Reading the raw value in JS, reactively:
341
+ function Sidebar() {
342
+ const width = useDynamicToken('sidebar-width'); // '240px' | undefined
343
+ }
344
+ ```
345
+
346
+ On web, `setDynamicToken` writes the real CSS custom property to the DOM —
347
+ every element using it repaints instantly via the browser's own CSS engine,
348
+ no React re-render involved. On native, the className is substituted with
349
+ the token's current value before every resolve, and an element referencing
350
+ a token re-renders/repaints on its own when that token changes (same
351
+ subscription mechanism `dark:` and `sm:`/`md:`/... already use — see
352
+ `jsxRuntimeCore.ts`'s own doc comments). An unregistered token name falls
353
+ through to the same native invalid-value warning above.
146
354
 
147
355
  ## License
148
356
 
@@ -0,0 +1,40 @@
1
+ import {
2
+ resolveClassName
3
+ } from "./chunk-ZYE4VIZE.mjs";
4
+
5
+ // src/jsxRuntimeCoreWeb.ts
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ import { Fragment } from "react";
8
+ function makeElement(isStaticChildren, type, props, key) {
9
+ return (isStaticChildren ? _jsxs : _jsx)(type, props, key);
10
+ }
11
+ function processElement(resolveClassName2, type, rawProps, key, isStaticChildren) {
12
+ if (type === null || typeof type === "symbol" || !rawProps) {
13
+ return makeElement(isStaticChildren, type, rawProps ?? {}, key);
14
+ }
15
+ const { className: classStrRaw, dataSet: userDataSet, ...rest } = rawProps;
16
+ if (typeof classStrRaw !== "string") {
17
+ return makeElement(isStaticChildren, type, rawProps, key);
18
+ }
19
+ const resolved = resolveClassName2(classStrRaw);
20
+ const dataSet = { ...userDataSet, kb: resolved };
21
+ return makeElement(isStaticChildren, type, { ...rest, className: classStrRaw, dataSet }, key);
22
+ }
23
+ function createJsxFunctionsWeb(resolveClassName2) {
24
+ function jsx2(type, props, key) {
25
+ return processElement(resolveClassName2, type, props, key, false);
26
+ }
27
+ function jsxs2(type, props, key) {
28
+ return processElement(resolveClassName2, type, props, key, true);
29
+ }
30
+ return { jsx: jsx2, jsxs: jsxs2 };
31
+ }
32
+
33
+ // src/jsx-runtime.web.tsx
34
+ var { jsx, jsxs } = createJsxFunctionsWeb(resolveClassName);
35
+
36
+ export {
37
+ Fragment,
38
+ jsx,
39
+ jsxs
40
+ };
@@ -0,0 +1,112 @@
1
+ import {
2
+ getTheme
3
+ } from "./chunk-ZYE4VIZE.mjs";
4
+
5
+ // src/darkModeStore.ts
6
+ import { Appearance } from "react-native";
7
+ var listeners = /* @__PURE__ */ new Set();
8
+ function systemPrefersDark() {
9
+ return Appearance.getColorScheme() === "dark";
10
+ }
11
+ function resolveIsDark(m) {
12
+ return m === "system" ? systemPrefersDark() : m === "dark";
13
+ }
14
+ function applyToDom(dark) {
15
+ if (typeof document === "undefined") return;
16
+ const root = document.documentElement;
17
+ const strategy = getTheme().darkMode;
18
+ root.classList.toggle("dark", strategy === "class" && dark);
19
+ root.classList.toggle("light", strategy === "class" && !dark);
20
+ if (strategy === "attribute") {
21
+ root.setAttribute("data-theme", dark ? "dark" : "light");
22
+ } else {
23
+ root.removeAttribute("data-theme");
24
+ }
25
+ }
26
+ var mode = "system";
27
+ var isDark = resolveIsDark(mode);
28
+ applyToDom(isDark);
29
+ var hasExplicitChoice = false;
30
+ var subscription = Appearance.addChangeListener(({ colorScheme }) => {
31
+ if (mode !== "system") return;
32
+ isDark = colorScheme === "dark";
33
+ applyToDom(isDark);
34
+ notify();
35
+ });
36
+ function notify() {
37
+ for (const listener of listeners) listener();
38
+ }
39
+ function resyncDomWithActiveTheme() {
40
+ applyToDom(isDark);
41
+ }
42
+ function getGlobalDarkMode() {
43
+ return isDark;
44
+ }
45
+ function getGlobalThemeMode() {
46
+ return mode;
47
+ }
48
+ function setGlobalThemeMode(nextMode) {
49
+ mode = nextMode;
50
+ isDark = resolveIsDark(nextMode);
51
+ hasExplicitChoice = true;
52
+ applyToDom(isDark);
53
+ notify();
54
+ }
55
+ function toggleGlobalDarkMode() {
56
+ setGlobalThemeMode(isDark ? "light" : "dark");
57
+ }
58
+ function subscribeGlobalDarkMode(listener) {
59
+ listeners.add(listener);
60
+ return () => listeners.delete(listener);
61
+ }
62
+ function seedDefaultMode(fallback) {
63
+ if (hasExplicitChoice || mode === fallback) return;
64
+ mode = fallback;
65
+ isDark = resolveIsDark(fallback);
66
+ applyToDom(isDark);
67
+ notify();
68
+ }
69
+
70
+ // src/dynamicTokens.ts
71
+ var tokens = /* @__PURE__ */ new Map();
72
+ var listeners2 = /* @__PURE__ */ new Set();
73
+ var version = 0;
74
+ function applyToDom2(name, value) {
75
+ if (typeof document === "undefined") return;
76
+ document.documentElement.style.setProperty(`--${name}`, value);
77
+ }
78
+ function notify2() {
79
+ version++;
80
+ for (const listener of listeners2) listener();
81
+ }
82
+ function setDynamicToken(name, value) {
83
+ tokens.set(name, value);
84
+ applyToDom2(name, value);
85
+ notify2();
86
+ }
87
+ function getDynamicToken(name) {
88
+ return tokens.get(name);
89
+ }
90
+ function deleteDynamicToken(name) {
91
+ if (!tokens.delete(name)) return;
92
+ if (typeof document !== "undefined") document.documentElement.style.removeProperty(`--${name}`);
93
+ notify2();
94
+ }
95
+ function subscribeDynamicTokens(listener) {
96
+ listeners2.add(listener);
97
+ return () => listeners2.delete(listener);
98
+ }
99
+
100
+ export {
101
+ resyncDomWithActiveTheme,
102
+ getGlobalDarkMode,
103
+ getGlobalThemeMode,
104
+ setGlobalThemeMode,
105
+ toggleGlobalDarkMode,
106
+ subscribeGlobalDarkMode,
107
+ seedDefaultMode,
108
+ setDynamicToken,
109
+ getDynamicToken,
110
+ deleteDynamicToken,
111
+ subscribeDynamicTokens
112
+ };