@botiverse/raft-ui 0.20.0 → 0.21.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 +157 -0
- package/codemods/button-sizes.mjs +340 -0
- package/dist/cn.mjs +8 -1
- package/dist/foundation.css +13 -0
- package/dist/{touch-drag-BA_Fxs2Z.mjs → inbox-item.recipe-DelZxRmY.mjs} +525 -32
- package/dist/{index-RFu5pOr7.d.mts → index-jTmbN7ov.d.mts} +95 -3
- package/dist/index.d.mts +2 -44
- package/dist/index.mjs +45 -291
- package/dist/styles.css +67 -0
- package/dist/tokens.json +24 -0
- package/dist/wip.d.mts +112 -3
- package/dist/wip.mjs +370 -3
- package/package.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,160 @@
|
|
|
1
|
+
## [0.21.1](https://github.com/botiverse/raft-design/compare/v0.21.0...v0.21.1) (2026-10-22)
|
|
2
|
+
|
|
3
|
+
### Design System
|
|
4
|
+
|
|
5
|
+
- **Chips**: the ink at a chip's two ends sits the same distance from its fill's edges. A chip keeps its total padding and its width, and splits the padding between its ends so each end makes up for the empty space its own content brings. A smaller note after a chip's label centers on the label's letters, and the label stays on the sentence's baseline. It applies to MessageReferenceChip, MessageReferencePending, MessageReferenceUnavailable, MessageReactionChip, MessageItemSavedIndicator, MessageForwardedBundleAttachment, TaskChip and TaskCardStatus, and to the new MessageReferenceSuffix. ([#700](https://github.com/botiverse/raft-design/pull/700))
|
|
6
|
+
- Why: the chips split their padding as if every end were the same, so an icon or a status pixel sat closer to its edge than the last letter did to the other (3.6px against 7px in a reference chip), and the end after the text looked empty; each chip's padding is now set from where its ink is drawn.
|
|
7
|
+
- Affects: the padding at each end, left / right, totals unchanged; see Fixed below.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#chips
|
|
9
|
+
- **A control and its label**: a settings row is the exception to the first-line rule. The leading icon, avatar or tile and the trailing switch, checkbox, radio, value or chevron of a `ListRow variant="plain"` center on the whole row, with or without a description, as they did in 0.20. A checkbox or radio beside wrapping text in a `<label>` row still sits on that text's first line. It applies to ListRow. ([#703](https://github.com/botiverse/raft-design/pull/703))
|
|
10
|
+
- Why: a setting reads as one unit, and with its controls on the title's first line they belonged to the title instead of the row; settings rows center them on the row again.
|
|
11
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
|
|
12
|
+
- **Timeline rhythm: the edge that messages scroll under fades**: the bottom edge of a message list, over the composer, fades from the panel's color to transparent over the section step, at all times, instead of cutting the message under it. The line of who is about to post sits on the same fade and has none of its own. It applies to the new MessageListEndFade. ([#699](https://github.com/botiverse/raft-design/pull/699))
|
|
13
|
+
- Why: a message scrolled under the composer was cut off by a hard edge; the edge now fades, as a tab bar fades on the side that hides tabs.
|
|
14
|
+
- Affects: fade height `timeline-section` 24px, `layer-canvas` to transparent.
|
|
15
|
+
- Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **MessageReferenceSuffix**: a short note after a reference chip's label, in muted Space Mono, such as `msg` on a link to a message. Put it inside `MessageReferenceChip`, after `MessageReferenceLabel`. ([#700](https://github.com/botiverse/raft-design/pull/700))
|
|
20
|
+
- Why: a note in a span of the caller's own sat on the label's baseline, 0.3px low beside a 12.25px label and 0.75px low beside a 14px one, and its descenders made it look lower still; the part keeps the label's baseline in the layout and draws the note a quarter of the size difference higher, so the middles of the two x-heights meet.
|
|
21
|
+
- Affects: `text-label` (11px) in `foreground-hint`, line height 1; drawn `(label size - 1em) / 4` higher.
|
|
22
|
+
- Docs: https://raft-design.botiverse.build/components/message-item/
|
|
23
|
+
- **MessageListEndFade**: the fade over a message list's bottom edge. Put it beside `MessageList`, in the same positioned parent (`ConversationPanelBody` is one), like the jump controls; it lies under them, is hidden from assistive technology and takes no pointer events. ([#699](https://github.com/botiverse/raft-design/pull/699))
|
|
24
|
+
- Why: see Timeline rhythm: the edge that messages scroll under fades, above.
|
|
25
|
+
- Affects: 24px tall (`h-timeline-section`), `layer-canvas` to transparent upward, `z-index` 5.
|
|
26
|
+
- Docs: https://raft-design.botiverse.build/components/message-list/
|
|
27
|
+
- **SegmentedControl** `variant="on-frame"`, for a control in the frame (a top bar). `variant="default"` is unchanged and stays the default. ([#693](https://github.com/botiverse/raft-design/pull/693))
|
|
28
|
+
- Why: a yellow sticker on the yellow frame did not read as selected, and a white segment beside it looked chosen instead; in the frame the selected segment takes the frame's active colors, as a selected rail item does.
|
|
29
|
+
- Affects: track `fill-frame-medium`; other segments `foreground-on-frame-muted`, with `foreground-on-frame` on hover and `overlay-frame-medium` on hover and press; the selected segment `layer-frame-active` with a `line-frame-active` edge and `foreground-on-frame-active` text (white on the yellow frame, yellow on the dark one).
|
|
30
|
+
- Docs: https://raft-design.botiverse.build/components/segmented-control/
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **ListRow** `variant="plain"` with a description: the leading icon, avatar or tile and the trailing switch, checkbox, radio, value or chevron center on the whole row again, as in 0.20, instead of on the title's first line. With a title alone, nothing changes. This reverts the 0.21.0 change to ListRow. ([#703](https://github.com/botiverse/raft-design/pull/703))
|
|
35
|
+
- Why: see A control and its label above.
|
|
36
|
+
- Affects: the ends move without changing size, so row heights are unchanged (54px on a phone for a one-line row with a switch).
|
|
37
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **MessageReferenceChip**, **MessageReferencePending**, **MessageReferenceUnavailable**, **MessageReactionChip**, **MessageItemSavedIndicator**, **MessageForwardedBundleAttachment**, **TaskChip** and **TaskCardStatus**: the ink at both ends sits the same distance from the edges; each chip keeps its total padding and its width. ([#700](https://github.com/botiverse/raft-design/pull/700))
|
|
42
|
+
- Why: see Chips above.
|
|
43
|
+
- Affects: padding left / right: MessageReferenceChip, MessageReferencePending and MessageReferenceUnavailable with an icon 3px / 6px → 4.5px / 4.5px (without an icon unchanged, 6px / 6px); MessageReactionChip 6px / 8px → 7px / 7px; MessageItemSavedIndicator and MessageForwardedBundleAttachment 6px / 6px → 5.5px / 6.5px (their bookmark and file icons are drawn inside their boxes); TaskChip 6px / 8px → 7px / 7px, `size="sm"` 4px / 6px → 5px / 5px and `variant="inline"` 3px / 6px → 4.5px / 4.5px; TaskCardStatus 6px / 8px → 7px / 7px.
|
|
44
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#chips
|
|
45
|
+
- **MessageReferencePending**, and any reference chip, holding a `Spinner`: the spinner centers on the middle of the label's line, as an icon does. ([#700](https://github.com/botiverse/raft-design/pull/700))
|
|
46
|
+
- Why: the chip centered only `svg` icons on the label's line, and a spinner is not one, so it sat on the baseline, 5–6px below the middle of the label's line.
|
|
47
|
+
- Docs: https://raft-design.botiverse.build/components/message-item/
|
|
48
|
+
- **MessageQuotedPreviewChannel** and **MessageQuotedPreviewThread**: the icon sits on the first line of the name when a long name wraps, instead of in the middle of both lines. ([#700](https://github.com/botiverse/raft-design/pull/700))
|
|
49
|
+
- Why: the icon was centered on the whole name, so it slid between the lines of a wrapped name; it now follows A mark that leads text, like the other components.
|
|
50
|
+
- Affects: the icon (14px) is placed `(1lh - 14px) / 2` from the top of the name's first line.
|
|
51
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-mark-that-leads-text
|
|
52
|
+
|
|
53
|
+
## [0.21.0](https://github.com/botiverse/raft-design/compare/v0.20.1...v0.21.0) (2026-10-22)
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
### Design System
|
|
58
|
+
|
|
59
|
+
- **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))
|
|
60
|
+
- 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.
|
|
61
|
+
- 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).
|
|
62
|
+
- Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
|
|
63
|
+
- **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))
|
|
64
|
+
- 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.
|
|
65
|
+
- Affects: switch 50 × 30px and checkbox and radio 20px, unchanged; 8px from control to label.
|
|
66
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
|
|
67
|
+
|
|
68
|
+
### Breaking Changes
|
|
69
|
+
|
|
70
|
+
- **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))
|
|
71
|
+
- 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.
|
|
72
|
+
- Affects: footer top padding 12px → 0; `pb-timeline-end` is 20px (the 24px section step less the last row's 4px inset).
|
|
73
|
+
- 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.
|
|
74
|
+
- Docs: https://raft-design.botiverse.build/components/conversation-panel/
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
- **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))
|
|
79
|
+
- 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.
|
|
80
|
+
- 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.
|
|
81
|
+
- Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
|
|
82
|
+
- **`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))
|
|
83
|
+
- 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.
|
|
84
|
+
- 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.
|
|
85
|
+
- Docs: https://raft-design.botiverse.build/components/button/#sizes
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **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))
|
|
90
|
+
- 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.
|
|
91
|
+
- 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.
|
|
92
|
+
- Docs: https://raft-design.botiverse.build/components/button/#size-by-place
|
|
93
|
+
- **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))
|
|
94
|
+
- 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.
|
|
95
|
+
- 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`.
|
|
96
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
|
|
97
|
+
- **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))
|
|
98
|
+
- 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.
|
|
99
|
+
- 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.
|
|
100
|
+
- Docs: https://raft-design.botiverse.build/components/description-list/
|
|
101
|
+
- **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))
|
|
102
|
+
- Why: with a description the ends sat between the title and the description, level with neither and with no line of text.
|
|
103
|
+
- Affects: the ends move without changing size, so row heights are unchanged (54px on a phone for a one-line row with a switch).
|
|
104
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/
|
|
105
|
+
- **MessageListDateDivider**, **SystemMessage**, **SystemMessageGroupHeader**, **MessageListNotice**, **ThreadPanelOriginalMessage**, **ThreadPanelReplySummary** and **ThreadPanelReplyCount**: spaced on the timeline rhythm. ([#694](https://github.com/botiverse/raft-design/pull/694))
|
|
106
|
+
- Why: see Timeline rhythm above.
|
|
107
|
+
- 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.
|
|
108
|
+
- Docs: https://raft-design.botiverse.build/foundations/spacing/#timeline-rhythm
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **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))
|
|
113
|
+
- 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.
|
|
114
|
+
- Affects: tolerance 1px (whole pixels) → 0.5px (fractional).
|
|
115
|
+
- Docs: https://raft-design.botiverse.build/components/select/
|
|
116
|
+
- **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))
|
|
117
|
+
- 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.
|
|
118
|
+
- 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.
|
|
119
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
|
|
120
|
+
|
|
121
|
+
## [0.20.1](https://github.com/botiverse/raft-design/compare/v0.20.0...v0.20.1) (2026-10-22)
|
|
122
|
+
|
|
123
|
+
### Design System
|
|
124
|
+
|
|
125
|
+
- **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))
|
|
126
|
+
- 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.
|
|
127
|
+
- Affects: offset `(1lh - size) / 2` on the row's own type; status light 9px with a 1.5px ink edge.
|
|
128
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-mark-that-leads-text
|
|
129
|
+
- **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))
|
|
130
|
+
- 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.
|
|
131
|
+
- Docs: https://raft-design.botiverse.build/foundations/elevation/#popups
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- **`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))
|
|
136
|
+
- Why: a mark placed outside the components above had no way to follow the same rule except a hand-tuned margin.
|
|
137
|
+
- 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.
|
|
138
|
+
- Docs: https://raft-design.botiverse.build/foundations/alignment/#a-mark-that-leads-text
|
|
139
|
+
|
|
140
|
+
### Changed
|
|
141
|
+
|
|
142
|
+
- **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))
|
|
143
|
+
- 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.
|
|
144
|
+
- 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.
|
|
145
|
+
- Docs: https://raft-design.botiverse.build/components/status/#sizes
|
|
146
|
+
|
|
147
|
+
### Fixed
|
|
148
|
+
|
|
149
|
+
- **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))
|
|
150
|
+
- 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.
|
|
151
|
+
- Affects: icon size and gap are unchanged (your icon's size, 10px before the text).
|
|
152
|
+
- Docs: https://raft-design.botiverse.build/components/panel/
|
|
153
|
+
- **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))
|
|
154
|
+
- 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.
|
|
155
|
+
- 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`.
|
|
156
|
+
- Docs: https://raft-design.botiverse.build/foundations/elevation/#popups
|
|
157
|
+
|
|
1
158
|
## [0.20.0](https://github.com/botiverse/raft-design/compare/v0.19.0...v0.20.0) (2026-10-22)
|
|
2
159
|
|
|
3
160
|
### Design System
|
|
@@ -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));
|
package/dist/cn.mjs
CHANGED
|
@@ -11,7 +11,14 @@ const twMerge = extendTailwindMerge({ extend: {
|
|
|
11
11
|
"embedded-inset",
|
|
12
12
|
"option",
|
|
13
13
|
"option-inset",
|
|
14
|
-
"icon-inset"
|
|
14
|
+
"icon-inset",
|
|
15
|
+
"timeline-message",
|
|
16
|
+
"timeline-section",
|
|
17
|
+
"timeline-row",
|
|
18
|
+
"timeline-turn",
|
|
19
|
+
"timeline-lead",
|
|
20
|
+
"timeline-end",
|
|
21
|
+
"timeline-start"
|
|
15
22
|
] },
|
|
16
23
|
classGroups: {
|
|
17
24
|
shadow: [{ shadow: [
|
package/dist/foundation.css
CHANGED
|
@@ -310,6 +310,19 @@
|
|
|
310
310
|
/* A chip or a button embedded in a field: the control height less the embedded
|
|
311
311
|
inset above and below, so the field keeps its height. */
|
|
312
312
|
--control-embedded-size: 32px;
|
|
313
|
+
/* The message step of a timeline: from the last line of one turn to the first
|
|
314
|
+
line of the next (a sender line, an event line). Every row keeps the row
|
|
315
|
+
inset above and below its text; the first row of a turn adds the rest above
|
|
316
|
+
it. */
|
|
317
|
+
--timeline-gap-message: 16px;
|
|
318
|
+
/* The section step of a timeline, one step wider than the message step: either
|
|
319
|
+
side of a date line, from the top of the list to the first line, and from
|
|
320
|
+
the last line to the composer, so a boundary reads as one and the last
|
|
321
|
+
message never sits on the composer. */
|
|
322
|
+
--timeline-gap-section: 24px;
|
|
323
|
+
/* How far a timeline row's hover and selected highlight reaches above and
|
|
324
|
+
below its text. Two rows of one turn sit two insets apart. */
|
|
325
|
+
--timeline-row-inset: 4px;
|
|
313
326
|
--sticker-ink: oklch(0.1813 0.005 17.62);
|
|
314
327
|
--sticker-paper: oklch(1 0 0);
|
|
315
328
|
--sticker-edge: oklch(0.1813 0.005 17.62 / 0);
|