@mrmeg/expo-ui 0.16.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 +60 -0
- package/LLM_USAGE.md +9 -0
- package/README.md +50 -7
- package/dist/components/Button.js +25 -14
- package/dist/components/InputOTP.js +3 -10
- package/dist/components/Label.js +11 -10
- package/dist/components/StyledText.d.ts +11 -2
- package/dist/components/StyledText.js +17 -43
- package/dist/components/Switch.js +2 -5
- package/dist/components/TextInput.js +13 -5
- package/dist/constants/fonts.d.ts +34 -9
- package/dist/constants/fonts.js +81 -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/state/themeStore.d.ts +32 -0
- package/dist/state/themeStore.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,66 @@
|
|
|
3
3
|
All notable changes to `@mrmeg/expo-ui` are documented here. This project
|
|
4
4
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [0.17.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`mono` font variant.** `StyledText` accepts `variant="mono"` (plus a
|
|
11
|
+
`MonoText` convenience alias) for code, IDs, and tabular figures. Defaults
|
|
12
|
+
to the platform's system monospace (Menlo on iOS, `monospace` on Android, a
|
|
13
|
+
`ui-monospace` stack on web); host apps swap in their own faces via the new
|
|
14
|
+
`mono` group in `setFonts`.
|
|
15
|
+
|
|
16
|
+
- **Per-weight-partial font override groups.** `setFonts` families are now
|
|
17
|
+
partial at both levels: per group (`sansSerif` alone leaves `serif`/`mono`
|
|
18
|
+
at their defaults, as before) and per weight — weights missing from an
|
|
19
|
+
overridden group fall back to that group's `regular`, never to a package
|
|
20
|
+
face. Registering only Regular + Medium is enough. `serif` also gained the
|
|
21
|
+
full weight range (previously `regular`/`bold` only, and `SerifText`
|
|
22
|
+
ignored the requested weight; it now resolves per weight).
|
|
23
|
+
|
|
24
|
+
- **`useResources` skips the Inter download when `sansSerif` is overridden.**
|
|
25
|
+
A host app that forwards its own sans faces owns loading them; nothing
|
|
26
|
+
would reference the packaged Inter, so the native `.ttf` fetch and the web
|
|
27
|
+
Google-Fonts stylesheet are skipped. Call `setFonts` before the hook mounts
|
|
28
|
+
for the skip to apply; the Feather icon font always loads. Without
|
|
29
|
+
overrides, loading is unchanged.
|
|
30
|
+
|
|
31
|
+
- **`setShape` — shape injection, completing the `setColors`/`setFonts`
|
|
32
|
+
trio.** Grouped per component for future growth; currently covers Button:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
useThemeStore.getState().setShape({
|
|
36
|
+
button: { borderRadius: 9999, withShadow: false },
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`borderRadius` applies to every Button preset (package default stays 12);
|
|
41
|
+
`withShadow` controls the `default` preset's resting shadow (default stays
|
|
42
|
+
on). Precedence is caller-wins throughout: the per-instance `withShadow`
|
|
43
|
+
prop and caller `style` beat the global override, which beats the package
|
|
44
|
+
default. `setShape({})` clears back to the defaults.
|
|
45
|
+
|
|
46
|
+
- **Every text-rendering component now resolves families through the theme
|
|
47
|
+
store.** Button labels, Label, Switch track labels, InputOTP cells, and
|
|
48
|
+
TextInput previously hardcoded Inter family constants in their static
|
|
49
|
+
styles, so `setFonts` re-skinned `StyledText` but left control chrome on
|
|
50
|
+
Inter. They now share one resolver (`resolveFontStyle`, surfaced to
|
|
51
|
+
components as the `useFontStyle` hook, both exported), so a `setFonts` call
|
|
52
|
+
re-skins the whole package at once.
|
|
53
|
+
|
|
54
|
+
- Exported `FontVariant`, `FontFamilyOverride`, `ResolvedFontStyle`,
|
|
55
|
+
`resolveFontStyle` (from `constants`), `useFontStyle` (from `hooks`),
|
|
56
|
+
`ShapeOverrides` (from `state`), and `MonoText` (from `components`).
|
|
57
|
+
|
|
58
|
+
### Why
|
|
59
|
+
|
|
60
|
+
A consuming app rethemed to a new design language (custom sans/serif/mono
|
|
61
|
+
faces, pill buttons, flat surfaces) and hit the package's remaining hardcoded
|
|
62
|
+
Inter references, the fixed button radius, and the always-on default-preset
|
|
63
|
+
shadow — none of which were themable. Fully backward compatible: with no
|
|
64
|
+
`setFonts`/`setShape` call, fonts, radii, and shadows are identical to 0.16.0.
|
|
65
|
+
|
|
6
66
|
## [0.16.0]
|
|
7
67
|
|
|
8
68
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -164,6 +164,15 @@ React context, scoped keys win over the global brand inside it, and nested
|
|
|
164
164
|
scopes merge (inner wins, outer fills in). With no override at either layer,
|
|
165
165
|
`useTheme()` returns the base theme by reference.
|
|
166
166
|
|
|
167
|
+
Fonts and shape have matching global injection points. `setFonts({ families:
|
|
168
|
+
{ sansSerif?, serif?, mono? }, webWeightStrategy? })` replaces the bundled
|
|
169
|
+
faces (Inter/Georgia/system-mono) everywhere text renders; groups and weights
|
|
170
|
+
are partial, missing weights fall back to the group's `regular`, and an
|
|
171
|
+
overridden `sansSerif` makes `useResources` skip downloading Inter (call
|
|
172
|
+
`setFonts` before mount for the skip). `setShape({ button: { borderRadius?,
|
|
173
|
+
withShadow? } })` re-shapes Buttons globally (e.g. pill radius 9999, shadow
|
|
174
|
+
off). Per-instance props and caller `style` always win over both.
|
|
175
|
+
|
|
167
176
|
## Component Use-Case Index
|
|
168
177
|
|
|
169
178
|
Use this table before creating a new app-local primitive.
|
package/README.md
CHANGED
|
@@ -165,9 +165,10 @@ import { ThemeColorScope } from "@mrmeg/expo-ui/state";
|
|
|
165
165
|
|
|
166
166
|
### Font overrides
|
|
167
167
|
|
|
168
|
-
The package bundles Inter (sans)
|
|
169
|
-
|
|
170
|
-
Call it once at startup,
|
|
168
|
+
The package bundles Inter (sans), Georgia (serif), and the platform's system
|
|
169
|
+
monospace (mono). Most apps ship their own faces — forward them with
|
|
170
|
+
`setFonts`, the font counterpart to `setColors`. Call it once at startup,
|
|
171
|
+
after your fonts are registered:
|
|
171
172
|
|
|
172
173
|
```tsx
|
|
173
174
|
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
@@ -175,19 +176,40 @@ import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
|
175
176
|
useThemeStore.getState().setFonts({
|
|
176
177
|
families: {
|
|
177
178
|
sansSerif: {
|
|
178
|
-
light: "HankenGrotesk_300Light",
|
|
179
179
|
regular: "HankenGrotesk_400Regular",
|
|
180
180
|
medium: "HankenGrotesk_500Medium",
|
|
181
181
|
semibold: "HankenGrotesk_600SemiBold",
|
|
182
182
|
bold: "HankenGrotesk_700Bold",
|
|
183
183
|
},
|
|
184
|
+
serif: {
|
|
185
|
+
regular: "Newsreader_400Regular",
|
|
186
|
+
semibold: "Newsreader_600SemiBold",
|
|
187
|
+
},
|
|
188
|
+
mono: {
|
|
189
|
+
regular: "JetBrainsMono_400Regular",
|
|
190
|
+
medium: "JetBrainsMono_500Medium",
|
|
191
|
+
},
|
|
184
192
|
},
|
|
185
193
|
webWeightStrategy: "family",
|
|
186
194
|
});
|
|
187
195
|
```
|
|
188
196
|
|
|
189
|
-
Overrides are partial
|
|
190
|
-
|
|
197
|
+
Overrides are partial at both levels:
|
|
198
|
+
|
|
199
|
+
- **Per group** — pass `sansSerif` alone and `serif`/`mono` keep the package
|
|
200
|
+
defaults.
|
|
201
|
+
- **Per weight** — within an overridden group, weights you don't provide fall
|
|
202
|
+
back to that group's `regular` (never to a package face), so registering
|
|
203
|
+
only Regular + Medium is enough.
|
|
204
|
+
|
|
205
|
+
`setFonts({})` clears back to the defaults.
|
|
206
|
+
|
|
207
|
+
When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
208
|
+
Inter faces entirely (native `.ttf`s and the web Google-Fonts stylesheet) —
|
|
209
|
+
your app owns loading its own families, and nothing would reference Inter.
|
|
210
|
+
For the skip to apply, call `setFonts` before the hook mounts (module scope,
|
|
211
|
+
or ahead of rendering the root); a later call still re-skins all text, it
|
|
212
|
+
just doesn't prevent the Inter download. The Feather icon font always loads.
|
|
191
213
|
|
|
192
214
|
**Pick the right `webWeightStrategy`** — this is the one non-obvious part:
|
|
193
215
|
|
|
@@ -207,6 +229,26 @@ Native always behaves as `"family"` regardless of this setting.
|
|
|
207
229
|
> `patchedDependencies` to an exact `name@version`, so a font patch silently
|
|
208
230
|
> stops applying the next time you bump the package — with no warning.
|
|
209
231
|
|
|
232
|
+
### Shape overrides
|
|
233
|
+
|
|
234
|
+
`setShape` is the geometry counterpart to `setColors`/`setFonts`, grouped per
|
|
235
|
+
component. It currently covers Button:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
239
|
+
|
|
240
|
+
useThemeStore.getState().setShape({
|
|
241
|
+
button: {
|
|
242
|
+
borderRadius: 9999, // pill buttons everywhere; package default is 12
|
|
243
|
+
withShadow: false, // flatten the `default` preset; package default is true
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Precedence is unchanged and caller-wins: a per-instance `style={{ borderRadius }}`
|
|
249
|
+
or `withShadow` prop still beats the global override, which beats the package
|
|
250
|
+
default. `setShape({})` clears back to the defaults.
|
|
251
|
+
|
|
210
252
|
Use `StyledText` for theme-aware text:
|
|
211
253
|
|
|
212
254
|
```tsx
|
|
@@ -223,7 +265,8 @@ Useful `StyledText` props:
|
|
|
223
265
|
- `semantic`: `title`, `heading`, `subheading`, `body`, `caption`, `label`
|
|
224
266
|
- `size`: `xs`, `sm`, `base`, `body`, `lg`, `xl`, `xxl`, `display`
|
|
225
267
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
226
|
-
- `variant`: `sansSerif`, `serif`
|
|
268
|
+
- `variant`: `sansSerif`, `serif`, `mono` (or the `MonoText` alias for code,
|
|
269
|
+
IDs, and tabular figures)
|
|
227
270
|
- `align`, `tx`, `txOptions`
|
|
228
271
|
- `selectable`: defaults to `true` for readable copy; package controls disable
|
|
229
272
|
selection for labels and other interactive chrome where accidental drag
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { use, useCallback, useState } from "react";
|
|
2
|
+
import { use, useCallback, useMemo, useState } from "react";
|
|
3
3
|
import { Pressable, StyleSheet, View, Platform, ActivityIndicator, Animated, } from "react-native";
|
|
4
4
|
import { spacing } from "../constants/spacing.js";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
6
6
|
import { Icon } from "./Icon.js";
|
|
7
7
|
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
8
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
9
8
|
import { palette } from "../constants/colors.js";
|
|
10
9
|
import { useTheme } from "../hooks/useTheme.js";
|
|
10
|
+
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
11
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
12
13
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
13
14
|
const SIZE_CONFIGS = {
|
|
14
15
|
sm: {
|
|
@@ -76,11 +77,21 @@ function ButtonRoot(props) {
|
|
|
76
77
|
const styles = themedStyles(theme)[size];
|
|
77
78
|
const shadowStyle = getShadowStyle("subtle");
|
|
78
79
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
80
|
+
// Host-app shape overrides (radius, default-preset shadow) — see `setShape`.
|
|
81
|
+
const buttonShape = useThemeStore((s) => s.shapeOverrides.button);
|
|
79
82
|
// Filled (`default`) buttons float by default; ghost/outline/link stay flat
|
|
80
83
|
// (transparent background reads oddly with a shadow), and destructive/secondary
|
|
81
|
-
// keep the old opt-in behavior.
|
|
82
|
-
|
|
84
|
+
// keep the old opt-in behavior. Precedence: the per-instance `withShadow`
|
|
85
|
+
// prop, then the host app's `setShape` flag, then the preset default.
|
|
86
|
+
const withShadow = withShadowProp
|
|
87
|
+
?? (preset === "default" ? buttonShape?.withShadow ?? true : false);
|
|
83
88
|
const focusRingStyle = getFocusRingStyle();
|
|
89
|
+
// Label typography resolves through the theme store so `setFonts` re-skins
|
|
90
|
+
// button labels along with everything else. Provided via TextStyleContext
|
|
91
|
+
// (after the static size styles) so nested Button.Text / StyledText children
|
|
92
|
+
// inherit it too, exactly like the previous hardcoded family did.
|
|
93
|
+
const labelFontStyle = useFontStyle("medium");
|
|
94
|
+
const labelTextStyle = useMemo(() => [styles.text, { fontFamily: labelFontStyle.fontFamily, fontWeight: labelFontStyle.fontWeight }], [styles.text, labelFontStyle.fontFamily, labelFontStyle.fontWeight]);
|
|
84
95
|
// Pre-compute background color for contrast calculation
|
|
85
96
|
// Always flatten to handle both array styles (from Slot) and RegisteredStyle
|
|
86
97
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
@@ -170,8 +181,12 @@ function ButtonRoot(props) {
|
|
|
170
181
|
return nextWidth;
|
|
171
182
|
});
|
|
172
183
|
}, [fullWidth, loading]);
|
|
173
|
-
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TextStyleContext.Provider, { value:
|
|
184
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TextStyleContext.Provider, { value: labelTextStyle, children: _jsx(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!isDisabled, busy: loading }, ...rest, onPressIn: handlePressIn, onPressOut: handlePressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: { alignSelf: fullWidth ? "stretch" : flattenedStyle?.alignSelf ?? "flex-start" }, hitSlop: rest.hitSlop ?? (Platform.OS === "web" ? undefined : getNativeHitSlop(sizeConfig)), disabled: isDisabled, children: (state) => (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(View, { style: [
|
|
174
185
|
styles.button,
|
|
186
|
+
// Host-app radius override (see `setShape`). Sits right
|
|
187
|
+
// after the static radius so presets, state styles, and —
|
|
188
|
+
// critically — the caller `style` spread below still win.
|
|
189
|
+
buttonShape?.borderRadius !== undefined && { borderRadius: buttonShape.borderRadius },
|
|
175
190
|
preset === "default" && styles.buttonDefault,
|
|
176
191
|
preset === "outline" && styles.buttonOutline,
|
|
177
192
|
preset === "ghost" && styles.buttonGhost,
|
|
@@ -191,7 +206,7 @@ function ButtonRoot(props) {
|
|
|
191
206
|
// Spread array styles from Slot to prevent nested arrays on web
|
|
192
207
|
...(Array.isArray(styleOverride) ? styleOverride : [styleOverride]),
|
|
193
208
|
], onLayout: handleButtonLayout, children: [loading && (_jsx(View, { style: [styles.loaderOverlay, { pointerEvents: "none" }], children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })), _jsxs(View, { style: [styles.content, loading && styles.loadingContent, { pointerEvents: loading ? "none" : "auto" }], children: [!!LeftAccessory && (_jsx(LeftAccessory, { style: styles.leftAccessory, pressableState: state, disabled: isDisabled })), (tx || text) ? (_jsx(StyledText, { tx: tx, text: text, txOptions: txOptions, style: [
|
|
194
|
-
|
|
209
|
+
labelTextStyle,
|
|
195
210
|
state.pressed && styles.pressedText,
|
|
196
211
|
state.pressed && pressedTextStyleOverride,
|
|
197
212
|
isDisabled && disabledTextStyleOverride,
|
|
@@ -283,15 +298,11 @@ const createStyles = (theme, size) => {
|
|
|
283
298
|
width: "100%",
|
|
284
299
|
},
|
|
285
300
|
text: {
|
|
286
|
-
// Button labels render at medium weight.
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// fontWeight to pick the right @font-face variant. (Same rule as
|
|
291
|
-
// StyledText's getFontFamilyWeight / WEB_FONT_WEIGHTS.)
|
|
292
|
-
fontFamily: fontFamilies.sansSerif.medium,
|
|
301
|
+
// Button labels render at medium weight. The fontFamily (+ numeric
|
|
302
|
+
// fontWeight on web) is NOT set here: it's resolved per-render through
|
|
303
|
+
// the theme store (`useFontStyle("medium")` in ButtonRoot) and layered
|
|
304
|
+
// right after this style, so `setFonts` overrides reach button labels.
|
|
293
305
|
fontSize: sizeConfig.fontSize,
|
|
294
|
-
...(Platform.OS === "web" && { fontWeight: "500" }),
|
|
295
306
|
textAlign: "center",
|
|
296
307
|
lineHeight: sizeConfig.fontSize * 1.4,
|
|
297
308
|
flexShrink: 0,
|
|
@@ -3,13 +3,11 @@ import { useRef, useState, useCallback } from "react";
|
|
|
3
3
|
import { View, TextInput as RNTextInput, Pressable, StyleSheet, Platform, } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { spacing } from "../constants/spacing.js";
|
|
6
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
7
6
|
import { StyledText } from "./StyledText.js";
|
|
8
7
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
8
|
const CELL_WIDTH = 36;
|
|
10
9
|
const CELL_HEIGHT = 40;
|
|
11
10
|
const CELL_FONT_SIZE = 20;
|
|
12
|
-
const CELL_FONT_WEIGHT = "600";
|
|
13
11
|
const BULLET = "\u2022";
|
|
14
12
|
/**
|
|
15
13
|
* OTP/verification code input with individual character cells.
|
|
@@ -98,14 +96,8 @@ function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPr
|
|
|
98
96
|
opacity: disabled ? 0.5 : 1,
|
|
99
97
|
borderColor,
|
|
100
98
|
},
|
|
101
|
-
], children: _jsx(StyledText, { selectable: false, style: {
|
|
99
|
+
], children: _jsx(StyledText, { selectable: false, fontWeight: "semibold", style: {
|
|
102
100
|
fontSize: CELL_FONT_SIZE,
|
|
103
|
-
// Semibold weight: the family carries it on native (a real static
|
|
104
|
-
// Inter_600SemiBold file); web shares one "Inter" family across
|
|
105
|
-
// weights, so it needs the numeric fontWeight to pick the right
|
|
106
|
-
// @font-face variant (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
107
|
-
...(Platform.OS === "web" && { fontWeight: CELL_FONT_WEIGHT }),
|
|
108
|
-
fontFamily: fontFamilies.sansSerif.semibold,
|
|
109
101
|
color: theme.colors.text,
|
|
110
102
|
textAlign: "center",
|
|
111
103
|
lineHeight: CELL_FONT_SIZE * 1.2,
|
|
@@ -127,8 +119,9 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
127
119
|
gap: spacing.sm,
|
|
128
120
|
},
|
|
129
121
|
errorText: {
|
|
122
|
+
// Regular weight — StyledText's default resolves the family through the
|
|
123
|
+
// theme store, so no hardcoded fontFamily here.
|
|
130
124
|
fontSize: 12,
|
|
131
|
-
fontFamily: fontFamilies.sansSerif.regular,
|
|
132
125
|
color: theme.colors.destructive,
|
|
133
126
|
marginTop: spacing.xs,
|
|
134
127
|
},
|
package/dist/components/Label.js
CHANGED
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { StyleSheet, Platform } from "react-native";
|
|
3
3
|
import * as LabelPrimitive from "@rn-primitives/label";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
+
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
5
6
|
import { spacing } from "../constants/spacing.js";
|
|
6
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
7
7
|
import { StyledText } from "./StyledText.js";
|
|
8
8
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
9
|
const SIZE_CONFIGS = {
|
|
@@ -39,8 +39,12 @@ export function Label({ children, nativeID, required, size = "md", error, disabl
|
|
|
39
39
|
const { theme } = useTheme();
|
|
40
40
|
const styles = themedStyles(theme);
|
|
41
41
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
42
|
+
// Resolved through the theme store so `setFonts` overrides apply; identical
|
|
43
|
+
// to the old hardcoded medium family when no override is set.
|
|
44
|
+
const labelFont = useFontStyle("medium");
|
|
42
45
|
const textStyle = StyleSheet.flatten([
|
|
43
46
|
styles.label,
|
|
47
|
+
labelFont,
|
|
44
48
|
{ fontSize: sizeConfig.fontSize },
|
|
45
49
|
error && styles.errorLabel,
|
|
46
50
|
disabled && styles.disabledLabel,
|
|
@@ -49,26 +53,23 @@ export function Label({ children, nativeID, required, size = "md", error, disabl
|
|
|
49
53
|
return (_jsx(LabelPrimitive.Root, { nativeID: nativeID, onPress: onPress, style: {
|
|
50
54
|
...styles.root,
|
|
51
55
|
...(Platform.OS === "web" && onPress && { cursor: "pointer" }),
|
|
52
|
-
}, children: _jsxs(LabelPrimitive.Text, { style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) }));
|
|
56
|
+
}, children: _jsxs(LabelPrimitive.Text, { style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) }));
|
|
53
57
|
}
|
|
54
58
|
const createStyles = (theme) => StyleSheet.create({
|
|
55
59
|
root: {
|
|
56
60
|
marginBottom: spacing.xs,
|
|
57
61
|
},
|
|
58
62
|
label: {
|
|
59
|
-
// Medium weight
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// variant. Setting fontWeight on native would faux-bold on top of the
|
|
63
|
-
// already-medium file (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
64
|
-
fontFamily: fontFamilies.sansSerif.medium,
|
|
65
|
-
...(Platform.OS === "web" && { fontWeight: "500" }),
|
|
63
|
+
// Medium weight — the fontFamily (+ numeric fontWeight on web) is
|
|
64
|
+
// resolved per-render through the theme store (`useFontStyle("medium")`
|
|
65
|
+
// in Label) so `setFonts` overrides apply here too.
|
|
66
66
|
color: theme.colors.text,
|
|
67
67
|
userSelect: "none",
|
|
68
68
|
},
|
|
69
69
|
required: {
|
|
70
|
+
// Bold weight comes from the StyledText `fontWeight="bold"` prop, which
|
|
71
|
+
// resolves the family through the theme store.
|
|
70
72
|
color: theme.colors.destructive,
|
|
71
|
-
fontFamily: fontFamilies.sansSerif.bold,
|
|
72
73
|
userSelect: "none",
|
|
73
74
|
},
|
|
74
75
|
errorLabel: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Ref } from "react";
|
|
2
2
|
import { Text as RNText, TextProps as RNTextProps } from "react-native";
|
|
3
|
+
import { type FontVariant } from "../constants/fonts";
|
|
3
4
|
/**
|
|
4
5
|
* Font size variants for the type scale (Inter sans-serif / Georgia serif)
|
|
5
6
|
*/
|
|
@@ -18,9 +19,10 @@ export type FontWeight = "light" | "regular" | "medium" | "semibold" | "bold";
|
|
|
18
19
|
export type TextAlign = "left" | "center" | "right" | "justify" | "auto";
|
|
19
20
|
export type TextProps = RNTextProps & {
|
|
20
21
|
/**
|
|
21
|
-
* Font variant - serif (Georgia)
|
|
22
|
+
* Font variant - serif (Georgia), sans-serif (Inter), or mono (the
|
|
23
|
+
* platform's system monospace). Host apps swap the families via `setFonts`.
|
|
22
24
|
*/
|
|
23
|
-
variant?:
|
|
25
|
+
variant?: FontVariant;
|
|
24
26
|
/**
|
|
25
27
|
* Font weight - light, regular, medium, semibold, bold
|
|
26
28
|
*/
|
|
@@ -83,6 +85,13 @@ export declare function SerifText(props: TextProps): import("react").JSX.Element
|
|
|
83
85
|
* Uses the sans-serif font family (Inter)
|
|
84
86
|
*/
|
|
85
87
|
export declare function SansSerifText(props: TextProps): import("react").JSX.Element;
|
|
88
|
+
/**
|
|
89
|
+
* Mono Text Component
|
|
90
|
+
* Uses the monospace font family (the platform's system monospace by
|
|
91
|
+
* default; host apps swap in their own via `setFonts`) — for code, IDs,
|
|
92
|
+
* and tabular figures
|
|
93
|
+
*/
|
|
94
|
+
export declare function MonoText(props: TextProps): import("react").JSX.Element;
|
|
86
95
|
/**
|
|
87
96
|
* Serif Bold Text Component
|
|
88
97
|
* Uses the serif font family — Georgia's only shipped weight is regular
|
|
@@ -1,8 +1,8 @@
|
|
|
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
6
|
import { useThemeStore } from "../state/themeStore.js";
|
|
7
7
|
import { translateText } from "../lib/i18n.js";
|
|
8
8
|
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
@@ -55,22 +55,6 @@ const SEMANTIC_CONFIGS = {
|
|
|
55
55
|
// and the textTransform applied in StyledText below.
|
|
56
56
|
eyebrow: { size: "sm", weight: "semibold" },
|
|
57
57
|
};
|
|
58
|
-
// Font family weight keys now map 1:1 onto FontWeight — every weight has a
|
|
59
|
-
// real family entry (native: a distinct static Inter file; web: shared
|
|
60
|
-
// "Inter" family + numeric fontWeight, see WEB_FONT_WEIGHTS). Defaults to
|
|
61
|
-
// "regular" when no weight is requested.
|
|
62
|
-
const getFontFamilyWeight = (weight) => weight ?? "regular";
|
|
63
|
-
// Web-only: the numeric fontWeight that picks the right @font-face variant
|
|
64
|
-
// out of the single shared "Inter" CSS family. "light" has no loaded 300
|
|
65
|
-
// weight (mirrors the native light->regular mapping in constants/fonts.ts),
|
|
66
|
-
// so it renders at 400 like "regular".
|
|
67
|
-
const WEB_FONT_WEIGHTS = {
|
|
68
|
-
light: "400",
|
|
69
|
-
regular: "400",
|
|
70
|
-
medium: "500",
|
|
71
|
-
semibold: "600",
|
|
72
|
-
bold: "700",
|
|
73
|
-
};
|
|
74
58
|
/**
|
|
75
59
|
* Base Text component with theme and variant support
|
|
76
60
|
* Uses theme colors automatically - text by default
|
|
@@ -104,31 +88,12 @@ export function StyledText(props) {
|
|
|
104
88
|
const semanticConfig = semantic ? SEMANTIC_CONFIGS[semantic] : undefined;
|
|
105
89
|
const finalSize = semanticConfig?.size ?? size ?? "body";
|
|
106
90
|
const finalFontWeight = semanticConfig?.weight ?? fontWeight ?? "regular";
|
|
107
|
-
// Get font family
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const sansSerifFamilies = overriddenFamilies?.sansSerif ?? fontFamilies.sansSerif;
|
|
114
|
-
// The serif slot has a single usable weight — use it regardless of the
|
|
115
|
-
// requested weight.
|
|
116
|
-
const fontFamily = variant === "serif"
|
|
117
|
-
? serifFamilies.regular
|
|
118
|
-
: sansSerifFamilies[fontFamilyWeight] ?? sansSerifFamilies.regular;
|
|
119
|
-
// Weight is carried either by a numeric `fontWeight` (one multi-weight CSS
|
|
120
|
-
// family) or by the family name itself (discrete per-weight faces). Native
|
|
121
|
-
// is always the latter: adding a numeric weight on top of an already-bold
|
|
122
|
-
// file faux-bolds it. Web defaults to numeric for the package's own single
|
|
123
|
-
// "Inter" family, but an app loading per-weight faces through `expo-font`
|
|
124
|
-
// gets its own family per weight on web too, and must declare
|
|
125
|
-
// `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
|
|
126
|
-
const weightStrategy = Platform.OS === "web"
|
|
127
|
-
? fontOverrides.webWeightStrategy ?? defaultWebWeightStrategy
|
|
128
|
-
: "family";
|
|
129
|
-
const resolvedFontWeight = weightStrategy === "numeric"
|
|
130
|
-
? WEB_FONT_WEIGHTS[finalFontWeight]
|
|
131
|
-
: 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);
|
|
132
97
|
// Get fontSize and lineHeight from size variant
|
|
133
98
|
const fontSize = FONT_SIZES[finalSize];
|
|
134
99
|
const lineHeight = LINE_HEIGHTS[finalSize];
|
|
@@ -177,6 +142,15 @@ export function SerifText(props) {
|
|
|
177
142
|
export function SansSerifText(props) {
|
|
178
143
|
return _jsx(StyledText, { ...props, variant: "sansSerif" });
|
|
179
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
|
+
}
|
|
180
154
|
/**
|
|
181
155
|
* Serif Bold Text Component
|
|
182
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,7 +4,7 @@ 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";
|
|
@@ -93,6 +93,7 @@ export function TextInput(props) {
|
|
|
93
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 }) {
|
|
94
94
|
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
95
95
|
const styles = themedStyles(theme)[variant][size];
|
|
96
|
+
const inputFont = useFontStyle("regular");
|
|
96
97
|
const [focused, setFocused] = useState(false);
|
|
97
98
|
const [contentHeight, setContentHeight] = useState(0);
|
|
98
99
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
@@ -149,6 +150,9 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
149
150
|
};
|
|
150
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: [
|
|
151
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,
|
|
152
156
|
{
|
|
153
157
|
backgroundColor,
|
|
154
158
|
borderColor,
|
|
@@ -199,6 +203,7 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
199
203
|
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
200
204
|
const { theme, getContrastingColor } = useTheme();
|
|
201
205
|
const styles = themedStyles(theme)[variant][size];
|
|
206
|
+
const inputFont = useFontStyle("regular");
|
|
202
207
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
203
208
|
const hasError = error || !!errorText;
|
|
204
209
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
@@ -319,10 +324,12 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
319
324
|
// @expo/ui passes the family verbatim to SwiftUI's Font.custom / Compose,
|
|
320
325
|
// where no such font exists — the fallback ignores fontSize and renders at
|
|
321
326
|
// the 17pt default, blowing up text and secure-entry dots. Omit the family
|
|
322
|
-
// so the native side uses the system font at our requested size.
|
|
323
|
-
|
|
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"
|
|
324
331
|
? undefined
|
|
325
|
-
:
|
|
332
|
+
: inputFont.fontFamily;
|
|
326
333
|
const textStyle = {
|
|
327
334
|
color: textColor,
|
|
328
335
|
fontSize: sizeConfig.fontSize,
|
|
@@ -353,7 +360,8 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
353
360
|
justifyContent: "center",
|
|
354
361
|
},
|
|
355
362
|
input: {
|
|
356
|
-
fontFamily
|
|
363
|
+
// fontFamily is resolved per-render through the theme store (see
|
|
364
|
+
// `useFontStyle("regular")` in WebTextInput) so `setFonts` applies.
|
|
357
365
|
borderRadius: spacing.radiusMd,
|
|
358
366
|
borderWidth: 1,
|
|
359
367
|
...(Platform.OS === "web" && { outlineStyle: "none" }),
|
|
@@ -27,13 +27,15 @@ export type FontFamilyWeight = "light" | "regular" | "medium" | "semibold" | "bo
|
|
|
27
27
|
* registers each file as its own family on web too — must declare `"family"`.
|
|
28
28
|
*/
|
|
29
29
|
export type FontWeightStrategy = "numeric" | "family";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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>>;
|
|
37
39
|
/**
|
|
38
40
|
* Font overrides a host app can inject to brand the package.
|
|
39
41
|
*
|
|
@@ -43,10 +45,11 @@ export type FontFamilyMap = {
|
|
|
43
45
|
* app's — and the only remaining lever is patching `node_modules`, which is
|
|
44
46
|
* keyed to an exact version and silently stops applying on the next bump.
|
|
45
47
|
*
|
|
46
|
-
* Partial by design: override `sansSerif` alone and `serif`
|
|
48
|
+
* Partial by design: override `sansSerif` alone and `serif`/`mono` keep their
|
|
49
|
+
* defaults.
|
|
47
50
|
*/
|
|
48
51
|
export type FontOverrides = {
|
|
49
|
-
families?: Partial<
|
|
52
|
+
families?: Partial<Record<FontVariant, FontFamilyOverride>>;
|
|
50
53
|
/** Defaults to the package's own per-platform behaviour when omitted. */
|
|
51
54
|
webWeightStrategy?: FontWeightStrategy;
|
|
52
55
|
};
|
|
@@ -58,6 +61,28 @@ export declare const fontFamilies: FontFamilyMap;
|
|
|
58
61
|
* `"family"` regardless of this value.
|
|
59
62
|
*/
|
|
60
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;
|
|
61
86
|
/**
|
|
62
87
|
* Typography scale — Tailwind-matching sizes for consistent text hierarchy.
|
|
63
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
|
|
@@ -46,6 +82,7 @@ const sansSerifFamilies = isWebRuntime
|
|
|
46
82
|
export const fontFamilies = {
|
|
47
83
|
serif: serifFamilies,
|
|
48
84
|
sansSerif: sansSerifFamilies,
|
|
85
|
+
mono: monoFamilies,
|
|
49
86
|
};
|
|
50
87
|
/**
|
|
51
88
|
* The package's own web weight strategy: one multi-weight "Inter" family, so
|
|
@@ -53,6 +90,48 @@ export const fontFamilies = {
|
|
|
53
90
|
* `"family"` regardless of this value.
|
|
54
91
|
*/
|
|
55
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",
|
|
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
|
+
}
|
|
56
135
|
export const typography = {
|
|
57
136
|
xs: { fontSize: 12, lineHeight: 16 },
|
|
58
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) => {
|
|
@@ -20,6 +20,27 @@ export type ColorOverrides = {
|
|
|
20
20
|
light?: Partial<ThemeColors>;
|
|
21
21
|
dark?: Partial<ThemeColors>;
|
|
22
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
|
+
};
|
|
23
44
|
export type ThemeStore = {
|
|
24
45
|
userTheme: ThemePreference;
|
|
25
46
|
systemTheme: ResolvedTheme;
|
|
@@ -35,6 +56,12 @@ export type ThemeStore = {
|
|
|
35
56
|
* exactly as before this field existed (fully backward compatible).
|
|
36
57
|
*/
|
|
37
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;
|
|
38
65
|
setTheme: (theme: ThemePreference) => void;
|
|
39
66
|
setSystemTheme: (theme: ResolvedTheme) => void;
|
|
40
67
|
/**
|
|
@@ -52,6 +79,11 @@ export type ThemeStore = {
|
|
|
52
79
|
* `webWeightStrategy: "family"` — see `FontWeightStrategy`.
|
|
53
80
|
*/
|
|
54
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;
|
|
55
87
|
loadTheme: () => void;
|
|
56
88
|
};
|
|
57
89
|
export declare function resolveThemePreference(userTheme: ThemePreference, systemTheme: ResolvedTheme): ResolvedTheme;
|
package/dist/state/themeStore.js
CHANGED
|
@@ -22,12 +22,17 @@ export const useThemeStore = create((set) => ({
|
|
|
22
22
|
// Same contract as `colorOverrides`: the package renders with its bundled
|
|
23
23
|
// faces until a host app calls `setFonts`.
|
|
24
24
|
fontOverrides: {},
|
|
25
|
+
// And again for shape: package geometry until a host app calls `setShape`.
|
|
26
|
+
shapeOverrides: {},
|
|
25
27
|
setColors: (overrides) => {
|
|
26
28
|
set({ colorOverrides: overrides ?? {} });
|
|
27
29
|
},
|
|
28
30
|
setFonts: (overrides) => {
|
|
29
31
|
set({ fontOverrides: overrides ?? {} });
|
|
30
32
|
},
|
|
33
|
+
setShape: (overrides) => {
|
|
34
|
+
set({ shapeOverrides: overrides ?? {} });
|
|
35
|
+
},
|
|
31
36
|
setTheme: (theme) => {
|
|
32
37
|
set({
|
|
33
38
|
userTheme: theme,
|