@thewhileloop/whileui 4.1.0 → 4.1.2
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/README.md +120 -22
- package/dist/blocks/datepicker/date-range-picker-modal.js +3 -2
- package/dist/blocks/datepicker/datepicker-modal.js +3 -2
- package/dist/blocks/datepicker/time-picker-modal.js +3 -2
- package/dist/blocks/layout/confirm-action-sheet.js +3 -2
- package/dist/blocks/layout/index.d.ts +1 -0
- package/dist/blocks/layout/index.js +1 -0
- package/dist/blocks/layout/loading-state.d.ts +8 -0
- package/dist/blocks/layout/loading-state.js +12 -0
- package/dist/blocks/layout/sheet.js +21 -2
- package/dist/blocks/media/smart-image.js +5 -7
- package/dist/blocks/navigation/drawer-menu.js +10 -55
- package/dist/blocks/navigation/header.d.ts +2 -0
- package/dist/blocks/navigation/header.js +1 -1
- package/dist/components/alert/alert.js +4 -1
- package/dist/components/alert-dialog/alert-dialog.js +3 -2
- package/dist/components/avatar/avatar.d.ts +3 -2
- package/dist/components/avatar/avatar.js +19 -13
- package/dist/components/avatar/index.d.ts +1 -1
- package/dist/components/context-menu/context-menu.js +4 -3
- package/dist/components/dialog/dialog.js +4 -3
- package/dist/components/dropdown-menu/dropdown-menu.js +4 -3
- package/dist/components/hover-card/hover-card.js +5 -1
- package/dist/components/menubar/menubar.js +5 -4
- package/dist/components/popover/popover.js +5 -1
- package/dist/components/select/select.js +36 -15
- package/dist/components/text/text.d.ts +3 -0
- package/dist/components/text/text.js +2 -0
- package/dist/components/toast/toast.js +5 -1
- package/dist/components/toggle/toggle.d.ts +1 -1
- package/dist/components/toggle/toggle.js +1 -1
- package/dist/components/tooltip/tooltip.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/lib/overlay-host.d.ts +26 -0
- package/dist/lib/overlay-host.js +77 -0
- package/dist/lib/overlay-presence.d.ts +15 -0
- package/dist/lib/overlay-presence.js +55 -0
- package/dist/lib/recipes.d.ts +1 -1
- package/dist/lib/recipes.js +24 -5
- package/dist/lib/theme-bridge.d.ts +1 -1
- package/dist/lib/theme-bridge.js +52 -18
- package/dist/metro.cjs +33 -0
- package/dist/metro.d.cts +21 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -11,6 +11,18 @@ production targets — iOS and Android set the interaction baseline. See
|
|
|
11
11
|
[ROADMAP.md](./ROADMAP.md) for the product boundary and extraction rules, and
|
|
12
12
|
[PLAN.md](./PLAN.md) for the supported stack and production contracts.
|
|
13
13
|
|
|
14
|
+
## See it in action
|
|
15
|
+
|
|
16
|
+
One React Native component contract across native and web, with semantic typography,
|
|
17
|
+
token-driven light and dark themes, accessible controls, and application-owned chat
|
|
18
|
+
composition. The same component tree can feel airy and atmospheric or compact and
|
|
19
|
+
editorial by changing tokens alone.
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="./docs/assets/whileui-showcase.gif" width="300" alt="WhileUI Cosmic Aura mobile theme demonstrating light and dark modes, controls, Select, and composable chat" />
|
|
23
|
+
<img src="./docs/assets/whileui-showcase-atelier.gif" width="300" alt="WhileUI Atelier mobile theme demonstrating compact controls, authentication forms, Select, and composable chat" />
|
|
24
|
+
</p>
|
|
25
|
+
|
|
14
26
|
## Installation
|
|
15
27
|
|
|
16
28
|
```bash
|
|
@@ -26,23 +38,26 @@ Use `npm install` instead of `bun add` if you prefer npm.
|
|
|
26
38
|
|
|
27
39
|
### Setup Uniwind (required)
|
|
28
40
|
|
|
29
|
-
1. **metro.config.js** (wrap with
|
|
41
|
+
1. **metro.config.js** (wrap with `withWhileUIMetro`):
|
|
30
42
|
|
|
31
43
|
```js
|
|
32
44
|
const { getDefaultConfig } = require('expo/metro-config');
|
|
33
|
-
const {
|
|
45
|
+
const { withWhileUIMetro } = require('@thewhileloop/whileui/metro');
|
|
34
46
|
|
|
35
47
|
/** @type {import('expo/metro-config').MetroConfig} */
|
|
36
48
|
const config = getDefaultConfig(__dirname);
|
|
37
49
|
|
|
38
|
-
module.exports =
|
|
50
|
+
module.exports = withWhileUIMetro(config, {
|
|
39
51
|
cssEntryFile: './global.css',
|
|
40
52
|
});
|
|
41
53
|
```
|
|
42
54
|
|
|
43
|
-
> `withUniwindConfig(config, options)`
|
|
44
|
-
> config
|
|
45
|
-
> `
|
|
55
|
+
> `withWhileUIMetro` calls Uniwind's `withUniwindConfig(config, options)` for you — same
|
|
56
|
+
> options, config first, `cssEntryFile` a relative path — and keeps `expo-asset`,
|
|
57
|
+
> `expo-font` and `@expo/vector-icons` on the real `react-native`. Without that, Uniwind's
|
|
58
|
+
> import rewrite leaves every icon glyph silently missing on iOS and Android while web
|
|
59
|
+
> renders fine. Pass `exemptFromUniwindRewrite: ['pkg']` for any other package that
|
|
60
|
+
> imports a non-component export from `react-native`. It must be the outermost wrapper.
|
|
46
61
|
|
|
47
62
|
2. **global.css** at app root:
|
|
48
63
|
|
|
@@ -132,6 +147,12 @@ export default function App() {
|
|
|
132
147
|
|
|
133
148
|
> **Note:** If you use Select, Popover, Tooltip, or HoverCard, add `<PortalHost />` at the root of your app (as the last child).
|
|
134
149
|
|
|
150
|
+
Inside a Dialog, AlertDialog, Sheet, ConfirmActionSheet, DrawerMenu, menu or picker, those
|
|
151
|
+
four render into that overlay's own layer automatically, so a Select in a sheet opens above
|
|
152
|
+
the sheet. Android back and Escape on web close the innermost open layer first. Pass
|
|
153
|
+
`portalHost` on their content only to target a `PortalHost` you placed yourself, such as
|
|
154
|
+
inside a navigation-presented modal screen that WhileUI does not host.
|
|
155
|
+
|
|
135
156
|
### Vite + React Native Web + WhileUI setup
|
|
136
157
|
|
|
137
158
|
Use the WhileUI Vite compatibility helper when your app includes portal-based primitives.
|
|
@@ -225,11 +246,15 @@ function MyScreen() {
|
|
|
225
246
|
- **Layout:** `Stack` (vertical), `Row` (horizontal) — both support `gap`, `align`, `justify`
|
|
226
247
|
- **Auth callbacks:** Auth templates use objects: `onSubmit({ email, password })`, `onSubmit({ firstName, lastName, email, password })`, etc. Copy templates from `apps/showcase/templates/auth/`.
|
|
227
248
|
- **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.
|
|
249
|
+
Inside any WhileUI overlay they render into that overlay's layer without configuration.
|
|
228
250
|
- **Motion:** Wrap the root in `<MotionProvider>`; use `useMotionPolicy()` for semantic
|
|
229
251
|
durations, the responsive spring, and reduced-motion behavior.
|
|
230
|
-
- **
|
|
231
|
-
|
|
252
|
+
- **Metro:** `withWhileUIMetro(config, { cssEntryFile: './global.css' })` from
|
|
253
|
+
`@thewhileloop/whileui/metro` — wraps Uniwind and keeps icon fonts loading on device.
|
|
254
|
+
`global.css` at app root, imported in `App.tsx`. Installing from npm also needs
|
|
232
255
|
`@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
|
|
256
|
+
- **States:** `LoadingState`, `EmptyState` and `ErrorState` share one shape; `ErrorState`
|
|
257
|
+
takes `onRetry`. `Text variant="eyebrow"` is the muted section label above a title.
|
|
233
258
|
- **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
|
|
234
259
|
|
|
235
260
|
### Composition grammar
|
|
@@ -372,6 +397,7 @@ Copy from `apps/showcase/templates/auth/`:
|
|
|
372
397
|
| **ErrorBoundary** | React ErrorBoundary that renders ErrorState by default |
|
|
373
398
|
| **EmptyState** | Empty content placeholder |
|
|
374
399
|
| **ErrorState** | Error display with retry |
|
|
400
|
+
| **LoadingState** | In-flow loading placeholder for a section still fetching |
|
|
375
401
|
| **LoadingScreen** | Full-screen loading indicator |
|
|
376
402
|
| **PullToRefreshScrollView** | Themed ScrollView with RefreshControl (useThemeTokens for colors) |
|
|
377
403
|
| **KeyboardAwareScreen** | One screen-level keyboard policy for bottom-docked controls |
|
|
@@ -425,9 +451,9 @@ Copy from `apps/showcase/templates/profile/`:
|
|
|
425
451
|
|
|
426
452
|
### Media
|
|
427
453
|
|
|
428
|
-
| Block | Description
|
|
429
|
-
| -------------- |
|
|
430
|
-
| **SmartImage** | Image with
|
|
454
|
+
| Block | Description |
|
|
455
|
+
| -------------- | ------------------------------------------------------------------ |
|
|
456
|
+
| **SmartImage** | Image with explicit loading, loaded, fallback, and error lifecycle |
|
|
431
457
|
|
|
432
458
|
### Date Picker
|
|
433
459
|
|
|
@@ -926,6 +952,9 @@ const adapter: ThemeBridgeAdapter = {
|
|
|
926
952
|
const { mode, resolvedTheme, setMode, cycleMode } = useThemeBridge({ adapter });
|
|
927
953
|
```
|
|
928
954
|
|
|
955
|
+
The bridge loads a new adapter before saving into it. If a user changes the mode while
|
|
956
|
+
storage is still loading, that explicit choice wins over the late persisted value.
|
|
957
|
+
|
|
929
958
|
When another store already owns the mode, mount the controlled bridge once near the
|
|
930
959
|
application root. Change `mode` in the owning event handler; the bridge applies and
|
|
931
960
|
persists it without mirroring state back through a callback effect.
|
|
@@ -1031,10 +1060,10 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
|
|
|
1031
1060
|
classes are unnecessary for normal hierarchy. `heading` also sets
|
|
1032
1061
|
`accessibilityRole="header"` unless the caller supplies another role.
|
|
1033
1062
|
|
|
1034
|
-
| Prop | Type
|
|
1035
|
-
| ------- |
|
|
1036
|
-
| variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'code'` | `'default'` |
|
|
1037
|
-
| size | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' \| '3xl'`
|
|
1063
|
+
| Prop | Type | Default |
|
|
1064
|
+
| ------- | ---------------------------------------------------------------------------------------------------------- | ----------- |
|
|
1065
|
+
| variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'eyebrow' \| 'code'` | `'default'` |
|
|
1066
|
+
| size | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | by variant |
|
|
1038
1067
|
|
|
1039
1068
|
## Input
|
|
1040
1069
|
|
|
@@ -1379,9 +1408,12 @@ import {
|
|
|
1379
1408
|
|
|
1380
1409
|
### React Native: stacked modals
|
|
1381
1410
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1411
|
+
Render a confirmation **inside** the overlay it belongs to. An `AlertDialog` or `Dialog`
|
|
1412
|
+
nested in a `Sheet` opens above it, and Android back or Escape closes the confirmation
|
|
1413
|
+
before the sheet. If `AlertDialog` must be a **sibling** of another fullscreen `Modal`, iOS
|
|
1414
|
+
often fails to show a second `Modal` on top; use **`presentation="overlay"`** inside that
|
|
1415
|
+
`Modal`, or **`presentation="native"`** for a system `Alert.alert` when themed UI is not
|
|
1416
|
+
required.
|
|
1385
1417
|
|
|
1386
1418
|
## Checkbox
|
|
1387
1419
|
|
|
@@ -1446,6 +1478,8 @@ import {
|
|
|
1446
1478
|
Focused and pressed options use the subtle interaction-state tokens; the primary token
|
|
1447
1479
|
marks the selected indicator without replacing the option's foreground colour. Long
|
|
1448
1480
|
selected values stay on one line and truncate without displacing the trigger chevron.
|
|
1481
|
+
For popover presentation on web, the options panel matches the trigger width, caps
|
|
1482
|
+
itself to the collision-available viewport, and wraps long option labels inside it.
|
|
1449
1483
|
|
|
1450
1484
|
`SelectContent` supports optional frosted props:
|
|
1451
1485
|
|
|
@@ -1455,6 +1489,11 @@ selected values stay on one line and truncate without displacing the trigger che
|
|
|
1455
1489
|
| blurIntensity | `number` | subtle preset |
|
|
1456
1490
|
| blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
|
|
1457
1491
|
|
|
1492
|
+
`SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to
|
|
1493
|
+
change that margin. Inside a Dialog, Sheet or other WhileUI overlay it renders in that
|
|
1494
|
+
overlay's layer automatically. Option values must be non-empty strings: the web
|
|
1495
|
+
primitive rejects `""`, so model "none" as an explicit value such as `'none'`.
|
|
1496
|
+
|
|
1458
1497
|
## Tabs
|
|
1459
1498
|
|
|
1460
1499
|
```tsx
|
|
@@ -1507,7 +1546,7 @@ import {
|
|
|
1507
1546
|
import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
|
|
1508
1547
|
|
|
1509
1548
|
<Avatar size="default">
|
|
1510
|
-
<AvatarImage
|
|
1549
|
+
<AvatarImage source={{ uri: 'https://...' }} />
|
|
1511
1550
|
<AvatarFallback>JD</AvatarFallback>
|
|
1512
1551
|
</Avatar>;
|
|
1513
1552
|
```
|
|
@@ -1516,6 +1555,10 @@ import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
|
|
|
1516
1555
|
| ---- | --------------------------- | ----------- |
|
|
1517
1556
|
| size | `'sm' \| 'default' \| 'lg'` | `'default'` |
|
|
1518
1557
|
|
|
1558
|
+
`AvatarImage` keeps `AvatarFallback` visible while loading or after an error. Its
|
|
1559
|
+
`onLoadingStatusChange` callback reports `'loading'`, `'loaded'`, and `'error'`, including
|
|
1560
|
+
when a changed source begins loading, while the standard image event callbacks still run.
|
|
1561
|
+
|
|
1519
1562
|
## Progress
|
|
1520
1563
|
|
|
1521
1564
|
```tsx
|
|
@@ -1552,6 +1595,9 @@ toast({ title: 'Success', description: 'Saved!', variant: 'success' });
|
|
|
1552
1595
|
| variant | `'default' \| 'success' \| 'destructive'` |
|
|
1553
1596
|
| duration | `number` (ms) |
|
|
1554
1597
|
|
|
1598
|
+
Each toast owns its duration from the moment it appears; adding or dismissing another
|
|
1599
|
+
toast does not restart timers already in progress.
|
|
1600
|
+
|
|
1555
1601
|
## Popover
|
|
1556
1602
|
|
|
1557
1603
|
```tsx
|
|
@@ -1889,6 +1935,36 @@ code, and fenced code. It never opens links or writes to the clipboard itself:
|
|
|
1889
1935
|
`onLinkPress` and `codeActions` keep those capabilities in the application. `CodeBlock`
|
|
1890
1936
|
is dependency-free and accepts an application-owned `actions` slot.
|
|
1891
1937
|
|
|
1938
|
+
## SmartImage
|
|
1939
|
+
|
|
1940
|
+
```tsx
|
|
1941
|
+
import { SmartImage } from '@thewhileloop/whileui';
|
|
1942
|
+
|
|
1943
|
+
<SmartImage
|
|
1944
|
+
source={{ uri: imageUrl }}
|
|
1945
|
+
fallbackSource={{ uri: fallbackUrl }}
|
|
1946
|
+
alt="Product preview"
|
|
1947
|
+
className="h-48 w-full"
|
|
1948
|
+
/>;
|
|
1949
|
+
```
|
|
1950
|
+
|
|
1951
|
+
| Prop | Type | Default |
|
|
1952
|
+
| ------------------- | -------------------------------------------------- | --------- |
|
|
1953
|
+
| `source` | `ImageProps['source']` | required |
|
|
1954
|
+
| `alt` | `string` | required |
|
|
1955
|
+
| `fallbackSource` | `ImageProps['source']` | — |
|
|
1956
|
+
| `skeleton` | `boolean` | `true` |
|
|
1957
|
+
| `skeletonClassName` | `string` | — |
|
|
1958
|
+
| `transition` | `number` | `200` |
|
|
1959
|
+
| `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` |
|
|
1960
|
+
| `objectFit` | `'cover' \| 'contain' \| 'fill' \| 'scale-down'` | `'cover'` |
|
|
1961
|
+
| `onLoad` | `() => void` | — |
|
|
1962
|
+
| `onError` | `() => void` | — |
|
|
1963
|
+
|
|
1964
|
+
The skeleton returns whenever the primary image starts loading again. A failed primary
|
|
1965
|
+
source switches to `fallbackSource` when supplied without re-entering a primary/fallback
|
|
1966
|
+
loop.
|
|
1967
|
+
|
|
1892
1968
|
## DatePickerModal / DatePickerInline / DateRangePickerModal / TimePickerModal
|
|
1893
1969
|
|
|
1894
1970
|
Date selection blocks using react-native-calendars. Theme-aware via `useCalendarTheme` (Uniwind light/dark). Optional `theme` prop for custom RN color strings (hex/rgb/hsl/named). `CalendarTheme` maps to react-native-calendars Theme and supports `arrowColor`, `disabledArrowColor`, and optional font keys.
|
|
@@ -2032,7 +2108,8 @@ keeps taps on inputs while the keyboard is open (`keyboardShouldPersistTaps="han
|
|
|
2032
2108
|
Your `contentContainerStyle` is merged after the default padding. Tapping the backdrop
|
|
2033
2109
|
closes the sheet; a drag across the backdrop does not, so a missed scroll never dismisses
|
|
2034
2110
|
it. There is no drag-to-dismiss: close with the backdrop, Android back, Escape on web or
|
|
2035
|
-
`SheetClose`.
|
|
2111
|
+
`SheetClose`. A Select, Popover or confirmation Dialog placed inside the sheet opens above
|
|
2112
|
+
it, and back or Escape closes that inner layer before the sheet.
|
|
2036
2113
|
|
|
2037
2114
|
## Header
|
|
2038
2115
|
|
|
@@ -2043,7 +2120,9 @@ import { Header, HeaderBackButton } from '@thewhileloop/whileui';
|
|
|
2043
2120
|
title="Settings"
|
|
2044
2121
|
subtitle="Manage preferences"
|
|
2045
2122
|
leftAction={<HeaderBackButton onPress={() => {}} />}
|
|
2046
|
-
rightActions={[
|
|
2123
|
+
rightActions={[
|
|
2124
|
+
{ key: 'search', icon: <Icon />, onPress: () => {}, accessibilityLabel: 'Search' },
|
|
2125
|
+
]}
|
|
2047
2126
|
/>;
|
|
2048
2127
|
```
|
|
2049
2128
|
|
|
@@ -2185,6 +2264,22 @@ import { EmptyState } from '@thewhileloop/whileui';
|
|
|
2185
2264
|
/>;
|
|
2186
2265
|
```
|
|
2187
2266
|
|
|
2267
|
+
## LoadingState
|
|
2268
|
+
|
|
2269
|
+
```tsx
|
|
2270
|
+
import { LoadingState } from '@thewhileloop/whileui';
|
|
2271
|
+
|
|
2272
|
+
<LoadingState label="Loading activity" />;
|
|
2273
|
+
```
|
|
2274
|
+
|
|
2275
|
+
The in-flow counterpart of `EmptyState` and `ErrorState` for a section that is still
|
|
2276
|
+
fetching. The spinner is named for assistive technology by `label` (default "Loading").
|
|
2277
|
+
|
|
2278
|
+
| Prop | Type | Default |
|
|
2279
|
+
| ----- | --------------------------- | ----------- |
|
|
2280
|
+
| label | `string` | `'Loading'` |
|
|
2281
|
+
| size | `'sm' \| 'default' \| 'lg'` | `'default'` |
|
|
2282
|
+
|
|
2188
2283
|
## ProfileHeader
|
|
2189
2284
|
|
|
2190
2285
|
Copy from `apps/showcase/templates/profile/profile-header.tsx`, then:
|
|
@@ -2384,6 +2479,9 @@ import { DrawerMenu } from '@thewhileloop/whileui';
|
|
|
2384
2479
|
| `blurIntensity` | `number` | medium preset |
|
|
2385
2480
|
| `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
|
|
2386
2481
|
|
|
2482
|
+
Closes from the backdrop, an item, Android back or Escape on web. It is hosted in a modal
|
|
2483
|
+
on every platform, so keyboard focus stays inside it while it is open.
|
|
2484
|
+
|
|
2387
2485
|
## Roadmap
|
|
2388
2486
|
|
|
2389
2487
|
Tracked work items for future releases.
|
|
@@ -2410,7 +2508,7 @@ Tracked work items for future releases.
|
|
|
2410
2508
|
### Documentation Gaps
|
|
2411
2509
|
|
|
2412
2510
|
- [ ] Add `## API` sections for: Textarea, Toggle, ToggleGroup, Label, Separator, Skeleton, AspectRatio, Collapsible, Text, View
|
|
2413
|
-
- [ ] Add `## API` sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, ListItem, NotificationItem, MetricCard,
|
|
2511
|
+
- [ ] Add `## API` sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, ListItem, NotificationItem, MetricCard, TimelineFeed
|
|
2414
2512
|
|
|
2415
2513
|
### Infrastructure
|
|
2416
2514
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
|
+
import { OverlayHost } from '../../lib/overlay-host';
|
|
4
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
6
|
import { useUniwind } from 'uniwind';
|
|
6
7
|
import { Calendar } from 'react-native-calendars';
|
|
@@ -124,7 +125,7 @@ function DateRangePickerModalSession({ value, onValueChange, open, onOpenChange,
|
|
|
124
125
|
setDraftStart(null);
|
|
125
126
|
setDraftEnd(null);
|
|
126
127
|
}, [onOpenChange]);
|
|
127
|
-
return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(
|
|
128
|
+
return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(OverlayHost, { open: open, animationType: "slide", onRequestClose: handleBackdropPress, children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", style: { backgroundColor: colors.overlay }, onPress: handleBackdropPress, children: _jsxs(Pressable, { accessible: false, className: cn('border border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-background'), style: [
|
|
128
129
|
{
|
|
129
130
|
borderTopLeftRadius: surfaceRadius(visual, 'xl'),
|
|
130
131
|
borderTopRightRadius: surfaceRadius(visual, 'xl'),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
|
+
import { OverlayHost } from '../../lib/overlay-host';
|
|
4
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
6
|
import { useUniwind } from 'uniwind';
|
|
6
7
|
import { Calendar } from 'react-native-calendars';
|
|
@@ -70,7 +71,7 @@ function DatePickerModalSession({ value, onValueChange, open, onOpenChange, minD
|
|
|
70
71
|
setDraftDate(value ?? null);
|
|
71
72
|
onOpenChange(true);
|
|
72
73
|
}, [onOpenChange, value]);
|
|
73
|
-
return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(
|
|
74
|
+
return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(OverlayHost, { open: open, animationType: "slide", onRequestClose: handleDismiss, children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", style: { backgroundColor: colors.overlay }, onPress: handleDismiss, children: _jsxs(Pressable, { accessible: false, className: cn('border border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-background'), style: [
|
|
74
75
|
{
|
|
75
76
|
borderTopLeftRadius: surfaceRadius(visual, 'xl'),
|
|
76
77
|
borderTopRightRadius: surfaceRadius(visual, 'xl'),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Platform, Pressable, ScrollView, View, } from 'react-native';
|
|
4
4
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
5
|
import { Text } from '../../components/text';
|
|
6
6
|
import { Button, ButtonText } from '../../components/button';
|
|
7
7
|
import { cn } from '../../lib/cn';
|
|
8
8
|
import { focusRingInset } from '../../lib/focus-ring';
|
|
9
|
+
import { OverlayHost } from '../../lib/overlay-host';
|
|
9
10
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
10
11
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
11
12
|
import { fieldRecipe, surfacePadding, surfaceRadius, typographyStyle } from '../../lib/recipes';
|
|
@@ -157,7 +158,7 @@ function TimePickerModalSession({ value, onValueChange, open, onOpenChange, minu
|
|
|
157
158
|
}, [hour, minute, onOpenChange, onValueChange]);
|
|
158
159
|
const handleDismiss = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
159
160
|
const handleOpen = useCallback(() => onOpenChange(true), [onOpenChange]);
|
|
160
|
-
return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(
|
|
161
|
+
return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(OverlayHost, { open: open, animationType: "slide", onRequestClose: handleDismiss, children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", style: { backgroundColor: colors.overlay }, onPress: handleDismiss, children: _jsxs(Pressable, { accessible: false, className: cn('border border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-background'), style: [
|
|
161
162
|
{
|
|
162
163
|
borderTopLeftRadius: surfaceRadius(visual, 'xl'),
|
|
163
164
|
borderTopRightRadius: surfaceRadius(visual, 'xl'),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
4
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
5
|
import { Text } from '../../components/text';
|
|
6
6
|
import { cn } from '../../lib/cn';
|
|
7
|
+
import { OverlayHost } from '../../lib/overlay-host';
|
|
7
8
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
8
9
|
import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
|
|
9
10
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
@@ -47,7 +48,7 @@ export function ConfirmActionSheet({ open, onOpenChange, title, description, act
|
|
|
47
48
|
onOpenChange(false);
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
|
-
return (_jsx(
|
|
51
|
+
return (_jsx(OverlayHost, { open: open, animationType: "fade", onRequestClose: handleClose, children: _jsxs(View, { className: "flex-1 justify-end", style: { backgroundColor: colors.overlay }, children: [_jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1", onPress: handleClose }), _jsxs(View, { className: cn('border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-background', className), style: [
|
|
51
52
|
{
|
|
52
53
|
borderTopLeftRadius: surfaceRadius(visual, 'xl'),
|
|
53
54
|
borderTopRightRadius: surfaceRadius(visual, 'xl'),
|
|
@@ -9,6 +9,7 @@ export { ScreenSkeleton, type ScreenSkeletonProps } from './screen-skeleton';
|
|
|
9
9
|
export { ErrorBoundary, type ErrorBoundaryProps } from './error-boundary';
|
|
10
10
|
export { EmptyState, type EmptyStateProps } from './empty-state';
|
|
11
11
|
export { ErrorState, type ErrorStateProps } from './error-state';
|
|
12
|
+
export { LoadingState, type LoadingStateProps } from './loading-state';
|
|
12
13
|
export { FormModalScreen, type FormModalScreenProps } from './form-modal-screen';
|
|
13
14
|
export { LoadingScreen, type LoadingScreenProps } from './loading-screen';
|
|
14
15
|
export { PullToRefreshScrollView, type PullToRefreshScrollViewProps, } from './pull-to-refresh-scroll-view';
|
|
@@ -9,6 +9,7 @@ export { ScreenSkeleton } from './screen-skeleton';
|
|
|
9
9
|
export { ErrorBoundary } from './error-boundary';
|
|
10
10
|
export { EmptyState } from './empty-state';
|
|
11
11
|
export { ErrorState } from './error-state';
|
|
12
|
+
export { LoadingState } from './loading-state';
|
|
12
13
|
export { FormModalScreen } from './form-modal-screen';
|
|
13
14
|
export { LoadingScreen } from './loading-screen';
|
|
14
15
|
export { PullToRefreshScrollView, } from './pull-to-refresh-scroll-view';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ViewProps } from 'react-native';
|
|
2
|
+
export interface LoadingStateProps extends ViewProps {
|
|
3
|
+
/** Read by assistive technology as the spinner's name; shown under it when given. */
|
|
4
|
+
label?: string;
|
|
5
|
+
size?: 'sm' | 'default' | 'lg';
|
|
6
|
+
}
|
|
7
|
+
/** The in-flow sibling of `EmptyState` and `ErrorState`, for a section still fetching. */
|
|
8
|
+
export declare function LoadingState({ label, size, className, style, ...props }: LoadingStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { Spinner } from '../../components/spinner';
|
|
4
|
+
import { Text } from '../../components/text';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
7
|
+
import { surfacePadding } from '../../lib/recipes';
|
|
8
|
+
/** The in-flow sibling of `EmptyState` and `ErrorState`, for a section still fetching. */
|
|
9
|
+
export function LoadingState({ label, size = 'default', className, style, ...props }) {
|
|
10
|
+
const visual = useVisualTokens();
|
|
11
|
+
return (_jsxs(View, { className: cn('items-center justify-center', className), style: [{ paddingVertical: surfacePadding(visual, 'lg') }, style], ...props, children: [_jsx(Spinner, { size: size, accessibilityLabel: label ?? 'Loading' }), label ? (_jsx(Text, { variant: "caption", className: "mt-3 text-center", children: label })) : null] }));
|
|
12
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { createContext, useCallback, useContext, useMemo } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Pressable, ScrollView, StyleSheet, useWindowDimensions, View, } from 'react-native';
|
|
4
|
+
import Animated, { interpolate, useAnimatedStyle } from 'react-native-reanimated';
|
|
4
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
6
|
import { Text } from '../../components/text';
|
|
6
7
|
import { cn } from '../../lib/cn';
|
|
7
8
|
import { composeEventHandlers } from '../../lib/compose-event-handlers';
|
|
8
9
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
9
10
|
import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
|
|
11
|
+
import { OverlayHost } from '../../lib/overlay-host';
|
|
12
|
+
import { useOverlayPresence } from '../../lib/overlay-presence';
|
|
10
13
|
import { useTapOnly } from '../../lib/tap-only';
|
|
11
14
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
12
15
|
import { surfacePadding, surfaceRadius, typographyStyle } from '../../lib/recipes';
|
|
@@ -16,6 +19,7 @@ export function Sheet({ open, onOpenChange, children, maxHeight = 'full', maxWid
|
|
|
16
19
|
const insets = useSafeAreaInsets();
|
|
17
20
|
const colors = useThemeColors();
|
|
18
21
|
const visual = useVisualTokens();
|
|
22
|
+
const interaction = useInteractionTokens();
|
|
19
23
|
const onClose = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
20
24
|
const backdropPress = useTapOnly(onClose);
|
|
21
25
|
const contextValue = useMemo(() => ({ onClose }), [onClose]);
|
|
@@ -34,6 +38,17 @@ export function Sheet({ open, onOpenChange, children, maxHeight = 'full', maxWid
|
|
|
34
38
|
tintColor: colors.overlay,
|
|
35
39
|
});
|
|
36
40
|
const { height: screenHeight, width: windowWidth } = useWindowDimensions();
|
|
41
|
+
// The host Modal does not animate: the backdrop fades in place while the panel slides,
|
|
42
|
+
// so the dim layer never rises with the sheet.
|
|
43
|
+
const { mounted, progress } = useOverlayPresence({
|
|
44
|
+
visible: open,
|
|
45
|
+
openDuration: interaction.motionSlow,
|
|
46
|
+
closeDuration: interaction.motionNormal,
|
|
47
|
+
});
|
|
48
|
+
const backdropStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
49
|
+
const panelStyle = useAnimatedStyle(() => ({
|
|
50
|
+
transform: [{ translateY: interpolate(progress.value, [0, 1], [screenHeight, 0]) }],
|
|
51
|
+
}));
|
|
37
52
|
const maxWidth = maxWidthProp ?? visual.sheetMaxWidth;
|
|
38
53
|
const widthStyle = typeof maxWidth === 'number' && windowWidth >= visual.sheetTabletBreakpoint
|
|
39
54
|
? { maxWidth, alignSelf: 'center', width: maxWidth }
|
|
@@ -64,7 +79,11 @@ export function Sheet({ open, onOpenChange, children, maxHeight = 'full', maxWid
|
|
|
64
79
|
widthStyle,
|
|
65
80
|
frostedSurface.surfaceStyle,
|
|
66
81
|
];
|
|
67
|
-
return (_jsx(
|
|
82
|
+
return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: onClose, children: _jsxs(View, { className: "flex-1 justify-end", children: [_jsx(Animated.View, { style: [
|
|
83
|
+
StyleSheet.absoluteFill,
|
|
84
|
+
{ backgroundColor: frosted ? 'transparent' : colors.overlay },
|
|
85
|
+
backdropStyle,
|
|
86
|
+
], children: frostedBackdrop }), _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1", ...backdropPress }), _jsxs(Animated.View, { className: cn('border-border overflow-hidden relative', frosted ? 'bg-transparent' : 'bg-background'), style: [sheetStyle, panelStyle], children: [frostedSurface.overlay, _jsx(SheetContext.Provider, { value: contextValue, children: children })] })] }) }));
|
|
68
87
|
}
|
|
69
88
|
export function SheetHeader({ title, description, className, children, style, ...props }) {
|
|
70
89
|
const { onClose } = useContext(SheetContext);
|
|
@@ -44,21 +44,19 @@ function resolveRadius(visual, radius) {
|
|
|
44
44
|
return visual.radiusMd;
|
|
45
45
|
}
|
|
46
46
|
const SmartImage = React.forwardRef(({ className, source, alt, skeleton = true, skeletonClassName, transition = 200, radius, objectFit, onLoad, onError, fallbackSource, style, ...props }, ref) => {
|
|
47
|
-
const [
|
|
48
|
-
const [error, setError] = useState(false);
|
|
47
|
+
const [phase, setPhase] = useState('loading');
|
|
49
48
|
const visual = useVisualTokens();
|
|
50
49
|
const borderRadius = resolveRadius(visual, radius ?? 'md');
|
|
51
50
|
const handleLoad = () => {
|
|
52
|
-
|
|
51
|
+
setPhase((current) => (current === 'fallback' ? 'fallback' : 'loaded'));
|
|
53
52
|
onLoad?.();
|
|
54
53
|
};
|
|
55
54
|
const handleError = () => {
|
|
56
|
-
|
|
57
|
-
setError(true);
|
|
55
|
+
setPhase((current) => current === 'fallback' ? 'fallback' : fallbackSource ? 'fallback' : 'error');
|
|
58
56
|
onError?.();
|
|
59
57
|
};
|
|
60
|
-
const imageSource =
|
|
61
|
-
return (_jsxs(View, { ref: ref, className: cn(smartImageVariants({ radius, objectFit }), className), style: [{ borderRadius }, style], ...props, children: [skeleton && loading && (_jsx(Skeleton, { className: cn('absolute inset-0', skeletonClassName) })), _jsx(Image, { source: imageSource, alt: alt, transition: transition, onLoad: handleLoad, onError: handleError, className: cn('w-full h-full', objectFit === 'cover' && 'object-cover', objectFit === 'contain' && 'object-contain', objectFit === 'fill' && 'object-fill', objectFit === 'scale-down' && 'object-scale-down'), style: { opacity:
|
|
58
|
+
const imageSource = phase === 'fallback' ? fallbackSource : source;
|
|
59
|
+
return (_jsxs(View, { ref: ref, className: cn(smartImageVariants({ radius, objectFit }), className), style: [{ borderRadius }, style], ...props, children: [skeleton && phase === 'loading' && (_jsx(Skeleton, { className: cn('absolute inset-0', skeletonClassName) })), _jsx(Image, { source: imageSource, alt: alt, transition: transition, onLoadStart: () => setPhase((current) => (current === 'fallback' ? current : 'loading')), onLoad: handleLoad, onError: handleError, className: cn('w-full h-full', objectFit === 'cover' && 'object-cover', objectFit === 'contain' && 'object-contain', objectFit === 'fill' && 'object-fill', objectFit === 'scale-down' && 'object-scale-down'), style: { opacity: phase === 'loading' ? 0 : 1 } })] }));
|
|
62
60
|
});
|
|
63
61
|
SmartImage.displayName = 'SmartImage';
|
|
64
62
|
export { SmartImage, smartImageVariants };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { View, Pressable, Platform,
|
|
4
|
-
import Animated, {
|
|
3
|
+
import { View, Pressable, Platform, I18nManager, SectionList, useWindowDimensions, StyleSheet, } from 'react-native';
|
|
4
|
+
import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
|
|
5
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
6
|
import { Text } from '../../components/text';
|
|
7
7
|
import { cn } from '../../lib/cn';
|
|
@@ -9,7 +9,8 @@ import { useThemeColors } from '../../lib/theme-colors';
|
|
|
9
9
|
import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
|
|
10
10
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
11
11
|
import { shadowStyle, surfaceRadius } from '../../lib/recipes';
|
|
12
|
-
import {
|
|
12
|
+
import { OverlayHost } from '../../lib/overlay-host';
|
|
13
|
+
import { useOverlayPresence } from '../../lib/overlay-presence';
|
|
13
14
|
import { useTapOnly } from '../../lib/tap-only';
|
|
14
15
|
import { useFrostedSurface, useFrostedBackdrop, } from '../../lib/frosted-surface';
|
|
15
16
|
function DrawerItemRow({ item, active, onSelect, interaction, visual }) {
|
|
@@ -34,54 +35,12 @@ function DrawerSectionList({ sections, activeKey, onSelect, interaction, visual,
|
|
|
34
35
|
const renderSectionHeader = React.useCallback(({ section }) => section.title ? (_jsx(Text, { accessibilityRole: "header", className: "font-medium text-muted-foreground uppercase tracking-wide px-3 mb-2", style: { fontSize: visual.navSectionTitleFontSize }, children: section.title })) : null, [visual.navSectionTitleFontSize]);
|
|
35
36
|
return (_jsx(SectionList, { sections: data, keyExtractor: (item) => item.key, className: "flex-1 px-3", keyboardShouldPersistTaps: "handled", showsVerticalScrollIndicator: false, renderSectionHeader: renderSectionHeader, renderSectionFooter: DrawerSectionGap, ItemSeparatorComponent: DrawerItemGap, renderItem: renderItem }));
|
|
36
37
|
}
|
|
37
|
-
function useDrawerAnimation({ visible, drawerWidth, interaction
|
|
38
|
-
const progress =
|
|
39
|
-
const [mounted, setMounted] = React.useState(visible);
|
|
40
|
-
const closeTimerRef = React.useRef(null);
|
|
41
|
-
const [prevVisible, setPrevVisible] = React.useState(visible);
|
|
42
|
-
// Open in the prop-change render so the first animation frame has a mounted panel;
|
|
43
|
-
// closing stays mounted on the timer until the exit animation finishes.
|
|
44
|
-
if (prevVisible !== visible) {
|
|
45
|
-
setPrevVisible(visible);
|
|
46
|
-
if (visible)
|
|
47
|
-
setMounted(true);
|
|
48
|
-
}
|
|
49
|
-
React.useEffect(() => {
|
|
50
|
-
if (!mounted)
|
|
51
|
-
return;
|
|
52
|
-
if (closeTimerRef.current) {
|
|
53
|
-
clearTimeout(closeTimerRef.current);
|
|
54
|
-
closeTimerRef.current = null;
|
|
55
|
-
}
|
|
56
|
-
const duration = motion.reduceMotion || !motion.animationsEnabled
|
|
57
|
-
? 0
|
|
58
|
-
: visible
|
|
59
|
-
? interaction.drawerOpenDuration
|
|
60
|
-
: interaction.drawerCloseDuration;
|
|
61
|
-
progress.value = withTiming(visible ? 1 : 0, {
|
|
62
|
-
duration,
|
|
63
|
-
reduceMotion: motion.reanimatedReduceMotion,
|
|
64
|
-
easing: visible ? Easing.bezier(0.2, 0.8, 0.2, 1) : Easing.bezier(0.4, 0, 1, 1),
|
|
65
|
-
});
|
|
66
|
-
if (!visible) {
|
|
67
|
-
closeTimerRef.current = setTimeout(() => setMounted(false), duration);
|
|
68
|
-
}
|
|
69
|
-
return () => {
|
|
70
|
-
if (closeTimerRef.current) {
|
|
71
|
-
clearTimeout(closeTimerRef.current);
|
|
72
|
-
closeTimerRef.current = null;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}, [
|
|
76
|
-
mounted,
|
|
38
|
+
function useDrawerAnimation({ visible, drawerWidth, interaction }) {
|
|
39
|
+
const { mounted, progress } = useOverlayPresence({
|
|
77
40
|
visible,
|
|
78
|
-
|
|
79
|
-
interaction.
|
|
80
|
-
|
|
81
|
-
motion.animationsEnabled,
|
|
82
|
-
motion.reduceMotion,
|
|
83
|
-
motion.reanimatedReduceMotion,
|
|
84
|
-
]);
|
|
41
|
+
openDuration: interaction.drawerOpenDuration,
|
|
42
|
+
closeDuration: interaction.drawerCloseDuration,
|
|
43
|
+
});
|
|
85
44
|
const backdropStyle = useAnimatedStyle(() => ({
|
|
86
45
|
opacity: progress.value,
|
|
87
46
|
pointerEvents: progress.value > 0 ? 'auto' : 'none',
|
|
@@ -119,7 +78,6 @@ export function DrawerMenu({ visible, onClose, sections, activeKey, onSelect, he
|
|
|
119
78
|
const colors = useThemeColors();
|
|
120
79
|
const interaction = useInteractionTokens();
|
|
121
80
|
const visual = useVisualTokens();
|
|
122
|
-
const motion = useMotionPolicy();
|
|
123
81
|
const backdropPress = useTapOnly(onClose);
|
|
124
82
|
const frostedSurface = useFrostedSurface({
|
|
125
83
|
frosted,
|
|
@@ -140,7 +98,6 @@ export function DrawerMenu({ visible, onClose, sections, activeKey, onSelect, he
|
|
|
140
98
|
visible,
|
|
141
99
|
drawerWidth: layout.width,
|
|
142
100
|
interaction,
|
|
143
|
-
motion,
|
|
144
101
|
});
|
|
145
102
|
const handleItemPress = React.useCallback((key) => {
|
|
146
103
|
onSelect?.(key);
|
|
@@ -168,7 +125,5 @@ export function DrawerMenu({ visible, onClose, sections, activeKey, onSelect, he
|
|
|
168
125
|
frostedSurface.surfaceStyle,
|
|
169
126
|
style,
|
|
170
127
|
], className: drawerPanelClassName(frosted, className), children: [frostedSurface.overlay, _jsxs(View, { className: "flex-1 pb-8", style: { paddingTop: contentTopPadding }, children: [header && _jsx(View, { className: "px-5 pb-4 mb-2 border-b border-border", children: header }), _jsx(DrawerSectionList, { sections: sections, activeKey: activeKey, onSelect: handleItemPress, interaction: interaction, visual: visual }), footer && _jsx(View, { className: "px-5 py-4 mt-auto border-t border-border", children: footer })] })] })] }));
|
|
171
|
-
|
|
172
|
-
return drawerTree;
|
|
173
|
-
return (_jsx(Modal, { visible: mounted, transparent: true, animationType: "none", onRequestClose: onClose, statusBarTranslucent: true, navigationBarTranslucent: true, presentationStyle: "overFullScreen", children: drawerTree }));
|
|
128
|
+
return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: onClose, children: drawerTree }));
|
|
174
129
|
}
|
|
@@ -4,6 +4,8 @@ export interface HeaderAction {
|
|
|
4
4
|
key: string;
|
|
5
5
|
icon: React.ReactNode;
|
|
6
6
|
onPress: () => void;
|
|
7
|
+
/** Required in practice: an icon-only button has no other accessible name. */
|
|
8
|
+
accessibilityLabel?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface HeaderProps extends ViewProps {
|
|
9
11
|
title?: string;
|