@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 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
- This file ships in the npm package. In a consumer repo, read it from
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 that this package already provides. Import from
9
- `@mrmeg/expo-ui` and compose the exported components in the app.
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
- The root barrel also exports the public surface:
23
-
24
- ```tsx
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
- 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
- near the root when the app uses package feedback or overlay components.
40
- `UIProvider` owns the package `Notification`, `StatusBar`, and default
41
- `@rn-primitives` portal host.
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
- <UIProvider>
64
- {/* App navigation goes here. */}
65
- </UIProvider>
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` mounts the default portal host required before using `Dialog`,
72
- `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`,
73
- `SelectContent`, or `Tooltip`.
74
-
75
- On native, `BottomSheet.Content` avoids the soft keyboard by default with
76
- React Native keyboard events. Pass `avoidKeyboard={false}` to opt out for a
77
- specific sheet.
78
-
79
- `BottomSheet.Content` themes the native sheet surface with the card color.
80
- Pass `backgroundStyle={{ backgroundColor: "transparent" }}` (with a `style`
81
- that clears the content column's card fill) when custom chrome such as a
82
- glass backdrop should show through; omitting it keeps the card background.
83
-
84
- i18n is optional. Do not add app-level i18n setup just to use this package.
85
- Plain children and `text` props work without `i18next` or `react-i18next`.
86
- `tx` props render fallback text when provided and otherwise render the key
87
- until the consumer opts in with a package-local translator. Package-owned
88
- defaults such as notification titles stay human-readable without app i18n:
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`, for buttons.
102
- - Button visible heights are compact: `sm` 28px, `md` 32px, and `lg` 40px.
103
- - Use `Button size="sm"` for compact popover, tooltip, and toolbar triggers; nested `StyledText` inherits the selected Button size.
104
- - Use `notify` plus root-mounted `UIProvider` for transient global feedback. (`globalUIStore` remains available for reactive subscriptions and tests.)
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
- Useful theme tokens include:
108
-
109
- ```tsx
110
- theme.colors.surfaceSunken;
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 color
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` would blend in
138
-
139
- Elevation is a surface-tier ladder, not shadow depth:
140
- `surfaceSunken` (chrome) < `background` (content) < `card`/`popover` (raised)
141
- < `muted` (chips, insets). Border a raised or filled element with
142
- `borderStrong`; `border` is for hairlines on `background`/`card`.
143
-
144
- Use `getShadowStyle()` for package surfaces that need elevation. It supports
145
- `base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
146
- `cardHover`, and `cardSubtle`, returning a cross-platform `boxShadow` value
147
- (RN 0.85 + react-native-web 0.21 deprecate the legacy `shadow*` props). Use
148
- `getFocusRingStyle()` for web focus styling. Keep
149
- web controls compact, but preserve mobile tap comfort with package controls
150
- that already provide native hit slop or 44px touch rows.
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 }`, so components can derive alpha-adjusted
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.md,
149
+ padding: spacing.cardPadding,
161
150
  },
162
151
  }));
163
152
  ```
164
153
 
165
- When the saved theme preference is `system`, the package theme store owns the
166
- OS color-scheme subscription, including web `prefers-color-scheme`. Do not add
167
- app-local Appearance or `matchMedia` listeners for package components; import
168
- `useTheme()`, `useStyles()`, and `useThemeStore` from `@mrmeg/expo-ui`.
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
- override (`ThemeColorScope`). Each override is `{ light?, dark? }` of
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
- { sansSerif?, serif?, mono? }, webWeightStrategy? })` replaces the bundled
183
- faces (Inter/Georgia/system-mono) everywhere text renders; groups and weights
184
- are partial, missing weights fall back to the group's `regular`, and an
185
- overridden `sansSerif` makes `useResources` skip downloading Inter (call
186
- `setFonts` before mount for the skip). `setShape({ button: { borderRadius?,
187
- withShadow? } })` re-shapes Buttons globally (e.g. pill radius 9999, shadow
188
- off). Per-instance props and caller `style` always win over both.
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
- Use this table before creating a new app-local primitive.
193
-
194
- | Component | Use For | Prefer It Instead Of | Common Example Use Cases |
195
- |-----------|---------|----------------------|--------------------------|
196
- | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections |
197
- | `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs |
198
- | `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers | Staggered list rows, revealed panels, animated empty states |
199
- | `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback | Hand-rolled circles with a nested `Image` and initials `Text` | Account menu, comment authors, assignee pickers, overlapping team stacks |
200
- | `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
201
- | `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms |
202
- | `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs |
203
- | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries |
204
- | `Carousel` | Horizontally snapping slide rows with pressable dot indicators | Hand-rolled snap `ScrollView` plus manual offset math | Testimonial strips, onboarding pages, feature highlights, image galleries |
205
- | `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters |
206
- | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text |
207
- | `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user |
208
- | `DismissKeyboard` | Tap-away keyboard dismissal | Screen-level keyboard handling | Forms, search screens, sign-in screens |
209
- | `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers | Screen roots, composer footers, form-heavy subtrees |
210
- | `Drawer` | Side panels and drawer navigation | Custom sliding panels | Filter drawer, app navigation drawer, inspector panel |
211
- | `DropdownMenu` | Menus and command lists | Homemade popover menus | Row actions, account menu, sort menu |
212
- | `EmptyState` | No-data or recoverable error regions | One-off empty placeholders | Empty inbox, no search results, failed list load |
213
- | `ErrorBoundary` | React render error fallback | Unhandled screen crashes | Route-level fallback, feature boundary |
214
- | `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors | Button accessories, empty-state icons, menu icons |
215
- | `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
216
- | `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
217
- | `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms |
218
- | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, bottom-position alerts |
219
- | `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls |
220
- | `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion |
221
- | `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer |
222
- | `Select` | Option menus | Custom dropdowns | Country picker, category selector, status selector |
223
- | `Separator` | Horizontal or vertical dividers | Border-only spacer views | Menu dividers, section dividers, card dividers |
224
- | `Skeleton`, `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` | Loading placeholders | Blank space or generic spinners | List loading, profile card loading, dashboard placeholders |
225
- | `Slider` | Numeric value selection | Custom pan gesture track | Volume, percentage, rating, threshold settings |
226
- | `StatusBar` | Theme-aware native status bar | Per-screen status-bar duplication | Root layout status styling |
227
- | `StyledText` and text aliases | Theme-aware typography | Raw `Text` with hardcoded styles | Titles, headings, labels, body copy, captions |
228
- | `Switch` | Binary settings | Custom toggle switches | Enable notifications, privacy setting, feature toggles |
229
- | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls | Profile sections, report views, settings categories |
230
- | `TextInput` | Text entry | Raw `TextInput` with repeated label/error code | Email/password, search, numeric input, multiline notes |
231
- | `Toggle`, `ToggleIcon` | Pressed/unpressed control | Button with local selected styling | Favorite, mute, bold/italic, view mode button |
232
- | `ToggleGroup`, `ToggleGroupItem`, `ToggleGroupIcon` | Single or multi toggle groups | Custom segmented controls | Alignment, formatting toolbar, filter chips |
233
- | `Tooltip` | Short hover/focus help | Persistent helper text or custom hover cards | Icon button labels, field hints, disabled action explanations |
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
- - Use `Button` for commands, `Toggle` for one pressed state, `ToggleGroup` for related pressed states, and `Switch` for binary settings.
238
- - Use `RadioGroup` for small mutually exclusive choices and `Select` for longer option sets.
239
- - Use `Dialog` for blocking decisions, `Popover` for contextual controls, `Tooltip` for short explanations, and `DropdownMenu` for action lists.
240
- - Use `Card` for individual repeated or framed items, not as a wrapper around full page sections.
241
- - Use `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; reach for `FlatList` when the data set is large or unbounded. Its dots are pressable and jump to their slide. A fractional `itemWidth` 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.
242
- - Use `EmptyState` for no-data or recoverable error regions.
243
- - Use `Skeleton` for loading content with stable layout.
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 (no auto-dismiss)
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"`.