@mrmeg/expo-ui 0.1.7 → 0.1.8
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/LLM_USAGE.md +5 -0
- package/README.md +5 -0
- package/dist/hooks/useTheme.js +4 -8
- package/dist/state/themeStore.d.ts +9 -2
- package/dist/state/themeStore.js +58 -2
- package/package.json +1 -1
package/LLM_USAGE.md
CHANGED
|
@@ -139,6 +139,11 @@ const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
|
139
139
|
}));
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
When the saved theme preference is `system`, the package theme store owns the
|
|
143
|
+
OS color-scheme subscription, including web `prefers-color-scheme`. Do not add
|
|
144
|
+
app-local Appearance or `matchMedia` listeners for package components; import
|
|
145
|
+
`useTheme()`, `useStyles()`, and `useThemeStore` from `@mrmeg/expo-ui`.
|
|
146
|
+
|
|
142
147
|
## Component Use-Case Index
|
|
143
148
|
|
|
144
149
|
Use this table before creating a new app-local primitive.
|
package/README.md
CHANGED
|
@@ -97,6 +97,11 @@ const styles = StyleSheet.create({
|
|
|
97
97
|
|
|
98
98
|
`useTheme()` returns the active `theme`, resolved `scheme`, persisted `currentTheme`, `setTheme`, `toggleTheme`, cross-platform shadow helpers, a web focus-ring helper, contrast helpers, and `withAlpha`. `getShadowStyle(type)` supports `base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`, `cardHover`, and `cardSubtle`. Use semantic tokens such as `theme.colors.background`, `foreground`, `card`, `popover`, `border`, `input`, `ring`, `primary`, `secondary`, `accent`, `mutedForeground`, `destructive`, `success`, and `warning`. `primary` is the neutral action color, `secondary` is a neutral secondary surface, `accent` is the teal highlight color, `input` is the default form-control border, and `ring` is the focus outline color.
|
|
99
99
|
|
|
100
|
+
When `currentTheme` is `"system"`, the package tracks the OS color scheme and
|
|
101
|
+
updates all `useTheme()` and `useStyles()` consumers through the package theme
|
|
102
|
+
store. Apps should not add their own Appearance or `matchMedia` listeners just
|
|
103
|
+
to make package components follow system light/dark changes.
|
|
104
|
+
|
|
100
105
|
Use `useStyles()` when a component needs memoized theme-aware styles. The style factory receives `{ theme, spacing, withAlpha }`, and the returned hook value also includes the normal `useTheme()` helpers.
|
|
101
106
|
|
|
102
107
|
```tsx
|
package/dist/hooks/useTheme.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { colors } from "../constants/colors.js";
|
|
3
|
-
import {
|
|
4
|
-
import { useThemeStore } from "../state/themeStore.js";
|
|
3
|
+
import { Platform, StyleSheet } from "react-native";
|
|
4
|
+
import { resolveThemePreference, useThemeStore } from "../state/themeStore.js";
|
|
5
5
|
import { spacing as spacingConstants } from "../constants/spacing.js";
|
|
6
6
|
// Module-level cache for contrast calculations to avoid memory leak
|
|
7
7
|
// and share across components
|
|
@@ -45,14 +45,10 @@ function getCachedOrCompute(key, compute) {
|
|
|
45
45
|
*/
|
|
46
46
|
export function useTheme() {
|
|
47
47
|
const userTheme = useThemeStore((s) => s.userTheme);
|
|
48
|
+
const systemTheme = useThemeStore((s) => s.systemTheme);
|
|
48
49
|
const setTheme = useThemeStore((s) => s.setTheme);
|
|
49
|
-
let defaultScheme = useColorSchemeDefault();
|
|
50
|
-
// Ensure a scheme is selected, even if we fail to get one
|
|
51
|
-
if (!defaultScheme) {
|
|
52
|
-
defaultScheme = "light";
|
|
53
|
-
}
|
|
54
50
|
// Determine which theme to use (user preference or system)
|
|
55
|
-
const effectiveScheme = userTheme
|
|
51
|
+
const effectiveScheme = resolveThemePreference(userTheme, systemTheme);
|
|
56
52
|
const theme = colors[effectiveScheme];
|
|
57
53
|
// Sync theme to DOM so CSS in +html.tsx follows the app's runtime theme
|
|
58
54
|
useEffect(() => {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
export type ThemePreference = "system" | "light" | "dark";
|
|
2
|
+
export type ResolvedTheme = "light" | "dark";
|
|
1
3
|
export type ThemeStore = {
|
|
2
|
-
userTheme:
|
|
3
|
-
|
|
4
|
+
userTheme: ThemePreference;
|
|
5
|
+
systemTheme: ResolvedTheme;
|
|
6
|
+
setTheme: (theme: ThemePreference) => void;
|
|
7
|
+
setSystemTheme: (theme: ResolvedTheme) => void;
|
|
4
8
|
loadTheme: () => void;
|
|
5
9
|
};
|
|
10
|
+
export declare function resolveThemePreference(userTheme: ThemePreference, systemTheme: ResolvedTheme): ResolvedTheme;
|
|
6
11
|
export declare const useThemeStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ThemeStore>>;
|
|
12
|
+
export declare function syncSystemTheme(): void;
|
|
13
|
+
export declare function startSystemThemeListener(): () => void;
|
package/dist/state/themeStore.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import { Platform } from "react-native";
|
|
1
|
+
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
|
+
export function resolveThemePreference(userTheme, systemTheme) {
|
|
6
|
+
return userTheme === "system" ? systemTheme : userTheme;
|
|
7
|
+
}
|
|
8
|
+
function getSystemTheme() {
|
|
9
|
+
if (Platform.OS === "web" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
10
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
11
|
+
}
|
|
12
|
+
return Appearance.getColorScheme() === "dark" ? "dark" : "light";
|
|
13
|
+
}
|
|
5
14
|
export const useThemeStore = create((set) => ({
|
|
6
15
|
userTheme: "system",
|
|
16
|
+
systemTheme: getSystemTheme(),
|
|
7
17
|
setTheme: (theme) => {
|
|
8
|
-
set({
|
|
18
|
+
set({
|
|
19
|
+
userTheme: theme,
|
|
20
|
+
...(theme === "system" ? { systemTheme: getSystemTheme() } : {}),
|
|
21
|
+
});
|
|
9
22
|
// Save directly when setting theme
|
|
10
23
|
if (Platform.OS !== "web") {
|
|
11
24
|
AsyncStorage.setItem(THEME_KEY, theme).catch(() => {
|
|
@@ -16,6 +29,9 @@ export const useThemeStore = create((set) => ({
|
|
|
16
29
|
localStorage.setItem(THEME_KEY, theme);
|
|
17
30
|
}
|
|
18
31
|
},
|
|
32
|
+
setSystemTheme: (theme) => {
|
|
33
|
+
set({ systemTheme: theme });
|
|
34
|
+
},
|
|
19
35
|
loadTheme: () => {
|
|
20
36
|
if (Platform.OS !== "web") {
|
|
21
37
|
AsyncStorage.getItem(THEME_KEY).then((saved) => {
|
|
@@ -34,5 +50,45 @@ export const useThemeStore = create((set) => ({
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
}));
|
|
53
|
+
let stopSystemThemeListener = null;
|
|
54
|
+
export function syncSystemTheme() {
|
|
55
|
+
useThemeStore.getState().setSystemTheme(getSystemTheme());
|
|
56
|
+
}
|
|
57
|
+
export function startSystemThemeListener() {
|
|
58
|
+
if (stopSystemThemeListener) {
|
|
59
|
+
return stopSystemThemeListener;
|
|
60
|
+
}
|
|
61
|
+
syncSystemTheme();
|
|
62
|
+
if (Platform.OS === "web" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
63
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
64
|
+
const onChange = () => {
|
|
65
|
+
useThemeStore.getState().setSystemTheme(mediaQuery.matches ? "dark" : "light");
|
|
66
|
+
};
|
|
67
|
+
if (typeof mediaQuery.addEventListener === "function") {
|
|
68
|
+
mediaQuery.addEventListener("change", onChange);
|
|
69
|
+
stopSystemThemeListener = () => {
|
|
70
|
+
mediaQuery.removeEventListener("change", onChange);
|
|
71
|
+
stopSystemThemeListener = null;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
mediaQuery.addListener(onChange);
|
|
76
|
+
stopSystemThemeListener = () => {
|
|
77
|
+
mediaQuery.removeListener(onChange);
|
|
78
|
+
stopSystemThemeListener = null;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return stopSystemThemeListener;
|
|
82
|
+
}
|
|
83
|
+
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
|
84
|
+
useThemeStore.getState().setSystemTheme(colorScheme === "dark" ? "dark" : "light");
|
|
85
|
+
});
|
|
86
|
+
stopSystemThemeListener = () => {
|
|
87
|
+
subscription.remove();
|
|
88
|
+
stopSystemThemeListener = null;
|
|
89
|
+
};
|
|
90
|
+
return stopSystemThemeListener;
|
|
91
|
+
}
|
|
37
92
|
// Load saved theme on store creation
|
|
38
93
|
useThemeStore.getState().loadTheme();
|
|
94
|
+
startSystemThemeListener();
|