@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LLM_USAGE.md +172 -157
  3. package/README.md +447 -352
  4. package/dist/components/Avatar.js +6 -1
  5. package/dist/components/BottomSheet.js +1 -1
  6. package/dist/components/Button.d.ts +1 -1
  7. package/dist/components/Carousel.js +2 -0
  8. package/dist/components/Dialog.d.ts +15 -93
  9. package/dist/components/Dialog.js +5 -0
  10. package/dist/components/DismissKeyboard.d.ts +14 -3
  11. package/dist/components/DismissKeyboard.js +17 -48
  12. package/dist/components/DropdownMenu.d.ts +5 -34
  13. package/dist/components/DropdownMenu.js +2 -1
  14. package/dist/components/InputOTP.js +6 -1
  15. package/dist/components/Popover.d.ts +11 -11
  16. package/dist/components/Popover.js +3 -2
  17. package/dist/components/Select.d.ts +12 -20
  18. package/dist/components/Select.js +2 -1
  19. package/dist/components/StatusBar.js +2 -6
  20. package/dist/components/StyledText.context.d.ts +1 -1
  21. package/dist/components/StyledText.d.ts +6 -2
  22. package/dist/components/TextInput.d.ts +6 -2
  23. package/dist/components/TextInput.js +189 -42
  24. package/dist/components/Tooltip.d.ts +10 -24
  25. package/dist/components/Tooltip.js +5 -2
  26. package/dist/components/index.d.ts +1 -0
  27. package/dist/components/index.js +1 -0
  28. package/dist/components/keyboardController.d.ts +4 -2
  29. package/dist/components/keyboardController.js +4 -0
  30. package/dist/components/keyboardDismiss.d.ts +37 -0
  31. package/dist/components/keyboardDismiss.js +170 -0
  32. package/dist/components/keyboardFocusRegistry.d.ts +10 -14
  33. package/dist/components/keyboardFocusRegistry.js +8 -24
  34. package/dist/design-system.json +2236 -0
  35. package/dist/hooks/useTheme.d.ts +11 -3
  36. package/llms-full.md +134 -124
  37. package/llms.txt +24 -22
  38. 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
- 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,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
- The root barrel also exports the public surface:
23
-
24
- ```tsx
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
- 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.
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
- 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.
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
- <UIProvider>
64
- {/* App navigation goes here. */}
65
- </UIProvider>
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` 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:
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`, 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.)
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
- 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
- ```
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 color
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` 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.
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 }`, so components can derive alpha-adjusted
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 rather than raw scale steps:
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
- OS color-scheme subscription, including web `prefers-color-scheme`. Do not add
174
- app-local Appearance or `matchMedia` listeners for package components; import
175
- `useTheme()`, `useStyles()`, and `useThemeStore` from `@mrmeg/expo-ui`.
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
- override (`ThemeColorScope`). Each override is `{ light?, dark? }` of
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
- { sansSerif?, serif?, mono? }, webWeightStrategy? })` replaces the bundled
190
- faces (Inter/Georgia/system-mono) everywhere text renders; groups and weights
191
- are partial, missing weights fall back to the group's `regular`, and an
192
- overridden `sansSerif` makes `useResources` skip downloading Inter (call
193
- `setFonts` before mount for the skip). `setShape({ button: { borderRadius?,
194
- withShadow? } })` re-shapes Buttons globally (e.g. pill radius 9999, shadow
195
- off). Per-instance props and caller `style` always win over both.
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
- Use this table before creating a new app-local primitive.
200
-
201
- | Component | Use For | Prefer It Instead Of | Common Example Use Cases |
202
- |-----------|---------|----------------------|--------------------------|
203
- | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections |
204
- | `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs |
205
- | `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers | Staggered list rows, revealed panels, animated empty states |
206
- | `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 |
207
- | `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
208
- | `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms |
209
- | `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs |
210
- | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries |
211
- | `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 |
212
- | `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters |
213
- | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text |
214
- | `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user |
215
- | `DismissKeyboard` | Tap-away keyboard dismissal | Screen-level keyboard handling | Forms, search screens, sign-in screens |
216
- | `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers | Screen roots, composer footers, form-heavy subtrees |
217
- | `Drawer` | Side panels and drawer navigation | Custom sliding panels | Filter drawer, app navigation drawer, inspector panel |
218
- | `DropdownMenu` | Menus and command lists | Homemade popover menus | Row actions, account menu, sort menu |
219
- | `EmptyState` | No-data or recoverable error regions | One-off empty placeholders | Empty inbox, no search results, failed list load |
220
- | `ErrorBoundary` | React render error fallback | Unhandled screen crashes | Route-level fallback, feature boundary |
221
- | `Icon` | Feather or custom icons with theme tokens | Raw vector icons with hardcoded colors | Button accessories, empty-state icons, menu icons |
222
- | `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
223
- | `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
224
- | `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms |
225
- | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, bottom-position alerts |
226
- | `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls |
227
- | `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion |
228
- | `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer |
229
- | `Select` | Option menus | Custom dropdowns | Country picker, category selector, status selector |
230
- | `Separator` | Horizontal or vertical dividers | Border-only spacer views | Menu dividers, section dividers, card dividers |
231
- | `Skeleton`, `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` | Loading placeholders | Blank space or generic spinners | List loading, profile card loading, dashboard placeholders |
232
- | `Slider` | Numeric value selection | Custom pan gesture track | Volume, percentage, rating, threshold settings |
233
- | `StatusBar` | Theme-aware native status bar | Per-screen status-bar duplication | Root layout status styling |
234
- | `StyledText` and text aliases | Theme-aware typography | Raw `Text` with hardcoded styles | Titles, headings, labels, body copy, captions |
235
- | `Switch` | Binary settings | Custom toggle switches | Enable notifications, privacy setting, feature toggles |
236
- | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls | Profile sections, report views, settings categories |
237
- | `TextInput` | Text entry | Raw `TextInput` with repeated label/error code | Email/password, search, numeric input, multiline notes |
238
- | `Toggle`, `ToggleIcon` | Pressed/unpressed control | Button with local selected styling | Favorite, mute, bold/italic, view mode button |
239
- | `ToggleGroup`, `ToggleGroupItem`, `ToggleGroupIcon` | Single or multi toggle groups | Custom segmented controls | Alignment, formatting toolbar, filter chips |
240
- | `Tooltip` | Short hover/focus help | Persistent helper text or custom hover cards | Icon button labels, field hints, disabled action explanations |
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
- - Use `Button` for commands, `Toggle` for one pressed state, `ToggleGroup` for related pressed states, and `Switch` for binary settings.
245
- - Use `RadioGroup` for small mutually exclusive choices and `Select` for longer option sets.
246
- - Use `Dialog` for blocking decisions, `Popover` for contextual controls, `Tooltip` for short explanations, and `DropdownMenu` for action lists.
247
- - Use `Card` for individual repeated or framed items, not as a wrapper around full page sections.
248
- - 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.
249
- - Use `EmptyState` for no-data or recoverable error regions.
250
- - Use `Skeleton` for loading content with stable layout.
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 (no auto-dismiss)
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"`.