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