@mrmeg/expo-ui 0.23.0 → 0.24.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 +58 -0
- package/LLM_USAGE.md +159 -157
- package/README.md +421 -352
- package/dist/components/BottomSheet.js +1 -1
- package/dist/components/DismissKeyboard.d.ts +14 -3
- package/dist/components/DismissKeyboard.js +17 -48
- package/dist/components/InputOTP.js +4 -1
- package/dist/components/TextInput.js +189 -42
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/keyboardDismiss.d.ts +37 -0
- package/dist/components/keyboardDismiss.js +170 -0
- package/dist/components/keyboardFocusRegistry.d.ts +10 -14
- package/dist/components/keyboardFocusRegistry.js +8 -24
- package/llms-full.md +134 -124
- package/llms.txt +24 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,44 +1,30 @@
|
|
|
1
1
|
# @mrmeg/expo-ui
|
|
2
2
|
|
|
3
|
-
Reusable Expo and React Native UI primitives
|
|
4
|
-
|
|
5
|
-
This package is public for installability, reuse across MrMeg projects, and
|
|
6
|
-
discoverability. It is a personal reusable Expo / React Native design-system
|
|
7
|
-
package, not a generally supported open-source UI library. The package is
|
|
8
|
-
published as `UNLICENSED`; use outside MrMeg projects requires explicit
|
|
9
|
-
permission.
|
|
3
|
+
Reusable Expo and React Native UI primitives: design tokens, a theme store, and
|
|
4
|
+
a universal component library for iOS, Android, and web. MIT licensed.
|
|
10
5
|
|
|
11
6
|
## For LLMs And Coding Agents
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
`node_modules/@mrmeg/expo-ui/llms-full.md` before creating app-local UI
|
|
17
|
-
primitives. Those files are shipped in the npm package and give the short
|
|
18
|
-
component-selection rules, import paths, setup requirements, component catalog,
|
|
19
|
-
and examples that help agents choose existing package components instead of
|
|
20
|
-
rebuilding them.
|
|
8
|
+
Read `node_modules/@mrmeg/expo-ui/llms.txt` first, then `LLM_USAGE.md` (concise
|
|
9
|
+
rules) or `llms-full.md` (expanded contract), before creating app-local UI
|
|
10
|
+
primitives. All three ship in the npm tarball.
|
|
21
11
|
|
|
22
12
|
## Install
|
|
23
13
|
|
|
24
|
-
Install from npm after publishing:
|
|
25
|
-
|
|
26
14
|
```sh
|
|
27
15
|
bun add @mrmeg/expo-ui
|
|
28
16
|
```
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ranges deliberately. Keep npm auth tokens in developer or CI configuration,
|
|
41
|
-
not in this repository.
|
|
18
|
+
Tested hosts: Expo SDK 56–57, React 19.2, React Native 0.85–0.86, React Native
|
|
19
|
+
Web 0.21. Install these peers at the versions your Expo SDK recommends: `expo`,
|
|
20
|
+
`@expo/ui`, `expo-font`, `expo-haptics`, `react`, `react-native`,
|
|
21
|
+
`react-native-web`, `react-native-gesture-handler`,
|
|
22
|
+
`react-native-keyboard-controller`, `react-native-safe-area-context`,
|
|
23
|
+
`react-native-screens`, `@react-native-async-storage/async-storage`, `zustand`.
|
|
24
|
+
|
|
25
|
+
`@rn-primitives/*` and `@expo-google-fonts/inter` are package dependencies, not
|
|
26
|
+
peers. Animations use React Native `Animated`. `BottomSheet`, `Slider`, and
|
|
27
|
+
`SegmentedControl` render native `@expo/ui` controls. i18n is optional.
|
|
42
28
|
|
|
43
29
|
## Imports
|
|
44
30
|
|
|
@@ -47,21 +33,73 @@ import { Button, StyledText, UIProvider } from "@mrmeg/expo-ui/components";
|
|
|
47
33
|
import { Button as ButtonDirect } from "@mrmeg/expo-ui/components/Button";
|
|
48
34
|
import { colors, spacing, typography } from "@mrmeg/expo-ui/constants";
|
|
49
35
|
import { colors as colorsDirect } from "@mrmeg/expo-ui/constants/colors";
|
|
50
|
-
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
36
|
+
import { useResources, useStyles, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
51
37
|
import { useTheme as useThemeDirect } from "@mrmeg/expo-ui/hooks/useTheme";
|
|
52
|
-
import { globalUIStore, notify, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
38
|
+
import { globalUIStore, notify, ThemeColorScope, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
53
39
|
import { configureExpoUiI18n, hapticLight } from "@mrmeg/expo-ui/lib";
|
|
40
|
+
// The root barrel re-exports the whole public surface:
|
|
41
|
+
import { Button, colors, notify, UIProvider, useTheme } from "@mrmeg/expo-ui";
|
|
54
42
|
```
|
|
55
43
|
|
|
56
|
-
|
|
44
|
+
Importable paths: root, `components`, `components/*`, `constants`,
|
|
45
|
+
`constants/*`, `hooks`, `hooks/*`, `state`, `state/*`, `lib`. Never import from
|
|
46
|
+
`dist/*` or a source checkout path.
|
|
47
|
+
|
|
48
|
+
## App Startup
|
|
49
|
+
|
|
50
|
+
Call `useResources()` once near the Expo app root before hiding the splash
|
|
51
|
+
screen, and mount `UIProvider` once near the root. On native, mount
|
|
52
|
+
`react-native-keyboard-controller`'s `KeyboardProvider` above `UIProvider` so
|
|
53
|
+
the package's root keyboard avoidance works.
|
|
57
54
|
|
|
58
55
|
```tsx
|
|
59
|
-
import {
|
|
56
|
+
import { ThemeProvider } from "expo-router";
|
|
57
|
+
import { KeyboardProvider } from "react-native-keyboard-controller";
|
|
58
|
+
import { UIProvider } from "@mrmeg/expo-ui/components";
|
|
59
|
+
import { colors } from "@mrmeg/expo-ui/constants";
|
|
60
|
+
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
61
|
+
|
|
62
|
+
export default function RootLayout() {
|
|
63
|
+
const { scheme } = useTheme();
|
|
64
|
+
const { loaded } = useResources();
|
|
65
|
+
|
|
66
|
+
if (!loaded) return null;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ThemeProvider
|
|
70
|
+
value={{
|
|
71
|
+
dark: colors[scheme ?? "light"].dark,
|
|
72
|
+
colors: colors[scheme ?? "light"].navigation,
|
|
73
|
+
fonts: colors[scheme ?? "light"].fonts,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<KeyboardProvider>
|
|
77
|
+
<UIProvider>
|
|
78
|
+
{/* App navigation goes here. */}
|
|
79
|
+
</UIProvider>
|
|
80
|
+
</KeyboardProvider>
|
|
81
|
+
</ThemeProvider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
|
|
86
|
+
`UIProvider` is required before `Dialog`, `AlertDialog`, `BottomSheet`,
|
|
87
|
+
`Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, and package
|
|
88
|
+
notifications.
|
|
63
89
|
|
|
64
|
-
|
|
90
|
+
| `UIProvider` prop | Default | Effect |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `notification` | `true` | Mounts the `Notification` renderer for `notify` / `globalUIStore` |
|
|
93
|
+
| `portalHost` | `true` | Mounts the default `@rn-primitives` portal host |
|
|
94
|
+
| `statusBar` | `true` | Mounts the theme-aware `StatusBar` |
|
|
95
|
+
| `keyboardAvoiding` | `true` native, `false` web | Wraps children in `KeyboardAvoidingView` |
|
|
96
|
+
| `keyboardAvoidingProps` | — | Forwarded to that root wrapper |
|
|
97
|
+
|
|
98
|
+
For a subtree with custom keyboard behavior, use `KeyboardAvoidingView`
|
|
99
|
+
directly (`behavior`, `automaticOffset`, `contentContainerStyle`,
|
|
100
|
+
`keyboardVerticalOffset`).
|
|
101
|
+
|
|
102
|
+
## Theme System
|
|
65
103
|
|
|
66
104
|
```tsx
|
|
67
105
|
import { StyleSheet, View } from "react-native";
|
|
@@ -75,11 +113,9 @@ export function Panel() {
|
|
|
75
113
|
return (
|
|
76
114
|
<View
|
|
77
115
|
style={[
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
borderColor: theme.colors.border,
|
|
82
|
-
},
|
|
116
|
+
{ backgroundColor: theme.colors.card, borderColor: theme.colors.border },
|
|
117
|
+
{ borderWidth: StyleSheet.hairlineWidth, borderRadius: spacing.radiusLg },
|
|
118
|
+
{ padding: spacing.cardPadding, gap: spacing.sm },
|
|
83
119
|
getShadowStyle("subtle"),
|
|
84
120
|
]}
|
|
85
121
|
>
|
|
@@ -90,27 +126,40 @@ export function Panel() {
|
|
|
90
126
|
</View>
|
|
91
127
|
);
|
|
92
128
|
}
|
|
93
|
-
|
|
94
|
-
const styles = StyleSheet.create({
|
|
95
|
-
panel: {
|
|
96
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
97
|
-
borderRadius: spacing.radiusLg,
|
|
98
|
-
padding: spacing.cardPadding,
|
|
99
|
-
gap: spacing.sm,
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
129
|
```
|
|
103
130
|
|
|
104
|
-
`useTheme()` returns the active `theme`, resolved `scheme`, persisted
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
`useTheme()` returns the active `theme`, resolved `scheme`, persisted
|
|
132
|
+
`currentTheme`, `setTheme`, `toggleTheme`, `getShadowStyle`,
|
|
133
|
+
`getFocusRingStyle(offset = 2)`, `getContrastingColor`, `getContrastRatio`,
|
|
134
|
+
`getTextColorForBackground`, and `withAlpha`. `getShadowStyle(type)` takes
|
|
135
|
+
`base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
|
|
136
|
+
`cardHover`, or `cardSubtle` and returns a cross-platform `boxShadow` (RN 0.85
|
|
137
|
+
and react-native-web 0.21 deprecate the legacy `shadow*` props).
|
|
138
|
+
|
|
139
|
+
Semantic tokens on `theme.colors`: `surfaceSunken`, `background`, `foreground`,
|
|
140
|
+
`card`, `cardForeground`, `popover`, `popoverForeground`, `text`, `textDim`,
|
|
141
|
+
`muted`, `mutedForeground`, `border`, `borderStrong`, `input`, `ring`,
|
|
142
|
+
`overlay`, `primary`, `secondary`, `accent`, their `*Foreground` pairs,
|
|
143
|
+
`destructive`, `success`, `warning`. `primary` is the neutral action color,
|
|
144
|
+
`secondary` a neutral secondary surface, `accent` the teal highlight, `input`
|
|
145
|
+
the default form-control border, `ring` the focus outline.
|
|
146
|
+
|
|
147
|
+
Surfaces are layered tiers, not shadow depths: `surfaceSunken` (app chrome such
|
|
148
|
+
as the Drawer rail) < `background` (content) < `card`/`popover` (raised panels)
|
|
149
|
+
< `muted` (chips, insets). `border` is the hairline on `background`/`card`; use
|
|
150
|
+
`borderStrong` on filled surfaces. `textDim`/`mutedForeground` hold at least
|
|
151
|
+
7:1 against `background`/`card` and 6:1 against `muted` in both schemes,
|
|
152
|
+
enforced by a package test.
|
|
153
|
+
|
|
154
|
+
When `currentTheme` is `"system"` the package tracks the OS color scheme and
|
|
155
|
+
updates every `useTheme()`/`useStyles()` consumer through the theme store. Do
|
|
156
|
+
not add app-level `Appearance` or `matchMedia` listeners for package
|
|
157
|
+
components. `THEME_STORAGE_KEY` (from `state`) is the persisted-preference key,
|
|
158
|
+
for a pre-boot theme script.
|
|
159
|
+
|
|
160
|
+
`useStyles()` memoizes theme-aware styles; its factory receives
|
|
161
|
+
`{ theme, spacing, withAlpha }` and its return also carries the `useTheme()`
|
|
162
|
+
helpers.
|
|
114
163
|
|
|
115
164
|
```tsx
|
|
116
165
|
import { useStyles } from "@mrmeg/expo-ui/hooks";
|
|
@@ -124,139 +173,89 @@ const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
|
124
173
|
}));
|
|
125
174
|
```
|
|
126
175
|
|
|
176
|
+
### Web theming is CSS variables
|
|
177
|
+
|
|
178
|
+
On web every `theme.colors.*` value resolves to a CSS custom property
|
|
179
|
+
(`var(--c-<kebab-token>)`), so styles built from the theme — including HTML
|
|
180
|
+
shells baked at export time — re-theme purely in CSS when `html[data-theme]`
|
|
181
|
+
changes. Native keeps literal values.
|
|
182
|
+
|
|
183
|
+
Consequence: hex-suffix alpha (`theme.colors.x + "15"`) does not work. Use
|
|
184
|
+
`withAlpha(theme.colors.x, 0.08)`, exported standalone from `hooks` as well as
|
|
185
|
+
from `useTheme()`.
|
|
186
|
+
|
|
187
|
+
`constants` exports the escape hatches:
|
|
188
|
+
|
|
189
|
+
| Export | Use |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `getThemeCssVariables(overrides?)` | The `--c-*` (and `--c-*-rgb`) definitions per scheme for a global `<style>` in `+html.tsx`; takes per-scheme brand overrides |
|
|
192
|
+
| `rawThemeColors.light` / `.dark` | Literal hex/rgba maps on every platform, for sinks that cannot take `var()` (e.g. `<meta name="theme-color">`) |
|
|
193
|
+
| `resolveRawColor(color, scheme)` | Maps a `var(--c-*)` color back to that scheme's literal; literals pass through |
|
|
194
|
+
|
|
195
|
+
The React Navigation maps (`colors.<scheme>.navigation`) stay literal on web
|
|
196
|
+
too, because @react-navigation parses them with a color library.
|
|
197
|
+
|
|
127
198
|
### Spacing and density
|
|
128
199
|
|
|
129
|
-
`spacing` exposes an 8px base scale (`xxs` 2, `xs` 4, `sm` 8, `smd` 12, `md`
|
|
200
|
+
`spacing` exposes an 8px base scale (`xxs` 2, `xs` 4, `sm` 8, `smd` 12, `md`
|
|
201
|
+
16, `mdl` 20, `lg` 24, `xl` 32, `xxl` 48, `xxxl` 64), radii (`radiusNone` 0,
|
|
202
|
+
`radiusXs` 4, `radiusSm` 8, `radiusMd` 12, `radiusLg` 14, `radiusXl` 18,
|
|
203
|
+
`radius2xl` 24, `radiusFull` 9999), icon sizes (`iconXs` 12, `iconSm` 16,
|
|
204
|
+
`iconMd` 24, `iconLg` 32, `iconXl` 48), and the semantic density tokens package
|
|
205
|
+
components read from. Prefer the semantic names so a density tune lands
|
|
206
|
+
everywhere at once:
|
|
130
207
|
|
|
131
208
|
| Token | Value | Use |
|
|
132
209
|
|-------|-------|-----|
|
|
133
|
-
| `screenPadding` | 16 | Horizontal gutter for screens, scroll content,
|
|
134
|
-
| `sectionSpacing` | 24 | Gap between grouped lists, cards,
|
|
135
|
-
| `cardPadding` | 16 | `Card`, `StatCard`,
|
|
210
|
+
| `screenPadding` | 16 | Horizontal gutter for screens, scroll content, landing blocks |
|
|
211
|
+
| `sectionSpacing` | 24 | Gap between grouped lists, cards, settings groups |
|
|
212
|
+
| `cardPadding` | 16 | `Card`, `StatCard`, any bordered panel |
|
|
136
213
|
| `dialogPadding` | 20 | `Dialog` and `AlertDialog` content |
|
|
137
214
|
| `rowPaddingY` / `rowPaddingX` | 10 / 16 | `Item` rows and hand-rolled list rows |
|
|
138
|
-
| `rowGap` | 12 | Gap between row media, content,
|
|
215
|
+
| `rowGap` | 12 | Gap between row media, content, actions |
|
|
139
216
|
| `rowMinHeight` | 40 | Visual row height on web; native rows keep `touchTarget` |
|
|
140
217
|
| `formRowMinHeight` | 32 | Checkbox and radio rows on web; native keeps `touchTarget` |
|
|
141
218
|
| `touchTarget` | 44 | Minimum native hit area; pair a smaller visual height with `hitSlop` |
|
|
142
219
|
|
|
143
|
-
Controls
|
|
220
|
+
Controls size themselves from their own `size` prop and ignore these tokens:
|
|
221
|
+
`Button` 28/32/40 (`sm`/`md`/`lg`), `TextInput` and `Select` 32/36/40
|
|
222
|
+
(`sm`/`md`/`lg`), `Toggle` 32/36/40 (`sm`/`default`/`lg`), `Tabs` 32/36
|
|
223
|
+
(`sm`/`md`).
|
|
144
224
|
|
|
145
225
|
### Color overrides
|
|
146
226
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
3. **Scoped override** (`ThemeColorScope`) — a per-subtree palette layered on
|
|
153
|
-
top of the brand, for transient theming that should not leak globally.
|
|
154
|
-
|
|
155
|
-
With no override at either layer, `useTheme()` returns the base theme by
|
|
156
|
-
reference — identical to pre-override behavior, with no extra allocation.
|
|
157
|
-
|
|
158
|
-
Forward your app's brand palette once (e.g. from a top-level effect). Each
|
|
159
|
-
scheme is a `Partial<ThemeColors>`, so you only specify the keys you want to
|
|
160
|
-
re-skin:
|
|
227
|
+
Colors resolve in three layers, last wins: package defaults (zinc/teal) →
|
|
228
|
+
global brand (`setColors`) → subtree scope (`ThemeColorScope`). Each override is
|
|
229
|
+
`{ light?, dark? }` of `Partial<ThemeColors>`, so you only pass the keys you
|
|
230
|
+
re-skin. With no override at either layer, `useTheme()` returns the base theme
|
|
231
|
+
by reference — no extra allocation.
|
|
161
232
|
|
|
162
233
|
```tsx
|
|
163
|
-
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
234
|
+
import { ThemeColorScope, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
164
235
|
|
|
236
|
+
// One app-wide brand palette.
|
|
165
237
|
useThemeStore.getState().setColors({
|
|
166
238
|
light: { primary: "#7c3aed", accent: "#14b8a6" },
|
|
167
239
|
dark: { primary: "#a78bfa", accent: "#2dd4bf" },
|
|
168
240
|
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Use `ThemeColorScope` to override colors for one subtree without touching the
|
|
172
|
-
global theme — a survey with a user-created palette, a preview pane, an embed.
|
|
173
|
-
It is React context, so it applies only inside the provider and unwinds when
|
|
174
|
-
that subtree unmounts. Nested scopes compose (inner keys win, outer fill in),
|
|
175
|
-
and a scoped key wins over the global brand for components inside it:
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
import { ThemeColorScope } from "@mrmeg/expo-ui/state";
|
|
179
241
|
|
|
242
|
+
// Per-subtree palette that must not leak globally: user-created themes,
|
|
243
|
+
// preview panes, embeds.
|
|
180
244
|
<ThemeColorScope colors={{ light: surveyColors, dark: surveyColors }}>
|
|
181
|
-
{/* every package component in here renders with the survey palette */}
|
|
182
245
|
<SurveyScreen />
|
|
183
246
|
</ThemeColorScope>
|
|
184
247
|
```
|
|
185
248
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
monospace (mono). Most apps ship their own faces — forward them with
|
|
190
|
-
`setFonts`, the font counterpart to `setColors`. Call it once at startup,
|
|
191
|
-
after your fonts are registered:
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
195
|
-
|
|
196
|
-
useThemeStore.getState().setFonts({
|
|
197
|
-
families: {
|
|
198
|
-
sansSerif: {
|
|
199
|
-
regular: "HankenGrotesk_400Regular",
|
|
200
|
-
medium: "HankenGrotesk_500Medium",
|
|
201
|
-
semibold: "HankenGrotesk_600SemiBold",
|
|
202
|
-
bold: "HankenGrotesk_700Bold",
|
|
203
|
-
},
|
|
204
|
-
serif: {
|
|
205
|
-
regular: "Newsreader_400Regular",
|
|
206
|
-
semibold: "Newsreader_600SemiBold",
|
|
207
|
-
},
|
|
208
|
-
mono: {
|
|
209
|
-
regular: "JetBrainsMono_400Regular",
|
|
210
|
-
medium: "JetBrainsMono_500Medium",
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
webWeightStrategy: "family",
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Overrides are partial at both levels:
|
|
218
|
-
|
|
219
|
-
- **Per group** — pass `sansSerif` alone and `serif`/`mono` keep the package
|
|
220
|
-
defaults.
|
|
221
|
-
- **Per weight** — within an overridden group, weights you don't provide fall
|
|
222
|
-
back to that group's `regular` (never to a package face), so registering
|
|
223
|
-
only Regular + Medium is enough.
|
|
224
|
-
|
|
225
|
-
`setFonts({})` clears back to the defaults.
|
|
226
|
-
|
|
227
|
-
When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
228
|
-
Inter faces entirely (native `.ttf`s and the web Google-Fonts stylesheet) —
|
|
229
|
-
your app owns loading its own families, and nothing would reference Inter.
|
|
230
|
-
For the skip to apply, call `setFonts` before the hook mounts (module scope,
|
|
231
|
-
or ahead of rendering the root); a later call still re-skins all text, it
|
|
232
|
-
just doesn't prevent the Inter download. The Feather icon font always loads.
|
|
233
|
-
|
|
234
|
-
**Pick the right `webWeightStrategy`** — this is the one non-obvious part:
|
|
235
|
-
|
|
236
|
-
| Strategy | Use when | Effect |
|
|
237
|
-
|---|---|---|
|
|
238
|
-
| `"numeric"` (default) | You ship **one multi-weight CSS family** (a variable font, or Google Fonts' single `"Inter"` family) | A numeric `fontWeight` selects the `@font-face` variant |
|
|
239
|
-
| `"family"` | You load **per-weight faces via `expo-font`** | The family name alone carries the weight; the numeric `fontWeight` is suppressed |
|
|
240
|
-
|
|
241
|
-
If you load fonts through `expo-font` / `@expo-google-fonts`, choose
|
|
242
|
-
`"family"`. `expo-font` registers each weight as its own single-face family on
|
|
243
|
-
web as well as native, so emitting a numeric weight on top makes the browser
|
|
244
|
-
synthesise a *second* layer of bold over an already-bold face.
|
|
245
|
-
|
|
246
|
-
Native always behaves as `"family"` regardless of this setting.
|
|
247
|
-
|
|
248
|
-
> Prefer `setFonts` over patching `node_modules`. Bun keys
|
|
249
|
-
> `patchedDependencies` to an exact `name@version`, so a font patch silently
|
|
250
|
-
> stops applying the next time you bump the package — with no warning.
|
|
249
|
+
`ThemeColorScope` is React context: it applies only inside the provider and
|
|
250
|
+
unwinds when that subtree unmounts. Nested scopes compose (inner keys win,
|
|
251
|
+
outer fill in), and a scoped key beats the global brand inside it.
|
|
251
252
|
|
|
252
253
|
### Shape overrides
|
|
253
254
|
|
|
254
|
-
`setShape` is the geometry counterpart
|
|
255
|
-
|
|
255
|
+
`setShape` is the geometry counterpart, grouped per component. It currently
|
|
256
|
+
covers Button:
|
|
256
257
|
|
|
257
258
|
```tsx
|
|
258
|
-
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
259
|
-
|
|
260
259
|
useThemeStore.getState().setShape({
|
|
261
260
|
button: {
|
|
262
261
|
borderRadius: 9999, // pill buttons everywhere; package default is 12
|
|
@@ -265,11 +264,11 @@ useThemeStore.getState().setShape({
|
|
|
265
264
|
});
|
|
266
265
|
```
|
|
267
266
|
|
|
268
|
-
Precedence is
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
Precedence is caller-wins: a per-instance `style={{ borderRadius }}` or
|
|
268
|
+
`withShadow` prop beats the global override, which beats the package default.
|
|
269
|
+
`setShape({})` clears back to the defaults.
|
|
271
270
|
|
|
272
|
-
|
|
271
|
+
## Typography
|
|
273
272
|
|
|
274
273
|
```tsx
|
|
275
274
|
import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/components";
|
|
@@ -280,22 +279,26 @@ import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/c
|
|
|
280
279
|
<StyledText semantic="label" tx="common.email" />
|
|
281
280
|
```
|
|
282
281
|
|
|
283
|
-
|
|
282
|
+
`StyledText` props:
|
|
284
283
|
|
|
285
|
-
- `semantic`: `title`, `heading`, `subheading`, `body`, `caption`, `label`
|
|
284
|
+
- `semantic`: `title`, `heading`, `subheading`, `body`, `caption`, `label`, `eyebrow`
|
|
286
285
|
- `size`: `xs`, `sm`, `base`, `body`, `lg`, `xl`, `xxl`, `display`
|
|
287
286
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
288
|
-
- `variant`: `sansSerif`, `serif`, `mono`
|
|
289
|
-
|
|
290
|
-
- `
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
287
|
+
- `variant`: `sansSerif`, `serif`, `mono`
|
|
288
|
+
- `align`, `text`, `tx`, `txOptions`
|
|
289
|
+
- `selectable`: defaults to `true`; package controls disable it for labels and
|
|
290
|
+
interactive chrome where accidental drag selection would feel broken.
|
|
291
|
+
|
|
292
|
+
Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
293
|
+
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText` (code, IDs,
|
|
294
|
+
tabular figures), `SerifText`, `SansSerifText`, `SerifBoldText`,
|
|
295
|
+
`SansSerifBoldText`. `TextClassContext`, `TextColorContext`,
|
|
296
|
+
`TextStyleContext`, and `TextSelectabilityContext` are advanced exports package
|
|
297
|
+
controls use to pass nested text styling and control-label selectability.
|
|
298
|
+
|
|
299
|
+
`tx` is opt-in. Without a configured translator it renders its fallback text if
|
|
300
|
+
provided, otherwise the key; package-owned defaults such as notification titles
|
|
301
|
+
use readable fallbacks. Connect an existing i18n setup once at startup:
|
|
299
302
|
|
|
300
303
|
```tsx
|
|
301
304
|
import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
|
|
@@ -304,87 +307,223 @@ import { i18n } from "./i18n";
|
|
|
304
307
|
configureExpoUiI18n((key, options) => i18n.t(key, options));
|
|
305
308
|
```
|
|
306
309
|
|
|
310
|
+
### Bundled fonts
|
|
311
|
+
|
|
312
|
+
Inter is the sans face on every platform, in four static weights
|
|
313
|
+
(`Inter_400Regular`, `Inter_500Medium`, `Inter_600SemiBold`, `Inter_700Bold`)
|
|
314
|
+
from the bundled `@expo-google-fonts/inter`. Serif is Georgia; mono is the
|
|
315
|
+
platform system monospace.
|
|
316
|
+
|
|
317
|
+
`useResources()` loads those four weights plus `Feather.font` (from the
|
|
318
|
+
package-managed `@expo/vector-icons`) on native, so `StyledText`'s
|
|
319
|
+
`light`–`bold` range resolves to real files instead of a faked OS bold. On web
|
|
320
|
+
it injects one Google Fonts Inter stylesheet (all four weights) after hydration
|
|
321
|
+
unless the app already added it; web weight differentiation then comes from a
|
|
322
|
+
numeric `fontWeight` on the shared `"Inter"` family. For better first paint in
|
|
323
|
+
Expo Router web apps, add the links to app-owned `app/+html.tsx`:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
327
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
328
|
+
<link
|
|
329
|
+
id="mrmeg-expo-ui-inter"
|
|
330
|
+
rel="stylesheet"
|
|
331
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
332
|
+
/>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Font overrides
|
|
336
|
+
|
|
337
|
+
Forward your own faces with `setFonts`, once at startup after they are
|
|
338
|
+
registered:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
useThemeStore.getState().setFonts({
|
|
342
|
+
families: {
|
|
343
|
+
sansSerif: {
|
|
344
|
+
regular: "HankenGrotesk_400Regular",
|
|
345
|
+
medium: "HankenGrotesk_500Medium",
|
|
346
|
+
semibold: "HankenGrotesk_600SemiBold",
|
|
347
|
+
bold: "HankenGrotesk_700Bold",
|
|
348
|
+
},
|
|
349
|
+
serif: { regular: "Newsreader_400Regular", semibold: "Newsreader_600SemiBold" },
|
|
350
|
+
mono: { regular: "JetBrainsMono_400Regular", medium: "JetBrainsMono_500Medium" },
|
|
351
|
+
},
|
|
352
|
+
webWeightStrategy: "family",
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Overrides are partial at both levels: pass `sansSerif` alone and `serif`/`mono`
|
|
357
|
+
keep the package defaults, and within an overridden group any weight you omit
|
|
358
|
+
falls back to that group's `regular` (never to a package face), so Regular +
|
|
359
|
+
Medium is enough. `setFonts({})` clears back to the defaults.
|
|
360
|
+
|
|
361
|
+
When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
362
|
+
Inter faces entirely (native `.ttf`s and the web stylesheet). For the skip to
|
|
363
|
+
apply, call `setFonts` before the hook mounts (module scope, or ahead of
|
|
364
|
+
rendering the root); a later call still re-skins all text, it just doesn't
|
|
365
|
+
prevent the download. Feather always loads.
|
|
366
|
+
|
|
367
|
+
| `webWeightStrategy` | Use when | Effect |
|
|
368
|
+
|---|---|---|
|
|
369
|
+
| `"numeric"` (default) | You ship **one multi-weight CSS family** (a variable font, or Google Fonts' single `"Inter"` family) | A numeric `fontWeight` selects the `@font-face` variant |
|
|
370
|
+
| `"family"` | You load **per-weight faces via `expo-font`** | The family name alone carries the weight; the numeric `fontWeight` is suppressed |
|
|
371
|
+
|
|
372
|
+
`expo-font` registers each weight as its own single-face family on web as well
|
|
373
|
+
as native, so emitting a numeric weight on top makes the browser synthesise a
|
|
374
|
+
*second* bold layer over an already-bold face. Native always behaves as
|
|
375
|
+
`"family"`.
|
|
376
|
+
|
|
377
|
+
> Prefer `setFonts` over patching `node_modules`. Bun keys
|
|
378
|
+
> `patchedDependencies` to an exact `name@version`, so a font patch silently
|
|
379
|
+
> stops applying the next time you bump the package.
|
|
380
|
+
|
|
307
381
|
## Component Guide
|
|
308
382
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
| Component | Use For |
|
|
314
|
-
|
|
315
|
-
| `Accordion
|
|
316
|
-
| `Alert` | Cross-platform imperative alerts |
|
|
317
|
-
| `AnimatedView` | Entrance and visibility animation |
|
|
318
|
-
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback |
|
|
319
|
-
| `Badge` | Short status labels |
|
|
320
|
-
| `BottomSheet` | Mobile-first modal sheets |
|
|
321
|
-
| `Button` | Commands and CTAs |
|
|
322
|
-
| `Card
|
|
323
|
-
| `Carousel` | Horizontally snapping
|
|
324
|
-
| `Checkbox` | Boolean selection |
|
|
325
|
-
| `Collapsible
|
|
326
|
-
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content |
|
|
327
|
-
| `DismissKeyboard` | Tap-away keyboard dismissal |
|
|
328
|
-
| `Drawer` | Side panels and drawer navigation |
|
|
329
|
-
| `DropdownMenu` | Menus and command lists |
|
|
330
|
-
| `EmptyState` | No-data or recoverable error regions |
|
|
331
|
-
| `ErrorBoundary` | React render error fallback |
|
|
332
|
-
| `Icon` | Feather or custom icons
|
|
333
|
-
| `InputOTP` | Verification code entry |
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
383
|
+
Every component is exported from `@mrmeg/expo-ui/components`, and from
|
|
384
|
+
`@mrmeg/expo-ui/components/<Name>` for a direct import. Check this table before
|
|
385
|
+
building a new primitive.
|
|
386
|
+
|
|
387
|
+
| Component | Use For | Examples |
|
|
388
|
+
|-----------|---------|----------|
|
|
389
|
+
| `Accordion` | Multi-section disclosure | FAQ lists, grouped settings, help sections |
|
|
390
|
+
| `Alert` | Cross-platform imperative alerts | Destructive confirms, blocking messages |
|
|
391
|
+
| `AnimatedView` | Entrance and visibility animation | Staggered rows, revealed panels |
|
|
392
|
+
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback | Account menu, comment authors, overlapping team stacks |
|
|
393
|
+
| `Badge` | Short status labels | Draft/active state, counts, plan and role tags |
|
|
394
|
+
| `BottomSheet` | Mobile-first modal sheets | Action pickers, mobile filters, quick edit forms |
|
|
395
|
+
| `Button` | Commands and CTAs | Submit, save, delete, navigation CTAs; loading state keeps the resting width |
|
|
396
|
+
| `Card` | Framed content groups | List items, pricing plans, settings sections, dashboards |
|
|
397
|
+
| `Carousel` | Horizontally snapping slides with pressable dots | Testimonials, onboarding pages, image galleries |
|
|
398
|
+
| `Checkbox` | Boolean selection | Terms consent, checklists, multi-select filters |
|
|
399
|
+
| `Collapsible` | One-off disclosure | Advanced settings, hidden helper text |
|
|
400
|
+
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Confirm delete, edit profile, blocking warnings |
|
|
401
|
+
| `DismissKeyboard` | Tap-away keyboard dismissal | Forms, search and sign-in screens |
|
|
402
|
+
| `Drawer` | Side panels and drawer navigation | Filter drawer, app navigation, inspector panel |
|
|
403
|
+
| `DropdownMenu` | Menus and command lists | Row actions, account menu, sort menu, checkbox/radio groups |
|
|
404
|
+
| `EmptyState` | No-data or recoverable error regions | Empty inbox, no results, failed list load |
|
|
405
|
+
| `ErrorBoundary` | React render error fallback | Route-level and feature-level fallbacks |
|
|
406
|
+
| `Icon` | Feather or custom icons on theme tokens | Button accessories, menu icons, status glyphs |
|
|
407
|
+
| `InputOTP` | Verification code entry | Email/SMS codes, MFA, invite codes |
|
|
408
|
+
| `Item` | List and settings rows on the density tokens | Settings lists, inbox rows, pickers, detail rows |
|
|
409
|
+
| `KeyboardAvoidingView` | Native keyboard-aware layout | Screen roots, composer footers, form-heavy subtrees |
|
|
410
|
+
| `Label` | Accessible form labels | Required, disabled, and group labels |
|
|
411
|
+
| `MaxWidthContainer` | Centered responsive width | Web pages, tablet layouts, auth panels |
|
|
412
|
+
| `Notification` | Global toast surface | Saved/error/sync toasts, action toasts, loading toast |
|
|
413
|
+
| `Popover` | Anchored contextual content | Inline help, quick previews, small forms |
|
|
414
|
+
| `Progress` | Determinate or indeterminate progress | Upload progress, onboarding completion |
|
|
415
|
+
| `RadioGroup` | Mutually exclusive choices | Plan interval, visibility choice, survey answer |
|
|
416
|
+
| `SectionHeader` | Eyebrow / title / description section intro | Landing sections, settings groups, report headers |
|
|
417
|
+
| `SegmentedControl` | Native segmented picker (`@expo/ui`) | Platform-native view switchers, iOS-style filters |
|
|
418
|
+
| `Select` | Option menus | Country, category, status pickers; `label` drives default item text |
|
|
419
|
+
| `Separator` | Horizontal or vertical dividers | Menu, section, and card dividers |
|
|
420
|
+
| `Skeleton` | Loading placeholders | List, profile card, dashboard loading |
|
|
421
|
+
| `Slider` | Numeric value selection (`@expo/ui`) | Volume, percentage, rating, threshold |
|
|
422
|
+
| `StatCard` | Metric tile: `label`, `value`, `unit`, `change`, `icon`, `onPress` | KPI rows, analytics summaries, usage meters |
|
|
423
|
+
| `StatusBar` | Theme-aware native status bar | Root layout status styling |
|
|
424
|
+
| `StyledText` and aliases | Theme-aware typography | Titles, labels, body copy, captions, translated text |
|
|
425
|
+
| `Switch` | Binary settings | Notification, privacy, and feature toggles |
|
|
426
|
+
| `Tabs` | In-page tabbed views | Profile sections, report views, settings categories |
|
|
427
|
+
| `TextInput` | Text entry with label, helper/error text, clear button, password reveal, numeric filtering, left/right elements | Email/password, search, multiline notes |
|
|
428
|
+
| `Toggle` | Pressed/unpressed control | Favorite, mute, bold/italic, view mode |
|
|
429
|
+
| `ToggleGroup` | Single or multi toggle groups | Alignment, formatting toolbar, filter chips |
|
|
430
|
+
| `Tooltip` | Short hover/focus help | Icon button labels, field hints, disabled-action reasons |
|
|
431
|
+
|
|
432
|
+
### Compound Parts
|
|
357
433
|
|
|
358
434
|
| Root | Exported Parts |
|
|
359
435
|
|------|----------------|
|
|
360
436
|
| `Accordion` | `AccordionItem`, `AccordionTrigger`, `AccordionContent` |
|
|
361
|
-
| `AlertDialog` | `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel` |
|
|
437
|
+
| `AlertDialog` | `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel`, plus `DialogHeader`/`DialogFooter` (as `AlertDialog.Header`/`.Footer`) |
|
|
438
|
+
| `Avatar` | `AvatarGroup`, `getAvatarInitials` |
|
|
362
439
|
| `BottomSheet` | `BottomSheetTrigger`, `BottomSheetContent`, `BottomSheetHandle`, `BottomSheetHeader`, `BottomSheetBody`, `BottomSheetFooter`, `BottomSheetClose` |
|
|
440
|
+
| `Button` | `ButtonText`, `ButtonIcon` |
|
|
363
441
|
| `Card` | `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` |
|
|
364
442
|
| `Collapsible` | `CollapsibleTrigger`, `CollapsibleContent` |
|
|
365
443
|
| `Dialog` | `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter`, `DialogTitle`, `DialogDescription`, `DialogClose` |
|
|
366
444
|
| `Drawer` | `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerBody`, `DrawerFooter`, `DrawerClose`, `DrawerToggleCollapse` |
|
|
367
445
|
| `DropdownMenu` | `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuLabel`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuPortal`, `DropdownMenuSub`, `DropdownMenuSubTrigger`, `DropdownMenuSubContent` |
|
|
446
|
+
| `Item` | `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions` |
|
|
368
447
|
| `Popover` | `PopoverTrigger`, `PopoverContent`, `PopoverHeader`, `PopoverBody`, `PopoverFooter` |
|
|
369
448
|
| `RadioGroup` | `RadioGroupItem` |
|
|
370
449
|
| `Select` | `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `SelectGroup`, `SelectLabel`, `SelectSeparator` |
|
|
371
450
|
| `Skeleton` | `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` |
|
|
372
|
-
| `Tabs` | `TabsList`, `TabsTrigger`, `TabsContent` |
|
|
451
|
+
| `Tabs` | `TabsList`, `TabsTrigger`, `TabsContent`, `TabsTriggerText` (`Tabs.Trigger.Text`) |
|
|
373
452
|
| `Toggle` | `ToggleIcon` |
|
|
374
453
|
| `ToggleGroup` | `ToggleGroupItem`, `ToggleGroupIcon` |
|
|
375
454
|
| `Tooltip` | `TooltipTrigger`, `TooltipContent`, `TooltipBody` |
|
|
376
455
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
456
|
+
Every part above has a named export. Dot notation on the root exists only for
|
|
457
|
+
`AlertDialog`, `BottomSheet`, `Button`, `Dialog`, `Drawer`, `Popover`,
|
|
458
|
+
`RadioGroup`, `Select`, `Tabs`, and `Tooltip`; `Accordion`, `Avatar`, `Card`,
|
|
459
|
+
`Collapsible`, `DropdownMenu`, `Item`, `Skeleton`, `Toggle`, and `ToggleGroup`
|
|
460
|
+
are named exports only.
|
|
461
|
+
|
|
462
|
+
### Hooks
|
|
463
|
+
|
|
464
|
+
| Hook | Returns |
|
|
465
|
+
|------|---------|
|
|
466
|
+
| `useResources()` | `{ loaded, error }` — loads Feather plus the Inter faces |
|
|
467
|
+
| `useTheme()` | Active theme, scheme helpers, shadow/contrast/alpha helpers |
|
|
468
|
+
| `useStyles(factory)` | Memoized theme-aware styles plus the `useTheme()` helpers |
|
|
469
|
+
| `useDimensions()` | `{ width, height, orientation, isSmallScreen, isMediumScreen, isLargeScreen }` against `SCREEN_SIZES` (768 / 1000 / 1200) |
|
|
470
|
+
| `useFontStyle(weight?, variant?)` | `{ fontFamily, fontWeight? }` resolved through `setFonts` overrides |
|
|
471
|
+
| `useReducedMotion()` | `true` when the OS asks for reduced motion |
|
|
472
|
+
| `useScalePress(options?)` | Animated style plus `onPressIn`/`onPressOut` for a `Pressable` (`scaleTo`, `haptic`, `disabled`) |
|
|
473
|
+
| `useStaggeredEntrance(options?)` | Entrance animated style for list rows (`delay`; `STAGGER_DELAY` is 30) |
|
|
474
|
+
|
|
475
|
+
`SsrViewportContext` (from `state`) supplies the first-render viewport width on
|
|
476
|
+
web, where the window cannot be read during export or hydration.
|
|
477
|
+
|
|
478
|
+
### Patterns And Gotchas
|
|
479
|
+
|
|
480
|
+
- Use `Button.preset`, not `variant`: `default` is the neutral primary action,
|
|
481
|
+
`secondary` a neutral secondary surface, `outline` lower emphasis, `ghost`
|
|
482
|
+
compact toolbars, `link` text-like commands, `destructive` dangerous actions.
|
|
483
|
+
Native targets add computed hit slop up to 44px. Nested `StyledText` inherits
|
|
484
|
+
the Button size, so use `size="sm"` for popover, tooltip, and toolbar
|
|
485
|
+
triggers.
|
|
486
|
+
- Pair a standalone `Label` with its control using two DISTINCT ids: `nativeID`
|
|
487
|
+
is the label's own id, `htmlFor` is the input's id (`<Label
|
|
488
|
+
nativeID="email-label" htmlFor="email-input">` + `<TextInput
|
|
489
|
+
nativeID="email-input" />`). One id on both renders duplicate ids on web and
|
|
490
|
+
associates nothing. Prefer `TextInput`'s own `label` prop when no separate
|
|
491
|
+
label element is needed.
|
|
492
|
+
- `BottomSheet` renders the platform's native sheet through `@expo/ui`: iOS
|
|
493
|
+
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`. The
|
|
494
|
+
platform owns gestures and keyboard avoidance, so `swipeEnabled`,
|
|
495
|
+
`avoidKeyboard`, and `dismissKeyboardOnDrag` are accepted for call-site
|
|
496
|
+
ergonomics but have no effect; `BottomSheet.Content` does mount its own
|
|
497
|
+
tap-away keyboard-dismiss overlay while a field is focused. Android has only
|
|
498
|
+
two snap states (partial / expanded) and maps extra snap points to the
|
|
499
|
+
nearest. `BottomSheet.Content` also takes `backgroundStyle`, merged over the
|
|
500
|
+
themed card default on the native sheet surface (web panel, Android
|
|
501
|
+
`containerColor`, iOS `presentationBackground`) — pass
|
|
502
|
+
`{ backgroundColor: "transparent" }`, plus a `style` clearing the content
|
|
503
|
+
column's card fill, to let custom chrome such as a glass backdrop show
|
|
504
|
+
through.
|
|
505
|
+
- `Carousel` renders every child (no virtualization), so slides survive into
|
|
506
|
+
the exported HTML shell and the first client frame; use `FlatList` for large
|
|
507
|
+
or unbounded data. An `itemWidth` below 1 (default `0.85`) is a fraction of
|
|
508
|
+
the carousel width so the next slide peeks, but that fraction measures the
|
|
509
|
+
viewport until first layout — pass absolute pixels (`> 1`) inside a
|
|
510
|
+
constrained parent.
|
|
511
|
+
- Pass `Avatar` both a `source` and a `name` whenever you have them: `name`
|
|
512
|
+
supplies the initials shown while the image is missing, loading, or failed,
|
|
513
|
+
plus the default accessibility label. `SkeletonAvatar` is the loading
|
|
514
|
+
placeholder, not a substitute. Inside `AvatarGroup`, children inherit the
|
|
515
|
+
group's `size`/`shape` and gain the ring, so set those on the group; the
|
|
516
|
+
group announces its count as a hidden summary and leaves each member
|
|
517
|
+
individually announceable, so do not wrap it in your own `accessible`
|
|
518
|
+
container.
|
|
519
|
+
- `Drawer.Header` takes `icon`, `title`, and `action` slots for a compact
|
|
520
|
+
app-brand row; a string `title` uses the package typography, and
|
|
521
|
+
`Drawer.ToggleCollapse` in `action` gives a trailing rail control.
|
|
522
|
+
`Drawer.Content` owns safe-area top/bottom padding — do not duplicate it in
|
|
523
|
+
children.
|
|
387
524
|
```tsx
|
|
525
|
+
import { Drawer, Icon } from "@mrmeg/expo-ui/components";
|
|
526
|
+
|
|
388
527
|
<Drawer.Header
|
|
389
528
|
icon={<Icon name="hexagon" color="accent" />}
|
|
390
529
|
title="Acme"
|
|
@@ -396,18 +535,8 @@ Use `StyledText` or its aliases instead of raw `Text` whenever the text is part
|
|
|
396
535
|
/>
|
|
397
536
|
```
|
|
398
537
|
|
|
399
|
-
Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `UIProvider` also wraps app content in the package keyboard-avoiding root by default, so ordinary screens and fixed footers stay above the soft keyboard without repeated app-local wrappers; pass `keyboardAvoiding={false}` to opt out, or use `KeyboardAvoidingView` directly for a subtree with custom behavior. Web skips the root keyboard wrapper unless `keyboardAvoiding` is explicitly enabled. `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. `BottomSheet.Content` also accepts `backgroundStyle`, merged over the themed card default on the native sheet surface (web panel, Android `containerColor`, iOS `presentationBackground`) — pass `{ backgroundColor: "transparent" }`, plus a `style` clearing the content column's card fill, to let custom chrome like a glass backdrop show through. Trigger transient feedback with `notify`.
|
|
400
|
-
|
|
401
|
-
Use `Skeleton` components for loading content with stable dimensions, `EmptyState` for no-data/recoverable errors, `Alert` for blocking confirm/alert dialogs, and `Notification` for transient global feedback.
|
|
402
|
-
|
|
403
|
-
Use `Toggle` for one pressed state, `ToggleGroup` for a related set of pressed states, `Switch` for binary settings, `RadioGroup` for small mutually exclusive choices, and `Select` for longer option sets.
|
|
404
|
-
|
|
405
|
-
Pass `Avatar` both a `source` and a `name` whenever you have them: `name` supplies the initials shown while the image is missing, still loading, or after it fails, and the default accessibility label. `SkeletonAvatar` is the loading placeholder, not a substitute. Inside `AvatarGroup`, child avatars inherit the group's `size` and `shape` and gain the ring, so set those on the group rather than per child. `AvatarGroup` announces its count as a hidden summary and leaves each member individually announceable, so don't wrap it in an `accessible` container of your own.
|
|
406
|
-
|
|
407
538
|
### Quick Examples
|
|
408
539
|
|
|
409
|
-
Card, badge, and CTA:
|
|
410
|
-
|
|
411
540
|
```tsx
|
|
412
541
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@mrmeg/expo-ui/components";
|
|
413
542
|
|
|
@@ -424,10 +553,8 @@ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@mrmeg/
|
|
|
424
553
|
</Card>
|
|
425
554
|
```
|
|
426
555
|
|
|
427
|
-
Form controls:
|
|
428
|
-
|
|
429
556
|
```tsx
|
|
430
|
-
import { Button,
|
|
557
|
+
import { Button, Switch, TextInput } from "@mrmeg/expo-ui/components";
|
|
431
558
|
|
|
432
559
|
<TextInput
|
|
433
560
|
label="Email"
|
|
@@ -444,8 +571,6 @@ import { Button, TextInput, Switch } from "@mrmeg/expo-ui/components";
|
|
|
444
571
|
</Button>
|
|
445
572
|
```
|
|
446
573
|
|
|
447
|
-
Feedback:
|
|
448
|
-
|
|
449
574
|
```tsx
|
|
450
575
|
import { EmptyState, Progress, SkeletonCard } from "@mrmeg/expo-ui/components";
|
|
451
576
|
import { notify } from "@mrmeg/expo-ui/state";
|
|
@@ -456,121 +581,66 @@ import { notify } from "@mrmeg/expo-ui/state";
|
|
|
456
581
|
|
|
457
582
|
notify.success("Saved", {
|
|
458
583
|
messages: ["Your changes were saved."],
|
|
459
|
-
action: {
|
|
460
|
-
label: "View",
|
|
461
|
-
onPress: openSavedItem,
|
|
462
|
-
},
|
|
584
|
+
action: { label: "View", onPress: openSavedItem },
|
|
463
585
|
});
|
|
464
586
|
```
|
|
465
587
|
|
|
466
|
-
|
|
588
|
+
`notify` also exposes `error`, `warning`, `info`, `loading`, `hide`, and
|
|
589
|
+
`promise(promise, { loading, success, error })`. Notifications auto-dismiss
|
|
590
|
+
after 4s (`DEFAULT_NOTIFICATION_DURATION`) unless a `duration` is given;
|
|
591
|
+
`duration: 0` keeps one up until dismissed and `notify.loading` never
|
|
592
|
+
auto-dismisses. `position` is `"top"` (default) or `"bottom"`. `globalUIStore`
|
|
593
|
+
stays available for reactive selectors and tests.
|
|
467
594
|
|
|
468
|
-
|
|
469
|
-
- Use `useTheme()` and semantic tokens instead of hardcoded colors.
|
|
470
|
-
- Use `Button.preset`, not `variant`, for buttons.
|
|
471
|
-
- Mount `UIProvider` before using overlays, menus, select content, popovers, tooltips, or package notifications.
|
|
472
|
-
- Import from package exports, not `packages/ui/src/*`.
|
|
473
|
-
|
|
474
|
-
## App Startup
|
|
475
|
-
|
|
476
|
-
Call `useResources()` once near the Expo app root before hiding the splash screen:
|
|
477
|
-
|
|
478
|
-
```tsx
|
|
479
|
-
import { ThemeProvider } from "expo-router";
|
|
480
|
-
import { colors } from "@mrmeg/expo-ui/constants";
|
|
481
|
-
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
482
|
-
import { UIProvider } from "@mrmeg/expo-ui/components";
|
|
483
|
-
|
|
484
|
-
export default function RootLayout() {
|
|
485
|
-
const { scheme } = useTheme();
|
|
486
|
-
const { loaded } = useResources();
|
|
487
|
-
|
|
488
|
-
if (!loaded) return null;
|
|
489
|
-
|
|
490
|
-
return (
|
|
491
|
-
<ThemeProvider
|
|
492
|
-
value={{
|
|
493
|
-
dark: colors[scheme ?? "light"].dark,
|
|
494
|
-
colors: colors[scheme ?? "light"].navigation,
|
|
495
|
-
fonts: colors[scheme ?? "light"].fonts,
|
|
496
|
-
}}
|
|
497
|
-
>
|
|
498
|
-
<UIProvider>
|
|
499
|
-
{/* App navigation goes here. */}
|
|
500
|
-
</UIProvider>
|
|
501
|
-
</ThemeProvider>
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
## Fonts
|
|
507
|
-
|
|
508
|
-
Typography is Inter across every platform, in four static weights (`Inter_400Regular`, `Inter_500Medium`, `Inter_600SemiBold`, `Inter_700Bold`) from `@expo-google-fonts/inter` — a package dependency, so consumers get it automatically.
|
|
509
|
-
|
|
510
|
-
On native, `useResources()` loads the four static weights so `StyledText`'s `light`–`bold` range resolves to real font files instead of a faked OS bold. On web, `useResources()` injects a single Google Fonts Inter stylesheet (covering all four weights) after hydration if the app has not already added it; weight differentiation on web comes from a numeric `fontWeight` alongside the shared `"Inter"` CSS family. For better first paint in Expo Router web apps, add the links in app-owned `app/+html.tsx`:
|
|
511
|
-
|
|
512
|
-
```tsx
|
|
513
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
514
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
515
|
-
<link
|
|
516
|
-
id="mrmeg-expo-ui-inter"
|
|
517
|
-
rel="stylesheet"
|
|
518
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
519
|
-
/>
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
`useResources()` also loads `Feather.font` from the package-managed `@expo/vector-icons` dependency for icon rendering.
|
|
523
|
-
|
|
524
|
-
## Package Checks
|
|
525
|
-
|
|
526
|
-
For a one-command release from the repo root, use:
|
|
595
|
+
## Package Release
|
|
527
596
|
|
|
528
597
|
```sh
|
|
529
598
|
bun run ui:release -- --patch --publish
|
|
530
599
|
```
|
|
531
600
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
601
|
+
Use `--patch`, `--minor`, `--major`, or an exact version such as `0.2.0`. The
|
|
602
|
+
command updates `packages/ui/package.json` and `bun.lock`, runs
|
|
603
|
+
`bun run packages:peer-check` then the `ui:typecheck`, `ui:test`, `ui:build`,
|
|
604
|
+
`ui:pack`, and `ui:consumer-smoke` gates, and publishes with
|
|
605
|
+
`npm publish --access public` only when `--publish` is present (which also
|
|
606
|
+
requires a working `npm whoami`). Without `--publish` it is the same bump and
|
|
607
|
+
gate run as a dry run.
|
|
608
|
+
|
|
609
|
+
A clean working tree is required; commit first or pass `--allow-dirty`.
|
|
537
610
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
from uncommitted local changes.
|
|
611
|
+
CI also installs and exports packed consumers against Expo 56 and 57
|
|
612
|
+
(`.github/workflows/package-compatibility.yml`).
|
|
541
613
|
|
|
542
|
-
|
|
543
|
-
exports packed consumers against Expo 56 and 57.
|
|
614
|
+
### GitHub Publishing
|
|
544
615
|
|
|
545
616
|
If npm login email is unavailable, publish through GitHub Actions trusted
|
|
546
|
-
publishing
|
|
617
|
+
publishing:
|
|
547
618
|
|
|
548
619
|
1. In npm package settings for `@mrmeg/expo-ui`, add a trusted publisher:
|
|
549
|
-
GitHub Actions, owner `mrmeg`, repository `expo-template`, workflow
|
|
550
|
-
|
|
620
|
+
GitHub Actions, owner `mrmeg`, repository `expo-template`, workflow filename
|
|
621
|
+
`publish-ui.yml`.
|
|
551
622
|
2. Bump `packages/ui/package.json` in a commit and push it to `main`.
|
|
552
623
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
the version
|
|
624
|
+
`publish-ui.yml` runs on a push to `main` touching `packages/ui/package.json`
|
|
625
|
+
and on `workflow_dispatch`, using npm OIDC rather than a checked-in token or
|
|
626
|
+
local npm login. On push it reads the committed version, skips cleanly if that
|
|
627
|
+
version is already published, otherwise runs the gates and publishes from
|
|
628
|
+
`packages/ui`. Manual runs take `version` (`patch`, `minor`, `major`, or exact
|
|
629
|
+
`x.y.z`) and `ref` (default `main`); they bump the version, update `bun.lock`,
|
|
630
|
+
run the gates, publish, then commit and push the bump. If a manual run fails
|
|
631
|
+
after the bump landed, rerun it with the exact current version (e.g.
|
|
632
|
+
`version=0.1.3`) — exact versions do not bump again.
|
|
559
633
|
|
|
560
634
|
Keep `repository.url` in `package.json` as
|
|
561
|
-
`git+https://github.com/mrmeg/expo-template.git
|
|
562
|
-
that metadata
|
|
563
|
-
auth.
|
|
635
|
+
`git+https://github.com/mrmeg/expo-template.git`: npm trusted publishing checks
|
|
636
|
+
that metadata, and publish-time URL normalization can block OIDC auth.
|
|
564
637
|
|
|
565
|
-
If
|
|
566
|
-
|
|
567
|
-
|
|
638
|
+
If package settings block trusted publishing, add an npm automation or granular
|
|
639
|
+
publish token to GitHub Actions secrets as `NPM_TOKEN` and rerun the same
|
|
640
|
+
workflow; it is used only for the publish step. Keep npm tokens in developer or
|
|
641
|
+
CI configuration, never in the repository.
|
|
568
642
|
|
|
569
|
-
|
|
570
|
-
the exact current package version, for example `version=0.1.3`. Exact-version
|
|
571
|
-
reruns do not bump again.
|
|
572
|
-
|
|
573
|
-
Manual package checks:
|
|
643
|
+
## Package Checks
|
|
574
644
|
|
|
575
645
|
```sh
|
|
576
646
|
bun run packages:peer-check
|
|
@@ -581,9 +651,8 @@ bun run ui:pack
|
|
|
581
651
|
bun run ui:consumer-smoke
|
|
582
652
|
```
|
|
583
653
|
|
|
584
|
-
`
|
|
585
|
-
|
|
586
|
-
checks
|
|
587
|
-
entrypoints,
|
|
588
|
-
|
|
589
|
-
against the packed package without a custom Metro config.
|
|
654
|
+
`ui:pack` is a dry pack, so the published file list and package size can be
|
|
655
|
+
inspected before release. `ui:consumer-smoke` installs the packed tarball into
|
|
656
|
+
a clean fixture, checks every documented export-map target resolves,
|
|
657
|
+
type-checks all public entrypoints, and runs an iOS `expo export` against the
|
|
658
|
+
packed package at the workspace's Expo version, without a custom Metro config.
|