@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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,95 @@
|
|
|
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
|
+
|
|
64
|
+
## [0.23.0]
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- **Density tokens.** `spacing` gains `smd` (12) and `mdl` (20) scale steps plus
|
|
69
|
+
semantic layout tokens: `screenPadding`, `sectionSpacing` (now 24),
|
|
70
|
+
`cardPadding`, `dialogPadding`, `rowPaddingY`/`rowPaddingX`, `rowGap`,
|
|
71
|
+
`rowMinHeight`, and `formRowMinHeight`. Components read these so a single
|
|
72
|
+
retune shifts the whole library's density.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **Compact surfaces and rows.** `Card` header/content/footer padding drops
|
|
77
|
+
from 24 to 16 with a 16px title. `Item` rows use 10/16 padding and a 12px
|
|
78
|
+
gap, with a 40px visual min height on web while native keeps the 44pt touch
|
|
79
|
+
target. `Checkbox` and `RadioGroup` rows are 32px on web (native unchanged).
|
|
80
|
+
`Dialog`/`AlertDialog` padding is 20, `StatCard` padding is 16,
|
|
81
|
+
`EmptyState` vertical padding is 32, and `Accordion` triggers use 12px
|
|
82
|
+
vertical padding.
|
|
83
|
+
- **Dialog spacing is symmetric.** `DialogContent` and `AlertDialogContent`
|
|
84
|
+
space their children with a 16px gap instead of fixed header/footer
|
|
85
|
+
margins, so a header-only dialog no longer reads bottom-heavy.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **Dialog could not be dismissed on web.** The web overlay primitive wrapped
|
|
90
|
+
the backdrop in a react-native-web `Pressable` whose click handler stopped
|
|
91
|
+
propagation, so outside presses (mouse and touch) and Escape never reached
|
|
92
|
+
Radix. `DialogContent` now renders the overlay `asChild` on web; native is
|
|
93
|
+
unchanged.
|
|
94
|
+
|
|
6
95
|
## [0.22.0]
|
|
7
96
|
|
|
8
97
|
### 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
|
-
```
|
|
27
|
-
|
|
28
|
-
Use only exported package paths: root, `components`, `components/*`,
|
|
29
|
-
`constants`, `constants/*`, `hooks`, `hooks/*`, `state`, and `lib`. Do not
|
|
30
|
-
import from `@mrmeg/expo-ui/dist/*` or from a source checkout path.
|
|
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.
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
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,78 +100,72 @@ 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 }) => ({
|
|
158
147
|
card: {
|
|
159
148
|
backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
160
|
-
padding: spacing.
|
|
149
|
+
padding: spacing.cardPadding,
|
|
161
150
|
},
|
|
162
151
|
}));
|
|
163
152
|
```
|
|
164
153
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`
|
|
154
|
+
Layout spacing uses semantic density tokens, not raw scale steps:
|
|
155
|
+
`spacing.screenPadding` (16) for screen and block gutters, `spacing.cardPadding`
|
|
156
|
+
(16) for bordered panels, `spacing.sectionSpacing` (24) between grouped lists,
|
|
157
|
+
`spacing.dialogPadding` (20) for dialogs, and `spacing.rowPaddingY`/`rowPaddingX`
|
|
158
|
+
(10/16) with `spacing.rowGap` (12) for list rows. `Item` already applies the row
|
|
159
|
+
tokens and keeps a 44px hit area on native while rendering 40px on web.
|
|
160
|
+
|
|
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.
|
|
169
165
|
|
|
170
166
|
`useTheme()` resolves colors in three layers, last wins: package defaults →
|
|
171
|
-
global brand (`useThemeStore.getState().setColors(overrides)`) → scoped
|
|
172
|
-
|
|
167
|
+
global brand (`useThemeStore.getState().setColors(overrides)`) → scoped override
|
|
168
|
+
(`ThemeColorScope`). Each override is `{ light?, dark? }` of
|
|
173
169
|
`Partial<ThemeColors>`; only the keys you pass are replaced. Call `setColors`
|
|
174
170
|
once to forward the app's brand palette globally. Wrap a subtree in
|
|
175
171
|
`<ThemeColorScope colors={{ light, dark }}>` for transient per-subtree theming
|
|
@@ -178,73 +174,83 @@ React context, scoped keys win over the global brand inside it, and nested
|
|
|
178
174
|
scopes merge (inner wins, outer fills in). With no override at either layer,
|
|
179
175
|
`useTheme()` returns the base theme by reference.
|
|
180
176
|
|
|
181
|
-
Fonts and shape have matching global injection points. `setFonts({ families:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
189
186
|
|
|
190
187
|
## Component Use-Case Index
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `Drawer` | Side panels and drawer navigation | Custom sliding panels |
|
|
211
|
-
| `DropdownMenu` | Menus and command lists | Homemade popover menus |
|
|
212
|
-
| `EmptyState` | No-data or recoverable error regions | One-off empty placeholders |
|
|
213
|
-
| `ErrorBoundary` | React render error fallback | Unhandled screen crashes |
|
|
214
|
-
| `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors |
|
|
215
|
-
| `InputOTP` | Verification code entry |
|
|
216
|
-
| `
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
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`.
|
|
234
244
|
|
|
235
245
|
## Component Selection Rules
|
|
236
246
|
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
- 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.
|
|
245
|
-
- 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.
|
|
246
|
-
- Use `Progress` for real progress or indeterminate long-running work.
|
|
247
|
-
- 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.
|
|
248
254
|
|
|
249
255
|
## Minimal Examples
|
|
250
256
|
|
|
@@ -304,13 +310,12 @@ import { notify } from "@mrmeg/expo-ui/state";
|
|
|
304
310
|
<Progress value={65} variant="accent" />
|
|
305
311
|
<EmptyState icon="inbox" title="No messages" description="New messages will appear here." />
|
|
306
312
|
|
|
307
|
-
// Convenience helpers
|
|
308
313
|
notify.success("Saved", { messages: ["Your changes were saved."] });
|
|
309
314
|
notify.error("Upload failed");
|
|
310
315
|
notify.warning("Connection slow");
|
|
311
316
|
notify.info("Copied to clipboard");
|
|
312
317
|
|
|
313
|
-
// Loading spinner — stays until replaced or hidden
|
|
318
|
+
// Loading spinner — no auto-dismiss; stays until replaced or hidden
|
|
314
319
|
notify.loading("Uploading…");
|
|
315
320
|
notify.hide();
|
|
316
321
|
|
|
@@ -324,3 +329,7 @@ await notify.promise(saveProfile(), {
|
|
|
324
329
|
error: "Could not save profile", // or (err) => err.message
|
|
325
330
|
});
|
|
326
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"`.
|