@botiverse/raft-ui 0.83.0 → 0.85.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/cn.d.mts +2 -0
  3. package/dist/cn.mjs +21 -1
  4. package/dist/components/alert-dialog/alert-dialog.mjs +2 -2
  5. package/dist/components/app-link-card/app-link-card.mjs +19 -37
  6. package/dist/components/app-shell/app-shell.mjs +2 -2
  7. package/dist/components/avatar/avatar.mjs +1 -1
  8. package/dist/components/banner/banner.mjs +15 -2
  9. package/dist/components/button/button-split.mjs +2 -2
  10. package/dist/components/button/button.mjs +2 -2
  11. package/dist/components/choice-list/choice-list.recipe.mjs +1 -1
  12. package/dist/components/code-block/code-block.recipe.mjs +2 -2
  13. package/dist/components/combobox/combobox.mjs +1 -1
  14. package/dist/components/combobox-select/combobox-select.css +1 -1
  15. package/dist/components/comment/comment.recipe.mjs +2 -2
  16. package/dist/components/composer/composer-attachment.recipe.mjs +5 -5
  17. package/dist/components/composer/composer-drop-overlay.mjs +2 -2
  18. package/dist/components/composer/composer-drop-overlay.recipe.mjs +1 -1
  19. package/dist/components/composer/sortable-composer-attachments.mjs +1 -1
  20. package/dist/components/context-menu/context-menu.mjs +1 -1
  21. package/dist/components/conversation-panel/conversation-panel-empty-state.mjs +1 -1
  22. package/dist/components/dialog/dialog.mjs +4 -4
  23. package/dist/components/drag/drag-lift.d.mts +3 -2
  24. package/dist/components/drag/drag-lift.mjs +3 -2
  25. package/dist/components/drag/drag.recipe.mjs +1 -1
  26. package/dist/components/drawer/drawer.recipe.mjs +4 -4
  27. package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
  28. package/dist/components/file-preview/file-preview.recipe.mjs +1 -1
  29. package/dist/components/fit-toolbar/fit-toolbar.d.mts +63 -8
  30. package/dist/components/fit-toolbar/fit-toolbar.mjs +310 -43
  31. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -7
  32. package/dist/components/fit-toolbar/index.d.mts +2 -2
  33. package/dist/components/inbox-item/inbox-item.recipe.mjs +7 -7
  34. package/dist/components/lightbox/index.d.mts +2 -2
  35. package/dist/components/lightbox/lightbox.d.mts +70 -4
  36. package/dist/components/lightbox/lightbox.mjs +285 -91
  37. package/dist/components/list-row/list-row.d.mts +14 -1
  38. package/dist/components/list-row/list-row.mjs +4 -2
  39. package/dist/components/list-row/list-row.recipe.d.mts +12 -0
  40. package/dist/components/list-row/list-row.recipe.mjs +19 -7
  41. package/dist/components/message-action-card/message-action-card-content.recipe.mjs +3 -3
  42. package/dist/components/message-action-card/message-action-card.mjs +7 -3
  43. package/dist/components/message-action-card/message-action-card.recipe.mjs +2 -6
  44. package/dist/components/message-attachment/message-image-gallery.recipe.mjs +1 -1
  45. package/dist/components/message-embed/index.d.mts +2 -1
  46. package/dist/components/message-embed/message-embed.d.mts +8 -1
  47. package/dist/components/message-embed/message-embed.mjs +2 -2
  48. package/dist/components/message-embed/message-embed.recipe.d.mts +160 -0
  49. package/dist/components/message-embed/message-embed.recipe.mjs +49 -10
  50. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.mjs +1 -0
  51. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.recipe.mjs +1 -1
  52. package/dist/components/message-item/message-item.recipe.mjs +2 -2
  53. package/dist/components/message-list/message-list.recipe.mjs +2 -2
  54. package/dist/components/message-quoted-preview/message-quoted-preview.mjs +2 -0
  55. package/dist/components/message-replies/message-replies.mjs +1 -1
  56. package/dist/components/message-replies/message-replies.recipe.mjs +1 -1
  57. package/dist/components/mobile-nav/mobile-nav.mjs +2 -2
  58. package/dist/components/notification-center/notification-center.mjs +1 -1
  59. package/dist/components/panel/panel-header.recipe.mjs +1 -1
  60. package/dist/components/panel/panel-split-action.recipe.mjs +2 -2
  61. package/dist/components/popover/popover.d.mts +11 -2
  62. package/dist/components/popover/popover.mjs +51 -38
  63. package/dist/components/preview-card/preview-card.d.mts +5 -1
  64. package/dist/components/preview-card/preview-card.mjs +25 -28
  65. package/dist/components/pull-to-refresh/pull-to-refresh-indicator.mjs +1 -1
  66. package/dist/components/resizable/resizable.recipe.mjs +1 -1
  67. package/dist/components/save-status/save-status.d.mts +16 -9
  68. package/dist/components/save-status/save-status.mjs +19 -30
  69. package/dist/components/scroll-area/scroll-area.css +1 -0
  70. package/dist/components/segmented-control/segmented-control.mjs +45 -8
  71. package/dist/components/select/select.mjs +2 -2
  72. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +1 -1
  73. package/dist/components/sidebar/sidebar.recipe.mjs +1 -1
  74. package/dist/components/tabs/tabs.mjs +4 -4
  75. package/dist/components/task-board/task-board.d.mts +5 -0
  76. package/dist/components/task-board/task-board.mjs +8 -1
  77. package/dist/components/task-dnd/task-dnd.d.mts +10 -1
  78. package/dist/components/task-dnd/task-dnd.mjs +2 -1
  79. package/dist/components/task-dnd/task-dnd.recipe.mjs +2 -2
  80. package/dist/components/thread-panel/thread-panel.recipe.mjs +1 -1
  81. package/dist/components/toast/toast.mjs +5 -5
  82. package/dist/components/tooltip/tooltip.mjs +2 -2
  83. package/dist/index.d.mts +2 -1
  84. package/dist/index.mjs +2 -1
  85. package/dist/lib/laid-out-children.mjs +32 -0
  86. package/dist/lib/list-cell.mjs +1 -1
  87. package/dist/lib/stacked-actions.mjs +7 -4
  88. package/dist/lint/no-raw-z-index.mjs +142 -0
  89. package/dist/lint/prefer-scroll-area.mjs +1 -1
  90. package/dist/lint/recommended.json +2 -1
  91. package/dist/lint.d.mts +8 -2
  92. package/dist/lint.mjs +16 -3
  93. package/dist/styles.css +28 -0
  94. package/dist/wip.d.mts +4 -2
  95. package/dist/wip.mjs +3 -2
  96. package/package.json +1 -1
  97. package/skills/raft-ui-guide/SKILL.md +1 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,128 @@
1
+ ## [0.85.0](https://github.com/botiverse/raft-design/compare/v0.84.0...v0.85.0) (2026-11-10)
2
+
3
+ ### Added
4
+
5
+ - **Named layers, `z-below` to `z-drag`, and the lint rule `raft-ui/no-raw-z-index`**: every z-index has a name and a place in one order ([Layer order](https://raft-design.botiverse.build/foundations/elevation/#layer-order)). The names are ordinary Tailwind `z-*` utilities from `--z-index-*` theme variables in `styles.css` (`@theme static`, so CSS can always read `var(--z-index-sticky)`): in the page `z-below` -1, `z-base` 0, `z-raised` 1, `z-control` 10, `z-sticky` 20, `z-float` 30, `z-chrome` 35, `z-cover` 40, `z-app` 45, `z-scrollbar` 49; overlays `z-popup` and `z-dialog` 50, `z-dialog-layer` 60, `z-lightbox` 70, `z-toast` 80, `z-drag` 999. They take every variant (`max-md:z-chrome`, `before:z-below`), and `cn` and the recipes merge them with a number an app passes (`z-popup` and `z-40` keep `z-40`). raft-ui's own components use them. The new rule reports a z class with a number or an arbitrary value (`z-10`, `-z-1`, `z-[60]`), a numeric `zIndex` in a style object, a `zIndex={n}` prop and `el.style.zIndex = n`; a number kept on purpose takes a `// raft-ui: z-index <reason>` comment on the line above. `z-index-named.test.ts` holds raft-ui's own code to the same rule, CSS included, with its kept exceptions listed.
6
+ - Why: slock alone writes 186 z-index values, and the same role has up to five numbers (sticky headers at 2, 10, 15, 20 and 30) while one number has up to seven roles; a share lightbox at 100 hides its own toast, and a badge menu at 1000 sits above the drag lift (#proj-raft-ui:#218, step 1, approved by Artea).
7
+ - Affects: nothing on the upgrade for an app that extends `recommended.json`: the rule is `"off"` in it for now, so an app that lints with `--max-warnings=0` (slock) stays green. It turns on there as `"error"` once slock has moved to the names (#218 step 2). An app can opt in early with `"raft-ui/no-raw-z-index": "warn"` or `"error"` in its own `rules`. Map each value with the table in Elevation: a local lift is `z-raised` inside an `isolate` root (there is no third local level: order a third part by DOM order); a popup, menu, dialog, lightbox, toast or drag copy takes raft-ui's component and no z-index of its own. Nothing may sit above the toasts (80) or the drag lift (999). slock's InlineBadgeEditor (1000) and SelectShareLightbox (100) are not exceptions: they move onto `Popover` and `z-lightbox` in step 2.
8
+ - Docs: https://raft-design.botiverse.build/foundations/elevation/#layer-order, https://raft-design.botiverse.build/about/use-raft-ui/#lint-rules
9
+
10
+ ### Changed
11
+
12
+ - **raft-ui's in-page layers take the scale's values**: where a component's z-index moved to a name with another number, it renders the same inside the component, but its number against an app's own layers changes. Overlays keep theirs (50, 60, 70, 80, 999).
13
+ - Raised parts 10 → 1 (`z-raised`): Button's and Toast's label and loading indicator, Avatar's badge, InboxItem's lead, badge, selection, text, trail and corner (its trigger is `z-base`, 0), ChoiceList's control column, Comment's cover and active comment, CodeBlock's actions, ComposerAttachment's veils, FilePreview's corner badge, MessageImageGallery's "+N" veil, MessageItem's trailing meta, MobileNav's icons, Select's scroll arrows, Tabs' tab content and drop placeholder, Tooltip's kbd, Lightbox's arrows, info dock and counter, ConversationPanelEmptyState's illustration. 20 → 1: CodeBlock's corners, ComposerAttachment's progress bar (over the veil by DOM order), ComposerDropOverlay's second icon (and its first, 10 → 1), Lightbox's close, a dragged TaskDraggable. MessageList's end fade 5 → 1.
14
+ - Up: MessageList's jump controls and SelectionToolbar 10 → 30 (`z-float`), the pull-to-refresh indicator 10 → 20 (`z-sticky`), PanelHeader's sticky phone header 30 → 35 (`z-chrome`), ThreadPanel's header 20 → 35 (inside the panel's `isolate`), AppShellOverlaySlot 20 → 40 (`z-cover`).
15
+ - Down: MessageItem's hover toolbar 20 → 10 and ComposerAttachment's remove button 20 → 10 (`z-control`), the composer attachment row's arrows 30 → 10 (outside the row's ScrollArea, so still over it). The sidebar header mask -10 → -1 (`z-below`).
16
+ - Isolation: a tab is now `isolate` instead of `z-10`, and a dragged tab is lifted by `var(--z-index-raised)` instead of 20, all inside the tab list's `isolate`. TaskDraggable is `isolate`, so a card's own parts rank only inside the card, and a dragged card at `z-raised` is above every other card.
17
+ - Why: the scale has one number per role; the old numbers made the same role differ between components and forced slock to workarounds (a day heading at `z-15`, an `isolate` around the artifacts dashboard, `max-md:z-40` beside the phone header) (#proj-raft-ui:#218).
18
+ - Affects: an app layer that sat between the old and the new number of a raft-ui layer it overlaps. In slock, move these in the same upgrade: a cover at `z-30` beside a PanelHeader (MainLayout, ProfilePanel, ChannelSideArea, LegacyTaskPanel, TaskConversationDialog, `index.css`) to `z-cover`, or the header, now 35, is drawn over it; the typing line (`z-6`) to `z-float`, so it stays over the end fade and beside the jump pills; a hit target at `z-10` in a message (MessageItem) to `z-raised`, so the hover toolbar, now 10, stays above it; ThreadsInbox's day heading (`z-15`) and "new" pill (`z-20`) to `z-sticky` and `z-float`, which InboxItem's parts at 1 now allow.
19
+ - Docs: https://raft-design.botiverse.build/foundations/elevation/#in-page-layers
20
+
21
+ ## [0.84.0](https://github.com/botiverse/raft-design/compare/v0.83.0...v0.84.0) (2026-11-10)
22
+
23
+ ### Added
24
+
25
+ - **LightboxBody, the scrolling body of a document-mode lightbox** (from `@botiverse/raft-ui/wip`): a ScrollArea under `LightboxToolbar` that fills the rest of the content (`min-h-0 flex-1`), takes the pointer back from `LightboxContent` (`pointer-events-auto`), and never passes a scroll on to the page under the scrim (`overscroll-contain`), on `layer-canvas`, the toolbar's surface. `className` sets another surface (`bg-layer-sidebar`); every other ScrollArea prop, `render` included, is the area's, except `overscroll`. It carries `data-slot="lightbox-body"`. It is a tab stop even when it has nothing to scroll (`tabIndex={0}`), with the inset focus ring (a 2px `foreground` outline inside its edge, after keyboard input only) instead of ScrollArea's ring outside it, which the window's edges hid. Its content is padded by `env(safe-area-inset-left|right|bottom)`, and `contentClassName` and `contentStyle` style a box of the caller's inside that (`data-slot="lightbox-body-content"`), so a phone held sideways doesn't put a wide file under the notch or its end under the home bar, and the scrollbar stays at the window's edge.
26
+ - Why: slock's FileViewerShell and SelectShareLightbox each write a lightbox's scrolling body of their own (Kirby's review of #1130, #proj-raft-ui:#226). A ScrollArea stops a scroll only while it has something to scroll, so a body too short to scroll passed the wheel on to the page under the scrim, which document mode doesn't lock.
27
+ - Affects: nothing until a lightbox uses it. The document-mode example on the Lightbox page now does, with the same DOM and classes as before apart from `data-slot`, `overscroll-contain`, `tabIndex` and the focus ring, and the safe-area box its `contentClassName` now sits in.
28
+ - Docs: https://raft-design.botiverse.build/components/lightbox/#document-mode
29
+ - **Lightbox document mode.** `<Lightbox modal="trap-focus">` traps focus without locking the page's scroll, for a file read top to bottom whose own body scrolls (a PDF, Markdown, a diagram); the content stays neither `inert` nor hidden, so find in page reaches it. In document mode `LightboxContent` takes Base UI's own focus defaults: focus moves into the lightbox when it opens and back to `LightboxTrigger` when it closes. A lightbox opened from state passes `finalFocus={openerRef}` to send focus back to what opened it.
30
+ - Why: slock's FileViewerShell (images and attachment previews: PDF, documents, Mermaid) and SelectShareLightbox move onto raft-ui's Lightbox, and they kept their own body-scroll switch and focus return (#triage:#278, #proj-raft-ui:#226).
31
+ - Affects: nothing changes for a lightbox until it passes `modal="trap-focus"`; `modal` keeps its default, `true`, and in it `initialFocus` and `finalFocus` stay `false`.
32
+ - Docs: https://raft-design.botiverse.build/components/lightbox/#document-mode
33
+ - **`attached` on ListRow: a row that belongs to the setting above it**: in a `ListRowGroup`, an `attached` row draws no hairline above it, and a `plain` one has no top padding, so it follows the row it belongs to with only that row's 12px bottom inset between them. An interactive one (a chevron, a menu) keeps the control height as its least, so it stays a 44px target on a phone and a touch screen (34px with a mouse on a desktop, which it already is). The row carries `data-attached`. Tie it to its setting for assistive technology with the setting's title id: a display-only attached row takes `role="group"` and `aria-labelledby`, an interactive one `aria-describedby`; ListRow can't know which title it belongs to. The row above keeps its height, and its switch stays centered on its own title and description; the row after draws its hairline as usual.
34
+ - Why: a hairline says "another setting", so quiet hours' start and end times, or a server's channel overrides under its level, read as settings of their own (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e).
35
+ - Affects: nothing until a row passes `attached`: without it, `ListRow` renders the same DOM and classes as before. slock's quiet hours times, its channel overrides and the message font size preview should take it.
36
+ - Docs: https://raft-design.botiverse.build/components/list-row/#a-row-that-belongs-to-the-setting-above, https://raft-design.botiverse.build/patterns/forms/#a-settings-row
37
+ - **FitToolbar's `plus` step: the primary action as + alone** (from `@botiverse/raft-ui/wip`). `FitToolbarButton` takes `plusOrder`: from that step on it is a + square, its icon and label gone, the label kept as its accessible name and shown as its tooltip; it carries `data-fit-plus`, and `useFitItem({ plus })` gives any item the step. Every item's steps share one order for the toolbar, by their numbers (lower first; at the same number the last item first, and one item's steps as `label`, `more`, `collapse`, `wrap`, `plus`). Give `plus` a higher number than any `wrap`, so the primary becomes + only once the groups have wrapped. `FitToolbarButton`'s `icon` is optional now, for a primary that has none until it is +.
38
+ - Why: Tasks at 375px and 420px: once the filters have wrapped to their own scrolling line, the first line (Mine and All with their counts, sort, list or board, Create task) fits only with Create task as + (sindresorhus, #proj-raft-ui:#225). The step was left out of 0.83.0 for want of a test.
39
+ - Affects: nothing until a button passes `plusOrder`: without it, `FitToolbarButton` renders the same DOM and classes as before.
40
+ - Docs: https://raft-design.botiverse.build/components/fit-toolbar/#behavior
41
+ - **`messageCard` and `messageEmbed`, from `@botiverse/raft-ui`**: the recipes every card in a message is drawn from, so an app's own card takes the same parts. `messageCard()` gives `source` (with `sourceIcon`, `sourceName`, `sourceHost`, `sourceHostText`), `sourceFooter`, `body`, `eyebrow`, `title`, `heading`, `meta`, `actions`, `actionRow`, `inset`, `actionGroup` (the buttons as one item of a row that also holds a status line) and `popover`; `messageEmbed({ size })` the frame and its widths, which `MessageEmbed size` applies.
42
+ - Why: the rule for a card in a message is to take its inset, title, details and buttons from these recipes, never classes of its own; slock's Artifact, audio and app message cards can't follow it without them (Jasper's review of raft-design #1125).
43
+ - Affects: nothing until an app uses them.
44
+ - Docs: https://raft-design.botiverse.build/patterns/cards-in-messages/#one-place-for-the-styles
45
+ - **`size="item"` on PopoverContent**: a popover about an app's item is 400px wide (never wider than the room the positioner leaves it, so on a phone the screen less its padding), and its `PopoverBody`, `PopoverTitle` and `PopoverActions` take a card's parts: the 12px inset, the `text-subhead` bold title, one row of buttons. The content carries `data-size="item"`.
46
+ - Why: the question an app's link asks before it shows previews stands in for the item card it leads to, so it looks like that card (stdrc, 2026-11-10); a popover belongs to the family of what it is about.
47
+ - Affects: nothing until a popover passes `size="item"`; the default renders the same DOM and classes as before. slock's link-preview question should take it.
48
+ - Docs: https://raft-design.botiverse.build/components/popover/#about-an-item
49
+ - **FitToolbarPicker and FitToolbarButton's `collapsedIcon`, for the Tasks filters** (from `@botiverse/raft-ui/wip`). `FitToolbarPicker` is a picker's trigger as one toolbar item: pass the popup's trigger as `trigger` (`<DropdownMenuTrigger />`, `<PopoverTrigger />`) with an `icon`, a label and what is chosen (`value`); it is a PickerTriggerButton, filled `info-soft` once a value is chosen (an applied filter, Select › Filters); a count is set in Space Mono and any other value in the label's font. From `labelOrder` on its label gives way and the icon, the value and the chevron stay (👤 1 ⌄), named "Assignee 1". `FitToolbarButton`'s `collapsedIcon` shows only once its label has given way, in place of `icon` (Clear filters becomes ×). A `FitToolbarButton` with neither refuses its `label` step.
50
+ - Why: the Tasks filters are pickers that must keep what is chosen in view once they narrow, and Clear filters reads as text while it has room (sindresorhus, #proj-raft-ui:#225).
51
+ - Affects: nothing until a page uses them. A `FitToolbarButton` without `collapsedIcon` renders the same DOM and classes as before, plus `data-fit-needs-icon`.
52
+ - Docs: https://raft-design.botiverse.build/components/fit-toolbar/#anatomy
53
+
54
+ ### Changed
55
+
56
+ - **Toasts are drawn above a lightbox**: the toast viewport and an anchored toast's positioner move from `z-70` to `z-80`, above the lightbox's `z-70`. Elevation now writes the order down ([Layer order](https://raft-design.botiverse.build/foundations/elevation/#layer-order)).
57
+ - Why: a toast fired from inside a lightbox (Link copied, a failed download from FileViewerShell's toolbar) was drawn under it, and in document mode the lightbox's body is opaque, so it couldn't be seen (Kirby's review of #1130).
58
+ - Affects: every toast; it is drawn as before over the page, the popups and the dialogs. Nothing else sits between `z-70` and `z-80`.
59
+ - Docs: https://raft-design.botiverse.build/foundations/elevation/#layer-order, https://raft-design.botiverse.build/components/toast/#where-it-appears
60
+
61
+ - **SaveStatus shows only a failure, and takes no room otherwise**: an empty `SaveStatus` is hidden (`empty:hidden`) and has no height, so a setting with it is as tall as one without it. With a `SaveStatusError` in it, the line appears 4px under the control or the description, as before, and the setting grows by it. `SaveStatusSaved` is deprecated: it renders nothing, whatever its children (no check, no "Saved", no live region). `ListRow variant="plain"` no longer leaves a SaveStatus line out of what its ends center on: its ends center on the whole row, with an error shown too.
62
+ - Why: a setting that applies at once needs no "Saved": the control showing its new value is the confirmation, as in macOS's and iOS's settings. The reserved, usually empty line made those rows taller than their neighbors (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e). This replaces the reserved "Saved" line chosen on 2026-10-30 (Kirby's option A, which stdrc agreed to, #proj-raft-ui:401f3241).
63
+ - Affects: every `SaveStatus` while it holds no error, and every plain `ListRow` whose body ends in one (slock's `ImmediateSaveStatus` and `ImmediateSwitchRow`, its Language & region, onboarding, member permissions, server translation and system channels settings, the composer's send key and the channel's thread sort): the row or field is shorter by the line and its 4px, and a switch at a row's end moves up by half of that. A failure now grows the setting by its line, and a row's ends move down by half of it. Drop `SaveStatusSaved` and its "Saved" copy; render `SaveStatusError` when a change failed and nothing otherwise. A plain row without a SaveStatus is unchanged except that it no longer carries the `--list-row-status-space` classes. slock, on upgrading:
64
+ - `SETTINGS_STACK_CARD_DESKTOP` (settingsCardClasses.ts:12) still draws a stacked card's last SaveStatus inside the 16px below it (`[&_[data-slot=save-status]:last-child]:mt-0` and `mb-[calc(-1lh)]`, the 2026-11-04 rule this replaces). With an error shown, its 18px line overlaps the next setting by 2px, or runs into the card's padding, and the setting doesn't grow. On staging only PublicVisibilitySection takes the class, with no SaveStatus, so nothing shows it yet; drop the two classes.
65
+ - `settingsImmediateApply.behavior.test.tsx` fails: `assertIdleLine` looks for the "Saved" live region (`getByRole("status")`, `data-slot=save-status-saved`), and "Saved shows the check inside the same line" waits for "Saved". Assert the idle line is hidden and empty instead, and drop the Saved test. The "saved" state in ImmediateSetting.tsx becomes dead; `SETTING_SAVED_MS` is also used by AppNotificationsControls.tsx, so keep it while that does.
66
+ - Docs: https://raft-design.botiverse.build/components/save-status/, https://raft-design.botiverse.build/patterns/forms/#settings-that-apply-at-once
67
+
68
+ - **Cards in messages take their styles from one place**: MessageEmbed's recipe now holds the card frame and its two widths, `size="item"` (at most 400px, the message's width below 768px) and `size="content"` (at most 560px), and the `messageCard` recipe beside it holds the parts every card and item popover shares: the source row, the body and its 12px inset, the id line, the title (`text-subhead`, 2 lines), the state and details row and the buttons row (at most two buttons; a third action goes in a More menu the card supplies; a status line or note wraps onto its own line before the buttons do, and they go under each other, whole, only in a column narrower than both). AppLinkCard, PreviewCard's body parts, MessageActionCard, MessageForwardedBundle and MessageReplies are drawn from them, and a test (`card-styles.test.ts`) fails, naming the file, line and class, on a card or popover component that writes its own radius, width, inset or title size.
69
+ - Why: the same frame had widths from 176 to 576px, insets of 16, 12 and 10px and titles of 17 and 15px; a card's style changed alone and drifted from the others (stdrc, 2026-11-10: change one card and the others follow).
70
+ - Affects: `MessageEmbed` without `size` renders as before.
71
+ - AppLinkCard: its header, body and buttons sit 12px from the edges instead of 16px, and its title is `text-subhead` (15px on a phone, 14px on a desktop) instead of `text-headline` (17/16px), so the card is shorter.
72
+ - PreviewCard: `PreviewCardBody`, `PreviewCardActions` and `PreviewCardFooter` take the same 12px, and a `PreviewCardTitle` in a body the same title and its link styles. This is every card that uses them, the item card (`size="item"`) and a default-size card that puts its sentence in `PreviewCardBody`, such as slock's 300px connect-account card. A profile card (`PreviewCardHeader`, `PreviewCardDescription`) is unchanged.
73
+ - MessageActionCard: at most 400px instead of 440px, and the message's width on a phone; its properties stack at the narrower width sooner. `MessageActionCardActionsNote` is now a line of its own above the buttons, not beside them.
74
+ - MessageForwardedBundle: at most 560px instead of 544px. MessageReplies: at most 560px instead of 480px; it stays a filled, borderless block, 10px x 8px.
75
+ - Docs: https://raft-design.botiverse.build/patterns/cards-in-messages/
76
+
77
+ ### Fixed
78
+
79
+ - **Lightbox document mode: the keys scroll the document, never the page under the scrim**: with `initialFocus` unset, a document lightbox with a `LightboxBody` focuses the body when opened by a click or a key, so Page Down, the arrow keys and Space scroll the file at once; before, focus landed on the first toolbar control (Close), and those keys scrolled the page under the scrim while the file stayed put. A tap still focuses the lightbox itself, and a lightbox without a body still focuses its first control. A key that would scroll the page from elsewhere in the lightbox (a toolbar button, the lightbox itself after a tap) scrolls `LightboxBody` instead, by the browser's own steps, and does nothing with a body of the caller's own; keys a control uses (a text field's, a button's Space, a menu's arrows) are left to it (Kirby's review of #1142).
80
+ - Affects: document-mode lightboxes with a `LightboxBody` (none in slock yet). An `initialFocus` of the caller's still wins.
81
+ - Docs: https://raft-design.botiverse.build/components/lightbox/#accessibility
82
+ - **Lightbox document mode focuses as Base UI does when opened by a tap**: with `initialFocus` unset, a document lightbox opened by a tap focuses itself, not its first toolbar control, so a phone shows no ring and opens no keyboard; a click or a key still focuses the first control (Kirby).
83
+ - **The drag hint is drawn with the lifted copy, at `999`**: it sat at `z-70` with the lightbox, where the DOM order decided which was on top; elevation.md's Layer order lists it (Kirby).
84
+ - **TaskBoard:** `TaskBoardColumnChevron` draws a `ChevronDown` when given no children, so a column heading always shows whether the column is open. slock passed it empty, so no column had the arrow and a collapsed Closed column read as "Closed 2" over nothing (Kirby, Noel).
85
+
86
+ - **What opens inside a lightbox is drawn above it, and a lightbox opened from a dialog has its scrim**: `LightboxPortal` is a stacking context of its own at `z-70` (`relative isolate z-70`), with the scrim and the content inside it in document order. Base UI already put a Tooltip, Popover, menu and its submenus, Select, Combobox, PreviewCard, Dialog, AlertDialog or Drawer opened inside `LightboxContent` in the lightbox's portal; now its own `z-50` counts inside the lightbox, above the content. The lightbox's scrim is always rendered (`forceRender`), and the lightbox joins the stack of modal surfaces: opened from a dialog it draws `layer-backdrop-nested` (`data-nested`), and a Dialog, AlertDialog or Drawer opened inside it draws the nested scrim over it.
87
+ - Why: the scrim and the content sat at `z-70` and `z-71` among the page's layers, so a popup opened inside, at `z-50` in the lightbox's portal, was drawn under the scrim, and a press on it landed on the scrim and closed the lightbox. And Base UI renders no backdrop for a dialog opened under another, so a lightbox opened from a dialog had no scrim: the dialog showed through undimmed, and a press beside the file went to the dialog (#proj-raft-ui:#226, Kirby's review of #1130).
88
+ - Affects: every lightbox. The DOM is the same; the portal element gains `relative isolate z-70`, the scrim loses `z-70` and the content `z-71`, and a scrim stacked on another surface carries `data-nested`. A popup given a `container` outside the lightbox, or an app's own portal to the body, is drawn under the lightbox, as before. `LightboxPortalProps` takes a string `className`, as `LightboxContentProps` does.
89
+ - Docs: https://raft-design.botiverse.build/components/lightbox/#popups-inside, https://raft-design.botiverse.build/components/lightbox/#behavior
90
+ - **A cover or an excerpt no longer cuts off a card's corners**: `MessageEmbedContent` that is the card's first or last part takes the card's inner corner, `radius-container` less the 1px border, on the two corners it shares with the card, and clips what it holds to them (`overflow: clip`). A cover at the top is round at the top and square at its foot. The card itself still clips nothing, so a title link's focus ring around the whole card (its `::after`) still shows.
91
+ - Why: an Artifact card's cover or excerpt ran square to the card's edges and covered the border's arc, so its top corners looked cut off, in light and dark (cindyz, #triage:6be3db1b). The docs' link card had the same fault.
92
+ - Affects: every card on MessageEmbed whose first or last part is `MessageEmbedContent`: slock's Artifact card, the docs' link card, and MessageForwardedBundle's items when the bundle has no footer (they now clip at the card's bottom corners). Content between two rows is unchanged, and so is MessageQuotedPreview: its frame is a square bar (`data-frame="bar"` on its root and on `MessageQuotedPreviewLoading`), so its content stays square and clips nothing.
93
+ - Docs: https://raft-design.botiverse.build/components/message-embed/#anatomy
94
+ - **FitToolbar measures only what is laid out in its rows**: it leaves out what is hidden, out of flow (`position: absolute` or `fixed`) or of no size, and looks through `display: contents` on its lines too. A line has broken only where an item sits wholly below or above the first on it, so items of different heights on one line (a primary Button beside 32px filters) no longer read as a broken line.
95
+ - Why: a popover or menu opened from a toolbar item puts Base UI focus guards beside its trigger, fixed at the window's corner, and the toolbar counted them: opening it made labels give way (Jasper). And a wrapping toolbar whose first line mixed heights wrapped or gave way where it fit.
96
+ - Affects: every FitToolbar and SelectionToolbar. Opening a popup from one no longer changes it; a wrapping toolbar with items of different heights on a line gives way less, only where it doesn't fit.
97
+ - Docs: https://raft-design.botiverse.build/components/fit-toolbar/#behavior
98
+ - **DialogFooter and AlertDialogFooter decide whether to stack from the buttons laid out in their row only**: they leave out what is hidden, out of flow (`position: absolute` or `fixed`) or of no size, and look through `display: contents`, as FitToolbar does (one shared measure). There is no new prop.
99
+ - Why: an open Menu or Popover puts Base UI focus guards beside its trigger, and a Select or Combobox always adds a hidden input, each fixed at the window's corner. The footer counted them as the row's start, so the row reached to the window's left edge: a menu opened from the footer stacked the buttons at any width, and a Select in the footer stacked them for good, nothing open (Jasper's audit, after the Tasks filters lost their labels whenever a filter was open: stdrc, 2026-11-10, #wg-task-v2:8a7ae237).
100
+ - Affects: a footer holding a Menu or Popover trigger, a Select or a Combobox now stacks only when its buttons don't fit. A button inside a `display: contents` wrapper now counts toward the row; before, the wrapper read as no size and its button was left out. Every other footer is unchanged.
101
+ - Docs: https://raft-design.botiverse.build/components/dialog/#the-footer, https://raft-design.botiverse.build/patterns/responsive/#switch-on-the-regions-width
102
+ - **BannerAction comes back beside the text with a menu open from it, or a Select in it**: once the actions had taken a line of their own, they measured their width with a Range over all their contents. They now measure from their first laid-out item's left to the last one's right, with the same shared measure. There is no new prop.
103
+ - Why: the Range took in an open menu's focus guards and a Select's hidden input, fixed at the window's corner, so the actions seemed as wide as the distance to the window's left edge: they stayed under the text while a menu was open, and for good with a Select in them (Jasper's audit, stdrc, 2026-11-10, #wg-task-v2:8a7ae237).
104
+ - Affects: a banner whose actions hold a Menu or Popover trigger, a Select or a Combobox. Actions of plain buttons measure the same width as before.
105
+ - Docs: https://raft-design.botiverse.build/components/banner/#anatomy, https://raft-design.botiverse.build/patterns/responsive/#switch-on-the-regions-width
106
+ - **A sorted board column shows that it is the place while a card is over it**: `TaskDropZone` takes `dragInto`. With it, the zone carries `data-drag-into` and takes `fill-subtle` while a card it allows is over it, its cards and all; never at rest, and never while it refuses the card (`dropState` `blocked`).
107
+ - Why: Drag and drop says a column whose order isn't the person's to set (sorted by last update, by due date) is itself the place and takes `fill-subtle` while the item is over it, with no placeholder between its items. TaskDropZone had no way to: a sorted column with cards showed nothing while a card was over it, only an empty one its placeholder (review of slock #13852; the channel Tasks panel shares the column).
108
+ - Affects: nothing until a `TaskDropZone` passes `dragInto`: without it, every zone renders the same DOM and classes as before, and a column whose cards are drop targets keeps its placeholder as the one signal.
109
+ - Docs: https://raft-design.botiverse.build/components/task-dnd/#states, https://raft-design.botiverse.build/components/task-board/#states, https://raft-design.botiverse.build/patterns/drag-and-drop/#where-it-will-land
110
+ - **FitToolbar's focus follows the reading order, and a SegmentedControl in it is one toolbar item**: the arrow keys go through its controls line by line as they show, and Tab first enters at the first control on screen. A SegmentedControl inside registers as one item of the toolbar, so the toolbar stays one Tab stop and its arrow keys reach the segments without choosing one (Space chooses; Up and Down choose nothing). When the focused item moves into More, focus goes to More instead of the page. Items of the same kind in one group (the same `label` or `plus` step at the same number) change form at the same time. A SegmentedControl of text alone, or a FitToolbarButton with no icon, refuses its `label` step. There is no new prop.
111
+ - Why: a wrapped group moves with CSS `order`, later on screen than in the DOM, and the browser's Tab order and Base UI's toolbar followed the DOM: focus jumped to the second line and back (WCAG 2.4.3). The SegmentedControl was a second Tab stop whose arrow keys chose as they moved, and Mine and All, with no icons, emptied to nothing at their `label` step; at 780px three filters became icons while Assignee kept its label (Jasper and sindresorhus, 2026-11-10).
112
+ - Affects: every FitToolbar and SelectionToolbar: arrow-key order is the screen's, which is the DOM's unless a group has wrapped. A SegmentedControl in a FitToolbar is no longer a Tab stop of its own. Outside a FitToolbar nothing changes.
113
+ - Docs: https://raft-design.botiverse.build/components/fit-toolbar/#accessibility, https://raft-design.botiverse.build/components/segmented-control/#accessibility
114
+ - **A FitToolbar's scrolling group scrolls only as its last state, and stays where the person scrolled it**: while it can still wrap, it counts as not fitting whenever its items overflow, so its labels give way and it wraps before it scrolls. Each measure keeps its scroll position, only the item that just became active comes into view (on mount the first active one), not every active one with the first winning, and a resize after the person has scrolled leaves it there. There is no new prop.
115
+ - Why: at 780px the filters scrolled on the first line, the last ones hidden past the fade, where giving their labels fit them all; and a count changing or a measure pulled the group back to the active filter under the person's finger (risk 3 of sindresorhus' review, with Jasper, 2026-11-10).
116
+ - Affects: a FitToolbar with `wrap` and an `overflow="scroll"` group that can wrap now gives its labels and wraps sooner. Every scrolling group keeps its scroll position across a measure.
117
+ - Docs: https://raft-design.botiverse.build/components/fit-toolbar/#behavior
118
+
119
+ ### Design System
120
+
121
+ - **A setting that applies at once shows no "Saved" and reserves no line** (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e): the control showing its new value is the confirmation; only a failure appears under it, saying what failed, while the control goes back to its old value. Where a section's settings are stored ("Saved on this device") is said once for the section, not on every row. This replaces the way chosen on 2026-10-30 (Kirby's option A, which stdrc agreed to, #proj-raft-ui:401f3241: "Saved" for a moment, on a line kept when empty) and the settings row's centering on its title and description above that line (2026-11-09).
122
+ - Docs: https://raft-design.botiverse.build/patterns/forms/#settings-that-apply-at-once, https://raft-design.botiverse.build/foundations/alignment/#a-control-and-its-label
123
+ - **A section of settings may hold more than one group, split by topic** (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e): in Appearance, the message font size and its preview are one `ListRowGroup` and the display switches another, 12px apart (`stack-y-3`) under the section's one header, with no hairline joining them. `ListRowGroup` is unchanged: it sets no space around itself, and the column of groups sets it.
124
+ - Docs: https://raft-design.botiverse.build/components/list-row/#more-than-one-group-in-a-section, https://raft-design.botiverse.build/patterns/forms/#a-settings-row
125
+
1
126
  ## [0.83.0](https://github.com/botiverse/raft-design/compare/v0.82.0...v0.83.0) (2026-11-10)
2
127
 
3
128
  ### Added
package/dist/cn.d.mts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ClassValue } from "cnfast";
2
2
  //#region src/lib/cn.d.ts
3
+ /** The `--z-index-*` names of styles.css, in order (design/foundations/elevation.md#layer-order). */
4
+ export declare const Z_INDEX_NAMES: string[];
3
5
  export declare function cn(...inputs: ClassValue[]): string;
4
6
  //#endregion
5
7
  export type { ClassValue };
package/dist/cn.mjs CHANGED
@@ -1,6 +1,25 @@
1
1
  import { clsx } from "cnfast";
2
2
  import { extendTailwindMerge } from "tailwind-merge";
3
3
  //#region src/lib/cn.ts
4
+ /** The `--z-index-*` names of styles.css, in order (design/foundations/elevation.md#layer-order). */
5
+ const Z_INDEX_NAMES = [
6
+ "below",
7
+ "base",
8
+ "raised",
9
+ "control",
10
+ "sticky",
11
+ "float",
12
+ "chrome",
13
+ "cover",
14
+ "app",
15
+ "scrollbar",
16
+ "popup",
17
+ "dialog",
18
+ "dialog-layer",
19
+ "lightbox",
20
+ "toast",
21
+ "drag"
22
+ ];
4
23
  const twMerge = extendTailwindMerge({ extend: {
5
24
  theme: { spacing: [
6
25
  "control",
@@ -59,6 +78,7 @@ const twMerge = extendTailwindMerge({ extend: {
59
78
  "x"
60
79
  ] }],
61
80
  opacity: [{ opacity: ["dimmed"] }],
81
+ z: [{ z: Z_INDEX_NAMES }],
62
82
  "bg-image": [{ overlay: [
63
83
  "subtle",
64
84
  "medium",
@@ -71,4 +91,4 @@ function cn(...inputs) {
71
91
  return twMerge(clsx(inputs));
72
92
  }
73
93
  //#endregion
74
- export { cn };
94
+ export { Z_INDEX_NAMES, cn };
@@ -13,9 +13,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
13
13
  //#region src/components/alert-dialog/alert-dialog.tsx
14
14
  const alertDialog = tv({
15
15
  slots: {
16
- overlay: ["fixed inset-0 z-50 bg-layer-backdrop"],
16
+ overlay: ["fixed inset-0 z-dialog bg-layer-backdrop"],
17
17
  content: [
18
- "fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] -translate-1/2 outline-none",
18
+ "fixed top-1/2 left-1/2 z-dialog grid w-[calc(100%-2rem)] -translate-1/2 outline-none",
19
19
  decisionWidth,
20
20
  "overflow-clip rounded-dialog bg-layer-popover text-foreground",
21
21
  "shadow-float ring-1 ring-line-seam"
@@ -1,35 +1,16 @@
1
1
  import { tv } from "../../lib/tv.mjs";
2
- import { messageEmbed } from "../message-embed/message-embed.recipe.mjs";
2
+ import { messageCard, messageEmbed } from "../message-embed/message-embed.recipe.mjs";
3
3
  import { Skeleton } from "../skeleton/skeleton.mjs";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/app-link-card/app-link-card.tsx
6
6
  const appLinkCard = tv({ slots: {
7
- root: ["flex w-full max-w-[400px] min-w-0 flex-col max-md:max-w-none", "my-2 first:mt-0 last:mb-0"],
8
- header: [
9
- "flex min-w-0 items-center gap-1.5",
10
- "border-b border-line-hairline px-4 py-2",
11
- "font-sans text-caption text-foreground-muted"
12
- ],
13
- icon: ["inline-flex size-4 shrink-0 items-center justify-center", "*:size-full *:shrink-0 [&>img]:rounded-[2px] [&>img]:object-cover"],
14
- name: ["min-w-0 truncate", "font-semibold text-foreground"],
15
- host: ["flex min-w-0 items-baseline", "before:shrink-0 before:pe-1.5 before:content-['·']"],
16
- hostText: ["min-w-0 truncate"],
17
- body: ["flex min-w-0 flex-col gap-1.5", "px-4 py-3 last:pb-4"],
18
- eyebrow: ["m-0 truncate", "font-mono text-mono-meta text-foreground-muted"],
19
- title: [
20
- "m-0 line-clamp-2",
21
- "font-sans text-headline font-bold text-foreground",
22
- "[&_a]:rounded-[2px] [&_a]:decoration-[1.5px] [&_a]:underline-offset-[3px] [&_a]:hover:underline",
23
- "[&_a]:outline-none [&_a]:focus-visible:shadow-focus"
24
- ],
25
- meta: ["flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", "m-0 font-sans text-callout text-foreground-muted"],
26
- actions: ["flex flex-wrap items-center justify-end gap-2", "px-4 pb-3 last:pb-4"],
7
+ root: ["flex flex-col", "my-2 first:mt-0 last:mb-0"],
27
8
  placeholderLine: ["flex h-lh items-center"],
28
9
  placeholderButton: ["h-8 md:pointer-fine:h-control w-28 rounded-control"]
29
10
  } });
30
11
  function frame(className) {
31
12
  const { root } = appLinkCard();
32
- return messageEmbed().root({ className: root({ className }) });
13
+ return messageEmbed({ size: "item" }).root({ className: root({ className }) });
33
14
  }
34
15
  /**
35
16
  * An app link's card drawn in the message, after the link's paragraph, where
@@ -53,7 +34,8 @@ function AppLinkCard({ className, ...props }) {
53
34
  * lines and a button follow, as gray bars.
54
35
  */
55
36
  function AppLinkCardPlaceholder({ className, children, ...props }) {
56
- const { body, eyebrow, title, meta, actions, placeholderLine, placeholderButton } = appLinkCard();
37
+ const { body, eyebrow, title, meta, actions } = messageCard();
38
+ const { placeholderLine, placeholderButton } = appLinkCard();
57
39
  return /* @__PURE__ */ jsxs("div", {
58
40
  "data-slot": "app-link-card-placeholder",
59
41
  "aria-busy": "true",
@@ -107,28 +89,28 @@ function AppLinkCardPlaceholder({ className, children, ...props }) {
107
89
  }
108
90
  /** Raft's row at the top: `AppLinkCardIcon`, `AppLinkCardName`, then `AppLinkCardHost`. */
109
91
  function AppLinkCardHeader({ className, ...props }) {
110
- const { header } = appLinkCard();
92
+ const { source } = messageCard();
111
93
  return /* @__PURE__ */ jsx("div", {
112
94
  "data-slot": "app-link-card-header",
113
- className: header({ className }),
95
+ className: source({ className }),
114
96
  ...props
115
97
  });
116
98
  }
117
99
  /** The app's registered icon, the image Raft stores and serves: one child at 16px. */
118
100
  function AppLinkCardIcon({ className, ...props }) {
119
- const { icon } = appLinkCard();
101
+ const { sourceIcon } = messageCard();
120
102
  return /* @__PURE__ */ jsx("span", {
121
103
  "data-slot": "app-link-card-icon",
122
- className: icon({ className }),
104
+ className: sourceIcon({ className }),
123
105
  ...props
124
106
  });
125
107
  }
126
108
  /** The app's name as Raft stores it. One line, truncated. */
127
109
  function AppLinkCardName({ className, ...props }) {
128
- const { name } = appLinkCard();
110
+ const { sourceName } = messageCard();
129
111
  return /* @__PURE__ */ jsx("span", {
130
112
  "data-slot": "app-link-card-name",
131
- className: name({ className }),
113
+ className: sourceName({ className }),
132
114
  ...props
133
115
  });
134
116
  }
@@ -137,14 +119,14 @@ function AppLinkCardName({ className, ...props }) {
137
119
  * the end, which says where the link goes, stays.
138
120
  */
139
121
  function AppLinkCardHost({ className, children, ...props }) {
140
- const { host, hostText } = appLinkCard();
122
+ const { sourceHost, sourceHostText } = messageCard();
141
123
  return /* @__PURE__ */ jsx("span", {
142
124
  "data-slot": "app-link-card-host",
143
- className: host({ className }),
125
+ className: sourceHost({ className }),
144
126
  ...props,
145
127
  children: /* @__PURE__ */ jsx("span", {
146
128
  dir: "rtl",
147
- className: hostText(),
129
+ className: sourceHostText(),
148
130
  children: /* @__PURE__ */ jsx("span", {
149
131
  dir: "ltr",
150
132
  children
@@ -154,7 +136,7 @@ function AppLinkCardHost({ className, children, ...props }) {
154
136
  }
155
137
  /** The item: `AppLinkCardEyebrow`, `AppLinkCardTitle`, `AppLinkCardMeta`, in that order. */
156
138
  function AppLinkCardBody({ className, ...props }) {
157
- const { body } = appLinkCard();
139
+ const { body } = messageCard();
158
140
  return /* @__PURE__ */ jsx("div", {
159
141
  "data-slot": "app-link-card-body",
160
142
  className: body({ className }),
@@ -163,7 +145,7 @@ function AppLinkCardBody({ className, ...props }) {
163
145
  }
164
146
  /** The app's id for the item, above the title. One line, truncated. */
165
147
  function AppLinkCardEyebrow({ className, ...props }) {
166
- const { eyebrow } = appLinkCard();
148
+ const { eyebrow } = messageCard();
167
149
  return /* @__PURE__ */ jsx("p", {
168
150
  "data-slot": "app-link-card-eyebrow",
169
151
  className: eyebrow({ className }),
@@ -175,7 +157,7 @@ function AppLinkCardEyebrow({ className, ...props }) {
175
157
  * to the item in it: the title opens the item, in the title's ink.
176
158
  */
177
159
  function AppLinkCardTitle({ className, ...props }) {
178
- const { title } = appLinkCard();
160
+ const { title } = messageCard();
179
161
  return /* @__PURE__ */ jsx("p", {
180
162
  "data-slot": "app-link-card-title",
181
163
  className: title({ className }),
@@ -184,7 +166,7 @@ function AppLinkCardTitle({ className, ...props }) {
184
166
  }
185
167
  /** One state in a neutral Badge, then muted details. Wraps when it runs out of room. */
186
168
  function AppLinkCardMeta({ className, ...props }) {
187
- const { meta } = appLinkCard();
169
+ const { meta } = messageCard();
188
170
  return /* @__PURE__ */ jsx("div", {
189
171
  "data-slot": "app-link-card-meta",
190
172
  className: meta({ className }),
@@ -196,7 +178,7 @@ function AppLinkCardMeta({ className, ...props }) {
196
178
  * last, with a More menu (⋯) to their left for the rest.
197
179
  */
198
180
  function AppLinkCardActions({ className, ...props }) {
199
- const { actions } = appLinkCard();
181
+ const { actions } = messageCard();
200
182
  return /* @__PURE__ */ jsx("div", {
201
183
  "data-slot": "app-link-card-actions",
202
184
  className: actions({ className }),
@@ -19,7 +19,7 @@ const appShell = tv({
19
19
  sidebarSlot: ["hidden min-h-0 shrink-0 flex-col md:flex"],
20
20
  mainSlot: ["flex min-h-0 min-w-0 flex-1 flex-col"],
21
21
  panelSlot: ["hidden min-h-0 min-w-0 flex-col md:flex"],
22
- overlaySlot: ["absolute inset-0 z-20 flex min-h-0 min-w-0"]
22
+ overlaySlot: ["absolute inset-0 z-cover flex min-h-0 min-w-0"]
23
23
  },
24
24
  variants: { overlay: {
25
25
  false: {},
@@ -135,7 +135,7 @@ function AppShellSidebarSlot({ className, id: idProp, render, ...props }) {
135
135
  id,
136
136
  inert: collapsed ? true : void 0,
137
137
  className: sidebarSlot({ className: cn(className, sidebar?.collapsible && [
138
- "group-data-[state=expanded]/app-shell:relative group-data-[state=expanded]/app-shell:z-1",
138
+ "group-data-[state=expanded]/app-shell:relative group-data-[state=expanded]/app-shell:z-raised",
139
139
  "group-data-[state=collapsed]/app-shell:pointer-events-none",
140
140
  "group-data-[state=collapsed]/app-shell:opacity-0"
141
141
  ]) })
@@ -37,7 +37,7 @@ const avatar = tv({
37
37
  "[[data-size='3xs']>&]:[&>svg]:size-2"
38
38
  ],
39
39
  badge: [
40
- "absolute z-10 select-none",
40
+ "absolute z-raised select-none",
41
41
  "size-[11px] -right-1 -bottom-1 rounded-[2px]",
42
42
  "outline-2 outline-solid outline-layer-canvas",
43
43
  "group-data-[size=xl]/avatar:size-5 group-data-[size=xl]/avatar:-right-2 group-data-[size=xl]/avatar:-bottom-2",
@@ -1,3 +1,4 @@
1
+ import { laidOutChildren } from "../../lib/laid-out-children.mjs";
1
2
  import { tv } from "../../lib/tv.mjs";
2
3
  import { Button } from "../button/button.mjs";
3
4
  import { X } from "lucide-react";
@@ -67,7 +68,7 @@ const banner = tv({
67
68
  defaultVariants: { size: "md" },
68
69
  compoundVariants: [{
69
70
  size: "sm",
70
- class: { root: ["[--banner-button:--spacing(8)] md:pointer-fine:[--banner-button:var(--control-height)]", "has-[>[data-slot=banner-action],>[data-banner-close]]:py-[calc((var(--banner-button)-1lh)/2+--spacing(1))]"] }
71
+ class: { root: ["[--banner-button:--spacing(8)] md:pointer-fine:[--banner-button:var(--control-height)]", "has-[>[data-slot=banner-action],>[data-banner-close]]:py-[calc((var(--banner-button)-1lh)/2+(--spacing(1)))]"] }
71
72
  }]
72
73
  });
73
74
  /**
@@ -126,6 +127,18 @@ function contentWidth(element) {
126
127
  range.selectNodeContents(element);
127
128
  return range.getBoundingClientRect().width;
128
129
  }
130
+ function actionsWidth(action) {
131
+ const items = laidOutChildren(action);
132
+ if (items.length === 0) return contentWidth(action);
133
+ let start = Number.POSITIVE_INFINITY;
134
+ let end = Number.NEGATIVE_INFINITY;
135
+ for (const item of items) {
136
+ const box = item.getBoundingClientRect();
137
+ start = Math.min(start, box.left);
138
+ end = Math.max(end, box.right);
139
+ }
140
+ return end - start;
141
+ }
129
142
  function useWrapsBesideText(action) {
130
143
  const [wraps, setWraps] = useState(false);
131
144
  useLayoutEffect(() => {
@@ -148,7 +161,7 @@ function useWrapsBesideText(action) {
148
161
  }
149
162
  if (lines > 1) return;
150
163
  const gap = Number.parseFloat(getComputedStyle(root).columnGap) || 0;
151
- if (contentWidth(text) + contentWidth(action) + gap + 1 <= text.getBoundingClientRect().width) setWraps(false);
164
+ if (contentWidth(text) + actionsWidth(action) + gap + 1 <= text.getBoundingClientRect().width) setWraps(false);
152
165
  };
153
166
  let frame = 0;
154
167
  const schedule = () => {
@@ -10,11 +10,11 @@ const buttonSplit = tv({ base: [
10
10
  "data-sticker-edge:ring-[1.5px]",
11
11
  "data-[size=lg]:rounded-(--control-radius)"
12
12
  ] });
13
- const buttonSplitMain = tv({ base: ["rounded-e-none border-e-0", "ring-0 focus-visible:z-1"] });
13
+ const buttonSplitMain = tv({ base: ["rounded-e-none border-e-0", "ring-0 focus-visible:z-raised"] });
14
14
  const buttonSplitMenu = tv({
15
15
  base: [
16
16
  "min-w-0 rounded-s-none px-0",
17
- "ring-0 focus-visible:z-1",
17
+ "ring-0 focus-visible:z-raised",
18
18
  "after:inset-s-0"
19
19
  ],
20
20
  variants: {
@@ -38,8 +38,8 @@ const buttonVariants = tv({
38
38
  "disabled:border-line-hairline disabled:bg-fill-muted disabled:bg-none disabled:text-foreground-disabled disabled:ring-0",
39
39
  "aria-disabled:border-line-hairline aria-disabled:bg-fill-muted aria-disabled:bg-none aria-disabled:text-foreground-disabled aria-disabled:ring-0"
40
40
  ],
41
- content: "relative z-10 inline-flex min-w-0 max-w-full items-center justify-center gap-[inherit] data-[loading=true]:invisible",
42
- loadingIndicator: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center"
41
+ content: "relative z-raised inline-flex min-w-0 max-w-full items-center justify-center gap-[inherit] data-[loading=true]:invisible",
42
+ loadingIndicator: "pointer-events-none absolute inset-0 z-raised flex items-center justify-center"
43
43
  },
44
44
  variants: {
45
45
  variant: {
@@ -24,7 +24,7 @@ const choiceList = tv({
24
24
  description: ["block truncate font-sans text-footnote font-normal text-foreground-muted", "in-data-disabled:text-foreground-disabled"],
25
25
  meta: ["block truncate font-sans text-footnote font-normal text-foreground-muted", "in-data-disabled:text-foreground-disabled"],
26
26
  action: [
27
- "relative z-10 col-start-2 flex h-lh items-center self-start pr-1",
27
+ "relative z-raised col-start-2 flex h-lh items-center self-start pr-1",
28
28
  "mt-option-inset pointer-coarse:mt-[calc((--spacing(11)-1lh)/2)]",
29
29
  "font-sans text-subhead"
30
30
  ],
@@ -16,11 +16,11 @@ const codeBlock = tv({ slots: {
16
16
  actions: [
17
17
  "ml-auto flex items-center gap-1",
18
18
  "[[data-slot=code-block-body]>&]:col-start-2 [[data-slot=code-block-body]>&]:row-start-1",
19
- "[[data-slot=code-block-body]>&]:z-10 [[data-slot=code-block-body]>&]:ml-3",
19
+ "[[data-slot=code-block-body]>&]:z-raised [[data-slot=code-block-body]>&]:ml-3",
20
20
  "[[data-slot=code-block-body]>&]:my-[calc(var(--control-embedded-inset)-0.5rem-1px)] [[data-slot=code-block-body]>&]:mr-[calc(var(--control-embedded-inset)-0.75rem-1px)]"
21
21
  ],
22
22
  corner: [
23
- "pointer-events-none absolute z-20 size-1",
23
+ "pointer-events-none absolute z-raised size-1",
24
24
  "data-[y=top]:top-0 data-[y=bottom]:bottom-0",
25
25
  "data-[x=left]:left-0 data-[x=right]:right-0",
26
26
  "hidden"
@@ -60,7 +60,7 @@ const combobox = tv({
60
60
  "first:rounded-t-container has-focus-visible:keyboard:inset-ring-2 has-focus-visible:keyboard:inset-ring-foreground"
61
61
  ],
62
62
  input: ["w-full outline-none"],
63
- positioner: ["isolate z-50 outline-none"],
63
+ positioner: ["isolate z-popup outline-none"],
64
64
  content: [
65
65
  "h-auto max-h-(--available-height) min-w-[220px] overflow-hidden",
66
66
  "max-w-[min(var(--available-width),max(var(--anchor-width),24rem))]",
@@ -161,7 +161,7 @@
161
161
  box-shadow: inset 0 0 0 2px var(--foreground);
162
162
  }
163
163
  .r-combobox-select__positioner {
164
- z-index: 50;
164
+ z-index: var(--z-index-popup);
165
165
  isolation: isolate;
166
166
  outline: 0;
167
167
  }
@@ -7,7 +7,7 @@ const comment = tv({
7
7
  "absolute inset-x-0 top-0",
8
8
  "data-animate:transition-transform data-animate:duration-150 data-animate:ease-out",
9
9
  "motion-reduce:transition-none",
10
- "data-active:z-10"
10
+ "data-active:z-raised"
11
11
  ],
12
12
  card: [
13
13
  "relative flex min-w-0 flex-col gap-2",
@@ -16,7 +16,7 @@ const comment = tv({
16
16
  "data-active:border-[1.5px] data-active:border-line-strong data-active:p-[11.5px] data-active:shadow-float"
17
17
  ],
18
18
  cardTrigger: [
19
- "absolute -inset-px z-10 cursor-pointer",
19
+ "absolute -inset-px z-raised cursor-pointer",
20
20
  "rounded-container border-0 bg-transparent p-0",
21
21
  "hover:bg-fill-subtle active:bg-fill-medium",
22
22
  "outline-none focus-visible:shadow-focus"
@@ -20,7 +20,7 @@ const composerAttachment = tv({
20
20
  "pe-1.5"
21
21
  ],
22
22
  attachmentsNav: [
23
- "absolute top-1/2 z-30 -translate-y-1/2",
23
+ "absolute top-1/2 z-control -translate-y-1/2",
24
24
  "size-8 rounded-chip shadow-none",
25
25
  "[&_svg:not([class*='size-'])]:size-4.5"
26
26
  ],
@@ -42,26 +42,26 @@ const composerAttachment = tv({
42
42
  attachmentTitle: ["block truncate", "font-sans text-footnote font-semibold text-foreground"],
43
43
  attachmentMeta: ["block w-full truncate", "text-left font-mono text-mono-meta text-foreground-hint"],
44
44
  attachmentUploadingOverlay: [
45
- "absolute inset-0 z-10 flex flex-col items-center justify-center px-1",
45
+ "absolute inset-0 z-raised flex flex-col items-center justify-center px-1",
46
46
  "text-center",
47
47
  "rounded-control bg-layer-veil",
48
48
  "font-mono text-mono-meta font-bold text-foreground"
49
49
  ],
50
50
  attachmentUploadProgressBar: [
51
- "absolute z-20 h-1 overflow-hidden",
51
+ "absolute z-raised h-1 overflow-hidden",
52
52
  "right-1.5 bottom-1.5 left-1.5 w-auto rounded-[2px]",
53
53
  "bg-layer-card"
54
54
  ],
55
55
  attachmentUploadProgressBarIndicator: ["block h-full", "rounded-[inherit] bg-info"],
56
56
  attachmentFailedOverlay: [
57
- "absolute inset-0 z-10 flex flex-col items-center justify-center px-1",
57
+ "absolute inset-0 z-raised flex flex-col items-center justify-center px-1",
58
58
  "[border-width:var(--ca-overlay-border,2px)]",
59
59
  "text-center",
60
60
  "rounded-control border-danger-strong bg-layer-veil",
61
61
  "font-sans text-caption font-semibold text-danger-strong"
62
62
  ],
63
63
  attachmentRemove: [
64
- "absolute z-20 flex items-center justify-center",
64
+ "absolute z-control flex items-center justify-center",
65
65
  "transition-opacity group-hover/composer-attachment:opacity-100",
66
66
  "[@media(hover:none)]:opacity-100",
67
67
  "[&_svg]:shrink-0",
@@ -42,11 +42,11 @@ function ComposerDropOverlayPrompt({ children, ...props }) {
42
42
  children: /* @__PURE__ */ jsx(FileText, { className: "text-foreground-muted" })
43
43
  }),
44
44
  /* @__PURE__ */ jsx("span", {
45
- className: dropOverlayIcon({ className: "absolute z-10" }),
45
+ className: dropOverlayIcon({ className: "absolute z-raised" }),
46
46
  children: /* @__PURE__ */ jsx(Upload, { className: "text-foreground-muted" })
47
47
  }),
48
48
  /* @__PURE__ */ jsx("span", {
49
- className: dropOverlayIcon({ className: "absolute z-20 translate-x-7 translate-y-1 rotate-12" }),
49
+ className: dropOverlayIcon({ className: "absolute z-raised translate-x-7 translate-y-1 rotate-12" }),
50
50
  children: /* @__PURE__ */ jsx(Image, { className: "text-foreground-muted" })
51
51
  })
52
52
  ]
@@ -2,7 +2,7 @@ import { tv } from "../../lib/tv.mjs";
2
2
  //#region src/components/composer/composer-drop-overlay.recipe.ts
3
3
  const composerDropOverlay = tv({ slots: {
4
4
  dropOverlay: [
5
- "absolute inset-0 z-30 flex items-center justify-center",
5
+ "absolute inset-0 z-float flex items-center justify-center",
6
6
  "rounded-container border-[1.5px] border-dashed border-line-seam",
7
7
  "bg-layer-card bg-[linear-gradient(var(--info-soft),var(--info-soft))]"
8
8
  ],
@@ -225,7 +225,7 @@ function SortableComposerAttachment({ id, label, children }) {
225
225
  const style = {
226
226
  transform: isDragging ? void 0 : CSS.Translate.toString(transform),
227
227
  transition: isDragging ? void 0 : transition,
228
- zIndex: isDragging ? 0 : void 0
228
+ zIndex: isDragging ? "var(--z-index-base)" : void 0
229
229
  };
230
230
  const place = isDragging && !home;
231
231
  return /* @__PURE__ */ jsxs("div", {