@mrmeg/expo-ui 0.22.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 +89 -0
- package/LLM_USAGE.md +166 -157
- package/README.md +433 -346
- package/dist/components/Accordion.js +1 -1
- package/dist/components/BottomSheet.js +1 -1
- package/dist/components/Card.js +7 -7
- package/dist/components/Checkbox.js +1 -1
- package/dist/components/Dialog.js +24 -5
- package/dist/components/DismissKeyboard.d.ts +14 -3
- package/dist/components/DismissKeyboard.js +17 -48
- package/dist/components/EmptyState.js +2 -2
- package/dist/components/InputOTP.js +4 -1
- package/dist/components/Item.d.ts +3 -2
- package/dist/components/Item.js +8 -6
- package/dist/components/RadioGroup.js +1 -1
- package/dist/components/StatCard.js +1 -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/dist/constants/spacing.d.ts +16 -6
- package/dist/constants/spacing.js +17 -6
- package/llms-full.md +134 -124
- package/llms.txt +24 -22
- package/package.json +3 -3
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,121 +173,89 @@ const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
|
124
173
|
}));
|
|
125
174
|
```
|
|
126
175
|
|
|
127
|
-
###
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
|
|
198
|
+
### Spacing and density
|
|
199
|
+
|
|
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:
|
|
207
|
+
|
|
208
|
+
| Token | Value | Use |
|
|
209
|
+
|-------|-------|-----|
|
|
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 |
|
|
213
|
+
| `dialogPadding` | 20 | `Dialog` and `AlertDialog` content |
|
|
214
|
+
| `rowPaddingY` / `rowPaddingX` | 10 / 16 | `Item` rows and hand-rolled list rows |
|
|
215
|
+
| `rowGap` | 12 | Gap between row media, content, actions |
|
|
216
|
+
| `rowMinHeight` | 40 | Visual row height on web; native rows keep `touchTarget` |
|
|
217
|
+
| `formRowMinHeight` | 32 | Checkbox and radio rows on web; native keeps `touchTarget` |
|
|
218
|
+
| `touchTarget` | 44 | Minimum native hit area; pair a smaller visual height with `hitSlop` |
|
|
219
|
+
|
|
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`).
|
|
131
224
|
|
|
132
|
-
|
|
133
|
-
2. **Global brand** (`setColors`) — your app's one palette, applied everywhere.
|
|
134
|
-
3. **Scoped override** (`ThemeColorScope`) — a per-subtree palette layered on
|
|
135
|
-
top of the brand, for transient theming that should not leak globally.
|
|
136
|
-
|
|
137
|
-
With no override at either layer, `useTheme()` returns the base theme by
|
|
138
|
-
reference — identical to pre-override behavior, with no extra allocation.
|
|
225
|
+
### Color overrides
|
|
139
226
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
143
232
|
|
|
144
233
|
```tsx
|
|
145
|
-
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
234
|
+
import { ThemeColorScope, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
146
235
|
|
|
236
|
+
// One app-wide brand palette.
|
|
147
237
|
useThemeStore.getState().setColors({
|
|
148
238
|
light: { primary: "#7c3aed", accent: "#14b8a6" },
|
|
149
239
|
dark: { primary: "#a78bfa", accent: "#2dd4bf" },
|
|
150
240
|
});
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Use `ThemeColorScope` to override colors for one subtree without touching the
|
|
154
|
-
global theme — a survey with a user-created palette, a preview pane, an embed.
|
|
155
|
-
It is React context, so it applies only inside the provider and unwinds when
|
|
156
|
-
that subtree unmounts. Nested scopes compose (inner keys win, outer fill in),
|
|
157
|
-
and a scoped key wins over the global brand for components inside it:
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
import { ThemeColorScope } from "@mrmeg/expo-ui/state";
|
|
161
241
|
|
|
242
|
+
// Per-subtree palette that must not leak globally: user-created themes,
|
|
243
|
+
// preview panes, embeds.
|
|
162
244
|
<ThemeColorScope colors={{ light: surveyColors, dark: surveyColors }}>
|
|
163
|
-
{/* every package component in here renders with the survey palette */}
|
|
164
245
|
<SurveyScreen />
|
|
165
246
|
</ThemeColorScope>
|
|
166
247
|
```
|
|
167
248
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
monospace (mono). Most apps ship their own faces — forward them with
|
|
172
|
-
`setFonts`, the font counterpart to `setColors`. Call it once at startup,
|
|
173
|
-
after your fonts are registered:
|
|
174
|
-
|
|
175
|
-
```tsx
|
|
176
|
-
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
177
|
-
|
|
178
|
-
useThemeStore.getState().setFonts({
|
|
179
|
-
families: {
|
|
180
|
-
sansSerif: {
|
|
181
|
-
regular: "HankenGrotesk_400Regular",
|
|
182
|
-
medium: "HankenGrotesk_500Medium",
|
|
183
|
-
semibold: "HankenGrotesk_600SemiBold",
|
|
184
|
-
bold: "HankenGrotesk_700Bold",
|
|
185
|
-
},
|
|
186
|
-
serif: {
|
|
187
|
-
regular: "Newsreader_400Regular",
|
|
188
|
-
semibold: "Newsreader_600SemiBold",
|
|
189
|
-
},
|
|
190
|
-
mono: {
|
|
191
|
-
regular: "JetBrainsMono_400Regular",
|
|
192
|
-
medium: "JetBrainsMono_500Medium",
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
webWeightStrategy: "family",
|
|
196
|
-
});
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Overrides are partial at both levels:
|
|
200
|
-
|
|
201
|
-
- **Per group** — pass `sansSerif` alone and `serif`/`mono` keep the package
|
|
202
|
-
defaults.
|
|
203
|
-
- **Per weight** — within an overridden group, weights you don't provide fall
|
|
204
|
-
back to that group's `regular` (never to a package face), so registering
|
|
205
|
-
only Regular + Medium is enough.
|
|
206
|
-
|
|
207
|
-
`setFonts({})` clears back to the defaults.
|
|
208
|
-
|
|
209
|
-
When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
210
|
-
Inter faces entirely (native `.ttf`s and the web Google-Fonts stylesheet) —
|
|
211
|
-
your app owns loading its own families, and nothing would reference Inter.
|
|
212
|
-
For the skip to apply, call `setFonts` before the hook mounts (module scope,
|
|
213
|
-
or ahead of rendering the root); a later call still re-skins all text, it
|
|
214
|
-
just doesn't prevent the Inter download. The Feather icon font always loads.
|
|
215
|
-
|
|
216
|
-
**Pick the right `webWeightStrategy`** — this is the one non-obvious part:
|
|
217
|
-
|
|
218
|
-
| Strategy | Use when | Effect |
|
|
219
|
-
|---|---|---|
|
|
220
|
-
| `"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 |
|
|
221
|
-
| `"family"` | You load **per-weight faces via `expo-font`** | The family name alone carries the weight; the numeric `fontWeight` is suppressed |
|
|
222
|
-
|
|
223
|
-
If you load fonts through `expo-font` / `@expo-google-fonts`, choose
|
|
224
|
-
`"family"`. `expo-font` registers each weight as its own single-face family on
|
|
225
|
-
web as well as native, so emitting a numeric weight on top makes the browser
|
|
226
|
-
synthesise a *second* layer of bold over an already-bold face.
|
|
227
|
-
|
|
228
|
-
Native always behaves as `"family"` regardless of this setting.
|
|
229
|
-
|
|
230
|
-
> Prefer `setFonts` over patching `node_modules`. Bun keys
|
|
231
|
-
> `patchedDependencies` to an exact `name@version`, so a font patch silently
|
|
232
|
-
> 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.
|
|
233
252
|
|
|
234
253
|
### Shape overrides
|
|
235
254
|
|
|
236
|
-
`setShape` is the geometry counterpart
|
|
237
|
-
|
|
255
|
+
`setShape` is the geometry counterpart, grouped per component. It currently
|
|
256
|
+
covers Button:
|
|
238
257
|
|
|
239
258
|
```tsx
|
|
240
|
-
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
241
|
-
|
|
242
259
|
useThemeStore.getState().setShape({
|
|
243
260
|
button: {
|
|
244
261
|
borderRadius: 9999, // pill buttons everywhere; package default is 12
|
|
@@ -247,11 +264,11 @@ useThemeStore.getState().setShape({
|
|
|
247
264
|
});
|
|
248
265
|
```
|
|
249
266
|
|
|
250
|
-
Precedence is
|
|
251
|
-
|
|
252
|
-
|
|
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.
|
|
253
270
|
|
|
254
|
-
|
|
271
|
+
## Typography
|
|
255
272
|
|
|
256
273
|
```tsx
|
|
257
274
|
import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/components";
|
|
@@ -262,22 +279,26 @@ import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/c
|
|
|
262
279
|
<StyledText semantic="label" tx="common.email" />
|
|
263
280
|
```
|
|
264
281
|
|
|
265
|
-
|
|
282
|
+
`StyledText` props:
|
|
266
283
|
|
|
267
|
-
- `semantic`: `title`, `heading`, `subheading`, `body`, `caption`, `label`
|
|
284
|
+
- `semantic`: `title`, `heading`, `subheading`, `body`, `caption`, `label`, `eyebrow`
|
|
268
285
|
- `size`: `xs`, `sm`, `base`, `body`, `lg`, `xl`, `xxl`, `display`
|
|
269
286
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
270
|
-
- `variant`: `sansSerif`, `serif`, `mono`
|
|
271
|
-
|
|
272
|
-
- `
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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:
|
|
281
302
|
|
|
282
303
|
```tsx
|
|
283
304
|
import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
|
|
@@ -286,87 +307,223 @@ import { i18n } from "./i18n";
|
|
|
286
307
|
configureExpoUiI18n((key, options) => i18n.t(key, options));
|
|
287
308
|
```
|
|
288
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
|
+
|
|
289
381
|
## Component Guide
|
|
290
382
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
| Component | Use For |
|
|
296
|
-
|
|
297
|
-
| `Accordion
|
|
298
|
-
| `Alert` | Cross-platform imperative alerts |
|
|
299
|
-
| `AnimatedView` | Entrance and visibility animation |
|
|
300
|
-
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback |
|
|
301
|
-
| `Badge` | Short status labels |
|
|
302
|
-
| `BottomSheet` | Mobile-first modal sheets |
|
|
303
|
-
| `Button` | Commands and CTAs |
|
|
304
|
-
| `Card
|
|
305
|
-
| `Carousel` | Horizontally snapping
|
|
306
|
-
| `Checkbox` | Boolean selection |
|
|
307
|
-
| `Collapsible
|
|
308
|
-
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content |
|
|
309
|
-
| `DismissKeyboard` | Tap-away keyboard dismissal |
|
|
310
|
-
| `Drawer` | Side panels and drawer navigation |
|
|
311
|
-
| `DropdownMenu` | Menus and command lists |
|
|
312
|
-
| `EmptyState` | No-data or recoverable error regions |
|
|
313
|
-
| `ErrorBoundary` | React render error fallback |
|
|
314
|
-
| `Icon` | Feather or custom icons
|
|
315
|
-
| `InputOTP` | Verification code entry |
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
| `
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
339
433
|
|
|
340
434
|
| Root | Exported Parts |
|
|
341
435
|
|------|----------------|
|
|
342
436
|
| `Accordion` | `AccordionItem`, `AccordionTrigger`, `AccordionContent` |
|
|
343
|
-
| `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` |
|
|
344
439
|
| `BottomSheet` | `BottomSheetTrigger`, `BottomSheetContent`, `BottomSheetHandle`, `BottomSheetHeader`, `BottomSheetBody`, `BottomSheetFooter`, `BottomSheetClose` |
|
|
440
|
+
| `Button` | `ButtonText`, `ButtonIcon` |
|
|
345
441
|
| `Card` | `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` |
|
|
346
442
|
| `Collapsible` | `CollapsibleTrigger`, `CollapsibleContent` |
|
|
347
443
|
| `Dialog` | `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter`, `DialogTitle`, `DialogDescription`, `DialogClose` |
|
|
348
444
|
| `Drawer` | `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerBody`, `DrawerFooter`, `DrawerClose`, `DrawerToggleCollapse` |
|
|
349
445
|
| `DropdownMenu` | `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuLabel`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuPortal`, `DropdownMenuSub`, `DropdownMenuSubTrigger`, `DropdownMenuSubContent` |
|
|
446
|
+
| `Item` | `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions` |
|
|
350
447
|
| `Popover` | `PopoverTrigger`, `PopoverContent`, `PopoverHeader`, `PopoverBody`, `PopoverFooter` |
|
|
351
448
|
| `RadioGroup` | `RadioGroupItem` |
|
|
352
449
|
| `Select` | `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `SelectGroup`, `SelectLabel`, `SelectSeparator` |
|
|
353
450
|
| `Skeleton` | `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` |
|
|
354
|
-
| `Tabs` | `TabsList`, `TabsTrigger`, `TabsContent` |
|
|
451
|
+
| `Tabs` | `TabsList`, `TabsTrigger`, `TabsContent`, `TabsTriggerText` (`Tabs.Trigger.Text`) |
|
|
355
452
|
| `Toggle` | `ToggleIcon` |
|
|
356
453
|
| `ToggleGroup` | `ToggleGroupItem`, `ToggleGroupIcon` |
|
|
357
454
|
| `Tooltip` | `TooltipTrigger`, `TooltipContent`, `TooltipBody` |
|
|
358
455
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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.
|
|
369
524
|
```tsx
|
|
525
|
+
import { Drawer, Icon } from "@mrmeg/expo-ui/components";
|
|
526
|
+
|
|
370
527
|
<Drawer.Header
|
|
371
528
|
icon={<Icon name="hexagon" color="accent" />}
|
|
372
529
|
title="Acme"
|
|
@@ -378,18 +535,8 @@ Use `StyledText` or its aliases instead of raw `Text` whenever the text is part
|
|
|
378
535
|
/>
|
|
379
536
|
```
|
|
380
537
|
|
|
381
|
-
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`.
|
|
382
|
-
|
|
383
|
-
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.
|
|
384
|
-
|
|
385
|
-
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.
|
|
386
|
-
|
|
387
|
-
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.
|
|
388
|
-
|
|
389
538
|
### Quick Examples
|
|
390
539
|
|
|
391
|
-
Card, badge, and CTA:
|
|
392
|
-
|
|
393
540
|
```tsx
|
|
394
541
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@mrmeg/expo-ui/components";
|
|
395
542
|
|
|
@@ -406,10 +553,8 @@ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@mrmeg/
|
|
|
406
553
|
</Card>
|
|
407
554
|
```
|
|
408
555
|
|
|
409
|
-
Form controls:
|
|
410
|
-
|
|
411
556
|
```tsx
|
|
412
|
-
import { Button,
|
|
557
|
+
import { Button, Switch, TextInput } from "@mrmeg/expo-ui/components";
|
|
413
558
|
|
|
414
559
|
<TextInput
|
|
415
560
|
label="Email"
|
|
@@ -426,8 +571,6 @@ import { Button, TextInput, Switch } from "@mrmeg/expo-ui/components";
|
|
|
426
571
|
</Button>
|
|
427
572
|
```
|
|
428
573
|
|
|
429
|
-
Feedback:
|
|
430
|
-
|
|
431
574
|
```tsx
|
|
432
575
|
import { EmptyState, Progress, SkeletonCard } from "@mrmeg/expo-ui/components";
|
|
433
576
|
import { notify } from "@mrmeg/expo-ui/state";
|
|
@@ -438,121 +581,66 @@ import { notify } from "@mrmeg/expo-ui/state";
|
|
|
438
581
|
|
|
439
582
|
notify.success("Saved", {
|
|
440
583
|
messages: ["Your changes were saved."],
|
|
441
|
-
action: {
|
|
442
|
-
label: "View",
|
|
443
|
-
onPress: openSavedItem,
|
|
444
|
-
},
|
|
584
|
+
action: { label: "View", onPress: openSavedItem },
|
|
445
585
|
});
|
|
446
586
|
```
|
|
447
587
|
|
|
448
|
-
|
|
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.
|
|
449
594
|
|
|
450
|
-
|
|
451
|
-
- Use `useTheme()` and semantic tokens instead of hardcoded colors.
|
|
452
|
-
- Use `Button.preset`, not `variant`, for buttons.
|
|
453
|
-
- Mount `UIProvider` before using overlays, menus, select content, popovers, tooltips, or package notifications.
|
|
454
|
-
- Import from package exports, not `packages/ui/src/*`.
|
|
455
|
-
|
|
456
|
-
## App Startup
|
|
457
|
-
|
|
458
|
-
Call `useResources()` once near the Expo app root before hiding the splash screen:
|
|
459
|
-
|
|
460
|
-
```tsx
|
|
461
|
-
import { ThemeProvider } from "expo-router";
|
|
462
|
-
import { colors } from "@mrmeg/expo-ui/constants";
|
|
463
|
-
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
464
|
-
import { UIProvider } from "@mrmeg/expo-ui/components";
|
|
465
|
-
|
|
466
|
-
export default function RootLayout() {
|
|
467
|
-
const { scheme } = useTheme();
|
|
468
|
-
const { loaded } = useResources();
|
|
469
|
-
|
|
470
|
-
if (!loaded) return null;
|
|
471
|
-
|
|
472
|
-
return (
|
|
473
|
-
<ThemeProvider
|
|
474
|
-
value={{
|
|
475
|
-
dark: colors[scheme ?? "light"].dark,
|
|
476
|
-
colors: colors[scheme ?? "light"].navigation,
|
|
477
|
-
fonts: colors[scheme ?? "light"].fonts,
|
|
478
|
-
}}
|
|
479
|
-
>
|
|
480
|
-
<UIProvider>
|
|
481
|
-
{/* App navigation goes here. */}
|
|
482
|
-
</UIProvider>
|
|
483
|
-
</ThemeProvider>
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
## Fonts
|
|
489
|
-
|
|
490
|
-
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.
|
|
491
|
-
|
|
492
|
-
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`:
|
|
493
|
-
|
|
494
|
-
```tsx
|
|
495
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
496
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
497
|
-
<link
|
|
498
|
-
id="mrmeg-expo-ui-inter"
|
|
499
|
-
rel="stylesheet"
|
|
500
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
501
|
-
/>
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
`useResources()` also loads `Feather.font` from the package-managed `@expo/vector-icons` dependency for icon rendering.
|
|
505
|
-
|
|
506
|
-
## Package Checks
|
|
507
|
-
|
|
508
|
-
For a one-command release from the repo root, use:
|
|
595
|
+
## Package Release
|
|
509
596
|
|
|
510
597
|
```sh
|
|
511
598
|
bun run ui:release -- --patch --publish
|
|
512
599
|
```
|
|
513
600
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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`.
|
|
519
610
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
from uncommitted local changes.
|
|
611
|
+
CI also installs and exports packed consumers against Expo 56 and 57
|
|
612
|
+
(`.github/workflows/package-compatibility.yml`).
|
|
523
613
|
|
|
524
|
-
|
|
525
|
-
exports packed consumers against Expo 56 and 57.
|
|
614
|
+
### GitHub Publishing
|
|
526
615
|
|
|
527
616
|
If npm login email is unavailable, publish through GitHub Actions trusted
|
|
528
|
-
publishing
|
|
617
|
+
publishing:
|
|
529
618
|
|
|
530
619
|
1. In npm package settings for `@mrmeg/expo-ui`, add a trusted publisher:
|
|
531
|
-
GitHub Actions, owner `mrmeg`, repository `expo-template`, workflow
|
|
532
|
-
|
|
620
|
+
GitHub Actions, owner `mrmeg`, repository `expo-template`, workflow filename
|
|
621
|
+
`publish-ui.yml`.
|
|
533
622
|
2. Bump `packages/ui/package.json` in a commit and push it to `main`.
|
|
534
623
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
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.
|
|
541
633
|
|
|
542
634
|
Keep `repository.url` in `package.json` as
|
|
543
|
-
`git+https://github.com/mrmeg/expo-template.git
|
|
544
|
-
that metadata
|
|
545
|
-
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.
|
|
546
637
|
|
|
547
|
-
If
|
|
548
|
-
|
|
549
|
-
|
|
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.
|
|
550
642
|
|
|
551
|
-
|
|
552
|
-
the exact current package version, for example `version=0.1.3`. Exact-version
|
|
553
|
-
reruns do not bump again.
|
|
554
|
-
|
|
555
|
-
Manual package checks:
|
|
643
|
+
## Package Checks
|
|
556
644
|
|
|
557
645
|
```sh
|
|
558
646
|
bun run packages:peer-check
|
|
@@ -563,9 +651,8 @@ bun run ui:pack
|
|
|
563
651
|
bun run ui:consumer-smoke
|
|
564
652
|
```
|
|
565
653
|
|
|
566
|
-
`
|
|
567
|
-
|
|
568
|
-
checks
|
|
569
|
-
entrypoints,
|
|
570
|
-
|
|
571
|
-
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.
|