fix(web): mobile viewport overflow on session-detail page

Section headers (Vulnerability Report, Analysis Results, Integrity Bypass)
and the page action-bar packed icon + title + counter + severity badges +
action button + chevron into a single non-wrapping flex row. On phone-width
viewports that row was wider than the card, widening <main> past the viewport
and making <body> horizontally scrollable. iOS Safari then mis-sized the
fixed-position mobile drawer/overlay, so the drawer no longer covered the
full screen.

- globals.css: overflow-x: clip on <html> as a safety net (clip, not hidden,
  so it does not create a containing block for the fixed drawer/overlay).
- The three section headers + page header now wrap (flex-wrap + gap), with
  shrink-0 on icons/action groups and break-all on the mono finding string.

tsc clean; vitest 390/390 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-05-27 17:01:55 +02:00
parent 31958e7ba8
commit e8c6ec49a0
5 changed files with 23 additions and 17 deletions
@@ -15,6 +15,12 @@
html {
scroll-behavior: smooth;
overflow-y: auto;
/* Clip (not hidden) any accidental horizontal overflow so a single
too-wide row can't make the whole page horizontally scrollable on
mobile. `clip` — unlike `hidden` — does not establish a scroll
container or a containing block, so fixed-position children (mobile
drawer + overlay) keep sizing against the viewport. */
overflow-x: clip;
scrollbar-gutter: stable;
}
@@ -66,17 +66,17 @@ export default function AnalysisResultsSection({
<div className="bg-white shadow rounded-lg p-6 mb-6">
<div
onClick={() => setAnalysisExpanded(!analysisExpanded)}
className="flex items-center justify-between w-full text-left mb-4 cursor-pointer"
className="flex items-start justify-between gap-2 w-full text-left mb-4 cursor-pointer"
>
<div className="flex items-center space-x-2">
<svg className="w-6 h-6 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className="flex items-center flex-wrap gap-x-2 gap-y-1 min-w-0">
<svg className="w-6 h-6 shrink-0 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<h2 className="text-xl font-semibold text-gray-900">Analysis Results</h2>
{analysisResults.length > 0 && (
<>
<span className="text-xs text-gray-400">({analysisResults.length} {analysisResults.length === 1 ? 'issue' : 'issues'})</span>
<div className="flex items-center space-x-2 text-xs">
<div className="flex items-center flex-wrap gap-2 text-xs">
{analysisResults.filter(r => r.severity === 'critical').length > 0 && (
<span className="px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
{analysisResults.filter(r => r.severity === 'critical').length} Critical
@@ -96,7 +96,7 @@ export default function AnalysisResultsSection({
</>
)}
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-3 shrink-0">
<button
onClick={(e) => { e.stopPropagation(); onReanalyze(); }}
disabled={loadingAnalysis}
@@ -110,10 +110,10 @@ export default function IntegrityBypassSection({ events, expanded, setExpanded }
<div className="bg-white shadow rounded-lg p-6 mb-6">
<div
onClick={() => setExpanded(!expanded)}
className="flex items-center justify-between w-full text-left cursor-pointer"
className="flex items-start justify-between gap-2 w-full text-left cursor-pointer"
>
<div className="flex items-center space-x-2">
<svg className="w-6 h-6 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className="flex items-center flex-wrap gap-x-2 gap-y-1 min-w-0">
<svg className="w-6 h-6 shrink-0 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<h2 className="text-xl font-semibold text-gray-900">Integrity Bypass Analyzer</h2>
@@ -121,10 +121,10 @@ export default function IntegrityBypassSection({ events, expanded, setExpanded }
{severity.toUpperCase()}
</span>
{payload.finding && (
<span className="text-xs text-gray-500 font-mono">{payload.finding}</span>
<span className="text-xs text-gray-500 font-mono break-all">{payload.finding}</span>
)}
</div>
<svg className={`w-5 h-5 text-gray-400 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className={`w-5 h-5 shrink-0 text-gray-400 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
@@ -409,10 +409,10 @@ export default function VulnerabilityReportSection({
{/* Section Header */}
<div
onClick={() => setExpanded(!expanded)}
className="flex items-center justify-between w-full text-left mb-4 cursor-pointer"
className="flex items-start justify-between gap-2 w-full text-left mb-4 cursor-pointer"
>
<div className="flex items-center space-x-2">
<svg className="w-6 h-6 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<div className="flex items-center flex-wrap gap-x-2 gap-y-1 min-w-0">
<svg className="w-6 h-6 shrink-0 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
<h2 className="text-xl font-semibold text-gray-900">Vulnerability Report</h2>
@@ -421,7 +421,7 @@ export default function VulnerabilityReportSection({
<span className="text-xs text-gray-400">
({findings.reduce((sum, f) => sum + f.cves.length, 0)} CVEs across {findings.length} {findings.length === 1 ? "package" : "packages"})
</span>
<div className="flex items-center space-x-2 text-xs">
<div className="flex items-center flex-wrap gap-2 text-xs">
{severityCounts.critical > 0 && (
<span className={`px-2 py-0.5 rounded-full font-medium ${severityHeaderBadgeColors.critical}`}>
{severityCounts.critical} Critical
@@ -451,7 +451,7 @@ export default function VulnerabilityReportSection({
</>
)}
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-3 shrink-0">
{onRescan && (
<button
onClick={async (e) => {
@@ -366,13 +366,13 @@ export default function SessionDetailPage() {
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8 flex items-center justify-between">
<div className="py-6 px-4 sm:px-6 lg:px-8 flex flex-wrap items-center justify-between gap-y-3 gap-x-4">
<div>
<h1 className="text-2xl font-normal text-gray-900">
Session Details
</h1>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-3">
{session?.diagnosticsBlobName && (
<button
onClick={async () => {