@oxyhq/bloom 0.6.12 → 0.6.14
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/lib/commonjs/hooks/useControllableState.js +35 -0
- package/lib/commonjs/hooks/useControllableState.js.map +1 -0
- package/lib/commonjs/theme/BloomThemeProvider.js +134 -112
- package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
- package/lib/commonjs/theme/apply-dark-class.js +6 -1
- package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
- package/lib/commonjs/theme/build-theme.js +110 -0
- package/lib/commonjs/theme/build-theme.js.map +1 -0
- package/lib/commonjs/theme/color-presets.js +15 -0
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/index.js +15 -1
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/persistence.js +127 -0
- package/lib/commonjs/theme/persistence.js.map +1 -0
- package/lib/commonjs/theme/use-isomorphic-layout-effect.js +15 -0
- package/lib/commonjs/theme/use-isomorphic-layout-effect.js.map +1 -0
- package/lib/module/hooks/useControllableState.js +31 -0
- package/lib/module/hooks/useControllableState.js.map +1 -0
- package/lib/module/theme/BloomThemeProvider.js +135 -112
- package/lib/module/theme/BloomThemeProvider.js.map +1 -1
- package/lib/module/theme/apply-dark-class.js +6 -1
- package/lib/module/theme/apply-dark-class.js.map +1 -1
- package/lib/module/theme/build-theme.js +105 -0
- package/lib/module/theme/build-theme.js.map +1 -0
- package/lib/module/theme/color-presets.js +15 -0
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/index.js +3 -1
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/persistence.js +120 -0
- package/lib/module/theme/persistence.js.map +1 -0
- package/lib/module/theme/use-isomorphic-layout-effect.js +12 -0
- package/lib/module/theme/use-isomorphic-layout-effect.js.map +1 -0
- package/lib/typescript/commonjs/hooks/useControllableState.d.ts +19 -0
- package/lib/typescript/commonjs/hooks/useControllableState.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts +29 -12
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +5 -0
- package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/build-theme.d.ts +20 -0
- package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-presets.d.ts +18 -3
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.d.ts +7 -4
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/persistence.d.ts +48 -0
- package/lib/typescript/commonjs/theme/persistence.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/use-isomorphic-layout-effect.d.ts +8 -0
- package/lib/typescript/commonjs/theme/use-isomorphic-layout-effect.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useControllableState.d.ts +19 -0
- package/lib/typescript/module/hooks/useControllableState.d.ts.map +1 -0
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts +29 -12
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/module/theme/apply-dark-class.d.ts +5 -0
- package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
- package/lib/typescript/module/theme/build-theme.d.ts +20 -0
- package/lib/typescript/module/theme/build-theme.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-presets.d.ts +18 -3
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.d.ts +7 -4
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/persistence.d.ts +48 -0
- package/lib/typescript/module/theme/persistence.d.ts.map +1 -0
- package/lib/typescript/module/theme/use-isomorphic-layout-effect.d.ts +8 -0
- package/lib/typescript/module/theme/use-isomorphic-layout-effect.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/BloomThemeProvider.modes.test.tsx +159 -0
- package/src/__tests__/BloomThemeProvider.persistence.test.tsx +217 -0
- package/src/__tests__/persistence.test.ts +155 -0
- package/src/__tests__/theme.test.ts +1 -1
- package/src/hooks/useControllableState.ts +44 -0
- package/src/theme/BloomThemeProvider.tsx +219 -157
- package/src/theme/apply-dark-class.ts +6 -1
- package/src/theme/build-theme.ts +128 -0
- package/src/theme/color-presets.ts +19 -3
- package/src/theme/index.ts +16 -4
- package/src/theme/persistence.ts +149 -0
- package/src/theme/use-isomorphic-layout-effect.ts +10 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { APP_COLOR_NAMES, type AppColorName } from './color-presets';
|
|
3
|
+
import type { ThemeMode } from './types';
|
|
4
|
+
|
|
5
|
+
const VALID_PRESETS = new Set<string>(APP_COLOR_NAMES);
|
|
6
|
+
const VALID_MODES = new Set<string>(['light', 'dark', 'system', 'adaptive']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Storage adapter for persisting Bloom theme state.
|
|
10
|
+
*
|
|
11
|
+
* Methods may be synchronous or asynchronous. The provider awaits both, so
|
|
12
|
+
* `AsyncStorage`-compatible adapters on native and `localStorage`-backed
|
|
13
|
+
* adapters on web both work without consumer-side branching.
|
|
14
|
+
*
|
|
15
|
+
* On web, a synchronous adapter lets Bloom hydrate before the first paint —
|
|
16
|
+
* preventing a flash of the default palette. On native, the provider can
|
|
17
|
+
* gate `children` rendering until hydration completes (see
|
|
18
|
+
* `awaitHydration` on `BloomThemeProvider`).
|
|
19
|
+
*/
|
|
20
|
+
export interface BloomThemeStorage {
|
|
21
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
22
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PersistedThemeState {
|
|
26
|
+
mode?: ThemeMode;
|
|
27
|
+
colorPreset?: AppColorName;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isValidMode = (value: unknown): value is ThemeMode =>
|
|
31
|
+
typeof value === 'string' && VALID_MODES.has(value);
|
|
32
|
+
|
|
33
|
+
const isValidPreset = (value: unknown): value is AppColorName =>
|
|
34
|
+
typeof value === 'string' && VALID_PRESETS.has(value);
|
|
35
|
+
|
|
36
|
+
function parsePersistedTheme(raw: unknown): PersistedThemeState | null {
|
|
37
|
+
if (typeof raw !== 'string') return null;
|
|
38
|
+
|
|
39
|
+
let parsed: unknown;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
47
|
+
const obj = parsed as { mode?: unknown; colorPreset?: unknown };
|
|
48
|
+
|
|
49
|
+
const mode = isValidMode(obj.mode) ? obj.mode : undefined;
|
|
50
|
+
const colorPreset = isValidPreset(obj.colorPreset) ? obj.colorPreset : undefined;
|
|
51
|
+
|
|
52
|
+
if (!mode && !colorPreset) return null;
|
|
53
|
+
return { mode, colorPreset };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type SyncReadResult =
|
|
57
|
+
| { kind: 'sync'; state: PersistedThemeState | null }
|
|
58
|
+
| { kind: 'async' }
|
|
59
|
+
| { kind: 'none' };
|
|
60
|
+
|
|
61
|
+
const isThenable = (value: unknown): value is Promise<unknown> =>
|
|
62
|
+
!!value && typeof (value as { then?: unknown }).then === 'function';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read persisted state during the first render. Returns a discriminated union
|
|
66
|
+
* so the provider can tell apart:
|
|
67
|
+
* - `none`: persistence not configured.
|
|
68
|
+
* - `sync`: adapter returned a value synchronously (web `localStorage`).
|
|
69
|
+
* Provider can render immediately with the resolved state.
|
|
70
|
+
* - `async`: adapter returned a Promise (native `AsyncStorage`). Provider
|
|
71
|
+
* must await `readPersistedTheme` before painting if gating is enabled.
|
|
72
|
+
*/
|
|
73
|
+
export function readPersistedThemeSync(
|
|
74
|
+
persistKey: string | undefined,
|
|
75
|
+
storage: BloomThemeStorage | undefined,
|
|
76
|
+
): SyncReadResult {
|
|
77
|
+
if (!persistKey || !storage) return { kind: 'none' };
|
|
78
|
+
|
|
79
|
+
let raw: string | null | Promise<string | null>;
|
|
80
|
+
try {
|
|
81
|
+
raw = storage.getItem(persistKey);
|
|
82
|
+
} catch {
|
|
83
|
+
return { kind: 'sync', state: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isThenable(raw)) return { kind: 'async' };
|
|
87
|
+
return { kind: 'sync', state: parsePersistedTheme(raw) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function readPersistedTheme(
|
|
91
|
+
persistKey: string | undefined,
|
|
92
|
+
storage: BloomThemeStorage | undefined,
|
|
93
|
+
): Promise<PersistedThemeState | null> {
|
|
94
|
+
if (!persistKey || !storage) return null;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const raw = await storage.getItem(persistKey);
|
|
98
|
+
return parsePersistedTheme(raw);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function writePersistedTheme(
|
|
105
|
+
persistKey: string | undefined,
|
|
106
|
+
storage: BloomThemeStorage | undefined,
|
|
107
|
+
state: PersistedThemeState,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (!persistKey || !storage) return;
|
|
110
|
+
|
|
111
|
+
const payload: PersistedThemeState = {};
|
|
112
|
+
if (state.mode !== undefined) payload.mode = state.mode;
|
|
113
|
+
if (state.colorPreset !== undefined) payload.colorPreset = state.colorPreset;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await storage.setItem(persistKey, JSON.stringify(payload));
|
|
117
|
+
} catch {
|
|
118
|
+
// Persistence is best-effort: ignore quota and privacy-mode errors.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `localStorage`-backed storage adapter. Only defined on web; on native the
|
|
124
|
+
* export is `undefined` so consumers pass an `AsyncStorage` adapter explicitly.
|
|
125
|
+
*/
|
|
126
|
+
export const webLocalStorage: BloomThemeStorage | undefined = (() => {
|
|
127
|
+
if (Platform.OS !== 'web') return undefined;
|
|
128
|
+
if (typeof globalThis === 'undefined') return undefined;
|
|
129
|
+
if (!('localStorage' in globalThis)) return undefined;
|
|
130
|
+
|
|
131
|
+
const ls = (globalThis as { localStorage: Storage }).localStorage;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
getItem: (key) => {
|
|
135
|
+
try {
|
|
136
|
+
return ls.getItem(key);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
setItem: (key, value) => {
|
|
142
|
+
try {
|
|
143
|
+
ls.setItem(key, value);
|
|
144
|
+
} catch {
|
|
145
|
+
// Swallow quota / privacy-mode errors.
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
})();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `useLayoutEffect` on web (and native — react-native polyfills it) but falls
|
|
6
|
+
* back to `useEffect` during SSR to avoid the React warning. Bloom apps are
|
|
7
|
+
* primarily Expo (no SSR), but Next.js consumers of the web bundle need this.
|
|
8
|
+
*/
|
|
9
|
+
export const useIsomorphicLayoutEffect =
|
|
10
|
+
Platform.OS === 'web' && typeof document === 'undefined' ? useEffect : useLayoutEffect;
|