@mrmeg/expo-ui 0.15.0 → 0.17.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 +118 -0
- package/LLM_USAGE.md +9 -0
- package/README.md +88 -1
- package/dist/components/Badge.js +4 -2
- package/dist/components/Button.js +31 -14
- package/dist/components/Card.js +5 -3
- package/dist/components/EmptyState.js +3 -2
- package/dist/components/InputOTP.js +7 -12
- package/dist/components/Label.js +14 -12
- package/dist/components/Notification.js +4 -2
- package/dist/components/Skeleton.js +3 -1
- package/dist/components/StyledText.d.ts +11 -2
- package/dist/components/StyledText.js +21 -30
- package/dist/components/Switch.js +2 -5
- package/dist/components/TextInput.js +22 -7
- package/dist/constants/fonts.d.ts +73 -12
- package/dist/constants/fonts.js +88 -2
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useFontStyle.d.ts +11 -0
- package/dist/hooks/useFontStyle.js +17 -0
- package/dist/hooks/useResources.d.ts +7 -0
- package/dist/hooks/useResources.js +13 -2
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/themedStyles.d.ts +21 -0
- package/dist/lib/themedStyles.js +33 -0
- package/dist/state/themeStore.d.ts +49 -0
- package/dist/state/themeStore.js +11 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { use } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { Text as RNText, StyleSheet } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveFontStyle } from "../constants/fonts.js";
|
|
6
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
6
7
|
import { translateText } from "../lib/i18n.js";
|
|
7
8
|
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
8
9
|
const FONT_SIZES = {
|
|
@@ -54,22 +55,6 @@ const SEMANTIC_CONFIGS = {
|
|
|
54
55
|
// and the textTransform applied in StyledText below.
|
|
55
56
|
eyebrow: { size: "sm", weight: "semibold" },
|
|
56
57
|
};
|
|
57
|
-
// Font family weight keys now map 1:1 onto FontWeight — every weight has a
|
|
58
|
-
// real family entry (native: a distinct static Inter file; web: shared
|
|
59
|
-
// "Inter" family + numeric fontWeight, see WEB_FONT_WEIGHTS). Defaults to
|
|
60
|
-
// "regular" when no weight is requested.
|
|
61
|
-
const getFontFamilyWeight = (weight) => weight ?? "regular";
|
|
62
|
-
// Web-only: the numeric fontWeight that picks the right @font-face variant
|
|
63
|
-
// out of the single shared "Inter" CSS family. "light" has no loaded 300
|
|
64
|
-
// weight (mirrors the native light->regular mapping in constants/fonts.ts),
|
|
65
|
-
// so it renders at 400 like "regular".
|
|
66
|
-
const WEB_FONT_WEIGHTS = {
|
|
67
|
-
light: "400",
|
|
68
|
-
regular: "400",
|
|
69
|
-
medium: "500",
|
|
70
|
-
semibold: "600",
|
|
71
|
-
bold: "700",
|
|
72
|
-
};
|
|
73
58
|
/**
|
|
74
59
|
* Base Text component with theme and variant support
|
|
75
60
|
* Uses theme colors automatically - text by default
|
|
@@ -89,6 +74,9 @@ const WEB_FONT_WEIGHTS = {
|
|
|
89
74
|
export function StyledText(props) {
|
|
90
75
|
const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, selectable, children, ref, ...otherProps } = props;
|
|
91
76
|
const { theme } = useTheme();
|
|
77
|
+
// Subscribed, not read via getState(), so a `setFonts` call after mount
|
|
78
|
+
// re-renders text instead of leaving stale families on screen.
|
|
79
|
+
const fontOverrides = useThemeStore((s) => s.fontOverrides);
|
|
92
80
|
// Check if there's a color override from parent context (e.g., Button)
|
|
93
81
|
const contextColor = use(TextColorContext);
|
|
94
82
|
const contextTextStyle = use(TextStyleContext);
|
|
@@ -100,18 +88,12 @@ export function StyledText(props) {
|
|
|
100
88
|
const semanticConfig = semantic ? SEMANTIC_CONFIGS[semantic] : undefined;
|
|
101
89
|
const finalSize = semanticConfig?.size ?? size ?? "body";
|
|
102
90
|
const finalFontWeight = semanticConfig?.weight ?? fontWeight ?? "regular";
|
|
103
|
-
// Get font family
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Web ships one Inter family for every weight, so a numeric fontWeight picks
|
|
110
|
-
// the right @font-face variant. Native ships a discrete static font file per
|
|
111
|
-
// weight (see constants/fonts.ts) — the family name alone carries the
|
|
112
|
-
// weight there, and adding a numeric fontWeight on top would faux-bold an
|
|
113
|
-
// already-bold file, so this stays web-only.
|
|
114
|
-
const resolvedFontWeight = Platform.OS === "web" ? WEB_FONT_WEIGHTS[finalFontWeight] : undefined;
|
|
91
|
+
// Get font family (+ numeric fontWeight under the web "numeric" strategy)
|
|
92
|
+
// based on variant and weight. Host apps replace the package's bundled
|
|
93
|
+
// faces via `setFonts`; resolution — including the per-group merge and the
|
|
94
|
+
// numeric-vs-family weight strategy — lives in `resolveFontStyle`
|
|
95
|
+
// (constants/fonts.ts), shared with the control components' `useFontStyle`.
|
|
96
|
+
const { fontFamily, fontWeight: resolvedFontWeight } = resolveFontStyle(fontOverrides, variant, finalFontWeight);
|
|
115
97
|
// Get fontSize and lineHeight from size variant
|
|
116
98
|
const fontSize = FONT_SIZES[finalSize];
|
|
117
99
|
const lineHeight = LINE_HEIGHTS[finalSize];
|
|
@@ -160,6 +142,15 @@ export function SerifText(props) {
|
|
|
160
142
|
export function SansSerifText(props) {
|
|
161
143
|
return _jsx(StyledText, { ...props, variant: "sansSerif" });
|
|
162
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Mono Text Component
|
|
147
|
+
* Uses the monospace font family (the platform's system monospace by
|
|
148
|
+
* default; host apps swap in their own via `setFonts`) — for code, IDs,
|
|
149
|
+
* and tabular figures
|
|
150
|
+
*/
|
|
151
|
+
export function MonoText(props) {
|
|
152
|
+
return _jsx(StyledText, { ...props, variant: "mono" });
|
|
153
|
+
}
|
|
163
154
|
/**
|
|
164
155
|
* Serif Bold Text Component
|
|
165
156
|
* Uses the serif font family — Georgia's only shipped weight is regular
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { palette } from "../constants/colors.js";
|
|
3
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
4
3
|
import { spacing } from "../constants/spacing.js";
|
|
5
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
5
|
import { hapticLight } from "../lib/haptics.js";
|
|
@@ -119,8 +118,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
119
118
|
left: labelHorizontalInset,
|
|
120
119
|
right: thumbInset + thumbSize + labelGap,
|
|
121
120
|
opacity: props.checked ? 1 : 0,
|
|
122
|
-
}, children: _jsx(StyledText, { selectable: false, style: {
|
|
123
|
-
fontFamily: fontFamilies.sansSerif.bold,
|
|
121
|
+
}, children: _jsx(StyledText, { selectable: false, fontWeight: "bold", style: {
|
|
124
122
|
fontSize: labelFontSize,
|
|
125
123
|
color: labelOnColor,
|
|
126
124
|
userSelect: "none",
|
|
@@ -143,8 +141,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
143
141
|
left: thumbInset + thumbSize + labelGap,
|
|
144
142
|
right: labelHorizontalInset,
|
|
145
143
|
opacity: props.checked ? 0 : 1,
|
|
146
|
-
}, children: _jsx(StyledText, { selectable: false, style: {
|
|
147
|
-
fontFamily: fontFamilies.sansSerif.bold,
|
|
144
|
+
}, children: _jsx(StyledText, { selectable: false, fontWeight: "bold", style: {
|
|
148
145
|
fontSize: labelFontSize,
|
|
149
146
|
color: theme.colors.text,
|
|
150
147
|
userSelect: "none",
|
|
@@ -4,10 +4,11 @@ import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from
|
|
|
4
4
|
import { Host, TextInput as ExpoTextInput, useNativeState, } from "@expo/ui";
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
6
|
import { spacing } from "../constants/spacing.js";
|
|
7
|
-
import {
|
|
7
|
+
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
8
8
|
import { StyledText } from "./StyledText.js";
|
|
9
9
|
import { Icon } from "./Icon.js";
|
|
10
10
|
import { hapticLight } from "../lib/haptics.js";
|
|
11
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
11
12
|
import { palette } from "../constants/colors.js";
|
|
12
13
|
import { clearKeyboardFocusedInputLayout, setKeyboardFocusedInputLayout, } from "./keyboardFocusRegistry.js";
|
|
13
14
|
const NUMERIC_REGEX = /^[0-9]*$/;
|
|
@@ -91,7 +92,8 @@ export function TextInput(props) {
|
|
|
91
92
|
*/
|
|
92
93
|
function WebTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, showSecureEntryToggle, leftElement, rightElement, clearable = false, wrapperStyle, focusedStyle, forceLight, secureTextEntry, inputMode, style, onChangeText, onFocus, onBlur, value, multiline, editable = true, ref, ...rest }) {
|
|
93
94
|
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
94
|
-
const styles =
|
|
95
|
+
const styles = themedStyles(theme)[variant][size];
|
|
96
|
+
const inputFont = useFontStyle("regular");
|
|
95
97
|
const [focused, setFocused] = useState(false);
|
|
96
98
|
const [contentHeight, setContentHeight] = useState(0);
|
|
97
99
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
@@ -148,6 +150,9 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
148
150
|
};
|
|
149
151
|
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
150
152
|
styles.input,
|
|
153
|
+
// Resolved through the theme store so `setFonts` overrides apply;
|
|
154
|
+
// identical to the old hardcoded regular family by default.
|
|
155
|
+
inputFont,
|
|
151
156
|
{
|
|
152
157
|
backgroundColor,
|
|
153
158
|
borderColor,
|
|
@@ -197,7 +202,8 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
197
202
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
198
203
|
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
199
204
|
const { theme, getContrastingColor } = useTheme();
|
|
200
|
-
const styles =
|
|
205
|
+
const styles = themedStyles(theme)[variant][size];
|
|
206
|
+
const inputFont = useFontStyle("regular");
|
|
201
207
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
202
208
|
const hasError = error || !!errorText;
|
|
203
209
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
@@ -318,10 +324,12 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
318
324
|
// @expo/ui passes the family verbatim to SwiftUI's Font.custom / Compose,
|
|
319
325
|
// where no such font exists — the fallback ignores fontSize and renders at
|
|
320
326
|
// the 17pt default, blowing up text and secure-entry dots. Omit the family
|
|
321
|
-
// so the native side uses the system font at our requested size.
|
|
322
|
-
|
|
327
|
+
// so the native side uses the system font at our requested size. The family
|
|
328
|
+
// itself resolves through the theme store (`useFontStyle` above) so
|
|
329
|
+
// `setFonts` overrides reach the native field too.
|
|
330
|
+
const nativeFontFamily = inputFont.fontFamily === "System"
|
|
323
331
|
? undefined
|
|
324
|
-
:
|
|
332
|
+
: inputFont.fontFamily;
|
|
325
333
|
const textStyle = {
|
|
326
334
|
color: textColor,
|
|
327
335
|
fontSize: sizeConfig.fontSize,
|
|
@@ -352,7 +360,8 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
352
360
|
justifyContent: "center",
|
|
353
361
|
},
|
|
354
362
|
input: {
|
|
355
|
-
fontFamily
|
|
363
|
+
// fontFamily is resolved per-render through the theme store (see
|
|
364
|
+
// `useFontStyle("regular")` in WebTextInput) so `setFonts` applies.
|
|
356
365
|
borderRadius: spacing.radiusMd,
|
|
357
366
|
borderWidth: 1,
|
|
358
367
|
...(Platform.OS === "web" && { outlineStyle: "none" }),
|
|
@@ -443,3 +452,9 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
443
452
|
zIndex: 1,
|
|
444
453
|
},
|
|
445
454
|
});
|
|
455
|
+
const VARIANT_KEYS = ["outline", "filled", "underlined"];
|
|
456
|
+
const SIZE_KEYS = ["sm", "md", "lg"];
|
|
457
|
+
const themedStyles = createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
458
|
+
variant,
|
|
459
|
+
Object.fromEntries(SIZE_KEYS.map((size) => [size, createStyles(theme, variant, size)])),
|
|
460
|
+
])));
|
|
@@ -9,19 +9,80 @@ interface NavigationFonts {
|
|
|
9
9
|
bold: FontStyle;
|
|
10
10
|
heavy: FontStyle;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
/** Weight slots every sans-serif family map must provide. */
|
|
13
|
+
export type FontFamilyWeight = "light" | "regular" | "medium" | "semibold" | "bold";
|
|
14
|
+
/**
|
|
15
|
+
* How a platform expresses weight for a given family map.
|
|
16
|
+
*
|
|
17
|
+
* - `"numeric"` — one multi-weight CSS family (the package's own web default:
|
|
18
|
+
* a single `"Inter"` family where a numeric `fontWeight` selects the
|
|
19
|
+
* `@font-face` variant).
|
|
20
|
+
* - `"family"` — each weight is its own separately-registered single-face
|
|
21
|
+
* family, so the family name alone carries the weight. Emitting a numeric
|
|
22
|
+
* `fontWeight` on top of one makes the renderer synthesise a second layer of
|
|
23
|
+
* bold, so `StyledText` suppresses it.
|
|
24
|
+
*
|
|
25
|
+
* Native is always `"family"` (discrete static font files). Web is `"numeric"`
|
|
26
|
+
* by default, but apps that load per-weight faces through `expo-font` — which
|
|
27
|
+
* registers each file as its own family on web too — must declare `"family"`.
|
|
28
|
+
*/
|
|
29
|
+
export type FontWeightStrategy = "numeric" | "family";
|
|
30
|
+
/** The font variants text components can render. */
|
|
31
|
+
export type FontVariant = "sansSerif" | "serif" | "mono";
|
|
32
|
+
export type FontFamilyMap = Record<FontVariant, Record<FontFamilyWeight, string>>;
|
|
33
|
+
/**
|
|
34
|
+
* A per-variant override group. Partial per weight: missing weights fall back
|
|
35
|
+
* to the group's own `regular` (not the package default), so an app that
|
|
36
|
+
* registers only Regular + Medium never leaks a package face into its brand.
|
|
37
|
+
*/
|
|
38
|
+
export type FontFamilyOverride = Partial<Record<FontFamilyWeight, string>>;
|
|
39
|
+
/**
|
|
40
|
+
* Font overrides a host app can inject to brand the package.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors `setColors`: the package ships neutral defaults, but a consuming app
|
|
43
|
+
* almost always bundles its own faces. Without an injection point, package
|
|
44
|
+
* components render the package's fonts while app-authored siblings render the
|
|
45
|
+
* app's — and the only remaining lever is patching `node_modules`, which is
|
|
46
|
+
* keyed to an exact version and silently stops applying on the next bump.
|
|
47
|
+
*
|
|
48
|
+
* Partial by design: override `sansSerif` alone and `serif`/`mono` keep their
|
|
49
|
+
* defaults.
|
|
50
|
+
*/
|
|
51
|
+
export type FontOverrides = {
|
|
52
|
+
families?: Partial<Record<FontVariant, FontFamilyOverride>>;
|
|
53
|
+
/** Defaults to the package's own per-platform behaviour when omitted. */
|
|
54
|
+
webWeightStrategy?: FontWeightStrategy;
|
|
24
55
|
};
|
|
56
|
+
/** The package's built-in families. Override per-app via `setFonts`. */
|
|
57
|
+
export declare const fontFamilies: FontFamilyMap;
|
|
58
|
+
/**
|
|
59
|
+
* The package's own web weight strategy: one multi-weight "Inter" family, so
|
|
60
|
+
* weight comes from a numeric `fontWeight`. Native always behaves as
|
|
61
|
+
* `"family"` regardless of this value.
|
|
62
|
+
*/
|
|
63
|
+
export declare const defaultWebWeightStrategy: FontWeightStrategy;
|
|
64
|
+
export type ResolvedFontStyle = {
|
|
65
|
+
fontFamily: string;
|
|
66
|
+
/** Present only under the `"numeric"` weight strategy (web). */
|
|
67
|
+
fontWeight?: "400" | "500" | "600" | "700";
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the `fontFamily` (+ optional numeric `fontWeight`) for a variant and
|
|
71
|
+
* weight, honouring any host-app `setFonts` overrides.
|
|
72
|
+
*
|
|
73
|
+
* This is the single source of truth every text-rendering component goes
|
|
74
|
+
* through — `StyledText` directly, control components via `useFontStyle` —
|
|
75
|
+
* so a host override re-skins package text everywhere at once.
|
|
76
|
+
*
|
|
77
|
+
* Weight is carried either by a numeric `fontWeight` (one multi-weight CSS
|
|
78
|
+
* family) or by the family name itself (discrete per-weight faces). Native is
|
|
79
|
+
* always the latter: adding a numeric weight on top of an already-bold file
|
|
80
|
+
* faux-bolds it. Web defaults to numeric for the package's own single "Inter"
|
|
81
|
+
* family, but an app loading per-weight faces through `expo-font` gets its
|
|
82
|
+
* own family per weight on web too, and must declare
|
|
83
|
+
* `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveFontStyle(overrides: FontOverrides, variant: FontVariant, weight: FontFamilyWeight): ResolvedFontStyle;
|
|
25
86
|
/**
|
|
26
87
|
* Typography scale — Tailwind-matching sizes for consistent text hierarchy.
|
|
27
88
|
* Components can progressively adopt these tokens.
|
package/dist/constants/fonts.js
CHANGED
|
@@ -8,9 +8,45 @@ const WEB_FONT_STACK = "system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-
|
|
|
8
8
|
// both environments, so the value is stable.
|
|
9
9
|
const isWebRuntime = Platform.OS === "web";
|
|
10
10
|
const isReactNativeRuntime = Platform.OS !== "web";
|
|
11
|
+
// Georgia ships as one face per platform; every weight slot points at the
|
|
12
|
+
// same family and weight differentiation only appears when a host app
|
|
13
|
+
// overrides serif with real per-weight faces via `setFonts`.
|
|
14
|
+
const webSerifFamily = "Georgia, 'Times New Roman', serif";
|
|
11
15
|
const serifFamilies = isWebRuntime
|
|
12
|
-
? {
|
|
13
|
-
|
|
16
|
+
? {
|
|
17
|
+
light: webSerifFamily,
|
|
18
|
+
regular: webSerifFamily,
|
|
19
|
+
medium: webSerifFamily,
|
|
20
|
+
semibold: webSerifFamily,
|
|
21
|
+
bold: webSerifFamily,
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
light: "Georgia",
|
|
25
|
+
regular: "Georgia",
|
|
26
|
+
medium: "Georgia",
|
|
27
|
+
semibold: "Georgia",
|
|
28
|
+
bold: "Georgia",
|
|
29
|
+
};
|
|
30
|
+
// Same single-face story as serif: the package doesn't bundle a mono font, it
|
|
31
|
+
// leans on each platform's system monospace. Menlo on iOS ("monospace" isn't
|
|
32
|
+
// a resolvable family name there); Android resolves "monospace" natively.
|
|
33
|
+
const WEB_MONO_STACK = "ui-monospace, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace";
|
|
34
|
+
const nativeMonoFamily = Platform.OS === "ios" ? "Menlo" : "monospace";
|
|
35
|
+
const monoFamilies = isWebRuntime
|
|
36
|
+
? {
|
|
37
|
+
light: WEB_MONO_STACK,
|
|
38
|
+
regular: WEB_MONO_STACK,
|
|
39
|
+
medium: WEB_MONO_STACK,
|
|
40
|
+
semibold: WEB_MONO_STACK,
|
|
41
|
+
bold: WEB_MONO_STACK,
|
|
42
|
+
}
|
|
43
|
+
: {
|
|
44
|
+
light: nativeMonoFamily,
|
|
45
|
+
regular: nativeMonoFamily,
|
|
46
|
+
medium: nativeMonoFamily,
|
|
47
|
+
semibold: nativeMonoFamily,
|
|
48
|
+
bold: nativeMonoFamily,
|
|
49
|
+
};
|
|
14
50
|
// Web ships one Inter CSS family for every weight — weight differentiation
|
|
15
51
|
// comes from a numeric `fontWeight` set alongside this family (see
|
|
16
52
|
// StyledText's getFontFamilyWeight). Native ships four discrete Inter static
|
|
@@ -42,10 +78,60 @@ const sansSerifFamilies = isWebRuntime
|
|
|
42
78
|
semibold: "sans-serif",
|
|
43
79
|
bold: "sans-serif",
|
|
44
80
|
};
|
|
81
|
+
/** The package's built-in families. Override per-app via `setFonts`. */
|
|
45
82
|
export const fontFamilies = {
|
|
46
83
|
serif: serifFamilies,
|
|
47
84
|
sansSerif: sansSerifFamilies,
|
|
85
|
+
mono: monoFamilies,
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The package's own web weight strategy: one multi-weight "Inter" family, so
|
|
89
|
+
* weight comes from a numeric `fontWeight`. Native always behaves as
|
|
90
|
+
* `"family"` regardless of this value.
|
|
91
|
+
*/
|
|
92
|
+
export const defaultWebWeightStrategy = "numeric";
|
|
93
|
+
/**
|
|
94
|
+
* Web-only numeric weights, used under the `"numeric"` strategy to pick the
|
|
95
|
+
* right `@font-face` variant out of a single multi-weight CSS family.
|
|
96
|
+
* `light` has no loaded 300 weight (mirrors the native light->regular file
|
|
97
|
+
* mapping above), so it renders at 400 like `regular`.
|
|
98
|
+
*/
|
|
99
|
+
const WEB_FONT_WEIGHTS = {
|
|
100
|
+
light: "400",
|
|
101
|
+
regular: "400",
|
|
102
|
+
medium: "500",
|
|
103
|
+
semibold: "600",
|
|
104
|
+
bold: "700",
|
|
48
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the `fontFamily` (+ optional numeric `fontWeight`) for a variant and
|
|
108
|
+
* weight, honouring any host-app `setFonts` overrides.
|
|
109
|
+
*
|
|
110
|
+
* This is the single source of truth every text-rendering component goes
|
|
111
|
+
* through — `StyledText` directly, control components via `useFontStyle` —
|
|
112
|
+
* so a host override re-skins package text everywhere at once.
|
|
113
|
+
*
|
|
114
|
+
* Weight is carried either by a numeric `fontWeight` (one multi-weight CSS
|
|
115
|
+
* family) or by the family name itself (discrete per-weight faces). Native is
|
|
116
|
+
* always the latter: adding a numeric weight on top of an already-bold file
|
|
117
|
+
* faux-bolds it. Web defaults to numeric for the package's own single "Inter"
|
|
118
|
+
* family, but an app loading per-weight faces through `expo-font` gets its
|
|
119
|
+
* own family per weight on web too, and must declare
|
|
120
|
+
* `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
|
|
121
|
+
*/
|
|
122
|
+
export function resolveFontStyle(overrides, variant, weight) {
|
|
123
|
+
const defaults = fontFamilies[variant];
|
|
124
|
+
const overrideGroup = overrides.families?.[variant];
|
|
125
|
+
const fontFamily = overrideGroup
|
|
126
|
+
? overrideGroup[weight] ?? overrideGroup.regular ?? defaults[weight]
|
|
127
|
+
: defaults[weight];
|
|
128
|
+
const strategy = Platform.OS === "web"
|
|
129
|
+
? overrides.webWeightStrategy ?? defaultWebWeightStrategy
|
|
130
|
+
: "family";
|
|
131
|
+
return strategy === "numeric"
|
|
132
|
+
? { fontFamily, fontWeight: WEB_FONT_WEIGHTS[weight] }
|
|
133
|
+
: { fontFamily };
|
|
134
|
+
}
|
|
49
135
|
export const typography = {
|
|
50
136
|
xs: { fontSize: 12, lineHeight: 16 },
|
|
51
137
|
sm: { fontSize: 14, lineHeight: 20 },
|
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/index.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FontFamilyWeight, type FontVariant, type ResolvedFontStyle } from "../constants/fonts";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a `{ fontFamily, fontWeight? }` style through the theme store's
|
|
4
|
+
* font overrides, re-rendering when a host app calls `setFonts`.
|
|
5
|
+
*
|
|
6
|
+
* For control components (Button, Label, TextInput, …) whose typography is
|
|
7
|
+
* a fixed weight rather than caller-driven. With no overrides set, the result
|
|
8
|
+
* is identical to the package's hardcoded families, so layering it after a
|
|
9
|
+
* static themed style changes nothing by default.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useFontStyle(weight?: FontFamilyWeight, variant?: FontVariant): ResolvedFontStyle;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolveFontStyle, } from "../constants/fonts.js";
|
|
2
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a `{ fontFamily, fontWeight? }` style through the theme store's
|
|
5
|
+
* font overrides, re-rendering when a host app calls `setFonts`.
|
|
6
|
+
*
|
|
7
|
+
* For control components (Button, Label, TextInput, …) whose typography is
|
|
8
|
+
* a fixed weight rather than caller-driven. With no overrides set, the result
|
|
9
|
+
* is identical to the package's hardcoded families, so layering it after a
|
|
10
|
+
* static themed style changes nothing by default.
|
|
11
|
+
*/
|
|
12
|
+
export function useFontStyle(weight = "regular", variant = "sansSerif") {
|
|
13
|
+
// Subscribed, not read via getState(), so a `setFonts` call after mount
|
|
14
|
+
// re-renders instead of leaving stale families on screen.
|
|
15
|
+
const fontOverrides = useThemeStore((s) => s.fontOverrides);
|
|
16
|
+
return resolveFontStyle(fontOverrides, variant, weight);
|
|
17
|
+
}
|
|
@@ -9,6 +9,13 @@ interface LoadResourcesResult {
|
|
|
9
9
|
* @expo-google-fonts/inter) so StyledText's weight range resolves to real
|
|
10
10
|
* font files. Web loads Inter from Google Fonts as a single CSS family;
|
|
11
11
|
* weight differentiation there comes from a numeric fontWeight instead.
|
|
12
|
+
*
|
|
13
|
+
* A host app that overrides the sans-serif families via `setFonts` owns
|
|
14
|
+
* loading its own faces (typically through `expo-font`), so the Inter fetch
|
|
15
|
+
* is skipped entirely — nothing would reference those files. For the skip to
|
|
16
|
+
* apply, call `setFonts` before this hook mounts (module scope or ahead of
|
|
17
|
+
* rendering the root); a later call still re-skins text, it just doesn't
|
|
18
|
+
* un-download Inter. The Feather icon font always loads.
|
|
12
19
|
*/
|
|
13
20
|
export declare const useResources: () => LoadResourcesResult;
|
|
14
21
|
export {};
|
|
@@ -3,6 +3,7 @@ import * as Font from "expo-font";
|
|
|
3
3
|
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
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
6
7
|
// Eager, module-scope load so expo-font registers Feather in its SSR
|
|
7
8
|
// serverContext. When the server renders the HTML document, expo-font emits
|
|
8
9
|
// an @font-face <style> into the head — without this kickoff, server-rendered
|
|
@@ -56,18 +57,28 @@ function ensureWebFontStylesheet() {
|
|
|
56
57
|
* @expo-google-fonts/inter) so StyledText's weight range resolves to real
|
|
57
58
|
* font files. Web loads Inter from Google Fonts as a single CSS family;
|
|
58
59
|
* weight differentiation there comes from a numeric fontWeight instead.
|
|
60
|
+
*
|
|
61
|
+
* A host app that overrides the sans-serif families via `setFonts` owns
|
|
62
|
+
* loading its own faces (typically through `expo-font`), so the Inter fetch
|
|
63
|
+
* is skipped entirely — nothing would reference those files. For the skip to
|
|
64
|
+
* apply, call `setFonts` before this hook mounts (module scope or ahead of
|
|
65
|
+
* rendering the root); a later call still re-skins text, it just doesn't
|
|
66
|
+
* un-download Inter. The Feather icon font always loads.
|
|
59
67
|
*/
|
|
60
68
|
export const useResources = () => {
|
|
61
69
|
const [loaded, setLoaded] = useState(false);
|
|
62
70
|
const [error, setError] = useState(null);
|
|
63
71
|
useEffect(() => {
|
|
64
72
|
let timeoutId;
|
|
73
|
+
// Read once at mount, not subscribed: font loading is a one-shot startup
|
|
74
|
+
// effect and cannot be undone by a later override.
|
|
75
|
+
const sansSerifOverridden = !!useThemeStore.getState().fontOverrides.families?.sansSerif;
|
|
65
76
|
async function loadResourcesAndDataAsync() {
|
|
66
77
|
try {
|
|
67
78
|
const fontPromise = Promise.all([
|
|
68
79
|
Font.loadAsync(Feather.font),
|
|
69
|
-
loadNativeInterFonts(),
|
|
70
|
-
ensureWebFontStylesheet(),
|
|
80
|
+
sansSerifOverridden ? Promise.resolve() : loadNativeInterFonts(),
|
|
81
|
+
sansSerifOverridden ? Promise.resolve() : ensureWebFontStylesheet(),
|
|
71
82
|
]);
|
|
72
83
|
// Timeout after 5 seconds — proceed with system fallback fonts
|
|
73
84
|
const timeoutPromise = new Promise((_, reject) => {
|
package/dist/lib/index.d.ts
CHANGED
package/dist/lib/index.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Theme } from "../constants/colors";
|
|
2
|
+
/**
|
|
3
|
+
* Registers theme-dependent styles at module scope so react-native-web
|
|
4
|
+
* inserts their CSS rules into the server-side sheet before the document
|
|
5
|
+
* <head> is serialized.
|
|
6
|
+
*
|
|
7
|
+
* Styles created lazily during render — the
|
|
8
|
+
* `useMemo(() => createStyles(theme), [theme])` idiom — are inserted into
|
|
9
|
+
* RNW's sheet *after* expo-router has already captured the
|
|
10
|
+
* `<style id="react-native-stylesheet">` snapshot for the head. The SSR
|
|
11
|
+
* HTML then references class names that have no rules, which paints
|
|
12
|
+
* completely unstyled until hydration re-inserts them client-side (worst
|
|
13
|
+
* on the first request after a server cold start, and on every dev
|
|
14
|
+
* rebundle). See docs/ssr-hydration.md.
|
|
15
|
+
*
|
|
16
|
+
* Both base themes are computed eagerly at module load. Themes carrying
|
|
17
|
+
* color overrides (global brand or scoped) are computed once per theme
|
|
18
|
+
* object identity — `useTheme` keeps that identity stable, so the WeakMap
|
|
19
|
+
* behaves like the old `useMemo` without the render-time registration.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createThemedStyles<T extends object>(factory: (theme: Theme) => T): (theme: Theme) => T;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { colors } from "../constants/colors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Registers theme-dependent styles at module scope so react-native-web
|
|
4
|
+
* inserts their CSS rules into the server-side sheet before the document
|
|
5
|
+
* <head> is serialized.
|
|
6
|
+
*
|
|
7
|
+
* Styles created lazily during render — the
|
|
8
|
+
* `useMemo(() => createStyles(theme), [theme])` idiom — are inserted into
|
|
9
|
+
* RNW's sheet *after* expo-router has already captured the
|
|
10
|
+
* `<style id="react-native-stylesheet">` snapshot for the head. The SSR
|
|
11
|
+
* HTML then references class names that have no rules, which paints
|
|
12
|
+
* completely unstyled until hydration re-inserts them client-side (worst
|
|
13
|
+
* on the first request after a server cold start, and on every dev
|
|
14
|
+
* rebundle). See docs/ssr-hydration.md.
|
|
15
|
+
*
|
|
16
|
+
* Both base themes are computed eagerly at module load. Themes carrying
|
|
17
|
+
* color overrides (global brand or scoped) are computed once per theme
|
|
18
|
+
* object identity — `useTheme` keeps that identity stable, so the WeakMap
|
|
19
|
+
* behaves like the old `useMemo` without the render-time registration.
|
|
20
|
+
*/
|
|
21
|
+
export function createThemedStyles(factory) {
|
|
22
|
+
const cache = new WeakMap();
|
|
23
|
+
cache.set(colors.light, factory(colors.light));
|
|
24
|
+
cache.set(colors.dark, factory(colors.dark));
|
|
25
|
+
return (theme) => {
|
|
26
|
+
const cached = cache.get(theme);
|
|
27
|
+
if (cached)
|
|
28
|
+
return cached;
|
|
29
|
+
const created = factory(theme);
|
|
30
|
+
cache.set(theme, created);
|
|
31
|
+
return created;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ThemeColors } from "../constants/colors";
|
|
2
|
+
import type { FontOverrides } from "../constants/fonts";
|
|
2
3
|
export type ThemePreference = "system" | "light" | "dark";
|
|
3
4
|
export type ResolvedTheme = "light" | "dark";
|
|
4
5
|
/**
|
|
@@ -19,6 +20,27 @@ export type ColorOverrides = {
|
|
|
19
20
|
light?: Partial<ThemeColors>;
|
|
20
21
|
dark?: Partial<ThemeColors>;
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Shape overrides a host app can inject, grouped per component so future
|
|
25
|
+
* shape knobs (card radius, input radius, …) slot in without reshaping the
|
|
26
|
+
* API. Every field is optional; omitted fields keep the package default.
|
|
27
|
+
*/
|
|
28
|
+
export type ShapeOverrides = {
|
|
29
|
+
button?: {
|
|
30
|
+
/**
|
|
31
|
+
* Border radius applied to every Button preset. Package default:
|
|
32
|
+
* `spacing.radiusMd` (12). Use 9999 for pill buttons. A caller `style`
|
|
33
|
+
* still wins over this, as it always has.
|
|
34
|
+
*/
|
|
35
|
+
borderRadius?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the `default` preset renders its shadow. Package default: true.
|
|
38
|
+
* Other presets stay flat regardless; the per-instance `withShadow` prop
|
|
39
|
+
* still wins over this.
|
|
40
|
+
*/
|
|
41
|
+
withShadow?: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
22
44
|
export type ThemeStore = {
|
|
23
45
|
userTheme: ThemePreference;
|
|
24
46
|
systemTheme: ResolvedTheme;
|
|
@@ -28,6 +50,18 @@ export type ThemeStore = {
|
|
|
28
50
|
* behaves exactly as before this field existed (fully backward compatible).
|
|
29
51
|
*/
|
|
30
52
|
colorOverrides: ColorOverrides;
|
|
53
|
+
/**
|
|
54
|
+
* App-injected font overrides, applied by `StyledText` on top of the package
|
|
55
|
+
* defaults. Empty by default — zero override means the package behaves
|
|
56
|
+
* exactly as before this field existed (fully backward compatible).
|
|
57
|
+
*/
|
|
58
|
+
fontOverrides: FontOverrides;
|
|
59
|
+
/**
|
|
60
|
+
* App-injected shape overrides (button radius, default-preset shadow).
|
|
61
|
+
* Same contract as the other override slots: empty by default, fully
|
|
62
|
+
* backward compatible when unset.
|
|
63
|
+
*/
|
|
64
|
+
shapeOverrides: ShapeOverrides;
|
|
31
65
|
setTheme: (theme: ThemePreference) => void;
|
|
32
66
|
setSystemTheme: (theme: ResolvedTheme) => void;
|
|
33
67
|
/**
|
|
@@ -35,6 +69,21 @@ export type ThemeStore = {
|
|
|
35
69
|
* clear overrides and fall back to the package defaults.
|
|
36
70
|
*/
|
|
37
71
|
setColors: (overrides: ColorOverrides) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Replace the active font overrides. Pass `{}` to clear them and fall back
|
|
74
|
+
* to the package's bundled faces.
|
|
75
|
+
*
|
|
76
|
+
* Call this once at startup, after the app's fonts have been registered
|
|
77
|
+
* (e.g. alongside `setColors` in a theme-sync component). Apps loading
|
|
78
|
+
* per-weight faces via `expo-font` should also pass
|
|
79
|
+
* `webWeightStrategy: "family"` — see `FontWeightStrategy`.
|
|
80
|
+
*/
|
|
81
|
+
setFonts: (overrides: FontOverrides) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Replace the active shape overrides. Pass `{}` to clear them and fall back
|
|
84
|
+
* to the package defaults (button radius 12, default-preset shadow on).
|
|
85
|
+
*/
|
|
86
|
+
setShape: (overrides: ShapeOverrides) => void;
|
|
38
87
|
loadTheme: () => void;
|
|
39
88
|
};
|
|
40
89
|
export declare function resolveThemePreference(userTheme: ThemePreference, systemTheme: ResolvedTheme): ResolvedTheme;
|