@botiverse/raft-ui 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,191 @@
1
+ ## [0.21.0](https://github.com/botiverse/raft-design/compare/v0.20.1...v0.21.0) (2026-10-22)
2
+
3
+ 0.20.1 was not published to npm. This release includes its changes, listed under [0.20.1](https://github.com/botiverse/raft-design/blob/v0.21.0/packages/ui/CHANGELOG.md#0201-2026-10-22) in the changelog.
4
+
5
+ ### Design System
6
+
7
+ - **Timeline rhythm**: a conversation uses three distances between its lines, measured from text to text: 8px between the rows of one turn, the message step (16px) between turns, and the section step (24px) at a boundary: either side of a date line, from the top of the list to its first line, and from the last line to the composer's frame. It applies to MessageItem, SystemMessage, SystemMessageGroupHeader, MessageListDateDivider, MessageListNotice, ThreadPanelOriginalMessage, ThreadPanelReplySummary, ThreadPanelReplyCount, ConversationPanelFooter and ThreadPanelFooter. ([#694](https://github.com/botiverse/raft-design/pull/694))
8
+ - Why: each part kept its own spacing, so a date line sat closer to the message above it than to the one below and looked attached to it, an event line had more space under it than over it, and the composer's frame read as touching the last message; the parts now read one rhythm from tokens, one step wider for each kind of break.
9
+ - Affects: new tokens `timeline-gap-message` 16px, `timeline-gap-section` 24px and `timeline-row-inset` 4px (the reach of a row's highlight above and below its text).
10
+ - Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
11
+ - **A control and its label**: a checkbox, radio or switch sits 8px before its label, centered on the label's first line, and keeps its size wherever it sits. In a settings row with a description, both ends center on the title's first line. A tap between two options lands on the nearer one. It applies to Checkbox, RadioGroupItem and ListRow; FieldItem already followed it. ([#686](https://github.com/botiverse/raft-design/pull/686))
12
+ - Why: a box in a `<label>` row followed the row instead of the text's line, so callers nudged it with a margin, and a settings row's ends centered on the title and description together, level with neither; the components now place the control themselves.
13
+ - Affects: switch 50 × 30px and checkbox and radio 20px, unchanged; 8px from control to label.
14
+ - Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
15
+
16
+ ### Breaking Changes
17
+
18
+ - **ConversationPanelFooter** and **ThreadPanelFooter**: no space of their own above the composer; the end of the list sets it. ([#694](https://github.com/botiverse/raft-design/pull/694))
19
+ - Why: the footer's 12px stacked on whatever space the list left under its last message, so the gap to the composer differed from list to list; now the list ends with one token and the composer's frame sits a section step, 24px, under the last line.
20
+ - Affects: footer top padding 12px → 0; `pb-timeline-end` is 20px (the 24px section step less the last row's 4px inset).
21
+ - Migrate: end your message list with `pb-timeline-end` under its last row, in place of any spacer you had there; without it the composer moves 12px closer to the last message. You can also start the list with `pt-timeline-start` over its first row, so its top follows the same step.
22
+ - Docs: https://raft-design.botiverse.build/components/conversation-panel/
23
+
24
+ ### Added
25
+
26
+ - **Timeline spacing utilities**: `pt-timeline-start` and `pb-timeline-end` for the top and end of a message list you build, and `timeline-message`, `timeline-section`, `timeline-row`, `timeline-turn` and `timeline-lead`, which the components use. All are registered with the `cn` helper, so they merge with other padding and margin classes. ([#694](https://github.com/botiverse/raft-design/pull/694))
27
+ - Why: a list's top and end were written by hand in each product, which is how the gaps drifted; two utilities now give the section step with the rows' own insets taken out.
28
+ - Affects: `timeline-start` 12px, `timeline-end` 20px, `timeline-turn` 8px, `timeline-lead` 12px, `timeline-message` 16px, `timeline-section` 24px, `timeline-row` 4px. Use only `timeline-start` and `timeline-end` in a product.
29
+ - Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
30
+ - **`ButtonSize`** and **`DeprecatedButtonSize`** types, and the **`button-sizes` codemod**, which renames deprecated Button sizes to the size they render as. ([#686](https://github.com/botiverse/raft-design/pull/686))
31
+ - Why: the deprecated names could only be found by hand, and `sm` and `xs` are also sizes of other components; the codemod parses TSX and renames `size` only on Buttons, the raft-ui parts that pass `size` to a Button and wrappers you name, plus `icon-sm` / `icon-xs` on any element and `size` in `buttonVariants()` calls.
32
+ - Affects: run `node node_modules/@botiverse/raft-ui/codemods/button-sizes.mjs <src> [--dry] [--components=MyButton]`; it renames `xs`, `sm` → `md` and `icon-xs`, `icon-sm` → `icon-md`, then lists every `sm`, `xs`, `icon-sm` and `icon-xs` it left for review. It needs `@babel/parser`, a new optional peer dependency (`pnpm add -D @babel/parser`); nothing else uses it.
33
+ - Docs: https://raft-design.botiverse.build/components/button/#sizes
34
+
35
+ ### Changed
36
+
37
+ - **Button** `size`: `xs` and `sm` are deprecated aliases of `md`, and `icon-xs` and `icon-sm` of `icon-md`, kept until raft-ui 1.0. They already rendered as those sizes, so nothing on screen changes. ([#686](https://github.com/botiverse/raft-design/pull/686))
38
+ - Why: nine names for five sizes made a smaller button look available where there is none; a new Size by place table says which size each place takes.
39
+ - Affects: sizes `md` (32px), `lg`, `xl`, `icon-md` and `icon-lg`; `size` is typed `ButtonSize | DeprecatedButtonSize`, so every existing size name still type-checks. No runtime warning.
40
+ - Docs: https://raft-design.botiverse.build/components/button/#size-by-place
41
+ - **Checkbox** and **RadioGroupItem** directly in a `<label>`: the box takes one line of the label's height and centers on it, so it sits on the first line in an `items-start` row and on the one line in an `items-center` row, at any min-height. ([#686](https://github.com/botiverse/raft-design/pull/686))
42
+ - Why: the box followed the row rather than the text's line, so it sat above the center of a line taller than 20px and callers added a `margin-top`; see A control and its label above.
43
+ - Affects: vertical margin `max(0, (1lh - 20px) / 2)` from the `<label>`'s type (2px on a 24px line); the box stays 20px and a `text-xs` row stays 20px tall. A `margin-top` you added to line it up is no longer needed. Set the label's type on the `<label>`, use `items-start` when the text can wrap, and give a wrapping `text-xs` label `leading-5`.
44
+ - Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
45
+ - **DescriptionList** `direction="inline"`: every fact is at most 24rem wide, or the row's width if that is narrower, and keeps the same 24px between facts and 8px between lines as the others. ([#697](https://github.com/botiverse/raft-design/pull/697))
46
+ - Why: a long value, such as a message, made its fact as wide as the row on a line of its own, so one line of facts no longer read as one flow; every fact now wraps the same way, and a long value wraps at the cap, or is cut there when the caller truncates it.
47
+ - Affects: fact `max-width` none → `min(100%, 24rem)` (384px); plain text longer than that wraps inside the 24rem. To cut a long value on one line instead, truncate it with an ellipsis and make the whole value available elsewhere, such as a tooltip.
48
+ - Docs: https://raft-design.botiverse.build/components/description-list/
49
+ - **ListRow** `variant="plain"` with a description: the leading icon, avatar or tile and the trailing switch, checkbox, radio, value or chevron center on the title's first line instead of the middle of the title and description. With a title alone, nothing changes. ([#686](https://github.com/botiverse/raft-design/pull/686))
50
+ - Why: with a description the ends sat between the title and the description, level with neither and with no line of text.
51
+ - Affects: the ends move without changing size, so row heights are unchanged (54px on a phone for a one-line row with a switch).
52
+ - Docs: https://raft-design.botiverse.build/components/list-row/
53
+ - **MessageListDateDivider**, **SystemMessage**, **SystemMessageGroupHeader**, **MessageListNotice**, **ThreadPanelOriginalMessage**, **ThreadPanelReplySummary** and **ThreadPanelReplyCount**: spaced on the timeline rhythm. ([#694](https://github.com/botiverse/raft-design/pull/694))
54
+ - Why: see Timeline rhythm above.
55
+ - Affects: a date line's rule sits 24px from the text above and below it (was 24px / 32px on a phone, 25px / 33px on a desktop); SystemMessage padding 12px / 12px → 12px / 4px, 16px from the text on both sides; a closed SystemMessageGroupHeader keeps the same 12px / 4px around its disclosure's text (padding 6px / 6px → 6px top and −2px bottom on a phone, 7px and −1px on a desktop), and an open one keeps its 6px under the header; MessageListNotice padding 0 / 8px → 12px / 4px; ThreadPanelOriginalMessage bottom padding 8px → 20px and ThreadPanelReplySummary top padding 8px → 12px, so the thread's rule sits 24px from the text on both sides; ThreadPanelReplyCount gains 4px above it (margin 0 / 4px → 4px / 4px). MessageItem keeps its 4px / 8px, now read from the tokens.
56
+ - Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
57
+
58
+ ### Fixed
59
+
60
+ - **Select**, **Combobox**, **ComboboxSelect** and **InputGroup**: a value cut with an ellipsis gets its full text as a `title` whenever it is cut. ([#695](https://github.com/botiverse/raft-design/pull/695))
61
+ - Why: the check compared whole pixels with 1px to spare, so it missed cuts of about 0.6–1.3px that still showed an ellipsis, and it did not measure again when a web font finished loading; it now compares fractional sizes and measures again after fonts load.
62
+ - Affects: tolerance 1px (whole pixels) → 0.5px (fractional).
63
+ - Docs: https://raft-design.botiverse.build/components/select/
64
+ - **Checkbox** and **RadioGroupItem** hit areas: in a `<label>` row the box or circle reaches 4px past its line, half the 8px gap to the next option, so a tap between two options lands on the nearer one; the label is the target for the rest. On its own, its hit area is 44px tall on a touch screen. ([#686](https://github.com/botiverse/raft-design/pull/686))
65
+ - Why: each option's hit area reached across the gap and the next option's covered it, so a tap just under one option could select the next one.
66
+ - Affects: standalone touch hit area 44px tall (was 36px for a checkbox, 33px for a radio); standalone mouse hit area unchanged; in a `<label>` row the hit area is the line plus 4px above and below, for every pointer.
67
+ - Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
68
+
69
+ ## [0.20.1](https://github.com/botiverse/raft-design/compare/v0.20.0...v0.20.1) (2026-10-22)
70
+
71
+ ### Design System
72
+
73
+ - **A mark that leads text**: an icon or status light that starts a line of text is centered on the first line of that text and stays there when the text wraps or a description follows. Only the place is shared; each component keeps its own icon size and gap. A status light beside text is always 9px. It applies to Banner, PanelNotice, ToastIcon, MessageActionCardNotice, Badge and PanelActivity. ([#670](https://github.com/botiverse/raft-design/pull/670), [#687](https://github.com/botiverse/raft-design/pull/687))
74
+ - Why: each component placed its mark with its own fixed offset, which held for one type size and drifted at the next; one rule, the line box center (the cap-height center in Space Grotesk), now places every mark.
75
+ - Affects: offset `(1lh - size) / 2` on the row's own type; status light 9px with a 1.5px ink edge.
76
+ - Docs: https://raft-design.botiverse.build/foundations/alignment/#a-mark-that-leads-text
77
+ - **Popups stay on their trigger**: when the window or a pane scrolls under an open popup, the popup moves with the element that opened it. A context menu stays with the point where it was opened. It applies to SelectContent, ComboboxContent, PopoverContent, PreviewCardContent and NotificationCenterPopup. ([#669](https://github.com/botiverse/raft-design/pull/669))
78
+ - Why: popups stayed where they opened while the page scrolled, pointing at whatever had scrolled into place; the wrappers no longer turn Base UI's anchor tracking off.
79
+ - Docs: https://raft-design.botiverse.build/foundations/elevation/#popups
80
+
81
+ ### Added
82
+
83
+ - **`lead-icon`** and **`lead-dot`** utilities: center an icon (`lead-icon`) or a `Status` light (`lead-dot`) you place yourself on the first line of the text beside it. ([#670](https://github.com/botiverse/raft-design/pull/670), [#687](https://github.com/botiverse/raft-design/pull/687))
84
+ - Why: a mark placed outside the components above had no way to follow the same rule except a hand-tuned margin.
85
+ - Affects: `lead-dot` sizes the light 9px with a 1.5px edge, so don't pass the `Status` a `size`. Both read `1lh`, so put the text's type on the row that holds the mark and the text.
86
+ - Docs: https://raft-design.botiverse.build/foundations/alignment/#a-mark-that-leads-text
87
+
88
+ ### Changed
89
+
90
+ - **Status** in Banner, PanelNotice, ToastIcon, Badge and PanelActivity: held at 9px and centered on the first line, whatever `size` or vertical margin it was given. ([#670](https://github.com/botiverse/raft-design/pull/670), [#687](https://github.com/botiverse/raft-design/pull/687))
91
+ - Why: the same light showed at different sizes in a toast, a banner and a chip; a light beside text has one size, 9px, the least color that tells green from yellow from orange, plus its ink edges.
92
+ - Affects: a `Status` passed `xs` (6px), `sm` (8px) or `lg` (11px) in these components renders 9px; the `xs` edge 1px → 1.5px (its 1px corner stays). The default `md` is unchanged, as are icon sizes and gaps.
93
+ - Docs: https://raft-design.botiverse.build/components/status/#sizes
94
+
95
+ ### Fixed
96
+
97
+ - **PanelNotice**: a leading status light or icon is centered on the first line of the notice's text. ([#670](https://github.com/botiverse/raft-design/pull/670), [#687](https://github.com/botiverse/raft-design/pull/687))
98
+ - Why: the mark sat at the top of the first line, a light 6.5px above the line's center on a desktop (5.5px on a phone) and a 16px icon 3px (2px); the notice now follows A mark that leads text.
99
+ - Affects: icon size and gap are unchanged (your icon's size, 10px before the text).
100
+ - Docs: https://raft-design.botiverse.build/components/panel/
101
+ - **Popups**: SelectContent, ComboboxContent, PopoverContent, PreviewCardContent and NotificationCenterPopup follow their trigger while the window or a scrolling pane moves. ([#669](https://github.com/botiverse/raft-design/pull/669))
102
+ - Why: these wrappers turned anchor tracking off by default, so an open popup kept its opening position and drifted off its trigger as soon as anything scrolled; they now leave it to Base UI's default.
103
+ - Affects: `disableAnchorTracking` default `true` → unset (Base UI's default, tracking on). ContextMenuContent drops its `true` default too; it is anchored to a point, so it looks the same. Pass `disableAnchorTracking` to turn it off for one popup. Select still turns tracking off with `alignItemWithTrigger`.
104
+ - Docs: https://raft-design.botiverse.build/foundations/elevation/#popups
105
+
106
+ ## [0.20.0](https://github.com/botiverse/raft-design/compare/v0.19.0...v0.20.0) (2026-10-22)
107
+
108
+ ### Design System
109
+
110
+ - **Hover and press**: hover and press are one translucent ink layer over the resting colour, never a fill swap, in two tiers sized by what is under the pointer. Subtle for rows, messages, readable cards and a row whose menu is open; medium for small controls and for pressing anything clickable. A colour sticker and the frame get their own medium alphas. Selected items and fields don't hover, a message has no press, and every hover sits under `(hover: hover)`. It applies to every row, card, button, badge, tab, segment, sidebar and rail item, table row, inbox item and menu. ([#681](https://github.com/botiverse/raft-design/pull/681))
111
+ - Why: the same hover looked different on paper, the sidebar and a card, rows and messages used three different steps, and some clickable parts gave no feedback on press; one layer in two tiers reads the same everywhere.
112
+ - Affects: subtle `fill-subtle` 4% light / 3% dark; medium `fill-medium` 7.8% light / 6.5% dark (was 7.8%); `sticker-medium` 10% (was 12.2%); `fill-frame-medium` 10% light (was 12.2%) / 6% dark (was 7.8%); dark `brand-soft` 16% → 20%; colour `-hover` / `-active` pairs mix 10% ink (was 12%).
113
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
114
+ - **Inside a frame**: a control embedded in another component's frame (a field, the composer, a toolbar, a card or media corner) wears `radius-chip` (6px) and sits the same distance from every side of the frame and from its siblings; frames themselves don't change. It applies to ComboboxSelect, Combobox, InputGroup, the composer, the message hover and text-selection toolbars, the lightbox and multi-select bars, CodeBlock, media and file card actions, popover close, menus and SegmentedControl / ToggleGroup. ([#682](https://github.com/botiverse/raft-design/pull/682))
115
+ - Why: embedded buttons at the free-standing 8px corner read nearly round at 20px, and their offsets differed per component; one corner and one even inset make them read as one kind of thing.
116
+ - Affects: new tokens `control-embedded-inset` 6px and `control-embedded-size` 20px desktop / 32px phone (utilities `embedded-inset`, `control-embedded`); insets 6px in a field, the composer and a corner, 4px in a floating toolbar and for a popup's highlight, 3px between segments.
117
+ - Docs: https://raft-design.botiverse.build/foundations/shape/#inside-a-frame
118
+
119
+ ### Breaking Changes
120
+
121
+ - **Dialog** `size`: `sm` is 400px (was 384) and `lg` is 640px (was 512); `md` 448 and `wide` 896 are unchanged. Sizes are chosen by content: `sm` for a decision, `md` for a small task, `lg` for content that needs width. ([#676](https://github.com/botiverse/raft-design/pull/676))
122
+ - Why: the steps between sizes were too small to mean anything, and a confirmation could be 384 or 400 wide depending on the component; now every confirmation is 400, the same as AlertDialog.
123
+ - Affects: `sm` `max-w-sm` (384px) → 400px; `lg` `max-w-lg` (512px) → 640px.
124
+ - Migrate: dialogs with `size="sm"` grow 16px and `size="lg"` 128px; check their content and pick the size by content. Set width with `size` only, not your own `max-w-*`.
125
+ - Docs: https://raft-design.botiverse.build/components/dialog/#variants
126
+ - **Hover tokens**: renamed to their tier. The old names stay as deprecated aliases until 1.0, but they now resolve to the tier values, so some hovers change. ([#681](https://github.com/botiverse/raft-design/pull/681))
127
+ - Why: names that said where a fill was used (row hover, message hover, row open, pressed) could not describe two tiers that apply everywhere.
128
+ - Affects: `fill-row-hover`, `fill-row-open`, `fill-message-hover` → `fill-subtle` (row open 5% → 4%, message hover 3% → 4%); `fill-pressed` → `fill-medium`; `fill-frame-pressed` → `fill-frame-medium`; `sticker-pressed` → `sticker-medium`; utilities `row-hover-overlay`, `row-open-overlay` → `overlay-subtle`, `press-overlay` → `overlay-medium`, `press-overlay-sticker` → `overlay-sticker-medium`. Overlays are registered with the `cn` helper as background images, so a later `hover:bg-none` or another tier replaces them.
129
+ - Migrate: run `node node_modules/@botiverse/raft-ui/codemods/token-names.mjs <paths>`; it renames classes, `var(--…)` and `var(--color-…)` forms. Then move readable cards that used `press-overlay` for hover to `overlay-subtle`. Tests that assert a token name in a class (for example `var(--fill-row-open)` on a message) look for the new name.
130
+ - Docs: https://raft-design.botiverse.build/foundations/color/#hover-overlays
131
+ - **MessageItemToolbar** and **MessageTextSelectionToolbar**: 36px tall (was 32), buttons 4px from the frame and 4px apart (was 2px), at `radius-chip`. The hover toolbar rides 18px above the row (was 16). ([#682](https://github.com/botiverse/raft-design/pull/682))
132
+ - Why: at a 2px inset the buttons' hover fill nearly touched the frame; see Inside a frame above.
133
+ - Affects: toolbar height 32 → 36px; inset and gap 2 → 4px; the message header's default reserve `--mi-toolbar-reserve` 6.5rem → 7rem (three buttons).
134
+ - Migrate: if you set `--mi-toolbar-reserve` yourself, compute it as `1rem + 2rem × buttons` (was `1.25rem + 1.75rem × buttons`). Leave at least 18px above the first message of a scroll area and above a thread's root message.
135
+ - Docs: https://raft-design.botiverse.build/components/message-item/
136
+ - **MessageMultiSelectToolbar**: 36px tall (was 48) with 28px buttons (was 32) 4px from the frame; on a touch screen each button takes a 44px tap. The buttons go in the new `MessageMultiSelectToolbarActions`. ([#682](https://github.com/botiverse/raft-design/pull/682))
137
+ - Why: every floating toolbar is now 36px with 28px buttons; the touch hit area keeps the smaller buttons easy to tap.
138
+ - Affects: height 48 → 36px; buttons 32 → 28px; inset 8 / 10 → 4px; gap 6 → 4px.
139
+ - Migrate: wrap the toolbar's buttons in `MessageMultiSelectToolbarActions`, which owns their 4px gap and the touch hit areas; remove your own gap or padding there.
140
+ - Docs: https://raft-design.botiverse.build/components/message-item/
141
+ - **ComboboxSelect**, **Combobox** and **InputGroup** chips and end buttons: chips and embedded buttons in a field are 20px on a desktop and 32px on a phone (chips were 28px, end buttons 32px), 6px from the field's edge and from each other, at `radius-chip`, so a field with chips stays 32 / 44px tall (it grew to 42). ([#682](https://github.com/botiverse/raft-design/pull/682))
142
+ - Why: 28px chips made a field with chips taller than one without, and the embedded buttons sat at different offsets per component.
143
+ - Affects: chips 28 → 20px desktop / 32px phone; chip remove 16 / 28px at `radius-sticker`; InputGroup end-addon Button 32 → 20 / 32px with a 16px icon; ComboboxSelect `sm` keeps 22px chips at 3px, `lg` uses 40px chips in a 52px field.
144
+ - Migrate: remove size, offset or radius overrides on chips and end-addon buttons in fields. In an InputGroup end addon, put a Kbd before the Button, so on a phone (Kbd hidden) the button is the last child and ends 6px from the edge.
145
+ - Docs: https://raft-design.botiverse.build/components/combobox-select/
146
+
147
+ ### Added
148
+
149
+ - **CodeBlockPre**: renders the code's `pre`. While the code overflows it is a keyboard-reachable named region (`tabindex=0`, `role=region`, `aria-label` "Code" by default), so it can be scrolled from the keyboard; code that fits adds no Tab stop. ([#682](https://github.com/botiverse/raft-design/pull/682))
150
+ - Why: a scrolling code block could not be reached from the keyboard in Safari.
151
+ - Docs: https://raft-design.botiverse.build/components/code-block/
152
+ - **MessageMultiSelectToolbarActions**: the multi-select toolbar's actions at its end, 4px apart, with touch hit areas. ([#682](https://github.com/botiverse/raft-design/pull/682))
153
+ - Why: see MessageMultiSelectToolbar above.
154
+ - Docs: https://raft-design.botiverse.build/components/message-item/
155
+ - **Press feedback**: Badge, the mention sticker, InboxItem, Table rows, the system-message disclosure and the selected SidebarItem now press with the medium tier. ([#681](https://github.com/botiverse/raft-design/pull/681))
156
+ - Why: anything clickable presses with the medium tier; these gave no feedback on press.
157
+ - Affects: solid Badge `sticker-medium`; soft and outline Badge, InboxItem, Table row, system-message disclosure `fill-medium`; selected SidebarItem `sticker-medium`.
158
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
159
+
160
+ ### Changed
161
+
162
+ - **MessageItem**, **MessageAttachmentCard**, **MessageReplies**, **TaskCard** and menu-open rows: hover with the subtle tier. ([#681](https://github.com/botiverse/raft-design/pull/681))
163
+ - Why: a message, a readable card and a row whose menu is open are large things people read; one subtle step keeps text contrast and never stacks with the menu-open fill.
164
+ - Affects: message hover 3% / 2.2% → 4% / 3%; menu-open row 5% / 4% → 4% / 3%; attachment card, replies bar and task card 7.8% → 4% / 3%. Press stays medium.
165
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
166
+ - **SegmentedControl**, **ToggleGroup** and **Select**: a selected segment or toggle no longer darkens on hover and presses with `sticker-medium`; the Select trigger and the top-bar SearchTrigger don't hover, only press. SegmentedControl and ToggleGroup items are 3px apart (was 2). ([#681](https://github.com/botiverse/raft-design/pull/681), [#682](https://github.com/botiverse/raft-design/pull/682))
167
+ - Why: selected items and fields don't hover, like selected tabs and the active sidebar item.
168
+ - Affects: segment gap 2 → 3px.
169
+ - Docs: https://raft-design.botiverse.build/components/segmented-control/
170
+ - **DropdownMenu**, **ContextMenu**, **Select**, **Combobox**, **ComboboxSelect**, the suggestion list and quick reactions: the highlighted item sits 4px inside the popup at `radius-chip`, and separators take 4px. ([#682](https://github.com/botiverse/raft-design/pull/682))
171
+ - Why: see Inside a frame above.
172
+ - Affects: highlight inset 6px (quick reactions 8px) → 4px; highlight radius 8 → 6px; separator margins 6 → 4px.
173
+ - Docs: https://raft-design.botiverse.build/components/dropdown-menu/
174
+ - **CodeBlock**: the copy and other actions sit 6px from the corner at `radius-chip`, and without a header they take a column of their own instead of covering the first line; the code scrolls beside them. With a header, the actions sit 6px from its end, centred in it, 4px apart. ([#682](https://github.com/botiverse/raft-design/pull/682))
175
+ - Why: on a phone the copy button covered the first line of code.
176
+ - Affects: action offset 9px (no header) / 5px (header) → 6px; radius 8 → 6px; a one-line block keeps 6px under the button.
177
+ - Docs: https://raft-design.botiverse.build/components/code-block/
178
+ - **Composer**, **MessageActionCard**, **Popover** header close, **Lightbox** info bar, video preview, gallery and file card actions: embedded buttons at `radius-chip`, 6px from the frame's sides (4px in the lightbox bar, whose buttons are 28px with a 44px touch target). ([#682](https://github.com/botiverse/raft-design/pull/682))
179
+ - Why: see Inside a frame above.
180
+ - Affects: composer buttons 8 / 13px from the sides and 9px from the bottom → 6px; media and file card actions 9 / 7 / 5px → 6px; lightbox buttons 32 → 28px.
181
+ - Docs: https://raft-design.botiverse.build/foundations/shape/#inside-a-frame
182
+
183
+ ### Fixed
184
+
185
+ - **ComboboxSelect** and **FloatButton**: hover no longer sticks after a tap on a touch screen; their `:hover` rules are under `(hover: hover)`, and the ghost FloatButton hovers and presses with the overlay instead of a fill swap. ([#681](https://github.com/botiverse/raft-design/pull/681))
186
+ - Why: ungated `:hover` rules stayed applied after a tap.
187
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
188
+
1
189
  ## [0.19.0](https://github.com/botiverse/raft-design/compare/v0.18.0...v0.19.0) (2026-10-21)
2
190
 
3
191
  ### Design System
@@ -165,7 +353,7 @@
165
353
  - **Row hover**: rows hover with their own lightest visible step, not the press overlay; a row whose menu is open is marked with one slightly stronger step. ([#641](https://github.com/botiverse/raft-design/pull/641), [#643](https://github.com/botiverse/raft-design/pull/643))
166
354
  - Why: the press fill made hovered messages and list rows read as grey and heavy, and on a phone a long-press menu left no mark of which row it belonged to.
167
355
  - Affects: hover `fill-pressed` 7.8% → `fill-row-hover` 4% (dark 3%), messages `fill-message-hover` 3% (dark 2.2%), menu-open `fill-row-open` 5% (dark 4%). Press and selection are unchanged; hover stays lighter than selection.
168
- - Docs: https://raft-design.botiverse.build/foundations/states/#row-hover
356
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
169
357
 
170
358
  ### Added
171
359
 
@@ -179,11 +367,11 @@
179
367
  - **Message, SidebarItem, List, Inbox, Search, Table and File rows**: hover with `fill-row-hover` (messages with `fill-message-hover`) instead of `fill-pressed`. ([#641](https://github.com/botiverse/raft-design/pull/641))
180
368
  - Why: see Row hover above.
181
369
  - Affects: hover background 7.8% → 4% (messages 3%).
182
- - Docs: https://raft-design.botiverse.build/foundations/states/#row-hover
370
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
183
371
  - **Rows with a menu open** (right-click, ⋯ menu, phone long-press): take `fill-row-open` while the menu is open; selection still wins. **Comment cards** hover with `fill-row-hover`. ([#643](https://github.com/botiverse/raft-design/pull/643))
184
372
  - Why: the open menu needs one marker of which row it belongs to, including on a phone where there is no hover.
185
373
  - Affects: menu-open background `fill-row-open` 5% / 4%.
186
- - Docs: https://raft-design.botiverse.build/foundations/states/#row-hover
374
+ - Docs: https://raft-design.botiverse.build/foundations/states/#hover-and-press
187
375
  - **Select, Combobox**: `collisionPadding` defaults to 8px. ([#642](https://github.com/botiverse/raft-design/pull/642))
188
376
  - Why: popups touched the screen edge on phones.
189
377
  - Affects: popup inset from the screen edge, 8px.
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ // Renames Button's duplicate sizes to the one they always rendered as.
3
+ //
4
+ // node node_modules/@botiverse/raft-ui/codemods/button-sizes.mjs <dir-or-file>... [--dry] [--components=MyButton,…]
5
+ //
6
+ // `xs` and `sm` render exactly as `md`, and `icon-xs` and `icon-sm` exactly as
7
+ // `icon-md`, so nothing on screen changes: xs, sm -> md; icon-xs, icon-sm ->
8
+ // icon-md. The old names keep working as deprecated aliases until raft-ui 1.0,
9
+ // so the rewrite can land at any time after upgrading.
10
+ //
11
+ // Needs @babel/parser, an optional peer dependency of raft-ui: install it
12
+ // in the project you migrate (`pnpm add -D @babel/parser`).
13
+ //
14
+ // Each file is parsed as TypeScript and JSX (@babel/parser), so comments,
15
+ // strings and JSX text never confuse it, and a Button anywhere is found: as a
16
+ // child, in another component's prop (`actions={<Button …/>}`,
17
+ // `render={<Button …/>}`), in a callback. It rewrites:
18
+ // - the `size` of `<Button>`, of the raft-ui parts that pass `size` to a
19
+ // Button (BUTTON_COMPONENTS), and of the components named with
20
+ // `--components`, for your own wrappers: a string (`size="sm"`,
21
+ // `size={"sm"}`) or the strings in an expression (`size={inHead ? "sm" :
22
+ // "lg"}`);
23
+ // - `icon-sm` and `icon-xs` in the `size` of any JSX element: only Button has
24
+ // those names;
25
+ // - `size: "sm"` in a `buttonVariants({…})` call.
26
+ // `sm` and `xs` are names other components use too (Dialog, Banner, Avatar,
27
+ // Spinner …), so nothing else is touched. It then lists every `sm`, `xs`,
28
+ // `icon-sm` and `icon-xs` string it left, with the element it belongs to, for
29
+ // review by hand: a size kept in a variable, a wrapper you didn't name, a test.
30
+ import { readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
31
+ import { createRequire } from "node:module";
32
+ import path from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+
35
+ let babelParse;
36
+
37
+ /**
38
+ * @babel/parser is an optional peer dependency of raft-ui: only this codemod
39
+ * uses it. Look in the project being migrated first, then beside raft-ui.
40
+ */
41
+ function loadParser() {
42
+ if (babelParse) return babelParse;
43
+ for (const from of [path.join(process.cwd(), "package.json"), import.meta.url]) {
44
+ try {
45
+ babelParse = createRequire(from)("@babel/parser").parse;
46
+ return babelParse;
47
+ } catch {
48
+ // Try the next place.
49
+ }
50
+ }
51
+ throw new Error(
52
+ "install @babel/parser to run this codemod (it parses your TSX): pnpm add -D @babel/parser",
53
+ );
54
+ }
55
+
56
+ /** Deprecated Button size -> the size it renders as. */
57
+ export const BUTTON_SIZE_RENAMES = {
58
+ xs: "md",
59
+ sm: "md",
60
+ "icon-xs": "icon-md",
61
+ "icon-sm": "icon-md",
62
+ };
63
+
64
+ /** raft-ui components whose `size` is a Button size. */
65
+ export const BUTTON_COMPONENTS = [
66
+ "Button",
67
+ "AlertDialogAction",
68
+ "AlertDialogCancel",
69
+ "AlertDialogClose",
70
+ "AnnouncementDialogAction",
71
+ "AnnouncementDialogSecondaryAction",
72
+ "ComposerSubmit",
73
+ "CopyableCodeAction",
74
+ "DialogClose",
75
+ "DrawerClose",
76
+ "FileAction",
77
+ "SidebarSectionControl",
78
+ ];
79
+
80
+ const ICON_ONLY = new Set(["icon-xs", "icon-sm"]);
81
+ const OLD = new Set(Object.keys(BUTTON_SIZE_RENAMES));
82
+
83
+ function parseSource(source, file) {
84
+ const ext = path.extname(file);
85
+ const plugins =
86
+ ext === ".ts" || ext === ".mts" || ext === ".cts" ? ["typescript"] : ["typescript", "jsx"];
87
+ return loadParser()(source, {
88
+ sourceType: "unambiguous",
89
+ plugins,
90
+ errorRecovery: true,
91
+ allowReturnOutsideFunction: true,
92
+ });
93
+ }
94
+
95
+ /** Depth-first walk over a Babel AST, with each node's parent chain. */
96
+ function walk(node, visit, parents = []) {
97
+ if (!node || typeof node.type !== "string") return;
98
+ visit(node, parents);
99
+ const next = [...parents, node];
100
+ for (const key of Object.keys(node)) {
101
+ if (key === "loc" || key === "start" || key === "end" || key === "extra") continue;
102
+ const value = node[key];
103
+ if (Array.isArray(value)) for (const child of value) walk(child, visit, next);
104
+ else if (value && typeof value.type === "string") walk(value, visit, next);
105
+ }
106
+ }
107
+
108
+ function elementName(name) {
109
+ if (!name) return "";
110
+ if (name.type === "JSXIdentifier") return name.name;
111
+ if (name.type === "JSXMemberExpression")
112
+ return `${elementName(name.object)}.${name.property.name}`;
113
+ if (name.type === "JSXNamespacedName") return `${name.namespace.name}:${name.name.name}`;
114
+ return "";
115
+ }
116
+
117
+ /** The string literals a `size` value can resolve to, without entering nested functions or JSX. */
118
+ function sizeStrings(node, out = []) {
119
+ if (!node) return out;
120
+ switch (node.type) {
121
+ case "StringLiteral":
122
+ out.push(node);
123
+ break;
124
+ case "TemplateLiteral":
125
+ if (node.expressions.length === 0 && node.quasis.length === 1) out.push(node);
126
+ break;
127
+ case "JSXExpressionContainer":
128
+ case "ParenthesizedExpression":
129
+ case "TSAsExpression":
130
+ case "TSSatisfiesExpression":
131
+ case "TSNonNullExpression":
132
+ sizeStrings(node.expression, out);
133
+ break;
134
+ case "ConditionalExpression":
135
+ sizeStrings(node.consequent, out);
136
+ sizeStrings(node.alternate, out);
137
+ break;
138
+ case "LogicalExpression":
139
+ sizeStrings(node.left, out);
140
+ sizeStrings(node.right, out);
141
+ break;
142
+ default:
143
+ break;
144
+ }
145
+ return out;
146
+ }
147
+
148
+ const literalValue = (node) =>
149
+ node.type === "StringLiteral" ? node.value : node.quasis[0].value.cooked;
150
+
151
+ function lineOf(source, index) {
152
+ let line = 1;
153
+ for (let i = 0; i < index; i += 1) if (source.charCodeAt(i) === 10) line += 1;
154
+ return line;
155
+ }
156
+
157
+ /**
158
+ * Renames the deprecated sizes in one file. Returns the new source and the
159
+ * strings it left, as `{ line, value, where }`.
160
+ */
161
+ export function migrateButtonSizes(
162
+ source,
163
+ { file = "file.tsx", components = BUTTON_COMPONENTS } = {},
164
+ ) {
165
+ const buttons = new Set(components);
166
+ const ast = parseSource(source, file);
167
+ const edits = new Map();
168
+ const rename = (literal) => {
169
+ const value = literalValue(literal);
170
+ const quote = source[literal.start];
171
+ edits.set(literal.start, {
172
+ end: literal.end,
173
+ text: `${quote}${BUTTON_SIZE_RENAMES[value]}${quote}`,
174
+ });
175
+ };
176
+ const handled = new Set();
177
+
178
+ walk(ast.program, (node) => {
179
+ if (node.type === "JSXOpeningElement") {
180
+ const name = elementName(node.name);
181
+ const isButton = buttons.has(name);
182
+ for (const attribute of node.attributes) {
183
+ if (attribute.type !== "JSXAttribute" || attribute.name.name !== "size") continue;
184
+ for (const literal of sizeStrings(attribute.value)) {
185
+ const value = literalValue(literal);
186
+ if (!OLD.has(value)) continue;
187
+ handled.add(literal.start);
188
+ if (isButton || ICON_ONLY.has(value)) rename(literal);
189
+ }
190
+ }
191
+ }
192
+ if (
193
+ node.type === "CallExpression" &&
194
+ node.callee.type === "Identifier" &&
195
+ node.callee.name === "buttonVariants"
196
+ ) {
197
+ for (const argument of node.arguments) {
198
+ if (argument.type !== "ObjectExpression") continue;
199
+ for (const property of argument.properties) {
200
+ if (property.type !== "ObjectProperty") continue;
201
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.value;
202
+ if (key !== "size") continue;
203
+ for (const literal of sizeStrings(property.value)) {
204
+ if (!OLD.has(literalValue(literal))) continue;
205
+ handled.add(literal.start);
206
+ rename(literal);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ });
212
+
213
+ // Every deprecated name still standing, and what it belongs to.
214
+ const left = [];
215
+ walk(ast.program, (node, parents) => {
216
+ // A test that matches the source (`/size="sm"/`) has to change with it.
217
+ if (node.type === "RegExpLiteral") {
218
+ const name = /["'](icon-)?(sm|xs)["']/.exec(node.pattern);
219
+ if (name) {
220
+ left.push({
221
+ line: lineOf(source, node.start),
222
+ value: name[0].slice(1, -1),
223
+ where: "a regular expression",
224
+ });
225
+ }
226
+ return;
227
+ }
228
+ const isString =
229
+ node.type === "StringLiteral" ||
230
+ (node.type === "TemplateLiteral" &&
231
+ node.expressions.length === 0 &&
232
+ node.quasis.length === 1);
233
+ if (!isString || !OLD.has(literalValue(node)) || edits.has(node.start)) return;
234
+ let where = "a string";
235
+ let prop = "";
236
+ for (let i = parents.length - 1; i >= 0; i -= 1) {
237
+ const parent = parents[i];
238
+ if (parent.type === "JSXAttribute" && !prop) prop = ` ${parent.name.name}`;
239
+ if (parent.type === "JSXOpeningElement") {
240
+ where = `<${elementName(parent.name)}${prop}>`;
241
+ break;
242
+ }
243
+ if (parent.type === "JSXElement" || parent.type === "JSXFragment") break;
244
+ }
245
+ left.push({ line: lineOf(source, node.start), value: literalValue(node), where });
246
+ });
247
+
248
+ let out = source;
249
+ for (const start of [...edits.keys()].sort((a, b) => b - a)) {
250
+ const { end, text } = edits.get(start);
251
+ out = out.slice(0, start) + text + out.slice(end);
252
+ }
253
+ return { source: out, left };
254
+ }
255
+
256
+ /** The source with the deprecated sizes renamed (see migrateButtonSizes). */
257
+ export function renameButtonSizes(source, components = BUTTON_COMPONENTS, file = "file.tsx") {
258
+ return migrateButtonSizes(source, { file, components }).source;
259
+ }
260
+
261
+ const EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
262
+ const SKIP = new Set(["node_modules", "dist", "build", ".git"]);
263
+
264
+ function* walkFiles(target) {
265
+ const stat = statSync(target);
266
+ if (stat.isFile()) {
267
+ if (EXTENSIONS.has(path.extname(target))) yield target;
268
+ return;
269
+ }
270
+ for (const name of readdirSync(target)) {
271
+ if (SKIP.has(name) || name.startsWith(".")) continue;
272
+ yield* walkFiles(path.join(target, name));
273
+ }
274
+ }
275
+
276
+ function main(argv) {
277
+ const dry = argv.includes("--dry");
278
+ const option = argv.find((arg) => arg.startsWith("--components="));
279
+ const components = option
280
+ ? [...BUTTON_COMPONENTS, ...option.slice("--components=".length).split(",").filter(Boolean)]
281
+ : BUTTON_COMPONENTS;
282
+ const targets = argv.filter((arg) => !arg.startsWith("--"));
283
+ if (targets.length === 0) {
284
+ console.error("usage: button-sizes.mjs <dir-or-file>... [--dry] [--components=MyButton,…]");
285
+ process.exit(1);
286
+ }
287
+ let changed = 0;
288
+ const review = [];
289
+ try {
290
+ loadParser();
291
+ } catch (error) {
292
+ console.error(error.message);
293
+ process.exit(1);
294
+ }
295
+ const groups = new Map();
296
+ for (const target of targets) {
297
+ for (const file of walkFiles(target)) {
298
+ const before = readFileSync(file, "utf8");
299
+ if (!/["'`](?:icon-)?(?:sm|xs)["'`]/.test(before)) continue;
300
+ let result;
301
+ try {
302
+ result = migrateButtonSizes(before, { file, components });
303
+ } catch (error) {
304
+ review.push(`${file}: could not parse (${error.message}); not changed`);
305
+ continue;
306
+ }
307
+ for (const { line, value, where } of result.left) {
308
+ review.push(`${file}:${line} "${value}" in ${where}`);
309
+ groups.set(where, (groups.get(where) ?? 0) + 1);
310
+ }
311
+ if (result.source === before) continue;
312
+ changed += 1;
313
+ console.log(`${dry ? "would update" : "updated"} ${file}`);
314
+ if (!dry) writeFileSync(file, result.source);
315
+ }
316
+ }
317
+ console.log(`${changed} file${changed === 1 ? "" : "s"} ${dry ? "to update" : "updated"}`);
318
+ if (review.length > 0) {
319
+ console.log(
320
+ "\nLeft as they are; check each by hand (another component's size, a variable, your own wrapper).",
321
+ );
322
+ console.log("\nBy element and prop:");
323
+ for (const [where, count] of [...groups].sort((a, b) => b[1] - a[1])) {
324
+ console.log(` ${String(count).padStart(4)} ${where}`);
325
+ }
326
+ console.log("\nEach one:");
327
+ for (const where of review) console.log(` ${where}`);
328
+ }
329
+ }
330
+
331
+ // Run only when invoked directly (see token-names.mjs: pnpm installs through
332
+ // symlinks, so compare real paths).
333
+ function isInvokedDirectly() {
334
+ try {
335
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
336
+ } catch {
337
+ return false;
338
+ }
339
+ }
340
+ if (isInvokedDirectly()) main(process.argv.slice(2));
@@ -32,6 +32,12 @@ export const TOKEN_RENAMES = {
32
32
  "theme-shadow-sticker": "elevation-sticker",
33
33
  "theme-focus-ring": "focus-ring",
34
34
  "soft-signal": "palette-yellow",
35
+ "fill-pressed": "fill-medium",
36
+ "fill-frame-pressed": "fill-frame-medium",
37
+ "fill-row-hover": "fill-subtle",
38
+ "fill-row-open": "fill-subtle",
39
+ "fill-message-hover": "fill-subtle",
40
+ "sticker-pressed": "sticker-medium",
35
41
  };
36
42
 
37
43
  /** Families renamed with every step: `primary-soft` -> `brand-soft`. */
@@ -61,6 +67,10 @@ export const UTILITY_RENAMES = {
61
67
  "shadow-raft-xl": "shadow-float",
62
68
  "shadow-raft-sticker": "shadow-sticker",
63
69
  "shadow-raft-focus": "shadow-focus",
70
+ "press-overlay": "overlay-medium",
71
+ "press-overlay-sticker": "overlay-sticker-medium",
72
+ "row-hover-overlay": "overlay-subtle",
73
+ "row-open-overlay": "overlay-subtle",
64
74
  };
65
75
 
66
76
  const COLOR_UTILITY =
@@ -83,6 +93,8 @@ function rules() {
83
93
  for (const [from, to] of tokens) {
84
94
  list.push([new RegExp(`(?<![\\w-])(${COLOR_UTILITY})-${escape(from)}${END}`, "g"), `$1-${to}`]);
85
95
  list.push([new RegExp(`${VAR}${escape(from)}${REF_END}`, "g"), `--${to}`]);
96
+ // The Tailwind theme variable of the same token: `var(--color-fill-pressed)`.
97
+ list.push([new RegExp(`${VAR}color-${escape(from)}${REF_END}`, "g"), `--color-${to}`]);
86
98
  }
87
99
  for (const [from, to] of Object.entries(FAMILY_RENAMES)) {
88
100
  list.push([
@@ -90,6 +102,10 @@ function rules() {
90
102
  `$1-${to}$2`,
91
103
  ]);
92
104
  list.push([new RegExp(`${VAR}${from}((?:-[a-z0-9]+)*)${REF_END}`, "g"), `--${to}$1`]);
105
+ list.push([
106
+ new RegExp(`${VAR}color-${from}((?:-[a-z0-9]+)*)${REF_END}`, "g"),
107
+ `--color-${to}$1`,
108
+ ]);
93
109
  }
94
110
  for (const [from, to] of Object.entries(PALETTE_COLORS)) {
95
111
  list.push([