mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-09-29 04:21:44 +02:00
fix(web): responsive pass — real device sizes, scroll in the right containers
Audited at 390×844, 844×390 (phone landscape), 768×1024, 1024×768, 1440×900
and ≥1600px. What was actually broken:
- **The run header collapsed.** `justify-content: space-between` let the two
action buttons take the whole row, so at 390px the title wrapped one
character per line ("Test AspNe / t") and the facts line wrapped one word per
line. It now stacks below 680px, with the title block sized to its content
instead of stretching (the desktop `flex: 1` was what left a 180px void above
the buttons in the stacked layout).
- **The terminal header overflowed** its dock at 390px (557px of content in a
390px box) — it wraps now, and the status text drops out on narrow screens
where the coloured dot already carries it.
- **`100vh` is wrong on mobile.** It measures the viewport without the
collapsing address bar, so the wizard footer and its CTA sit underneath it.
Switched to `dvh` with the `vh` line kept as the fallback.
- **The dock took 82% of a phone in landscape** at its fixed 320px. It now
tracks the viewport (`clamp(180px, 42dvh, 340px)`, tighter still under
500px of height).
- **The off-canvas drawer had no way out but the button that opened it.**
Added a scrim that closes it, Esc, and auto-close when a run is picked —
and it closes itself if the window grows past the breakpoint, which
otherwise left a scrim over a sidebar that was no longer a drawer.
- **The stepper scrolls horizontally on a phone**, so advancing to an
off-screen step looked like nothing happened; the active step is scrolled
into view.
Device-type rules rather than width alone: `pointer: coarse` gets 38-44px hit
targets and 16px inputs (under 16px, iOS zooms the page on focus and breaks the
layout the user is typing into); `prefers-reduced-motion` drops the drawer
slide and the progress animation, which are decoration.
Scrolling stays where it belongs — one scroller per pane (`.wizard-body`,
`.run-body`, `.dash-body`, `.sb-groups`, `.modal-body`, `.term-host`), wide
tables scroll inside `.table-wrap`, and the page itself never scrolls
horizontally at any tested size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6475dba752
commit
4f277838c6
+23
-4
@@ -138,6 +138,10 @@ function goToStep(n) {
|
||||
show($('#btnLaunch'), state.step === STEP_COUNT - 1);
|
||||
if (state.step === STEP_COUNT - 1) renderReview();
|
||||
updateWizardSummary();
|
||||
// The stepper scrolls horizontally on a phone; advancing to a step that is
|
||||
// off-screen would look like nothing happened.
|
||||
const active = $('.step-tab.active');
|
||||
if (active?.scrollIntoView) active.scrollIntoView({ block: 'nearest', inline: 'center', behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Errors land next to the field they belong to. A modal alert forced the
|
||||
@@ -1902,6 +1906,7 @@ document.addEventListener('keydown', (e) => {
|
||||
if (!$('#findingModal').hidden) return show($('#findingModal'), false);
|
||||
if (!$('#leadModal').hidden) return closeLeadModal();
|
||||
if (!$('#authModal').hidden) return show($('#authModal'), false);
|
||||
if ($('#sidebar').classList.contains('open')) return setSidebar(false);
|
||||
if (!$('#termDock').hidden) termClose();
|
||||
});
|
||||
|
||||
@@ -2250,12 +2255,26 @@ $('#runFilter').addEventListener('input', (e) => { state.runFilter = e.target.va
|
||||
// boot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Below 768px the sidebar slides off-canvas. It previously had no way back:
|
||||
// Below 900px the sidebar slides off-canvas. It previously had no way back:
|
||||
// the CSS hid it and nothing could set .open.
|
||||
$('#btnSidebarToggle').addEventListener('click', () => $('#sidebar').classList.toggle('open'));
|
||||
$('#sbGroups').addEventListener('click', () => {
|
||||
if (window.matchMedia('(max-width: 768px)').matches) $('#sidebar').classList.remove('open');
|
||||
const NARROW = '(max-width: 900px)';
|
||||
function setSidebar(open) {
|
||||
$('#sidebar').classList.toggle('open', open);
|
||||
show($('#sidebarScrim'), open && window.matchMedia(NARROW).matches);
|
||||
$('#btnSidebarToggle').setAttribute('aria-expanded', String(open));
|
||||
}
|
||||
$('#btnSidebarToggle').addEventListener('click', () => setSidebar(!$('#sidebar').classList.contains('open')));
|
||||
$('#sidebarScrim').addEventListener('click', () => setSidebar(false));
|
||||
// Picking a run is the drawer's whole purpose — it should get out of the way
|
||||
// once you have.
|
||||
$('#sbGroups').addEventListener('click', (e) => {
|
||||
if (e.target.closest('.sb-run') && window.matchMedia(NARROW).matches) setSidebar(false);
|
||||
});
|
||||
$('#btnDashboard').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
|
||||
$('#btnNewEngagement').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
|
||||
// Growing the window past the breakpoint leaves a scrim over a sidebar that is
|
||||
// no longer a drawer.
|
||||
window.matchMedia(NARROW).addEventListener('change', (m) => { if (!m.matches) setSidebar(false); });
|
||||
|
||||
async function boot() {
|
||||
applyTheme();
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button class="sidebar-toggle" id="btnSidebarToggle" title="Show / hide run history" aria-label="Show or hide run history">☰</button>
|
||||
<button class="sidebar-toggle" id="btnSidebarToggle" title="Show / hide run history" aria-label="Show or hide run history" aria-expanded="false" aria-controls="sidebar">☰</button>
|
||||
<div class="sidebar-scrim" id="sidebarScrim" hidden></div>
|
||||
|
||||
<div class="app">
|
||||
|
||||
|
||||
+106
-18
@@ -91,7 +91,11 @@ a { color: var(--accent); text-decoration: none; }
|
||||
|
||||
/* ============================================================ AppShell */
|
||||
|
||||
.app { display: flex; height: 100vh; overflow: hidden; }
|
||||
/* 100vh is wrong on mobile: it measures the viewport WITHOUT the browser's
|
||||
collapsing address bar, so the bottom of the app sits under it and the
|
||||
footer/CTA is unreachable until you scroll the chrome away. dvh tracks the
|
||||
visible box; the vh line stays as the fallback for older engines. */
|
||||
.app { display: flex; height: 100vh; height: 100dvh; overflow: hidden; }
|
||||
|
||||
.sidebar {
|
||||
width: 248px; flex: none; background: var(--surface); border-right: 1px solid var(--border);
|
||||
@@ -402,7 +406,12 @@ textarea { resize: vertical; min-height: 72px; }
|
||||
|
||||
.runpage { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.runpage[hidden] { display: none; }
|
||||
.run-head { display: flex; align-items: flex-start; justify-content: space-between; padding: var(--sp-5) var(--sp-5) var(--sp-4); border-bottom: 1px solid var(--border); }
|
||||
.run-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); padding: var(--sp-5) var(--sp-5) var(--sp-4); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
||||
/* The header is a title plus a row of buttons. Without a flex basis the
|
||||
buttons win the width fight and the title wraps one character per line —
|
||||
which is exactly what happened at 390px. */
|
||||
.run-head-main { flex: 1 1 260px; }
|
||||
.run-actions { flex: 0 0 auto; flex-wrap: wrap; }
|
||||
.run-head-main { min-width: 0; }
|
||||
.run-target { font-size: 15px; font-weight: 600; font-family: var(--mono); overflow-wrap: anywhere; }
|
||||
.run-meta { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-1); font-size: 12px; color: var(--text-dim); font-family: var(--mono); }
|
||||
@@ -446,7 +455,7 @@ textarea { resize: vertical; min-height: 72px; }
|
||||
/* The canvas is a fixed viewport that the graph pans inside — letting the box
|
||||
grow to the graph's height (1187px on a 27-finding run) meant scrolling the
|
||||
page blind, with no way to see the shape of the path. */
|
||||
.ap-canvas-wrap { position: relative; height: min(60vh, 560px); border-radius: var(--radius-md); overflow: hidden; background: var(--surface-2); border: 1px solid var(--border); cursor: grab; touch-action: none; }
|
||||
.ap-canvas-wrap { position: relative; height: min(60dvh, 560px); min-height: 240px; border-radius: var(--radius-md); overflow: hidden; background: var(--surface-2); border: 1px solid var(--border); cursor: grab; touch-action: none; }
|
||||
.ap-canvas-wrap.dragging { cursor: grabbing; }
|
||||
.ap-canvas { display: block; width: 100%; height: 100%; }
|
||||
.ap-canvas text { font-family: var(--sans); }
|
||||
@@ -533,7 +542,7 @@ textarea { resize: vertical; min-height: 72px; }
|
||||
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(10,9,8,.45); display: flex; align-items: center; justify-content: center; z-index: 60; }
|
||||
.modal-overlay[hidden] { display: none; }
|
||||
.modal { width: 620px; max-width: calc(100vw - 40px); max-height: calc(100vh - 80px); background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-float); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border); }
|
||||
.modal { width: 620px; max-width: calc(100vw - 40px); max-height: calc(100vh - 80px); max-height: calc(100dvh - 80px); background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-float); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border); }
|
||||
.modal-sm { width: 520px; }
|
||||
.modal-lg { width: 760px; }
|
||||
.modal-foot { display: flex; justify-content: flex-end; gap: var(--sp-2); padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border); }
|
||||
@@ -560,7 +569,9 @@ textarea { resize: vertical; min-height: 72px; }
|
||||
floating drawer covered "Next →" and "Start Exploitation". */
|
||||
|
||||
.term-dock {
|
||||
position: relative; flex: none; height: 320px; min-height: 140px;
|
||||
/* A fixed 320px dock eats 82% of a phone in landscape. Track the viewport
|
||||
and keep a floor that still shows a usable number of rows. */
|
||||
position: relative; flex: none; height: clamp(180px, 42dvh, 340px); min-height: 140px;
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
background: var(--term-bg); color: var(--term-fg); border-top: 1px solid var(--border-strong);
|
||||
}
|
||||
@@ -572,7 +583,7 @@ textarea { resize: vertical; min-height: 72px; }
|
||||
body.resizing-ns { user-select: none; cursor: ns-resize; }
|
||||
|
||||
.term-head {
|
||||
display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
|
||||
display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); flex-wrap: wrap;
|
||||
background: rgba(255,255,255,.03); border-bottom: 1px solid rgba(255,255,255,.07);
|
||||
font-size: 11.5px; color: #a5a099;
|
||||
}
|
||||
@@ -616,28 +627,105 @@ body.resizing-ns { user-select: none; cursor: ns-resize; }
|
||||
|
||||
/* ============================================================ Responsive */
|
||||
|
||||
.sidebar-scrim {
|
||||
display: none; position: fixed; inset: 0; z-index: 54; background: rgba(10, 9, 8, .45);
|
||||
}
|
||||
.sidebar-scrim[hidden] { display: none !important; }
|
||||
.sidebar-toggle {
|
||||
display: none; position: fixed; left: 10px; top: 10px; z-index: 60;
|
||||
width: 32px; height: 32px; border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar { width: 208px; }
|
||||
/* Wide screens: line length, not width, is the constraint — a findings table
|
||||
stretched across 2560px is harder to read, not easier. */
|
||||
@media (min-width: 1600px) {
|
||||
.dash-body > *, .run-body > * { max-width: 1500px; margin-left: auto; margin-right: auto; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.sidebar { width: 216px; }
|
||||
}
|
||||
|
||||
/* Tablet portrait and small laptops: the sidebar is the first thing to go,
|
||||
because the main pane is where the work happens. */
|
||||
@media (max-width: 900px) {
|
||||
.sidebar-toggle { display: block; }
|
||||
.sidebar { position: fixed; left: -260px; top: 0; bottom: 0; width: 240px; z-index: 55; transition: left .2s; box-shadow: var(--shadow-float); }
|
||||
.sidebar {
|
||||
position: fixed; left: -280px; top: 0; bottom: 0; width: min(280px, 82vw); z-index: 55;
|
||||
transition: left .2s ease; box-shadow: var(--shadow-float);
|
||||
}
|
||||
.sidebar.open { left: 0; }
|
||||
.topbar, .run-head { padding-left: 52px; }
|
||||
/* Tapping the page behind an open drawer must close it — otherwise the only
|
||||
way out is the same small button that opened it. */
|
||||
.sidebar-scrim { display: block; }
|
||||
.topbar, .run-head { padding-left: 56px; }
|
||||
.field-row { flex-direction: column; }
|
||||
.modal { width: calc(100vw - 24px); }
|
||||
.review-grid { grid-template-columns: 1fr; }
|
||||
.term-dock { height: 260px; }
|
||||
.review-grid { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.topbar { flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4) var(--sp-3) 56px; }
|
||||
.topbar-sub { display: none; }
|
||||
.run-head { flex-direction: column; padding: var(--sp-4) var(--sp-4) var(--sp-3) 56px; }
|
||||
/* In the column layout the title block must size to its content — the
|
||||
desktop `flex: 1` made it grow and pushed the buttons to the bottom of a
|
||||
tall empty header. */
|
||||
.run-head-main { flex: 0 0 auto; width: 100%; }
|
||||
.run-actions { width: 100%; }
|
||||
.run-actions .btn { flex: 1 1 auto; justify-content: center; }
|
||||
.run-facts { display: block; }
|
||||
.run-facts span { display: inline; }
|
||||
.wizard-body, .run-body, .dash-body { padding: var(--sp-4) var(--sp-3) var(--sp-6); }
|
||||
.wizard-panel { gap: var(--sp-4); }
|
||||
.modal { width: calc(100vw - 16px); max-width: none; max-height: calc(100dvh - 32px); }
|
||||
.modal-body, .modal-head, .modal-tabs, .modal-foot { padding-left: var(--sp-4); padding-right: var(--sp-4); }
|
||||
.review-grid, .dash-grid, .stat-row { grid-template-columns: 1fr; }
|
||||
.data-table { min-width: 560px; }
|
||||
.lead-toolbar .btn { flex: 1 1 auto; justify-content: center; }
|
||||
.search-wrap { max-width: none; }
|
||||
.term-status { display: none; }
|
||||
.term-target { max-width: 150px; }
|
||||
.bar-row { grid-template-columns: 76px 1fr 34px; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.topbar { flex-wrap: wrap; gap: var(--sp-2); }
|
||||
.stepper { padding: 0 var(--sp-3); }
|
||||
.wizard-footer { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
|
||||
.footer-actions { justify-content: flex-end; }
|
||||
.stepper { padding: 0 var(--sp-2); }
|
||||
.step-tab { padding: var(--sp-3) var(--sp-2) var(--sp-2); }
|
||||
.wizard-footer { flex-direction: column; align-items: stretch; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); }
|
||||
.footer-actions .btn { flex: 1 1 auto; justify-content: center; }
|
||||
.mode-tiles { grid-template-columns: 1fr 1fr; }
|
||||
.fair-likely .v { font-size: 24px; }
|
||||
.stat-v { font-size: 26px; }
|
||||
}
|
||||
|
||||
/* Phone in landscape: ~390px of height, most of it browser chrome. Anything
|
||||
with a fixed vertical share has to shrink or the content disappears. */
|
||||
@media (max-height: 500px) and (orientation: landscape) {
|
||||
.topbar, .run-head { padding-top: var(--sp-2); padding-bottom: var(--sp-2); min-height: 0; }
|
||||
.term-dock { height: clamp(120px, 46dvh, 220px); }
|
||||
.ap-canvas-wrap { height: 58dvh; min-height: 160px; }
|
||||
.term-hints { display: none; }
|
||||
}
|
||||
|
||||
/* Touch devices: a 26px button is a miss target, and iOS zooms the whole page
|
||||
when a focused input is under 16px — which then breaks the layout the user
|
||||
was typing into. */
|
||||
@media (pointer: coarse) {
|
||||
.icon-btn { width: 38px; height: 38px; }
|
||||
.btn { min-height: 40px; }
|
||||
.btn-sm { min-height: 34px; }
|
||||
.sb-run { padding: var(--sp-3) var(--sp-2); }
|
||||
.run-tab, .step-tab, .modal-tab { min-height: 44px; }
|
||||
input[type="text"], input[type="number"], input[type="password"], select, textarea { font-size: 16px; }
|
||||
.switch { width: 38px; height: 22px; }
|
||||
.switch .thumb { width: 18px; height: 18px; }
|
||||
.switch input:checked + .track + .thumb { transform: translateX(16px); }
|
||||
.switch input:indeterminate + .track + .thumb { transform: translateX(8px); }
|
||||
}
|
||||
|
||||
/* Respect a reduced-motion preference: the drawer slide and the progress
|
||||
animation are decoration, not information. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user