@kbach/react-native 1.0.0-beta.4 → 1.0.0-beta.40
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 +84 -132
- package/android/src/main/jniLibs/arm64-v8a/libkbach_core_engine.so +0 -0
- package/android/src/main/jniLibs/x86_64/libkbach_core_engine.so +0 -0
- package/dist/chunk-FLPPVP5G.mjs +40 -0
- package/dist/chunk-XPVSILI4.mjs +112 -0
- package/dist/chunk-ZYE4VIZE.mjs +564 -0
- package/dist/clsx-BLLSnqAa.d.ts +277 -0
- package/dist/darkModeStore.d.ts +73 -0
- package/dist/darkModeStore.js +118 -0
- package/dist/dynamicTokens.d.ts +36 -0
- package/dist/dynamicTokens.js +75 -0
- package/dist/index.d.ts +11 -156
- package/dist/index.js +2072 -393
- package/dist/index.web.d.ts +15 -4
- package/dist/index.web.js +438 -401
- package/dist/index.web.mjs +268 -14
- package/dist/jsx-dev-runtime.js +2177 -332
- package/dist/jsx-dev-runtime.web.js +206 -361
- package/dist/jsx-dev-runtime.web.mjs +19 -7
- package/dist/jsx-runtime.js +2177 -332
- package/dist/jsx-runtime.web.js +191 -353
- package/dist/jsx-runtime.web.mjs +3 -6
- package/dist/theme.d.ts +54 -0
- package/dist/{chunk-AEVOM2NB.mjs → theme.js} +86 -61
- package/package.json +43 -14
- package/src/babel-plugin.js +144 -2
- package/dist/chunk-4KCCEMEN.mjs +0 -66
- package/dist/chunk-DLIXI5OW.mjs +0 -53
- package/dist/chunk-FG5E4NLJ.mjs +0 -18
- package/dist/chunk-KSDFO34H.mjs +0 -32
- package/dist/chunk-OJQAJLYT.mjs +0 -14
- package/dist/chunk-ZUE6SCTE.mjs +0 -14
- package/dist/index.d.mts +0 -161
- package/dist/index.mjs +0 -32
- package/dist/index.web.d.mts +0 -7
- package/dist/jsx-dev-runtime.d.mts +0 -10
- package/dist/jsx-dev-runtime.mjs +0 -17
- package/dist/jsx-dev-runtime.web.d.mts +0 -10
- package/dist/jsx-runtime.d.mts +0 -7
- package/dist/jsx-runtime.mjs +0 -14
- package/dist/jsx-runtime.web.d.mts +0 -7
|
@@ -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,36 @@
|
|
|
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
|
+
/** `name` is the bare token name, WITHOUT the leading `--` (matching how `getDynamicToken`/`var(--name)` substitution both address it). */
|
|
28
|
+
declare function setDynamicToken(name: string, value: string): void;
|
|
29
|
+
declare function getDynamicToken(name: string): string | undefined;
|
|
30
|
+
declare function deleteDynamicToken(name: string): void;
|
|
31
|
+
declare function subscribeDynamicTokens(listener: () => void): () => void;
|
|
32
|
+
declare function getDynamicTokensVersion(): number;
|
|
33
|
+
/** Exported for tests only — resets every module-level field back to its as-if-just-imported state. */
|
|
34
|
+
declare function _resetForTests(): void;
|
|
35
|
+
|
|
36
|
+
export { _resetForTests, deleteDynamicToken, getDynamicToken, getDynamicTokensVersion, setDynamicToken, subscribeDynamicTokens };
|
|
@@ -0,0 +1,75 @@
|
|
|
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/dynamicTokens.ts
|
|
21
|
+
var dynamicTokens_exports = {};
|
|
22
|
+
__export(dynamicTokens_exports, {
|
|
23
|
+
_resetForTests: () => _resetForTests,
|
|
24
|
+
deleteDynamicToken: () => deleteDynamicToken,
|
|
25
|
+
getDynamicToken: () => getDynamicToken,
|
|
26
|
+
getDynamicTokensVersion: () => getDynamicTokensVersion,
|
|
27
|
+
setDynamicToken: () => setDynamicToken,
|
|
28
|
+
subscribeDynamicTokens: () => subscribeDynamicTokens
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(dynamicTokens_exports);
|
|
31
|
+
var tokens = /* @__PURE__ */ new Map();
|
|
32
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
33
|
+
var version = 0;
|
|
34
|
+
function applyToDom(name, value) {
|
|
35
|
+
if (typeof document === "undefined") return;
|
|
36
|
+
document.documentElement.style.setProperty(`--${name}`, value);
|
|
37
|
+
}
|
|
38
|
+
function notify() {
|
|
39
|
+
version++;
|
|
40
|
+
for (const listener of listeners) listener();
|
|
41
|
+
}
|
|
42
|
+
function setDynamicToken(name, value) {
|
|
43
|
+
tokens.set(name, value);
|
|
44
|
+
applyToDom(name, value);
|
|
45
|
+
notify();
|
|
46
|
+
}
|
|
47
|
+
function getDynamicToken(name) {
|
|
48
|
+
return tokens.get(name);
|
|
49
|
+
}
|
|
50
|
+
function deleteDynamicToken(name) {
|
|
51
|
+
if (!tokens.delete(name)) return;
|
|
52
|
+
if (typeof document !== "undefined") document.documentElement.style.removeProperty(`--${name}`);
|
|
53
|
+
notify();
|
|
54
|
+
}
|
|
55
|
+
function subscribeDynamicTokens(listener) {
|
|
56
|
+
listeners.add(listener);
|
|
57
|
+
return () => listeners.delete(listener);
|
|
58
|
+
}
|
|
59
|
+
function getDynamicTokensVersion() {
|
|
60
|
+
return version;
|
|
61
|
+
}
|
|
62
|
+
function _resetForTests() {
|
|
63
|
+
tokens.clear();
|
|
64
|
+
listeners.clear();
|
|
65
|
+
version = 0;
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
_resetForTests,
|
|
70
|
+
deleteDynamicToken,
|
|
71
|
+
getDynamicToken,
|
|
72
|
+
getDynamicTokensVersion,
|
|
73
|
+
setDynamicToken,
|
|
74
|
+
subscribeDynamicTokens
|
|
75
|
+
});
|