@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LLM_USAGE.md +4 -3
  3. package/README.md +2 -2
  4. package/dist/components/Avatar.d.ts +19 -5
  5. package/dist/components/Avatar.js +84 -22
  6. package/dist/components/BottomSheet.js +1 -1
  7. package/dist/components/Carousel.d.ts +32 -6
  8. package/dist/components/Carousel.js +79 -17
  9. package/dist/components/DismissKeyboard.js +1 -1
  10. package/dist/components/KeyboardAvoidingView.d.ts +4 -2
  11. package/dist/components/KeyboardAvoidingView.js +5 -3
  12. package/dist/components/Label.d.ts +30 -10
  13. package/dist/components/Label.js +19 -10
  14. package/dist/components/Notification.js +7 -7
  15. package/dist/components/keyboardController.d.ts +46 -0
  16. package/dist/components/keyboardController.js +89 -0
  17. package/dist/components/keyboardController.native.d.ts +9 -0
  18. package/dist/components/keyboardController.native.js +9 -0
  19. package/dist/constants/colors.d.ts +34 -0
  20. package/dist/constants/colors.js +93 -3
  21. package/dist/constants/fonts.js +5 -4
  22. package/dist/hooks/useDimensions.d.ts +21 -9
  23. package/dist/hooks/useDimensions.js +29 -18
  24. package/dist/hooks/useResources.d.ts +10 -7
  25. package/dist/hooks/useResources.js +21 -24
  26. package/dist/hooks/useTheme.d.ts +18 -0
  27. package/dist/hooks/useTheme.js +45 -29
  28. package/dist/lib/themedStyles.d.ts +7 -7
  29. package/dist/lib/themedStyles.js +7 -7
  30. package/dist/state/SsrViewportContext.d.ts +9 -38
  31. package/dist/state/SsrViewportContext.js +9 -38
  32. package/dist/state/index.d.ts +1 -2
  33. package/dist/state/index.js +1 -2
  34. package/dist/state/themeStore.d.ts +9 -13
  35. package/dist/state/themeStore.js +19 -40
  36. package/llms-full.md +1 -1
  37. package/package.json +2 -2
  38. package/dist/state/ssrTheme.d.ts +0 -41
  39. 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
- * 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
- * ```
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(SSR_VIEWPORT_DEFAULT_WIDTH);
13
+ export const SsrViewportContext = createContext(null);
@@ -1,6 +1,5 @@
1
1
  export * from "./globalUIStore";
2
2
  export * from "./notify";
3
+ export * from "./SsrViewportContext";
3
4
  export * from "./themeStore";
4
5
  export * from "./themeColorScope";
5
- export * from "./ssrTheme";
6
- export * from "./SsrViewportContext";
@@ -1,6 +1,5 @@
1
1
  export * from "./globalUIStore.js";
2
2
  export * from "./notify.js";
3
+ export * from "./SsrViewportContext.js";
3
4
  export * from "./themeStore.js";
4
5
  export * from "./themeColorScope.js";
5
- export * from "./ssrTheme.js";
6
- export * from "./SsrViewportContext.js";
@@ -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
- * 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.
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 THEME_COOKIE_NAME = "user-theme-preference";
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, a web render must not trust `userTheme` /
60
- * `systemTheme` — they still hold the SSR-safe boot defaults, and `useTheme`
61
- * uses `SsrThemeSeedContext` (cookie-derived, identical on server and
62
- * client) instead. Mirrors `hasLoadedOnboarding` in the host app's
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
  /**
@@ -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
- * 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.
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 THEME_COOKIE_NAME = THEME_KEY;
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 SSR and the first client render agree.
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.
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. `useTheme` uses this to decide
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: `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".
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 has no SSR mismatch concern, so keep the historical auto-init
169
- // behavior there. On web the host app must call `syncThemeFromEnvironment()`
170
- // from a top-level `useEffect` to avoid hydration mismatches.
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 | Use with package form controls. |
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.20.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.17",
128
+ "@types/react": "~19.2.18",
129
129
  "typescript": "~6.0.3"
130
130
  }
131
131
  }
@@ -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;
@@ -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
- }