@oxy.so/bloom 2.11.0 → 2.12.1
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/docs/app-shell.mdx +90 -4
- package/docs/content-panel.mdx +65 -19
- package/docs/date-picker.mdx +80 -2
- package/docs/listing-card.mdx +54 -0
- package/docs/rating.mdx +52 -2
- package/docs/stay-search.mdx +38 -0
- package/lib/commonjs/app-shell/AppShell.js +121 -5
- package/lib/commonjs/app-shell/AppShell.js.map +1 -1
- package/lib/commonjs/app-shell/layout.js +10 -5
- package/lib/commonjs/app-shell/layout.js.map +1 -1
- package/lib/commonjs/content-panel/ContentPanel.js +8 -2
- package/lib/commonjs/content-panel/ContentPanel.js.map +1 -1
- package/lib/commonjs/content-panel/ContentPanel.web.js +38 -25
- package/lib/commonjs/content-panel/ContentPanel.web.js.map +1 -1
- package/lib/commonjs/date-picker/TimeField.js +175 -0
- package/lib/commonjs/date-picker/TimeField.js.map +1 -0
- package/lib/commonjs/date-picker/index.js +7 -0
- package/lib/commonjs/date-picker/index.js.map +1 -1
- package/lib/commonjs/date-picker/time.js +160 -0
- package/lib/commonjs/date-picker/time.js.map +1 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/listing-card/ListingCard.js +34 -1
- package/lib/commonjs/listing-card/ListingCard.js.map +1 -1
- package/lib/commonjs/listing-card/WishlistCard.js +50 -8
- package/lib/commonjs/listing-card/WishlistCard.js.map +1 -1
- package/lib/commonjs/listing-card/shared.js +23 -1
- package/lib/commonjs/listing-card/shared.js.map +1 -1
- package/lib/commonjs/message-bubble/MessageBubble.js +16 -1
- package/lib/commonjs/message-bubble/MessageBubble.js.map +1 -1
- package/lib/commonjs/rating/RatingInput.js +218 -0
- package/lib/commonjs/rating/RatingInput.js.map +1 -0
- package/lib/commonjs/rating/index.js +7 -0
- package/lib/commonjs/rating/index.js.map +1 -1
- package/lib/commonjs/stay-search/DestinationSuggestions.js +46 -9
- package/lib/commonjs/stay-search/DestinationSuggestions.js.map +1 -1
- package/lib/commonjs/stay-search/GuestPicker.js +9 -0
- package/lib/commonjs/stay-search/GuestPicker.js.map +1 -1
- package/lib/commonjs/track-list/parts.js +0 -13
- package/lib/commonjs/track-list/parts.js.map +1 -1
- package/lib/module/app-shell/AppShell.js +121 -5
- package/lib/module/app-shell/AppShell.js.map +1 -1
- package/lib/module/app-shell/layout.js +10 -5
- package/lib/module/app-shell/layout.js.map +1 -1
- package/lib/module/content-panel/ContentPanel.js +8 -2
- package/lib/module/content-panel/ContentPanel.js.map +1 -1
- package/lib/module/content-panel/ContentPanel.web.js +38 -25
- package/lib/module/content-panel/ContentPanel.web.js.map +1 -1
- package/lib/module/date-picker/TimeField.js +170 -0
- package/lib/module/date-picker/TimeField.js.map +1 -0
- package/lib/module/date-picker/index.js +3 -0
- package/lib/module/date-picker/index.js.map +1 -1
- package/lib/module/date-picker/time.js +151 -0
- package/lib/module/date-picker/time.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/listing-card/ListingCard.js +34 -1
- package/lib/module/listing-card/ListingCard.js.map +1 -1
- package/lib/module/listing-card/WishlistCard.js +50 -8
- package/lib/module/listing-card/WishlistCard.js.map +1 -1
- package/lib/module/listing-card/shared.js +22 -0
- package/lib/module/listing-card/shared.js.map +1 -1
- package/lib/module/message-bubble/MessageBubble.js +16 -1
- package/lib/module/message-bubble/MessageBubble.js.map +1 -1
- package/lib/module/rating/RatingInput.js +212 -0
- package/lib/module/rating/RatingInput.js.map +1 -0
- package/lib/module/rating/index.js +1 -0
- package/lib/module/rating/index.js.map +1 -1
- package/lib/module/stay-search/DestinationSuggestions.js +45 -9
- package/lib/module/stay-search/DestinationSuggestions.js.map +1 -1
- package/lib/module/stay-search/GuestPicker.js +9 -0
- package/lib/module/stay-search/GuestPicker.js.map +1 -1
- package/lib/module/track-list/parts.js +1 -14
- package/lib/module/track-list/parts.js.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShell.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/layout.d.ts +5 -4
- package/lib/typescript/commonjs/app-shell/layout.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/types.d.ts +12 -2
- package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/types.d.ts +53 -42
- package/lib/typescript/commonjs/content-panel/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/date-picker/TimeField.d.ts +6 -0
- package/lib/typescript/commonjs/date-picker/TimeField.d.ts.map +1 -0
- package/lib/typescript/commonjs/date-picker/index.d.ts +2 -1
- package/lib/typescript/commonjs/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/date-picker/time.d.ts +56 -0
- package/lib/typescript/commonjs/date-picker/time.d.ts.map +1 -0
- package/lib/typescript/commonjs/date-picker/types.d.ts +49 -3
- package/lib/typescript/commonjs/date-picker/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/ListingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/WishlistCard.d.ts +1 -1
- package/lib/typescript/commonjs/listing-card/WishlistCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/shared.d.ts +8 -1
- package/lib/typescript/commonjs/listing-card/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/listing-card/types.d.ts +37 -0
- package/lib/typescript/commonjs/listing-card/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/message-bubble/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/RatingInput.d.ts +8 -0
- package/lib/typescript/commonjs/rating/RatingInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/rating/index.d.ts +2 -1
- package/lib/typescript/commonjs/rating/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +31 -0
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/stay-search/DestinationSuggestions.d.ts +8 -0
- package/lib/typescript/commonjs/stay-search/DestinationSuggestions.d.ts.map +1 -1
- package/lib/typescript/commonjs/stay-search/GuestPicker.d.ts +6 -1
- package/lib/typescript/commonjs/stay-search/GuestPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/stay-search/types.d.ts +20 -0
- package/lib/typescript/commonjs/stay-search/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/track-list/parts.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShell.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/layout.d.ts +5 -4
- package/lib/typescript/module/app-shell/layout.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/types.d.ts +12 -2
- package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/ContentPanel.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/ContentPanel.web.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/types.d.ts +53 -42
- package/lib/typescript/module/content-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/date-picker/TimeField.d.ts +6 -0
- package/lib/typescript/module/date-picker/TimeField.d.ts.map +1 -0
- package/lib/typescript/module/date-picker/index.d.ts +2 -1
- package/lib/typescript/module/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/module/date-picker/time.d.ts +56 -0
- package/lib/typescript/module/date-picker/time.d.ts.map +1 -0
- package/lib/typescript/module/date-picker/types.d.ts +49 -3
- package/lib/typescript/module/date-picker/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/ListingCard.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/WishlistCard.d.ts +1 -1
- package/lib/typescript/module/listing-card/WishlistCard.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/shared.d.ts +8 -1
- package/lib/typescript/module/listing-card/shared.d.ts.map +1 -1
- package/lib/typescript/module/listing-card/types.d.ts +37 -0
- package/lib/typescript/module/listing-card/types.d.ts.map +1 -1
- package/lib/typescript/module/message-bubble/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/module/rating/RatingInput.d.ts +8 -0
- package/lib/typescript/module/rating/RatingInput.d.ts.map +1 -0
- package/lib/typescript/module/rating/index.d.ts +2 -1
- package/lib/typescript/module/rating/index.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +31 -0
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/stay-search/DestinationSuggestions.d.ts +8 -0
- package/lib/typescript/module/stay-search/DestinationSuggestions.d.ts.map +1 -1
- package/lib/typescript/module/stay-search/GuestPicker.d.ts +6 -1
- package/lib/typescript/module/stay-search/GuestPicker.d.ts.map +1 -1
- package/lib/typescript/module/stay-search/types.d.ts +20 -0
- package/lib/typescript/module/stay-search/types.d.ts.map +1 -1
- package/lib/typescript/module/track-list/parts.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app-shell/AppShell.tsx +91 -4
- package/src/app-shell/layout.ts +10 -5
- package/src/app-shell/types.ts +12 -2
- package/src/content-panel/ContentPanel.tsx +8 -2
- package/src/content-panel/ContentPanel.web.tsx +39 -25
- package/src/content-panel/types.ts +50 -42
- package/src/date-picker/TimeField.tsx +162 -0
- package/src/date-picker/index.ts +6 -0
- package/src/date-picker/time.ts +158 -0
- package/src/date-picker/types.ts +52 -3
- package/src/index.ts +9 -2
- package/src/index.web.ts +9 -2
- package/src/listing-card/ListingCard.tsx +34 -1
- package/src/listing-card/WishlistCard.tsx +42 -6
- package/src/listing-card/shared.ts +22 -0
- package/src/listing-card/types.ts +37 -0
- package/src/message-bubble/MessageBubble.tsx +21 -1
- package/src/rating/RatingInput.tsx +214 -0
- package/src/rating/index.ts +9 -1
- package/src/rating/types.ts +33 -0
- package/src/stay-search/DestinationSuggestions.tsx +48 -9
- package/src/stay-search/GuestPicker.tsx +9 -0
- package/src/stay-search/types.ts +20 -0
- package/src/track-list/parts.tsx +1 -20
package/docs/app-shell.mdx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: AppShell
|
|
3
|
-
description: Page frame with
|
|
3
|
+
description: Page frame with five layout variants, the sidebar and a responsive drawer, pinned top/bottom/floating slots, a breadcrumb + title header, a notification bell and a floating Pro offer card.
|
|
4
4
|
order: 83
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -27,9 +27,9 @@ import { AppShell, NotificationBell, ProOfferCard } from '@oxy.so/bloom/app-shel
|
|
|
27
27
|
|
|
28
28
|
## Variants
|
|
29
29
|
|
|
30
|
-
One engine,
|
|
30
|
+
One engine, five shapes. `variant` picks the shape; everything else — the nav,
|
|
31
31
|
the drawer, the pinned regions, the slots, the sizes — works the same in all
|
|
32
|
-
|
|
32
|
+
five.
|
|
33
33
|
|
|
34
34
|
| `variant` | The page | Scroll |
|
|
35
35
|
| --- | --- | --- |
|
|
@@ -37,6 +37,7 @@ four.
|
|
|
37
37
|
| `feed` | nav + a `contentWidth` reading column CENTRED in what the nav leaves, optional `aside` beside it, optionally in a `ContentPanel` | `document` / `container` / `fixed` |
|
|
38
38
|
| `split` | a list pane, a detail pane and an optional info pane, each scrolling on its own, with a draggable divider | always one screen |
|
|
39
39
|
| `focus` | one centred `contentWidth` column and NO nav | `document` / `container` / `fixed` |
|
|
40
|
+
| `canvas` | ONE edge-to-edge area — no column, no cap, no padding — with the chrome floating over it | always one screen |
|
|
40
41
|
|
|
41
42
|
`dashboard` is what this component always was, unchanged to the pixel — the
|
|
42
43
|
existing stories render byte-identically against the version before variants
|
|
@@ -62,12 +63,59 @@ aside at `y = 16` while the content moved from `16` to `-390`.
|
|
|
62
63
|
|
|
63
64
|
`panel` wraps the centre column in a [`ContentPanel`](/docs/bloom/main/content-panel),
|
|
64
65
|
so the page background reads as a gutter around a framed surface; `framedFrom`
|
|
65
|
-
is forwarded.
|
|
66
|
+
is forwarded.
|
|
67
|
+
|
|
68
|
+
**The panel occupies the screen either way — what differs is what scrolls.**
|
|
69
|
+
|
|
70
|
+
- `scroll="document"` (the default): the page scrolls, and the panel's frame is
|
|
71
|
+
a screen-tall sticky rectangle pinned at the shell's own `gutter`, so the
|
|
72
|
+
panel reads as a surface filling the window while the content moves inside
|
|
73
|
+
it. Measured in Chrome at 1280 × 900: the frame reports `top: 16, height: 868`
|
|
74
|
+
at scrollY 0, 1, 200, 600 and 846 — the same rectangle every time — while the
|
|
75
|
+
content behind it travels from 16 to −830. (The inset is what makes that
|
|
76
|
+
true: at the 8px default the frame would sit above the panel's real top edge
|
|
77
|
+
and snap down on the first scroll.)
|
|
78
|
+
- `scroll="fixed"` / `"container"`: the shell bounds the panel, so the panel IS
|
|
79
|
+
the box — it closes at the bottom like the rail beside it, the header is
|
|
80
|
+
pinned inside it, and only the routed content scrolls, under the frame. The
|
|
81
|
+
document never moves at all.
|
|
82
|
+
|
|
83
|
+
Both are right; they are different apps. A feed with the address bar collapsing
|
|
84
|
+
on a phone and real anchor links wants `document`; a workspace whose regions
|
|
85
|
+
each keep their own position wants `fixed`. The panel's content is inset by `gutter` — without it the page sits
|
|
66
86
|
ON the frame, and a radius-28 corner clips whatever is in it (the heading loses
|
|
67
87
|
its first letter). So `contentWidth` is the COLUMN's width, and the reading
|
|
68
88
|
measure inside a panel is that minus two gutters. Do not render your own
|
|
69
89
|
`ContentPanel` inside this one; it trips the nesting guard.
|
|
70
90
|
|
|
91
|
+
### `canvas`
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<AppShell
|
|
95
|
+
variant="canvas"
|
|
96
|
+
sidebar={{ ...sidebar, surface: 'docked' }}
|
|
97
|
+
aside={<PlacesInView />}
|
|
98
|
+
floatingAction={<Button variant="primary" leadingIcon={RiAddFill}>Add a pin</Button>}
|
|
99
|
+
>
|
|
100
|
+
<Map />
|
|
101
|
+
</AppShell>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The content **is** the screen: one area, edge to edge, no reading column, no max
|
|
105
|
+
width, no padding, exactly one viewport tall. A map, a board, an editor, a photo
|
|
106
|
+
viewer. The header (if you pass one) sits above it in flow, because a toolbar
|
|
107
|
+
over a map belongs to the map; `topBar`, `bottomBar`, `floatingAction` and
|
|
108
|
+
`overlay` float over it as everywhere else.
|
|
109
|
+
|
|
110
|
+
The shell's padding moves OUT of the row and onto the regions, so the nav is
|
|
111
|
+
still a card with a gutter around it and the aside keeps its inset while the
|
|
112
|
+
canvas alone runs to the window's edge — no negative margins anywhere: each
|
|
113
|
+
region states its own inset.
|
|
114
|
+
|
|
115
|
+
**`canvas` never scrolls the document**, for the same reason `split` doesn't: a
|
|
116
|
+
canvas that grew the page would pan the page instead of the canvas.
|
|
117
|
+
`scroll="document"` resolves to `fixed`, `scroll="container"` is honoured.
|
|
118
|
+
|
|
71
119
|
### `split`
|
|
72
120
|
|
|
73
121
|
```tsx
|
|
@@ -267,6 +315,44 @@ overlapping another, and no console error.
|
|
|
267
315
|
A second column on the right of the page (`dashboard` and `feed`; `split` has
|
|
268
316
|
its `info` pane instead and `focus` has no second column at all). From `asideFrom` (`xl`, 1280; or `md` / `lg`) it sits beside the content at `asideWidth` (320), pinned like the sidebar — with document scroll it is sticky and scrolls its own overflow, so give it `flex: 1` content if it should fill the height. Below the breakpoint it is stacked after the content (`asideCollapse="stack"`, the default) or dropped (`"hidden"`). Under `drawer="reveal"` it slides with the page.
|
|
269
317
|
|
|
318
|
+
## Eight shells, one component
|
|
319
|
+
|
|
320
|
+
The shape (`variant`), what scrolls (`scroll`), whether the column is a panel
|
|
321
|
+
(`panel`) and the nav's own look (`sidebar.surface` / `sidebar.size`) are
|
|
322
|
+
independent. That is the point: an app picks a row from this table and does not
|
|
323
|
+
look like the app that picked the row above it.
|
|
324
|
+
|
|
325
|
+
| The app | variant | scroll | nav |
|
|
326
|
+
| --- | --- | --- | --- |
|
|
327
|
+
| Product dashboard | `dashboard` | `document` | `card`, `medium` |
|
|
328
|
+
| Social reader | `feed` + `panel` | `fixed` | `plain`, `large` |
|
|
329
|
+
| Long-form feed | `feed` | `document` | `card`, `medium` |
|
|
330
|
+
| Workspace (channels, files) | `dashboard` | `fixed` | `docked`, `small` |
|
|
331
|
+
| Messenger | `split` | one screen | `docked`, `small` |
|
|
332
|
+
| Map / board / editor | `canvas` | one screen | `docked`, `medium` |
|
|
333
|
+
| Marketplace | `dashboard`, no `sidebar`, `topBar` `always` | `document` | — |
|
|
334
|
+
| Sign-in, onboarding, reader | `focus` | `document` | none |
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
// Social reader: a framed column that fills the screen, a nav with no edge.
|
|
338
|
+
<AppShell variant="feed" panel scroll="fixed" sidebar={{ items, surface: 'plain', size: 'large' }} title="Home">
|
|
339
|
+
{feed}
|
|
340
|
+
</AppShell>
|
|
341
|
+
|
|
342
|
+
// Workspace: a column flush to the window, the page bounded, its own scrollers.
|
|
343
|
+
<AppShell scroll="fixed" sidebar={{ items, surface: 'docked', size: 'small' }} title="Reports">
|
|
344
|
+
{report}
|
|
345
|
+
</AppShell>
|
|
346
|
+
|
|
347
|
+
// Marketplace: no rail at all — the navigation is the bar across the top.
|
|
348
|
+
<AppShell topBar={<MarketHeader />} topBarVisibility="always" contentMaxWidth={1280}>
|
|
349
|
+
{results}
|
|
350
|
+
</AppShell>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The stories **Three identities**, **Feed in a full-height panel** and **Canvas**
|
|
354
|
+
render three of these side by side with nothing positioned by hand.
|
|
355
|
+
|
|
270
356
|
## The nav's identity is not the shell's layout
|
|
271
357
|
|
|
272
358
|
`variant` picks the LAYOUT; `sidebar.surface` and `sidebar.size` pick what the navigation looks like. They are separate axes on purpose — the same shell carries a social reader, a workspace and a product dashboard without any of them re-implementing it:
|
package/docs/content-panel.mdx
CHANGED
|
@@ -51,28 +51,48 @@ the shadow is what makes it a panel lifted off it.
|
|
|
51
51
|
| `surfaceColor` | what the panel tells its content it is painted in — only when `surfaceClassName`/`surfaceStyle` repainted it |
|
|
52
52
|
| `contentStyle` / `contentClassName` | the column inside it |
|
|
53
53
|
| `maskColor` | the colour the gutter mask fades to |
|
|
54
|
-
| `overlaySizing` | (web)
|
|
54
|
+
| `overlaySizing` | (web) the screen's frame (default) or the panel's own box — see below |
|
|
55
|
+
| `overlayInset` | (web) where that sticky frame pins — match your layout's gutter |
|
|
56
|
+
| `fill` | the panel is the height it is given; its content scrolls inside |
|
|
55
57
|
|
|
56
|
-
## `overlaySizing`: the
|
|
58
|
+
## `overlaySizing`: the screen's frame, or the panel's own box
|
|
57
59
|
|
|
58
60
|
The web overlays (the border frame and the gutter bleed-mask) default to
|
|
59
|
-
**`overlaySizing="
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
**`overlaySizing="viewport"`**: a screen-tall sticky rectangle, `100dvh` minus
|
|
62
|
+
the insets. The panel's own box grows with its content and the document scrolls
|
|
63
|
+
it, while the edge stays where the screen is — so the panel occupies the window
|
|
64
|
+
from top to bottom and the content moves inside it. That is what makes a
|
|
65
|
+
document-scrolled column read as a surface rather than as an open-ended strip
|
|
66
|
+
whose bottom edge is somewhere past the fold.
|
|
67
|
+
|
|
68
|
+
**`overlayInset` has to match where the panel really starts.** Sticky
|
|
69
|
+
positioning leaves an element at its static position until scrolling would push
|
|
70
|
+
it past the inset, so a panel that begins 16px down the page with the default
|
|
71
|
+
8px inset draws its frame at 16 and then SNAPS it to 8 on the first scroll. That
|
|
72
|
+
8px jump is what "the panel moves when I scroll" is. Pass the gutter your
|
|
73
|
+
layout uses:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<View style={{ padding: 16 }}>
|
|
77
|
+
<ContentPanel framed overlayInset={16}>{page}</ContentPanel>
|
|
78
|
+
</View>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`AppShell` passes its own `gutter` for you. Measured in Chrome at 1280 × 900,
|
|
82
|
+
`variant="feed" panel`: the frame reports `top: 16, height: 868` at scrollY 0,
|
|
83
|
+
1, 200, 600 and 846 — the same rectangle at every one — while the surface
|
|
84
|
+
behind it moves from 16 to −830.
|
|
85
|
+
|
|
86
|
+
An inset PAIR (`{ top, bottom }`) is for persistent chrome the consumer keeps
|
|
87
|
+
outside the panel: a sticky header above it shifts where the panel visually
|
|
88
|
+
starts without moving its DOM position, so the frame starts lower without also
|
|
89
|
+
ending higher.
|
|
90
|
+
|
|
91
|
+
`overlaySizing="panel"` is the other mode: the overlays are the panel's own box,
|
|
92
|
+
through CSS Grid layer-stacking. Use it when the panel's box already IS the
|
|
93
|
+
visible area (`fill`, below) or when something placed outside the panel must
|
|
94
|
+
never be painted over by an overlay sized to the whole viewport — a bounded app
|
|
95
|
+
shell with its own internal scrollers.
|
|
76
96
|
|
|
77
97
|
```tsx
|
|
78
98
|
<View style={{ flex: 1 }}>
|
|
@@ -83,6 +103,32 @@ as a header above it.
|
|
|
83
103
|
</View>
|
|
84
104
|
```
|
|
85
105
|
|
|
106
|
+
## `fill`: a panel that reaches the bottom
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<AppShell scroll="fixed"> {/* something has to bound it */}
|
|
110
|
+
<ContentPanel framed fill>
|
|
111
|
+
<FlatList … /> {/* scrolls INSIDE the panel */}
|
|
112
|
+
</ContentPanel>
|
|
113
|
+
</AppShell>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
By default the panel grows with its content and the page scrolls: on a feed
|
|
117
|
+
taller than the window you see the panel's top edge and its bottom edge is
|
|
118
|
+
somewhere past the fold — an open-ended column, not a surface.
|
|
119
|
+
|
|
120
|
+
`fill` makes the panel the height of the space it is given, with its content
|
|
121
|
+
scrolling inside it. It closes at the bottom of the screen, the way the
|
|
122
|
+
[`Sidebar`](/docs/bloom/main/sidebar) beside it does, and the frame never moves
|
|
123
|
+
because the frame IS that box.
|
|
124
|
+
|
|
125
|
+
It needs a parent that bounds it — an `AppShell` with `scroll="fixed"` or
|
|
126
|
+
`scroll="container"` (which passes `fill` for you when you ask for `panel`), or
|
|
127
|
+
any screen that is `flex: 1`. In a document-scrolled page there is no height to
|
|
128
|
+
fill and the panel keeps growing; that page wants `scroll="document"`, not this
|
|
129
|
+
prop. On web the content wrapper becomes the scroller; on native the panel is
|
|
130
|
+
already `flex: 1`, so a `FlatList` inside fills it and scrolls within it.
|
|
131
|
+
|
|
86
132
|
## `framedFrom` is why the branch is not in your screen
|
|
87
133
|
|
|
88
134
|
A phone wants a plain column: a frame a few pixels from the screen edge reads
|
package/docs/date-picker.mdx
CHANGED
|
@@ -6,16 +6,19 @@ order: 13
|
|
|
6
6
|
|
|
7
7
|
# DatePicker
|
|
8
8
|
|
|
9
|
-
`DatePicker`, `DateRangePicker` and `
|
|
9
|
+
`DatePicker`, `DateRangePicker`, `MeetingScheduler` and `TimeField`. Bloom takes no dependency on react-aria or `@internationalized/date`, so the month layout, selection, constraints and keyboard navigation are plain TypeScript over local-midnight `Date`s. Colours come from the Bloom-derived ramps in `src/button/shared.ts`.
|
|
10
10
|
|
|
11
11
|
```tsx
|
|
12
|
-
import { DatePicker, DateRangePicker, MeetingScheduler, Calendar, RangeCalendar } from '@oxy.so/bloom/date-picker';
|
|
12
|
+
import { DatePicker, DateRangePicker, MeetingScheduler, TimeField, Calendar, RangeCalendar } from '@oxy.so/bloom/date-picker';
|
|
13
13
|
|
|
14
14
|
const [day, setDay] = useState<Date | null>(null);
|
|
15
15
|
<DatePicker value={day} onChange={setDay} minDate={new Date()} />
|
|
16
16
|
|
|
17
17
|
const [range, setRange] = useState<DateRange | null>(null);
|
|
18
18
|
<DateRangePicker value={range} onChange={setRange} />
|
|
19
|
+
|
|
20
|
+
const [time, setTime] = useState<string | null>('18:30');
|
|
21
|
+
<TimeField value={time} onChange={setTime} step={15} accessibilityLabel="Viewing time" />
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
## Pickers
|
|
@@ -81,3 +84,78 @@ A range takes two presses in either order; between them the band follows the poi
|
|
|
81
84
|
| `disabled?` | `boolean` | `false` | Blocks opening (pickers). |
|
|
82
85
|
| `RangeCalendar.visibleMonths?` | `1 \| 2` | `1` | Months side by side. |
|
|
83
86
|
| `defaultMonth?` | `Date` | value's month, else today's | First month shown (calendars). |
|
|
87
|
+
|
|
88
|
+
## TimeField
|
|
89
|
+
|
|
90
|
+
`DatePicker` is the day; `TimeField` is the hour and minute. It is a **field,
|
|
91
|
+
not a fourth popover** — a viewing at 18:30 is four keystrokes, and a popover is
|
|
92
|
+
the wrong instrument for four keystrokes. (`MeetingScheduler` still has the
|
|
93
|
+
other shape: a fixed list of bookable slots, which is a different question —
|
|
94
|
+
"which of these?" rather than "when?".)
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const [time, setTime] = useState<string | null>(null);
|
|
98
|
+
<TimeField
|
|
99
|
+
value={time}
|
|
100
|
+
onChange={setTime}
|
|
101
|
+
min="09:00"
|
|
102
|
+
max="20:00"
|
|
103
|
+
step={15}
|
|
104
|
+
accessibilityLabel="Viewing time"
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| prop | |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `value` | 24h `"HH:mm"`, or `null` while empty — fully controlled |
|
|
111
|
+
| `onChange` | the committed time, or `null` once emptied; never fires while typing |
|
|
112
|
+
| `min`, `max` | 24h `"HH:mm"` bounds; a time outside is pulled to the bound |
|
|
113
|
+
| `step` | minutes per arrow press, and the grid a typed time snaps to. Default `1` |
|
|
114
|
+
| `hourFormat` | `'24h'` (default) or `'12h'` — how it is DRAWN; the value stays 24h |
|
|
115
|
+
| `size` | `'small'` (96 × 32) or `'medium'` (default, 104 × 38) |
|
|
116
|
+
| `disabled` | not editable, `aria-disabled`, `DISABLED_OPACITY` |
|
|
117
|
+
| `accessibilityLabel` | **required** — the field draws no label, and `"--:--"` names nothing |
|
|
118
|
+
| `placeholder` | default `"--:--"`, or `"--:-- --"` in 12h |
|
|
119
|
+
| `width`, `style`, `testID` | |
|
|
120
|
+
|
|
121
|
+
**The value is always 24h.** `hourFormat` decides what the reader sees, never
|
|
122
|
+
what the caller receives, so a `TimeField` value drops straight into one of
|
|
123
|
+
`MeetingScheduler`'s `timeSlots` and the app needs no locale to read it back.
|
|
124
|
+
|
|
125
|
+
**It looks like the date chip because it is the same control.** Geometry,
|
|
126
|
+
palette and focus behaviour are `DateChipInput`'s — the `DD/MM/YYYY` chip inside
|
|
127
|
+
the pickers — so a date and a time typed beside each other line up: 1px
|
|
128
|
+
`chipBorder` darkening to `chipBorderFocused` on focus (the border IS the focus
|
|
129
|
+
indicator, `outline-none`), radius 10, shadow-xs, the panel fill.
|
|
130
|
+
|
|
131
|
+
### Typing
|
|
132
|
+
|
|
133
|
+
The field keeps its own **draft** while you type, so the text is not reformatted
|
|
134
|
+
under the cursor, and commits on blur or submit. Escape reverts to the committed
|
|
135
|
+
value, and so does a draft that does not parse — rather than an error the field
|
|
136
|
+
has no room to draw.
|
|
137
|
+
|
|
138
|
+
Parsing is deliberately forgiving, because a field that only accepts what it
|
|
139
|
+
printed is a field people fight. All of these commit to `09:30`:
|
|
140
|
+
|
|
141
|
+
`9:30` · `930` · `9.30` · `9 30` · `0930` · `9:3` · `9:30 am` · `9:30 a.m.`
|
|
142
|
+
|
|
143
|
+
and `9` alone is `09:00`, `21:30` is `21:30`, `9pm` is `21:00`. An `am`/`pm`
|
|
144
|
+
marker decides the half of the day whatever `hourFormat` says — somebody who
|
|
145
|
+
typed `pm` means pm.
|
|
146
|
+
|
|
147
|
+
`step` snaps a committed time to a grid counted **from midnight**, so `step={15}`
|
|
148
|
+
always means :00 :15 :30 :45. Counting from `min` instead would knock the round
|
|
149
|
+
times off the grid the moment a shop opened at 09:10.
|
|
150
|
+
|
|
151
|
+
### Keyboard
|
|
152
|
+
|
|
153
|
+
ArrowUp and ArrowDown move by `step` and commit immediately, clamped at `min`
|
|
154
|
+
and `max` rather than wrapping — a wrap from `max` back to `min` reads as the
|
|
155
|
+
field refusing the key. With no value yet, ArrowUp starts at `min` (or 00:00)
|
|
156
|
+
and ArrowDown at `max` (or 23:59).
|
|
157
|
+
|
|
158
|
+
They are a convenience **on a text box**, not a `spinbutton`: the control
|
|
159
|
+
announces itself as the text field it is, named by `accessibilityLabel`, and
|
|
160
|
+
every value the arrows can reach can also be typed. A `spinbutton` role would
|
|
161
|
+
promise `aria-valuenow` over a value that is a string and may be empty.
|
package/docs/listing-card.mdx
CHANGED
|
@@ -84,6 +84,8 @@ and plural rules. Nothing here computes a price.
|
|
|
84
84
|
| `badge` | top-left over the photo — a string draws the white pill, any node is placed as given |
|
|
85
85
|
| `favorite`, `onFavoriteChange` | the heart, top-right; drawn only when `onFavoriteChange` is given |
|
|
86
86
|
| `onPress`, `href` | see below |
|
|
87
|
+
| `onLongPress`, `onContextMenu` | the press shortcut — see below |
|
|
88
|
+
| `hoverZoom` | web only, off by default: the photo scales 1.06 under the pointer |
|
|
87
89
|
| `loading` | a skeleton in the same geometry (`aria-busy`) |
|
|
88
90
|
| `layout` | `'vertical'` (default, photo 20:19) or `'horizontal'` (square photo 40% wide, for lists and map sheets) |
|
|
89
91
|
| `density` | `'comfortable'` (default) or `'compact'`, a dense list row — see below |
|
|
@@ -106,6 +108,32 @@ anchor's default navigation is prevented and `onPress` runs — a router takes i
|
|
|
106
108
|
On native, `href` alone opens with `Linking`. With neither the card is not a
|
|
107
109
|
control.
|
|
108
110
|
|
|
111
|
+
**The press shortcut.** `onLongPress` fires on a long press — native, and a
|
|
112
|
+
touch-and-hold on web. `onContextMenu` is its pointer spelling: a right-click on
|
|
113
|
+
web, with the event defaulted for you, which is the same contract
|
|
114
|
+
[`MessageBubble`](/docs/message-bubble) carries. Two props rather than one
|
|
115
|
+
bridged prop, because they are two different events and an app may well want
|
|
116
|
+
only one of them.
|
|
117
|
+
|
|
118
|
+
Two things follow from defaulting the event. The browser's own menu — "open in
|
|
119
|
+
a new tab", "copy link address" — no longer appears **on the card** while
|
|
120
|
+
`onContextMenu` is set, so set it only where the shortcut is worth that. And
|
|
121
|
+
neither event has a keyboard spelling or a name to announce, so whatever they
|
|
122
|
+
open must be reachable another way: the heart, a row in a menu, a button on the
|
|
123
|
+
detail screen. A long press is a shortcut, never the only door.
|
|
124
|
+
|
|
125
|
+
**Hover zoom.** `hoverZoom` scales the photo to 1.06 while the pointer is over
|
|
126
|
+
the card, 400ms, inside the tile that already clips — the photo comes forward,
|
|
127
|
+
the card does not move and the grid does not reflow. It is WEB ONLY and guarded
|
|
128
|
+
by `@media (any-hover: hover)`, so a touch screen reporting a hover it cannot
|
|
129
|
+
deliver is left out, and it does nothing at all under
|
|
130
|
+
`prefers-reduced-motion: reduce`. The compact density ignores it: a 112px
|
|
131
|
+
thumbnail has nothing to come forward from.
|
|
132
|
+
|
|
133
|
+
It is **off by default.** A card that grows under the pointer is a decision an
|
|
134
|
+
app makes for its own grid, not one to turn on under every app already shipping
|
|
135
|
+
this card.
|
|
136
|
+
|
|
109
137
|
**The controls are siblings of the link, not children.** The heart and arrows are
|
|
110
138
|
laid over the photo from outside the link: a button inside an anchor is invalid
|
|
111
139
|
HTML, and would make pressing the heart open the stay. Each is its own tab stop.
|
|
@@ -190,6 +218,32 @@ it, two sit side by side, three put one tall photo beside two, four (or more) ma
|
|
|
190
218
|
a 2×2 of the first four. The name and `description` sit under it. `onPress` and
|
|
191
219
|
`href` behave as on `ListingCard`; the name is "Coast weekends, 12 saved".
|
|
192
220
|
|
|
221
|
+
| prop | decides |
|
|
222
|
+
| ----------------- | ---------------------------------------------------------------------------------------- |
|
|
223
|
+
| `name` | the collection's name, one line |
|
|
224
|
+
| `description` | the pre-formatted line under it ("12 saved") |
|
|
225
|
+
| `photos` | up to four covers, URLs or `ImageResolver` ids |
|
|
226
|
+
| `icon` | a 16px glyph left of the name — decorative |
|
|
227
|
+
| `color` | the `icon`'s colour, and the empty cover's tint |
|
|
228
|
+
| `empty` | what the cover holds while `photos` is empty |
|
|
229
|
+
| `onPress`, `href` | as on `ListingCard` |
|
|
230
|
+
| `accessibilityLabel`, `style`, `testID` | `testID` derives `-cover`, `-icon`, `-empty` |
|
|
231
|
+
|
|
232
|
+
**The icon goes beside the name, never inside it.** A folder emoji typed into
|
|
233
|
+
`name` is read out by a screen reader as its character, cannot be coloured or
|
|
234
|
+
sized, and counts against the one line the name gets. `icon` is a
|
|
235
|
+
[`BloomIconComponent`](/docs/icons), `aria-hidden`, drawn at 16 in `color` (or
|
|
236
|
+
text-secondary), and the name keeps its own line.
|
|
237
|
+
|
|
238
|
+
`color` is the caller's colour rather than a token, because a collection's
|
|
239
|
+
colour is the person's choice and Bloom has no ramp for it. It paints the glyph
|
|
240
|
+
and tints the cover while the collection is empty.
|
|
241
|
+
|
|
242
|
+
**Empty.** With no photos the cover is the placeholder square, and `empty` is
|
|
243
|
+
centred in it — a glyph, a line, an "add your first save" affordance. Without
|
|
244
|
+
`empty` it is the plain square it has always been, so nothing changes for a
|
|
245
|
+
caller that has not asked for one.
|
|
246
|
+
|
|
193
247
|
## Colours
|
|
194
248
|
|
|
195
249
|
All from the theme: the text is `text` / `textSecondary`; the badge pill and the
|
package/docs/rating.mdx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Rating
|
|
3
|
-
description: A compact star, value and review count,
|
|
3
|
+
description: A compact star, value and review count, the bar for a rating breakdown, and the star picker a review form is written with.
|
|
4
4
|
order: 61
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Rating
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import { Rating, RatingBar } from '@oxy.so/bloom/rating';
|
|
10
|
+
import { Rating, RatingBar, RatingInput } from '@oxy.so/bloom/rating';
|
|
11
11
|
|
|
12
12
|
<Rating value={4.92} count={128} /> // ★ 4.92 (128)
|
|
13
13
|
<Rating value={4.81} count={214} countStyle="reviews" /> // ★ 4.81 · 214 reviews
|
|
@@ -15,8 +15,13 @@ import { Rating, RatingBar } from '@oxy.so/bloom/rating';
|
|
|
15
15
|
|
|
16
16
|
<RatingBar label="Cleanliness" value={4.9} display="4.9" />
|
|
17
17
|
<RatingBar label="5" labelWidth={12} value={0.86} max={1} display="86%" />
|
|
18
|
+
|
|
19
|
+
const [stars, setStars] = useState<number | null>(null);
|
|
20
|
+
<RatingInput value={stars} onChange={setStars} accessibilityLabel="Overall rating" />
|
|
18
21
|
```
|
|
19
22
|
|
|
23
|
+
`Rating` shows a rating that exists; `RatingInput` is how one comes to exist.
|
|
24
|
+
|
|
20
25
|
## `Rating`
|
|
21
26
|
|
|
22
27
|
| prop | decides |
|
|
@@ -53,3 +58,48 @@ measurement — and a rating breakdown is not a chart: the rows do not encode
|
|
|
53
58
|
different categories by colour, they all say "how good is this, out of 5". It is
|
|
54
59
|
a `progressbar` carrying `aria-valuemin`, `aria-valuemax`, `aria-valuenow` and
|
|
55
60
|
`aria-valuetext` (from `display`).
|
|
61
|
+
|
|
62
|
+
## `RatingInput`
|
|
63
|
+
|
|
64
|
+
The picker for a review form. Fully controlled: `value` is `1..max` or `null`
|
|
65
|
+
while nothing is chosen, and `onChange` fires with the star that was chosen.
|
|
66
|
+
|
|
67
|
+
| prop | decides |
|
|
68
|
+
| -------------------- | -------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `value` | the chosen rating, or `null` for nothing chosen yet |
|
|
70
|
+
| `onChange` | called with the chosen rating; never called with the value already chosen |
|
|
71
|
+
| `max` | how many stars, default `5` |
|
|
72
|
+
| `size` | `'small'` (24px star), `'medium'` (default, 32) or `'large'` (40) |
|
|
73
|
+
| `disabled` | no press, no keyboard, the group at `DISABLED_OPACITY` |
|
|
74
|
+
| `accessibilityLabel` | **required** — the name of what is being rated ("Overall rating") |
|
|
75
|
+
| `formatStarLabel` | each star's own name, default `"1 star"` / `"4 stars"` — translate it |
|
|
76
|
+
| `style`, `testID` | `testID` derives `-star-<n>` |
|
|
77
|
+
|
|
78
|
+
Chosen stars fill with the ACCENT, the rest are `RiStarLine` outlines in the
|
|
79
|
+
neutral ramp — the same reasoning as [`RatingBar`](#ratingbar)'s fill: a chosen
|
|
80
|
+
star is an answer somebody gave, and text-primary reads as ink rather than as
|
|
81
|
+
one. Under a pointer the stars up to the hovered one fill as a preview, which
|
|
82
|
+
never touches `value`.
|
|
83
|
+
|
|
84
|
+
### Operating it without a mouse
|
|
85
|
+
|
|
86
|
+
The group is a `radiogroup` named by `accessibilityLabel`, holding one `radio`
|
|
87
|
+
per star named by `formatStarLabel`. Exactly one of N is what a rating IS, so
|
|
88
|
+
`radio` is the role rather than a `slider` — and it is the only one that gives
|
|
89
|
+
each star its own touch target and its own name to announce.
|
|
90
|
+
|
|
91
|
+
The tab stop roves: the chosen star is the one in the tab order, or the first
|
|
92
|
+
star while nothing is chosen, so one Tab reaches the group either way. Inside
|
|
93
|
+
it, ArrowRight/ArrowUp, ArrowLeft/ArrowDown, Home and End move **and** choose,
|
|
94
|
+
which is how an ARIA radio group behaves. The focus ring is drawn on the star.
|
|
95
|
+
|
|
96
|
+
### No half stars, and no clearing
|
|
97
|
+
|
|
98
|
+
Half a star doubles the keyboard stops and halves the target — 16px of a 32px
|
|
99
|
+
star, under any touch floor — and the icon set has no half-star glyph, so the
|
|
100
|
+
state could not be drawn honestly either. `Rating` still DISPLAYS fractional
|
|
101
|
+
values (`4.92`): a display has no target and no keyboard.
|
|
102
|
+
|
|
103
|
+
Clearing is left out for the reason re-choosing a [`Radio`](/docs/radio) is a
|
|
104
|
+
no-op — a radio has no "off". A form that has no rating yet holds `null`; it
|
|
105
|
+
does not need the group to express a state it cannot draw.
|
package/docs/stay-search.mdx
CHANGED
|
@@ -118,6 +118,38 @@ It is a `listbox` of `option`s with `aria-selected` on the highlighted row. The
|
|
|
118
118
|
list takes focus on web and answers ArrowDown/ArrowUp (wrapping) and Enter;
|
|
119
119
|
hover moves the same highlight. The rows stay out of the tab order.
|
|
120
120
|
|
|
121
|
+
**A row that cannot be chosen.** `disabled` on an item dims it to
|
|
122
|
+
`tokens.DISABLED_OPACITY`, gives it `aria-disabled`, refuses press and Enter,
|
|
123
|
+
keeps hover from highlighting it, and makes the arrow keys step OVER it —
|
|
124
|
+
ArrowDown from the row above lands past it, and a list where every row is
|
|
125
|
+
disabled leaves the highlight alone rather than parking it on a row that answers
|
|
126
|
+
nothing.
|
|
127
|
+
|
|
128
|
+
`disabledReason` is the line that says WHY ("Location is off"). It replaces
|
|
129
|
+
`description` while the row is disabled and is read after the title, so the row
|
|
130
|
+
announces "Use my location, Location is off".
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<DestinationSuggestions
|
|
134
|
+
items={[
|
|
135
|
+
{
|
|
136
|
+
id: 'here',
|
|
137
|
+
title: 'Use my location',
|
|
138
|
+
icon: RiFocus3Line,
|
|
139
|
+
disabled: !locationOn,
|
|
140
|
+
disabledReason: 'Location is off',
|
|
141
|
+
},
|
|
142
|
+
{ id: 'lisbon', title: 'Lisbon', description: 'Portugal' },
|
|
143
|
+
]}
|
|
144
|
+
onSelect={choose}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The row stays in the list rather than disappearing, and it is drawn by the list
|
|
149
|
+
rather than beside it: a row that vanishes teaches nothing, and one hand-drawn
|
|
150
|
+
outside the `listbox` is not an `option` — it breaks the arrow keys and the
|
|
151
|
+
"2 of 5" a screen reader counts.
|
|
152
|
+
|
|
121
153
|
### GuestPicker
|
|
122
154
|
|
|
123
155
|
`StepperRow`s for adults, children, infants and pets, with hairlines between
|
|
@@ -125,6 +157,12 @@ rows. `value: { adults, children, infants, pets }`, `onChange`, `max` (one
|
|
|
125
157
|
number or per kind), `kinds` (which rows, in order), `labels`, `descriptions`
|
|
126
158
|
(`null` hides one).
|
|
127
159
|
|
|
160
|
+
`decrementLabel` and `incrementLabel` name every row's `−` and `+` buttons —
|
|
161
|
+
`Stepper`'s own prop names, forwarded to all of them. They default to the
|
|
162
|
+
English `"Decrease"` / `"Increase"`, and they are the last two words in this
|
|
163
|
+
family that a translated app could not reach. The row's title is what says
|
|
164
|
+
which counter a button belongs to, so one pair covers every row.
|
|
165
|
+
|
|
128
166
|
It is the ONE guests picker: the search bar's "Who" panel and `BookingCard`'s
|
|
129
167
|
guests popover both use it. For the booking side:
|
|
130
168
|
|