@mrmeg/expo-ui 0.17.0 → 0.20.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 +146 -0
- package/LLM_USAGE.md +18 -0
- package/README.md +8 -2
- package/dist/components/Avatar.d.ts +80 -0
- package/dist/components/Avatar.js +234 -0
- package/dist/components/BottomSheet.d.ts +9 -1
- package/dist/components/BottomSheet.js +8 -3
- package/dist/components/Carousel.d.ts +73 -0
- package/dist/components/Carousel.js +184 -0
- package/dist/components/Drawer.js +5 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/constants/colors.d.ts +4 -0
- package/dist/constants/colors.js +12 -4
- package/dist/hooks/useResources.d.ts +14 -0
- package/dist/hooks/useResources.js +43 -9
- package/dist/hooks/useTheme.js +16 -2
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/ssrTheme.d.ts +41 -0
- package/dist/state/ssrTheme.js +33 -0
- package/dist/state/themeStore.d.ts +19 -0
- package/dist/state/themeStore.js +55 -6
- package/llms-full.md +8 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,152 @@
|
|
|
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.20.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`surfaceSunken` semantic token.** The app-chrome tier below `background`,
|
|
11
|
+
so sidebars, rails, and side panels read as a layer *beneath* the content
|
|
12
|
+
pane instead of the same surface. Dark `#050506` (below zinc-950), light
|
|
13
|
+
`#FAFAFA` (zinc-50). Elevation is now a surface-tier ladder rather than a
|
|
14
|
+
shadow scale: `surfaceSunken` < `background` < `card`/`popover` < `muted`.
|
|
15
|
+
|
|
16
|
+
- **`borderStrong` semantic token.** A hairline for elements sitting on a
|
|
17
|
+
filled surface — chips on `muted`, raised panels — one visible step above
|
|
18
|
+
the fill, where `border` shares `muted`'s hex and disappears. Dark
|
|
19
|
+
`#3F3F46` (zinc-700), light `#D4D4D8` (zinc-300). `border` stays the
|
|
20
|
+
hairline for elements on `background`/`card`.
|
|
21
|
+
|
|
22
|
+
- **SSR theme seeding (`SsrThemeSeedContext`).** On a server-rendered web app,
|
|
23
|
+
`useTheme`/`useStyles` had no way to know the visitor's theme during the
|
|
24
|
+
server render, so every SSR page shipped a fully light-themed tree that
|
|
25
|
+
recolored after hydration. The store can't carry a per-request value — it's a
|
|
26
|
+
module singleton shared by concurrent SSR requests — so the new
|
|
27
|
+
`SsrThemeSeedContext` (with `SsrThemeSeed`, `SSR_THEME_SEED_DEFAULT`, and
|
|
28
|
+
`useSsrThemeSeed`) is the per-request channel. Provide it from a signal the
|
|
29
|
+
server and browser read identically (a cookie), and `useTheme` resolves from
|
|
30
|
+
it on web until persistence has loaded.
|
|
31
|
+
|
|
32
|
+
`SSR_THEME_SEED_DEFAULT` is `system`/light, i.e. what the store boots with —
|
|
33
|
+
so a host that receives no signal renders exactly as it did before the seed
|
|
34
|
+
existed. Note that this default is a *fallback*, not a reading: a host that
|
|
35
|
+
also writes the resolved scheme into its served HTML (`<html data-theme>`, a
|
|
36
|
+
blocking script's early-return, a `prefers-color-scheme` CSS fallback) must
|
|
37
|
+
distinguish "seed came from the visitor" from "seed is the default" and skip
|
|
38
|
+
the write in the latter case, or it will pin a dark-OS first-time visitor to
|
|
39
|
+
light with its own failsafes disabled.
|
|
40
|
+
|
|
41
|
+
- **`user-theme-preference` cookie mirror.** On web, `setTheme` now dual-writes
|
|
42
|
+
`localStorage` **and** a `user-theme-preference` cookie (`path=/;
|
|
43
|
+
max-age≈1y; SameSite=Lax`, exported as `THEME_COOKIE_NAME`), and `loadTheme`
|
|
44
|
+
backfills/repairs it from `localStorage`, so a server-rendered host can read
|
|
45
|
+
the visitor's theme off the request. `localStorage` remains the source of
|
|
46
|
+
truth; the cookie is a render hint only. Native persistence is unchanged.
|
|
47
|
+
|
|
48
|
+
- **`hasLoadedTheme` on the theme store.** False until `loadTheme()` has read
|
|
49
|
+
persistence (or `setTheme` was called). While it's false on web, `useTheme`
|
|
50
|
+
resolves from the SSR seed instead of store state; after that, store state
|
|
51
|
+
wins for good. Mirrors the `hasLoadedOnboarding` pattern.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **Dim text is more readable in both schemes (visual change).**
|
|
56
|
+
`textDim`/`mutedForeground` moved from `#A1A1AA` → `#B0B0B8` in dark
|
|
57
|
+
(~9.2:1 on `background`, ~6.9:1 on `muted`, up from ~7.8:1/~5.9:1) and from
|
|
58
|
+
zinc-500 `#71717A` → zinc-600 `#52525B` in light (~7.7:1 on white, up from
|
|
59
|
+
~4.8:1). Captions, timestamps, meta rows, and neutral `StatCard` changes all
|
|
60
|
+
render a step brighter (dark) or darker (light) than before; both stay
|
|
61
|
+
clearly dimmer than `text`, so hierarchy is preserved. A new package test
|
|
62
|
+
enforces the floors (≥7:1 on `background`/`card`/`popover`, ≥6:1 on
|
|
63
|
+
`muted`/`secondary`), so these tokens cannot be dimmed back down silently.
|
|
64
|
+
Apps that hardcoded the old hexes, or asserted them in tests, need to update
|
|
65
|
+
those literals; apps using the tokens get the change for free.
|
|
66
|
+
|
|
67
|
+
- **`Drawer` chrome paints `surfaceSunken`.** Both the overlay drawer panel and
|
|
68
|
+
the in-flow rail switched from `background` to `surfaceSunken`, so navigation
|
|
69
|
+
chrome is visibly darker than the content it sits beside (dark) or a faint
|
|
70
|
+
step off white (light). Override via the existing `style` props, or re-skin
|
|
71
|
+
`surfaceSunken` through `setColors`/`ThemeColorScope`, to restore the old
|
|
72
|
+
flat look.
|
|
73
|
+
|
|
74
|
+
- **`syncThemeFromEnvironment()` starts the OS listener before reading
|
|
75
|
+
persistence.** `loadTheme()` is what flips `hasLoadedTheme` (the moment web
|
|
76
|
+
renders stop trusting the SSR seed), so the real OS scheme is now read first
|
|
77
|
+
and a `system` user never sees a frame of the boot-default light.
|
|
78
|
+
|
|
79
|
+
## [0.19.0]
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`Avatar` and `AvatarGroup`.** The package had `SkeletonAvatar` (a loading
|
|
84
|
+
placeholder) but no real avatar, so consumers hand-rolled circles. `Avatar`
|
|
85
|
+
renders an image with a graceful fallback chain — image → initials → icon —
|
|
86
|
+
and downgrades to initials at runtime when the image fails to load, so a
|
|
87
|
+
dead URL never leaves an empty hole. Accepts `source`, `name` (supplies 1–2
|
|
88
|
+
initials and the default accessibility label), `icon` (Feather, defaults to
|
|
89
|
+
`user`), `size` (`"sm" | "md" | "lg"` = 32/40/48, or an explicit pixel
|
|
90
|
+
diameter), and `shape` (`"circle" | "square"`). Initials derivation is
|
|
91
|
+
grapheme-aware: a decomposed accented character keeps its mark and an emoji
|
|
92
|
+
is not sliced into a lone surrogate.
|
|
93
|
+
|
|
94
|
+
`AvatarGroup` stacks children with a ring in the theme `background` color so
|
|
95
|
+
the stack reads on any surface, collapses anything past `max` into a `+N`
|
|
96
|
+
tile, propagates its `size` and `shape` to children that don't set their own,
|
|
97
|
+
and announces the total count (including the collapsed overflow) to screen
|
|
98
|
+
readers.
|
|
99
|
+
|
|
100
|
+
Uses React Native's own `Image`, so there is no new peer dependency.
|
|
101
|
+
|
|
102
|
+
- **`Carousel` — horizontally snapping slide row with dot indicators.**
|
|
103
|
+
Children-based: each child becomes one slide at the resolved item width.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<Carousel itemWidth={0.8} onIndexChange={setPage}>
|
|
107
|
+
{testimonials.map((t) => (
|
|
108
|
+
<Card key={t.name}>
|
|
109
|
+
<BodyText>{t.quote}</BodyText>
|
|
110
|
+
</Card>
|
|
111
|
+
))}
|
|
112
|
+
</Carousel>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`itemWidth` (default `0.85`) is a fraction of the carousel's own width when
|
|
116
|
+
`<= 1` — leaving the next slide peeking — and absolute pixels when `> 1`.
|
|
117
|
+
`gap`, `contentPadding`, `showDots`, `initialIndex`, `onIndexChange`, and
|
|
118
|
+
`snap` cover the rest; dots are plain themed views (active uses `accent`)
|
|
119
|
+
and auto-hide for a single slide. `onIndexChange` fires once per settle.
|
|
120
|
+
|
|
121
|
+
Built on `ScrollView`, not a virtualized list, so every slide is in the
|
|
122
|
+
server-rendered tree — web SSR ships the real slide content instead of an
|
|
123
|
+
empty scroller that fills in after measurement. No animation library is
|
|
124
|
+
involved: native snapping uses `snapToInterval`, and because
|
|
125
|
+
react-native-web drops that prop, web gets equivalent CSS scroll-snap
|
|
126
|
+
(`scroll-snap-type`/`scroll-snap-align` plus a `scroll-padding-left` that
|
|
127
|
+
keeps page `i` at `i * (itemWidth + gap)` on both platforms). Wheel and
|
|
128
|
+
trackpad scrolling keep the dots live via throttled scroll ticks, since RNW
|
|
129
|
+
emits no momentum events.
|
|
130
|
+
|
|
131
|
+
Accessibility: the dot row is a `tablist` of `tab`s carrying
|
|
132
|
+
`selected` state and per-slide labels, plus a visually clipped
|
|
133
|
+
`aria-live="polite"` "N of M" page announcement.
|
|
134
|
+
|
|
135
|
+
`getCarouselIndex(offsetX, interval, count)` and
|
|
136
|
+
`resolveCarouselItemWidth(itemWidth, containerWidth)` are exported for hosts
|
|
137
|
+
building custom controls on the same math.
|
|
138
|
+
|
|
139
|
+
## [0.18.0]
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
|
|
143
|
+
- **`BottomSheet.Content` `backgroundStyle` pass-through.** The native sheet
|
|
144
|
+
surface (web vaul panel, Android `containerColor`, iOS
|
|
145
|
+
`presentationBackground`) previously hardcoded the themed card color.
|
|
146
|
+
`BottomSheet.Content` now accepts an optional `backgroundStyle` merged over
|
|
147
|
+
that default, so hosts can pass `{ backgroundColor: "transparent" }` to let
|
|
148
|
+
custom chrome (e.g. a Liquid Glass backdrop) show through. Omitting the prop
|
|
149
|
+
preserves the existing card background. The RN content column still paints
|
|
150
|
+
its own card fill; clear that via `style` when going transparent.
|
|
151
|
+
|
|
6
152
|
## [0.17.0]
|
|
7
153
|
|
|
8
154
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -76,6 +76,11 @@ On native, `BottomSheet.Content` avoids the soft keyboard by default with
|
|
|
76
76
|
React Native keyboard events. Pass `avoidKeyboard={false}` to opt out for a
|
|
77
77
|
specific sheet.
|
|
78
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
|
+
|
|
79
84
|
i18n is optional. Do not add app-level i18n setup just to use this package.
|
|
80
85
|
Plain children and `text` props work without `i18next` or `react-i18next`.
|
|
81
86
|
`tx` props render fallback text when provided and otherwise render the key
|
|
@@ -102,11 +107,13 @@ configureExpoUiI18n((key, options) => i18n.t(key, options));
|
|
|
102
107
|
Useful theme tokens include:
|
|
103
108
|
|
|
104
109
|
```tsx
|
|
110
|
+
theme.colors.surfaceSunken;
|
|
105
111
|
theme.colors.background;
|
|
106
112
|
theme.colors.foreground;
|
|
107
113
|
theme.colors.card;
|
|
108
114
|
theme.colors.popover;
|
|
109
115
|
theme.colors.border;
|
|
116
|
+
theme.colors.borderStrong;
|
|
110
117
|
theme.colors.input;
|
|
111
118
|
theme.colors.ring;
|
|
112
119
|
theme.colors.primary;
|
|
@@ -126,6 +133,13 @@ Token intent:
|
|
|
126
133
|
- `input`: form-control border color
|
|
127
134
|
- `ring`: focus outline color
|
|
128
135
|
- `popover`: elevated overlay surface
|
|
136
|
+
- `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`.
|
|
129
143
|
|
|
130
144
|
Use `getShadowStyle()` for package surfaces that need elevation. It supports
|
|
131
145
|
`base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
|
|
@@ -182,10 +196,12 @@ Use this table before creating a new app-local primitive.
|
|
|
182
196
|
| `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections |
|
|
183
197
|
| `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs |
|
|
184
198
|
| `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers | Staggered list rows, revealed panels, animated empty states |
|
|
199
|
+
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback | Hand-rolled circles with a nested `Image` and initials `Text` | Account menu, comment authors, assignee pickers, overlapping team stacks |
|
|
185
200
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
186
201
|
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms |
|
|
187
202
|
| `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs |
|
|
188
203
|
| `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries |
|
|
204
|
+
| `Carousel` | Horizontally snapping slide rows with dot indicators | Hand-rolled snap `ScrollView` plus manual offset math | Testimonial strips, onboarding pages, feature highlights, image galleries |
|
|
189
205
|
| `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters |
|
|
190
206
|
| `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text |
|
|
191
207
|
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user |
|
|
@@ -222,8 +238,10 @@ Use this table before creating a new app-local primitive.
|
|
|
222
238
|
- Use `RadioGroup` for small mutually exclusive choices and `Select` for longer option sets.
|
|
223
239
|
- Use `Dialog` for blocking decisions, `Popover` for contextual controls, `Tooltip` for short explanations, and `DropdownMenu` for action lists.
|
|
224
240
|
- Use `Card` for individual repeated or framed items, not as a wrapper around full page sections.
|
|
241
|
+
- Use `Carousel` for a horizontal snap row of a known, small set of slides. It renders every child (no virtualization), so slides survive server rendering; reach for `FlatList` when the data set is large or unbounded.
|
|
225
242
|
- Use `EmptyState` for no-data or recoverable error regions.
|
|
226
243
|
- Use `Skeleton` for loading content with stable layout.
|
|
244
|
+
- Use `Avatar` with both `source` and `name` whenever both exist: `name` supplies the initials shown when the image is absent or fails, 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.
|
|
227
245
|
- Use `Progress` for real progress or indeterminate long-running work.
|
|
228
246
|
- Use `Drawer.Header` with `icon`, `title`, and `action` for a compact app-brand row; place `Drawer.ToggleCollapse` in `action` for a trailing rail control.
|
|
229
247
|
|
package/README.md
CHANGED
|
@@ -101,7 +101,9 @@ const styles = StyleSheet.create({
|
|
|
101
101
|
});
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
`useTheme()` returns the active `theme`, resolved `scheme`, persisted `currentTheme`, `setTheme`, `toggleTheme`, cross-platform shadow helpers, a web focus-ring helper, contrast helpers, and `withAlpha`. `getShadowStyle(type)` supports `base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`, `cardHover`, and `cardSubtle`. Use semantic tokens such as `theme.colors.background`, `foreground`, `card`, `popover`, `border`, `input`, `ring`, `primary`, `secondary`, `accent`, `mutedForeground`, `destructive`, `success`, and `warning`. `primary` is the neutral action color, `secondary` is a neutral secondary surface, `accent` is the teal highlight color, `input` is the default form-control border, and `ring` is the focus outline color.
|
|
104
|
+
`useTheme()` returns the active `theme`, resolved `scheme`, persisted `currentTheme`, `setTheme`, `toggleTheme`, cross-platform shadow helpers, a web focus-ring helper, contrast helpers, and `withAlpha`. `getShadowStyle(type)` supports `base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`, `cardHover`, and `cardSubtle`. Use semantic tokens such as `theme.colors.surfaceSunken`, `background`, `foreground`, `card`, `popover`, `border`, `borderStrong`, `input`, `ring`, `primary`, `secondary`, `accent`, `mutedForeground`, `destructive`, `success`, and `warning`. `primary` is the neutral action color, `secondary` is a neutral secondary surface, `accent` is the teal highlight color, `input` is the default form-control border, and `ring` is the focus outline color.
|
|
105
|
+
|
|
106
|
+
Surfaces are layered tiers rather than shadow depths: `surfaceSunken` (app chrome, e.g. the Drawer rail) sits below `background` (content), which sits below `card`/`popover` (raised panels), which sit below `muted` (chips and insets). `border` is the hairline for elements on `background`/`card`; use `borderStrong` when the element sits on a filled surface, where `border` would blend into the fill. `textDim`/`mutedForeground` are tuned for readability — at least 7:1 against `background`/`card` and 6:1 against `muted` in both schemes — and a package test enforces those floors.
|
|
105
107
|
|
|
106
108
|
When `currentTheme` is `"system"`, the package tracks the OS color scheme and
|
|
107
109
|
updates all `useTheme()` and `useStyles()` consumers through the package theme
|
|
@@ -295,10 +297,12 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
295
297
|
| `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections, dense detail pages |
|
|
296
298
|
| `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs, simple blocking messages |
|
|
297
299
|
| `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers | Staggered list rows, revealed panels, animated empty states |
|
|
300
|
+
| `Avatar`, `AvatarGroup` | Profile images with initials/icon fallback | Hand-rolled circles with a nested `Image` and initials `Text` | Account menu, comment authors, assignee pickers, testimonial cards, overlapping team/collaborator stacks |
|
|
298
301
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
299
302
|
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms, contextual details |
|
|
300
303
|
| `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs, icon-accessory buttons; loading state preserves resting width |
|
|
301
304
|
| `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries, dashboards |
|
|
305
|
+
| `Carousel` | Horizontally snapping slide rows with dot indicators | Hand-rolled snap `ScrollView` plus manual offset math | Testimonial strips, onboarding pages, feature highlights, image galleries; `itemWidth` below 1 is a fraction of the carousel width so the next slide peeks |
|
|
302
306
|
| `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters, notification opt-ins |
|
|
303
307
|
| `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text, optional details |
|
|
304
308
|
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user, blocking warnings |
|
|
@@ -374,12 +378,14 @@ Use `StyledText` or its aliases instead of raw `Text` whenever the text is part
|
|
|
374
378
|
/>
|
|
375
379
|
```
|
|
376
380
|
|
|
377
|
-
Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `UIProvider` also wraps app content in the package keyboard-avoiding root by default, so ordinary screens and fixed footers stay above the soft keyboard without repeated app-local wrappers; pass `keyboardAvoiding={false}` to opt out, or use `KeyboardAvoidingView` directly for a subtree with custom behavior. Web skips the root keyboard wrapper unless `keyboardAvoiding` is explicitly enabled. `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. Trigger transient feedback with `notify`.
|
|
381
|
+
Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `UIProvider` also wraps app content in the package keyboard-avoiding root by default, so ordinary screens and fixed footers stay above the soft keyboard without repeated app-local wrappers; pass `keyboardAvoiding={false}` to opt out, or use `KeyboardAvoidingView` directly for a subtree with custom behavior. Web skips the root keyboard wrapper unless `keyboardAvoiding` is explicitly enabled. `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. `BottomSheet.Content` also accepts `backgroundStyle`, merged over the themed card default on the native sheet surface (web panel, Android `containerColor`, iOS `presentationBackground`) — pass `{ backgroundColor: "transparent" }`, plus a `style` clearing the content column's card fill, to let custom chrome like a glass backdrop show through. Trigger transient feedback with `notify`.
|
|
378
382
|
|
|
379
383
|
Use `Skeleton` components for loading content with stable dimensions, `EmptyState` for no-data/recoverable errors, `Alert` for blocking confirm/alert dialogs, and `Notification` for transient global feedback.
|
|
380
384
|
|
|
381
385
|
Use `Toggle` for one pressed state, `ToggleGroup` for a related set of pressed states, `Switch` for binary settings, `RadioGroup` for small mutually exclusive choices, and `Select` for longer option sets.
|
|
382
386
|
|
|
387
|
+
Pass `Avatar` both a `source` and a `name` whenever you have them: `name` supplies the initials shown while the image is missing or after it fails, and the default accessibility label. `SkeletonAvatar` is the loading placeholder, not a substitute. Inside `AvatarGroup`, child avatars inherit the group's `size` and `shape` and gain the ring, so set those on the group rather than per child.
|
|
388
|
+
|
|
383
389
|
### Quick Examples
|
|
384
390
|
|
|
385
391
|
Card, badge, and CTA:
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type ImageSourcePropType, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type IconName } from "./Icon";
|
|
4
|
+
/** Named size tokens, or an explicit pixel diameter. */
|
|
5
|
+
export type AvatarSize = "sm" | "md" | "lg" | number;
|
|
6
|
+
export type AvatarShape = "circle" | "square";
|
|
7
|
+
/**
|
|
8
|
+
* Derive 1–2 initials from a display name.
|
|
9
|
+
*
|
|
10
|
+
* First + last word (middle names are skipped, as in most avatar stacks), one
|
|
11
|
+
* initial for a single-word name, and `""` when there is nothing to derive —
|
|
12
|
+
* callers treat that as "fall through to the icon".
|
|
13
|
+
*
|
|
14
|
+
* Grapheme-aware by way of `Array.from`, so an accented character written as
|
|
15
|
+
* base + combining mark keeps its accent and an astral code point (emoji) is
|
|
16
|
+
* not sliced into a lone surrogate. `toLocaleUpperCase` is deliberate: a
|
|
17
|
+
* locale-sensitive uppercase is right for names.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getAvatarInitials(name?: string): string;
|
|
20
|
+
export interface AvatarProps {
|
|
21
|
+
/** Image to display. Falls back to initials (then the icon) if it fails to load. */
|
|
22
|
+
source?: ImageSourcePropType;
|
|
23
|
+
/** Display name; supplies the fallback initials and the default accessibility label. */
|
|
24
|
+
name?: string;
|
|
25
|
+
/** Feather icon rendered when there is no image and no derivable initials. @default "user" */
|
|
26
|
+
icon?: IconName;
|
|
27
|
+
/** Size token or explicit pixel diameter. Inherited from `AvatarGroup` when unset. @default "md" */
|
|
28
|
+
size?: AvatarSize;
|
|
29
|
+
/** Inherited from `AvatarGroup` when unset. @default "circle" */
|
|
30
|
+
shape?: AvatarShape;
|
|
31
|
+
/**
|
|
32
|
+
* Accessibility label. Defaults to `name`; when neither is set the avatar is
|
|
33
|
+
* decorative and stays out of the accessibility tree.
|
|
34
|
+
*/
|
|
35
|
+
accessibilityLabel?: string;
|
|
36
|
+
style?: StyleProp<ViewStyle>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Avatar Component
|
|
40
|
+
*
|
|
41
|
+
* A circular (or rounded-square) profile image with a graceful fallback chain:
|
|
42
|
+
* image → initials → icon. A broken or slow-failing image downgrades to
|
|
43
|
+
* initials at runtime, so a dead URL never leaves an empty hole.
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
* ```tsx
|
|
47
|
+
* <Avatar source={{ uri: user.avatarUrl }} name={user.fullName} />
|
|
48
|
+
* <Avatar name="Ada Lovelace" size="lg" />
|
|
49
|
+
* <Avatar icon="camera" shape="square" />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function Avatar({ source, name, icon, size, shape, accessibilityLabel, style: styleOverride, }: AvatarProps): React.JSX.Element;
|
|
53
|
+
export interface AvatarGroupProps {
|
|
54
|
+
children?: React.ReactNode;
|
|
55
|
+
/** Maximum number of avatars to render; the rest collapse into a `+N` tile. */
|
|
56
|
+
max?: number;
|
|
57
|
+
/** Size applied to child avatars that don't set their own. @default "md" */
|
|
58
|
+
size?: AvatarSize;
|
|
59
|
+
/** Shape applied to child avatars that don't set their own. @default "circle" */
|
|
60
|
+
shape?: AvatarShape;
|
|
61
|
+
/** Defaults to `"<n> avatars"`, counting the collapsed overflow. */
|
|
62
|
+
accessibilityLabel?: string;
|
|
63
|
+
style?: StyleProp<ViewStyle>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* AvatarGroup Component
|
|
67
|
+
*
|
|
68
|
+
* Overlapping stack of avatars, each ringed in the theme `background` color so
|
|
69
|
+
* the stack reads on any surface. Earlier avatars sit on top; anything past
|
|
70
|
+
* `max` collapses into a `+N` tile.
|
|
71
|
+
*
|
|
72
|
+
* Usage:
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <AvatarGroup max={3}>
|
|
75
|
+
* {members.map((m) => <Avatar key={m.id} source={{ uri: m.avatarUrl }} name={m.name} />)}
|
|
76
|
+
* </AvatarGroup>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare function AvatarGroup({ children, max, size, shape, accessibilityLabel, style: styleOverride, }: AvatarGroupProps): React.JSX.Element;
|
|
80
|
+
export { Avatar, AvatarGroup };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { use, useState } from "react";
|
|
3
|
+
import { Image, StyleSheet, View, } from "react-native";
|
|
4
|
+
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
+
import { spacing } from "../constants/spacing.js";
|
|
6
|
+
import { Icon } from "./Icon.js";
|
|
7
|
+
import { StyledText } from "./StyledText.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
|
+
const SIZE_TOKENS = {
|
|
10
|
+
sm: spacing.iconLg, // 32
|
|
11
|
+
md: spacing.iconLg + spacing.sm, // 40
|
|
12
|
+
lg: spacing.iconXl, // 48
|
|
13
|
+
};
|
|
14
|
+
function isSizeToken(size) {
|
|
15
|
+
return typeof size === "string";
|
|
16
|
+
}
|
|
17
|
+
function resolveSize(size) {
|
|
18
|
+
return isSizeToken(size) ? SIZE_TOKENS[size] : size;
|
|
19
|
+
}
|
|
20
|
+
/** Rounded-square radius scales with the avatar so 32px and 88px read the same. */
|
|
21
|
+
function squareRadius(px) {
|
|
22
|
+
return Math.round(px / 4);
|
|
23
|
+
}
|
|
24
|
+
/** Initials/icon scale with the avatar rather than jumping between text tokens. */
|
|
25
|
+
function contentSize(px) {
|
|
26
|
+
return { fontSize: Math.round(px * 0.4), iconSize: Math.round(px * 0.5) };
|
|
27
|
+
}
|
|
28
|
+
/** Overlap for stacked avatars — 30% keeps ~70% of each face visible. */
|
|
29
|
+
function overlap(px) {
|
|
30
|
+
return -Math.round(px * 0.3);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive 1–2 initials from a display name.
|
|
34
|
+
*
|
|
35
|
+
* First + last word (middle names are skipped, as in most avatar stacks), one
|
|
36
|
+
* initial for a single-word name, and `""` when there is nothing to derive —
|
|
37
|
+
* callers treat that as "fall through to the icon".
|
|
38
|
+
*
|
|
39
|
+
* Grapheme-aware by way of `Array.from`, so an accented character written as
|
|
40
|
+
* base + combining mark keeps its accent and an astral code point (emoji) is
|
|
41
|
+
* not sliced into a lone surrogate. `toLocaleUpperCase` is deliberate: a
|
|
42
|
+
* locale-sensitive uppercase is right for names.
|
|
43
|
+
*/
|
|
44
|
+
export function getAvatarInitials(name) {
|
|
45
|
+
if (!name)
|
|
46
|
+
return "";
|
|
47
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
48
|
+
if (words.length === 0)
|
|
49
|
+
return "";
|
|
50
|
+
const picked = words.length === 1 ? [words[0]] : [words[0], words[words.length - 1]];
|
|
51
|
+
return picked
|
|
52
|
+
.map((word) => {
|
|
53
|
+
const [first] = Array.from(word.normalize("NFC"));
|
|
54
|
+
return first ?? "";
|
|
55
|
+
})
|
|
56
|
+
.join("")
|
|
57
|
+
.toLocaleUpperCase();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Identity of an image source by content, not object identity.
|
|
61
|
+
*
|
|
62
|
+
* `source={{ uri }}` allocates a new object on every render, so a failure
|
|
63
|
+
* flag keyed on object identity would reset immediately and loop
|
|
64
|
+
* image → error → image. Returns `null` when there is no usable image.
|
|
65
|
+
*/
|
|
66
|
+
function getSourceKey(source) {
|
|
67
|
+
if (source == null)
|
|
68
|
+
return null;
|
|
69
|
+
if (typeof source === "number")
|
|
70
|
+
return `asset:${source}`;
|
|
71
|
+
if (Array.isArray(source)) {
|
|
72
|
+
const key = source.map((entry) => entry?.uri ?? "").filter(Boolean).join("|");
|
|
73
|
+
return key || null;
|
|
74
|
+
}
|
|
75
|
+
return source.uri || null;
|
|
76
|
+
}
|
|
77
|
+
const AvatarGroupContext = React.createContext(null);
|
|
78
|
+
/**
|
|
79
|
+
* Avatar Component
|
|
80
|
+
*
|
|
81
|
+
* A circular (or rounded-square) profile image with a graceful fallback chain:
|
|
82
|
+
* image → initials → icon. A broken or slow-failing image downgrades to
|
|
83
|
+
* initials at runtime, so a dead URL never leaves an empty hole.
|
|
84
|
+
*
|
|
85
|
+
* Usage:
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <Avatar source={{ uri: user.avatarUrl }} name={user.fullName} />
|
|
88
|
+
* <Avatar name="Ada Lovelace" size="lg" />
|
|
89
|
+
* <Avatar icon="camera" shape="square" />
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function Avatar({ source, name, icon, size, shape, accessibilityLabel, style: styleOverride, }) {
|
|
93
|
+
const { theme } = useTheme();
|
|
94
|
+
const styles = themedStyles(theme);
|
|
95
|
+
const group = use(AvatarGroupContext);
|
|
96
|
+
const px = resolveSize(size ?? group?.size ?? "md");
|
|
97
|
+
const resolvedShape = shape ?? group?.shape ?? "circle";
|
|
98
|
+
const sourceKey = getSourceKey(source);
|
|
99
|
+
// Keyed on the source's content, so a retry with a new URL clears the
|
|
100
|
+
// failure while a re-rendered identical URL keeps it.
|
|
101
|
+
const [failedKey, setFailedKey] = useState(null);
|
|
102
|
+
const showImage = sourceKey !== null && failedKey !== sourceKey;
|
|
103
|
+
const initials = getAvatarInitials(name);
|
|
104
|
+
const label = accessibilityLabel ?? name;
|
|
105
|
+
return (_jsx(AvatarSurface, { px: px, shape: resolvedShape, ring: group?.ring ?? false, accessibilityLabel: label, style: [styles.fallback, styleOverride], children: showImage ? (_jsx(Image, { source: source, resizeMode: "cover", style: geometry.image, onError: () => setFailedKey(sourceKey) })) : initials ? (_jsx(AvatarLabel, { px: px, style: styles.fallbackText, children: initials })) : (_jsx(Icon, { name: icon ?? "user", size: contentSize(px).iconSize, color: "mutedForeground", decorative: true })) }));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Shared chrome for an avatar-shaped tile — used by `Avatar` and by
|
|
109
|
+
* `AvatarGroup`'s `+N` overflow tile so both stay the same size, shape, and
|
|
110
|
+
* ring without duplicating the geometry rules.
|
|
111
|
+
*/
|
|
112
|
+
function AvatarSurface({ px, shape, ring, accessibilityLabel, style, children, }) {
|
|
113
|
+
const { theme } = useTheme();
|
|
114
|
+
const styles = themedStyles(theme);
|
|
115
|
+
const token = SIZE_TOKEN_FOR_PX[px];
|
|
116
|
+
return (_jsx(View
|
|
117
|
+
// Only join the accessibility tree when there is something to announce;
|
|
118
|
+
// an unlabeled avatar is decorative next to the name it sits beside.
|
|
119
|
+
, {
|
|
120
|
+
// Only join the accessibility tree when there is something to announce;
|
|
121
|
+
// an unlabeled avatar is decorative next to the name it sits beside.
|
|
122
|
+
accessible: accessibilityLabel ? true : undefined,
|
|
123
|
+
// RN's role name is "image"; RNW maps it to aria-role "img" on the web.
|
|
124
|
+
accessibilityRole: accessibilityLabel ? "image" : undefined, accessibilityLabel: accessibilityLabel, style: [
|
|
125
|
+
geometry.base,
|
|
126
|
+
// Token sizes hit a pre-registered rule; a custom pixel size falls back
|
|
127
|
+
// to an inline style (which always ships in the SSR HTML).
|
|
128
|
+
token
|
|
129
|
+
? geometry[shape === "circle" ? `${token}Circle` : `${token}Square`]
|
|
130
|
+
: { width: px, height: px, borderRadius: shape === "circle" ? spacing.radiusFull : squareRadius(px) },
|
|
131
|
+
ring && geometry.ring,
|
|
132
|
+
ring && styles.ring,
|
|
133
|
+
style,
|
|
134
|
+
], children: children }));
|
|
135
|
+
}
|
|
136
|
+
function AvatarLabel({ px, style, children, }) {
|
|
137
|
+
return (_jsx(StyledText, { accessible: false, selectable: false, fontWeight: "semibold", style: [geometry.labelText, { fontSize: contentSize(px).fontSize }, style], children: children }));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* AvatarGroup Component
|
|
141
|
+
*
|
|
142
|
+
* Overlapping stack of avatars, each ringed in the theme `background` color so
|
|
143
|
+
* the stack reads on any surface. Earlier avatars sit on top; anything past
|
|
144
|
+
* `max` collapses into a `+N` tile.
|
|
145
|
+
*
|
|
146
|
+
* Usage:
|
|
147
|
+
* ```tsx
|
|
148
|
+
* <AvatarGroup max={3}>
|
|
149
|
+
* {members.map((m) => <Avatar key={m.id} source={{ uri: m.avatarUrl }} name={m.name} />)}
|
|
150
|
+
* </AvatarGroup>
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function AvatarGroup({ children, max, size = "md", shape = "circle", accessibilityLabel, style: styleOverride, }) {
|
|
154
|
+
const { theme } = useTheme();
|
|
155
|
+
const styles = themedStyles(theme);
|
|
156
|
+
// toArray drops null/undefined/false children so a conditionally rendered
|
|
157
|
+
// avatar doesn't inflate the count or the overflow tally.
|
|
158
|
+
const items = React.Children.toArray(children);
|
|
159
|
+
const visible = max != null && max >= 0 ? items.slice(0, max) : items;
|
|
160
|
+
const overflowCount = items.length - visible.length;
|
|
161
|
+
const px = resolveSize(size);
|
|
162
|
+
const token = SIZE_TOKEN_FOR_PX[px];
|
|
163
|
+
const stackStyle = token ? geometry[`${token}Stack`] : { marginLeft: overlap(px) };
|
|
164
|
+
const totalTiles = visible.length + (overflowCount > 0 ? 1 : 0);
|
|
165
|
+
const contextValue = React.useMemo(() => ({ size, shape, ring: true }), [size, shape]);
|
|
166
|
+
return (_jsx(View, { accessible: true, accessibilityRole: "image", accessibilityLabel: accessibilityLabel ?? `${items.length} avatars`, style: [geometry.group, styleOverride], children: _jsxs(AvatarGroupContext.Provider, { value: contextValue, children: [visible.map((child, index) => (_jsx(View
|
|
167
|
+
// toArray assigns every child a stable key, so reordering the list
|
|
168
|
+
// moves the wrapper with its avatar instead of remounting both.
|
|
169
|
+
, {
|
|
170
|
+
// Earlier avatars paint over later ones, so the stack reads
|
|
171
|
+
// left-to-right instead of the DOM's later-wins order.
|
|
172
|
+
style: [index > 0 && stackStyle, { zIndex: totalTiles - index }], children: child }, React.isValidElement(child) ? child.key : index))), overflowCount > 0 ? (_jsx(View, { style: [visible.length > 0 && stackStyle, { zIndex: 0 }], children: _jsx(AvatarSurface, { px: px, shape: shape, ring: true, style: styles.overflow, children: _jsx(AvatarLabel, { px: px, style: styles.overflowText, children: `+${overflowCount}` }) }) })) : null] }) }));
|
|
173
|
+
}
|
|
174
|
+
/** px → token, so a size passed as a raw number still hits the registered rules. */
|
|
175
|
+
const SIZE_TOKEN_FOR_PX = {
|
|
176
|
+
[SIZE_TOKENS.sm]: "sm",
|
|
177
|
+
[SIZE_TOKENS.md]: "md",
|
|
178
|
+
[SIZE_TOKENS.lg]: "lg",
|
|
179
|
+
};
|
|
180
|
+
// Geometry carries no theme values, so a plain module-scope sheet is enough to
|
|
181
|
+
// get these rules into the server-rendered <head>. Only colors go through
|
|
182
|
+
// createThemedStyles below. See docs/ssr-hydration.md.
|
|
183
|
+
const geometry = StyleSheet.create({
|
|
184
|
+
base: {
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
justifyContent: "center",
|
|
187
|
+
overflow: "hidden",
|
|
188
|
+
},
|
|
189
|
+
image: {
|
|
190
|
+
width: "100%",
|
|
191
|
+
height: "100%",
|
|
192
|
+
},
|
|
193
|
+
labelText: {
|
|
194
|
+
// The tile is the touch/visual target; text must not add its own metrics.
|
|
195
|
+
includeFontPadding: false,
|
|
196
|
+
textAlign: "center",
|
|
197
|
+
userSelect: "none",
|
|
198
|
+
},
|
|
199
|
+
ring: {
|
|
200
|
+
borderWidth: spacing.xxs,
|
|
201
|
+
},
|
|
202
|
+
group: {
|
|
203
|
+
flexDirection: "row",
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
},
|
|
206
|
+
smCircle: { width: SIZE_TOKENS.sm, height: SIZE_TOKENS.sm, borderRadius: spacing.radiusFull },
|
|
207
|
+
mdCircle: { width: SIZE_TOKENS.md, height: SIZE_TOKENS.md, borderRadius: spacing.radiusFull },
|
|
208
|
+
lgCircle: { width: SIZE_TOKENS.lg, height: SIZE_TOKENS.lg, borderRadius: spacing.radiusFull },
|
|
209
|
+
smSquare: { width: SIZE_TOKENS.sm, height: SIZE_TOKENS.sm, borderRadius: squareRadius(SIZE_TOKENS.sm) },
|
|
210
|
+
mdSquare: { width: SIZE_TOKENS.md, height: SIZE_TOKENS.md, borderRadius: squareRadius(SIZE_TOKENS.md) },
|
|
211
|
+
lgSquare: { width: SIZE_TOKENS.lg, height: SIZE_TOKENS.lg, borderRadius: squareRadius(SIZE_TOKENS.lg) },
|
|
212
|
+
smStack: { marginLeft: overlap(SIZE_TOKENS.sm) },
|
|
213
|
+
mdStack: { marginLeft: overlap(SIZE_TOKENS.md) },
|
|
214
|
+
lgStack: { marginLeft: overlap(SIZE_TOKENS.lg) },
|
|
215
|
+
});
|
|
216
|
+
const createStyles = (theme) => StyleSheet.create({
|
|
217
|
+
fallback: {
|
|
218
|
+
backgroundColor: theme.colors.muted,
|
|
219
|
+
},
|
|
220
|
+
fallbackText: {
|
|
221
|
+
color: theme.colors.mutedForeground,
|
|
222
|
+
},
|
|
223
|
+
ring: {
|
|
224
|
+
borderColor: theme.colors.background,
|
|
225
|
+
},
|
|
226
|
+
overflow: {
|
|
227
|
+
backgroundColor: theme.colors.accent,
|
|
228
|
+
},
|
|
229
|
+
overflowText: {
|
|
230
|
+
color: theme.colors.accentForeground,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
234
|
+
export { Avatar, AvatarGroup };
|
|
@@ -117,6 +117,14 @@ interface BottomSheetContentProps extends ViewProps {
|
|
|
117
117
|
avoidKeyboard?: boolean;
|
|
118
118
|
/** Accepted for call-site ergonomics; ignored (platform owns keyboard). */
|
|
119
119
|
dismissKeyboardOnDrag?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Style for the native sheet surface behind the RN content — the web (vaul)
|
|
122
|
+
* panel, Android `containerColor`, iOS `presentationBackground`. Merged over
|
|
123
|
+
* the themed card default, so `{ backgroundColor: "transparent" }` lets a
|
|
124
|
+
* custom chrome (e.g. a glass backdrop) show through. The RN content column
|
|
125
|
+
* paints its own card fill too; clear that via `style`.
|
|
126
|
+
*/
|
|
127
|
+
backgroundStyle?: StyleProp<ViewStyle>;
|
|
120
128
|
style?: StyleProp<ViewStyle>;
|
|
121
129
|
children: React.ReactNode;
|
|
122
130
|
}
|
|
@@ -140,7 +148,7 @@ interface BottomSheetCloseProps {
|
|
|
140
148
|
declare function useBottomSheetContext(): BottomSheetContextValue;
|
|
141
149
|
declare function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, snapPoints, closeOnBackdropPress, children, }: BottomSheetProps): React.JSX.Element;
|
|
142
150
|
declare function BottomSheetTrigger({ asChild, children, style: styleOverride }: BottomSheetTriggerProps): React.JSX.Element;
|
|
143
|
-
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, style: styleOverride, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
151
|
+
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
144
152
|
declare function BottomSheetHandle({ style }: BottomSheetHandleProps): React.JSX.Element;
|
|
145
153
|
declare function BottomSheetHeader({ children, style, ...props }: BottomSheetHeaderProps): React.JSX.Element;
|
|
146
154
|
declare function BottomSheetBody({ children, style, contentContainerStyle, onContentSizeChange, onLayout, ...props }: BottomSheetBodyProps): React.JSX.Element;
|
|
@@ -296,7 +296,7 @@ function BottomSheetTrigger({ asChild, children, style: styleOverride }) {
|
|
|
296
296
|
// ============================================================================
|
|
297
297
|
function BottomSheetContent({
|
|
298
298
|
// Accepted-but-ignored ergonomics props (platform owns these behaviors):
|
|
299
|
-
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, style: styleOverride, children, }) {
|
|
299
|
+
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, children, }) {
|
|
300
300
|
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader } = useBottomSheetContext();
|
|
301
301
|
const { theme } = useTheme();
|
|
302
302
|
const dismissDisabled = useDismissDisabled();
|
|
@@ -337,8 +337,13 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
337
337
|
if (open)
|
|
338
338
|
onOpenChange(false);
|
|
339
339
|
},
|
|
340
|
-
// Themes the scrim/background on web (vaul)
|
|
341
|
-
|
|
340
|
+
// Themes the scrim/background on web (vaul), Android (containerColor),
|
|
341
|
+
// and iOS (presentationBackground). Flattened so native readers that
|
|
342
|
+
// expect a plain object (not a style array) keep working.
|
|
343
|
+
backgroundStyle: StyleSheet.flatten([
|
|
344
|
+
{ backgroundColor: theme.colors.card },
|
|
345
|
+
backgroundStyleOverride,
|
|
346
|
+
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { style: [
|
|
342
347
|
{
|
|
343
348
|
flex: 1,
|
|
344
349
|
maxHeight: detentHeight,
|