@botiverse/raft-ui 0.82.0 → 0.84.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 +147 -0
- package/dist/components/app-link-card/app-link-card.mjs +19 -37
- package/dist/components/banner/banner.mjs +15 -2
- package/dist/components/dialog/dialog-sheet-context.mjs +7 -2
- package/dist/components/dialog/dialog-sheet.d.mts +3 -2
- package/dist/components/dialog/dialog-sheet.mjs +29 -43
- package/dist/components/dialog/dialog.d.mts +12 -1
- package/dist/components/dialog/dialog.mjs +50 -10
- package/dist/components/drag/drag.recipe.mjs +1 -1
- package/dist/components/fit-toolbar/fit-toolbar.d.mts +165 -0
- package/dist/components/fit-toolbar/fit-toolbar.mjs +736 -0
- package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +35 -0
- package/dist/components/fit-toolbar/index.d.mts +2 -0
- package/dist/components/lightbox/index.d.mts +2 -2
- package/dist/components/lightbox/lightbox.d.mts +70 -4
- package/dist/components/lightbox/lightbox.mjs +285 -91
- package/dist/components/list-row/list-row.d.mts +14 -1
- package/dist/components/list-row/list-row.mjs +4 -2
- package/dist/components/list-row/list-row.recipe.d.mts +15 -3
- package/dist/components/list-row/list-row.recipe.mjs +18 -1
- package/dist/components/message-action-card/message-action-card-content.recipe.mjs +3 -3
- package/dist/components/message-action-card/message-action-card.mjs +7 -3
- package/dist/components/message-action-card/message-action-card.recipe.mjs +2 -6
- package/dist/components/message-embed/index.d.mts +2 -1
- package/dist/components/message-embed/message-embed.d.mts +8 -1
- package/dist/components/message-embed/message-embed.mjs +2 -2
- package/dist/components/message-embed/message-embed.recipe.d.mts +160 -0
- package/dist/components/message-embed/message-embed.recipe.mjs +49 -10
- package/dist/components/message-forwarded-bundle/message-forwarded-bundle.mjs +1 -0
- package/dist/components/message-forwarded-bundle/message-forwarded-bundle.recipe.mjs +1 -1
- package/dist/components/message-quoted-preview/message-quoted-preview.mjs +2 -0
- package/dist/components/message-replies/message-replies.mjs +1 -1
- package/dist/components/message-replies/message-replies.recipe.mjs +1 -1
- package/dist/components/mobile-nav/mobile-nav-server.d.mts +21 -8
- package/dist/components/mobile-nav/mobile-nav-server.mjs +142 -13
- package/dist/components/popover/popover.d.mts +11 -2
- package/dist/components/popover/popover.mjs +51 -38
- package/dist/components/preview-card/preview-card.d.mts +5 -1
- package/dist/components/preview-card/preview-card.mjs +24 -27
- package/dist/components/save-status/save-status.d.mts +16 -9
- package/dist/components/save-status/save-status.mjs +19 -30
- package/dist/components/segmented-control/segmented-control.d.mts +1 -1
- package/dist/components/segmented-control/segmented-control.mjs +56 -10
- package/dist/components/selection-toolbar/index.d.mts +2 -0
- package/dist/components/selection-toolbar/selection-toolbar.d.mts +100 -0
- package/dist/components/selection-toolbar/selection-toolbar.mjs +217 -0
- package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +42 -0
- package/dist/components/task-board/task-board.d.mts +5 -0
- package/dist/components/task-board/task-board.mjs +8 -1
- package/dist/components/task-dnd/task-dnd.d.mts +10 -1
- package/dist/components/task-dnd/task-dnd.mjs +2 -1
- package/dist/components/toast/toast.mjs +2 -2
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +4 -2
- package/dist/lib/label-value-fit.mjs +33 -6
- package/dist/lib/laid-out-children.mjs +32 -0
- package/dist/lib/stacked-actions.mjs +7 -4
- package/dist/lib/use-mobile-viewport.mjs +5 -2
- package/dist/lib/use-soft-keyboard.mjs +72 -0
- package/dist/wip.d.mts +8 -3
- package/dist/wip.mjs +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
+
## [0.84.0](https://github.com/botiverse/raft-design/compare/v0.83.0...v0.84.0) (2026-11-10)
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **LightboxBody, the scrolling body of a document-mode lightbox** (from `@botiverse/raft-ui/wip`): a ScrollArea under `LightboxToolbar` that fills the rest of the content (`min-h-0 flex-1`), takes the pointer back from `LightboxContent` (`pointer-events-auto`), and never passes a scroll on to the page under the scrim (`overscroll-contain`), on `layer-canvas`, the toolbar's surface. `className` sets another surface (`bg-layer-sidebar`); every other ScrollArea prop, `render` included, is the area's, except `overscroll`. It carries `data-slot="lightbox-body"`. It is a tab stop even when it has nothing to scroll (`tabIndex={0}`), with the inset focus ring (a 2px `foreground` outline inside its edge, after keyboard input only) instead of ScrollArea's ring outside it, which the window's edges hid. Its content is padded by `env(safe-area-inset-left|right|bottom)`, and `contentClassName` and `contentStyle` style a box of the caller's inside that (`data-slot="lightbox-body-content"`), so a phone held sideways doesn't put a wide file under the notch or its end under the home bar, and the scrollbar stays at the window's edge.
|
|
6
|
+
- Why: slock's FileViewerShell and SelectShareLightbox each write a lightbox's scrolling body of their own (Kirby's review of #1130, #proj-raft-ui:#226). A ScrollArea stops a scroll only while it has something to scroll, so a body too short to scroll passed the wheel on to the page under the scrim, which document mode doesn't lock.
|
|
7
|
+
- Affects: nothing until a lightbox uses it. The document-mode example on the Lightbox page now does, with the same DOM and classes as before apart from `data-slot`, `overscroll-contain`, `tabIndex` and the focus ring, and the safe-area box its `contentClassName` now sits in.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/lightbox/#document-mode
|
|
9
|
+
- **Lightbox document mode.** `<Lightbox modal="trap-focus">` traps focus without locking the page's scroll, for a file read top to bottom whose own body scrolls (a PDF, Markdown, a diagram); the content stays neither `inert` nor hidden, so find in page reaches it. In document mode `LightboxContent` takes Base UI's own focus defaults: focus moves into the lightbox when it opens and back to `LightboxTrigger` when it closes. A lightbox opened from state passes `finalFocus={openerRef}` to send focus back to what opened it.
|
|
10
|
+
- Why: slock's FileViewerShell (images and attachment previews: PDF, documents, Mermaid) and SelectShareLightbox move onto raft-ui's Lightbox, and they kept their own body-scroll switch and focus return (#triage:#278, #proj-raft-ui:#226).
|
|
11
|
+
- Affects: nothing changes for a lightbox until it passes `modal="trap-focus"`; `modal` keeps its default, `true`, and in it `initialFocus` and `finalFocus` stay `false`.
|
|
12
|
+
- Docs: https://raft-design.botiverse.build/components/lightbox/#document-mode
|
|
13
|
+
- **`attached` on ListRow: a row that belongs to the setting above it**: in a `ListRowGroup`, an `attached` row draws no hairline above it, and a `plain` one has no top padding, so it follows the row it belongs to with only that row's 12px bottom inset between them. An interactive one (a chevron, a menu) keeps the control height as its least, so it stays a 44px target on a phone and a touch screen (34px with a mouse on a desktop, which it already is). The row carries `data-attached`. Tie it to its setting for assistive technology with the setting's title id: a display-only attached row takes `role="group"` and `aria-labelledby`, an interactive one `aria-describedby`; ListRow can't know which title it belongs to. The row above keeps its height, and its switch stays centered on its own title and description; the row after draws its hairline as usual.
|
|
14
|
+
- Why: a hairline says "another setting", so quiet hours' start and end times, or a server's channel overrides under its level, read as settings of their own (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e).
|
|
15
|
+
- Affects: nothing until a row passes `attached`: without it, `ListRow` renders the same DOM and classes as before. slock's quiet hours times, its channel overrides and the message font size preview should take it.
|
|
16
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/#a-row-that-belongs-to-the-setting-above, https://raft-design.botiverse.build/patterns/forms/#a-settings-row
|
|
17
|
+
- **FitToolbar's `plus` step: the primary action as + alone** (from `@botiverse/raft-ui/wip`). `FitToolbarButton` takes `plusOrder`: from that step on it is a + square, its icon and label gone, the label kept as its accessible name and shown as its tooltip; it carries `data-fit-plus`, and `useFitItem({ plus })` gives any item the step. Every item's steps share one order for the toolbar, by their numbers (lower first; at the same number the last item first, and one item's steps as `label`, `more`, `collapse`, `wrap`, `plus`). Give `plus` a higher number than any `wrap`, so the primary becomes + only once the groups have wrapped. `FitToolbarButton`'s `icon` is optional now, for a primary that has none until it is +.
|
|
18
|
+
- Why: Tasks at 375px and 420px: once the filters have wrapped to their own scrolling line, the first line (Mine and All with their counts, sort, list or board, Create task) fits only with Create task as + (sindresorhus, #proj-raft-ui:#225). The step was left out of 0.83.0 for want of a test.
|
|
19
|
+
- Affects: nothing until a button passes `plusOrder`: without it, `FitToolbarButton` renders the same DOM and classes as before.
|
|
20
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/#behavior
|
|
21
|
+
- **`messageCard` and `messageEmbed`, from `@botiverse/raft-ui`**: the recipes every card in a message is drawn from, so an app's own card takes the same parts. `messageCard()` gives `source` (with `sourceIcon`, `sourceName`, `sourceHost`, `sourceHostText`), `sourceFooter`, `body`, `eyebrow`, `title`, `heading`, `meta`, `actions`, `actionRow`, `inset`, `actionGroup` (the buttons as one item of a row that also holds a status line) and `popover`; `messageEmbed({ size })` the frame and its widths, which `MessageEmbed size` applies.
|
|
22
|
+
- Why: the rule for a card in a message is to take its inset, title, details and buttons from these recipes, never classes of its own; slock's Artifact, audio and app message cards can't follow it without them (Jasper's review of raft-design #1125).
|
|
23
|
+
- Affects: nothing until an app uses them.
|
|
24
|
+
- Docs: https://raft-design.botiverse.build/patterns/cards-in-messages/#one-place-for-the-styles
|
|
25
|
+
- **`size="item"` on PopoverContent**: a popover about an app's item is 400px wide (never wider than the room the positioner leaves it, so on a phone the screen less its padding), and its `PopoverBody`, `PopoverTitle` and `PopoverActions` take a card's parts: the 12px inset, the `text-subhead` bold title, one row of buttons. The content carries `data-size="item"`.
|
|
26
|
+
- Why: the question an app's link asks before it shows previews stands in for the item card it leads to, so it looks like that card (stdrc, 2026-11-10); a popover belongs to the family of what it is about.
|
|
27
|
+
- Affects: nothing until a popover passes `size="item"`; the default renders the same DOM and classes as before. slock's link-preview question should take it.
|
|
28
|
+
- Docs: https://raft-design.botiverse.build/components/popover/#about-an-item
|
|
29
|
+
- **FitToolbarPicker and FitToolbarButton's `collapsedIcon`, for the Tasks filters** (from `@botiverse/raft-ui/wip`). `FitToolbarPicker` is a picker's trigger as one toolbar item: pass the popup's trigger as `trigger` (`<DropdownMenuTrigger />`, `<PopoverTrigger />`) with an `icon`, a label and what is chosen (`value`); it is a PickerTriggerButton, filled `info-soft` once a value is chosen (an applied filter, Select › Filters); a count is set in Space Mono and any other value in the label's font. From `labelOrder` on its label gives way and the icon, the value and the chevron stay (👤 1 ⌄), named "Assignee 1". `FitToolbarButton`'s `collapsedIcon` shows only once its label has given way, in place of `icon` (Clear filters becomes ×). A `FitToolbarButton` with neither refuses its `label` step.
|
|
30
|
+
- Why: the Tasks filters are pickers that must keep what is chosen in view once they narrow, and Clear filters reads as text while it has room (sindresorhus, #proj-raft-ui:#225).
|
|
31
|
+
- Affects: nothing until a page uses them. A `FitToolbarButton` without `collapsedIcon` renders the same DOM and classes as before, plus `data-fit-needs-icon`.
|
|
32
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/#anatomy
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Toasts are drawn above a lightbox**: the toast viewport and an anchored toast's positioner move from `z-70` to `z-80`, above the lightbox's `z-70`. Elevation now writes the order down ([Layer order](https://raft-design.botiverse.build/foundations/elevation/#layer-order)).
|
|
37
|
+
- Why: a toast fired from inside a lightbox (Link copied, a failed download from FileViewerShell's toolbar) was drawn under it, and in document mode the lightbox's body is opaque, so it couldn't be seen (Kirby's review of #1130).
|
|
38
|
+
- Affects: every toast; it is drawn as before over the page, the popups and the dialogs. Nothing else sits between `z-70` and `z-80`.
|
|
39
|
+
- Docs: https://raft-design.botiverse.build/foundations/elevation/#layer-order, https://raft-design.botiverse.build/components/toast/#where-it-appears
|
|
40
|
+
|
|
41
|
+
- **SaveStatus shows only a failure, and takes no room otherwise**: an empty `SaveStatus` is hidden (`empty:hidden`) and has no height, so a setting with it is as tall as one without it. With a `SaveStatusError` in it, the line appears 4px under the control or the description, as before, and the setting grows by it. `SaveStatusSaved` is deprecated: it renders nothing, whatever its children (no check, no "Saved", no live region). `ListRow variant="plain"` no longer leaves a SaveStatus line out of what its ends center on: its ends center on the whole row, with an error shown too.
|
|
42
|
+
- Why: a setting that applies at once needs no "Saved": the control showing its new value is the confirmation, as in macOS's and iOS's settings. The reserved, usually empty line made those rows taller than their neighbors (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e). This replaces the reserved "Saved" line chosen on 2026-10-30 (Kirby's option A, which stdrc agreed to, #proj-raft-ui:401f3241).
|
|
43
|
+
- Affects: every `SaveStatus` while it holds no error, and every plain `ListRow` whose body ends in one (slock's `ImmediateSaveStatus` and `ImmediateSwitchRow`, its Language & region, onboarding, member permissions, server translation and system channels settings, the composer's send key and the channel's thread sort): the row or field is shorter by the line and its 4px, and a switch at a row's end moves up by half of that. A failure now grows the setting by its line, and a row's ends move down by half of it. Drop `SaveStatusSaved` and its "Saved" copy; render `SaveStatusError` when a change failed and nothing otherwise. A plain row without a SaveStatus is unchanged except that it no longer carries the `--list-row-status-space` classes. slock, on upgrading:
|
|
44
|
+
- `SETTINGS_STACK_CARD_DESKTOP` (settingsCardClasses.ts:12) still draws a stacked card's last SaveStatus inside the 16px below it (`[&_[data-slot=save-status]:last-child]:mt-0` and `mb-[calc(-1lh)]`, the 2026-11-04 rule this replaces). With an error shown, its 18px line overlaps the next setting by 2px, or runs into the card's padding, and the setting doesn't grow. On staging only PublicVisibilitySection takes the class, with no SaveStatus, so nothing shows it yet; drop the two classes.
|
|
45
|
+
- `settingsImmediateApply.behavior.test.tsx` fails: `assertIdleLine` looks for the "Saved" live region (`getByRole("status")`, `data-slot=save-status-saved`), and "Saved shows the check inside the same line" waits for "Saved". Assert the idle line is hidden and empty instead, and drop the Saved test. The "saved" state in ImmediateSetting.tsx becomes dead; `SETTING_SAVED_MS` is also used by AppNotificationsControls.tsx, so keep it while that does.
|
|
46
|
+
- Docs: https://raft-design.botiverse.build/components/save-status/, https://raft-design.botiverse.build/patterns/forms/#settings-that-apply-at-once
|
|
47
|
+
|
|
48
|
+
- **Cards in messages take their styles from one place**: MessageEmbed's recipe now holds the card frame and its two widths, `size="item"` (at most 400px, the message's width below 768px) and `size="content"` (at most 560px), and the `messageCard` recipe beside it holds the parts every card and item popover shares: the source row, the body and its 12px inset, the id line, the title (`text-subhead`, 2 lines), the state and details row and the buttons row (at most two buttons; a third action goes in a More menu the card supplies; a status line or note wraps onto its own line before the buttons do, and they go under each other, whole, only in a column narrower than both). AppLinkCard, PreviewCard's body parts, MessageActionCard, MessageForwardedBundle and MessageReplies are drawn from them, and a test (`card-styles.test.ts`) fails, naming the file, line and class, on a card or popover component that writes its own radius, width, inset or title size.
|
|
49
|
+
- Why: the same frame had widths from 176 to 576px, insets of 16, 12 and 10px and titles of 17 and 15px; a card's style changed alone and drifted from the others (stdrc, 2026-11-10: change one card and the others follow).
|
|
50
|
+
- Affects: `MessageEmbed` without `size` renders as before.
|
|
51
|
+
- AppLinkCard: its header, body and buttons sit 12px from the edges instead of 16px, and its title is `text-subhead` (15px on a phone, 14px on a desktop) instead of `text-headline` (17/16px), so the card is shorter.
|
|
52
|
+
- PreviewCard: `PreviewCardBody`, `PreviewCardActions` and `PreviewCardFooter` take the same 12px, and a `PreviewCardTitle` in a body the same title and its link styles. This is every card that uses them, the item card (`size="item"`) and a default-size card that puts its sentence in `PreviewCardBody`, such as slock's 300px connect-account card. A profile card (`PreviewCardHeader`, `PreviewCardDescription`) is unchanged.
|
|
53
|
+
- MessageActionCard: at most 400px instead of 440px, and the message's width on a phone; its properties stack at the narrower width sooner. `MessageActionCardActionsNote` is now a line of its own above the buttons, not beside them.
|
|
54
|
+
- MessageForwardedBundle: at most 560px instead of 544px. MessageReplies: at most 560px instead of 480px; it stays a filled, borderless block, 10px x 8px.
|
|
55
|
+
- Docs: https://raft-design.botiverse.build/patterns/cards-in-messages/
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **Lightbox document mode: the keys scroll the document, never the page under the scrim**: with `initialFocus` unset, a document lightbox with a `LightboxBody` focuses the body when opened by a click or a key, so Page Down, the arrow keys and Space scroll the file at once; before, focus landed on the first toolbar control (Close), and those keys scrolled the page under the scrim while the file stayed put. A tap still focuses the lightbox itself, and a lightbox without a body still focuses its first control. A key that would scroll the page from elsewhere in the lightbox (a toolbar button, the lightbox itself after a tap) scrolls `LightboxBody` instead, by the browser's own steps, and does nothing with a body of the caller's own; keys a control uses (a text field's, a button's Space, a menu's arrows) are left to it (Kirby's review of #1142).
|
|
60
|
+
- Affects: document-mode lightboxes with a `LightboxBody` (none in slock yet). An `initialFocus` of the caller's still wins.
|
|
61
|
+
- Docs: https://raft-design.botiverse.build/components/lightbox/#accessibility
|
|
62
|
+
- **Lightbox document mode focuses as Base UI does when opened by a tap**: with `initialFocus` unset, a document lightbox opened by a tap focuses itself, not its first toolbar control, so a phone shows no ring and opens no keyboard; a click or a key still focuses the first control (Kirby).
|
|
63
|
+
- **The drag hint is drawn with the lifted copy, at `999`**: it sat at `z-70` with the lightbox, where the DOM order decided which was on top; elevation.md's Layer order lists it (Kirby).
|
|
64
|
+
- **TaskBoard:** `TaskBoardColumnChevron` draws a `ChevronDown` when given no children, so a column heading always shows whether the column is open. slock passed it empty, so no column had the arrow and a collapsed Closed column read as "Closed 2" over nothing (Kirby, Noel).
|
|
65
|
+
|
|
66
|
+
- **What opens inside a lightbox is drawn above it, and a lightbox opened from a dialog has its scrim**: `LightboxPortal` is a stacking context of its own at `z-70` (`relative isolate z-70`), with the scrim and the content inside it in document order. Base UI already put a Tooltip, Popover, menu and its submenus, Select, Combobox, PreviewCard, Dialog, AlertDialog or Drawer opened inside `LightboxContent` in the lightbox's portal; now its own `z-50` counts inside the lightbox, above the content. The lightbox's scrim is always rendered (`forceRender`), and the lightbox joins the stack of modal surfaces: opened from a dialog it draws `layer-backdrop-nested` (`data-nested`), and a Dialog, AlertDialog or Drawer opened inside it draws the nested scrim over it.
|
|
67
|
+
- Why: the scrim and the content sat at `z-70` and `z-71` among the page's layers, so a popup opened inside, at `z-50` in the lightbox's portal, was drawn under the scrim, and a press on it landed on the scrim and closed the lightbox. And Base UI renders no backdrop for a dialog opened under another, so a lightbox opened from a dialog had no scrim: the dialog showed through undimmed, and a press beside the file went to the dialog (#proj-raft-ui:#226, Kirby's review of #1130).
|
|
68
|
+
- Affects: every lightbox. The DOM is the same; the portal element gains `relative isolate z-70`, the scrim loses `z-70` and the content `z-71`, and a scrim stacked on another surface carries `data-nested`. A popup given a `container` outside the lightbox, or an app's own portal to the body, is drawn under the lightbox, as before. `LightboxPortalProps` takes a string `className`, as `LightboxContentProps` does.
|
|
69
|
+
- Docs: https://raft-design.botiverse.build/components/lightbox/#popups-inside, https://raft-design.botiverse.build/components/lightbox/#behavior
|
|
70
|
+
- **A cover or an excerpt no longer cuts off a card's corners**: `MessageEmbedContent` that is the card's first or last part takes the card's inner corner, `radius-container` less the 1px border, on the two corners it shares with the card, and clips what it holds to them (`overflow: clip`). A cover at the top is round at the top and square at its foot. The card itself still clips nothing, so a title link's focus ring around the whole card (its `::after`) still shows.
|
|
71
|
+
- Why: an Artifact card's cover or excerpt ran square to the card's edges and covered the border's arc, so its top corners looked cut off, in light and dark (cindyz, #triage:6be3db1b). The docs' link card had the same fault.
|
|
72
|
+
- Affects: every card on MessageEmbed whose first or last part is `MessageEmbedContent`: slock's Artifact card, the docs' link card, and MessageForwardedBundle's items when the bundle has no footer (they now clip at the card's bottom corners). Content between two rows is unchanged, and so is MessageQuotedPreview: its frame is a square bar (`data-frame="bar"` on its root and on `MessageQuotedPreviewLoading`), so its content stays square and clips nothing.
|
|
73
|
+
- Docs: https://raft-design.botiverse.build/components/message-embed/#anatomy
|
|
74
|
+
- **FitToolbar measures only what is laid out in its rows**: it leaves out what is hidden, out of flow (`position: absolute` or `fixed`) or of no size, and looks through `display: contents` on its lines too. A line has broken only where an item sits wholly below or above the first on it, so items of different heights on one line (a primary Button beside 32px filters) no longer read as a broken line.
|
|
75
|
+
- Why: a popover or menu opened from a toolbar item puts Base UI focus guards beside its trigger, fixed at the window's corner, and the toolbar counted them: opening it made labels give way (Jasper). And a wrapping toolbar whose first line mixed heights wrapped or gave way where it fit.
|
|
76
|
+
- Affects: every FitToolbar and SelectionToolbar. Opening a popup from one no longer changes it; a wrapping toolbar with items of different heights on a line gives way less, only where it doesn't fit.
|
|
77
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/#behavior
|
|
78
|
+
- **DialogFooter and AlertDialogFooter decide whether to stack from the buttons laid out in their row only**: they leave out what is hidden, out of flow (`position: absolute` or `fixed`) or of no size, and look through `display: contents`, as FitToolbar does (one shared measure). There is no new prop.
|
|
79
|
+
- Why: an open Menu or Popover puts Base UI focus guards beside its trigger, and a Select or Combobox always adds a hidden input, each fixed at the window's corner. The footer counted them as the row's start, so the row reached to the window's left edge: a menu opened from the footer stacked the buttons at any width, and a Select in the footer stacked them for good, nothing open (Jasper's audit, after the Tasks filters lost their labels whenever a filter was open: stdrc, 2026-11-10, #wg-task-v2:8a7ae237).
|
|
80
|
+
- Affects: a footer holding a Menu or Popover trigger, a Select or a Combobox now stacks only when its buttons don't fit. A button inside a `display: contents` wrapper now counts toward the row; before, the wrapper read as no size and its button was left out. Every other footer is unchanged.
|
|
81
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#the-footer, https://raft-design.botiverse.build/patterns/responsive/#switch-on-the-regions-width
|
|
82
|
+
- **BannerAction comes back beside the text with a menu open from it, or a Select in it**: once the actions had taken a line of their own, they measured their width with a Range over all their contents. They now measure from their first laid-out item's left to the last one's right, with the same shared measure. There is no new prop.
|
|
83
|
+
- Why: the Range took in an open menu's focus guards and a Select's hidden input, fixed at the window's corner, so the actions seemed as wide as the distance to the window's left edge: they stayed under the text while a menu was open, and for good with a Select in them (Jasper's audit, stdrc, 2026-11-10, #wg-task-v2:8a7ae237).
|
|
84
|
+
- Affects: a banner whose actions hold a Menu or Popover trigger, a Select or a Combobox. Actions of plain buttons measure the same width as before.
|
|
85
|
+
- Docs: https://raft-design.botiverse.build/components/banner/#anatomy, https://raft-design.botiverse.build/patterns/responsive/#switch-on-the-regions-width
|
|
86
|
+
- **A sorted board column shows that it is the place while a card is over it**: `TaskDropZone` takes `dragInto`. With it, the zone carries `data-drag-into` and takes `fill-subtle` while a card it allows is over it, its cards and all; never at rest, and never while it refuses the card (`dropState` `blocked`).
|
|
87
|
+
- Why: Drag and drop says a column whose order isn't the person's to set (sorted by last update, by due date) is itself the place and takes `fill-subtle` while the item is over it, with no placeholder between its items. TaskDropZone had no way to: a sorted column with cards showed nothing while a card was over it, only an empty one its placeholder (review of slock #13852; the channel Tasks panel shares the column).
|
|
88
|
+
- Affects: nothing until a `TaskDropZone` passes `dragInto`: without it, every zone renders the same DOM and classes as before, and a column whose cards are drop targets keeps its placeholder as the one signal.
|
|
89
|
+
- Docs: https://raft-design.botiverse.build/components/task-dnd/#states, https://raft-design.botiverse.build/components/task-board/#states, https://raft-design.botiverse.build/patterns/drag-and-drop/#where-it-will-land
|
|
90
|
+
- **FitToolbar's focus follows the reading order, and a SegmentedControl in it is one toolbar item**: the arrow keys go through its controls line by line as they show, and Tab first enters at the first control on screen. A SegmentedControl inside registers as one item of the toolbar, so the toolbar stays one Tab stop and its arrow keys reach the segments without choosing one (Space chooses; Up and Down choose nothing). When the focused item moves into More, focus goes to More instead of the page. Items of the same kind in one group (the same `label` or `plus` step at the same number) change form at the same time. A SegmentedControl of text alone, or a FitToolbarButton with no icon, refuses its `label` step. There is no new prop.
|
|
91
|
+
- Why: a wrapped group moves with CSS `order`, later on screen than in the DOM, and the browser's Tab order and Base UI's toolbar followed the DOM: focus jumped to the second line and back (WCAG 2.4.3). The SegmentedControl was a second Tab stop whose arrow keys chose as they moved, and Mine and All, with no icons, emptied to nothing at their `label` step; at 780px three filters became icons while Assignee kept its label (Jasper and sindresorhus, 2026-11-10).
|
|
92
|
+
- Affects: every FitToolbar and SelectionToolbar: arrow-key order is the screen's, which is the DOM's unless a group has wrapped. A SegmentedControl in a FitToolbar is no longer a Tab stop of its own. Outside a FitToolbar nothing changes.
|
|
93
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/#accessibility, https://raft-design.botiverse.build/components/segmented-control/#accessibility
|
|
94
|
+
- **A FitToolbar's scrolling group scrolls only as its last state, and stays where the person scrolled it**: while it can still wrap, it counts as not fitting whenever its items overflow, so its labels give way and it wraps before it scrolls. Each measure keeps its scroll position, only the item that just became active comes into view (on mount the first active one), not every active one with the first winning, and a resize after the person has scrolled leaves it there. There is no new prop.
|
|
95
|
+
- Why: at 780px the filters scrolled on the first line, the last ones hidden past the fade, where giving their labels fit them all; and a count changing or a measure pulled the group back to the active filter under the person's finger (risk 3 of sindresorhus' review, with Jasper, 2026-11-10).
|
|
96
|
+
- Affects: a FitToolbar with `wrap` and an `overflow="scroll"` group that can wrap now gives its labels and wraps sooner. Every scrolling group keeps its scroll position across a measure.
|
|
97
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/#behavior
|
|
98
|
+
|
|
99
|
+
### Design System
|
|
100
|
+
|
|
101
|
+
- **A setting that applies at once shows no "Saved" and reserves no line** (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e): the control showing its new value is the confirmation; only a failure appears under it, saying what failed, while the control goes back to its old value. Where a section's settings are stored ("Saved on this device") is said once for the section, not on every row. This replaces the way chosen on 2026-10-30 (Kirby's option A, which stdrc agreed to, #proj-raft-ui:401f3241: "Saved" for a moment, on a line kept when empty) and the settings row's centering on its title and description above that line (2026-11-09).
|
|
102
|
+
- Docs: https://raft-design.botiverse.build/patterns/forms/#settings-that-apply-at-once, https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
|
|
103
|
+
- **A section of settings may hold more than one group, split by topic** (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e): in Appearance, the message font size and its preview are one `ListRowGroup` and the display switches another, 12px apart (`stack-y-3`) under the section's one header, with no hairline joining them. `ListRowGroup` is unchanged: it sets no space around itself, and the column of groups sets it.
|
|
104
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/#more-than-one-group-in-a-section, https://raft-design.botiverse.build/patterns/forms/#a-settings-row
|
|
105
|
+
|
|
106
|
+
## [0.83.0](https://github.com/botiverse/raft-design/compare/v0.82.0...v0.83.0) (2026-11-10)
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- **`placement="top"` on DialogContent**, on a desktop and a tablet (and on DialogSheetContent there): the dialog stays centered across, its top edge at `var(--dialog-top, 8vh)`, and it is never taller than the window less that top and 16px, so it never runs past the window's bottom. Its width still comes from `size`. The content carries `data-placement="top"`. To anchor the card to something, the app measures it and sets `--dialog-top` in the content's `style`; raft-ui measures nothing. Below 768px it does nothing.
|
|
111
|
+
- Why: search opens from the top bar's field, and its panel's top edge belongs on the field, which a centered dialog can't do (slock drew it on DialogSurface with its own positioning, #triage:#278).
|
|
112
|
+
- Affects: nothing until a dialog passes `placement="top"`: `"center"`, the default, renders the same DOM and classes as before.
|
|
113
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#at-the-top
|
|
114
|
+
- **On a phone every centered dialog moves up while the on-screen keyboard is open**, whatever its `placement`: its top edge goes where the phone's top bar ends (`--header-height` below the top of what is visible, under the status bar), it ends at least 16px above the keyboard, and it goes back to the middle when the keyboard closes. While lifted the content carries `data-soft-keyboard`. A phone sheet doesn't move; it keeps its own keyboard box.
|
|
115
|
+
- Why: a centered dialog covered by the keyboard is always wrong, its field or its buttons out of sight, so this is platform behavior and no app opts in (stdrc, 2026-10-21: centered unless the keyboard is open). slock did it with its own `narrow="top"`; that maps to the default now.
|
|
116
|
+
- Affects: every centered `DialogContent` below 768px, only while a text field in it has focus and the visible area is over 100px shorter than the window; at rest nothing changes. A DialogSheetContent's keyboard box now reads the keyboard through the same check, once a frame: a focused checkbox, radio or button no longer counts as typing. An app that lifted its dialogs itself (slock's `narrow="top"`) can drop that code.
|
|
117
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#the-keyboard
|
|
118
|
+
- **SelectionToolbar** (from `@botiverse/raft-ui/wip`): the floating toolbar for selected items (messages, tasks), in the BatchBar style. `SelectionToolbarDock` floats it 12px above the bottom (plus the safe area) and 20px from the sides; `SelectionToolbarCount` (with the number in `SelectionToolbarCountValue`, Space Mono), `SelectionToolbarHint`, `SelectionToolbarActions` with `SelectionToolbarAction` (`secondary`, `menu`, `moreItem`), `SelectionToolbarMore` with `SelectionToolbarMoreItems` in its menu, `SelectionToolbarItem` for another control such as a select-all checkbox, and `SelectionToolbarClose`. A floating panel's frame (`line-seam` ring, `shadow-float`) as tall as a head: 44px with `md` buttons 4px apart with a mouse, 56px with 44px bar buttons side by side showing their icons alone by touch. It is a Base UI Toolbar (one Tab stop, arrow keys inside) and never wraps: when its row doesn't fit the width it floats in, measured, the hint, the secondary actions, the labels (the first action's last) and then the other actions give way, the last first; the count and × stay.
|
|
119
|
+
- Why: the toolbar for selected items is one bar shared by message multi-select and task bulk actions, in Noel's BatchBar style: `md` ghost buttons with a 14px icon and a label, no primary (stdrc, 2026-11-10, #wg-task-v2); by touch it aligns with the head, so a phone fits the tasks' Status, Assignee and Nudge at 360px (Noel, 2026-11-10).
|
|
120
|
+
- Affects: nothing yet. The stable `MessageMultiSelectToolbar` and `MessageMultiSelectToolbarActions` keep their earlier bar until SelectionToolbar leaves wip; move to it from `@botiverse/raft-ui/wip` when you are ready.
|
|
121
|
+
- Docs: https://raft-design.botiverse.build/components/selection-toolbar/, https://raft-design.botiverse.build/components/#toolbar-for-selected-items
|
|
122
|
+
- **FitToolbar** (from `@botiverse/raft-ui/wip`): a Base UI Toolbar whose items give way in named steps as the region it sits in narrows: `label` (to an icon), `more` (into More), `collapse` (a group to one button) and `wrap` (a group to a line of its own, with `wrap`; groups with the same `wrapLine` share one, and a wrapped group gets back what its items gave before it wrapped). Items declare their steps and order with `useFitItem`, or are a `FitToolbarButton`; a `FitToolbarGroup overflow="scroll"` scrolls sideways instead, its `data-active` items kept in view; groups are `FitToolbarGroup` (`firstLine`, `wrapOrder`, `wrapLine`, `collapsed`, `collapseOrder`); More takes `useFitMore` and lists `FitToolbarMoreItems`. It measures its own region's width, never the window's, again when its content changes on its own, and writes each step as `data-fit-<step>`. A SegmentedControl inside it follows its `label` step; its own `iconsOnlyBelow` is off there.
|
|
123
|
+
- Why: one measured engine for every toolbar that has to give way (responsive.md "measure the content rather than guess a width"), used by SelectionToolbar now and meant for the list toolbars of #proj-raft-ui:#225, instead of a breakpoint guess or a hook per app. Its step names and API stay in wip until sindresorhus confirms them for those toolbars.
|
|
124
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- **DialogSheetContent, DialogCommit and DialogSheetBack are stable**: import them, and their props types, from `@botiverse/raft-ui`. `@botiverse/raft-ui/wip` no longer exports them.
|
|
129
|
+
- Why: slock is moving its own dialogs, its phone sheets among them, onto raft-ui (#triage:#278), and a product builds on the stable entry.
|
|
130
|
+
- Affects: an import from `@botiverse/raft-ui/wip` fails; import from `@botiverse/raft-ui` instead. No app imported them from `/wip` (slock draws its sheets itself today), and the docs' examples have moved.
|
|
131
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#making-a-dialog-a-sheet
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- **A label/value list puts its labels above when a chip or a long word of a wrapping value can't fit beside its label**: DescriptionList `direction="horizontal"` and MessageActionCard's properties now also stack when one chip of a value that wraps either way (a row of chips wider than the list) is wider than the value column but no wider than the list, and likewise when a long word in such a value (an ID, a commit SHA, a URL) is: beside the label the chip was cut and the word ran past the value column. A word set to `overflow-wrap: anywhere` breaks where it must, and doesn't decide. There is no new prop.
|
|
136
|
+
- Why: the one-line rule let a row of chips wider than the list stay beside its labels, where one long chip was cut off with no way to read it (mobile #4507, the Computer page's Detected Runtimes chip).
|
|
137
|
+
- Affects: lists whose value wraps either way and holds a chip or a long word wider than the value column now stack instead of staying beside their labels (slock's Computer page among them). Such a word used to run past the value column in DescriptionList and break mid-word in MessageActionCard properties; it now reads whole on its own line. Every other list lays out as before; the extra measurement runs only for values wider than the list.
|
|
138
|
+
- Docs: https://raft-design.botiverse.build/components/description-list/#labels-go-above-only-when-a-value-doesnt-fit
|
|
139
|
+
- **The server tab's popover keeps one whole server row and opens on the current server**: `MobileNavServerPopoverList` is never shorter than one two-line row. Where the footer actions and the pages would leave it less inside 60% of the screen, `MobileNavServerPopoverContent` grows past 60% as far as that row needs; where the screen is too short even for that, the whole popover scrolls, the footer and the pages with it, in a ScrollArea. On open the list scrolls, with no animation, to the row with `aria-current="true"`, and focus goes to that row (a touch still focuses the popover itself), so focus no longer scrolls the list back to the top.
|
|
140
|
+
- Why: with 5 pages in the grid, a 360dp phone squeezed the list until the current server was half hidden, and the list always opened at the top (Lagoon, mobile #4500; Jasper's review of raft-design #1117).
|
|
141
|
+
- Affects: every `MobileNavServerPopoverContent`. A popover that fit before is unchanged except that it opens scrolled to the current server; one whose list was squeezed below a row is now taller than 60%, or scrolls as a whole. Opening from a mouse or keyboard focuses the current server's row instead of the first row; pass `initialFocus` to choose otherwise. The popover's parts now sit in a ScrollArea (`data-slot="mobile-nav-server-popover-scroll"`) inside the popup, so a selector for a direct child of `mobile-nav-server-popover-content` no longer matches them. Mark the current server's row with `aria-current="true"` for the scroll and the focus to find it.
|
|
142
|
+
- Docs: https://raft-design.botiverse.build/components/mobile-nav/#the-server-tab
|
|
143
|
+
- **A settings row's switch centers on its text, not on the SaveStatus line under it**: in a `ListRow variant="plain"` whose `ListRowBody` ends with a `SaveStatus`, both ends (the switch, value, chevron, checkbox, radio or button, and a leading icon, avatar or tile) center on the title and description. The row moves the line's reserved space (4px and one footnote line) into its bottom padding, so its height is unchanged and nothing moves when "Saved" or a one-line error appears. There is no new prop.
|
|
144
|
+
- Why: the ends centered on the whole body, the empty reserved line included, so the switch sat visibly below the text (slock's Administration › Hide #all channel; Jasper and Kirby, 2026-11-09, #proj-raft-ui:#220).
|
|
145
|
+
- Affects: in such rows the ends move up by half the reserved line (11px). A row without a `SaveStatus` is unchanged. An error that wraps still grows the row, and the ends move down by half of each extra line. Beside a title alone on a phone, the 30px switch keeps 12px above it, so that row is 76px instead of 70px. A local wrapper that put an invisible line under the switch to compensate (slock's) now lifts it too far: remove it.
|
|
146
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/#anatomy, https://raft-design.botiverse.build/components/save-status/#behavior
|
|
147
|
+
|
|
1
148
|
## [0.82.0](https://github.com/botiverse/raft-design/compare/v0.81.0...v0.82.0) (2026-11-09)
|
|
2
149
|
|
|
3
150
|
### Changed
|
|
@@ -1,35 +1,16 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
|
-
import { messageEmbed } from "../message-embed/message-embed.recipe.mjs";
|
|
2
|
+
import { messageCard, messageEmbed } from "../message-embed/message-embed.recipe.mjs";
|
|
3
3
|
import { Skeleton } from "../skeleton/skeleton.mjs";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/app-link-card/app-link-card.tsx
|
|
6
6
|
const appLinkCard = tv({ slots: {
|
|
7
|
-
root: ["flex
|
|
8
|
-
header: [
|
|
9
|
-
"flex min-w-0 items-center gap-1.5",
|
|
10
|
-
"border-b border-line-hairline px-4 py-2",
|
|
11
|
-
"font-sans text-caption text-foreground-muted"
|
|
12
|
-
],
|
|
13
|
-
icon: ["inline-flex size-4 shrink-0 items-center justify-center", "*:size-full *:shrink-0 [&>img]:rounded-[2px] [&>img]:object-cover"],
|
|
14
|
-
name: ["min-w-0 truncate", "font-semibold text-foreground"],
|
|
15
|
-
host: ["flex min-w-0 items-baseline", "before:shrink-0 before:pe-1.5 before:content-['·']"],
|
|
16
|
-
hostText: ["min-w-0 truncate"],
|
|
17
|
-
body: ["flex min-w-0 flex-col gap-1.5", "px-4 py-3 last:pb-4"],
|
|
18
|
-
eyebrow: ["m-0 truncate", "font-mono text-mono-meta text-foreground-muted"],
|
|
19
|
-
title: [
|
|
20
|
-
"m-0 line-clamp-2",
|
|
21
|
-
"font-sans text-headline font-bold text-foreground",
|
|
22
|
-
"[&_a]:rounded-[2px] [&_a]:decoration-[1.5px] [&_a]:underline-offset-[3px] [&_a]:hover:underline",
|
|
23
|
-
"[&_a]:outline-none [&_a]:focus-visible:shadow-focus"
|
|
24
|
-
],
|
|
25
|
-
meta: ["flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", "m-0 font-sans text-callout text-foreground-muted"],
|
|
26
|
-
actions: ["flex flex-wrap items-center justify-end gap-2", "px-4 pb-3 last:pb-4"],
|
|
7
|
+
root: ["flex flex-col", "my-2 first:mt-0 last:mb-0"],
|
|
27
8
|
placeholderLine: ["flex h-lh items-center"],
|
|
28
9
|
placeholderButton: ["h-8 md:pointer-fine:h-control w-28 rounded-control"]
|
|
29
10
|
} });
|
|
30
11
|
function frame(className) {
|
|
31
12
|
const { root } = appLinkCard();
|
|
32
|
-
return messageEmbed().root({ className: root({ className }) });
|
|
13
|
+
return messageEmbed({ size: "item" }).root({ className: root({ className }) });
|
|
33
14
|
}
|
|
34
15
|
/**
|
|
35
16
|
* An app link's card drawn in the message, after the link's paragraph, where
|
|
@@ -53,7 +34,8 @@ function AppLinkCard({ className, ...props }) {
|
|
|
53
34
|
* lines and a button follow, as gray bars.
|
|
54
35
|
*/
|
|
55
36
|
function AppLinkCardPlaceholder({ className, children, ...props }) {
|
|
56
|
-
const { body, eyebrow, title, meta, actions
|
|
37
|
+
const { body, eyebrow, title, meta, actions } = messageCard();
|
|
38
|
+
const { placeholderLine, placeholderButton } = appLinkCard();
|
|
57
39
|
return /* @__PURE__ */ jsxs("div", {
|
|
58
40
|
"data-slot": "app-link-card-placeholder",
|
|
59
41
|
"aria-busy": "true",
|
|
@@ -107,28 +89,28 @@ function AppLinkCardPlaceholder({ className, children, ...props }) {
|
|
|
107
89
|
}
|
|
108
90
|
/** Raft's row at the top: `AppLinkCardIcon`, `AppLinkCardName`, then `AppLinkCardHost`. */
|
|
109
91
|
function AppLinkCardHeader({ className, ...props }) {
|
|
110
|
-
const {
|
|
92
|
+
const { source } = messageCard();
|
|
111
93
|
return /* @__PURE__ */ jsx("div", {
|
|
112
94
|
"data-slot": "app-link-card-header",
|
|
113
|
-
className:
|
|
95
|
+
className: source({ className }),
|
|
114
96
|
...props
|
|
115
97
|
});
|
|
116
98
|
}
|
|
117
99
|
/** The app's registered icon, the image Raft stores and serves: one child at 16px. */
|
|
118
100
|
function AppLinkCardIcon({ className, ...props }) {
|
|
119
|
-
const {
|
|
101
|
+
const { sourceIcon } = messageCard();
|
|
120
102
|
return /* @__PURE__ */ jsx("span", {
|
|
121
103
|
"data-slot": "app-link-card-icon",
|
|
122
|
-
className:
|
|
104
|
+
className: sourceIcon({ className }),
|
|
123
105
|
...props
|
|
124
106
|
});
|
|
125
107
|
}
|
|
126
108
|
/** The app's name as Raft stores it. One line, truncated. */
|
|
127
109
|
function AppLinkCardName({ className, ...props }) {
|
|
128
|
-
const {
|
|
110
|
+
const { sourceName } = messageCard();
|
|
129
111
|
return /* @__PURE__ */ jsx("span", {
|
|
130
112
|
"data-slot": "app-link-card-name",
|
|
131
|
-
className:
|
|
113
|
+
className: sourceName({ className }),
|
|
132
114
|
...props
|
|
133
115
|
});
|
|
134
116
|
}
|
|
@@ -137,14 +119,14 @@ function AppLinkCardName({ className, ...props }) {
|
|
|
137
119
|
* the end, which says where the link goes, stays.
|
|
138
120
|
*/
|
|
139
121
|
function AppLinkCardHost({ className, children, ...props }) {
|
|
140
|
-
const {
|
|
122
|
+
const { sourceHost, sourceHostText } = messageCard();
|
|
141
123
|
return /* @__PURE__ */ jsx("span", {
|
|
142
124
|
"data-slot": "app-link-card-host",
|
|
143
|
-
className:
|
|
125
|
+
className: sourceHost({ className }),
|
|
144
126
|
...props,
|
|
145
127
|
children: /* @__PURE__ */ jsx("span", {
|
|
146
128
|
dir: "rtl",
|
|
147
|
-
className:
|
|
129
|
+
className: sourceHostText(),
|
|
148
130
|
children: /* @__PURE__ */ jsx("span", {
|
|
149
131
|
dir: "ltr",
|
|
150
132
|
children
|
|
@@ -154,7 +136,7 @@ function AppLinkCardHost({ className, children, ...props }) {
|
|
|
154
136
|
}
|
|
155
137
|
/** The item: `AppLinkCardEyebrow`, `AppLinkCardTitle`, `AppLinkCardMeta`, in that order. */
|
|
156
138
|
function AppLinkCardBody({ className, ...props }) {
|
|
157
|
-
const { body } =
|
|
139
|
+
const { body } = messageCard();
|
|
158
140
|
return /* @__PURE__ */ jsx("div", {
|
|
159
141
|
"data-slot": "app-link-card-body",
|
|
160
142
|
className: body({ className }),
|
|
@@ -163,7 +145,7 @@ function AppLinkCardBody({ className, ...props }) {
|
|
|
163
145
|
}
|
|
164
146
|
/** The app's id for the item, above the title. One line, truncated. */
|
|
165
147
|
function AppLinkCardEyebrow({ className, ...props }) {
|
|
166
|
-
const { eyebrow } =
|
|
148
|
+
const { eyebrow } = messageCard();
|
|
167
149
|
return /* @__PURE__ */ jsx("p", {
|
|
168
150
|
"data-slot": "app-link-card-eyebrow",
|
|
169
151
|
className: eyebrow({ className }),
|
|
@@ -175,7 +157,7 @@ function AppLinkCardEyebrow({ className, ...props }) {
|
|
|
175
157
|
* to the item in it: the title opens the item, in the title's ink.
|
|
176
158
|
*/
|
|
177
159
|
function AppLinkCardTitle({ className, ...props }) {
|
|
178
|
-
const { title } =
|
|
160
|
+
const { title } = messageCard();
|
|
179
161
|
return /* @__PURE__ */ jsx("p", {
|
|
180
162
|
"data-slot": "app-link-card-title",
|
|
181
163
|
className: title({ className }),
|
|
@@ -184,7 +166,7 @@ function AppLinkCardTitle({ className, ...props }) {
|
|
|
184
166
|
}
|
|
185
167
|
/** One state in a neutral Badge, then muted details. Wraps when it runs out of room. */
|
|
186
168
|
function AppLinkCardMeta({ className, ...props }) {
|
|
187
|
-
const { meta } =
|
|
169
|
+
const { meta } = messageCard();
|
|
188
170
|
return /* @__PURE__ */ jsx("div", {
|
|
189
171
|
"data-slot": "app-link-card-meta",
|
|
190
172
|
className: meta({ className }),
|
|
@@ -196,7 +178,7 @@ function AppLinkCardMeta({ className, ...props }) {
|
|
|
196
178
|
* last, with a More menu (⋯) to their left for the rest.
|
|
197
179
|
*/
|
|
198
180
|
function AppLinkCardActions({ className, ...props }) {
|
|
199
|
-
const { actions } =
|
|
181
|
+
const { actions } = messageCard();
|
|
200
182
|
return /* @__PURE__ */ jsx("div", {
|
|
201
183
|
"data-slot": "app-link-card-actions",
|
|
202
184
|
className: actions({ className }),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { laidOutChildren } from "../../lib/laid-out-children.mjs";
|
|
1
2
|
import { tv } from "../../lib/tv.mjs";
|
|
2
3
|
import { Button } from "../button/button.mjs";
|
|
3
4
|
import { X } from "lucide-react";
|
|
@@ -67,7 +68,7 @@ const banner = tv({
|
|
|
67
68
|
defaultVariants: { size: "md" },
|
|
68
69
|
compoundVariants: [{
|
|
69
70
|
size: "sm",
|
|
70
|
-
class: { root: ["[--banner-button:--spacing(8)] md:pointer-fine:[--banner-button:var(--control-height)]", "has-[>[data-slot=banner-action],>[data-banner-close]]:py-[calc((var(--banner-button)-1lh)/2
|
|
71
|
+
class: { root: ["[--banner-button:--spacing(8)] md:pointer-fine:[--banner-button:var(--control-height)]", "has-[>[data-slot=banner-action],>[data-banner-close]]:py-[calc((var(--banner-button)-1lh)/2+(--spacing(1)))]"] }
|
|
71
72
|
}]
|
|
72
73
|
});
|
|
73
74
|
/**
|
|
@@ -126,6 +127,18 @@ function contentWidth(element) {
|
|
|
126
127
|
range.selectNodeContents(element);
|
|
127
128
|
return range.getBoundingClientRect().width;
|
|
128
129
|
}
|
|
130
|
+
function actionsWidth(action) {
|
|
131
|
+
const items = laidOutChildren(action);
|
|
132
|
+
if (items.length === 0) return contentWidth(action);
|
|
133
|
+
let start = Number.POSITIVE_INFINITY;
|
|
134
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
135
|
+
for (const item of items) {
|
|
136
|
+
const box = item.getBoundingClientRect();
|
|
137
|
+
start = Math.min(start, box.left);
|
|
138
|
+
end = Math.max(end, box.right);
|
|
139
|
+
}
|
|
140
|
+
return end - start;
|
|
141
|
+
}
|
|
129
142
|
function useWrapsBesideText(action) {
|
|
130
143
|
const [wraps, setWraps] = useState(false);
|
|
131
144
|
useLayoutEffect(() => {
|
|
@@ -148,7 +161,7 @@ function useWrapsBesideText(action) {
|
|
|
148
161
|
}
|
|
149
162
|
if (lines > 1) return;
|
|
150
163
|
const gap = Number.parseFloat(getComputedStyle(root).columnGap) || 0;
|
|
151
|
-
if (contentWidth(text) +
|
|
164
|
+
if (contentWidth(text) + actionsWidth(action) + gap + 1 <= text.getBoundingClientRect().width) setWraps(false);
|
|
152
165
|
};
|
|
153
166
|
let frame = 0;
|
|
154
167
|
const schedule = () => {
|
|
@@ -12,6 +12,11 @@ const DialogSheetContext = createContext(NO_SHEET);
|
|
|
12
12
|
function useDialogSheet() {
|
|
13
13
|
return useContext(DialogSheetContext);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* True around the DialogContent a `DialogSheetContent` draws as a phone
|
|
17
|
+
* sheet: the sheet has its own keyboard box, so the content doesn't lift.
|
|
18
|
+
*/
|
|
19
|
+
const DialogSheetSurfaceContext = createContext(false);
|
|
15
20
|
/** Inside the sheet's head: its × is a pushed page's leading action. */
|
|
16
21
|
const DialogSheetHeadContext = createContext(false);
|
|
17
22
|
function useInDialogSheetHead() {
|
|
@@ -23,7 +28,7 @@ function useInDialogSheetHead() {
|
|
|
23
28
|
* button instead of taking the sheet's.
|
|
24
29
|
*/
|
|
25
30
|
function DialogSheetReset({ children }) {
|
|
26
|
-
return createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, children));
|
|
31
|
+
return createElement(DialogSheetSurfaceContext.Provider, { value: false }, createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, children)));
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* A dialog drawn as a phone sheet (design: dialog.md "On a phone"). The
|
|
@@ -55,4 +60,4 @@ const dialogSheet = tv({ slots: {
|
|
|
55
60
|
bodyEnd: ["mt-4 empty:hidden", "text-start font-sans text-footnote text-foreground-muted *:data-[slot=text-link]:text-footnote"]
|
|
56
61
|
} });
|
|
57
62
|
//#endregion
|
|
58
|
-
export { DialogSheetContext, DialogSheetHeadContext, DialogSheetReset, NO_SHEET, dialogSheet, useDialogSheet, useInDialogSheetHead };
|
|
63
|
+
export { DialogSheetContext, DialogSheetHeadContext, DialogSheetReset, DialogSheetSurfaceContext, NO_SHEET, dialogSheet, useDialogSheet, useInDialogSheetHead };
|
|
@@ -10,9 +10,10 @@ export type DialogSheetContentProps = DialogContentProps;
|
|
|
10
10
|
* up over its scrim; `DialogHeader` becomes a pushed page's head (× at the
|
|
11
11
|
* start, the title, the `DialogCommit` button at the end), `DialogFooter` is
|
|
12
12
|
* hidden (× is Cancel) and `DialogBody` scrolls on the phone's 16px margin
|
|
13
|
-
* above the home indicator. Base UI keeps Esc, focus and close.
|
|
13
|
+
* above the home indicator. Base UI keeps Esc, focus and close. `placement`
|
|
14
|
+
* applies on a desktop and a tablet only: the sheet is docked to the bottom.
|
|
14
15
|
*/
|
|
15
|
-
export declare function DialogSheetContent({ className, overlay, style, ref, children, ...props }: DialogSheetContentProps): import("react").JSX.Element;
|
|
16
|
+
export declare function DialogSheetContent({ className, overlay, placement, style, ref, children, ...props }: DialogSheetContentProps): import("react").JSX.Element;
|
|
16
17
|
export type DialogCommitProps = ButtonProps;
|
|
17
18
|
/**
|
|
18
19
|
* The dialog's one commit button: a `Button`, written where it goes on a
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cn } from "../../cn.mjs";
|
|
2
2
|
import { Button } from "../button/button.mjs";
|
|
3
|
-
import { DialogSheetContext, dialogSheet, useDialogSheet } from "./dialog-sheet-context.mjs";
|
|
4
|
-
import { DialogContent } from "./dialog.mjs";
|
|
3
|
+
import { DialogSheetContext, DialogSheetSurfaceContext, dialogSheet, useDialogSheet } from "./dialog-sheet-context.mjs";
|
|
5
4
|
import { useMobileViewport } from "../../lib/use-mobile-viewport.mjs";
|
|
5
|
+
import { useSoftKeyboard } from "../../lib/use-soft-keyboard.mjs";
|
|
6
|
+
import { DialogContent } from "./dialog.mjs";
|
|
6
7
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import { createPortal } from "react-dom";
|
|
@@ -14,9 +15,10 @@ import { createPortal } from "react-dom";
|
|
|
14
15
|
* up over its scrim; `DialogHeader` becomes a pushed page's head (× at the
|
|
15
16
|
* start, the title, the `DialogCommit` button at the end), `DialogFooter` is
|
|
16
17
|
* hidden (× is Cancel) and `DialogBody` scrolls on the phone's 16px margin
|
|
17
|
-
* above the home indicator. Base UI keeps Esc, focus and close.
|
|
18
|
+
* above the home indicator. Base UI keeps Esc, focus and close. `placement`
|
|
19
|
+
* applies on a desktop and a tablet only: the sheet is docked to the bottom.
|
|
18
20
|
*/
|
|
19
|
-
function DialogSheetContent({ className, overlay, style, ref, children, ...props }) {
|
|
21
|
+
function DialogSheetContent({ className, overlay, placement, style, ref, children, ...props }) {
|
|
20
22
|
const phone = useMobileViewport();
|
|
21
23
|
const [commitSlot, setCommitSlot] = useState(null);
|
|
22
24
|
const [bodyEndSlot, setBodyEndSlot] = useState(null);
|
|
@@ -51,16 +53,20 @@ function DialogSheetContent({ className, overlay, style, ref, children, ...props
|
|
|
51
53
|
...style,
|
|
52
54
|
...keyboardBox
|
|
53
55
|
} : style, [style, keyboardBox]);
|
|
54
|
-
return /* @__PURE__ */ jsx(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
return /* @__PURE__ */ jsx(DialogSheetSurfaceContext.Provider, {
|
|
57
|
+
value: phone,
|
|
58
|
+
children: /* @__PURE__ */ jsx(DialogContent, {
|
|
59
|
+
...props,
|
|
60
|
+
ref: setRef,
|
|
61
|
+
placement: phone ? void 0 : placement,
|
|
62
|
+
overlay: phone ? sheetOverlay : overlay,
|
|
63
|
+
className: phone ? cn(className, sheet.content()) : className,
|
|
64
|
+
style: phone ? sheetStyle : style,
|
|
65
|
+
"data-sheet": phone ? "" : void 0,
|
|
66
|
+
children: /* @__PURE__ */ jsx(DialogSheetContext.Provider, {
|
|
67
|
+
value: state,
|
|
68
|
+
children
|
|
69
|
+
})
|
|
64
70
|
})
|
|
65
71
|
});
|
|
66
72
|
}
|
|
@@ -132,36 +138,16 @@ function DialogCommit({ size, form, onClick, ...props }) {
|
|
|
132
138
|
* keyboard. `undefined` otherwise (the CSS box).
|
|
133
139
|
*/
|
|
134
140
|
function useSheetKeyboardBox(active) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const top = Math.round(Math.max(viewport.offsetTop, 0) + viewport.height - height);
|
|
144
|
-
setBox((previous) => {
|
|
145
|
-
if (!keyboardUp) return void 0;
|
|
146
|
-
return previous?.top === `${top}px` && previous?.height === `${height}px` ? previous : {
|
|
147
|
-
top: `${top}px`,
|
|
148
|
-
bottom: "auto",
|
|
149
|
-
height: `${height}px`
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
viewport.addEventListener("resize", update);
|
|
154
|
-
viewport.addEventListener("scroll", update, { passive: true });
|
|
155
|
-
document.addEventListener("focusin", update);
|
|
156
|
-
document.addEventListener("focusout", update);
|
|
157
|
-
return () => {
|
|
158
|
-
viewport.removeEventListener("resize", update);
|
|
159
|
-
viewport.removeEventListener("scroll", update);
|
|
160
|
-
document.removeEventListener("focusin", update);
|
|
161
|
-
document.removeEventListener("focusout", update);
|
|
141
|
+
const keyboard = useSoftKeyboard(active);
|
|
142
|
+
return useMemo(() => {
|
|
143
|
+
if (!keyboard) return void 0;
|
|
144
|
+
const height = Math.round(Math.min(keyboard.height, keyboard.windowHeight * .9));
|
|
145
|
+
return {
|
|
146
|
+
top: `${keyboard.top + keyboard.height - height}px`,
|
|
147
|
+
bottom: "auto",
|
|
148
|
+
height: `${height}px`
|
|
162
149
|
};
|
|
163
|
-
}, [
|
|
164
|
-
return active ? box : void 0;
|
|
150
|
+
}, [keyboard]);
|
|
165
151
|
}
|
|
166
152
|
/**
|
|
167
153
|
* The commit is at the top of a sheet, so an error a failed commit adds near
|
|
@@ -22,8 +22,19 @@ export type DialogContentProps = StyledProps<Dialog.Popup.Props> & {
|
|
|
22
22
|
overlay?: false | DialogOverlayProps;
|
|
23
23
|
size?: "sm" | "md" | "lg" | "wide";
|
|
24
24
|
layer?: 0 | 1;
|
|
25
|
+
/**
|
|
26
|
+
* Where the card sits in the window. `"center"` (the default): in the
|
|
27
|
+
* middle. `"top"`: centred across, its top edge at `var(--dialog-top, 8vh)`,
|
|
28
|
+
* and never taller than the window less that top and 16px, so it never runs
|
|
29
|
+
* past the window's bottom; its width is still `size`'s. To anchor the card
|
|
30
|
+
* to something on the page (search under the top bar's field), measure it
|
|
31
|
+
* and set `--dialog-top` in `style`. Desktop and tablet only: below 768px
|
|
32
|
+
* every centred dialog sits in the middle and moves up only while the
|
|
33
|
+
* on-screen keyboard is open (dialog.md, "On a phone").
|
|
34
|
+
*/
|
|
35
|
+
placement?: "center" | "top";
|
|
25
36
|
};
|
|
26
|
-
export declare function DialogContent({ className, children, portalProps, overlay, size, layer, ...props }: DialogContentProps): import("react").JSX.Element;
|
|
37
|
+
export declare function DialogContent({ className, children, portalProps, overlay, size, layer, placement, ...props }: DialogContentProps): import("react").JSX.Element;
|
|
27
38
|
export type DialogCloseProps = Dialog.Close.Props & Pick<ButtonProps, "variant" | "size"> & {
|
|
28
39
|
className?: string;
|
|
29
40
|
children?: ReactNode;
|