@mrmeg/expo-ui 0.1.6 → 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 +24 -4
- package/README.md +20 -1
- package/dist/hooks/useTheme.d.ts +6 -5
- package/dist/hooks/useTheme.js +58 -13
- 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
|
@@ -119,10 +119,30 @@ Token intent:
|
|
|
119
119
|
- `ring`: focus outline color
|
|
120
120
|
- `popover`: elevated overlay surface
|
|
121
121
|
|
|
122
|
-
Use `getShadowStyle()` for package surfaces that need elevation
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
Use `getShadowStyle()` for package surfaces that need elevation. It supports
|
|
123
|
+
`base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
|
|
124
|
+
`cardHover`, and `cardSubtle`, returning native shadow/elevation off web and
|
|
125
|
+
CSS `boxShadow` on web. Use `getFocusRingStyle()` for web focus styling. Keep
|
|
126
|
+
web controls compact, but preserve mobile tap comfort with package controls
|
|
127
|
+
that already provide native hit slop or 44px touch rows.
|
|
128
|
+
|
|
129
|
+
Use `useStyles()` for memoized theme-aware local styles. Its factory receives
|
|
130
|
+
`{ theme, spacing, withAlpha }`, so components can derive alpha-adjusted
|
|
131
|
+
semantic colors without destructuring `withAlpha` outside the factory:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
135
|
+
card: {
|
|
136
|
+
backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
137
|
+
padding: spacing.md,
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
```
|
|
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`.
|
|
126
146
|
|
|
127
147
|
## Component Use-Case Index
|
|
128
148
|
|
package/README.md
CHANGED
|
@@ -95,7 +95,26 @@ const styles = StyleSheet.create({
|
|
|
95
95
|
});
|
|
96
96
|
```
|
|
97
97
|
|
|
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`. 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.
|
|
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
|
+
|
|
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
|
+
|
|
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.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { useStyles } from "@mrmeg/expo-ui/hooks";
|
|
109
|
+
|
|
110
|
+
const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
111
|
+
card: {
|
|
112
|
+
backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
113
|
+
borderRadius: spacing.radiusMd,
|
|
114
|
+
padding: spacing.md,
|
|
115
|
+
},
|
|
116
|
+
}));
|
|
117
|
+
```
|
|
99
118
|
|
|
100
119
|
Use `StyledText` for theme-aware text:
|
|
101
120
|
|
package/dist/hooks/useTheme.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Colors } from "../constants/colors";
|
|
2
2
|
import { ViewStyle, StyleSheet } from "react-native";
|
|
3
3
|
import { spacing as spacingConstants } from "../constants/spacing";
|
|
4
|
-
type ShadowType = "base" | "soft" | "sharp" | "subtle";
|
|
4
|
+
type ShadowType = "base" | "soft" | "sharp" | "subtle" | "elevated" | "glow" | "glass" | "card" | "cardHover" | "cardSubtle";
|
|
5
5
|
interface ExtendedColorScheme {
|
|
6
6
|
theme: Colors["light" | "dark"];
|
|
7
7
|
scheme: "light" | "dark";
|
|
@@ -45,6 +45,7 @@ export declare function useTheme(): ExtendedColorScheme & {
|
|
|
45
45
|
interface StyleContext {
|
|
46
46
|
theme: Colors["light" | "dark"];
|
|
47
47
|
spacing: typeof spacingConstants;
|
|
48
|
+
withAlpha: (color: string, alpha: number) => string;
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
50
51
|
* Return type for useStyles hook
|
|
@@ -58,17 +59,17 @@ type UseStylesReturn<T extends StyleSheet.NamedStyles<T>> = {
|
|
|
58
59
|
* useStyles
|
|
59
60
|
*
|
|
60
61
|
* A hook that combines useTheme with StyleSheet.create for theme-aware styling.
|
|
61
|
-
* Provides access to theme colors
|
|
62
|
+
* Provides access to theme colors, spacing constants, and color helpers within the style factory.
|
|
62
63
|
*
|
|
63
|
-
* @param factory - A function that receives { theme, spacing } and returns style definitions
|
|
64
|
+
* @param factory - A function that receives { theme, spacing, withAlpha } and returns style definitions
|
|
64
65
|
* @returns { styles, theme, spacing, ...themeUtilities }
|
|
65
66
|
*
|
|
66
67
|
* @example
|
|
67
68
|
* ```tsx
|
|
68
69
|
* function MyComponent() {
|
|
69
|
-
* const { styles, theme } = useStyles(({ theme, spacing }) => ({
|
|
70
|
+
* const { styles, theme } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
70
71
|
* container: {
|
|
71
|
-
* backgroundColor: theme.colors.
|
|
72
|
+
* backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
72
73
|
* padding: spacing.md,
|
|
73
74
|
* borderRadius: spacing.radiusMd,
|
|
74
75
|
* },
|
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(() => {
|
|
@@ -109,6 +105,48 @@ export function useTheme() {
|
|
|
109
105
|
shadowRadius: 2,
|
|
110
106
|
elevation: 1,
|
|
111
107
|
},
|
|
108
|
+
elevated: {
|
|
109
|
+
shadowColor: theme.colors.overlay,
|
|
110
|
+
shadowOffset: { width: 0, height: 20 },
|
|
111
|
+
shadowOpacity: 0.15,
|
|
112
|
+
shadowRadius: 40,
|
|
113
|
+
elevation: 16,
|
|
114
|
+
},
|
|
115
|
+
glow: {
|
|
116
|
+
shadowColor: theme.colors.primary,
|
|
117
|
+
shadowOffset: { width: 0, height: 4 },
|
|
118
|
+
shadowOpacity: 0.4,
|
|
119
|
+
shadowRadius: 20,
|
|
120
|
+
elevation: 10,
|
|
121
|
+
},
|
|
122
|
+
glass: {
|
|
123
|
+
shadowColor: theme.colors.overlay,
|
|
124
|
+
shadowOffset: { width: 0, height: 4 },
|
|
125
|
+
shadowOpacity: 0.05,
|
|
126
|
+
shadowRadius: 30,
|
|
127
|
+
elevation: 4,
|
|
128
|
+
},
|
|
129
|
+
card: {
|
|
130
|
+
shadowColor: theme.colors.overlay,
|
|
131
|
+
shadowOffset: { width: 0, height: 2 },
|
|
132
|
+
shadowOpacity: 0.08,
|
|
133
|
+
shadowRadius: 8,
|
|
134
|
+
elevation: 4,
|
|
135
|
+
},
|
|
136
|
+
cardHover: {
|
|
137
|
+
shadowColor: theme.colors.overlay,
|
|
138
|
+
shadowOffset: { width: 0, height: 8 },
|
|
139
|
+
shadowOpacity: 0.12,
|
|
140
|
+
shadowRadius: 24,
|
|
141
|
+
elevation: 8,
|
|
142
|
+
},
|
|
143
|
+
cardSubtle: {
|
|
144
|
+
shadowColor: theme.colors.overlay,
|
|
145
|
+
shadowOffset: { width: 0, height: 1 },
|
|
146
|
+
shadowOpacity: 0.08,
|
|
147
|
+
shadowRadius: 3,
|
|
148
|
+
elevation: 2,
|
|
149
|
+
},
|
|
112
150
|
};
|
|
113
151
|
const config = shadowConfigs[type];
|
|
114
152
|
if (Platform.OS === "web") {
|
|
@@ -117,6 +155,12 @@ export function useTheme() {
|
|
|
117
155
|
soft: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.10)" },
|
|
118
156
|
sharp: { boxShadow: theme.dark ? "0 1px 1px rgba(0, 0, 0, 0.55)" : "0 1px 1px rgba(0, 0, 0, 0.12)" },
|
|
119
157
|
subtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 2px rgba(0, 0, 0, 0.05)" },
|
|
158
|
+
elevated: { boxShadow: theme.dark ? "0 20px 40px rgba(0, 0, 0, 0.38)" : "0 20px 40px rgba(0, 0, 0, 0.15)" },
|
|
159
|
+
glow: { boxShadow: `0 0 20px ${theme.colors.primary}` },
|
|
160
|
+
glass: { boxShadow: theme.dark ? "0 4px 30px rgba(0, 0, 0, 0.32)" : "0 4px 30px rgba(0, 0, 0, 0.05)" },
|
|
161
|
+
card: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.08)" },
|
|
162
|
+
cardHover: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.12)" },
|
|
163
|
+
cardSubtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.05)" },
|
|
120
164
|
};
|
|
121
165
|
return webShadows[type];
|
|
122
166
|
}
|
|
@@ -304,17 +348,17 @@ function withAlpha(color, alpha) {
|
|
|
304
348
|
* useStyles
|
|
305
349
|
*
|
|
306
350
|
* A hook that combines useTheme with StyleSheet.create for theme-aware styling.
|
|
307
|
-
* Provides access to theme colors
|
|
351
|
+
* Provides access to theme colors, spacing constants, and color helpers within the style factory.
|
|
308
352
|
*
|
|
309
|
-
* @param factory - A function that receives { theme, spacing } and returns style definitions
|
|
353
|
+
* @param factory - A function that receives { theme, spacing, withAlpha } and returns style definitions
|
|
310
354
|
* @returns { styles, theme, spacing, ...themeUtilities }
|
|
311
355
|
*
|
|
312
356
|
* @example
|
|
313
357
|
* ```tsx
|
|
314
358
|
* function MyComponent() {
|
|
315
|
-
* const { styles, theme } = useStyles(({ theme, spacing }) => ({
|
|
359
|
+
* const { styles, theme } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
316
360
|
* container: {
|
|
317
|
-
* backgroundColor: theme.colors.
|
|
361
|
+
* backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
318
362
|
* padding: spacing.md,
|
|
319
363
|
* borderRadius: spacing.radiusMd,
|
|
320
364
|
* },
|
|
@@ -337,7 +381,8 @@ export function useStyles(factory) {
|
|
|
337
381
|
const styles = useMemo(() => StyleSheet.create(factory({
|
|
338
382
|
theme: themeContext.theme,
|
|
339
383
|
spacing: spacingConstants,
|
|
340
|
-
|
|
384
|
+
withAlpha: themeContext.withAlpha,
|
|
385
|
+
})), [factory, themeContext.theme, themeContext.withAlpha]);
|
|
341
386
|
return useMemo(() => ({
|
|
342
387
|
styles,
|
|
343
388
|
theme: themeContext.theme,
|
|
@@ -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();
|