@mrmeg/expo-ui 0.18.0 → 0.21.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 +283 -0
- package/LLM_USAGE.md +14 -0
- package/README.md +7 -1
- package/dist/components/Avatar.d.ts +94 -0
- package/dist/components/Avatar.js +296 -0
- package/dist/components/Carousel.d.ts +99 -0
- package/dist/components/Carousel.js +246 -0
- package/dist/components/Drawer.js +5 -2
- package/dist/components/Label.d.ts +30 -10
- package/dist/components/Label.js +19 -10
- 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/constants/fonts.js +5 -4
- package/dist/hooks/useDimensions.d.ts +19 -9
- package/dist/hooks/useDimensions.js +24 -32
- package/dist/hooks/useResources.d.ts +0 -14
- package/dist/hooks/useResources.js +7 -39
- package/dist/lib/themedStyles.d.ts +7 -7
- package/dist/lib/themedStyles.js +7 -7
- package/dist/state/index.d.ts +0 -1
- package/dist/state/index.js +0 -1
- package/dist/state/themeStore.d.ts +15 -0
- package/dist/state/themeStore.js +38 -10
- package/llms-full.md +9 -4
- package/package.json +1 -1
- package/dist/state/SsrViewportContext.d.ts +0 -41
- package/dist/state/SsrViewportContext.js +0 -42
package/dist/state/themeStore.js
CHANGED
|
@@ -2,6 +2,16 @@ 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
|
+
* Key the persisted preference lives under (`localStorage` on web,
|
|
7
|
+
* `AsyncStorage` on native). Exported so a host app's pre-boot script — e.g.
|
|
8
|
+
* the blocking color-scheme script in `app/+html.tsx` — can read the same
|
|
9
|
+
* value the store does.
|
|
10
|
+
*/
|
|
11
|
+
export const THEME_STORAGE_KEY = THEME_KEY;
|
|
12
|
+
function isThemePreference(value) {
|
|
13
|
+
return value === "system" || value === "light" || value === "dark";
|
|
14
|
+
}
|
|
5
15
|
export function resolveThemePreference(userTheme, systemTheme) {
|
|
6
16
|
return userTheme === "system" ? systemTheme : userTheme;
|
|
7
17
|
}
|
|
@@ -13,9 +23,13 @@ function getSystemTheme() {
|
|
|
13
23
|
}
|
|
14
24
|
export const useThemeStore = create((set) => ({
|
|
15
25
|
userTheme: "system",
|
|
16
|
-
// Always start with "light" so
|
|
17
|
-
//
|
|
26
|
+
// Always start with "light" so a web app's first render matches the HTML
|
|
27
|
+
// shell that was rendered in Node at export time (no `window`, no storage).
|
|
28
|
+
// Real values arrive from `syncThemeFromEnvironment()` after mount; a host
|
|
29
|
+
// app's pre-boot script paints the correct background in the meantime.
|
|
18
30
|
systemTheme: "light",
|
|
31
|
+
// False until persistence has been read.
|
|
32
|
+
hasLoadedTheme: false,
|
|
19
33
|
// No overrides by default: the package renders with its built-in palette
|
|
20
34
|
// until a host app calls `setColors`.
|
|
21
35
|
colorOverrides: {},
|
|
@@ -36,6 +50,7 @@ export const useThemeStore = create((set) => ({
|
|
|
36
50
|
setTheme: (theme) => {
|
|
37
51
|
set({
|
|
38
52
|
userTheme: theme,
|
|
53
|
+
hasLoadedTheme: true,
|
|
39
54
|
...(theme === "system" ? { systemTheme: getSystemTheme() } : {}),
|
|
40
55
|
});
|
|
41
56
|
// Save directly when setting theme
|
|
@@ -43,8 +58,9 @@ export const useThemeStore = create((set) => ({
|
|
|
43
58
|
AsyncStorage.setItem(THEME_KEY, theme).catch(() => {
|
|
44
59
|
// Silently fail if storage is not available
|
|
45
60
|
});
|
|
61
|
+
return;
|
|
46
62
|
}
|
|
47
|
-
|
|
63
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
48
64
|
localStorage.setItem(THEME_KEY, theme);
|
|
49
65
|
}
|
|
50
66
|
},
|
|
@@ -54,19 +70,23 @@ export const useThemeStore = create((set) => ({
|
|
|
54
70
|
loadTheme: () => {
|
|
55
71
|
if (Platform.OS !== "web") {
|
|
56
72
|
AsyncStorage.getItem(THEME_KEY).then((saved) => {
|
|
57
|
-
if (saved && (saved
|
|
73
|
+
if (saved && isThemePreference(saved)) {
|
|
58
74
|
set({ userTheme: saved });
|
|
59
75
|
}
|
|
60
76
|
}).catch(() => {
|
|
61
77
|
// Use default if loading fails
|
|
78
|
+
}).finally(() => {
|
|
79
|
+
set({ hasLoadedTheme: true });
|
|
62
80
|
});
|
|
81
|
+
return;
|
|
63
82
|
}
|
|
64
|
-
|
|
83
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
65
84
|
const saved = localStorage.getItem(THEME_KEY);
|
|
66
|
-
if (saved && (saved
|
|
85
|
+
if (saved && isThemePreference(saved)) {
|
|
67
86
|
set({ userTheme: saved });
|
|
68
87
|
}
|
|
69
88
|
}
|
|
89
|
+
set({ hasLoadedTheme: true });
|
|
70
90
|
}
|
|
71
91
|
}));
|
|
72
92
|
let stopSystemThemeListener = null;
|
|
@@ -112,13 +132,21 @@ export function startSystemThemeListener() {
|
|
|
112
132
|
// environment (persisted preference + OS color scheme listener). Safe to
|
|
113
133
|
// call multiple times — `startSystemThemeListener` is idempotent — and
|
|
114
134
|
// returns the unsubscribe so it can be used directly inside `useEffect`.
|
|
135
|
+
//
|
|
136
|
+
// The listener starts BEFORE `loadTheme()` on purpose: reading the real OS
|
|
137
|
+
// scheme first means a `system` user resolves straight from the boot-default
|
|
138
|
+
// "light" to their actual scheme in one commit.
|
|
115
139
|
export function syncThemeFromEnvironment() {
|
|
140
|
+
const stop = startSystemThemeListener();
|
|
116
141
|
useThemeStore.getState().loadTheme();
|
|
117
|
-
return
|
|
142
|
+
return stop;
|
|
118
143
|
}
|
|
119
|
-
// Native
|
|
120
|
-
// behavior there. On web the host app must call
|
|
121
|
-
// from a top-level `useEffect
|
|
144
|
+
// Native can read persistence at module load, so keep the historical
|
|
145
|
+
// auto-init behavior there. On web the host app must call
|
|
146
|
+
// `syncThemeFromEnvironment()` from a top-level `useEffect`: web bundles are
|
|
147
|
+
// also evaluated in Node when `expo export` renders the HTML shell, where
|
|
148
|
+
// `window`/`localStorage` don't exist, and reading them during the browser's
|
|
149
|
+
// first render would disagree with the markup being hydrated.
|
|
122
150
|
if (Platform.OS !== "web") {
|
|
123
151
|
useThemeStore.getState().loadTheme();
|
|
124
152
|
startSystemThemeListener();
|
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.
|
|
@@ -104,7 +109,7 @@ Use this catalog before creating a new app-local primitive.
|
|
|
104
109
|
| `Icon` | `@mrmeg/expo-ui/components` | Feather or custom icons with theme tokens | Avoid raw vector icons with hardcoded colors. |
|
|
105
110
|
| `InputOTP` | `@mrmeg/expo-ui/components` | Verification code entry | Prefer over manually managed text input groups. |
|
|
106
111
|
| `KeyboardAvoidingView` | `@mrmeg/expo-ui/components` | Native keyboard-aware layout roots, composer footers, and form-heavy subtrees | `UIProvider` already mounts one root by default; use this directly only for custom subtrees. |
|
|
107
|
-
| `Label` | `@mrmeg/expo-ui/components` | Accessible form labels |
|
|
112
|
+
| `Label` | `@mrmeg/expo-ui/components` | Accessible form labels | Pair with two distinct ids: `nativeID` is the label's id, `htmlFor` is the input's `nativeID`. Never reuse one id for both. |
|
|
108
113
|
| `MaxWidthContainer` | `@mrmeg/expo-ui/components` | Centered responsive width | Use for web and tablet constrained layouts. |
|
|
109
114
|
| `Notification` | `@mrmeg/expo-ui/components` | Global toast surface | Trigger through `notify` (or `globalUIStore` for subscriptions/tests) with root `UIProvider`; optional actions dismiss after press. |
|
|
110
115
|
| `Popover` | `@mrmeg/expo-ui/components` | Anchored contextual content | Requires root `UIProvider` portal setup. |
|
package/package.json
CHANGED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default viewport used during SSR and the initial client render. Desktop is
|
|
3
|
-
* chosen because:
|
|
4
|
-
* 1. Most web traffic for app dashboards / marketing pages skews desktop.
|
|
5
|
-
* 2. Desktop visitors see no layout reflow on hydration (SSR layout matches
|
|
6
|
-
* their actual viewport's breakpoint).
|
|
7
|
-
* 3. Mobile visitors get one frame of desktop-styled content before
|
|
8
|
-
* `useDimensions`'s post-mount effect snaps to real dimensions — better
|
|
9
|
-
* than the inverse (every desktop visitor sees mobile-tiny then snap).
|
|
10
|
-
*
|
|
11
|
-
* Projects that skew mobile can override with their own Provider higher in
|
|
12
|
-
* the tree, or per-page via a loader that detects from cookie / User-Agent.
|
|
13
|
-
*/
|
|
14
|
-
export declare const SSR_VIEWPORT_DEFAULT_WIDTH = 1280;
|
|
15
|
-
export declare const SSR_VIEWPORT_DEFAULT_HEIGHT = 800;
|
|
16
|
-
/**
|
|
17
|
-
* Per-request SSR viewport width. Consumed by `useDimensions` to seed the
|
|
18
|
-
* initial responsive render so it matches what the user sees.
|
|
19
|
-
*
|
|
20
|
-
* Set the provider near the top of your tree (typically in a route component
|
|
21
|
-
* whose loader detects the viewport from a cookie or User-Agent):
|
|
22
|
-
*
|
|
23
|
-
* ```tsx
|
|
24
|
-
* import { SsrViewportContext } from "@mrmeg/expo-ui/state";
|
|
25
|
-
* import { detectSsrViewportWidth } from "@/server/lib/ssrViewport";
|
|
26
|
-
*
|
|
27
|
-
* export const loader: LoaderFunction<{ ssrViewportWidth?: number }> = (request) => ({
|
|
28
|
-
* ssrViewportWidth: detectSsrViewportWidth(request),
|
|
29
|
-
* });
|
|
30
|
-
*
|
|
31
|
-
* export default function Page() {
|
|
32
|
-
* const { ssrViewportWidth } = useLoaderData<typeof loader>();
|
|
33
|
-
* return (
|
|
34
|
-
* <SsrViewportContext.Provider value={ssrViewportWidth ?? SSR_VIEWPORT_DEFAULT_WIDTH}>
|
|
35
|
-
* ...
|
|
36
|
-
* </SsrViewportContext.Provider>
|
|
37
|
-
* );
|
|
38
|
-
* }
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export declare const SsrViewportContext: import("react").Context<number>;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { createContext } from "react";
|
|
2
|
-
/**
|
|
3
|
-
* Default viewport used during SSR and the initial client render. Desktop is
|
|
4
|
-
* chosen because:
|
|
5
|
-
* 1. Most web traffic for app dashboards / marketing pages skews desktop.
|
|
6
|
-
* 2. Desktop visitors see no layout reflow on hydration (SSR layout matches
|
|
7
|
-
* their actual viewport's breakpoint).
|
|
8
|
-
* 3. Mobile visitors get one frame of desktop-styled content before
|
|
9
|
-
* `useDimensions`'s post-mount effect snaps to real dimensions — better
|
|
10
|
-
* than the inverse (every desktop visitor sees mobile-tiny then snap).
|
|
11
|
-
*
|
|
12
|
-
* Projects that skew mobile can override with their own Provider higher in
|
|
13
|
-
* the tree, or per-page via a loader that detects from cookie / User-Agent.
|
|
14
|
-
*/
|
|
15
|
-
export const SSR_VIEWPORT_DEFAULT_WIDTH = 1280;
|
|
16
|
-
export const SSR_VIEWPORT_DEFAULT_HEIGHT = 800;
|
|
17
|
-
/**
|
|
18
|
-
* Per-request SSR viewport width. Consumed by `useDimensions` to seed the
|
|
19
|
-
* initial responsive render so it matches what the user sees.
|
|
20
|
-
*
|
|
21
|
-
* Set the provider near the top of your tree (typically in a route component
|
|
22
|
-
* whose loader detects the viewport from a cookie or User-Agent):
|
|
23
|
-
*
|
|
24
|
-
* ```tsx
|
|
25
|
-
* import { SsrViewportContext } from "@mrmeg/expo-ui/state";
|
|
26
|
-
* import { detectSsrViewportWidth } from "@/server/lib/ssrViewport";
|
|
27
|
-
*
|
|
28
|
-
* export const loader: LoaderFunction<{ ssrViewportWidth?: number }> = (request) => ({
|
|
29
|
-
* ssrViewportWidth: detectSsrViewportWidth(request),
|
|
30
|
-
* });
|
|
31
|
-
*
|
|
32
|
-
* export default function Page() {
|
|
33
|
-
* const { ssrViewportWidth } = useLoaderData<typeof loader>();
|
|
34
|
-
* return (
|
|
35
|
-
* <SsrViewportContext.Provider value={ssrViewportWidth ?? SSR_VIEWPORT_DEFAULT_WIDTH}>
|
|
36
|
-
* ...
|
|
37
|
-
* </SsrViewportContext.Provider>
|
|
38
|
-
* );
|
|
39
|
-
* }
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export const SsrViewportContext = createContext(SSR_VIEWPORT_DEFAULT_WIDTH);
|