@mrmeg/expo-ui 0.20.0 → 0.22.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 +202 -0
- package/LLM_USAGE.md +4 -3
- package/README.md +2 -2
- package/dist/components/Avatar.d.ts +19 -5
- package/dist/components/Avatar.js +84 -22
- package/dist/components/BottomSheet.js +1 -1
- package/dist/components/Carousel.d.ts +32 -6
- package/dist/components/Carousel.js +79 -17
- package/dist/components/DismissKeyboard.js +1 -1
- package/dist/components/KeyboardAvoidingView.d.ts +4 -2
- package/dist/components/KeyboardAvoidingView.js +5 -3
- package/dist/components/Label.d.ts +30 -10
- package/dist/components/Label.js +19 -10
- package/dist/components/Notification.js +7 -7
- package/dist/components/keyboardController.d.ts +46 -0
- package/dist/components/keyboardController.js +89 -0
- package/dist/components/keyboardController.native.d.ts +9 -0
- package/dist/components/keyboardController.native.js +9 -0
- package/dist/constants/colors.d.ts +34 -0
- package/dist/constants/colors.js +93 -3
- package/dist/constants/fonts.js +5 -4
- package/dist/hooks/useDimensions.d.ts +21 -9
- package/dist/hooks/useDimensions.js +29 -18
- package/dist/hooks/useResources.d.ts +10 -7
- package/dist/hooks/useResources.js +21 -24
- package/dist/hooks/useTheme.d.ts +18 -0
- package/dist/hooks/useTheme.js +45 -29
- package/dist/lib/themedStyles.d.ts +7 -7
- package/dist/lib/themedStyles.js +7 -7
- package/dist/state/SsrViewportContext.d.ts +9 -38
- package/dist/state/SsrViewportContext.js +9 -38
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -2
- package/dist/state/themeStore.d.ts +9 -13
- package/dist/state/themeStore.js +19 -40
- package/llms-full.md +1 -1
- package/package.json +2 -2
- package/dist/state/ssrTheme.d.ts +0 -41
- package/dist/state/ssrTheme.js +0 -33
|
@@ -1,42 +1,13 @@
|
|
|
1
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
2
|
/**
|
|
18
3
|
* Per-request SSR viewport width. Consumed by `useDimensions` to seed the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
* ```
|
|
4
|
+
* server render and the browser's FIRST render, so a server that detected a
|
|
5
|
+
* phone lays the tree out at phone width and hydration matches byte-for-byte.
|
|
6
|
+
*
|
|
7
|
+
* `null` (the default, when no provider is mounted) means "no per-request
|
|
8
|
+
* signal" and `useDimensions` falls back to `DEFAULT_VIEWPORT_WIDTH`. The
|
|
9
|
+
* host app is expected to provide the same value on the server and on the
|
|
10
|
+
* client's first render — both derived from signals both sides can read
|
|
11
|
+
* (cookie / User-Agent), never from `window`.
|
|
41
12
|
*/
|
|
42
|
-
export const SsrViewportContext = createContext(
|
|
13
|
+
export const SsrViewportContext = createContext(null);
|
package/dist/state/index.d.ts
CHANGED
package/dist/state/index.js
CHANGED
|
@@ -3,15 +3,12 @@ import type { FontOverrides } from "../constants/fonts";
|
|
|
3
3
|
export type ThemePreference = "system" | "light" | "dark";
|
|
4
4
|
export type ResolvedTheme = "light" | "dark";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
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.
|
|
13
10
|
*/
|
|
14
|
-
export declare const
|
|
11
|
+
export declare const THEME_STORAGE_KEY = "user-theme-preference";
|
|
15
12
|
/**
|
|
16
13
|
* Per-scheme color overrides a host app can inject to brand the package.
|
|
17
14
|
*
|
|
@@ -56,11 +53,10 @@ export type ThemeStore = {
|
|
|
56
53
|
systemTheme: ResolvedTheme;
|
|
57
54
|
/**
|
|
58
55
|
* True once the persisted preference has actually been read (or explicitly
|
|
59
|
-
* set by the user). Until then
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* onboarding store.
|
|
56
|
+
* set by the user). Until then `userTheme` / `systemTheme` still hold the
|
|
57
|
+
* boot defaults ("system" / light), so a host app that needs to hold a
|
|
58
|
+
* paint until the real preference is known can wait on this. Mirrors
|
|
59
|
+
* `hasLoadedOnboarding` in the host app's onboarding store.
|
|
64
60
|
*/
|
|
65
61
|
hasLoadedTheme: boolean;
|
|
66
62
|
/**
|
package/dist/state/themeStore.js
CHANGED
|
@@ -3,25 +3,15 @@ import { create } from "zustand";
|
|
|
3
3
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
4
|
const THEME_KEY = "user-theme-preference";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
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.
|
|
13
10
|
*/
|
|
14
|
-
export const
|
|
15
|
-
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
|
11
|
+
export const THEME_STORAGE_KEY = THEME_KEY;
|
|
16
12
|
function isThemePreference(value) {
|
|
17
13
|
return value === "system" || value === "light" || value === "dark";
|
|
18
14
|
}
|
|
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
|
-
}
|
|
25
15
|
export function resolveThemePreference(userTheme, systemTheme) {
|
|
26
16
|
return userTheme === "system" ? systemTheme : userTheme;
|
|
27
17
|
}
|
|
@@ -33,13 +23,12 @@ function getSystemTheme() {
|
|
|
33
23
|
}
|
|
34
24
|
export const useThemeStore = create((set) => ({
|
|
35
25
|
userTheme: "system",
|
|
36
|
-
// Always start with "light" so
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
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.
|
|
40
30
|
systemTheme: "light",
|
|
41
|
-
// False until persistence has been read.
|
|
42
|
-
// whether to trust store state or the per-request SSR seed.
|
|
31
|
+
// False until persistence has been read.
|
|
43
32
|
hasLoadedTheme: false,
|
|
44
33
|
// No overrides by default: the package renders with its built-in palette
|
|
45
34
|
// until a host app calls `setColors`.
|
|
@@ -74,9 +63,6 @@ export const useThemeStore = create((set) => ({
|
|
|
74
63
|
if (typeof window !== "undefined" && window.localStorage) {
|
|
75
64
|
localStorage.setItem(THEME_KEY, theme);
|
|
76
65
|
}
|
|
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);
|
|
80
66
|
},
|
|
81
67
|
setSystemTheme: (theme) => {
|
|
82
68
|
set({ systemTheme: theme });
|
|
@@ -98,15 +84,6 @@ export const useThemeStore = create((set) => ({
|
|
|
98
84
|
const saved = localStorage.getItem(THEME_KEY);
|
|
99
85
|
if (saved && isThemePreference(saved)) {
|
|
100
86
|
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");
|
|
110
87
|
}
|
|
111
88
|
}
|
|
112
89
|
set({ hasLoadedTheme: true });
|
|
@@ -156,18 +133,20 @@ export function startSystemThemeListener() {
|
|
|
156
133
|
// call multiple times — `startSystemThemeListener` is idempotent — and
|
|
157
134
|
// returns the unsubscribe so it can be used directly inside `useEffect`.
|
|
158
135
|
//
|
|
159
|
-
// The listener starts BEFORE `loadTheme()` on purpose:
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// means a `system` user never sees a frame of the boot-default "light".
|
|
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.
|
|
163
139
|
export function syncThemeFromEnvironment() {
|
|
164
140
|
const stop = startSystemThemeListener();
|
|
165
141
|
useThemeStore.getState().loadTheme();
|
|
166
142
|
return stop;
|
|
167
143
|
}
|
|
168
|
-
// Native
|
|
169
|
-
// behavior there. On web the host app must call
|
|
170
|
-
// 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.
|
|
171
150
|
if (Platform.OS !== "web") {
|
|
172
151
|
useThemeStore.getState().loadTheme();
|
|
173
152
|
startSystemThemeListener();
|
package/llms-full.md
CHANGED
|
@@ -109,7 +109,7 @@ Use this catalog before creating a new app-local primitive.
|
|
|
109
109
|
| `Icon` | `@mrmeg/expo-ui/components` | Feather or custom icons with theme tokens | Avoid raw vector icons with hardcoded colors. |
|
|
110
110
|
| `InputOTP` | `@mrmeg/expo-ui/components` | Verification code entry | Prefer over manually managed text input groups. |
|
|
111
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. |
|
|
112
|
-
| `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. |
|
|
113
113
|
| `MaxWidthContainer` | `@mrmeg/expo-ui/components` | Centered responsive width | Use for web and tablet constrained layouts. |
|
|
114
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. |
|
|
115
115
|
| `Popover` | `@mrmeg/expo-ui/components` | Anchored contextual content | Requires root `UIProvider` portal setup. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrmeg/expo-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
|
|
6
6
|
"keywords": [
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"zustand": ">=5.0.0 <6.0.0"
|
|
126
126
|
},
|
|
127
127
|
"devDependencies": {
|
|
128
|
-
"@types/react": "~19.2.
|
|
128
|
+
"@types/react": "~19.2.18",
|
|
129
129
|
"typescript": "~6.0.3"
|
|
130
130
|
}
|
|
131
131
|
}
|
package/dist/state/ssrTheme.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { ResolvedTheme, ThemePreference } from "./themeStore";
|
|
2
|
-
/**
|
|
3
|
-
* The theme values the FIRST render must use on web — server and client alike.
|
|
4
|
-
*
|
|
5
|
-
* `useThemeStore` is a module singleton shared by every concurrent SSR request,
|
|
6
|
-
* so a per-request theme can never be written into it. This context is the
|
|
7
|
-
* per-request channel instead: the host app resolves it from the request (a
|
|
8
|
-
* cookie the client mirrors) and provides it above the tree, and `useTheme`
|
|
9
|
-
* reads it until the store has loaded real persistence after mount.
|
|
10
|
-
*/
|
|
11
|
-
export type SsrThemeSeed = {
|
|
12
|
-
userTheme: ThemePreference;
|
|
13
|
-
systemTheme: ResolvedTheme;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* What a first-time visitor gets: no persisted preference, light scheme. Same
|
|
17
|
-
* values the store boots with, so a host app that never provides the context
|
|
18
|
-
* behaves exactly as it did before the seed existed.
|
|
19
|
-
*/
|
|
20
|
-
export declare const SSR_THEME_SEED_DEFAULT: SsrThemeSeed;
|
|
21
|
-
/**
|
|
22
|
-
* Per-request theme seed consumed by `useTheme`/`useStyles` while
|
|
23
|
-
* `hasLoadedTheme` is false (i.e. on the server render and the client's first
|
|
24
|
-
* render, before `syncThemeFromEnvironment()` reads real persistence).
|
|
25
|
-
*
|
|
26
|
-
* Provide it near the top of the tree from a value BOTH the server and the
|
|
27
|
-
* browser can derive from the same bytes — a cookie. Reading `localStorage`
|
|
28
|
-
* for this would reintroduce the hydration mismatch it exists to remove,
|
|
29
|
-
* because the server cannot see `localStorage`.
|
|
30
|
-
*
|
|
31
|
-
* ```tsx
|
|
32
|
-
* import { SsrThemeSeedContext } from "@mrmeg/expo-ui/state";
|
|
33
|
-
*
|
|
34
|
-
* <SsrThemeSeedContext.Provider value={seedFromCookie}>
|
|
35
|
-
* <App />
|
|
36
|
-
* </SsrThemeSeedContext.Provider>
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export declare const SsrThemeSeedContext: import("react").Context<SsrThemeSeed>;
|
|
40
|
-
/** Read the active SSR theme seed (the default when no provider is mounted). */
|
|
41
|
-
export declare function useSsrThemeSeed(): SsrThemeSeed;
|
package/dist/state/ssrTheme.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { createContext, use } from "react";
|
|
2
|
-
/**
|
|
3
|
-
* What a first-time visitor gets: no persisted preference, light scheme. Same
|
|
4
|
-
* values the store boots with, so a host app that never provides the context
|
|
5
|
-
* behaves exactly as it did before the seed existed.
|
|
6
|
-
*/
|
|
7
|
-
export const SSR_THEME_SEED_DEFAULT = {
|
|
8
|
-
userTheme: "system",
|
|
9
|
-
systemTheme: "light",
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Per-request theme seed consumed by `useTheme`/`useStyles` while
|
|
13
|
-
* `hasLoadedTheme` is false (i.e. on the server render and the client's first
|
|
14
|
-
* render, before `syncThemeFromEnvironment()` reads real persistence).
|
|
15
|
-
*
|
|
16
|
-
* Provide it near the top of the tree from a value BOTH the server and the
|
|
17
|
-
* browser can derive from the same bytes — a cookie. Reading `localStorage`
|
|
18
|
-
* for this would reintroduce the hydration mismatch it exists to remove,
|
|
19
|
-
* because the server cannot see `localStorage`.
|
|
20
|
-
*
|
|
21
|
-
* ```tsx
|
|
22
|
-
* import { SsrThemeSeedContext } from "@mrmeg/expo-ui/state";
|
|
23
|
-
*
|
|
24
|
-
* <SsrThemeSeedContext.Provider value={seedFromCookie}>
|
|
25
|
-
* <App />
|
|
26
|
-
* </SsrThemeSeedContext.Provider>
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export const SsrThemeSeedContext = createContext(SSR_THEME_SEED_DEFAULT);
|
|
30
|
-
/** Read the active SSR theme seed (the default when no provider is mounted). */
|
|
31
|
-
export function useSsrThemeSeed() {
|
|
32
|
-
return use(SsrThemeSeedContext);
|
|
33
|
-
}
|