@mrmeg/expo-ui 0.25.0 → 0.26.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 +118 -0
- package/LLM_USAGE.md +13 -1
- package/README.md +89 -18
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/BottomSheet.d.ts +1 -1
- package/dist/components/BottomSheet.js +28 -51
- package/dist/components/DismissKeyboard.d.ts +8 -6
- package/dist/components/DismissKeyboard.js +9 -7
- package/dist/components/DropdownMenu.js +1 -1
- package/dist/components/Icon.d.ts +24 -22
- package/dist/components/Icon.js +26 -3
- package/dist/components/Notification.js +3 -3
- package/dist/components/Popover.js +1 -1
- package/dist/components/Select.js +1 -1
- package/dist/components/StatCard.d.ts +1 -1
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +55 -35
- package/dist/components/Tooltip.js +1 -1
- package/dist/components/iconRegistry.generated.d.ts +148 -0
- package/dist/components/iconRegistry.generated.js +296 -0
- package/dist/components/keyboardDismiss.js +126 -55
- package/dist/components/keyboardFocusRegistry.d.ts +14 -4
- package/dist/components/keyboardFocusRegistry.js +13 -5
- package/dist/components/nativeTextField.android.d.ts +3 -0
- package/dist/components/nativeTextField.android.js +226 -0
- package/dist/components/nativeTextField.d.ts +14 -0
- package/dist/components/nativeTextField.js +14 -0
- package/dist/constants/spacing.d.ts +2 -2
- package/dist/constants/spacing.js +2 -2
- package/dist/design-system.json +152 -3
- package/dist/hooks/useResources.d.ts +5 -18
- package/dist/hooks/useResources.js +5 -43
- package/dist/state/themeStore.d.ts +1 -1
- package/llms-full.md +3 -3
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,124 @@
|
|
|
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
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.26.0]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **`Icon` renders Lucide.** `<Icon name>` draws `lucide-react-native` SVGs
|
|
13
|
+
instead of the `@expo/vector-icons` Feather font. Lucide is a maintained
|
|
14
|
+
superset of Feather in the same 24px, 2px round-stroke style, so glyphs look
|
|
15
|
+
the same, but the set is ten times larger and nothing is loaded as a font.
|
|
16
|
+
`IconName` is now the union of kebab-case Lucide names in the package
|
|
17
|
+
registry (`src/components/icon-names.json`, generated into
|
|
18
|
+
`iconRegistry.generated.ts` by `bun run ui:icons`); only those icons ship in
|
|
19
|
+
the bundle. The `component` escape hatch is unchanged and accepts any
|
|
20
|
+
`LucideIcon` (`CustomIconComponentProps.style` is a view style). Names Lucide
|
|
21
|
+
renamed, which consumers must update: `alert-circle` → `circle-alert`,
|
|
22
|
+
`check-circle` → `circle-check-big`, `x-circle` → `circle-x`,
|
|
23
|
+
`alert-triangle` → `triangle-alert`, `help-circle` → `circle-question-mark`,
|
|
24
|
+
`edit` → `square-pen`, `edit-2` → `pen`, `edit-3` → `pencil`,
|
|
25
|
+
`sliders` → `sliders-horizontal`, `grid` → `layout-grid`, `home` → `house`,
|
|
26
|
+
`unlock` → `lock-open`, `trash-2` → `trash`, `tool` → `wrench`,
|
|
27
|
+
`layout` → `panels-top-left`, `bar-chart-2` → `chart-no-axes-column`,
|
|
28
|
+
`plus-circle` → `circle-plus`, `stop-circle` → `circle-stop`,
|
|
29
|
+
`more-vertical` → `ellipsis-vertical`, `more-horizontal` → `ellipsis`,
|
|
30
|
+
`sidebar` → `panel-left`, `smile` → `face-slightly-smiling`. Brand glyphs
|
|
31
|
+
(`github`, `chrome`, `facebook`, `instagram`, `linkedin`, `twitter`) have
|
|
32
|
+
no Lucide equivalent; pass your own SVG through `component`. Every other
|
|
33
|
+
Feather name resolves unchanged. A name outside the registry is a type error.
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- **`@expo/vector-icons` is no longer a dependency**, and `useResources` no
|
|
38
|
+
longer loads or registers an icon font. `ensureIconFontRegistered` is gone:
|
|
39
|
+
SVG icons render in export-time HTML as `<svg>`, so the hydration fix it
|
|
40
|
+
existed for has nothing left to fix.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Peers `lucide-react-native` (`>=1.46 <2`) and `react-native-svg`
|
|
45
|
+
(`>=15 <16`).** Install `react-native-svg` with `npx expo install` so it
|
|
46
|
+
matches your SDK. They are peers, not dependencies, so an app that imports
|
|
47
|
+
Lucide components for `<Icon component>` shares one copy.
|
|
48
|
+
- **`icons.names` in `dist/design-system.json`**, the registry's name list, so
|
|
49
|
+
`@mrmeg/eslint-plugin-expo-ui` can validate icon names against an installed
|
|
50
|
+
release (a lint rule is a follow-up). `schemaVersion` stays `1`.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **Popover, DropdownMenu, Select, and Tooltip content receives Android
|
|
55
|
+
touches.** The fade wrapper between each overlay and its absolutely
|
|
56
|
+
positioned content now fills the overlay with `pointerEvents="box-none"`, so
|
|
57
|
+
the card is inside its parent's bounds. Android dispatches `ACTION_DOWN` and
|
|
58
|
+
accessibility traversal only to children inside their parent's bounds, so
|
|
59
|
+
native controls (`Switch`, `TextInput`) inside a `PopoverContent` ignored
|
|
60
|
+
taps and the content was missing from the accessibility tree. Tap-away still
|
|
61
|
+
closes.
|
|
62
|
+
- **Sheet taps reach controls while the keyboard is up.** `BottomSheet.Content`
|
|
63
|
+
no longer mounts an absolute-fill "Dismiss keyboard" overlay that swallowed
|
|
64
|
+
the first tap on every button, tab and field inside the sheet. It spreads the
|
|
65
|
+
same release-based tap-away boundary `DismissKeyboard` uses onto its content
|
|
66
|
+
column: the boundary never claims the touch, an unclaimed dead-space tap
|
|
67
|
+
dismisses on release only, drags and multitouch do not dismiss, and presence
|
|
68
|
+
still comes from the focus registry so it works in the sheet's isolated
|
|
69
|
+
native window.
|
|
70
|
+
- **`dismissKeyboard()` clears the focus registry immediately.**
|
|
71
|
+
`dismissKeyboardFocusedInput()` blurs the registered field and drops its
|
|
72
|
+
registration in the same call instead of waiting for a native blur callback
|
|
73
|
+
that may never arrive (submit handlers, isolated sheet window, iOS
|
|
74
|
+
secure/plain handoff), so no stale focus presence survives a dismissal. A
|
|
75
|
+
field that takes focus during the blur keeps its registration.
|
|
76
|
+
- **Android sheet bodies fill the rendered sheet.** The content column's
|
|
77
|
+
`maxHeight` cap (last snap point as a window percentage) now applies only
|
|
78
|
+
off Android, where the iOS SwiftUI host needs it; Material's
|
|
79
|
+
`ModalBottomSheet` ignores percentage snap points and its host already bounds
|
|
80
|
+
the column, so a `55%` sheet no longer shows a blank strip below a capped
|
|
81
|
+
`BottomSheet.Body`.
|
|
82
|
+
- **Android secure fields declare a password keyboard.** `TextInput` with
|
|
83
|
+
`secureTextEntry` now reports `textPassword` to the IME (or `numberPassword`
|
|
84
|
+
for the `number-pad`, `decimal-pad`, `numeric` and `phone-pad` keyboards), so
|
|
85
|
+
Gboard shows no suggestion strip and learns nothing typed; autocorrect is off
|
|
86
|
+
and capitalization is `none` unless the props say otherwise. Revealing the
|
|
87
|
+
text with the eye toggle keeps the password keyboard and only drops the
|
|
88
|
+
masking, so focus, selection and the input connection survive exactly as in
|
|
89
|
+
0.25.1. The Android field is now package-owned — a port of `@expo/ui` 58.0.2's
|
|
90
|
+
universal Android `TextInput` on top of `@expo/ui/jetpack-compose`'s
|
|
91
|
+
`BasicTextField` — because the universal field offers no way to set Compose's
|
|
92
|
+
password keyboard type. iOS keeps `@expo/ui`'s universal field.
|
|
93
|
+
|
|
94
|
+
## [0.25.1]
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **Android password toggles retain the native field.** The secure/plain Host
|
|
99
|
+
keys previously replaced the Compose field on every eye press, leaving the
|
|
100
|
+
keyboard visible but disconnecting subsequent typing. Android now changes
|
|
101
|
+
password transformation on one mounted Host/input/ref, preserving focus,
|
|
102
|
+
selection, and the input connection. iOS retains its two-field handoff.
|
|
103
|
+
- **Non-scrolling drags no longer trigger tap-away dismissal.** `DismissKeyboard`
|
|
104
|
+
gives its boundary sole tap-dismiss ownership (`keyboardShouldPersistTaps="always"`)
|
|
105
|
+
instead of letting RN ScrollView independently blur a registered hosted field
|
|
106
|
+
on release. Both tap-away dismissal and text-surface focus check final travel
|
|
107
|
+
even when move events are missed, and cancel multitouch. Actual scrolling still
|
|
108
|
+
dismisses through the existing platform-specific scroll behavior.
|
|
109
|
+
|
|
110
|
+
Consumer note: raw `Pressable` labels made from `StyledText` must set
|
|
111
|
+
`selectable={false}`. On Android selectable label text can take native focus from
|
|
112
|
+
an input even when the press handler does not dismiss. The template's auth
|
|
113
|
+
controls and standalone scroll shell are corrected separately in app code;
|
|
114
|
+
updating this package does not update a consuming app's copied forms.
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
- **`spacing.radiusMd` is 10 instead of 12.** Buttons, inputs, selects, toggles,
|
|
119
|
+
tabs, menus, popovers, tooltips, list items and every other component on the
|
|
120
|
+
default radius render slightly squarer. `radiusSm` (8) and `radiusLg` (14) are
|
|
121
|
+
unchanged, so the scale still increases monotonically. Apps that want the old
|
|
122
|
+
look can pass `borderRadius: 12` through `setShape` or a `style` override.
|
|
123
|
+
|
|
6
124
|
## [0.25.0]
|
|
7
125
|
|
|
8
126
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -74,6 +74,13 @@ avoidance is `react-native-keyboard-controller`, so mount its `KeyboardProvider`
|
|
|
74
74
|
above `UIProvider`; use `KeyboardAvoidingView` directly only for a subtree with
|
|
75
75
|
custom behavior.
|
|
76
76
|
|
|
77
|
+
`DismissKeyboard` owns tap dismissal; its ScrollView uses
|
|
78
|
+
`keyboardShouldPersistTaps="always"` so RN cannot independently blur a hosted
|
|
79
|
+
field after a non-scrolling drag. Use `always` for your own scroll view inside
|
|
80
|
+
that boundary too. Dead-space taps dismiss on release; movement and multitouch
|
|
81
|
+
cancel. Actual scrolling retains platform-specific dismissal. Submit handlers
|
|
82
|
+
call `dismissKeyboard()` explicitly.
|
|
83
|
+
|
|
77
84
|
`BottomSheet` renders the platform's native sheet through `@expo/ui` (iOS
|
|
78
85
|
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`). The
|
|
79
86
|
platform owns gestures and keyboard avoidance: `swipeEnabled`, `avoidKeyboard`,
|
|
@@ -221,7 +228,7 @@ Check this before creating a new app-local primitive. All components come from
|
|
|
221
228
|
| `DropdownMenu` | Menus and command lists | Homemade popover menus |
|
|
222
229
|
| `EmptyState` | No-data or recoverable error regions | One-off empty placeholders |
|
|
223
230
|
| `ErrorBoundary` | React render error fallback | Unhandled screen crashes |
|
|
224
|
-
| `Icon` |
|
|
231
|
+
| `Icon` | Lucide or custom icons with theme tokens | Raw vector icons with hardcoded colors |
|
|
225
232
|
| `InputOTP` | Verification code entry | Several manually managed text inputs |
|
|
226
233
|
| `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions` | List / settings rows with density tokens | Hand-rolled row `View`s |
|
|
227
234
|
| `KeyboardAvoidingView` | Native keyboard-aware layout root | Repeated app-local keyboard wrappers |
|
|
@@ -255,6 +262,11 @@ Check this before creating a new app-local primitive. All components come from
|
|
|
255
262
|
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText`, `SerifText`,
|
|
256
263
|
`SansSerifText`, `SerifBoldText`, `SansSerifBoldText`.
|
|
257
264
|
|
|
265
|
+
Set `selectable={false}` on text labels inside app-owned `Pressable`s. Selectable
|
|
266
|
+
Android text can take input focus even if the press handler never dismisses the
|
|
267
|
+
keyboard. Package controls already scope label selectability; ordinary text
|
|
268
|
+
should remain selectable.
|
|
269
|
+
|
|
258
270
|
## Component Selection Rules
|
|
259
271
|
|
|
260
272
|
- `Button` commands · `Toggle` one pressed state · `ToggleGroup` a related set · `Switch` binary settings · `RadioGroup` few exclusive choices · `Select` longer option sets.
|
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ Web 0.21. Install these peers at the versions your Expo SDK recommends: `expo`,
|
|
|
32
32
|
`react-native-web`, `react-native-gesture-handler`,
|
|
33
33
|
`react-native-keyboard-controller`, `react-native-safe-area-context`,
|
|
34
34
|
`react-native-screens`, `@react-native-async-storage/async-storage`, `zustand`.
|
|
35
|
+
`Icon` renders [Lucide](https://lucide.dev/icons) SVGs, so also install its two
|
|
36
|
+
peers: `npx expo install react-native-svg` (a native module, pinned by your
|
|
37
|
+
SDK) and `bun add lucide-react-native` (`>=1.46 <2`).
|
|
35
38
|
|
|
36
39
|
`@rn-primitives/*` and `@expo-google-fonts/inter` are package dependencies, not
|
|
37
40
|
peers. Animations use React Native `Animated`. `BottomSheet`, `Slider`, and
|
|
@@ -110,6 +113,20 @@ For a subtree with custom keyboard behavior, use `KeyboardAvoidingView`
|
|
|
110
113
|
directly (`behavior`, `automaticOffset`, `contentContainerStyle`,
|
|
111
114
|
`keyboardVerticalOffset`).
|
|
112
115
|
|
|
116
|
+
### Keyboard dismissal
|
|
117
|
+
|
|
118
|
+
`DismissKeyboard` owns tap-away dismissal without claiming touches. Its scroll
|
|
119
|
+
view uses `keyboardShouldPersistTaps="always"`: RN's `handled` policy can blur
|
|
120
|
+
Expo-hosted fields independently when a finger travels without scrolling.
|
|
121
|
+
Single-finger dead-space taps dismiss on release; movement past 10 logical units
|
|
122
|
+
and multitouch cancel the tap. Real scrolling still dismisses (interactive on
|
|
123
|
+
iOS, an explicit scroll-begin handler on Android).
|
|
124
|
+
|
|
125
|
+
If a form supplies its own ScrollView inside `DismissKeyboard scrollable={false}`
|
|
126
|
+
or an app-level `useKeyboardDismissResponder` boundary, use `always` there too.
|
|
127
|
+
Avoid nested keyboard-avoiding wrappers; the package reuses `UIProvider`'s owner.
|
|
128
|
+
Call `dismissKeyboard()` explicitly from submit handlers.
|
|
129
|
+
|
|
113
130
|
## Theme System
|
|
114
131
|
|
|
115
132
|
```tsx
|
|
@@ -210,7 +227,7 @@ too, because @react-navigation parses them with a color library.
|
|
|
210
227
|
|
|
211
228
|
`spacing` exposes an 8px base scale (`xxs` 2, `xs` 4, `sm` 8, `smd` 12, `md`
|
|
212
229
|
16, `mdl` 20, `lg` 24, `xl` 32, `xxl` 48, `xxxl` 64), radii (`radiusNone` 0,
|
|
213
|
-
`radiusXs` 4, `radiusSm` 8, `radiusMd`
|
|
230
|
+
`radiusXs` 4, `radiusSm` 8, `radiusMd` 10, `radiusLg` 14, `radiusXl` 18,
|
|
214
231
|
`radius2xl` 24, `radiusFull` 9999), icon sizes (`iconXs` 12, `iconSm` 16,
|
|
215
232
|
`iconMd` 24, `iconLg` 32, `iconXl` 48), and the semantic density tokens package
|
|
216
233
|
components read from. Prefer the semantic names so a density tune lands
|
|
@@ -300,6 +317,10 @@ import { BodyText, CaptionText, HeadingText, StyledText } from "@mrmeg/expo-ui/c
|
|
|
300
317
|
- `selectable`: defaults to `true`; package controls disable it for labels and
|
|
301
318
|
interactive chrome where accidental drag selection would feel broken.
|
|
302
319
|
|
|
320
|
+
For app-owned `Pressable` labels, explicitly set `selectable={false}` on the
|
|
321
|
+
nested `StyledText`. On Android selectable label text can take focus and the IME
|
|
322
|
+
connection from an active input. Preserve selection for ordinary readable text.
|
|
323
|
+
|
|
303
324
|
Aliases: `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`,
|
|
304
325
|
`BodyText`, `CaptionText`, `LabelText`, `EyebrowText`, `MonoText` (code, IDs,
|
|
305
326
|
tabular figures), `SerifText`, `SansSerifText`, `SerifBoldText`,
|
|
@@ -325,9 +346,10 @@ Inter is the sans face on every platform, in four static weights
|
|
|
325
346
|
from the bundled `@expo-google-fonts/inter`. Serif is Georgia; mono is the
|
|
326
347
|
platform system monospace.
|
|
327
348
|
|
|
328
|
-
`useResources()` loads those four weights
|
|
329
|
-
|
|
330
|
-
`
|
|
349
|
+
`useResources()` loads those four weights on native, so `StyledText`'s
|
|
350
|
+
`light`–`bold` range resolves to real files instead of a faked OS bold. Icons
|
|
351
|
+
are SVG (`lucide-react-native`), so there is no icon font to load on any
|
|
352
|
+
platform. On web
|
|
331
353
|
it injects one Google Fonts Inter stylesheet (all four weights) after hydration
|
|
332
354
|
unless the app already added it; web weight differentiation then comes from a
|
|
333
355
|
numeric `fontWeight` on the shared `"Inter"` family. For better first paint in
|
|
@@ -373,7 +395,7 @@ When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
|
373
395
|
Inter faces entirely (native `.ttf`s and the web stylesheet). For the skip to
|
|
374
396
|
apply, call `setFonts` before the hook mounts (module scope, or ahead of
|
|
375
397
|
rendering the root); a later call still re-skins all text, it just doesn't
|
|
376
|
-
prevent the download.
|
|
398
|
+
prevent the download.
|
|
377
399
|
|
|
378
400
|
| `webWeightStrategy` | Use when | Effect |
|
|
379
401
|
|---|---|---|
|
|
@@ -414,7 +436,7 @@ building a new primitive.
|
|
|
414
436
|
| `DropdownMenu` | Menus and command lists | Row actions, account menu, sort menu, checkbox/radio groups |
|
|
415
437
|
| `EmptyState` | No-data or recoverable error regions | Empty inbox, no results, failed list load |
|
|
416
438
|
| `ErrorBoundary` | React render error fallback | Route-level and feature-level fallbacks |
|
|
417
|
-
| `Icon` |
|
|
439
|
+
| `Icon` | Lucide or custom icons on theme tokens | Button accessories, menu icons, status glyphs |
|
|
418
440
|
| `InputOTP` | Verification code entry | Email/SMS codes, MFA, invite codes |
|
|
419
441
|
| `Item` | List and settings rows on the density tokens | Settings lists, inbox rows, pickers, detail rows |
|
|
420
442
|
| `KeyboardAvoidingView` | Native keyboard-aware layout | Screen roots, composer footers, form-heavy subtrees |
|
|
@@ -440,6 +462,43 @@ building a new primitive.
|
|
|
440
462
|
| `ToggleGroup` | Single or multi toggle groups | Alignment, formatting toolbar, filter chips |
|
|
441
463
|
| `Tooltip` | Short hover/focus help | Icon button labels, field hints, disabled-action reasons |
|
|
442
464
|
|
|
465
|
+
### Icons
|
|
466
|
+
|
|
467
|
+
`Icon` renders `lucide-react-native` — Lucide is a maintained superset of
|
|
468
|
+
Feather in the same 24px, 2px round-stroke style. `name` is typed by
|
|
469
|
+
`IconName`, the union of kebab-case Lucide names in the package registry
|
|
470
|
+
(`src/components/icon-names.json`, about 150 names), so only the icons the package
|
|
471
|
+
and its consumers name ship in the bundle: the root `lucide-react-native`
|
|
472
|
+
entry (1,800+ icons) is never imported. `color` takes a theme color name or a
|
|
473
|
+
literal; `decorative` hides the glyph from assistive tech.
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
import { Icon } from "@mrmeg/expo-ui/components";
|
|
477
|
+
import Rocket from "lucide-react-native/icons/rocket";
|
|
478
|
+
|
|
479
|
+
<Icon name="circle-check-big" color="success" size={16} />
|
|
480
|
+
<Icon component={Rocket} color="accent" />
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Adding an icon: in this repo, add the kebab-case Lucide name to
|
|
484
|
+
`packages/ui/src/components/icon-names.json` (keep it sorted), run
|
|
485
|
+
`bun run ui:icons`, and commit both files — an unknown name fails with the
|
|
486
|
+
alias lines from Lucide's root entry that point at its current name
|
|
487
|
+
(`bun run ui:icons:check` guards freshness in CI). From a consuming app, pass
|
|
488
|
+
any Lucide component through `component` instead; it needs no registry entry.
|
|
489
|
+
Feather-era names that Lucide renamed: `alert-circle` → `circle-alert`,
|
|
490
|
+
`check-circle` → `circle-check-big`, `x-circle` → `circle-x`,
|
|
491
|
+
`alert-triangle` → `triangle-alert`, `help-circle` → `circle-question-mark`,
|
|
492
|
+
`edit` → `square-pen`, `edit-2` → `pen`, `edit-3` → `pencil`,
|
|
493
|
+
`sliders` → `sliders-horizontal`, `grid` → `layout-grid`, `home` → `house`,
|
|
494
|
+
`unlock` → `lock-open`, `trash-2` → `trash`, `tool` → `wrench`,
|
|
495
|
+
`layout` → `panels-top-left`, `bar-chart-2` → `chart-no-axes-column`,
|
|
496
|
+
`plus-circle` → `circle-plus`, `stop-circle` → `circle-stop`,
|
|
497
|
+
`more-vertical` → `ellipsis-vertical`, `more-horizontal` → `ellipsis`,
|
|
498
|
+
`sidebar` → `panel-left`, `smile` → `face-slightly-smiling`. Brand glyphs
|
|
499
|
+
(`github`, `chrome`, `facebook`, `instagram`, `linkedin`, `twitter`) have no
|
|
500
|
+
Lucide equivalent; use `component` with your own SVG.
|
|
501
|
+
|
|
443
502
|
### Compound Parts
|
|
444
503
|
|
|
445
504
|
| Root | Exported Parts |
|
|
@@ -474,7 +533,7 @@ are named exports only.
|
|
|
474
533
|
|
|
475
534
|
| Hook | Returns |
|
|
476
535
|
|------|---------|
|
|
477
|
-
| `useResources()` | `{ loaded, error }` — loads
|
|
536
|
+
| `useResources()` | `{ loaded, error }` — loads the Inter faces (icons are SVG, nothing to load) |
|
|
478
537
|
| `useTheme()` | Active theme, scheme helpers, shadow/contrast/alpha helpers |
|
|
479
538
|
| `useStyles(factory)` | Memoized theme-aware styles plus the `useTheme()` helpers |
|
|
480
539
|
| `useDimensions()` | `{ width, height, orientation, isSmallScreen, isMediumScreen, isLargeScreen }` against `SCREEN_SIZES` (768 / 1000 / 1200) |
|
|
@@ -500,19 +559,30 @@ web, where the window cannot be read during export or hydration.
|
|
|
500
559
|
nativeID="email-input" />`). One id on both renders duplicate ids on web and
|
|
501
560
|
associates nothing. Prefer `TextInput`'s own `label` prop when no separate
|
|
502
561
|
label element is needed.
|
|
562
|
+
- `TextInput` renders a native field on iOS and Android. On Android a
|
|
563
|
+
`secureTextEntry` field declares a password keyboard (`textPassword`, or
|
|
564
|
+
`numberPassword` for numeric keyboards) in both the masked and the revealed
|
|
565
|
+
eye-toggle state, with autocorrect off by default; that Android field is
|
|
566
|
+
package-owned on top of `@expo/ui/jetpack-compose`'s `BasicTextField`, while
|
|
567
|
+
iOS uses `@expo/ui`'s universal field (SwiftUI `SecureField`).
|
|
503
568
|
- `BottomSheet` renders the platform's native sheet through `@expo/ui`: iOS
|
|
504
569
|
SwiftUI `.sheet()`, Android Material3 `ModalBottomSheet`, web `vaul`. The
|
|
505
570
|
platform owns gestures and keyboard avoidance, so `swipeEnabled`,
|
|
506
571
|
`avoidKeyboard`, and `dismissKeyboardOnDrag` are accepted for call-site
|
|
507
|
-
ergonomics but have no effect
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
`
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
572
|
+
ergonomics but have no effect. The sheet hosts its content in a separate
|
|
573
|
+
native window outside the app's `DismissKeyboard`, so `BottomSheet.Content`
|
|
574
|
+
mounts its own tap-away keyboard-dismiss boundary on the content column: it
|
|
575
|
+
never claims the touch (buttons, tabs and other fields fire on the first tap
|
|
576
|
+
with the keyboard up) and dismisses an unclaimed dead-space tap on release,
|
|
577
|
+
matching `DismissKeyboard`. Android has only two snap states (partial /
|
|
578
|
+
expanded) and maps extra snap points to the nearest; because Material ignores
|
|
579
|
+
percentage snap points, the Android body fills the rendered sheet height
|
|
580
|
+
rather than a window-percentage cap. `BottomSheet.Content` also takes
|
|
581
|
+
`backgroundStyle`, merged over the themed card default on the native sheet
|
|
582
|
+
surface (web panel, Android `containerColor`, iOS `presentationBackground`) —
|
|
583
|
+
pass `{ backgroundColor: "transparent" }`, plus a `style` clearing the
|
|
584
|
+
content column's card fill, to let custom chrome such as a glass backdrop
|
|
585
|
+
show through.
|
|
516
586
|
- `Carousel` renders every child (no virtualization), so slides survive into
|
|
517
587
|
the exported HTML shell and the first client frame; use `FlatList` for large
|
|
518
588
|
or unbounded data. An `itemWidth` below 1 (default `0.85`) is a fraction of
|
|
@@ -673,8 +743,9 @@ packed package at the workspace's Expo version, without a custom Metro config.
|
|
|
673
743
|
`ui:build` also writes `dist/design-system.json`
|
|
674
744
|
([`scripts/build-design-system-manifest.mjs`](../../scripts/build-design-system-manifest.mjs)):
|
|
675
745
|
the spacing, radius and icon scales, the palette, the light and dark themes, the
|
|
676
|
-
font variants,
|
|
677
|
-
`src` by the same extractor the lint
|
|
746
|
+
font variants, every component's variant and size values, and `icons.names` (the
|
|
747
|
+
`IconName` registry), serialized out of `src` by the same extractor the lint
|
|
748
|
+
rules use. It is exported as
|
|
678
749
|
`@mrmeg/expo-ui/design-system.json`, and `@mrmeg/eslint-plugin-expo-ui` reads it
|
|
679
750
|
in projects that install this package instead of checking out its sources — which
|
|
680
751
|
is what lets the design-system rules quote this release's presets and tokens.
|
|
@@ -26,7 +26,7 @@ export interface AvatarProps {
|
|
|
26
26
|
source?: ImageSourcePropType;
|
|
27
27
|
/** Display name; supplies the fallback initials and the default accessibility label. */
|
|
28
28
|
name?: string;
|
|
29
|
-
/**
|
|
29
|
+
/** Icon (Lucide name) rendered when there is no image and no derivable initials. @default "user" */
|
|
30
30
|
icon?: IconName;
|
|
31
31
|
/** Size token or explicit pixel diameter. Inherited from `AvatarGroup` when unset. @default "md" */
|
|
32
32
|
size?: AvatarSize;
|
|
@@ -148,7 +148,7 @@ interface BottomSheetCloseProps {
|
|
|
148
148
|
declare function useBottomSheetContext(): BottomSheetContextValue;
|
|
149
149
|
declare function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, snapPoints, closeOnBackdropPress, children, }: BottomSheetProps): React.JSX.Element;
|
|
150
150
|
declare function BottomSheetTrigger({ asChild, children, style: styleOverride }: BottomSheetTriggerProps): React.JSX.Element;
|
|
151
|
-
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
151
|
+
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
152
152
|
declare function BottomSheetHandle({ style }: BottomSheetHandleProps): React.JSX.Element;
|
|
153
153
|
declare function BottomSheetHeader({ children, style, ...props }: BottomSheetHeaderProps): React.JSX.Element;
|
|
154
154
|
declare function BottomSheetBody({ children, style, contentContainerStyle, onContentSizeChange, onLayout, ...props }: BottomSheetBodyProps): React.JSX.Element;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef
|
|
2
|
+
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
|
3
3
|
import { View, Pressable, StyleSheet, ScrollView, Platform, Animated, useWindowDimensions, } from "react-native";
|
|
4
4
|
import { BottomSheet as NativeBottomSheet } from "@expo/ui/community/bottom-sheet";
|
|
5
|
-
import { KeyboardController } from "./keyboardController";
|
|
6
5
|
import { useSafeAreaInsets, initialWindowMetrics } from "react-native-safe-area-context";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
7
|
import { spacing } from "../constants/spacing.js";
|
|
9
8
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
10
9
|
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
11
10
|
import { Icon } from "./Icon.js";
|
|
12
|
-
import {
|
|
11
|
+
import { useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
13
12
|
const DEFAULT_SNAP_POINTS = ["50%"];
|
|
14
13
|
function clampSnapIndex(index, maxSnapIndex) {
|
|
15
14
|
return Math.min(Math.max(index, 0), maxSnapIndex);
|
|
@@ -162,48 +161,6 @@ function SheetCloseButton({ style }) {
|
|
|
162
161
|
], children: _jsx(Icon, { name: "x", size: 22, color: "text" }) }) }));
|
|
163
162
|
}
|
|
164
163
|
// ============================================================================
|
|
165
|
-
// Keyboard dismiss overlay — tap-away dismissal inside the sheet
|
|
166
|
-
// ============================================================================
|
|
167
|
-
/**
|
|
168
|
-
* Tap-away keyboard dismissal for sheet content.
|
|
169
|
-
*
|
|
170
|
-
* The native sheet (SwiftUI `.sheet()` / Material `ModalBottomSheet`) hosts its
|
|
171
|
-
* RN children in a separate native window via @expo/ui's `RNHostView`, OUTSIDE
|
|
172
|
-
* the app's `KeyboardProvider` / `KeyboardDismissBoundary`. Two consequences,
|
|
173
|
-
* both worked around here:
|
|
174
|
-
*
|
|
175
|
-
* 1. Detection: `react-native-keyboard-controller`'s `useKeyboardState` watches
|
|
176
|
-
* the main window and does NOT see a keyboard raised inside the sheet's
|
|
177
|
-
* window (the same isolation that makes `useSafeAreaInsets()` read zero in
|
|
178
|
-
* here). So presence is read from the `keyboardFocusRegistry` instead, which
|
|
179
|
-
* is fed by @expo/ui's own native focus events and is window-independent.
|
|
180
|
-
* 2. Dispatch: RN's JS responder chain (onStartShouldSetResponder*) is NOT
|
|
181
|
-
* dispatched across the `RNHostView` boundary on Android — only `Pressable`
|
|
182
|
-
* hit-testing (native `measure()`) works inside the host. So this is a
|
|
183
|
-
* transparent `Pressable` mounted ONLY while a field is focused, mirroring
|
|
184
|
-
* a tap-to-dismiss overlay in the app. The tap blurs the focused field via
|
|
185
|
-
* its own native ref (`dismissKeyboardFocusedInput`), which resigns the
|
|
186
|
-
* responder regardless of window; `KeyboardController.dismiss()` is a
|
|
187
|
-
* best-effort fallback for the (rare) case with no registered blur handle.
|
|
188
|
-
*
|
|
189
|
-
* iOS doesn't strictly need this (SwiftUI resigns first-responder on outside
|
|
190
|
-
* taps for free), but the same path is harmless and keeps behavior identical.
|
|
191
|
-
*/
|
|
192
|
-
function SheetKeyboardDismissOverlay() {
|
|
193
|
-
const hasFocus = useSyncExternalStore(subscribeKeyboardFocus, hasKeyboardFocusedInput, () => false);
|
|
194
|
-
if (Platform.OS === "web" || !hasFocus)
|
|
195
|
-
return null;
|
|
196
|
-
return (_jsx(Pressable, { style: [StyleSheet.absoluteFill, styles.keyboardDismissOverlay], onPressIn: () => {
|
|
197
|
-
if (!dismissKeyboardFocusedInput())
|
|
198
|
-
KeyboardController.dismiss();
|
|
199
|
-
}, accessibilityLabel: "Dismiss keyboard", accessibilityRole: "button" }));
|
|
200
|
-
}
|
|
201
|
-
const styles = StyleSheet.create({
|
|
202
|
-
keyboardDismissOverlay: {
|
|
203
|
-
zIndex: 999,
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
// ============================================================================
|
|
207
164
|
// Root
|
|
208
165
|
// ============================================================================
|
|
209
166
|
function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, snapPoints = DEFAULT_SNAP_POINTS, closeOnBackdropPress = true, children, }) {
|
|
@@ -296,21 +253,41 @@ function BottomSheetTrigger({ asChild, children, style: styleOverride }) {
|
|
|
296
253
|
// ============================================================================
|
|
297
254
|
function BottomSheetContent({
|
|
298
255
|
// Accepted-but-ignored ergonomics props (platform owns these behaviors):
|
|
299
|
-
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, children, }) {
|
|
256
|
+
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }) {
|
|
300
257
|
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader } = useBottomSheetContext();
|
|
301
258
|
const { theme } = useTheme();
|
|
302
259
|
const dismissDisabled = useDismissDisabled();
|
|
303
260
|
const showClose = useShowClose();
|
|
304
261
|
const { height: winH } = useWindowDimensions();
|
|
262
|
+
// Tap-away keyboard dismissal inside the sheet. The native sheet (SwiftUI
|
|
263
|
+
// `.sheet()` / Material `ModalBottomSheet`) hosts its RN children in a separate
|
|
264
|
+
// native window via @expo/ui's `RNHostView`, OUTSIDE the app's
|
|
265
|
+
// `KeyboardProvider` / `DismissKeyboard`, so an app-level boundary never sees
|
|
266
|
+
// these touches and the sheet mounts its own on the content column. It is the
|
|
267
|
+
// same boundary `DismissKeyboard` uses: presence comes from the
|
|
268
|
+
// `keyboardFocusRegistry` (keyboard-controller's `useKeyboardState` watches
|
|
269
|
+
// the main window and does not observe a keyboard raised in here), the
|
|
270
|
+
// boundary never claims the touch — buttons, tabs and other fields win the
|
|
271
|
+
// responder negotiation and fire on the first tap — and an unclaimed
|
|
272
|
+
// dead-space tap dismisses on release only, through the registered field's
|
|
273
|
+
// own window-independent blur handle.
|
|
274
|
+
const dismissResponderProps = useKeyboardDismissResponder();
|
|
305
275
|
// Boolean open → native imperative index. The root resets snapIndex to the
|
|
306
276
|
// highest point while closed; -1 keeps the native sheet closed.
|
|
307
277
|
const index = open ? snapIndex : -1;
|
|
308
|
-
//
|
|
278
|
+
// iOS: the RN-in-SwiftUI host does NOT clamp our RN column to the sheet's
|
|
309
279
|
// detent height — it lays the column out at its full intrinsic content height,
|
|
310
280
|
// SwiftUI then clips it at the sheet edge (footer/tail fall off-screen), and
|
|
311
281
|
// `flex:1` never gets a definite parent height so the ScrollView can't bound
|
|
312
282
|
// and scroll. Cap the column to the expanded detent height (highest snap
|
|
313
|
-
// point) so `flex:1` resolves and the Body becomes scrollable.
|
|
283
|
+
// point) so `flex:1` resolves and the Body becomes scrollable. Web (vaul)
|
|
284
|
+
// sizes the panel itself from the snap point, so the cap is inert there.
|
|
285
|
+
// Android: the Compose host wraps children in a `flexGrow: 1, height: 0` view
|
|
286
|
+
// that fills `RNHostView`'s measured height, so `flex:1` already resolves,
|
|
287
|
+
// and Material's `ModalBottomSheet` ignores percentage snap points
|
|
288
|
+
// (partial / expanded only) — a window-percentage cap would leave the Body
|
|
289
|
+
// short of the rendered sheet with a blank strip below it. Android therefore
|
|
290
|
+
// keeps `flex:1` alone (see the content column style).
|
|
314
291
|
const expandedSnap = snapPoints[snapPoints.length - 1];
|
|
315
292
|
const detentHeight = typeof expandedSnap === "number"
|
|
316
293
|
? expandedSnap
|
|
@@ -343,16 +320,16 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
343
320
|
backgroundStyle: StyleSheet.flatten([
|
|
344
321
|
{ backgroundColor: theme.colors.card },
|
|
345
322
|
backgroundStyleOverride,
|
|
346
|
-
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { style: [
|
|
323
|
+
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { testID: testID, style: [
|
|
347
324
|
{
|
|
348
325
|
flex: 1,
|
|
349
|
-
maxHeight: detentHeight,
|
|
350
326
|
// Themes the content surface across all platforms regardless of
|
|
351
327
|
// native sheet chrome.
|
|
352
328
|
backgroundColor: theme.colors.card,
|
|
353
329
|
},
|
|
330
|
+
Platform.OS !== "android" && { maxHeight: detentHeight },
|
|
354
331
|
styleOverride,
|
|
355
|
-
], children: [children,
|
|
332
|
+
], ...dismissResponderProps, children: [children, showFloatingClose && (_jsx(SheetCloseButton, { style: {
|
|
356
333
|
// The sheet card sits below the status bar, so no top inset
|
|
357
334
|
// is needed — just clear the sheet's own top edge / handle.
|
|
358
335
|
position: "absolute",
|
|
@@ -12,15 +12,17 @@ type Props = {
|
|
|
12
12
|
* Wrapper for a screen (or form) that dismisses the keyboard when the user taps
|
|
13
13
|
* outside of a text input.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* field
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* The wrapper never claims the touch: buttons, other fields and the focused
|
|
16
|
+
* field's own gestures win. It dismisses only an unclaimed single-finger tap
|
|
17
|
+
* within the travel slop, even if no scroll/move event was delivered. The inner
|
|
18
|
+
* ScrollView uses `always` to leave tap dismissal to this boundary; RN's
|
|
19
|
+
* `handled` policy can independently blur registered hosted inputs on release
|
|
20
|
+
* after a drag that produced no scroll. See `useKeyboardDismissResponder`.
|
|
20
21
|
*
|
|
21
22
|
* A drag on the inner ScrollView also hides the keyboard: `interactive` on iOS
|
|
22
23
|
* (UIKit resigns any first responder, SwiftUI fields included) and an explicit
|
|
23
|
-
* dismiss on drag start on Android,
|
|
24
|
+
* dismiss on drag start on Android, using the package's window-independent
|
|
25
|
+
* native blur handle with a keyboard-controller fallback.
|
|
24
26
|
*/
|
|
25
27
|
export declare function DismissKeyboard({ children, style, avoidKeyboard, scrollable, }: Props): React.JSX.Element;
|
|
26
28
|
export {};
|
|
@@ -6,20 +6,22 @@ import { dismissKeyboard, useKeyboardDismissResponder } from "./keyboardDismiss.
|
|
|
6
6
|
* Wrapper for a screen (or form) that dismisses the keyboard when the user taps
|
|
7
7
|
* outside of a text input.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* field
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The wrapper never claims the touch: buttons, other fields and the focused
|
|
10
|
+
* field's own gestures win. It dismisses only an unclaimed single-finger tap
|
|
11
|
+
* within the travel slop, even if no scroll/move event was delivered. The inner
|
|
12
|
+
* ScrollView uses `always` to leave tap dismissal to this boundary; RN's
|
|
13
|
+
* `handled` policy can independently blur registered hosted inputs on release
|
|
14
|
+
* after a drag that produced no scroll. See `useKeyboardDismissResponder`.
|
|
14
15
|
*
|
|
15
16
|
* A drag on the inner ScrollView also hides the keyboard: `interactive` on iOS
|
|
16
17
|
* (UIKit resigns any first responder, SwiftUI fields included) and an explicit
|
|
17
|
-
* dismiss on drag start on Android,
|
|
18
|
+
* dismiss on drag start on Android, using the package's window-independent
|
|
19
|
+
* native blur handle with a keyboard-controller fallback.
|
|
18
20
|
*/
|
|
19
21
|
export function DismissKeyboard({ children, style, avoidKeyboard = true, scrollable = true, }) {
|
|
20
22
|
const responderProps = useKeyboardDismissResponder();
|
|
21
23
|
const hasKeyboardAvoidance = useKeyboardAvoidance();
|
|
22
|
-
const content = scrollable ? (_jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { flexGrow: 1, justifyContent: "center" }, keyboardShouldPersistTaps: "
|
|
24
|
+
const content = scrollable ? (_jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { flexGrow: 1, justifyContent: "center" }, keyboardShouldPersistTaps: "always", keyboardDismissMode: Platform.OS === "ios" ? "interactive" : "none", onScrollBeginDrag: Platform.OS === "android" ? dismissKeyboard : undefined, showsVerticalScrollIndicator: false, children: children })) : (children);
|
|
23
25
|
if (!avoidKeyboard || hasKeyboardAvoidance) {
|
|
24
26
|
return (_jsx(View, { style: { flex: 1 }, ...responderProps, children: content }));
|
|
25
27
|
}
|
|
@@ -76,7 +76,7 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
|
|
|
76
76
|
return (_jsx(DropdownMenuPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(DropdownMenuPrimitive.Overlay, { style: Platform.select({
|
|
77
77
|
native: StyleSheet.absoluteFill,
|
|
78
78
|
default: undefined,
|
|
79
|
-
}), children: _jsx(AnimatedView, { type: "fade", children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
79
|
+
}), children: _jsx(AnimatedView, { type: "fade", style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
80
80
|
backgroundColor: theme.colors.popover,
|
|
81
81
|
borderWidth: 1,
|
|
82
82
|
borderColor: theme.colors.border,
|