fix(transactions): keep the assistant usable through the whole booking flow (#1975)

* fix(transactions): keep the assistant usable through the whole booking flow

Three gaps around the agent sheet during booking on Transaktioner:

- The AgentTrigger bubble lived inside #dash-shell, so the inert that
  non-modal dialogs set on the shell made the assistant impossible to
  OPEN once a booking dialog was up. Moved outside the shell (it is
  position: fixed) and raised to z-[45]: above the DialogVeil (z-40) so
  it stays clickable, below dialog content (z-50). Under true modal
  dialogs Radix's body pointer-events lock keeps it dead as before.
- Wide dialogs centered on the full viewport while the docked sheet
  (z-60) covered their right edge, hiding e.g. the Granska button.
  DialogContent now centers in the space left of --agent-dock-w, and
  the wide booking/review variants cap their width against it.
- Step 1 of the flow (template picker, QuickReviewDialog) was still
  fully modal, so the assistant was dead there. Both are now non-modal
  with DialogVeil + a shared ref-counted useDashShellInert hook (also
  replacing the duplicated inert effects in TransactionBookingDialog
  and NewInvoiceDialog). Their Esc/veil-click dismissal is kept: they
  hold no half-filled form. Clicks in the agent sheet or its trigger
  never dismiss any dialog: data-agent-ui counts as inside, same
  mechanism as data-dialog-companion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): dialog centering reads a docked-only sheet width variable

Skeptic review refuted the first cut: globals.css seeds --agent-dock-w
at the 10px frame gutter on :root by design, so the 0px fallback in the
new dialog centering never applied and every dialog sat 5px left of
center (full-bleed dialogs clipped 5px off-screen).

Introduce --agent-sheet-w, set inline by AgentSheetProvider only while
the sheet is docked and removed otherwise, so the 0px fallback is real:
sheet closed or floating renders byte-identical to the old left-[50%]
and old max widths. Also cap the template picker and QuickReview's
narrow variant, which could clip off-screen left on narrow desktops
with the sheet docked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-27 14:22:20 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 99c94d467a
commit c981384a0e
9 changed files with 107 additions and 44 deletions
+3
View File
@@ -563,6 +563,9 @@ export default function AgentSheet({
ref={sheetRef}
role="dialog"
aria-label={displayTitle}
// Interactions here must never dismiss an open (non-modal) dialog:
// DialogContent treats data-agent-ui as inside the dialog.
data-agent-ui=""
// z-[60] sits above the mobile bottom nav (z-50) so on phones the sheet
// covers the full screen including where the nav would otherwise show.
// `hidden` (display:none) when collapsed keeps the component mounted (the
+8
View File
@@ -274,15 +274,23 @@ export function AgentSheetProvider({
// beside the answer. Written as a CSS variable rather than a class on <main>
// because the frame layout is a server component; globals.css seeds the
// default so the first paint is not a jump.
// --agent-sheet-w is the docked-only sibling: unlike --agent-dock-w it has
// NO stylesheet default (globals.css seeds dock-w at the 10px frame gutter),
// so consumers that must be a no-op when the sheet is closed (dialog
// centering and width caps in ui/dialog.tsx and the wide booking/review
// dialogs) read this one and get their 0px fallback.
useEffect(() => {
const root = document.documentElement
if (dockWidth === null) {
root.style.removeProperty('--agent-dock-w')
root.style.removeProperty('--agent-sheet-w')
return
}
root.style.setProperty('--agent-dock-w', `${dockWidth}px`)
root.style.setProperty('--agent-sheet-w', `${dockWidth}px`)
return () => {
root.style.removeProperty('--agent-dock-w')
root.style.removeProperty('--agent-sheet-w')
}
}, [dockWidth])
+9 -1
View File
@@ -222,7 +222,15 @@ export default function AgentTrigger({ hidden = false }: { hidden?: boolean }) {
// the page declares a bottom action bar (body[data-page-bottom-bar],
// set by e.g. the standalone invoice editor): lift above it so the FAB
// never covers the bar's primary button.
className={`fixed right-4 z-30 ${visibilityClass} h-12 max-w-[calc(100vw-2rem)] items-stretch rounded-full bg-foreground text-background shadow-lg bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] md:bottom-4 md:[body[data-page-bottom-bar]_&]:bottom-20`}
// z-[45]: above the DialogVeil (z-40) so the assistant can be OPENED
// while a non-modal dialog (booking, invoice) holds the page inert, but
// below dialog content (z-50). Under a true modal dialog Radix sets
// pointer-events: none on <body>, which keeps the trigger dead there
// regardless of z.
// data-agent-ui: opening the assistant must not dismiss an open
// non-modal dialog (DialogContent treats this as inside).
data-agent-ui=""
className={`fixed right-4 z-[45] ${visibilityClass} h-12 max-w-[calc(100vw-2rem)] items-stretch rounded-full bg-foreground text-background shadow-lg bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] md:bottom-4 md:[body[data-page-bottom-bar]_&]:bottom-20`}
>
<button
onClick={handleClick}