@botiverse/raft-ui 0.22.1 → 0.23.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/CHANGELOG.md +91 -0
- package/dist/components/steps/steps.css +15 -10
- package/dist/foundation.css +6 -0
- package/dist/{inbox-item-BtvK75T1.mjs → inbox-item-B56I-Ckt.mjs} +143 -11
- package/dist/{inbox-item-ChANmbeB.d.mts → inbox-item-ChGeZBFb.d.mts} +1 -1
- package/dist/index.d.mts +14 -9
- package/dist/index.mjs +316 -106
- package/dist/style.css +6 -6
- package/dist/styles.css +4 -2
- package/dist/tokens.json +13 -0
- package/dist/wip.d.mts +1 -1
- package/dist/wip.mjs +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
+
## [0.23.1](https://github.com/botiverse/raft-design/compare/v0.23.0...v0.23.1) (2026-10-22)
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **MessageItemToolbar**: measures its width onto the row (`--mi-toolbar-width`) as the toolbar shows, on the row's `pointerenter` or `focusin`, instead of when the row mounts, so a message row mounts in one commit again and keeps no ResizeObserver of its own. The fade under the toolbar looks the same. ([#738](https://github.com/botiverse/raft-design/pull/738))
|
|
6
|
+
- Why: 0.23.0 kept the toolbar's element in React state and gave every toolbar its own ResizeObserver, and every message row renders a toolbar, so each row committed twice on mount and a long channel held one observer per message; only the toolbar that shows needs its width, so it is now measured then.
|
|
7
|
+
- Affects: per row on mount, 2 commits → 1 and 1 ResizeObserver → 0; while a toolbar shows (hover, focus inside the row, an open popup), one ResizeObserver shared by the page watches it; it lets go of a toolbar once the pointer and focus have left its row with no popup open (after a popup closed elsewhere, the next time any toolbar shows) and is disconnected when it watches none. On mount the row gets `--mi-toolbar-width: 164px` (five buttons' width), replaced by the measured width as the toolbar shows, so a toolbar that ever shows without a pointer or focus event on its row fades five buttons' width until it measures itself: too much for a smaller toolbar rather than too little. A row without a `MessageItemToolbar` still gets no width and masks nothing.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/message-item/#behavior
|
|
9
|
+
|
|
10
|
+
## [0.23.0](https://github.com/botiverse/raft-design/compare/v0.22.1...v0.23.0) (2026-10-22)
|
|
11
|
+
|
|
12
|
+
### Design System
|
|
13
|
+
|
|
14
|
+
- **Switch on the region's width**: a layout switches on the width of the region it sits in (a panel, a column, a card), not on the window's. A component that switches its own layout makes itself a container, so it lays out the same in a 360px panel on a wide window as on a phone; it then fills its row and needs a parent with a definite width. Only what the window itself sizes reads the window (the page shell, dialogs, sheets, toasts), and mouse or touch is read with `pointer` and `hover`, never with the width. Every row needs a way to give way: a row of controls that still doesn't fit wraps onto a second line, and a value such as an email truncates last. Whether a part's content fits is measured, not guessed from a width. It applies to DescriptionList, MessageActionCard, MessageImageGallery, MessageVideoPreview, MessageForwardedBundleGallery, Steps, the TasksPanel toolbar, ActivityInboxSelectionToolbar, ListRowMeta, SegmentedControl, SidebarSectionControls, SearchEntityResult and CodeBlockPre. ([#721](https://github.com/botiverse/raft-design/pull/721), [#733](https://github.com/botiverse/raft-design/pull/733))
|
|
15
|
+
- Why: the same component sits in a narrow panel on a wide window and fills a phone, so a layout that asked the window laid itself out for a screen it didn't have (a two-column list in a 360px panel, a step row that wrapped where it should stack); every layout that switched on the window was moved to its own region, or to the input where the switch is about touch.
|
|
16
|
+
- Affects: the switch points listed under Breaking Changes and Changed below.
|
|
17
|
+
- Docs: https://raft-design.botiverse.build/patterns/responsive/#switch-on-the-regions-width
|
|
18
|
+
- **Stacked buttons each take a frame**: when buttons stack full width (a dialog footer whose row doesn't fit, a bottom or top sheet's actions), a ghost button there takes the ink border of `default`, with its hover and press; it goes back to frameless when the row fits again. It applies to Button, DialogFooter, AlertDialogFooter and DrawerActions. ([#727](https://github.com/botiverse/raft-design/pull/727))
|
|
19
|
+
- Why: stretched to the full width, a frameless Cancel read as a line of text under the commit button, not as a button; once buttons stack, each needs its own box.
|
|
20
|
+
- Affects: a stacked ghost button: border `transparent` → `line-strong` on `layer-card`, hover and press `overlay-medium`, as `default`; text sizes `xs` to `xl` only, ghost icon buttons stay frameless.
|
|
21
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#the-footer
|
|
22
|
+
- **Text selection: its own token**: selected text (`::selection`) is painted with the new `selection` token and keeps its own text color. In light mode it is `brand-soft`, as before; in dark mode it is `brand` over the page at 22%, the same wash as a commented passage. `brand-soft` stays the menu highlight. It applies to MessageItem text. ([#730](https://github.com/botiverse/raft-design/pull/730))
|
|
23
|
+
- Why: in dark mode `brand-soft` left a selection at 1.23:1 against the canvas, so it was hard to see what was selected; a menu row's highlight and a selection need different values in dark mode, so they no longer share one.
|
|
24
|
+
- Affects: dark `selection` `brand-soft` (`oklch(0.5501 0.0999 91.5 / 0.2)`) → `color-mix(in oklch, var(--brand-400) 22%, transparent)`: 1.23:1 → 1.77:1 against the canvas, the selected text 7.7:1 (APCA Lc 80). Light mode is unchanged (1.14:1 against the canvas, the text 16.5:1, Lc 96).
|
|
25
|
+
- Docs: https://raft-design.botiverse.build/foundations/color/#text-selection
|
|
26
|
+
|
|
27
|
+
### Breaking Changes
|
|
28
|
+
|
|
29
|
+
- **MessageItem**: the header keeps no room for the hover toolbar, and `--mi-toolbar-reserve` is no longer read. At rest the sender line has the full width; while the toolbar shows (hover, focus inside the row, an open menu), the end of a sender line long enough to reach the corner fades out under it. `MessageItemToolbar` measures its own width onto the row as `--mi-toolbar-width`, so a toolbar with more or fewer buttons masks exactly its width. ([#726](https://github.com/botiverse/raft-design/pull/726))
|
|
30
|
+
- Why: keeping room for the toolbar cut the name to a few letters in a narrow column all the time, to protect a corner that is covered only while the pointer is on that message; the line now keeps its full width and nothing reflows on hover.
|
|
31
|
+
- Affects: with a mouse, header end padding `--mi-toolbar-reserve` (default 7rem) → 0. While the toolbar shows, the header is masked from 4px left of the toolbar's left edge to its end, after a 12px fade (`--mi-header-mask`); a row without a `MessageItemToolbar` masks nothing. The time's tooltip, when it fades out under the toolbar, can be pointed at once the pointer leaves the toolbar, and stays in the keyboard order.
|
|
32
|
+
- Migrate: remove `--mi-toolbar-reserve` wherever you set it; it has no effect. Render the toolbar with `MessageItemToolbar` inside the `MessageItem` row, which writes `--mi-toolbar-width`; a hover toolbar of your own gets no fade unless it sets `--mi-toolbar-width` (its width in px) on the row (`[data-message-item]`).
|
|
33
|
+
- Docs: https://raft-design.botiverse.build/components/message-item/#behavior
|
|
34
|
+
- **DescriptionList** `direction="horizontal"`, **MessageActionCardContent**, **MessageImageGallery**, **MessageForwardedBundleGallery**, **Steps** (without `vertical`) and **TasksPanelRoot** are size containers (`container-type: inline-size`) and switch their layout on their own width instead of the window's; the list, the galleries and the step row fill their row (`width: 100%`). ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
35
|
+
- Why: see Switch on the region's width, above.
|
|
36
|
+
- Affects: DescriptionList stacks below 24rem (384px) of its own width (was below a 640px window), the value right under its label (its 32px minimum row height dropped) and 10px between pairs (the 2px row gap plus 8px; was 2px). MessageActionCard properties stack below 24rem of the card's content (was below a 640px window) with the same spacing, the value's 6px padding above and below dropped; a card at its 440px cap keeps two columns. MessageImageGallery: at most `min(360px, 100%)` of its column (was `min(360px, 100vw − 112px)` below a 768px window); rows 8rem (7rem with three images) below 360px of the gallery's own width and 9rem (8rem) at it (was by a 640px window); three images stay two across and one under them. MessageForwardedBundleGallery: a row of three is three across from 360px of the gallery's own width (was from a 768px window) and two across below; its rows grow as above from 360px (was from a 640px window). Steps: below 40rem (640px) of the row's own width each step stacks its text under its marker (was below a 640px window), and from 40rem a row too long for its width wraps. TasksPanelToolbar: New Task and the view switch on one line and the filters on the next below 28rem (448px) of the panel's width (was below a 768px window), one wrapping row from 28rem; toolbar and viewport sides 16px below 28rem, 20px from it (was by a 768px window). The same window can now show a different layout: a narrow panel on a desktop takes the narrow layout, and a wide region on a small window keeps the wide one.
|
|
37
|
+
- Migrate: put each of these in a parent with a definite width: a block, a stretched flex or grid item, or anything with a width set. In a parent that shrinks to fit its content (`inline-block`, `w-fit` or `fit-content`, an absolutely positioned box or a floating layer with no width, a flex item with `flex-basis: auto` and no width) a size container takes no width from what it holds and collapses, its text one word to a line. Keep `TasksPanelToolbar` and `TasksPanelViewport` inside `TasksPanelRoot`, and `MessageActionCardProperties` inside `MessageActionCardContent`: outside them there is no container, so the tasks toolbar keeps its two-line layout with 20px sides and the properties stay in two columns at every width. Remove window breakpoint overrides (`sm:`, `md:`, `max-sm:`) you added to adjust these layouts, and check them in your narrowest panels.
|
|
38
|
+
- Docs: https://raft-design.botiverse.build/patterns/responsive/#switch-on-the-regions-width
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **SegmentedControl** `iconsOnlyBelow="auto"`: the segments show only their icons exactly when their labelled segments don't fit the width the control is given, and their labels again once they fit. It decides before the first frame paints, and again when the room, a label, a count, the selection or a web font changes, always against the labelled layout, so it doesn't flicker at the boundary. `iconsOnlyBelow` also takes any length in `px`, `rem` or `em` (`"18.5rem"`, `"296px"`), measured against the nearest `@container` (the control's parent when there is none), beside the presets `xs` to `2xl`, which stay CSS container queries. With `auto` or a length the track carries `data-icons-only` while it shows only icons. The presets' type is exported as `SegmentedControlIconsOnlyPreset`. ([#733](https://github.com/botiverse/raft-design/pull/733))
|
|
43
|
+
- Why: a width picked for one language, one count or one font was wrong for the others, and the room a control is given changes with what sits beside it; `auto` measures the labels against that room instead.
|
|
44
|
+
- Affects: with `auto`, the control is a shrinkable flex item (`min-w-0`); leave it free to shrink in its row (don't make it `shrink-0`). In development raft-ui warns when a segment has no icon, or when `iconsOnlyBelow` is neither a preset, `auto` nor a `px`, `rem` or `em` length (the segments then keep their labels).
|
|
45
|
+
- Docs: https://raft-design.botiverse.build/components/segmented-control/#behavior
|
|
46
|
+
- **`selection`** token: `--selection` in `foundation.css` and `tokens.json`, and the Tailwind color `selection` (`selection:bg-selection`). ([#730](https://github.com/botiverse/raft-design/pull/730))
|
|
47
|
+
- Why: see Text selection: its own token, above.
|
|
48
|
+
- Affects: light `var(--brand-soft)`; dark `color-mix(in oklch, var(--brand-400) 22%, transparent)`. Paint your own `::selection` with it in place of `brand-soft`.
|
|
49
|
+
- Docs: https://raft-design.botiverse.build/foundations/color/#text-selection
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **SegmentedControl**: the track may shrink to its row. When the row is narrower than the track, its neighbors have given way and the segments can't show only their icons (or no `iconsOnlyBelow` is set), the segments scroll sideways inside the track, the checked one kept in view, instead of spilling onto the next control; the track sets `data-overflowing` while they do. ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
54
|
+
- Why: the track never shrank below its segments, so in a narrow row it ran over its neighbor; scrolling inside it is the last way it gives way.
|
|
55
|
+
- Affects: track `shrink-0` → `min-w-0 max-w-full`; while overflowing, `overflow-x: auto` with no scrollbar. Segments still never shrink. A track you made `shrink-0` yourself still never shrinks.
|
|
56
|
+
- Docs: https://raft-design.botiverse.build/components/segmented-control/#behavior
|
|
57
|
+
- **SidebarSectionControls** and **SearchEntityResult**: decided by the input, not the window's width. The section controls show at rest wherever there is no fine pointer (a touch screen, a tablet at any width) and on hover, focus or an open menu with a mouse; a search result row is at least 64px tall on a touch screen at any width. ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
58
|
+
- Why: a tablet is touch at 1024px and a narrow desktop window still has a mouse, so the width was the wrong question.
|
|
59
|
+
- Affects: section controls at rest: shown below a 768px window → shown without `pointer: fine`; a narrow desktop window with a mouse now shows them on hover only. Search result `min-height: 64px`: below a 768px window → `pointer: coarse`.
|
|
60
|
+
- Docs: https://raft-design.botiverse.build/patterns/responsive/#touch-screens
|
|
61
|
+
- **ActivityInboxSelectionToolbar**: one 44px row while its controls fit, and they wrap onto a second line in a narrow column, whatever the window. ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
62
|
+
- Why: it wrapped only below a 768px window, so in a narrow column on a wide window its controls overflowed.
|
|
63
|
+
- Affects: `flex-wrap` at every width (was below a 768px window only); padding 6px above and 5px below (was 8px below a 768px window, 0 with a fixed 44px height from it).
|
|
64
|
+
- Docs: https://raft-design.botiverse.build/components/activity-inbox/
|
|
65
|
+
- **ListRowMeta**: stays whole while the title gives way first, then truncates when there is still no room, so a long value (an email beside a button in a 320px row) is cut instead of running under the button. ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
66
|
+
- Why: the meta never shrank, so a long value overflowed the row.
|
|
67
|
+
- Affects: `shrink-0` → `min-w-0 truncate`.
|
|
68
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/#content
|
|
69
|
+
- **MessageVideoPreview**: at most `min(360px, 100%)` of its column. ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
70
|
+
- Why: its cap was the window less a guess at the message's chrome, which was wrong in a thread beside a conversation.
|
|
71
|
+
- Affects: `max-width` `min(360px, 100vw − 112px)` → `min(360px, 100%)`.
|
|
72
|
+
- Docs: https://raft-design.botiverse.build/components/message-attachment/#rules
|
|
73
|
+
- **Button** `variant="ghost"`, so **DialogFooter**, **AlertDialogFooter** (`AlertDialogCancel`) and **DrawerActions**: a ghost text button inside an element with `data-stacked` takes the ink frame of `default`. Dialog and alert dialog footers set `data-stacked` only while their buttons stack; `DrawerActions` sets it in a bottom or top sheet, where its buttons are always stacked, and not in a drawer from a side. Keep writing Cancel as `ghost`. ([#727](https://github.com/botiverse/raft-design/pull/727))
|
|
74
|
+
- Why: see Stacked buttons each take a frame, above.
|
|
75
|
+
- Affects: as above; a disabled ghost button keeps the disabled fill, and ghost icon buttons stay frameless. A stacked footer's buttons have no transition until the row has been measured and painted once (`data-measured`), so the frame doesn't fade in as a dialog opens.
|
|
76
|
+
- Docs: https://raft-design.botiverse.build/components/button/#which-color
|
|
77
|
+
- **MessageItem** text: selected text uses the `selection` token, so in dark mode a selection is deeper; light mode is unchanged. ([#730](https://github.com/botiverse/raft-design/pull/730))
|
|
78
|
+
- Why: see Text selection: its own token, above.
|
|
79
|
+
- Affects: `selection:bg-brand-soft` → `selection:bg-selection`.
|
|
80
|
+
- Docs: https://raft-design.botiverse.build/foundations/color/#text-selection
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **TaskChip** (default and `size="sm"`): the chip's baseline is its text's, so its words sit on the line of the text beside it, such as a DescriptionList label; the status pixel and icons stay centered in the chip. ([#732](https://github.com/botiverse/raft-design/pull/732))
|
|
85
|
+
- Why: a centered flex box takes its baseline from its first item, so a chip that led with its status pixel sat on the pixel's bottom edge and its words stood a pixel off the label beside them.
|
|
86
|
+
- Affects: text laid out on the baseline inside the chip with 3px above and below (default, 22px) and 2px (`sm`, 20px), marks `self-center`; heights unchanged. `variant="inline"` is unchanged.
|
|
87
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#text-of-different-sizes-on-one-line
|
|
88
|
+
- **CodeBlockPre**: measures again whenever the `pre` resizes or its code changes (highlighting that arrives late, code that streams in), so a line that grows too long becomes the keyboard-reachable region (`tabindex="0"`, `role="region"`) it should be. ([#721](https://github.com/botiverse/raft-design/pull/721))
|
|
89
|
+
- Why: it measured once when it mounted, so code that changed later kept the first answer.
|
|
90
|
+
- Docs: https://raft-design.botiverse.build/components/code-block/#accessibility
|
|
91
|
+
|
|
1
92
|
## [0.22.1](https://github.com/botiverse/raft-design/compare/v0.22.0...v0.22.1) (2026-10-22)
|
|
2
93
|
|
|
3
94
|
### Design System
|
|
@@ -296,18 +296,23 @@
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
/* A row of steps is its own container, so it switches on its own width, not
|
|
300
|
+
the window's: a dialog or a 400px panel on a wide window is as narrow as a
|
|
301
|
+
phone (design/patterns/responsive.md#switch-on-the-regions-width). Being a
|
|
302
|
+
container, it fills its row. A row too long for its width wraps its steps
|
|
303
|
+
onto a second line. */
|
|
304
|
+
.r-steps--horizontal {
|
|
305
|
+
container-type: inline-size;
|
|
306
|
+
flex-wrap: wrap;
|
|
307
|
+
row-gap: 8px;
|
|
308
|
+
inline-size: 100%;
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
/*
|
|
307
|
-
the steps share the width and each stacks its
|
|
308
|
-
titles wrapping. The marker and the connector
|
|
309
|
-
spans the step below. */
|
|
310
|
-
@
|
|
311
|
+
/* A narrow row (below 40rem of its own width): the titles beside their
|
|
312
|
+
markers would overflow it, so the steps share the width and each stacks its
|
|
313
|
+
text under the marker, the titles wrapping. The marker and the connector
|
|
314
|
+
share the top line; the text spans the step below. */
|
|
315
|
+
@container (width < 40rem) {
|
|
311
316
|
.r-steps--horizontal > .r-step {
|
|
312
317
|
--r-steps-connector-gap: 6px;
|
|
313
318
|
--r-steps-connector-min-width: 8px;
|
package/dist/foundation.css
CHANGED
|
@@ -287,6 +287,10 @@
|
|
|
287
287
|
empties it while the root carries data-input-modality="pointer", which
|
|
288
288
|
installInputModality sets after a click or a tap. */
|
|
289
289
|
--focus-ring: 0 0 0 2px oklch(1 0 0), 0 0 0 4px oklch(0.1813 0.005 17.62);
|
|
290
|
+
/* Selected text (::selection). brand-soft in light mode; in dark mode brand
|
|
291
|
+
over the page at 22%, the wash of a commented passage, because brand-soft
|
|
292
|
+
left a selection almost the same as the page. */
|
|
293
|
+
--selection: var(--brand-soft);
|
|
290
294
|
--field-font-size: 16px;
|
|
291
295
|
--field-font-weight: 400;
|
|
292
296
|
--field-line-height: 24px;
|
|
@@ -415,6 +419,7 @@
|
|
|
415
419
|
0 0 0 1px oklch(0.9843 0.004 106.47 / 0.078), 0 16px 40px -12px oklch(0 0 0 / 0.8);
|
|
416
420
|
--elevation-sticker: 4px 4px 0 oklch(0 0 0);
|
|
417
421
|
--focus-ring: 0 0 0 2px oklch(0.2213 0.0038 106.71), 0 0 0 4px oklch(0.9211 0.004 106.48);
|
|
422
|
+
--selection: color-mix(in oklch, var(--brand-400) 22%, transparent);
|
|
418
423
|
--sticker-edge: oklch(0.9843 0.004 106.47 / 0.349);
|
|
419
424
|
}
|
|
420
425
|
|
|
@@ -497,6 +502,7 @@
|
|
|
497
502
|
0 0 0 1px oklch(0.9843 0.004 106.47 / 0.078), 0 16px 40px -12px oklch(0 0 0 / 0.8);
|
|
498
503
|
--elevation-sticker: 4px 4px 0 oklch(0 0 0);
|
|
499
504
|
--focus-ring: 0 0 0 2px oklch(0.2213 0.0038 106.71), 0 0 0 4px oklch(0.9211 0.004 106.48);
|
|
505
|
+
--selection: color-mix(in oklch, var(--brand-400) 22%, transparent);
|
|
500
506
|
--sticker-edge: oklch(0.9843 0.004 106.47 / 0.349);
|
|
501
507
|
}
|
|
502
508
|
}
|
|
@@ -56,15 +56,22 @@ const headEdges = ["px-5 max-md:px-4", ...headEdgePull];
|
|
|
56
56
|
* as the arithmetic so it follows the control tokens wherever they switch.
|
|
57
57
|
*/
|
|
58
58
|
const controlIconInset = "[--icon-inset:calc((var(--control-height)-var(--control-icon-size))/2)]";
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/lib/stacked-actions.ts
|
|
59
61
|
/**
|
|
60
62
|
* The row of buttons while `data-stacked` is set: a full-width column. A
|
|
61
63
|
* button alone in a wrapper (a tooltip's trigger) fills the width too.
|
|
64
|
+
*
|
|
65
|
+
* Until the row has been measured and painted once (`data-measured`), its
|
|
66
|
+
* buttons have no transition. The first measure lands after the buttons were
|
|
67
|
+
* styled as a row, so a stacked ghost Cancel would otherwise fade its frame in
|
|
68
|
+
* as the dialog opens; the first painted frame must already be the stack.
|
|
62
69
|
*/
|
|
63
70
|
const stackedActions = [
|
|
64
71
|
"data-stacked:flex-col-reverse",
|
|
65
72
|
"data-stacked:items-stretch",
|
|
66
73
|
"data-stacked:*:data-[slot=button]:w-full data-stacked:[&>*>[data-slot=button]:only-child]:w-full",
|
|
67
|
-
|
|
74
|
+
"not-data-measured:**:data-[slot=button]:transition-none"
|
|
68
75
|
];
|
|
69
76
|
function assignRef$2(ref, value) {
|
|
70
77
|
if (typeof ref === "function") ref(value);
|
|
@@ -117,11 +124,16 @@ function useStackedActions(ref, inParent = false) {
|
|
|
117
124
|
useLayoutEffect(() => {
|
|
118
125
|
const frame = inParent ? row?.parentElement : row;
|
|
119
126
|
if (!row || !frame || typeof ResizeObserver === "undefined") return;
|
|
127
|
+
let raf = 0;
|
|
128
|
+
let settle = 0;
|
|
120
129
|
const measure = () => {
|
|
121
130
|
const overflows = rowOverflows(row, frame);
|
|
122
|
-
if (overflows
|
|
131
|
+
if (overflows === void 0) return;
|
|
132
|
+
setStacked(overflows);
|
|
133
|
+
if (!settle && !row.hasAttribute("data-measured")) settle = requestAnimationFrame(() => {
|
|
134
|
+
settle = requestAnimationFrame(() => row.setAttribute("data-measured", ""));
|
|
135
|
+
});
|
|
123
136
|
};
|
|
124
|
-
let raf = 0;
|
|
125
137
|
const schedule = () => {
|
|
126
138
|
cancelAnimationFrame(raf);
|
|
127
139
|
raf = requestAnimationFrame(measure);
|
|
@@ -146,6 +158,7 @@ function useStackedActions(ref, inParent = false) {
|
|
|
146
158
|
return () => {
|
|
147
159
|
live = false;
|
|
148
160
|
cancelAnimationFrame(raf);
|
|
161
|
+
cancelAnimationFrame(settle);
|
|
149
162
|
resize.disconnect();
|
|
150
163
|
mutation.disconnect();
|
|
151
164
|
fonts?.removeEventListener("loadingdone", schedule);
|
|
@@ -446,6 +459,21 @@ const buttonVariants = tv$1({
|
|
|
446
459
|
size: "md"
|
|
447
460
|
},
|
|
448
461
|
compoundVariants: [
|
|
462
|
+
{
|
|
463
|
+
variant: "ghost",
|
|
464
|
+
size: [
|
|
465
|
+
"xs",
|
|
466
|
+
"sm",
|
|
467
|
+
"md",
|
|
468
|
+
"lg",
|
|
469
|
+
"xl"
|
|
470
|
+
],
|
|
471
|
+
class: { root: [
|
|
472
|
+
"in-data-stacked:border-line-strong in-data-stacked:bg-layer-card",
|
|
473
|
+
"in-data-stacked:hover:bg-layer-card in-data-stacked:active:bg-layer-card",
|
|
474
|
+
"in-data-stacked:hover:overlay-medium in-data-stacked:active:overlay-medium"
|
|
475
|
+
] }
|
|
476
|
+
},
|
|
449
477
|
{
|
|
450
478
|
variant: "ghost",
|
|
451
479
|
size: [
|
|
@@ -1602,7 +1630,7 @@ const listRow = tv$1({
|
|
|
1602
1630
|
"group-data-[tone=unread]/list-row:font-bold"
|
|
1603
1631
|
],
|
|
1604
1632
|
description: ["min-w-0 flex-1 truncate", "text-callout text-(--list-row-ink-2)"],
|
|
1605
|
-
meta: ["
|
|
1633
|
+
meta: ["min-w-0 truncate", "font-mono text-mono-meta font-normal tabular-nums text-(--list-row-ink-3)"],
|
|
1606
1634
|
value: ["max-w-[45%] shrink-0 truncate", "text-subhead text-(--list-row-ink-3)"],
|
|
1607
1635
|
chevron: ["flex shrink-0 items-center justify-center", "text-(--list-row-ink-3) [&>svg]:size-5 [&>svg]:shrink-0"]
|
|
1608
1636
|
},
|
|
@@ -3262,7 +3290,7 @@ const targets = /* @__PURE__ */ new Set();
|
|
|
3262
3290
|
const byElement = /* @__PURE__ */ new WeakMap();
|
|
3263
3291
|
const pending = /* @__PURE__ */ new Set();
|
|
3264
3292
|
let frame = 0;
|
|
3265
|
-
let resizeObserver;
|
|
3293
|
+
let resizeObserver$1;
|
|
3266
3294
|
let mutationObserver;
|
|
3267
3295
|
let fontsListening = false;
|
|
3268
3296
|
function flush() {
|
|
@@ -3294,7 +3322,7 @@ function ownerOf(node) {
|
|
|
3294
3322
|
}
|
|
3295
3323
|
}
|
|
3296
3324
|
function observers(view) {
|
|
3297
|
-
if (!resizeObserver && "ResizeObserver" in view) resizeObserver = new view.ResizeObserver((entries) => {
|
|
3325
|
+
if (!resizeObserver$1 && "ResizeObserver" in view) resizeObserver$1 = new view.ResizeObserver((entries) => {
|
|
3298
3326
|
for (const entry of entries) {
|
|
3299
3327
|
const target = byElement.get(entry.target);
|
|
3300
3328
|
if (target) scheduleMeasure(target);
|
|
@@ -3325,7 +3353,7 @@ function observeTruncation(target) {
|
|
|
3325
3353
|
targets.add(target);
|
|
3326
3354
|
for (const element of observed) {
|
|
3327
3355
|
byElement.set(element, target);
|
|
3328
|
-
resizeObserver?.observe(element);
|
|
3356
|
+
resizeObserver$1?.observe(element);
|
|
3329
3357
|
}
|
|
3330
3358
|
const watched = target.watch();
|
|
3331
3359
|
if (watched) {
|
|
@@ -3342,7 +3370,7 @@ function observeTruncation(target) {
|
|
|
3342
3370
|
pending.delete(target);
|
|
3343
3371
|
for (const element of observed) {
|
|
3344
3372
|
if (byElement.get(element) === target) byElement.delete(element);
|
|
3345
|
-
resizeObserver?.unobserve(element);
|
|
3373
|
+
resizeObserver$1?.unobserve(element);
|
|
3346
3374
|
}
|
|
3347
3375
|
if (watched && byElement.get(watched) === target) byElement.delete(watched);
|
|
3348
3376
|
if (targets.size === 0) {
|
|
@@ -3691,6 +3719,9 @@ const messageItem = tv$1({
|
|
|
3691
3719
|
"group/message relative flex h-fit gap-3",
|
|
3692
3720
|
"hover:[--mi-toolbar-opacity:1] hover:[--mi-toolbar-pe:auto] focus-within:[--mi-toolbar-opacity:1] focus-within:[--mi-toolbar-pe:auto]",
|
|
3693
3721
|
"has-data-popup-open:[--mi-toolbar-opacity:1] has-data-popup-open:[--mi-toolbar-pe:auto]",
|
|
3722
|
+
"hover:[--mi-header-mask:linear-gradient(to_left,transparent_calc(var(--mi-toolbar-width)+8px),black_calc(var(--mi-toolbar-width)+20px))]",
|
|
3723
|
+
"focus-within:[--mi-header-mask:linear-gradient(to_left,transparent_calc(var(--mi-toolbar-width)+8px),black_calc(var(--mi-toolbar-width)+20px))]",
|
|
3724
|
+
"has-data-popup-open:[--mi-header-mask:linear-gradient(to_left,transparent_calc(var(--mi-toolbar-width)+8px),black_calc(var(--mi-toolbar-width)+20px))]",
|
|
3694
3725
|
"min-h-0 px-2 py-timeline-row border-t-(length:--spacing-timeline-turn) border-transparent bg-clip-padding",
|
|
3695
3726
|
"hover:[--mi-ct-color:var(--foreground-hint)] has-data-popup-open:[--mi-ct-color:var(--foreground-hint)]",
|
|
3696
3727
|
"bg-(--mi-row-fill) [--mi-row-fill:var(--located-fill)]",
|
|
@@ -3715,7 +3746,7 @@ const messageItem = tv$1({
|
|
|
3715
3746
|
content: ["min-w-0 flex-1", "text-body text-foreground"],
|
|
3716
3747
|
header: [
|
|
3717
3748
|
"mb-px flex min-w-0 items-baseline gap-1.5 overflow-hidden",
|
|
3718
|
-
"
|
|
3749
|
+
"mask-(--mi-header-mask,none)",
|
|
3719
3750
|
"has-data-[slot=message-item-delegated-by]:[--mi-owner-basis:3rem] has-data-[slot=message-item-delegated-by]:[--mi-owner-flex:1_0]"
|
|
3720
3751
|
],
|
|
3721
3752
|
senderGroup: [
|
|
@@ -3760,7 +3791,7 @@ const messageItem = tv$1({
|
|
|
3760
3791
|
"wrap-break-word select-text",
|
|
3761
3792
|
"**:select-text",
|
|
3762
3793
|
"text-body text-foreground",
|
|
3763
|
-
"selection:bg-
|
|
3794
|
+
"selection:bg-selection"
|
|
3764
3795
|
],
|
|
3765
3796
|
searchMatch: searchHit,
|
|
3766
3797
|
embedded: ["min-w-0", "mt-2"],
|
|
@@ -4075,12 +4106,113 @@ function MessageItemDelegatedAvatar({ className, deleted, ...props }) {
|
|
|
4075
4106
|
}
|
|
4076
4107
|
//#endregion
|
|
4077
4108
|
//#region src/components/message-item/message-item-toolbar.tsx
|
|
4078
|
-
|
|
4109
|
+
const TOOLBAR_WIDTH = "--mi-toolbar-width";
|
|
4110
|
+
/**
|
|
4111
|
+
* Five buttons' width (5 × 28px, 4 × 4px apart, 4px from the frame each
|
|
4112
|
+
* side): what the row fades until the toolbar has measured itself.
|
|
4113
|
+
*/
|
|
4114
|
+
const FALLBACK_WIDTH = "164px";
|
|
4115
|
+
/**
|
|
4116
|
+
* Toolbars watched while their row shows them, each with its row, and the one
|
|
4117
|
+
* ResizeObserver that watches them: it exists only while a toolbar shows.
|
|
4118
|
+
*/
|
|
4119
|
+
const watched = /* @__PURE__ */ new Map();
|
|
4120
|
+
let resizeObserver = null;
|
|
4121
|
+
function writeWidth(toolbar, row) {
|
|
4122
|
+
row.style.setProperty(TOOLBAR_WIDTH, `${toolbar.offsetWidth}px`);
|
|
4123
|
+
}
|
|
4124
|
+
/** The recipe's has-data-popup-open / data-popup-open on the row. */
|
|
4125
|
+
function hasOpenPopup(row) {
|
|
4126
|
+
return row.hasAttribute("data-popup-open") || row.querySelector("[data-popup-open]") !== null;
|
|
4127
|
+
}
|
|
4128
|
+
function resized(entries) {
|
|
4129
|
+
for (const entry of entries) {
|
|
4130
|
+
const target = entry.target;
|
|
4131
|
+
const row = watched.get(target);
|
|
4132
|
+
if (row) writeWidth(target, row);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
function unwatch(toolbar) {
|
|
4136
|
+
if (!watched.delete(toolbar)) return;
|
|
4137
|
+
resizeObserver?.unobserve(toolbar);
|
|
4138
|
+
if (watched.size > 0) return;
|
|
4139
|
+
resizeObserver?.disconnect();
|
|
4140
|
+
resizeObserver = null;
|
|
4141
|
+
}
|
|
4142
|
+
function watch(toolbar, row) {
|
|
4143
|
+
if (watched.has(toolbar)) return;
|
|
4144
|
+
writeWidth(toolbar, row);
|
|
4145
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
4146
|
+
for (const [other, otherRow] of watched) if (!otherRow.matches(":hover, :focus-within") && !hasOpenPopup(otherRow)) unwatch(other);
|
|
4147
|
+
resizeObserver ??= new ResizeObserver(resized);
|
|
4148
|
+
watched.set(toolbar, row);
|
|
4149
|
+
resizeObserver.observe(toolbar);
|
|
4150
|
+
}
|
|
4151
|
+
/**
|
|
4152
|
+
* Tells the row how wide the toolbar is (`--mi-toolbar-width` on the row), so
|
|
4153
|
+
* the header can fade out its end under the toolbar while it shows: the
|
|
4154
|
+
* header keeps no room for the toolbar (message-item.md, "The hover
|
|
4155
|
+
* toolbar"), and the toolbar's card only reaches part of the sender line.
|
|
4156
|
+
* A toolbar with more or fewer actions covers exactly its own width; nothing
|
|
4157
|
+
* on the row moves.
|
|
4158
|
+
*
|
|
4159
|
+
* Measured when the toolbar is about to show, not when it mounts: every row
|
|
4160
|
+
* of a timeline renders a toolbar, and the width matters only on the row the
|
|
4161
|
+
* pointer or focus is on. So mounting costs no React state (one commit per
|
|
4162
|
+
* row) and no ResizeObserver per row. The row's `pointerenter` and `focusin`
|
|
4163
|
+
* write the width in the input event's own task, before the frame that
|
|
4164
|
+
* applies the row's :hover / :focus-within styles, so the first frame that
|
|
4165
|
+
* shows the toolbar already masks its width; the toolbar is laid out at rest
|
|
4166
|
+
* (only its opacity is 0), so its `offsetWidth` is right then. A popup opens
|
|
4167
|
+
* from the toolbar only after a hover or a focus, so it finds the width
|
|
4168
|
+
* written. While the row shows the toolbar, one ResizeObserver shared by all
|
|
4169
|
+
* rows watches it, so a button added while it shows re-measures; it lets go
|
|
4170
|
+
* when the pointer and focus have left the row and no popup is open, and is
|
|
4171
|
+
* disconnected once no toolbar shows.
|
|
4172
|
+
*
|
|
4173
|
+
* On attach the row gets five buttons' width (164px) as a fallback: a plain
|
|
4174
|
+
* DOM write, no commit and no read, so a show that ever came without a
|
|
4175
|
+
* pointerenter or focusin (a row that appears under a resting pointer) fades
|
|
4176
|
+
* too much rather than leaving letters beside the card. Rows without a
|
|
4177
|
+
* toolbar never get the property, so their header's gradient stays invalid
|
|
4178
|
+
* and masks nothing.
|
|
4179
|
+
*
|
|
4180
|
+
* A stable callback ref with a cleanup (React 19), merged with the caller's
|
|
4181
|
+
* ref by `useRender`.
|
|
4182
|
+
*/
|
|
4183
|
+
function measureWidthOnShow(toolbar) {
|
|
4184
|
+
const row = toolbar?.closest("[data-message-item]");
|
|
4185
|
+
if (!toolbar || !row) return void 0;
|
|
4186
|
+
const show = () => watch(toolbar, row);
|
|
4187
|
+
const pointerLeft = () => {
|
|
4188
|
+
if (!row.matches(":focus-within") && !hasOpenPopup(row)) unwatch(toolbar);
|
|
4189
|
+
};
|
|
4190
|
+
const focusLeft = (event) => {
|
|
4191
|
+
const next = event.relatedTarget instanceof Node ? event.relatedTarget : null;
|
|
4192
|
+
if (row.contains(next) || row.matches(":hover") || hasOpenPopup(row)) return;
|
|
4193
|
+
unwatch(toolbar);
|
|
4194
|
+
};
|
|
4195
|
+
row.style.setProperty(TOOLBAR_WIDTH, FALLBACK_WIDTH);
|
|
4196
|
+
row.addEventListener("pointerenter", show);
|
|
4197
|
+
row.addEventListener("focusin", show);
|
|
4198
|
+
row.addEventListener("pointerleave", pointerLeft);
|
|
4199
|
+
row.addEventListener("focusout", focusLeft);
|
|
4200
|
+
return () => {
|
|
4201
|
+
row.removeEventListener("pointerenter", show);
|
|
4202
|
+
row.removeEventListener("focusin", show);
|
|
4203
|
+
row.removeEventListener("pointerleave", pointerLeft);
|
|
4204
|
+
row.removeEventListener("focusout", focusLeft);
|
|
4205
|
+
unwatch(toolbar);
|
|
4206
|
+
row.style.removeProperty(TOOLBAR_WIDTH);
|
|
4207
|
+
};
|
|
4208
|
+
}
|
|
4209
|
+
function MessageItemToolbar({ className, render, ref, ...props }) {
|
|
4079
4210
|
const { toolbar } = messageItem();
|
|
4080
4211
|
const defaultProps = { className: toolbar({ className }) };
|
|
4081
4212
|
return useRender({
|
|
4082
4213
|
defaultTagName: "div",
|
|
4083
4214
|
render,
|
|
4215
|
+
ref: ref ? [ref, measureWidthOnShow] : measureWidthOnShow,
|
|
4084
4216
|
props: mergeProps(defaultProps, props),
|
|
4085
4217
|
state: { slot: "message-item-toolbar" }
|
|
4086
4218
|
});
|
|
@@ -1368,7 +1368,7 @@ declare function MessageItemDelegatedAvatar({ className, deleted, ...props }: Me
|
|
|
1368
1368
|
//#endregion
|
|
1369
1369
|
//#region src/components/message-item/message-item-toolbar.d.ts
|
|
1370
1370
|
type MessageItemToolbarProps = useRender.ComponentProps<"div">;
|
|
1371
|
-
declare function MessageItemToolbar({ className, render, ...props }: MessageItemToolbarProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
1371
|
+
declare function MessageItemToolbar({ className, render, ref, ...props }: MessageItemToolbarProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
1372
1372
|
type MessageItemToolbarButtonProps = useRender.ComponentProps<"button"> & {
|
|
1373
1373
|
active?: boolean;
|
|
1374
1374
|
};
|