@mrmeg/expo-ui 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/LLM_USAGE.md +159 -157
- package/README.md +421 -352
- package/dist/components/BottomSheet.js +1 -1
- package/dist/components/DismissKeyboard.d.ts +14 -3
- package/dist/components/DismissKeyboard.js +17 -48
- package/dist/components/InputOTP.js +4 -1
- package/dist/components/TextInput.js +189 -42
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/keyboardDismiss.d.ts +37 -0
- package/dist/components/keyboardDismiss.js +170 -0
- package/dist/components/keyboardFocusRegistry.d.ts +10 -14
- package/dist/components/keyboardFocusRegistry.js +8 -24
- package/llms-full.md +134 -124
- package/llms.txt +24 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,64 @@
|
|
|
3
3
|
All notable changes to `@mrmeg/expo-ui` are documented here. This project
|
|
4
4
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [0.24.0]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Tap-away keyboard dismissal no longer steals taps from controls.**
|
|
11
|
+
`DismissKeyboard` used to call `KeyboardController.dismiss()` in the responder
|
|
12
|
+
*capture* phase on touch start, for any touch outside a cached rectangle of
|
|
13
|
+
the focused field. That rectangle went stale as soon as keyboard avoidance
|
|
14
|
+
shifted the form, and the dismiss collapsed the layout under the finger, so
|
|
15
|
+
the password eye toggle, tapping straight into another field, and double-tap
|
|
16
|
+
text selection all closed the keyboard. The wrapper now mirrors RN's
|
|
17
|
+
`keyboardShouldPersistTaps="handled"` for native fields: it never claims the
|
|
18
|
+
touch, lets Pressables and text inputs win the responder negotiation, and
|
|
19
|
+
dismisses on release only when nothing else took the tap and the finger did
|
|
20
|
+
not move. Tapping a button keeps the keyboard open; tapping another field
|
|
21
|
+
hands focus over natively.
|
|
22
|
+
- **Native `TextInput`: the whole painted box now focuses the field.** The
|
|
23
|
+
SwiftUI `TextField` only hit-tests its text line (about 17pt of a 33pt `md`
|
|
24
|
+
box), so taps on the box's vertical padding were silent no-ops that read as
|
|
25
|
+
"the keyboard needs a second tap". The surface now focuses the field on
|
|
26
|
+
release of a tap nothing else claimed, and gains vertical `hitSlop` up to the
|
|
27
|
+
44pt touch-target guideline without changing the visual size.
|
|
28
|
+
- **Native `TextInput`: the password eye toggle keeps the keyboard on iOS.**
|
|
29
|
+
`@expo/ui` renders `secureTextEntry` as a different native view, so flipping
|
|
30
|
+
it remounted the field unfocused and the keyboard dropped. While the field is
|
|
31
|
+
focused, the toggle now keeps the old view mounted until the new one has
|
|
32
|
+
taken first responder, so UIKit hands the keyboard over instead of dismissing
|
|
33
|
+
it. A second tap during that handoff is applied once it settles instead of
|
|
34
|
+
dropping the keyboard. iOS also clears a secure field's text on the first
|
|
35
|
+
keystroke after it regains first responder, which made "hide, keep typing"
|
|
36
|
+
erase the password; the field now restores the text and appends the
|
|
37
|
+
keystroke. Android toggles in place and was unaffected.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`useKeyboardDismissResponder()`** returns the touch-handler props behind
|
|
42
|
+
`DismissKeyboard`, for apps that mount their own boundary. Nesting is safe:
|
|
43
|
+
the innermost boundary owns each touch.
|
|
44
|
+
- **`dismissKeyboard()`** resigns the focused native field (falling back to
|
|
45
|
+
`KeyboardController.dismiss()` for RN or third-party inputs). Call it from
|
|
46
|
+
submit handlers, since buttons no longer dismiss implicitly.
|
|
47
|
+
- **`markTextInputTouchStart`** is the `onStartShouldSetResponder` handler the
|
|
48
|
+
package `TextInput` uses to tag its touches; custom native-field wrappers can
|
|
49
|
+
reuse it to opt into the same exemption.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **`DismissKeyboard` lets a drag hide the keyboard**:
|
|
54
|
+
`keyboardDismissMode="interactive"` on iOS and an explicit dismiss on drag
|
|
55
|
+
start on Android, where RN's `on-drag` only knows RN inputs.
|
|
56
|
+
- **`keyboardFocusRegistry` no longer tracks layouts.** The internal module's
|
|
57
|
+
`setKeyboardFocusedInputLayout` / `clearKeyboardFocusedInputLayout` /
|
|
58
|
+
`getKeyboardFocusedInputLayout` / `isTouchInsideKeyboardFocusedInput` are
|
|
59
|
+
replaced by `setKeyboardFocusedInput(token, blur)` /
|
|
60
|
+
`clearKeyboardFocusedInput(token)`. The presence and blur APIs
|
|
61
|
+
(`hasKeyboardFocusedInput`, `subscribeKeyboardFocus`,
|
|
62
|
+
`dismissKeyboardFocusedInput`) are unchanged.
|
|
63
|
+
|
|
6
64
|
## [0.23.0]
|
|
7
65
|
|
|
8
66
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# @mrmeg/expo-ui LLM Usage Guide
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`node_modules/@mrmeg/expo-ui/LLM_USAGE.md` before building app UI.
|
|
3
|
+
Read from `node_modules/@mrmeg/expo-ui/LLM_USAGE.md` before building app UI.
|
|
5
4
|
|
|
6
5
|
## First Rule
|
|
7
6
|
|
|
8
|
-
Do not recreate primitives
|
|
9
|
-
`@mrmeg/expo-ui` and compose the exported components
|
|
7
|
+
Do not recreate primitives this package already provides. Import from
|
|
8
|
+
`@mrmeg/expo-ui` and compose the exported components.
|
|
10
9
|
|
|
11
10
|
## Stable Import Paths
|
|
12
11
|
|
|
@@ -14,34 +13,31 @@ Do not recreate primitives that this package already provides. Import from
|
|
|
14
13
|
import { Button, StyledText, UIProvider } from "@mrmeg/expo-ui/components";
|
|
15
14
|
import { Button as ButtonDirect } from "@mrmeg/expo-ui/components/Button";
|
|
16
15
|
import { colors, spacing, typography } from "@mrmeg/expo-ui/constants";
|
|
17
|
-
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
18
|
-
import { globalUIStore, notify, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
16
|
+
import { useResources, useStyles, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
17
|
+
import { globalUIStore, notify, ThemeColorScope, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
19
18
|
import { configureExpoUiI18n, hapticLight } from "@mrmeg/expo-ui/lib";
|
|
19
|
+
// The root barrel re-exports the whole public surface:
|
|
20
|
+
import { Button, colors, UIProvider, useTheme } from "@mrmeg/expo-ui";
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import { Button, UIProvider, colors, useTheme } from "@mrmeg/expo-ui";
|
|
26
|
-
```
|
|
23
|
+
Importable paths: root, `components`, `components/*`, `constants`,
|
|
24
|
+
`constants/*`, `hooks`, `hooks/*`, `state`, `state/*`, `lib`. Never import from
|
|
25
|
+
`@mrmeg/expo-ui/dist/*` or a source checkout path.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import from `@mrmeg/expo-ui/dist/*` or from a source checkout path.
|
|
31
|
-
|
|
32
|
-
Supported hosts are Expo 56–57, React 19.2, React Native 0.85–0.86, and React
|
|
33
|
-
Native Web 0.21. Install the peer versions recommended by the consuming app's
|
|
34
|
-
Expo SDK.
|
|
27
|
+
Hosts: Expo 56–57, React 19.2, React Native 0.85–0.86, React Native Web 0.21.
|
|
28
|
+
Install the peer versions recommended by the consuming app's Expo SDK.
|
|
35
29
|
|
|
36
30
|
## Required App Setup
|
|
37
31
|
|
|
38
|
-
Call `useResources()` once near the Expo app root. Mount `UIProvider` once
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
32
|
+
Call `useResources()` once near the Expo app root. Mount `UIProvider` once near
|
|
33
|
+
the root; it owns the package `Notification`, `StatusBar`, the default
|
|
34
|
+
`@rn-primitives` portal host, and the native keyboard-avoiding root. It is
|
|
35
|
+
required before `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`,
|
|
36
|
+
`DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or `notify`.
|
|
42
37
|
|
|
43
38
|
```tsx
|
|
44
39
|
import { ThemeProvider } from "expo-router";
|
|
40
|
+
import { KeyboardProvider } from "react-native-keyboard-controller";
|
|
45
41
|
import { UIProvider } from "@mrmeg/expo-ui/components";
|
|
46
42
|
import { colors } from "@mrmeg/expo-ui/constants";
|
|
47
43
|
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
@@ -60,32 +56,38 @@ export function RootLayout() {
|
|
|
60
56
|
fonts: colors[scheme ?? "light"].fonts,
|
|
61
57
|
}}
|
|
62
58
|
>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
<KeyboardProvider>
|
|
60
|
+
<UIProvider>
|
|
61
|
+
{/* App navigation goes here. */}
|
|
62
|
+
</UIProvider>
|
|
63
|
+
</KeyboardProvider>
|
|
66
64
|
</ThemeProvider>
|
|
67
65
|
);
|
|
68
66
|
}
|
|
69
67
|
```
|
|
70
68
|
|
|
71
|
-
`UIProvider`
|
|
72
|
-
`
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
69
|
+
`UIProvider` props, all opt-out: `notification`, `portalHost`, `statusBar`
|
|
70
|
+
(default `true`), `keyboardAvoiding` (default `true` on native, `false` on web),
|
|
71
|
+
and `keyboardAvoidingProps` forwarded to the root wrapper. Native keyboard
|
|
72
|
+
avoidance is `react-native-keyboard-controller`, so mount its `KeyboardProvider`
|
|
73
|
+
above `UIProvider`; use `KeyboardAvoidingView` directly only for a subtree with
|
|
74
|
+
custom behavior.
|
|
75
|
+
|
|
76
|
+
`BottomSheet` renders the platform's native sheet through `@expo/ui` (iOS
|
|
77
|
+
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`). The
|
|
78
|
+
platform owns gestures and keyboard avoidance: `swipeEnabled`, `avoidKeyboard`,
|
|
79
|
+
and `dismissKeyboardOnDrag` are accepted for call-site ergonomics but have no
|
|
80
|
+
effect. `Slider` and `SegmentedControl` are also `@expo/ui`-backed.
|
|
81
|
+
|
|
82
|
+
`BottomSheet.Content` themes the native sheet surface with the card color. Pass
|
|
83
|
+
`backgroundStyle={{ backgroundColor: "transparent" }}`, plus a `style` clearing
|
|
84
|
+
the content column's card fill, when custom chrome such as a glass backdrop must
|
|
85
|
+
show through.
|
|
86
|
+
|
|
87
|
+
i18n is optional. Do not add app-level i18n setup just to use this package;
|
|
88
|
+
plain children and `text` props work without `i18next` or `react-i18next`. `tx`
|
|
89
|
+
props render their fallback text when provided, otherwise the key. Package-owned
|
|
90
|
+
defaults such as notification titles stay human-readable without app i18n.
|
|
89
91
|
|
|
90
92
|
```tsx
|
|
91
93
|
import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
|
|
@@ -98,60 +100,47 @@ configureExpoUiI18n((key, options) => i18n.t(key, options));
|
|
|
98
100
|
|
|
99
101
|
- Use `useTheme()` and semantic tokens instead of hardcoded colors.
|
|
100
102
|
- Use `StyledText` or its semantic aliases instead of raw `Text` for app UI.
|
|
101
|
-
- Use `Button.preset`, not `variant
|
|
102
|
-
- Button visible heights
|
|
103
|
-
- Use `Button size="sm"` for compact popover, tooltip, and toolbar triggers; nested `StyledText` inherits the
|
|
104
|
-
- Use `notify` plus root
|
|
103
|
+
- Use `Button.preset`, not `variant`.
|
|
104
|
+
- Button visible heights: `sm` 28, `md` 32, `lg` 40. `TextInput`/`Select`: 32/36/40. `Toggle` sizes are `sm`/`default`/`lg` (32/36/40). `Tabs`: `sm`/`md` (32/36).
|
|
105
|
+
- Use `Button size="sm"` for compact popover, tooltip, and toolbar triggers; nested `StyledText` inherits the Button size.
|
|
106
|
+
- Use `notify` plus a root `UIProvider` for transient global feedback. (`globalUIStore` stays available for reactive subscriptions and tests.)
|
|
105
107
|
- Keep app monitoring, auth, API, and domain behavior outside this package.
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
theme.colors.background;
|
|
112
|
-
theme.colors.foreground;
|
|
113
|
-
theme.colors.card;
|
|
114
|
-
theme.colors.popover;
|
|
115
|
-
theme.colors.border;
|
|
116
|
-
theme.colors.borderStrong;
|
|
117
|
-
theme.colors.input;
|
|
118
|
-
theme.colors.ring;
|
|
119
|
-
theme.colors.primary;
|
|
120
|
-
theme.colors.secondary;
|
|
121
|
-
theme.colors.accent;
|
|
122
|
-
theme.colors.mutedForeground;
|
|
123
|
-
theme.colors.destructive;
|
|
124
|
-
theme.colors.success;
|
|
125
|
-
theme.colors.warning;
|
|
126
|
-
```
|
|
109
|
+
Semantic color tokens on `theme.colors`: `surfaceSunken`, `background`,
|
|
110
|
+
`foreground`, `card`, `popover`, `muted`, `mutedForeground`, `border`,
|
|
111
|
+
`borderStrong`, `input`, `ring`, `primary`, `secondary`, `accent`,
|
|
112
|
+
`destructive`, `success`, `warning`.
|
|
127
113
|
|
|
128
114
|
Token intent:
|
|
129
115
|
|
|
130
|
-
- `primary`: neutral action color
|
|
131
|
-
- `secondary`: neutral secondary surface
|
|
116
|
+
- `primary`: neutral action color; `secondary`: neutral secondary surface
|
|
132
117
|
- `accent`: teal highlight color
|
|
133
|
-
- `input`: form-control border
|
|
134
|
-
- `ring`: focus outline color
|
|
118
|
+
- `input`: form-control border; `ring`: focus outline
|
|
135
119
|
- `popover`: elevated overlay surface
|
|
136
120
|
- `surfaceSunken`: app-chrome surface one tier below `background`
|
|
137
|
-
- `borderStrong`: hairline for elements on filled surfaces, where `border`
|
|
138
|
-
|
|
139
|
-
Elevation is a surface-tier ladder, not shadow depth:
|
|
140
|
-
`
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
`
|
|
147
|
-
|
|
148
|
-
`
|
|
149
|
-
|
|
150
|
-
|
|
121
|
+
- `borderStrong`: hairline for elements on filled surfaces, where `border` blends in
|
|
122
|
+
|
|
123
|
+
Elevation is a surface-tier ladder, not shadow depth: `surfaceSunken` (chrome) <
|
|
124
|
+
`background` (content) < `card`/`popover` (raised) < `muted` (chips, insets).
|
|
125
|
+
|
|
126
|
+
On web every `theme.colors.*` value is a CSS custom property (`var(--c-*)`), so
|
|
127
|
+
themes swap in CSS when `html[data-theme]` changes; native keeps literals. Hex
|
|
128
|
+
alpha concatenation (`theme.colors.x + "15"`) does **not** work — use
|
|
129
|
+
`withAlpha(theme.colors.x, 0.08)`, exported standalone from `hooks` and from
|
|
130
|
+
`useTheme()`. For sinks that cannot take `var()` (e.g. `<meta name="theme-color">`)
|
|
131
|
+
use `rawThemeColors.light/.dark` or `resolveRawColor(color, scheme)`;
|
|
132
|
+
`getThemeCssVariables(overrides?)` emits the `--c-*` definitions for an app's
|
|
133
|
+
`+html.tsx`. All three come from `@mrmeg/expo-ui/constants`.
|
|
134
|
+
|
|
135
|
+
Use `getShadowStyle(type)` for elevation — `base`, `soft`, `sharp`, `subtle`,
|
|
136
|
+
`elevated`, `glow`, `glass`, `card`, `cardHover`, `cardSubtle` — returning a
|
|
137
|
+
cross-platform `boxShadow` (RN 0.85 + react-native-web 0.21 deprecate the legacy
|
|
138
|
+
`shadow*` props). Use `getFocusRingStyle(offset?)` for web focus styling. Keep
|
|
139
|
+
web controls compact; package controls already provide native hit slop or 44px
|
|
140
|
+
touch rows.
|
|
151
141
|
|
|
152
142
|
Use `useStyles()` for memoized theme-aware local styles. Its factory receives
|
|
153
|
-
`{ theme, spacing, withAlpha }
|
|
154
|
-
semantic colors without destructuring `withAlpha` outside the factory:
|
|
143
|
+
`{ theme, spacing, withAlpha }`:
|
|
155
144
|
|
|
156
145
|
```tsx
|
|
157
146
|
const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
@@ -162,21 +151,21 @@ const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
|
162
151
|
}));
|
|
163
152
|
```
|
|
164
153
|
|
|
165
|
-
Layout spacing uses semantic density tokens
|
|
154
|
+
Layout spacing uses semantic density tokens, not raw scale steps:
|
|
166
155
|
`spacing.screenPadding` (16) for screen and block gutters, `spacing.cardPadding`
|
|
167
156
|
(16) for bordered panels, `spacing.sectionSpacing` (24) between grouped lists,
|
|
168
157
|
`spacing.dialogPadding` (20) for dialogs, and `spacing.rowPaddingY`/`rowPaddingX`
|
|
169
158
|
(10/16) with `spacing.rowGap` (12) for list rows. `Item` already applies the row
|
|
170
159
|
tokens and keeps a 44px hit area on native while rendering 40px on web.
|
|
171
160
|
|
|
172
|
-
When the saved theme preference is `system`, the package theme store owns the
|
|
173
|
-
|
|
174
|
-
app-local Appearance or `matchMedia` listeners
|
|
175
|
-
`
|
|
161
|
+
When the saved theme preference is `system`, the package theme store owns the OS
|
|
162
|
+
color-scheme subscription, including web `prefers-color-scheme`. Do not add
|
|
163
|
+
app-local Appearance or `matchMedia` listeners. `THEME_STORAGE_KEY` (from
|
|
164
|
+
`state`) is the persisted-preference key, for a pre-boot theme script.
|
|
176
165
|
|
|
177
166
|
`useTheme()` resolves colors in three layers, last wins: package defaults →
|
|
178
|
-
global brand (`useThemeStore.getState().setColors(overrides)`) → scoped
|
|
179
|
-
|
|
167
|
+
global brand (`useThemeStore.getState().setColors(overrides)`) → scoped override
|
|
168
|
+
(`ThemeColorScope`). Each override is `{ light?, dark? }` of
|
|
180
169
|
`Partial<ThemeColors>`; only the keys you pass are replaced. Call `setColors`
|
|
181
170
|
once to forward the app's brand palette globally. Wrap a subtree in
|
|
182
171
|
`<ThemeColorScope colors={{ light, dark }}>` for transient per-subtree theming
|
|
@@ -185,73 +174,83 @@ React context, scoped keys win over the global brand inside it, and nested
|
|
|
185
174
|
scopes merge (inner wins, outer fills in). With no override at either layer,
|
|
186
175
|
`useTheme()` returns the base theme by reference.
|
|
187
176
|
|
|
188
|
-
Fonts and shape have matching global injection points. `setFonts({ families:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
177
|
+
Fonts and shape have matching global injection points. `setFonts({ families: {
|
|
178
|
+
sansSerif?, serif?, mono? }, webWeightStrategy? })` replaces the bundled faces
|
|
179
|
+
(Inter / Georgia / system-mono) everywhere text renders; groups and weights are
|
|
180
|
+
partial, missing weights fall back to that group's `regular`, and an overridden
|
|
181
|
+
`sansSerif` makes `useResources` skip downloading Inter (call `setFonts` before
|
|
182
|
+
mount for the skip). Use `webWeightStrategy: "family"` when loading per-weight
|
|
183
|
+
faces through `expo-font` / `@expo-google-fonts`, `"numeric"` (default) for one
|
|
184
|
+
multi-weight CSS family. `setShape({ button: { borderRadius?, withShadow? } })`
|
|
185
|
+
re-shapes Buttons globally. Per-instance props and caller `style` always win.
|
|
196
186
|
|
|
197
187
|
## Component Use-Case Index
|
|
198
188
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `
|
|
217
|
-
| `Drawer` | Side panels and drawer navigation | Custom sliding panels |
|
|
218
|
-
| `DropdownMenu` | Menus and command lists | Homemade popover menus |
|
|
219
|
-
| `EmptyState` | No-data or recoverable error regions | One-off empty placeholders |
|
|
220
|
-
| `ErrorBoundary` | React render error fallback | Unhandled screen crashes |
|
|
221
|
-
| `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors |
|
|
222
|
-
| `InputOTP` | Verification code entry |
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
189
|
+
Check this before creating a new app-local primitive. All components come from
|
|
190
|
+
`@mrmeg/expo-ui/components`; `@mrmeg/expo-ui/components/<Name>` also works.
|
|
191
|
+
|
|
192
|
+
| Component | Use for | Instead of |
|
|
193
|
+
|-----------|---------|------------|
|
|
194
|
+
| `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders |
|
|
195
|
+
| `Alert` | Cross-platform imperative alerts | `window.alert` or duplicated RN/web branching |
|
|
196
|
+
| `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers |
|
|
197
|
+
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback | Circles with a nested `Image` plus initials `Text` |
|
|
198
|
+
| `Badge` | Short status labels | Custom pill `View` + `Text` |
|
|
199
|
+
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets |
|
|
200
|
+
| `Button` | Commands and CTAs | Pressable plus custom text styling |
|
|
201
|
+
| `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels |
|
|
202
|
+
| `Carousel` | Horizontally snapping slide row with pressable dots | Snap `ScrollView` plus manual offset math |
|
|
203
|
+
| `Checkbox` | Boolean selection | Custom checkmark controls |
|
|
204
|
+
| `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers |
|
|
205
|
+
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays |
|
|
206
|
+
| `DismissKeyboard` | Tap-away keyboard dismissal | Screen-level keyboard handling |
|
|
207
|
+
| `Drawer` | Side panels and drawer navigation | Custom sliding panels |
|
|
208
|
+
| `DropdownMenu` | Menus and command lists | Homemade popover menus |
|
|
209
|
+
| `EmptyState` | No-data or recoverable error regions | One-off empty placeholders |
|
|
210
|
+
| `ErrorBoundary` | React render error fallback | Unhandled screen crashes |
|
|
211
|
+
| `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors |
|
|
212
|
+
| `InputOTP` | Verification code entry | Several manually managed text inputs |
|
|
213
|
+
| `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions` | List / settings rows with density tokens | Hand-rolled row `View`s |
|
|
214
|
+
| `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers |
|
|
215
|
+
| `Label` | Accessible form labels | Plain styled text labels |
|
|
216
|
+
| `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers |
|
|
217
|
+
| `Notification` | Global toast surface | Screen-local toast state |
|
|
218
|
+
| `Popover` | Anchored contextual content | Custom anchored views |
|
|
219
|
+
| `Progress` | Determinate or indeterminate progress | Layout-shifting spinners |
|
|
220
|
+
| `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows |
|
|
221
|
+
| `SectionHeader` | Eyebrow / title / description section intro | Stacked ad hoc heading text |
|
|
222
|
+
| `SegmentedControl` | Native segmented picker (`@expo/ui`) | Custom segmented views |
|
|
223
|
+
| `Select` | Option menus | Custom dropdowns |
|
|
224
|
+
| `Separator` | Horizontal or vertical dividers | Border-only spacer views |
|
|
225
|
+
| `Skeleton`, `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` | Loading placeholders | Blank space or generic spinners |
|
|
226
|
+
| `Slider` | Numeric value selection (`@expo/ui`) | Custom pan gesture track |
|
|
227
|
+
| `StatCard` | Metric tile with label, value, unit, change | Hand-rolled dashboard cards |
|
|
228
|
+
| `StatusBar` | Theme-aware native status bar | Per-screen status-bar duplication |
|
|
229
|
+
| `StyledText` and text aliases | Theme-aware typography | Raw `Text` with hardcoded styles |
|
|
230
|
+
| `Switch` | Binary settings | Custom toggle switches |
|
|
231
|
+
| `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls |
|
|
232
|
+
| `TextInput` | Text entry with label, helper/error text, clear, password reveal | Raw `TextInput` plus repeated label/error code |
|
|
233
|
+
| `Toggle`, `ToggleIcon` | Pressed/unpressed control | Button with local selected styling |
|
|
234
|
+
| `ToggleGroup`, `ToggleGroupItem`, `ToggleGroupIcon` | Single or multi toggle groups | Custom segmented controls |
|
|
235
|
+
| `Tooltip` | Short hover/focus help | Persistent helper text or custom hover cards |
|
|
236
|
+
|
|
237
|
+
`StyledText` props: `semantic` (`title`, `heading`, `subheading`, `body`,
|
|
238
|
+
`caption`, `label`, `eyebrow`), `size` (`xs`, `sm`, `base`, `body`, `lg`, `xl`,
|
|
239
|
+
`xxl`, `display`), `fontWeight` (`light`–`bold`), `variant` (`sansSerif`,
|
|
240
|
+
`serif`, `mono`), `align`, `text`, `tx`, `txOptions`, `selectable` (default
|
|
241
|
+
`true`). Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
242
|
+
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText`, `SerifText`,
|
|
243
|
+
`SansSerifText`, `SerifBoldText`, `SansSerifBoldText`.
|
|
241
244
|
|
|
242
245
|
## Component Selection Rules
|
|
243
246
|
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
- Use `Avatar` with both `source` and `name` whenever both exist: `name` supplies the initials shown when the image is absent, still loading, or failed, plus the default accessibility label. Never hand-roll a circle with a nested `Image` and initials `Text`. Inside `AvatarGroup`, set `size`/`shape` on the group — children inherit them and gain the ring; the group's count is a hidden summary node, so each member stays individually announceable and the group needs no `accessible` wrapper.
|
|
252
|
-
- Pair a standalone `Label` with its control using two DISTINCT ids: `nativeID` is the label's own id, `htmlFor` is the input's id (`<Label nativeID="email-label" htmlFor="email-input">` + `<TextInput nativeID="email-input" />`). Putting the same id on both renders duplicate ids on web and associates nothing. Prefer `TextInput`'s own `label` prop when you don't need a separate label element.
|
|
253
|
-
- Use `Progress` for real progress or indeterminate long-running work.
|
|
254
|
-
- Use `Drawer.Header` with `icon`, `title`, and `action` for a compact app-brand row; place `Drawer.ToggleCollapse` in `action` for a trailing rail control.
|
|
247
|
+
- `Button` commands · `Toggle` one pressed state · `ToggleGroup` a related set · `Switch` binary settings · `RadioGroup` few exclusive choices · `Select` longer option sets.
|
|
248
|
+
- `Dialog` blocking decisions · `Popover` contextual controls · `Tooltip` short explanations · `DropdownMenu` action lists.
|
|
249
|
+
- `Card` individual repeated or framed items, never a wrapper around full page sections · `EmptyState` no-data or recoverable errors · `Skeleton` loading content with stable layout · `Progress` real or indeterminate progress.
|
|
250
|
+
- `Carousel` for a horizontal snap row of a known, small set of slides. It renders every child (no virtualization), so slides survive into the exported HTML shell and the first client frame; use `FlatList` for large or unbounded data. Dots are pressable and jump to their slide. A fractional `itemWidth` (default `0.85`) measures the viewport until the first layout, so pass absolute pixels (`> 1`) when the parent is narrower than the window and the first frame matters.
|
|
251
|
+
- `Avatar` with both `source` and `name` whenever both exist: `name` supplies the initials shown when the image is absent, still loading, or failed, plus the default accessibility label. Inside `AvatarGroup`, set `size`/`shape` on the group — children inherit them and gain the ring; the group's count is a hidden summary node, so each member stays individually announceable and the group needs no `accessible` wrapper.
|
|
252
|
+
- Pair a standalone `Label` with its control using two DISTINCT ids: `nativeID` is the label's own id, `htmlFor` is the input's id (`<Label nativeID="email-label" htmlFor="email-input">` + `<TextInput nativeID="email-input" />`). One id on both renders duplicate ids on web and associates nothing. Prefer `TextInput`'s own `label` prop when no separate label element is needed.
|
|
253
|
+
- `Drawer.Header` takes `icon`, `title`, and `action` slots for a compact app-brand row; put `Drawer.ToggleCollapse` in `action` for a trailing rail control. `Drawer.Content` owns safe-area top/bottom padding — do not duplicate it in children.
|
|
255
254
|
|
|
256
255
|
## Minimal Examples
|
|
257
256
|
|
|
@@ -311,13 +310,12 @@ import { notify } from "@mrmeg/expo-ui/state";
|
|
|
311
310
|
<Progress value={65} variant="accent" />
|
|
312
311
|
<EmptyState icon="inbox" title="No messages" description="New messages will appear here." />
|
|
313
312
|
|
|
314
|
-
// Convenience helpers
|
|
315
313
|
notify.success("Saved", { messages: ["Your changes were saved."] });
|
|
316
314
|
notify.error("Upload failed");
|
|
317
315
|
notify.warning("Connection slow");
|
|
318
316
|
notify.info("Copied to clipboard");
|
|
319
317
|
|
|
320
|
-
// Loading spinner — stays until replaced or hidden
|
|
318
|
+
// Loading spinner — no auto-dismiss; stays until replaced or hidden
|
|
321
319
|
notify.loading("Uploading…");
|
|
322
320
|
notify.hide();
|
|
323
321
|
|
|
@@ -331,3 +329,7 @@ await notify.promise(saveProfile(), {
|
|
|
331
329
|
error: "Could not save profile", // or (err) => err.message
|
|
332
330
|
});
|
|
333
331
|
```
|
|
332
|
+
|
|
333
|
+
Notifications auto-dismiss after 4s (`DEFAULT_NOTIFICATION_DURATION`) unless a
|
|
334
|
+
`duration` is passed; `duration: 0` keeps one up until dismissed. `position` is
|
|
335
|
+
`"top"` (default) or `"bottom"`.
|