@mrmeg/expo-ui 0.18.0 → 0.21.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 +283 -0
- package/LLM_USAGE.md +14 -0
- package/README.md +7 -1
- package/dist/components/Avatar.d.ts +94 -0
- package/dist/components/Avatar.js +296 -0
- package/dist/components/Carousel.d.ts +99 -0
- package/dist/components/Carousel.js +246 -0
- package/dist/components/Drawer.js +5 -2
- package/dist/components/Label.d.ts +30 -10
- package/dist/components/Label.js +19 -10
- 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/constants/fonts.js +5 -4
- package/dist/hooks/useDimensions.d.ts +19 -9
- package/dist/hooks/useDimensions.js +24 -32
- package/dist/hooks/useResources.d.ts +0 -14
- package/dist/hooks/useResources.js +7 -39
- package/dist/lib/themedStyles.d.ts +7 -7
- package/dist/lib/themedStyles.js +7 -7
- package/dist/state/index.d.ts +0 -1
- package/dist/state/index.js +0 -1
- package/dist/state/themeStore.d.ts +15 -0
- package/dist/state/themeStore.js +38 -10
- package/llms-full.md +9 -4
- package/package.json +1 -1
- package/dist/state/SsrViewportContext.d.ts +0 -41
- package/dist/state/SsrViewportContext.js +0 -42
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,289 @@
|
|
|
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.21.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`Carousel` dots are pressable and scroll to their slide.** Each dot is a
|
|
11
|
+
`Pressable` that jumps the scroller to its page (animated) and reports the
|
|
12
|
+
new index through `onIndexChange`. The `tablist`/`tab` roles and `selected`
|
|
13
|
+
state the dots already carried are now honest: a screen reader that
|
|
14
|
+
announced N activatable tabs can finally focus and activate them, and on web
|
|
15
|
+
each dot is a keyboard tab-stop.
|
|
16
|
+
|
|
17
|
+
The visible dot stays 8px. The press target is a 24px wrapper plus vertical
|
|
18
|
+
`hitSlop` (44px total), and the row's `gap` moved into that wrapper so
|
|
19
|
+
neighboring targets sit flush with no dead strip between them. The slop is
|
|
20
|
+
vertical-only so a tap can't be ambiguous between two dots.
|
|
21
|
+
|
|
22
|
+
The selected dot also sets `aria-selected` explicitly: react-native-web
|
|
23
|
+
never maps `accessibilityState` to the DOM, so on web the tabs previously
|
|
24
|
+
exposed no selected state at all. Native maps `aria-selected` back into
|
|
25
|
+
`accessibilityState`, so the two props agree.
|
|
26
|
+
|
|
27
|
+
- **`Label` accepts `htmlFor`.** `@rn-primitives/label`'s web build only emits a
|
|
28
|
+
real `<label for="…">` — the thing that actually associates a label with an
|
|
29
|
+
input and makes clicking the label focus it — when `htmlFor` is passed, and
|
|
30
|
+
`Label` never forwarded it. No consumer could produce an associated label.
|
|
31
|
+
`htmlFor` now reaches the primitive's `Text`, which is where the prop is
|
|
32
|
+
declared and where the web build consumes it. Native ignores it, matching the
|
|
33
|
+
primitive.
|
|
34
|
+
|
|
35
|
+
Association takes **two distinct ids**:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<Label nativeID="email-label" htmlFor="email-input">Email</Label>
|
|
39
|
+
<TextInput nativeID="email-input" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`nativeID` is the label's own id (react-native-web maps it to `id`; on native
|
|
43
|
+
it's what a control's `accessibilityLabelledBy` points at). `htmlFor` is the
|
|
44
|
+
input's id.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **`Carousel` re-clamps its active index when `children` shrink.** The index
|
|
49
|
+
was seeded once, so removing slides while a late page was active left the
|
|
50
|
+
page announcement reading e.g. "5 of 3" with no active dot until the next
|
|
51
|
+
scroll. It now clamps to the last slide, reports the clamped value through
|
|
52
|
+
`onIndexChange`, and scrolls back to that page — the scroller was sitting
|
|
53
|
+
past the new content width.
|
|
54
|
+
|
|
55
|
+
- **`Carousel` index tracking is split by platform.** Native subscribes to
|
|
56
|
+
`onMomentumScrollEnd` — plus a velocity-gated `onScrollEndDrag`, for a drag
|
|
57
|
+
that releases at rest and so is never followed by a momentum event — and no
|
|
58
|
+
longer subscribes to `onScroll` at all, so a drag costs no per-frame JS
|
|
59
|
+
callback to compute a page the settle reports anyway. A drag past a slide
|
|
60
|
+
midpoint that snaps back to the starting page now reports nothing instead of
|
|
61
|
+
firing the intermediate page and then the original. Web keeps the throttled
|
|
62
|
+
`onScroll` path, which remains the only channel there — react-native-web
|
|
63
|
+
emits no momentum events.
|
|
64
|
+
|
|
65
|
+
The `onIndexChange` doc no longer claims "once per settle" unconditionally.
|
|
66
|
+
That now holds on native; on web (wheel/trackpad) it still fires once per
|
|
67
|
+
page *crossed*, and only consecutive duplicate indices are suppressed.
|
|
68
|
+
|
|
69
|
+
- **New `Carousel` testID: `<testID>-dot-<i>-indicator`.** `-dot-<i>` is now
|
|
70
|
+
the pressable wrapper; the visible dot — and therefore its
|
|
71
|
+
`backgroundColor` — is the `-indicator` node inside it. Tests asserting on a
|
|
72
|
+
dot's color need the new suffix.
|
|
73
|
+
|
|
74
|
+
- **`useDimensions` seeds from a constant, not context.** The first web render
|
|
75
|
+
uses the new `DEFAULT_VIEWPORT_WIDTH` / `DEFAULT_VIEWPORT_HEIGHT` exports
|
|
76
|
+
(1280×800, the previous context defaults) and switches to real dimensions
|
|
77
|
+
after mount. The hook no longer reads a context or writes an `mrmeg-vw`
|
|
78
|
+
cookie.
|
|
79
|
+
|
|
80
|
+
- **Theme persistence key is exported as `THEME_STORAGE_KEY`** (value
|
|
81
|
+
unchanged: `user-theme-preference`), replacing `THEME_COOKIE_NAME` as the
|
|
82
|
+
name callers reference.
|
|
83
|
+
|
|
84
|
+
### Removed
|
|
85
|
+
|
|
86
|
+
- **Server-rendering support surface.** The template that hosts this package
|
|
87
|
+
no longer server-renders web routes (routes are client-rendered from an
|
|
88
|
+
HTML shell), so the per-request plumbing that existed only to make a server
|
|
89
|
+
render and the browser's first render agree is gone:
|
|
90
|
+
|
|
91
|
+
- `SsrThemeSeedContext`, `SsrThemeSeed`, `SSR_THEME_SEED_DEFAULT`, and
|
|
92
|
+
`useSsrThemeSeed` (added in 0.20.0).
|
|
93
|
+
- `SsrViewportContext`, `SSR_VIEWPORT_DEFAULT_WIDTH`, and
|
|
94
|
+
`SSR_VIEWPORT_DEFAULT_HEIGHT`.
|
|
95
|
+
- `THEME_COOKIE_NAME`, and the `document.cookie` write `setTheme` performed
|
|
96
|
+
alongside its persisted write on web.
|
|
97
|
+
|
|
98
|
+
Hosts that server-render can keep doing so, but they now own the seeding
|
|
99
|
+
themselves; `useTheme` resolves from the store only.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **`Label`'s documented pairing pattern produced duplicate DOM ids.** The usage
|
|
104
|
+
docs recommended the *same* `nativeID` on the `Label` and its `TextInput`.
|
|
105
|
+
react-native-web maps `nativeID` to `id`, so that renders two elements sharing
|
|
106
|
+
one id — invalid HTML, and no association either way. The docs now show the
|
|
107
|
+
two-id pattern above.
|
|
108
|
+
|
|
109
|
+
- **`Avatar` renders image sources that carry no `uri`.** The internal source
|
|
110
|
+
key — which exists so a failure flag survives the fresh `{ uri }` object that
|
|
111
|
+
an inline literal allocates every render — returned `null` for any object
|
|
112
|
+
without a truthy `uri`, and `null` means "no image at all". An iOS
|
|
113
|
+
`{ bundle }` source, or a resolved asset object, therefore silently degraded
|
|
114
|
+
to initials and the image was never attempted. Every shape React Native
|
|
115
|
+
accepts is now keyed (numbers as `asset:<n>`, uri strings as themselves,
|
|
116
|
+
anything else by a stable serialization of its fields); `null` is reserved
|
|
117
|
+
for a missing, empty-object, or empty-array source. Retry-on-change and
|
|
118
|
+
keep-the-failure-on-re-render behavior now applies to those shapes too.
|
|
119
|
+
|
|
120
|
+
- **`AvatarGroup` members are announceable again (accessibility change).** The
|
|
121
|
+
group wrapper was `accessible` with `accessibilityRole="image"` and an
|
|
122
|
+
`"<n> avatars"` label, which is a leaf role on the web and swallows
|
|
123
|
+
descendants on iOS — so no member's name could be reached inside a group. The
|
|
124
|
+
wrapper is now a plain container with no accessibility semantics of its own,
|
|
125
|
+
and the count rides along as a visually clipped text node ahead of the
|
|
126
|
+
members, so a screen reader reads the summary and then each avatar. Passing
|
|
127
|
+
`accessibilityLabel` still replaces the count text. Apps that asserted on the
|
|
128
|
+
group by its accessibility label (`getByLabelText("3 avatars")`) should query
|
|
129
|
+
the text instead (`getByText("3 avatars")`).
|
|
130
|
+
|
|
131
|
+
- **`AvatarGroup`'s `max` is clamped to a whole count ≥ 0.** A negative `max`
|
|
132
|
+
reached `slice(0, max)`, which reads a negative end as an offset from the end
|
|
133
|
+
— `max={-3}` on three children rendered all of them, silently disabling the
|
|
134
|
+
clamp — and a fractional `max` truncated arbitrarily. `max` is now
|
|
135
|
+
`Math.max(0, Math.floor(max))`, so `max={0}` renders only the `+N` tile.
|
|
136
|
+
|
|
137
|
+
- **`Avatar` shows its initials/icon fallback while the image loads.** A slow
|
|
138
|
+
or large image left a bare `muted` tile for the whole request. The fallback
|
|
139
|
+
now stays mounted until the image reports `onLoad`, with the image layered
|
|
140
|
+
over it, so an avatar is never a blank circle in flight.
|
|
141
|
+
|
|
142
|
+
- **`Avatar`'s image carries its own corner radius.** Rounding relied solely on
|
|
143
|
+
the wrapper's `overflow: "hidden"`, which Android drops for a child image in
|
|
144
|
+
several cases, leaving square corners on a circular avatar.
|
|
145
|
+
|
|
146
|
+
### Documentation
|
|
147
|
+
|
|
148
|
+
- **`Carousel`'s `itemWidth` caveat.** A fractional `itemWidth` measures
|
|
149
|
+
against the viewport until the first `onLayout`, so a carousel inside a
|
|
150
|
+
horizontally constrained parent (a padded column, a `MaxWidthContainer`, a
|
|
151
|
+
sidebar pane) renders its first frame — and the exported HTML shell — with
|
|
152
|
+
slides sized to the window. Documented on the prop, with the workaround:
|
|
153
|
+
pass an absolute `itemWidth` (`> 1`) when the parent is narrower than the
|
|
154
|
+
viewport and that first frame matters.
|
|
155
|
+
|
|
156
|
+
## [0.20.0]
|
|
157
|
+
|
|
158
|
+
### Added
|
|
159
|
+
|
|
160
|
+
- **`surfaceSunken` semantic token.** The app-chrome tier below `background`,
|
|
161
|
+
so sidebars, rails, and side panels read as a layer *beneath* the content
|
|
162
|
+
pane instead of the same surface. Dark `#050506` (below zinc-950), light
|
|
163
|
+
`#FAFAFA` (zinc-50). Elevation is now a surface-tier ladder rather than a
|
|
164
|
+
shadow scale: `surfaceSunken` < `background` < `card`/`popover` < `muted`.
|
|
165
|
+
|
|
166
|
+
- **`borderStrong` semantic token.** A hairline for elements sitting on a
|
|
167
|
+
filled surface — chips on `muted`, raised panels — one visible step above
|
|
168
|
+
the fill, where `border` shares `muted`'s hex and disappears. Dark
|
|
169
|
+
`#3F3F46` (zinc-700), light `#D4D4D8` (zinc-300). `border` stays the
|
|
170
|
+
hairline for elements on `background`/`card`.
|
|
171
|
+
|
|
172
|
+
- **SSR theme seeding (`SsrThemeSeedContext`).** On a server-rendered web app,
|
|
173
|
+
`useTheme`/`useStyles` had no way to know the visitor's theme during the
|
|
174
|
+
server render, so every SSR page shipped a fully light-themed tree that
|
|
175
|
+
recolored after hydration. The store can't carry a per-request value — it's a
|
|
176
|
+
module singleton shared by concurrent SSR requests — so the new
|
|
177
|
+
`SsrThemeSeedContext` (with `SsrThemeSeed`, `SSR_THEME_SEED_DEFAULT`, and
|
|
178
|
+
`useSsrThemeSeed`) is the per-request channel. Provide it from a signal the
|
|
179
|
+
server and browser read identically (a cookie), and `useTheme` resolves from
|
|
180
|
+
it on web until persistence has loaded.
|
|
181
|
+
|
|
182
|
+
`SSR_THEME_SEED_DEFAULT` is `system`/light, i.e. what the store boots with —
|
|
183
|
+
so a host that receives no signal renders exactly as it did before the seed
|
|
184
|
+
existed. Note that this default is a *fallback*, not a reading: a host that
|
|
185
|
+
also writes the resolved scheme into its served HTML (`<html data-theme>`, a
|
|
186
|
+
blocking script's early-return, a `prefers-color-scheme` CSS fallback) must
|
|
187
|
+
distinguish "seed came from the visitor" from "seed is the default" and skip
|
|
188
|
+
the write in the latter case, or it will pin a dark-OS first-time visitor to
|
|
189
|
+
light with its own failsafes disabled.
|
|
190
|
+
|
|
191
|
+
- **`user-theme-preference` cookie mirror.** On web, `setTheme` now dual-writes
|
|
192
|
+
`localStorage` **and** a `user-theme-preference` cookie (`path=/;
|
|
193
|
+
max-age≈1y; SameSite=Lax`, exported as `THEME_COOKIE_NAME`), and `loadTheme`
|
|
194
|
+
backfills/repairs it from `localStorage`, so a server-rendered host can read
|
|
195
|
+
the visitor's theme off the request. `localStorage` remains the source of
|
|
196
|
+
truth; the cookie is a render hint only. Native persistence is unchanged.
|
|
197
|
+
|
|
198
|
+
- **`hasLoadedTheme` on the theme store.** False until `loadTheme()` has read
|
|
199
|
+
persistence (or `setTheme` was called). While it's false on web, `useTheme`
|
|
200
|
+
resolves from the SSR seed instead of store state; after that, store state
|
|
201
|
+
wins for good. Mirrors the `hasLoadedOnboarding` pattern.
|
|
202
|
+
|
|
203
|
+
### Changed
|
|
204
|
+
|
|
205
|
+
- **Dim text is more readable in both schemes (visual change).**
|
|
206
|
+
`textDim`/`mutedForeground` moved from `#A1A1AA` → `#B0B0B8` in dark
|
|
207
|
+
(~9.2:1 on `background`, ~6.9:1 on `muted`, up from ~7.8:1/~5.9:1) and from
|
|
208
|
+
zinc-500 `#71717A` → zinc-600 `#52525B` in light (~7.7:1 on white, up from
|
|
209
|
+
~4.8:1). Captions, timestamps, meta rows, and neutral `StatCard` changes all
|
|
210
|
+
render a step brighter (dark) or darker (light) than before; both stay
|
|
211
|
+
clearly dimmer than `text`, so hierarchy is preserved. A new package test
|
|
212
|
+
enforces the floors (≥7:1 on `background`/`card`/`popover`, ≥6:1 on
|
|
213
|
+
`muted`/`secondary`), so these tokens cannot be dimmed back down silently.
|
|
214
|
+
Apps that hardcoded the old hexes, or asserted them in tests, need to update
|
|
215
|
+
those literals; apps using the tokens get the change for free.
|
|
216
|
+
|
|
217
|
+
- **`Drawer` chrome paints `surfaceSunken`.** Both the overlay drawer panel and
|
|
218
|
+
the in-flow rail switched from `background` to `surfaceSunken`, so navigation
|
|
219
|
+
chrome is visibly darker than the content it sits beside (dark) or a faint
|
|
220
|
+
step off white (light). Override via the existing `style` props, or re-skin
|
|
221
|
+
`surfaceSunken` through `setColors`/`ThemeColorScope`, to restore the old
|
|
222
|
+
flat look.
|
|
223
|
+
|
|
224
|
+
- **`syncThemeFromEnvironment()` starts the OS listener before reading
|
|
225
|
+
persistence.** `loadTheme()` is what flips `hasLoadedTheme` (the moment web
|
|
226
|
+
renders stop trusting the SSR seed), so the real OS scheme is now read first
|
|
227
|
+
and a `system` user never sees a frame of the boot-default light.
|
|
228
|
+
|
|
229
|
+
## [0.19.0]
|
|
230
|
+
|
|
231
|
+
### Added
|
|
232
|
+
|
|
233
|
+
- **`Avatar` and `AvatarGroup`.** The package had `SkeletonAvatar` (a loading
|
|
234
|
+
placeholder) but no real avatar, so consumers hand-rolled circles. `Avatar`
|
|
235
|
+
renders an image with a graceful fallback chain — image → initials → icon —
|
|
236
|
+
and downgrades to initials at runtime when the image fails to load, so a
|
|
237
|
+
dead URL never leaves an empty hole. Accepts `source`, `name` (supplies 1–2
|
|
238
|
+
initials and the default accessibility label), `icon` (Feather, defaults to
|
|
239
|
+
`user`), `size` (`"sm" | "md" | "lg"` = 32/40/48, or an explicit pixel
|
|
240
|
+
diameter), and `shape` (`"circle" | "square"`). Initials derivation is
|
|
241
|
+
grapheme-aware: a decomposed accented character keeps its mark and an emoji
|
|
242
|
+
is not sliced into a lone surrogate.
|
|
243
|
+
|
|
244
|
+
`AvatarGroup` stacks children with a ring in the theme `background` color so
|
|
245
|
+
the stack reads on any surface, collapses anything past `max` into a `+N`
|
|
246
|
+
tile, propagates its `size` and `shape` to children that don't set their own,
|
|
247
|
+
and announces the total count (including the collapsed overflow) to screen
|
|
248
|
+
readers.
|
|
249
|
+
|
|
250
|
+
Uses React Native's own `Image`, so there is no new peer dependency.
|
|
251
|
+
|
|
252
|
+
- **`Carousel` — horizontally snapping slide row with dot indicators.**
|
|
253
|
+
Children-based: each child becomes one slide at the resolved item width.
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
<Carousel itemWidth={0.8} onIndexChange={setPage}>
|
|
257
|
+
{testimonials.map((t) => (
|
|
258
|
+
<Card key={t.name}>
|
|
259
|
+
<BodyText>{t.quote}</BodyText>
|
|
260
|
+
</Card>
|
|
261
|
+
))}
|
|
262
|
+
</Carousel>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`itemWidth` (default `0.85`) is a fraction of the carousel's own width when
|
|
266
|
+
`<= 1` — leaving the next slide peeking — and absolute pixels when `> 1`.
|
|
267
|
+
`gap`, `contentPadding`, `showDots`, `initialIndex`, `onIndexChange`, and
|
|
268
|
+
`snap` cover the rest; dots are plain themed views (active uses `accent`)
|
|
269
|
+
and auto-hide for a single slide. `onIndexChange` fires once per settle.
|
|
270
|
+
|
|
271
|
+
Built on `ScrollView`, not a virtualized list, so every slide is in the
|
|
272
|
+
server-rendered tree — web SSR ships the real slide content instead of an
|
|
273
|
+
empty scroller that fills in after measurement. No animation library is
|
|
274
|
+
involved: native snapping uses `snapToInterval`, and because
|
|
275
|
+
react-native-web drops that prop, web gets equivalent CSS scroll-snap
|
|
276
|
+
(`scroll-snap-type`/`scroll-snap-align` plus a `scroll-padding-left` that
|
|
277
|
+
keeps page `i` at `i * (itemWidth + gap)` on both platforms). Wheel and
|
|
278
|
+
trackpad scrolling keep the dots live via throttled scroll ticks, since RNW
|
|
279
|
+
emits no momentum events.
|
|
280
|
+
|
|
281
|
+
Accessibility: the dot row is a `tablist` of `tab`s carrying
|
|
282
|
+
`selected` state and per-slide labels, plus a visually clipped
|
|
283
|
+
`aria-live="polite"` "N of M" page announcement.
|
|
284
|
+
|
|
285
|
+
`getCarouselIndex(offsetX, interval, count)` and
|
|
286
|
+
`resolveCarouselItemWidth(itemWidth, containerWidth)` are exported for hosts
|
|
287
|
+
building custom controls on the same math.
|
|
288
|
+
|
|
6
289
|
## [0.18.0]
|
|
7
290
|
|
|
8
291
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -107,11 +107,13 @@ configureExpoUiI18n((key, options) => i18n.t(key, options));
|
|
|
107
107
|
Useful theme tokens include:
|
|
108
108
|
|
|
109
109
|
```tsx
|
|
110
|
+
theme.colors.surfaceSunken;
|
|
110
111
|
theme.colors.background;
|
|
111
112
|
theme.colors.foreground;
|
|
112
113
|
theme.colors.card;
|
|
113
114
|
theme.colors.popover;
|
|
114
115
|
theme.colors.border;
|
|
116
|
+
theme.colors.borderStrong;
|
|
115
117
|
theme.colors.input;
|
|
116
118
|
theme.colors.ring;
|
|
117
119
|
theme.colors.primary;
|
|
@@ -131,6 +133,13 @@ Token intent:
|
|
|
131
133
|
- `input`: form-control border color
|
|
132
134
|
- `ring`: focus outline color
|
|
133
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`.
|
|
134
143
|
|
|
135
144
|
Use `getShadowStyle()` for package surfaces that need elevation. It supports
|
|
136
145
|
`base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
|
|
@@ -187,10 +196,12 @@ Use this table before creating a new app-local primitive.
|
|
|
187
196
|
| `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections |
|
|
188
197
|
| `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs |
|
|
189
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 |
|
|
190
200
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
191
201
|
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms |
|
|
192
202
|
| `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs |
|
|
193
203
|
| `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries |
|
|
204
|
+
| `Carousel` | Horizontally snapping slide rows with pressable dot indicators | Hand-rolled snap `ScrollView` plus manual offset math | Testimonial strips, onboarding pages, feature highlights, image galleries |
|
|
194
205
|
| `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters |
|
|
195
206
|
| `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text |
|
|
196
207
|
| `Dialog`, `AlertDialog` | Modal decisions and custom modal content | Custom modal overlays | Confirm delete, edit profile, invite user |
|
|
@@ -227,8 +238,11 @@ Use this table before creating a new app-local primitive.
|
|
|
227
238
|
- Use `RadioGroup` for small mutually exclusive choices and `Select` for longer option sets.
|
|
228
239
|
- Use `Dialog` for blocking decisions, `Popover` for contextual controls, `Tooltip` for short explanations, and `DropdownMenu` for action lists.
|
|
229
240
|
- Use `Card` for individual repeated or framed items, not as a wrapper around full page sections.
|
|
241
|
+
- Use `Carousel` for a horizontal snap row of a known, small set of slides. It renders every child (no virtualization), so slides survive into the exported HTML shell and the first client frame; reach for `FlatList` when the data set is large or unbounded. Its dots are pressable and jump to their slide. A fractional `itemWidth` measures the viewport until the first layout, so pass absolute pixels (`> 1`) when the parent is narrower than the window and the first frame matters.
|
|
230
242
|
- Use `EmptyState` for no-data or recoverable error regions.
|
|
231
243
|
- Use `Skeleton` for loading content with stable layout.
|
|
244
|
+
- Use `Avatar` with both `source` and `name` whenever both exist: `name` supplies the initials shown when the image is absent, still loading, or failed, plus the default accessibility label. Never hand-roll a circle with a nested `Image` and initials `Text`. Inside `AvatarGroup`, set `size`/`shape` on the group — children inherit them and gain the ring; the group's count is a hidden summary node, so each member stays individually announceable and the group needs no `accessible` wrapper.
|
|
245
|
+
- Pair a standalone `Label` with its control using two DISTINCT ids: `nativeID` is the label's own id, `htmlFor` is the input's id (`<Label nativeID="email-label" htmlFor="email-input">` + `<TextInput nativeID="email-input" />`). Putting the same id on both renders duplicate ids on web and associates nothing. Prefer `TextInput`'s own `label` prop when you don't need a separate label element.
|
|
232
246
|
- Use `Progress` for real progress or indeterminate long-running work.
|
|
233
247
|
- Use `Drawer.Header` with `icon`, `title`, and `action` for a compact app-brand row; place `Drawer.ToggleCollapse` in `action` for a trailing rail control.
|
|
234
248
|
|
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 pressable 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, but that fraction measures the viewport until the first layout — pass absolute pixels (`> 1`) inside a constrained parent |
|
|
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 |
|
|
@@ -380,6 +384,8 @@ Use `Skeleton` components for loading content with stable dimensions, `EmptyStat
|
|
|
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, still loading, 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. `AvatarGroup` announces its count as a hidden summary and leaves each member individually announceable, so don't wrap it in an `accessible` container of your own.
|
|
388
|
+
|
|
383
389
|
### Quick Examples
|
|
384
390
|
|
|
385
391
|
Card, badge, and CTA:
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
/**
|
|
22
|
+
* Image to display. Any source React Native accepts — a uri object, a
|
|
23
|
+
* `require()`d asset, a uri-less descriptor, a multi-resolution array. The
|
|
24
|
+
* initials (then the icon) show while it loads and if it fails.
|
|
25
|
+
*/
|
|
26
|
+
source?: ImageSourcePropType;
|
|
27
|
+
/** Display name; supplies the fallback initials and the default accessibility label. */
|
|
28
|
+
name?: string;
|
|
29
|
+
/** Feather icon rendered when there is no image and no derivable initials. @default "user" */
|
|
30
|
+
icon?: IconName;
|
|
31
|
+
/** Size token or explicit pixel diameter. Inherited from `AvatarGroup` when unset. @default "md" */
|
|
32
|
+
size?: AvatarSize;
|
|
33
|
+
/** Inherited from `AvatarGroup` when unset. @default "circle" */
|
|
34
|
+
shape?: AvatarShape;
|
|
35
|
+
/**
|
|
36
|
+
* Accessibility label. Defaults to `name`; when neither is set the avatar is
|
|
37
|
+
* decorative and stays out of the accessibility tree.
|
|
38
|
+
*/
|
|
39
|
+
accessibilityLabel?: string;
|
|
40
|
+
style?: StyleProp<ViewStyle>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Avatar Component
|
|
44
|
+
*
|
|
45
|
+
* A circular (or rounded-square) profile image with a graceful fallback chain:
|
|
46
|
+
* image → initials → icon. The fallback shows while the image is in flight and
|
|
47
|
+
* stays put if the image fails, so neither a slow network nor a dead URL ever
|
|
48
|
+
* leaves an empty hole.
|
|
49
|
+
*
|
|
50
|
+
* Usage:
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <Avatar source={{ uri: user.avatarUrl }} name={user.fullName} />
|
|
53
|
+
* <Avatar name="Ada Lovelace" size="lg" />
|
|
54
|
+
* <Avatar icon="camera" shape="square" />
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function Avatar({ source, name, icon, size, shape, accessibilityLabel, style: styleOverride, }: AvatarProps): React.JSX.Element;
|
|
58
|
+
export interface AvatarGroupProps {
|
|
59
|
+
children?: React.ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* Maximum number of avatars to render; the rest collapse into a `+N` tile.
|
|
62
|
+
* Clamped to a whole count ≥ 0, so `max={0}` renders only the `+N` tile.
|
|
63
|
+
*/
|
|
64
|
+
max?: number;
|
|
65
|
+
/** Size applied to child avatars that don't set their own. @default "md" */
|
|
66
|
+
size?: AvatarSize;
|
|
67
|
+
/** Shape applied to child avatars that don't set their own. @default "circle" */
|
|
68
|
+
shape?: AvatarShape;
|
|
69
|
+
/**
|
|
70
|
+
* Summary announced ahead of the members, as a visually hidden text node.
|
|
71
|
+
* Defaults to `"<n> avatars"`, counting the collapsed overflow.
|
|
72
|
+
*/
|
|
73
|
+
accessibilityLabel?: string;
|
|
74
|
+
style?: StyleProp<ViewStyle>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* AvatarGroup Component
|
|
78
|
+
*
|
|
79
|
+
* Overlapping stack of avatars, each ringed in the theme `background` color so
|
|
80
|
+
* the stack reads on any surface. Earlier avatars sit on top; anything past
|
|
81
|
+
* `max` collapses into a `+N` tile.
|
|
82
|
+
*
|
|
83
|
+
* The group is not an accessibility element itself — its member avatars each
|
|
84
|
+
* announce their own name, preceded by a hidden count summary.
|
|
85
|
+
*
|
|
86
|
+
* Usage:
|
|
87
|
+
* ```tsx
|
|
88
|
+
* <AvatarGroup max={3}>
|
|
89
|
+
* {members.map((m) => <Avatar key={m.id} source={{ uri: m.avatarUrl }} name={m.name} />)}
|
|
90
|
+
* </AvatarGroup>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare function AvatarGroup({ children, max, size, shape, accessibilityLabel, style: styleOverride, }: AvatarGroupProps): React.JSX.Element;
|
|
94
|
+
export { Avatar, AvatarGroup };
|