@kbach/react-native 1.0.0-beta.5 → 1.0.0-beta.51

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.
Files changed (42) hide show
  1. package/AGENTS.md +361 -0
  2. package/README.md +150 -137
  3. package/android/src/main/jniLibs/arm64-v8a/libkbach_core_engine.so +0 -0
  4. package/android/src/main/jniLibs/x86_64/libkbach_core_engine.so +0 -0
  5. package/dist/chunk-CD5WFFIQ.mjs +40 -0
  6. package/dist/chunk-IJAXJVB4.mjs +566 -0
  7. package/dist/chunk-KHQ2BWYH.mjs +112 -0
  8. package/dist/clsx-BLLSnqAa.d.ts +277 -0
  9. package/dist/darkModeStore.d.ts +73 -0
  10. package/dist/darkModeStore.js +118 -0
  11. package/dist/dynamicTokens.d.ts +53 -0
  12. package/dist/dynamicTokens.js +75 -0
  13. package/dist/index.d.ts +92 -151
  14. package/dist/index.js +2119 -393
  15. package/dist/index.web.d.ts +101 -4
  16. package/dist/index.web.js +438 -401
  17. package/dist/index.web.mjs +268 -14
  18. package/dist/jsx-dev-runtime.js +2238 -332
  19. package/dist/jsx-dev-runtime.web.js +207 -361
  20. package/dist/jsx-dev-runtime.web.mjs +19 -7
  21. package/dist/jsx-runtime.js +2238 -332
  22. package/dist/jsx-runtime.web.js +191 -353
  23. package/dist/jsx-runtime.web.mjs +3 -6
  24. package/dist/theme.d.ts +79 -0
  25. package/dist/{chunk-AEVOM2NB.mjs → theme.js} +89 -62
  26. package/package.json +44 -14
  27. package/src/babel-plugin.js +144 -2
  28. package/dist/chunk-4KCCEMEN.mjs +0 -66
  29. package/dist/chunk-DLIXI5OW.mjs +0 -53
  30. package/dist/chunk-FG5E4NLJ.mjs +0 -18
  31. package/dist/chunk-KSDFO34H.mjs +0 -32
  32. package/dist/chunk-OJQAJLYT.mjs +0 -14
  33. package/dist/chunk-ZUE6SCTE.mjs +0 -14
  34. package/dist/index.d.mts +0 -161
  35. package/dist/index.mjs +0 -32
  36. package/dist/index.web.d.mts +0 -7
  37. package/dist/jsx-dev-runtime.d.mts +0 -10
  38. package/dist/jsx-dev-runtime.mjs +0 -17
  39. package/dist/jsx-dev-runtime.web.d.mts +0 -10
  40. package/dist/jsx-runtime.d.mts +0 -7
  41. package/dist/jsx-runtime.mjs +0 -14
  42. package/dist/jsx-runtime.web.d.mts +0 -7
@@ -0,0 +1,112 @@
1
+ import {
2
+ getTheme
3
+ } from "./chunk-IJAXJVB4.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
+ };
@@ -0,0 +1,277 @@
1
+ import { ThemeConfig, ContainerConfig } from './theme';
2
+ import { ThemeMode } from './darkModeStore';
3
+ import * as react from 'react';
4
+ import { ReactNode } from 'react';
5
+
6
+ /**
7
+ * A `kbach.config.js`-style theme customization — same shape as
8
+ * @kbach/react's `KbachConfig`, minus @kbach/react's own PER-COLOR
9
+ * `{ light, dark }` object form in `extend.colors` specifically (real
10
+ * mode-aware colors here are written via the grouped `dark` key instead —
11
+ * see `KbachConfig.extend.colors`'s own doc comment below): aliasing/
12
+ * opacity math on an individual color value here only ever operates on
13
+ * plain hex (`brand: 'blue-6'`, `brandSoft: 'brand/30'`), matching
14
+ * `jsEngine/resolvers/color.ts`'s `lookupHex`'s own Plain-only convention
15
+ * — a reference to an EXISTING color that happens to be mode-aware
16
+ * (`theme.colors` can hold those, for `useColors()`; see `theme.ts`'s own
17
+ * `ColorEntry` doc comment) just doesn't resolve as an alias, falling
18
+ * through to "literal CSS value" the same way an unknown name would. A
19
+ * config object goes in, a resolved `ThemeConfig` comes out, via one PURE
20
+ * function (`resolveKbachConfig`) — no plugins, no live-update store,
21
+ * nothing to keep "in sync."
22
+ */
23
+ interface KbachConfig {
24
+ darkMode?: ThemeConfig['darkMode'];
25
+ /**
26
+ * REPLACES the named section of `defaultTheme` entirely — use `extend` to
27
+ * add instead of replace. `container` isn't here (only under `extend`
28
+ * below) — see @kbach/react's identical `KbachConfig.theme` doc comment
29
+ * for why.
30
+ */
31
+ theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
32
+ /** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
33
+ extend?: {
34
+ /**
35
+ * A value is either a literal hex color, or a REFERENCE to another
36
+ * color name, optionally with an opacity suffix. Two things can be
37
+ * referenced: a BUILT-IN palette color, always available regardless of
38
+ * ordering (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`), or a
39
+ * custom color defined EARLIER in this same object (`{ brand:
40
+ * '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft` resolves to
41
+ * `brand`'s own value at 30% opacity). A string that doesn't match any
42
+ * known color name is treated as a literal CSS value instead.
43
+ *
44
+ * `dark` is a reserved key, not a color name: a parallel map of
45
+ * dark-mode overrides for any of the OTHER names in this same object —
46
+ * `{ surface: 'gray-2', card: 'white', dark: { surface: 'gray-11',
47
+ * card: 'gray-9' } }` makes `surface`/`card` mode-aware
48
+ * (`{ light, dark }` entries in the resolved theme), while `brand`
49
+ * (never mentioned under `dark`) stays a plain, mode-independent
50
+ * color. A name under `dark` with no matching top-level (light) entry
51
+ * is skipped — mode-aware colors need both sides written out, and
52
+ * inventing a "light" value out of nowhere would be worse than just
53
+ * not creating the entry at all. References inside `dark` resolve
54
+ * against the built-in palette and OTHER entries within `dark` itself
55
+ * (not against the light-side custom colors) — the same
56
+ * "self-contained, ordering-independent-of-the-other-side" resolution
57
+ * the light side already gets, just mirrored.
58
+ */
59
+ colors?: {
60
+ [colorName: string]: string | Record<string, string> | undefined;
61
+ dark?: Record<string, string>;
62
+ };
63
+ spacing?: Record<string, number>;
64
+ screens?: Record<string, number>;
65
+ /**
66
+ * Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` —
67
+ * adds to (or overrides one of) `defaultTheme.fontFamily`'s three
68
+ * names. Write it as a real web-shaped fallback stack even though this
69
+ * is the React Native package — native's own resolver strips it down
70
+ * to a single bare name at resolve time (RN's `fontFamily` prop can't
71
+ * take a fallback list), so the SAME config still works unmodified on
72
+ * Expo Web, which needs the full stack.
73
+ */
74
+ fontFamily?: Record<string, string>;
75
+ /** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
76
+ container?: ContainerConfig;
77
+ };
78
+ }
79
+ /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
80
+ declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
81
+ /**
82
+ * `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point
83
+ * from app code. Also re-syncs the DOM (Expo Web only) to the CURRENT
84
+ * dark-mode state right after `setTheme()` — see
85
+ * `resyncDomWithActiveTheme`'s own doc comment for why this is required,
86
+ * not optional.
87
+ */
88
+ declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
89
+
90
+ interface ThemeState {
91
+ mode: ThemeMode;
92
+ isDark: boolean;
93
+ setMode: (mode: ThemeMode) => void;
94
+ toggle: () => void;
95
+ }
96
+ /**
97
+ * The one hook for reading and writing Kbach's dark-mode state. Reads
98
+ * `darkModeStore.ts`'s global store directly via `useSyncExternalStore` —
99
+ * that module-level store, not React Context, is the actual source of
100
+ * truth (every provider used to just mirror it into a Context value), so
101
+ * this works from ANY component, with or without a `<ThemeProvider>`
102
+ * mounted anywhere in the tree. `<ThemeProvider>` still exists, but purely
103
+ * as an optional convenience for seeding an app's initial `defaultMode` —
104
+ * see its own doc comment. No server-snapshot argument (unlike
105
+ * @kbach/react's web version) — React Native has no SSR concept at all,
106
+ * so there's no third argument for `useSyncExternalStore` to need.
107
+ */
108
+ declare function useTheme(): ThemeState;
109
+
110
+ /**
111
+ * The exact shape `@react-native-async-storage/async-storage`'s default
112
+ * export already has — pass it directly as `persist`. Any other async
113
+ * key-value store (MMKV, SecureStore, a custom backend, ...) works too, as
114
+ * long as it exposes these two methods with this signature. Kept minimal
115
+ * on purpose: `ThemeProvider` never imports a storage library itself (see
116
+ * `darkModeStore.ts`'s own doc comment on why this package forces no real
117
+ * native dependency on consumers who don't want persistence) — deliberately
118
+ * NOT a `boolean`-only option that reaches for AsyncStorage internally,
119
+ * since Metro resolves every `require()`/`import` target at BUNDLE time
120
+ * regardless of try/catch, so a library-internal "if installed, use it"
121
+ * check can't actually degrade gracefully when the package is absent — an
122
+ * injected adapter sidesteps that entirely.
123
+ */
124
+ interface ThemeStorageAdapter {
125
+ getItem(key: string): Promise<string | null>;
126
+ setItem(key: string, value: string): Promise<void>;
127
+ }
128
+ interface ThemeProviderProps {
129
+ children: ReactNode;
130
+ /**
131
+ * Applied once, on this provider's first mount, but ONLY if nothing has
132
+ * made an explicit mode choice yet (see
133
+ * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
134
+ * wins over a component's own default. If `persist` is also set and a
135
+ * persisted mode is found, the persisted mode wins over this fallback.
136
+ */
137
+ defaultMode?: ThemeMode;
138
+ /**
139
+ * Opt-in persistence across app launches — `false` by default (this
140
+ * package never persists on its own; see `darkModeStore.ts`'s doc
141
+ * comment). Pass a storage adapter to enable it:
142
+ * ```tsx
143
+ * import AsyncStorage from '@react-native-async-storage/async-storage';
144
+ * <ThemeProvider persist={AsyncStorage}>...
145
+ * ```
146
+ * The persisted value (if any) is read once on mount and applied via the
147
+ * same "don't clobber a real choice already in flight" guard
148
+ * `seedDefaultMode`/`defaultMode` uses; every later explicit mode change
149
+ * is written back automatically for as long as this provider stays
150
+ * mounted.
151
+ */
152
+ persist?: ThemeStorageAdapter | false;
153
+ }
154
+ /**
155
+ * Optional — `useTheme()` reads `darkModeStore.ts`'s global store directly
156
+ * and works anywhere without this mounted at all. The only thing this
157
+ * component does is seed that store's initial `defaultMode` once, for apps
158
+ * that want to declare a startup default in JSX rather than calling
159
+ * `setGlobalThemeMode()` imperatively before their first render. It
160
+ * renders `children` as-is — no Context.Provider, since there's no value
161
+ * left to distribute; `useTheme()` never reads from Context.
162
+ *
163
+ * Because the store is global, MULTIPLE mounted providers would each try
164
+ * to seed the exact same store rather than getting their own scoped
165
+ * state — mounting more than one is very likely a mistake, so this warns
166
+ * once in dev.
167
+ */
168
+ declare function ThemeProvider({ children, defaultMode, persist }: ThemeProviderProps): react.JSX.Element;
169
+
170
+ interface ColorScale {
171
+ /** `colors.blue[6]` -> raw value */
172
+ readonly [shade: number]: string;
173
+ /** `colors.blue['6/50']` -> shade 6 at 50% opacity */
174
+ readonly [key: string]: string;
175
+ }
176
+ type ColorsAPI = {
177
+ /** `colors.white`, `colors.blue` (a shade family), ... */
178
+ readonly [name: string]: string | ColorScale;
179
+ } & {
180
+ /** Applies an opacity (0–100) to any color string this hook can return. */
181
+ readonly alpha: (color: string, opacity?: number) => string;
182
+ /**
183
+ * Same lookup dot/bracket access does (`colors.get('brand')` ===
184
+ * `colors.brand`), typed as plain `string` instead of `string |
185
+ * ColorScale` — use this whenever the result goes somewhere that expects
186
+ * a real `string` (a native style prop, a chart library) and hits a type
187
+ * error otherwise. The wider union on every OTHER property here isn't a
188
+ * bug: a flat custom color (`brand`) and a shade-family prefix (`blue`)
189
+ * are served by the exact same object, and TypeScript has no way to
190
+ * statically know which one any given key names — `kbach.config.js`'s
191
+ * colors are plain runtime JS, not a type declaration this package could
192
+ * inspect ahead of time. A name with no matching theme entry falls back
193
+ * to the name itself, unresolved — the same "not a known color name ->
194
+ * literal value" convention `kbach.config.js`'s own color aliasing
195
+ * already uses (see `config.ts`'s `resolveColorEntry`), never an error.
196
+ */
197
+ readonly get: (name: string) => string;
198
+ };
199
+ /**
200
+ * Resolved theme colors as real values — for anywhere you need an actual
201
+ * color (a chart library prop, a raw SVG `fill`), not a `className`. Ported
202
+ * from old-kbach's `useColors()`, adapted to this engine's flat
203
+ * `"blue-6"`-keyed palette (`colors.blue[6]` looks up `"blue-6"`).
204
+ *
205
+ * Any color — a shade-family entry like `"blue-6"` or a standalone name —
206
+ * can be defined in the theme as either a plain string (static, same in
207
+ * both modes) or a `{ light, dark }` pair — fully manual, no automatic
208
+ * shade-shifting or other assumption about what a "dark variant" should
209
+ * be; see `resolveEntry`'s own doc comment for how a `{ light, dark }`
210
+ * entry resolves on this platform specifically.
211
+ *
212
+ * ```ts
213
+ * const colors = useColors();
214
+ * colors.blue[6]; // "#2563eb" — static, same value in both modes
215
+ * colors.blue['6/50']; // same, at 50% opacity
216
+ * colors.brand; // resolves per isDark if defined as { light, dark }
217
+ * colors.get('brand'); // same value as colors.brand, typed as plain `string`
218
+ * ```
219
+ */
220
+ declare function useColors(): ColorsAPI;
221
+
222
+ /**
223
+ * Reads a dynamic token's CURRENT value reactively — the JS-value
224
+ * counterpart to using `var(--name)` directly in a `className` (see
225
+ * `jsxRuntimeCore.ts`'s own `substituteDynamicTokens`/`ReactiveElement` for
226
+ * how that path stays live). Useful when a component needs the raw value
227
+ * itself (e.g. to drive an Animated value, or a calculation), not just a
228
+ * resolved style. Subscribes to the store's shared version counter (same
229
+ * one every token change bumps — see `dynamicTokens.ts`'s own doc comment
230
+ * on why there's no per-token subscription), so this re-renders on ANY
231
+ * token change, then re-reads just the one name it was asked for.
232
+ */
233
+ declare function useDynamicToken(name: string): string | undefined;
234
+
235
+ /**
236
+ * A tiny class-name composer — combine strings, arrays, and objects (with
237
+ * boolean values) into one space-separated className, skipping anything
238
+ * falsy. Matches the widely-used `clsx` npm package's public API and
239
+ * behavior exactly (a from-scratch reimplementation, not a fork or a thin
240
+ * wrapper around it) — this package ships its own copy so using it adds
241
+ * zero runtime dependencies, and so it works identically whether or not
242
+ * `clsx`/`classnames` happens to already be installed.
243
+ *
244
+ * Kbach class strings never NEED this — a Kbach `className` is just a
245
+ * plain string, composable with plain JS (`"bg-" + color + "-6"`,
246
+ * `[base, active && "opacity-100"].join(" ")`, see this package's own
247
+ * README) with no special API required. This exists purely as an
248
+ * ergonomic convenience for the common "several conditional classes on one
249
+ * element" shape, for anyone who'd rather write it declaratively:
250
+ *
251
+ * ```ts
252
+ * clsx("px-4 py-2", isActive && "bg-blue-6", { "opacity-50": disabled });
253
+ * // "px-4 py-2 bg-blue-6" (when isActive is true and disabled is false)
254
+ * ```
255
+ *
256
+ * Identical to `@kbach/react`'s own `clsx.ts` — deliberately duplicated
257
+ * rather than factored into a shared package, since this is genuinely all
258
+ * there is to it and a whole extra published package for one ~20-line
259
+ * function would be more ceremony than the thing itself.
260
+ */
261
+ /** Anything `clsx` accepts as one argument — recursively, for arrays. */
262
+ type ClassValue = ClassArray | ClassDictionary | string | number | bigint | boolean | null | undefined;
263
+ /** A key is kept (as a literal class name) only when its value is truthy — `{ "opacity-50": disabled }`. */
264
+ interface ClassDictionary {
265
+ [className: string]: unknown;
266
+ }
267
+ type ClassArray = ClassValue[];
268
+ /**
269
+ * Joins every truthy piece across all arguments with a single space,
270
+ * skipping falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`)
271
+ * entirely — they contribute nothing, not even an extra space. Arrays are
272
+ * flattened (recursively, to any depth); a plain object keeps only the
273
+ * keys whose value is truthy.
274
+ */
275
+ declare function clsx(...inputs: ClassValue[]): string;
276
+
277
+ export { type ClassArray as C, type KbachConfig as K, ThemeProvider as T, type ClassDictionary as a, type ClassValue as b, type ColorScale as c, type ColorsAPI as d, type ThemeProviderProps as e, type ThemeState as f, type ThemeStorageAdapter as g, applyKbachConfig as h, clsx as i, useDynamicToken as j, useTheme as k, resolveKbachConfig as r, useColors as u };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Native port of @kbach/react's darkModeStore.ts — same architecture (a
3
+ * single module-level store is the real source of truth;
4
+ * ThemeProvider/useTheme are a thin, OPTIONAL wrapper around it; every
5
+ * function here works standalone with no provider mounted anywhere), with
6
+ * two real platform differences from the web version:
7
+ *
8
+ * 1. No persistence. There's no React-Native-builtin equivalent of
9
+ * localStorage — adding one would mean forcing a real native dependency
10
+ * (e.g. @react-native-async-storage/async-storage) on every consumer,
11
+ * even ones that don't want it. The mode always starts at 'system' on a
12
+ * fresh app launch. If you want persistence, read your own storage at
13
+ * startup (before your first render) and call setGlobalThemeMode() with
14
+ * the result — same "seed once, no built-in write-back" shape as
15
+ * seedDefaultMode() below, just driven by your own storage instead of
16
+ * one this package picks for you.
17
+ * 2. `applyToDom` below IS needed here, unlike what an earlier version of
18
+ * this file's own comment claimed ("no DOM to apply the resolved mode
19
+ * to") — that reasoning only holds for real native (Android/iOS), where
20
+ * nativeBridge.ts's resolveStyle() reads getGlobalDarkMode() directly on
21
+ * every call, no DOM involved at all. This file is ALSO used by Expo
22
+ * Web, though (no `.web.ts` sibling — Metro has nothing to swap in), and
23
+ * Expo Web resolves `dark:` classes to REAL CSS with `[data-theme=
24
+ * "dark"]`-style selectors (see nativeBridge.web.ts) that only ever
25
+ * match if something writes that attribute to the DOM — nothing did,
26
+ * which is exactly why toggling the mode changed `useTheme()`'s own
27
+ * state (a consumer reading `mode`/`isDark` directly, e.g. for a label,
28
+ * updated correctly) while every `dark:`-prefixed style silently never
29
+ * applied. `typeof document === 'undefined'` is what makes one function
30
+ * safe on both platforms: real native's Hermes has no `document` global
31
+ * at all (not even `undefined` as a defined-but-empty value — genuinely
32
+ * unset, which `typeof` can check without throwing), so this always
33
+ * no-ops there, while Expo Web's react-native-web renders into a real
34
+ * browser DOM where it's needed.
35
+ */
36
+ type ThemeMode = 'light' | 'dark' | 'system';
37
+ /**
38
+ * Re-applies the CURRENT `isDark` value to the DOM (Expo Web only — a
39
+ * genuine no-op on real native, same as `applyToDom` itself) under whatever
40
+ * `getTheme().darkMode` strategy is active right now, with no change to
41
+ * `mode`/`isDark` themselves, so it deliberately does NOT `notify()`.
42
+ * Mirrors @kbach/react's identical `resyncDomWithActiveTheme` — see that
43
+ * one's own doc comment for the full race it fixes: this module's initial
44
+ * `applyToDom(isDark)` call (above) runs at IMPORT time against whichever
45
+ * `darkMode` strategy `defaultTheme` had at that moment, which on Expo Web
46
+ * is BEFORE an app's own `applyKbachConfig({darkMode: 'attribute'})` call
47
+ * (in config.ts) has had a chance to switch it — without this, that config
48
+ * call would silently never get its `dark:` classes to apply on initial
49
+ * load, even though `useTheme().isDark` already reads correctly.
50
+ */
51
+ declare function resyncDomWithActiveTheme(): void;
52
+ declare function getGlobalDarkMode(): boolean;
53
+ declare function getGlobalThemeMode(): ThemeMode;
54
+ /** Explicit user choice — takes priority over any later `<ThemeProvider defaultMode>` seed (see `seedDefaultMode` below). Not persisted; see this module's own doc comment. */
55
+ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
56
+ declare function toggleGlobalDarkMode(): void;
57
+ declare function subscribeGlobalDarkMode(listener: () => void): () => void;
58
+ /**
59
+ * Applies `fallback` as the initial mode, but ONLY if nothing has made an
60
+ * explicit choice yet — called once by `<ThemeProvider defaultMode>` on
61
+ * mount, never exported for direct use elsewhere. Unlike the web store,
62
+ * there's no persisted-value check here (nothing to check); the only
63
+ * thing that can beat a seed is a `setGlobalThemeMode()` call that
64
+ * happened first (e.g. your own storage-restore logic run before the
65
+ * provider mounts).
66
+ */
67
+ declare function seedDefaultMode(fallback: ThemeMode): void;
68
+ /** Exported for tests only — resets every module-level field back to its as-if-just-imported state. */
69
+ declare function _resetForTests(): void;
70
+ /** Exported for tests only — removes the Appearance change subscription so a test's mock doesn't leak into the next one. */
71
+ declare function _unsubscribeForTests(): void;
72
+
73
+ export { type ThemeMode, _resetForTests, _unsubscribeForTests, getGlobalDarkMode, getGlobalThemeMode, resyncDomWithActiveTheme, seedDefaultMode, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode };
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/darkModeStore.ts
21
+ var darkModeStore_exports = {};
22
+ __export(darkModeStore_exports, {
23
+ _resetForTests: () => _resetForTests,
24
+ _unsubscribeForTests: () => _unsubscribeForTests,
25
+ getGlobalDarkMode: () => getGlobalDarkMode,
26
+ getGlobalThemeMode: () => getGlobalThemeMode,
27
+ resyncDomWithActiveTheme: () => resyncDomWithActiveTheme,
28
+ seedDefaultMode: () => seedDefaultMode,
29
+ setGlobalThemeMode: () => setGlobalThemeMode,
30
+ subscribeGlobalDarkMode: () => subscribeGlobalDarkMode,
31
+ toggleGlobalDarkMode: () => toggleGlobalDarkMode
32
+ });
33
+ module.exports = __toCommonJS(darkModeStore_exports);
34
+ var import_react_native = require("react-native");
35
+ var import_theme = require("./theme");
36
+ var listeners = /* @__PURE__ */ new Set();
37
+ function systemPrefersDark() {
38
+ return import_react_native.Appearance.getColorScheme() === "dark";
39
+ }
40
+ function resolveIsDark(m) {
41
+ return m === "system" ? systemPrefersDark() : m === "dark";
42
+ }
43
+ function applyToDom(dark) {
44
+ if (typeof document === "undefined") return;
45
+ const root = document.documentElement;
46
+ const strategy = (0, import_theme.getTheme)().darkMode;
47
+ root.classList.toggle("dark", strategy === "class" && dark);
48
+ root.classList.toggle("light", strategy === "class" && !dark);
49
+ if (strategy === "attribute") {
50
+ root.setAttribute("data-theme", dark ? "dark" : "light");
51
+ } else {
52
+ root.removeAttribute("data-theme");
53
+ }
54
+ }
55
+ var mode = "system";
56
+ var isDark = resolveIsDark(mode);
57
+ applyToDom(isDark);
58
+ var hasExplicitChoice = false;
59
+ var subscription = import_react_native.Appearance.addChangeListener(({ colorScheme }) => {
60
+ if (mode !== "system") return;
61
+ isDark = colorScheme === "dark";
62
+ applyToDom(isDark);
63
+ notify();
64
+ });
65
+ function notify() {
66
+ for (const listener of listeners) listener();
67
+ }
68
+ function resyncDomWithActiveTheme() {
69
+ applyToDom(isDark);
70
+ }
71
+ function getGlobalDarkMode() {
72
+ return isDark;
73
+ }
74
+ function getGlobalThemeMode() {
75
+ return mode;
76
+ }
77
+ function setGlobalThemeMode(nextMode) {
78
+ mode = nextMode;
79
+ isDark = resolveIsDark(nextMode);
80
+ hasExplicitChoice = true;
81
+ applyToDom(isDark);
82
+ notify();
83
+ }
84
+ function toggleGlobalDarkMode() {
85
+ setGlobalThemeMode(isDark ? "light" : "dark");
86
+ }
87
+ function subscribeGlobalDarkMode(listener) {
88
+ listeners.add(listener);
89
+ return () => listeners.delete(listener);
90
+ }
91
+ function seedDefaultMode(fallback) {
92
+ if (hasExplicitChoice || mode === fallback) return;
93
+ mode = fallback;
94
+ isDark = resolveIsDark(fallback);
95
+ applyToDom(isDark);
96
+ notify();
97
+ }
98
+ function _resetForTests() {
99
+ mode = "system";
100
+ isDark = resolveIsDark(mode);
101
+ hasExplicitChoice = false;
102
+ listeners.clear();
103
+ }
104
+ function _unsubscribeForTests() {
105
+ subscription.remove();
106
+ }
107
+ // Annotate the CommonJS export names for ESM import in node:
108
+ 0 && (module.exports = {
109
+ _resetForTests,
110
+ _unsubscribeForTests,
111
+ getGlobalDarkMode,
112
+ getGlobalThemeMode,
113
+ resyncDomWithActiveTheme,
114
+ seedDefaultMode,
115
+ setGlobalThemeMode,
116
+ subscribeGlobalDarkMode,
117
+ toggleGlobalDarkMode
118
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Cross-platform runtime-settable style values — the native counterpart to
3
+ * a real CSS custom property. Real `var(--sidebar-width)` already works on
4
+ * Expo Web today with ZERO code here (raw CSS text, resolved by the
5
+ * browser's own engine — see `jsxRuntimeCoreWeb.ts`, which does no
6
+ * className rewriting at all). Native has no such thing: RN's style system
7
+ * needs an actual JS-computed value at render time, so this module exists
8
+ * to give `var(--x)` an equivalent that works there too — same
9
+ * "module-level store is the real source of truth, no Context required"
10
+ * architecture as `darkModeStore.ts`, and `setDynamicToken` ALSO writes the
11
+ * real CSS custom property to the DOM when one exists (mirroring that
12
+ * file's own `applyToDom`), so a single call updates both platforms
13
+ * identically: instantly via native CSS reactivity on web, via this
14
+ * store's subscribers (see `jsxRuntimeCore.ts`'s `ReactiveElement`, which
15
+ * substitutes a registered token's current value into the className
16
+ * string before every resolve) on native.
17
+ *
18
+ * Deliberately ONE listener set shared by every token, not per-token
19
+ * subscriptions — same trade-off `darkModeStore.ts` already makes for
20
+ * "any relevant change re-renders every subscribed element" simplicity.
21
+ * An unrelated token's change still re-renders every subscribed element
22
+ * (there's no per-token filtering), but `substituteDynamicTokens` in
23
+ * `jsxRuntimeCore.ts` re-reads each token's CURRENT value fresh on every
24
+ * one of those re-renders, so the resolved style stays correct regardless
25
+ * — the redundant re-render is a cheap, accepted trade-off, not a bug.
26
+ */
27
+ /**
28
+ * `name` is the bare token name, WITHOUT the leading `--` (matching how
29
+ * `getDynamicToken`/`var(--name)` substitution both address it).
30
+ *
31
+ * `notify()` below is synchronous and immediately re-renders every
32
+ * already-mounted `ReactiveElement` subscribed via `useSyncExternalStore`
33
+ * (jsxRuntimeCore.ts) — safe when called from a component's own render
34
+ * body (React explicitly allows a component updating external state during
35
+ * its OWN render), a real hazard called from anywhere else with no fixed
36
+ * point in the render lifecycle: module-top-level code in particular runs
37
+ * whenever Metro's inline-requires first evaluates that module, which can
38
+ * land in the middle of React rendering a completely unrelated component
39
+ * elsewhere in the tree — the "Cannot update a component while rendering a
40
+ * different component" class of bug. Reported from a QA pass. Call it
41
+ * synchronously from your app's ROOT component's render (before any
42
+ * consumer has mounted), guarded so it only fires once — see this
43
+ * package's own AGENTS.md section 8 for the full Bad/Good example.
44
+ */
45
+ declare function setDynamicToken(name: string, value: string): void;
46
+ declare function getDynamicToken(name: string): string | undefined;
47
+ declare function deleteDynamicToken(name: string): void;
48
+ declare function subscribeDynamicTokens(listener: () => void): () => void;
49
+ declare function getDynamicTokensVersion(): number;
50
+ /** Exported for tests only — resets every module-level field back to its as-if-just-imported state. */
51
+ declare function _resetForTests(): void;
52
+
53
+ export { _resetForTests, deleteDynamicToken, getDynamicToken, getDynamicTokensVersion, setDynamicToken, subscribeDynamicTokens };