@mrmeg/expo-ui 0.17.0 → 0.20.0
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/CHANGELOG.md +146 -0
- package/LLM_USAGE.md +18 -0
- package/README.md +8 -2
- package/dist/components/Avatar.d.ts +80 -0
- package/dist/components/Avatar.js +234 -0
- package/dist/components/BottomSheet.d.ts +9 -1
- package/dist/components/BottomSheet.js +8 -3
- package/dist/components/Carousel.d.ts +73 -0
- package/dist/components/Carousel.js +184 -0
- package/dist/components/Drawer.js +5 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/constants/colors.d.ts +4 -0
- package/dist/constants/colors.js +12 -4
- package/dist/hooks/useResources.d.ts +14 -0
- package/dist/hooks/useResources.js +43 -9
- package/dist/hooks/useTheme.js +16 -2
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/ssrTheme.d.ts +41 -0
- package/dist/state/ssrTheme.js +33 -0
- package/dist/state/themeStore.d.ts +19 -0
- package/dist/state/themeStore.js +55 -6
- package/llms-full.md +8 -3
- package/package.json +1 -1
package/dist/state/themeStore.js
CHANGED
|
@@ -2,6 +2,26 @@ import { Appearance, Platform } from "react-native";
|
|
|
2
2
|
import { create } from "zustand";
|
|
3
3
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
4
|
const THEME_KEY = "user-theme-preference";
|
|
5
|
+
/**
|
|
6
|
+
* Cookie name the web build mirrors the persisted preference into, so a
|
|
7
|
+
* server-rendered host can seed its first render with the visitor's real
|
|
8
|
+
* theme instead of shipping a light tree that recolors after hydration.
|
|
9
|
+
*
|
|
10
|
+
* Same name as the `localStorage` key on purpose: one string to grep, and the
|
|
11
|
+
* blocking color-scheme script in a host's `+html.tsx` already reads it.
|
|
12
|
+
* `localStorage` stays the source of truth; the cookie is a render hint only.
|
|
13
|
+
*/
|
|
14
|
+
export const THEME_COOKIE_NAME = THEME_KEY;
|
|
15
|
+
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
|
16
|
+
function isThemePreference(value) {
|
|
17
|
+
return value === "system" || value === "light" || value === "dark";
|
|
18
|
+
}
|
|
19
|
+
function writeThemeCookie(theme) {
|
|
20
|
+
if (Platform.OS !== "web" || typeof document === "undefined")
|
|
21
|
+
return;
|
|
22
|
+
document.cookie =
|
|
23
|
+
`${THEME_COOKIE_NAME}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
24
|
+
}
|
|
5
25
|
export function resolveThemePreference(userTheme, systemTheme) {
|
|
6
26
|
return userTheme === "system" ? systemTheme : userTheme;
|
|
7
27
|
}
|
|
@@ -14,8 +34,13 @@ function getSystemTheme() {
|
|
|
14
34
|
export const useThemeStore = create((set) => ({
|
|
15
35
|
userTheme: "system",
|
|
16
36
|
// Always start with "light" so SSR and the first client render agree.
|
|
17
|
-
// Real values are populated by `syncThemeFromEnvironment()` after mount
|
|
37
|
+
// Real values are populated by `syncThemeFromEnvironment()` after mount;
|
|
38
|
+
// until then a web render reads `SsrThemeSeedContext` (cookie-derived) so
|
|
39
|
+
// the server and the browser paint the visitor's actual theme.
|
|
18
40
|
systemTheme: "light",
|
|
41
|
+
// False until persistence has been read. `useTheme` uses this to decide
|
|
42
|
+
// whether to trust store state or the per-request SSR seed.
|
|
43
|
+
hasLoadedTheme: false,
|
|
19
44
|
// No overrides by default: the package renders with its built-in palette
|
|
20
45
|
// until a host app calls `setColors`.
|
|
21
46
|
colorOverrides: {},
|
|
@@ -36,6 +61,7 @@ export const useThemeStore = create((set) => ({
|
|
|
36
61
|
setTheme: (theme) => {
|
|
37
62
|
set({
|
|
38
63
|
userTheme: theme,
|
|
64
|
+
hasLoadedTheme: true,
|
|
39
65
|
...(theme === "system" ? { systemTheme: getSystemTheme() } : {}),
|
|
40
66
|
});
|
|
41
67
|
// Save directly when setting theme
|
|
@@ -43,10 +69,14 @@ export const useThemeStore = create((set) => ({
|
|
|
43
69
|
AsyncStorage.setItem(THEME_KEY, theme).catch(() => {
|
|
44
70
|
// Silently fail if storage is not available
|
|
45
71
|
});
|
|
72
|
+
return;
|
|
46
73
|
}
|
|
47
|
-
|
|
74
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
48
75
|
localStorage.setItem(THEME_KEY, theme);
|
|
49
76
|
}
|
|
77
|
+
// Dual-write: localStorage for the client, cookie for the next SSR render
|
|
78
|
+
// (so the server paints the right theme on the visitor's next request).
|
|
79
|
+
writeThemeCookie(theme);
|
|
50
80
|
},
|
|
51
81
|
setSystemTheme: (theme) => {
|
|
52
82
|
set({ systemTheme: theme });
|
|
@@ -54,19 +84,32 @@ export const useThemeStore = create((set) => ({
|
|
|
54
84
|
loadTheme: () => {
|
|
55
85
|
if (Platform.OS !== "web") {
|
|
56
86
|
AsyncStorage.getItem(THEME_KEY).then((saved) => {
|
|
57
|
-
if (saved && (saved
|
|
87
|
+
if (saved && isThemePreference(saved)) {
|
|
58
88
|
set({ userTheme: saved });
|
|
59
89
|
}
|
|
60
90
|
}).catch(() => {
|
|
61
91
|
// Use default if loading fails
|
|
92
|
+
}).finally(() => {
|
|
93
|
+
set({ hasLoadedTheme: true });
|
|
62
94
|
});
|
|
95
|
+
return;
|
|
63
96
|
}
|
|
64
|
-
|
|
97
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
65
98
|
const saved = localStorage.getItem(THEME_KEY);
|
|
66
|
-
if (saved && (saved
|
|
99
|
+
if (saved && isThemePreference(saved)) {
|
|
67
100
|
set({ userTheme: saved });
|
|
101
|
+
// Backfill / repair the cookie so a visitor who set their preference
|
|
102
|
+
// before this mechanism existed (or who cleared cookies) gets a
|
|
103
|
+
// correctly themed server render on their very next request.
|
|
104
|
+
writeThemeCookie(saved);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Nothing persisted: mirror the implicit "system" default so the
|
|
108
|
+
// server stops guessing from a stale cookie.
|
|
109
|
+
writeThemeCookie("system");
|
|
68
110
|
}
|
|
69
111
|
}
|
|
112
|
+
set({ hasLoadedTheme: true });
|
|
70
113
|
}
|
|
71
114
|
}));
|
|
72
115
|
let stopSystemThemeListener = null;
|
|
@@ -112,9 +155,15 @@ export function startSystemThemeListener() {
|
|
|
112
155
|
// environment (persisted preference + OS color scheme listener). Safe to
|
|
113
156
|
// call multiple times — `startSystemThemeListener` is idempotent — and
|
|
114
157
|
// returns the unsubscribe so it can be used directly inside `useEffect`.
|
|
158
|
+
//
|
|
159
|
+
// The listener starts BEFORE `loadTheme()` on purpose: `loadTheme` is what
|
|
160
|
+
// flips `hasLoadedTheme`, which is the moment web renders stop trusting the
|
|
161
|
+
// SSR seed and start trusting store state. Reading the real OS scheme first
|
|
162
|
+
// means a `system` user never sees a frame of the boot-default "light".
|
|
115
163
|
export function syncThemeFromEnvironment() {
|
|
164
|
+
const stop = startSystemThemeListener();
|
|
116
165
|
useThemeStore.getState().loadTheme();
|
|
117
|
-
return
|
|
166
|
+
return stop;
|
|
118
167
|
}
|
|
119
168
|
// Native has no SSR mismatch concern, so keep the historical auto-init
|
|
120
169
|
// behavior there. On web the host app must call `syncThemeFromEnvironment()`
|
package/llms-full.md
CHANGED
|
@@ -51,9 +51,14 @@ package `tx` props translated by its app-owned i18n instance.
|
|
|
51
51
|
## Theme Rules
|
|
52
52
|
|
|
53
53
|
Use `useTheme()` and `useStyles()` from `@mrmeg/expo-ui/hooks`. Use semantic
|
|
54
|
-
tokens such as `background`, `foreground`, `card`, `popover`,
|
|
55
|
-
`input`, `ring`, `primary`, `secondary`, `accent`,
|
|
56
|
-
`destructive`, `success`, and `warning`.
|
|
54
|
+
tokens such as `surfaceSunken`, `background`, `foreground`, `card`, `popover`,
|
|
55
|
+
`border`, `borderStrong`, `input`, `ring`, `primary`, `secondary`, `accent`,
|
|
56
|
+
`mutedForeground`, `destructive`, `success`, and `warning`.
|
|
57
|
+
|
|
58
|
+
Surfaces form a tier ladder rather than a shadow scale: `surfaceSunken` (app
|
|
59
|
+
chrome such as the Drawer rail) < `background` (content) < `card`/`popover`
|
|
60
|
+
(raised) < `muted` (chips, insets). Use `borderStrong` for hairlines on filled
|
|
61
|
+
or raised elements, where `border` would blend into the fill.
|
|
57
62
|
|
|
58
63
|
Use `StyledText` and semantic text aliases instead of raw `Text` for app UI.
|
|
59
64
|
Use package controls instead of hardcoded Pressable/View/Text combinations.
|