fix(console): centre modals via a portal, not inside a transformed ancestor

The New Lead form opened off-centre and its backdrop covered only part of the
screen. Cause: the dialog uses position:fixed, but .shell__main animates
transform on entrance, and an element that has animated transform keeps acting
as the containing block in Chrome even after the animation finishes. So "fixed"
resolved against the 1460px max-width, margin-auto content area — offset by the
sidebar — instead of the viewport.

Every scrim dialog shared this latent bug. Each now renders through
createPortal(…, document.body), so the fixed overlay is always relative to the
viewport regardless of any transformed ancestor. New Lead, the conversation and
call threads, the full lead file, the agent card, and the reasoning dialog all
now centre and dim the whole screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-09 01:28:37 +05:30
parent adef7838fc
commit 119bea1e76
6 changed files with 24 additions and 7 deletions

View File

@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { AGENTS } from '../api/agents.js' import { AGENTS } from '../api/agents.js'
import './AgentCard.css' import './AgentCard.css'
@ -38,7 +39,7 @@ export default function AgentCard({ agentKey, onClose }) {
if (!a) return null if (!a) return null
return ( return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation"> <div className="prose__scrim" onClick={onClose} role="presentation">
<div <div
className="prose__dlg card" className="prose__dlg card"
@ -105,5 +106,7 @@ export default function AgentCard({ agentKey, onClose }) {
</div> </div>
</div> </div>
</div> </div>
,
document.body,
) )
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import './Conversation.css' import './Conversation.css'
/** /**
@ -60,7 +61,7 @@ export default function CallTranscript({ text, name, onClose }) {
} }
}, [onClose]) }, [onClose])
return ( return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation"> <div className="prose__scrim" onClick={onClose} role="presentation">
<div <div
className="prose__dlg conv" className="prose__dlg conv"
@ -101,5 +102,7 @@ export default function CallTranscript({ text, name, onClose }) {
</p> </p>
</div> </div>
</div> </div>
,
document.body,
) )
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import './ClampText.css' import './ClampText.css'
/** /**
@ -50,7 +51,7 @@ function ProseDialog({ title, lead, body, onClose }) {
} }
}, [onClose]) }, [onClose])
return ( return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation"> <div className="prose__scrim" onClick={onClose} role="presentation">
<div <div
className="prose__dlg" className="prose__dlg"
@ -77,7 +78,8 @@ function ProseDialog({ title, lead, body, onClose }) {
{body.split(/\n{2,}/).map((para, i) => <p key={i}>{para}</p>)} {body.split(/\n{2,}/).map((para, i) => <p key={i}>{para}</p>)}
</div> </div>
</div> </div>
</div> </div>,
document.body,
) )
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { baseFieldId } from '../api/config.js' import { baseFieldId } from '../api/config.js'
import './Conversation.css' import './Conversation.css'
@ -88,7 +89,7 @@ export default function Conversation({ rows, name, onClose }) {
} }
}, [onClose]) }, [onClose])
return ( return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation"> <div className="prose__scrim" onClick={onClose} role="presentation">
<div <div
className="prose__dlg conv" className="prose__dlg conv"
@ -128,5 +129,7 @@ export default function Conversation({ rows, name, onClose }) {
</p> </p>
</div> </div>
</div> </div>
,
document.body,
) )
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import ClampText from './ClampText.jsx' import ClampText from './ClampText.jsx'
import './LeadFileDialog.css' import './LeadFileDialog.css'
@ -59,7 +60,7 @@ export default function LeadFileDialog({ groups, labels, label, money, fileHref,
const found = shownGroups.reduce((n, [, rows]) => n + rows.length, 0) const found = shownGroups.reduce((n, [, rows]) => n + rows.length, 0)
return ( return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation"> <div className="prose__scrim" onClick={onClose} role="presentation">
<div <div
className="prose__dlg lfd" className="prose__dlg lfd"
@ -159,5 +160,7 @@ export default function LeadFileDialog({ groups, labels, label, money, fileHref,
</div> </div>
</div> </div>
</div> </div>
,
document.body,
) )
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import ActivityForm from './ActivityForm.jsx' import ActivityForm from './ActivityForm.jsx'
import { ENTRY } from '../api/config.js' import { ENTRY } from '../api/config.js'
@ -47,7 +48,7 @@ export default function NewLeadDialog({ onClose }) {
} }
}, [onClose]) }, [onClose])
return ( return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation"> <div className="prose__scrim" onClick={onClose} role="presentation">
<div <div
className="prose__dlg nld" className="prose__dlg nld"
@ -105,5 +106,7 @@ export default function NewLeadDialog({ onClose }) {
</div> </div>
</div> </div>
</div> </div>
,
document.body,
) )
} }