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