@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
|
@@ -5,9 +5,20 @@ export interface LabelProps {
|
|
|
5
5
|
*/
|
|
6
6
|
children: string;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* The label's OWN id. On web react-native-web maps it to `id`; on native it
|
|
9
|
+
* is the `nativeID` a control's `accessibilityLabelledBy` can point at.
|
|
10
|
+
*
|
|
11
|
+
* Must differ from the paired input's id — see `htmlFor`.
|
|
9
12
|
*/
|
|
10
13
|
nativeID?: string;
|
|
14
|
+
/**
|
|
15
|
+
* WEB ONLY — the id of the input this label describes (the input's
|
|
16
|
+
* `nativeID`). Passing it makes `@rn-primitives/label` render a real
|
|
17
|
+
* `<label for="…">`, which is what associates the two and makes clicking the
|
|
18
|
+
* label focus the input. No-op on native, where association goes the other
|
|
19
|
+
* way (the control points at the label's `nativeID`).
|
|
20
|
+
*/
|
|
21
|
+
htmlFor?: string;
|
|
11
22
|
/**
|
|
12
23
|
* Whether the field is required (shows asterisk)
|
|
13
24
|
*/
|
|
@@ -37,25 +48,34 @@ export interface LabelProps {
|
|
|
37
48
|
/**
|
|
38
49
|
* Label component for form fields using @rn-primitives/label
|
|
39
50
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
51
|
+
* Accessible labeling for form controls. Association needs **two distinct
|
|
52
|
+
* ids**: `nativeID` is the label's own id, `htmlFor` is the input's id. Giving
|
|
53
|
+
* both the same value renders two elements with one id on web and associates
|
|
54
|
+
* nothing.
|
|
42
55
|
*
|
|
43
56
|
* Usage:
|
|
44
57
|
* ```tsx
|
|
45
|
-
* //
|
|
46
|
-
*
|
|
58
|
+
* // Paired with an input. `htmlFor` (web) makes this a real <label for>, so
|
|
59
|
+
* // clicking it focuses the input; `nativeID` names the label itself, which is
|
|
60
|
+
* // what a control's `accessibilityLabelledBy` points at on native.
|
|
61
|
+
* <Label nativeID="email-label" htmlFor="email-input">Email</Label>
|
|
47
62
|
* <TextInput nativeID="email-input" />
|
|
48
63
|
*
|
|
49
64
|
* // Required field
|
|
50
|
-
* <Label nativeID="password" required>Password</Label>
|
|
65
|
+
* <Label nativeID="password-label" htmlFor="password-input" required>Password</Label>
|
|
66
|
+
* <TextInput nativeID="password-input" secureTextEntry />
|
|
51
67
|
*
|
|
52
68
|
* // With error state
|
|
53
|
-
* <Label nativeID="username" error>Username</Label>
|
|
69
|
+
* <Label nativeID="username-label" htmlFor="username-input" error>Username</Label>
|
|
70
|
+
*
|
|
71
|
+
* // Standalone caption with no control to associate — omit htmlFor
|
|
72
|
+
* <Label>Filters</Label>
|
|
54
73
|
*
|
|
55
|
-
* // With press handler to focus input
|
|
56
|
-
*
|
|
74
|
+
* // With press handler to focus input (needed on native, where htmlFor is a
|
|
75
|
+
* // no-op and pressing the label does nothing on its own)
|
|
76
|
+
* <Label nativeID="search-label" htmlFor="search-input" onPress={() => inputRef.current?.focus()}>
|
|
57
77
|
* Search
|
|
58
78
|
* </Label>
|
|
59
79
|
* ```
|
|
60
80
|
*/
|
|
61
|
-
export declare function Label({ children, nativeID, required, size, error, disabled, style, onPress, }: LabelProps): import("react").JSX.Element;
|
|
81
|
+
export declare function Label({ children, nativeID, htmlFor, required, size, error, disabled, style, onPress, }: LabelProps): import("react").JSX.Element;
|
package/dist/components/Label.js
CHANGED
|
@@ -14,28 +14,37 @@ const SIZE_CONFIGS = {
|
|
|
14
14
|
/**
|
|
15
15
|
* Label component for form fields using @rn-primitives/label
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Accessible labeling for form controls. Association needs **two distinct
|
|
18
|
+
* ids**: `nativeID` is the label's own id, `htmlFor` is the input's id. Giving
|
|
19
|
+
* both the same value renders two elements with one id on web and associates
|
|
20
|
+
* nothing.
|
|
19
21
|
*
|
|
20
22
|
* Usage:
|
|
21
23
|
* ```tsx
|
|
22
|
-
* //
|
|
23
|
-
*
|
|
24
|
+
* // Paired with an input. `htmlFor` (web) makes this a real <label for>, so
|
|
25
|
+
* // clicking it focuses the input; `nativeID` names the label itself, which is
|
|
26
|
+
* // what a control's `accessibilityLabelledBy` points at on native.
|
|
27
|
+
* <Label nativeID="email-label" htmlFor="email-input">Email</Label>
|
|
24
28
|
* <TextInput nativeID="email-input" />
|
|
25
29
|
*
|
|
26
30
|
* // Required field
|
|
27
|
-
* <Label nativeID="password" required>Password</Label>
|
|
31
|
+
* <Label nativeID="password-label" htmlFor="password-input" required>Password</Label>
|
|
32
|
+
* <TextInput nativeID="password-input" secureTextEntry />
|
|
28
33
|
*
|
|
29
34
|
* // With error state
|
|
30
|
-
* <Label nativeID="username" error>Username</Label>
|
|
35
|
+
* <Label nativeID="username-label" htmlFor="username-input" error>Username</Label>
|
|
31
36
|
*
|
|
32
|
-
* //
|
|
33
|
-
* <Label
|
|
37
|
+
* // Standalone caption with no control to associate — omit htmlFor
|
|
38
|
+
* <Label>Filters</Label>
|
|
39
|
+
*
|
|
40
|
+
* // With press handler to focus input (needed on native, where htmlFor is a
|
|
41
|
+
* // no-op and pressing the label does nothing on its own)
|
|
42
|
+
* <Label nativeID="search-label" htmlFor="search-input" onPress={() => inputRef.current?.focus()}>
|
|
34
43
|
* Search
|
|
35
44
|
* </Label>
|
|
36
45
|
* ```
|
|
37
46
|
*/
|
|
38
|
-
export function Label({ children, nativeID, required, size = "md", error, disabled, style, onPress, }) {
|
|
47
|
+
export function Label({ children, nativeID, htmlFor, required, size = "md", error, disabled, style, onPress, }) {
|
|
39
48
|
const { theme } = useTheme();
|
|
40
49
|
const styles = themedStyles(theme);
|
|
41
50
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
@@ -53,7 +62,7 @@ export function Label({ children, nativeID, required, size = "md", error, disabl
|
|
|
53
62
|
return (_jsx(LabelPrimitive.Root, { nativeID: nativeID, onPress: onPress, style: {
|
|
54
63
|
...styles.root,
|
|
55
64
|
...(Platform.OS === "web" && onPress && { cursor: "pointer" }),
|
|
56
|
-
}, children: _jsxs(LabelPrimitive.Text, { style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) }));
|
|
65
|
+
}, children: _jsxs(LabelPrimitive.Text, { htmlFor: htmlFor, style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) }));
|
|
57
66
|
}
|
|
58
67
|
const createStyles = (theme) => StyleSheet.create({
|
|
59
68
|
root: {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from "./Accordion";
|
|
2
2
|
export * from "./Alert";
|
|
3
3
|
export * from "./AnimatedView";
|
|
4
|
+
export * from "./Avatar";
|
|
4
5
|
export * from "./Badge";
|
|
5
6
|
export * from "./BottomSheet";
|
|
6
7
|
export * from "./Button";
|
|
7
8
|
export * from "./Card";
|
|
9
|
+
export * from "./Carousel";
|
|
8
10
|
export * from "./Checkbox";
|
|
9
11
|
export * from "./Collapsible";
|
|
10
12
|
export * from "./Dialog";
|
package/dist/components/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from "./Accordion.js";
|
|
2
2
|
export * from "./Alert.js";
|
|
3
3
|
export * from "./AnimatedView.js";
|
|
4
|
+
export * from "./Avatar.js";
|
|
4
5
|
export * from "./Badge.js";
|
|
5
6
|
export * from "./BottomSheet.js";
|
|
6
7
|
export * from "./Button.js";
|
|
7
8
|
export * from "./Card.js";
|
|
9
|
+
export * from "./Carousel.js";
|
|
8
10
|
export * from "./Checkbox.js";
|
|
9
11
|
export * from "./Collapsible.js";
|
|
10
12
|
export * from "./Dialog.js";
|
|
@@ -22,11 +22,13 @@ declare const palette: {
|
|
|
22
22
|
readonly gray900: "#18181B";
|
|
23
23
|
readonly gray950: "#09090B";
|
|
24
24
|
readonly black: "#000000";
|
|
25
|
+
readonly dark950: "#050506";
|
|
25
26
|
readonly dark900: "#09090B";
|
|
26
27
|
readonly dark800: "#18181B";
|
|
27
28
|
readonly dark700: "#27272A";
|
|
28
29
|
readonly dark600: "#3F3F46";
|
|
29
30
|
readonly dark400: "#A1A1AA";
|
|
31
|
+
readonly dark350: "#B0B0B8";
|
|
30
32
|
readonly dark300: "#D4D4D8";
|
|
31
33
|
readonly dark100: "#F4F4F5";
|
|
32
34
|
readonly green500: "#22C55E";
|
|
@@ -40,6 +42,7 @@ declare const palette: {
|
|
|
40
42
|
* Semantic color interface - what colors mean in the UI
|
|
41
43
|
*/
|
|
42
44
|
export interface ThemeColors {
|
|
45
|
+
surfaceSunken: string;
|
|
43
46
|
background: string;
|
|
44
47
|
foreground: string;
|
|
45
48
|
card: string;
|
|
@@ -61,6 +64,7 @@ export interface ThemeColors {
|
|
|
61
64
|
success: string;
|
|
62
65
|
warning: string;
|
|
63
66
|
border: string;
|
|
67
|
+
borderStrong: string;
|
|
64
68
|
input: string;
|
|
65
69
|
ring: string;
|
|
66
70
|
overlay: string;
|
package/dist/constants/colors.js
CHANGED
|
@@ -25,11 +25,15 @@ const palette = {
|
|
|
25
25
|
gray950: "#09090B",
|
|
26
26
|
black: "#000000",
|
|
27
27
|
// Dark mode surfaces — zinc-based
|
|
28
|
+
// `dark950` sits below zinc-950 as the app-chrome tier; `dark350` is a
|
|
29
|
+
// zinc-350 midpoint used for dim text that stays comfortably readable.
|
|
30
|
+
dark950: "#050506",
|
|
28
31
|
dark900: "#09090B",
|
|
29
32
|
dark800: "#18181B",
|
|
30
33
|
dark700: "#27272A",
|
|
31
34
|
dark600: "#3F3F46",
|
|
32
35
|
dark400: "#A1A1AA",
|
|
36
|
+
dark350: "#B0B0B8",
|
|
33
37
|
dark300: "#D4D4D8",
|
|
34
38
|
dark100: "#F4F4F5",
|
|
35
39
|
// Status
|
|
@@ -44,6 +48,7 @@ const lightTheme = {
|
|
|
44
48
|
dark: false,
|
|
45
49
|
fonts: navigationFonts,
|
|
46
50
|
colors: {
|
|
51
|
+
surfaceSunken: palette.gray50,
|
|
47
52
|
background: palette.white,
|
|
48
53
|
foreground: palette.gray950,
|
|
49
54
|
card: palette.white,
|
|
@@ -51,13 +56,13 @@ const lightTheme = {
|
|
|
51
56
|
popover: palette.white,
|
|
52
57
|
popoverForeground: palette.gray950,
|
|
53
58
|
text: palette.gray950,
|
|
54
|
-
textDim: palette.
|
|
59
|
+
textDim: palette.gray600,
|
|
55
60
|
primary: palette.gray900,
|
|
56
61
|
primaryForeground: palette.gray50,
|
|
57
62
|
secondary: palette.gray100,
|
|
58
63
|
secondaryForeground: palette.gray900,
|
|
59
64
|
muted: palette.gray100,
|
|
60
|
-
mutedForeground: palette.
|
|
65
|
+
mutedForeground: palette.gray600,
|
|
61
66
|
accent: palette.teal500,
|
|
62
67
|
accentForeground: palette.white,
|
|
63
68
|
destructive: palette.red500,
|
|
@@ -65,6 +70,7 @@ const lightTheme = {
|
|
|
65
70
|
success: palette.green500,
|
|
66
71
|
warning: palette.amber500,
|
|
67
72
|
border: palette.gray200,
|
|
73
|
+
borderStrong: palette.gray300,
|
|
68
74
|
input: palette.gray200,
|
|
69
75
|
ring: palette.gray400,
|
|
70
76
|
overlay: "rgba(0, 0, 0, 0.5)",
|
|
@@ -82,6 +88,7 @@ const darkTheme = {
|
|
|
82
88
|
dark: true,
|
|
83
89
|
fonts: navigationFonts,
|
|
84
90
|
colors: {
|
|
91
|
+
surfaceSunken: palette.dark950,
|
|
85
92
|
background: palette.dark900,
|
|
86
93
|
foreground: palette.dark100,
|
|
87
94
|
card: palette.dark800,
|
|
@@ -89,13 +96,13 @@ const darkTheme = {
|
|
|
89
96
|
popover: palette.dark800,
|
|
90
97
|
popoverForeground: palette.dark100,
|
|
91
98
|
text: palette.dark100,
|
|
92
|
-
textDim: palette.
|
|
99
|
+
textDim: palette.dark350,
|
|
93
100
|
primary: palette.gray50,
|
|
94
101
|
primaryForeground: palette.gray900,
|
|
95
102
|
secondary: palette.gray800,
|
|
96
103
|
secondaryForeground: palette.gray50,
|
|
97
104
|
muted: palette.dark700,
|
|
98
|
-
mutedForeground: palette.
|
|
105
|
+
mutedForeground: palette.dark350,
|
|
99
106
|
accent: palette.teal400,
|
|
100
107
|
accentForeground: palette.gray900,
|
|
101
108
|
destructive: palette.red400,
|
|
@@ -103,6 +110,7 @@ const darkTheme = {
|
|
|
103
110
|
success: palette.green400,
|
|
104
111
|
warning: palette.amber400,
|
|
105
112
|
border: palette.dark700,
|
|
113
|
+
borderStrong: palette.dark600,
|
|
106
114
|
input: palette.dark700,
|
|
107
115
|
ring: palette.dark400,
|
|
108
116
|
overlay: "rgba(0, 0, 0, 0.7)",
|
package/dist/constants/fonts.js
CHANGED
|
@@ -2,10 +2,11 @@ import { Platform } from "react-native";
|
|
|
2
2
|
// Web font stack fallback
|
|
3
3
|
const WEB_FONT_STACK = "system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"";
|
|
4
4
|
// IMPORTANT: do NOT key these on `typeof document` / `typeof navigator`.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// A web bundle is also evaluated in Node when a host app's HTML shell is
|
|
6
|
+
// rendered at export time, where both are `undefined` while the browser sees
|
|
7
|
+
// them — that produces different module-load snapshots and a hydration
|
|
8
|
+
// mismatch on every <StyledText>. `Platform.OS` (from react-native-web)
|
|
9
|
+
// returns "web" in both environments, so the value is stable.
|
|
9
10
|
const isWebRuntime = Platform.OS === "web";
|
|
10
11
|
const isReactNativeRuntime = Platform.OS !== "web";
|
|
11
12
|
// Georgia ships as one face per platform; every weight slot points at the
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport the first web render is computed from, before the real window can
|
|
3
|
+
* be read. Desktop is chosen because:
|
|
4
|
+
* 1. A desktop visitor sees no reflow — their real viewport lands in the
|
|
5
|
+
* same breakpoint.
|
|
6
|
+
* 2. A mobile visitor gets one frame of desktop-styled content before the
|
|
7
|
+
* post-mount effect snaps to real dimensions — better than the inverse
|
|
8
|
+
* (every desktop visitor seeing mobile-tiny, then snapping).
|
|
9
|
+
* 3. It matches the frame the app hands `SafeAreaProvider`, and it is the
|
|
10
|
+
* value the HTML shell `expo export` renders in Node is built from, so
|
|
11
|
+
* the browser's first render agrees with the markup it hydrates.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_VIEWPORT_WIDTH = 1280;
|
|
14
|
+
export declare const DEFAULT_VIEWPORT_HEIGHT = 800;
|
|
1
15
|
export declare const SCREEN_SIZES: {
|
|
2
16
|
readonly SMALL: 768;
|
|
3
17
|
readonly MEDIUM: 1000;
|
|
@@ -15,15 +29,11 @@ type WindowDimensions = {
|
|
|
15
29
|
* Provides a consistent way to access window dimensions and screen size
|
|
16
30
|
* information across mobile and web.
|
|
17
31
|
*
|
|
18
|
-
* On web
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Routes that don't provide the context get the package default
|
|
25
|
-
* (`SSR_VIEWPORT_DEFAULT_WIDTH` = 1280) — desktop-correct, mobile gets one
|
|
26
|
-
* frame of desktop layout before snapping.
|
|
32
|
+
* On web the first render uses `DEFAULT_VIEWPORT_WIDTH` / `_HEIGHT` — the
|
|
33
|
+
* window can't be read while the HTML shell is rendered in Node at export
|
|
34
|
+
* time, and reading it during the browser's first render would disagree with
|
|
35
|
+
* that markup. A post-mount effect swaps in the real viewport and follows
|
|
36
|
+
* resize from there.
|
|
27
37
|
*/
|
|
28
38
|
export declare const useDimensions: () => WindowDimensions;
|
|
29
39
|
export {};
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
import { Platform, useWindowDimensions } from "react-native";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Viewport the first web render is computed from, before the real window can
|
|
5
|
+
* be read. Desktop is chosen because:
|
|
6
|
+
* 1. A desktop visitor sees no reflow — their real viewport lands in the
|
|
7
|
+
* same breakpoint.
|
|
8
|
+
* 2. A mobile visitor gets one frame of desktop-styled content before the
|
|
9
|
+
* post-mount effect snaps to real dimensions — better than the inverse
|
|
10
|
+
* (every desktop visitor seeing mobile-tiny, then snapping).
|
|
11
|
+
* 3. It matches the frame the app hands `SafeAreaProvider`, and it is the
|
|
12
|
+
* value the HTML shell `expo export` renders in Node is built from, so
|
|
13
|
+
* the browser's first render agrees with the markup it hydrates.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_VIEWPORT_WIDTH = 1280;
|
|
16
|
+
export const DEFAULT_VIEWPORT_HEIGHT = 800;
|
|
4
17
|
export const SCREEN_SIZES = {
|
|
5
18
|
SMALL: 768,
|
|
6
19
|
MEDIUM: 1000,
|
|
@@ -20,53 +33,32 @@ const calculateDimensionFlags = (width, height) => {
|
|
|
20
33
|
isLargeScreen: width > SCREEN_SIZES.MEDIUM,
|
|
21
34
|
};
|
|
22
35
|
};
|
|
23
|
-
// Persist the real viewport width as a cookie on first mount so subsequent
|
|
24
|
-
// SSR requests can render with the user's actual layout (no flash on repeat
|
|
25
|
-
// visits). The server reads this cookie via your route loader using
|
|
26
|
-
// `detectSsrViewportWidth(request)` from server/lib/ssrViewport.ts.
|
|
27
|
-
const SSR_VIEWPORT_COOKIE = "mrmeg-vw";
|
|
28
|
-
const SSR_VIEWPORT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
|
29
|
-
function writeViewportCookie(width) {
|
|
30
|
-
if (typeof document === "undefined")
|
|
31
|
-
return;
|
|
32
|
-
// Round to nearest 10 so resize-driven writes don't bust HTTP caching on
|
|
33
|
-
// every pixel of horizontal movement.
|
|
34
|
-
const rounded = Math.round(width / 10) * 10;
|
|
35
|
-
document.cookie = `${SSR_VIEWPORT_COOKIE}=${rounded}; path=/; max-age=${SSR_VIEWPORT_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
36
|
-
}
|
|
37
36
|
/**
|
|
38
37
|
* Provides a consistent way to access window dimensions and screen size
|
|
39
38
|
* information across mobile and web.
|
|
40
39
|
*
|
|
41
|
-
* On web
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* Routes that don't provide the context get the package default
|
|
48
|
-
* (`SSR_VIEWPORT_DEFAULT_WIDTH` = 1280) — desktop-correct, mobile gets one
|
|
49
|
-
* frame of desktop layout before snapping.
|
|
40
|
+
* On web the first render uses `DEFAULT_VIEWPORT_WIDTH` / `_HEIGHT` — the
|
|
41
|
+
* window can't be read while the HTML shell is rendered in Node at export
|
|
42
|
+
* time, and reading it during the browser's first render would disagree with
|
|
43
|
+
* that markup. A post-mount effect swaps in the real viewport and follows
|
|
44
|
+
* resize from there.
|
|
50
45
|
*/
|
|
51
46
|
export const useDimensions = () => {
|
|
52
47
|
const isWeb = Platform.OS === "web";
|
|
53
|
-
const ssrWidth = use(SsrViewportContext);
|
|
54
48
|
// Native reads come from useWindowDimensions, which subscribes to rotation /
|
|
55
49
|
// split-screen / resize and tears the listener down for us — no manual
|
|
56
50
|
// Dimensions.addEventListener to leak. On web we ignore it and drive layout
|
|
57
|
-
// from the
|
|
51
|
+
// from the default frame + the resize listener below.
|
|
58
52
|
const native = useWindowDimensions();
|
|
59
|
-
|
|
60
|
-
// flags from the context value, so hydration matches.
|
|
61
|
-
const [dimensions, setDimensions] = useState(() => calculateDimensionFlags(ssrWidth, SSR_VIEWPORT_DEFAULT_HEIGHT));
|
|
53
|
+
const [dimensions, setDimensions] = useState(() => calculateDimensionFlags(DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT));
|
|
62
54
|
// Web: read the real viewport after mount and follow resize events. Keeping
|
|
63
|
-
// this in an effect (not render)
|
|
55
|
+
// this in an effect (not render) keeps the first render identical to the
|
|
56
|
+
// exported markup it hydrates.
|
|
64
57
|
useEffect(() => {
|
|
65
58
|
if (!isWeb)
|
|
66
59
|
return;
|
|
67
60
|
const syncFromWindow = () => {
|
|
68
61
|
setDimensions(calculateDimensionFlags(window.innerWidth, window.innerHeight));
|
|
69
|
-
writeViewportCookie(window.innerWidth);
|
|
70
62
|
};
|
|
71
63
|
syncFromWindow();
|
|
72
64
|
window.addEventListener("resize", syncFromWindow);
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registers the Feather icon font in expo-font's per-request SSR store.
|
|
3
|
-
*
|
|
4
|
-
* Must be called from a **render body** on server-web (see `useResources`
|
|
5
|
-
* below): expo-font enters an `AsyncLocalStorage` store per request, so a
|
|
6
|
-
* module-scope registration lands in at most one request's store. Registering
|
|
7
|
-
* during render means every request emits the `@font-face` rule into
|
|
8
|
-
* `<style id="expo-generated-fonts">` and server-rendered `<Icon>`s agree with
|
|
9
|
-
* the client's first render.
|
|
10
|
-
*
|
|
11
|
-
* No-op on native and on client web (both already covered: native by the
|
|
12
|
-
* effect below, client web by the module-scope call above).
|
|
13
|
-
*/
|
|
14
|
-
export declare function ensureIconFontRegistered(): void;
|
|
15
1
|
interface LoadResourcesResult {
|
|
16
2
|
loaded: boolean;
|
|
17
3
|
error: Error | null;
|
|
@@ -4,45 +4,17 @@ import Feather from "@expo/vector-icons/Feather";
|
|
|
4
4
|
import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, } from "@expo-google-fonts/inter";
|
|
5
5
|
import { Platform } from "react-native";
|
|
6
6
|
import { useThemeStore } from "../state/themeStore.js";
|
|
7
|
-
// Eager, module-scope load that primes the Feather font
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (which then no-ops for the already-loaded font).
|
|
7
|
+
// Eager, module-scope load that primes the Feather font before first render:
|
|
8
|
+
// on web it synchronously injects the @font-face rule, so icons have their
|
|
9
|
+
// glyphs on the first paint; on native it just starts the fetch ahead of the
|
|
10
|
+
// effect below (which then no-ops for the already-loaded font).
|
|
12
11
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// ever populated the store of whichever request happened to warm the module
|
|
17
|
-
// cache. Requests 2+ then shipped HTML with no @font-face and empty icons while
|
|
18
|
-
// the client rendered glyphs (React #418). Server registration happens
|
|
19
|
-
// per-request in ensureIconFontRegistered() instead.
|
|
12
|
+
// Guarded on `window` because web bundles are also evaluated in Node when
|
|
13
|
+
// `expo export` renders the HTML shell, where there is no document to inject
|
|
14
|
+
// into.
|
|
20
15
|
if (typeof window !== "undefined") {
|
|
21
16
|
void Font.loadAsync(Feather.font);
|
|
22
17
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Registers the Feather icon font in expo-font's per-request SSR store.
|
|
25
|
-
*
|
|
26
|
-
* Must be called from a **render body** on server-web (see `useResources`
|
|
27
|
-
* below): expo-font enters an `AsyncLocalStorage` store per request, so a
|
|
28
|
-
* module-scope registration lands in at most one request's store. Registering
|
|
29
|
-
* during render means every request emits the `@font-face` rule into
|
|
30
|
-
* `<style id="expo-generated-fonts">` and server-rendered `<Icon>`s agree with
|
|
31
|
-
* the client's first render.
|
|
32
|
-
*
|
|
33
|
-
* No-op on native and on client web (both already covered: native by the
|
|
34
|
-
* effect below, client web by the module-scope call above).
|
|
35
|
-
*/
|
|
36
|
-
export function ensureIconFontRegistered() {
|
|
37
|
-
if (Platform.OS !== "web" || typeof window !== "undefined")
|
|
38
|
-
return;
|
|
39
|
-
if (Font.isLoaded("feather"))
|
|
40
|
-
return; // per-request store; cheap
|
|
41
|
-
// Synchronous on server-web (expo-font routes to registerStaticFont). No
|
|
42
|
-
// `void`/try-catch: a throw here means we're outside a request scope, which
|
|
43
|
-
// must be loud.
|
|
44
|
-
Font.loadAsync(Feather.font);
|
|
45
|
-
}
|
|
46
18
|
// The four static Inter weights StyledText's native family keys point at
|
|
47
19
|
// (see constants/fonts.ts). Native-only: web never renders these family names
|
|
48
20
|
// ("Inter_400Regular" etc.) — fontFamilies.sansSerif resolves every weight to
|
|
@@ -96,10 +68,6 @@ function ensureWebFontStylesheet() {
|
|
|
96
68
|
* un-download Inter. The Feather icon font always loads.
|
|
97
69
|
*/
|
|
98
70
|
export const useResources = () => {
|
|
99
|
-
// Render-body (not effect) so server-web registers the icon font in *this*
|
|
100
|
-
// request's expo-font store — effects never run on the server and a
|
|
101
|
-
// module-scope call only reaches one request. See ensureIconFontRegistered.
|
|
102
|
-
ensureIconFontRegistered();
|
|
103
71
|
const [loaded, setLoaded] = useState(false);
|
|
104
72
|
const [error, setError] = useState(null);
|
|
105
73
|
useEffect(() => {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { type Theme } from "../constants/colors";
|
|
2
2
|
/**
|
|
3
3
|
* Registers theme-dependent styles at module scope so react-native-web
|
|
4
|
-
* inserts their CSS rules into the
|
|
5
|
-
* <head> is serialized.
|
|
4
|
+
* inserts their CSS rules into the sheet before the prerendered document
|
|
5
|
+
* <head> is serialized at export time.
|
|
6
6
|
*
|
|
7
7
|
* Styles created lazily during render — the
|
|
8
8
|
* `useMemo(() => createStyles(theme), [theme])` idiom — are inserted into
|
|
9
9
|
* RNW's sheet *after* expo-router has already captured the
|
|
10
|
-
* `<style id="react-native-stylesheet">` snapshot for the head. The
|
|
11
|
-
* HTML then references class names that have no rules, which paints
|
|
12
|
-
* completely unstyled until
|
|
13
|
-
* on
|
|
14
|
-
*
|
|
10
|
+
* `<style id="react-native-stylesheet">` snapshot for the head. The exported
|
|
11
|
+
* HTML shell then references class names that have no rules, which paints
|
|
12
|
+
* completely unstyled until the client re-inserts them on hydration (worst
|
|
13
|
+
* on a cold load of the shell, and on every dev rebundle). See "Enable
|
|
14
|
+
* Server Output" in docs/server-guide.md.
|
|
15
15
|
*
|
|
16
16
|
* Both base themes are computed eagerly at module load. Themes carrying
|
|
17
17
|
* color overrides (global brand or scoped) are computed once per theme
|
package/dist/lib/themedStyles.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { colors } from "../constants/colors.js";
|
|
2
2
|
/**
|
|
3
3
|
* Registers theme-dependent styles at module scope so react-native-web
|
|
4
|
-
* inserts their CSS rules into the
|
|
5
|
-
* <head> is serialized.
|
|
4
|
+
* inserts their CSS rules into the sheet before the prerendered document
|
|
5
|
+
* <head> is serialized at export time.
|
|
6
6
|
*
|
|
7
7
|
* Styles created lazily during render — the
|
|
8
8
|
* `useMemo(() => createStyles(theme), [theme])` idiom — are inserted into
|
|
9
9
|
* RNW's sheet *after* expo-router has already captured the
|
|
10
|
-
* `<style id="react-native-stylesheet">` snapshot for the head. The
|
|
11
|
-
* HTML then references class names that have no rules, which paints
|
|
12
|
-
* completely unstyled until
|
|
13
|
-
* on
|
|
14
|
-
*
|
|
10
|
+
* `<style id="react-native-stylesheet">` snapshot for the head. The exported
|
|
11
|
+
* HTML shell then references class names that have no rules, which paints
|
|
12
|
+
* completely unstyled until the client re-inserts them on hydration (worst
|
|
13
|
+
* on a cold load of the shell, and on every dev rebundle). See "Enable
|
|
14
|
+
* Server Output" in docs/server-guide.md.
|
|
15
15
|
*
|
|
16
16
|
* Both base themes are computed eagerly at module load. Themes carrying
|
|
17
17
|
* color overrides (global brand or scoped) are computed once per theme
|
package/dist/state/index.d.ts
CHANGED
package/dist/state/index.js
CHANGED
|
@@ -2,6 +2,13 @@ import type { ThemeColors } from "../constants/colors";
|
|
|
2
2
|
import type { FontOverrides } from "../constants/fonts";
|
|
3
3
|
export type ThemePreference = "system" | "light" | "dark";
|
|
4
4
|
export type ResolvedTheme = "light" | "dark";
|
|
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 declare const THEME_STORAGE_KEY = "user-theme-preference";
|
|
5
12
|
/**
|
|
6
13
|
* Per-scheme color overrides a host app can inject to brand the package.
|
|
7
14
|
*
|
|
@@ -44,6 +51,14 @@ export type ShapeOverrides = {
|
|
|
44
51
|
export type ThemeStore = {
|
|
45
52
|
userTheme: ThemePreference;
|
|
46
53
|
systemTheme: ResolvedTheme;
|
|
54
|
+
/**
|
|
55
|
+
* True once the persisted preference has actually been read (or explicitly
|
|
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.
|
|
60
|
+
*/
|
|
61
|
+
hasLoadedTheme: boolean;
|
|
47
62
|
/**
|
|
48
63
|
* App-injected palette overrides, applied by `useTheme` on top of the
|
|
49
64
|
* package defaults. Empty by default — zero override means the package
|