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
+4 -1
View File
@@ -515,7 +515,6 @@ export default async function DashboardLayout({
)}
</MainContainer>
</main>
<AgentTrigger hidden={userPrefs?.hide_assistant_fab === true} />
{/* One-time expired-trial notice. Sandbox/anonymous demo users have
no billing (their companies carry trial grants too), so the gate
lives here where both flags are known. Acknowledgement persists
@@ -533,6 +532,10 @@ export default async function DashboardLayout({
<SettingsHotkey />
{settingsModal}
</div>
{/* Outside #dash-shell on purpose: non-modal dialogs (booking,
invoice) set `inert` on the shell while open, and the assistant
entry point must stay clickable then, like the sheet itself. */}
<AgentTrigger hidden={userPrefs?.hide_assistant_fab === true} />
{!isSandbox && (
<AnalyticsIdentify
user={{
+15 -3
View File
@@ -8,7 +8,7 @@ import { useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogVeil, useDashShellInert } from '@/components/ui/dialog'
import { useToast } from '@/components/ui/use-toast'
import { ToastAction } from '@/components/ui/toast'
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
@@ -350,6 +350,9 @@ export default function TransactionsPage() {
// Template picker dialog
const [templatePickerOpen, setTemplatePickerOpen] = useState(false)
const [templatePickerTransaction, setTemplatePickerTransaction] = useState<TransactionWithInvoice | null>(null)
// The picker renders non-modal (agent sheet stays usable); hand-restore
// page modality while it is open. See useDashShellInert in ui/dialog.tsx.
useDashShellInert(templatePickerOpen)
// Invoice picker dialog (manual match)
const [invoicePickerOpen, setInvoicePickerOpen] = useState(false)
@@ -4020,8 +4023,17 @@ export default function TransactionsPage() {
/>
)}
{templatePickerOpen && <Dialog open onOpenChange={setTemplatePickerOpen}>
<DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
{/* Non-modal so the agent sheet and its trigger stay usable while
picking (useDashShellInert above hand-restores page modality).
Esc and veil-click still close: the picker holds no user input.
Clicks in the assistant don't dismiss: data-agent-ui counts as
inside (see DialogContent). */}
{templatePickerOpen && <Dialog open onOpenChange={setTemplatePickerOpen} modal={false}>
<DialogVeil />
{/* Width capped at the space left of a docked sheet (--agent-sheet-w
is docked-only) so the picker never clips off-screen left on
narrow desktops; sheet closed = the old max-w-lg. */}
<DialogContent className="max-w-[min(32rem,calc(100vw-var(--agent-sheet-w,0px)))] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Bokför transaktion</DialogTitle>
</DialogHeader>