Skip to content

Commit c9e7bda

Browse files
quic-boyucclaude
andcommitted
observatory: push-mode sidebar + natural compare-mode column widths
The left panel was a `position: fixed` overlay (300 px wide, covering the main graph area when open). Two issues: 1. **Overlay blocked the graph.** Sidebar slid in over the main panel via `transform: translateX(-100%)` → `translateX(0)`, never affecting layout. 2. **Compare-mode columns were squeezed.** `grid-template-columns: repeat(N, 1fr)` divided the fixed 300 px equally, making N-archive columns unreadably narrow. Fix: * **Push layout.** `.index-pane` moves from `position: fixed` to an in-flow flex child of `.container`. Closed state: `width: 0; overflow: hidden`. Open state: `width: var(--sidebar-w)` (driven by JS). `.main-pane` is the second flex child and naturally gets the remaining width, so the main area shrinks/grows as the sidebar opens/closes. * **Dynamic sidebar width via CSS variable.** JS sets `--sidebar-w` on `:root` at toggle time and on `renderIndex` when the archive count changes (single archive: 300 px; N archives: N × 200 px, capped at 50 vw). The `sidebar-toggle-btn` reads `left: var(--sidebar-w, 0)` so it always sits at the sidebar's right edge with no JS positional update. * **Natural column widths.** `grid-template-columns` changes from `repeat(N, minmax(0, 1fr))` to `repeat(N, 200px)` with `overflow-x: auto` on the grid container. Each archive column is 200 px; the panel expands to accommodate all of them. * **Hover-to-peek removed.** `.index-pane-trigger` (the invisible fixed strip that revealed the sidebar on hover) is removed from the HTML and hidden via CSS. It served the overlay model; in push mode it would unexpectedly push the main content aside on every mouse-near-edge hover. Verified: push mode (`mainLeft === sidebarWidth`), `--sidebar-w` driven correctly for single/compare archives, 98 observatory tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a66f82a commit c9e7bda

2 files changed

Lines changed: 41 additions & 35 deletions

File tree

devtools/observatory/templates/css/main.css

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
/* Sidebar toggle tab — always-visible click target on left edge */
144144
.sidebar-toggle-btn {
145145
position: fixed;
146-
left: 0;
146+
left: var(--sidebar-w, 0);
147147
top: 50%;
148148
transform: translateY(-50%);
149149
z-index: 210;
@@ -171,7 +171,6 @@
171171
box-shadow: 3px 0 12px var(--shadow);
172172
width: 26px;
173173
}
174-
.sidebar-toggle-btn.open { left: 300px; }
175174
[data-theme="dark"] .sidebar-toggle-btn {
176175
background: var(--header-bg);
177176
border-color: rgba(255,255,255,0.15);
@@ -187,48 +186,29 @@
187186

188187
/* Sidebar trigger zone — invisible strip on left edge (hover fallback) */
189188
.index-pane-trigger {
190-
position: fixed;
191-
left: 0;
192-
top: 0;
193-
bottom: 0;
194-
width: 12px;
195-
z-index: 200;
189+
display: none;
196190
}
197191

198192
/* Index Pane (Sidebar)
199-
Starts below the sticky header (z-index: 200) so the index
200-
header (which carries Tree-view / Select-items toggles) is
201-
never obscured. The pane itself sits at z-index: 199. */
193+
In-flow flex child of .container — not a fixed overlay.
194+
Width transitions between 0 (closed) and var(--sidebar-w) (open).
195+
The toggle button position is driven by the same --sidebar-w variable. */
202196
.index-pane {
203-
position: fixed;
204-
left: 0;
205-
top: 60px;
206-
bottom: 0;
207-
width: 300px;
197+
flex-shrink: 0;
198+
width: 0;
199+
overflow: hidden;
208200
background: var(--bg-secondary);
209201
border-right: 1px solid var(--border-color);
210202
display: flex;
211203
flex-direction: column;
212-
flex-shrink: 0;
213-
z-index: 199;
214-
transform: translateX(-100%);
215-
transition: transform 0.25s ease, box-shadow 0.25s ease;
216-
box-shadow: none;
217-
}
218-
219-
/* When the sticky header is hidden the pane reclaims the full
220-
viewport. */
221-
body:has(header.hidden) .index-pane {
222-
top: 0;
204+
transition: width 0.25s ease;
223205
}
224-
225-
/* Show when trigger zone or pane itself is hovered, or pinned open */
226-
.index-pane-trigger:hover ~ .index-pane,
227-
.index-pane:hover,
228206
.index-pane.pinned {
229-
transform: translateX(0);
230-
box-shadow: 4px 0 16px var(--shadow);
207+
width: var(--sidebar-w, 300px);
208+
overflow-y: auto;
209+
overflow-x: hidden;
231210
}
211+
/* header.hidden: full-height is handled by flex; no top offset needed */
232212

233213
.index-header {
234214
padding: 1rem;
@@ -493,7 +473,8 @@
493473
the renderer adds the `compare-mode` class. */
494474
.index-list.compare-mode {
495475
display: grid;
496-
grid-template-columns: repeat(var(--archive-cols, 1), minmax(0, 1fr));
476+
grid-template-columns: repeat(var(--archive-cols, 1), 200px);
477+
overflow-x: auto;
497478
gap: 0.5rem;
498479
padding: 0.5rem;
499480
align-items: start;

devtools/observatory/templates/js/02_layout.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@
77
let sidebarPinned = false;
88
try { sidebarPinned = localStorage.getItem('obs_sidebar_pinned') === '1'; } catch(e) {}
99

10+
// Width constants for push-layout sidebar.
11+
// Single-archive mode uses SIDEBAR_SINGLE_W; compare mode gives each
12+
// archive column ARCHIVE_COL_W px, so the panel expands with N archives.
13+
const SIDEBAR_SINGLE_W = 300;
14+
const ARCHIVE_COL_W = 200;
15+
16+
function _sidebarTargetWidth() {
17+
const archives = (state.data && state.data.archives) || [];
18+
if (archives.length <= 1) return SIDEBAR_SINGLE_W;
19+
const natural = archives.length * ARCHIVE_COL_W + (archives.length - 1) * 8 + 16;
20+
return Math.min(natural, Math.round(window.innerWidth * 0.5));
21+
}
22+
23+
// Writes --sidebar-w on :root, which drives both .index-pane width and
24+
// .sidebar-toggle-btn left via CSS variable.
25+
function _applySidebarWidth(open) {
26+
const w = open ? _sidebarTargetWidth() : 0;
27+
document.documentElement.style.setProperty('--sidebar-w', w + 'px');
28+
}
29+
1030
function renderLayout() {
1131
const icon = state.theme === 'dark' ? '☀️' : '🌙';
1232
OBS.app.innerHTML = `
@@ -24,7 +44,6 @@
2444
</div>
2545
</header>
2646
<div class="container">
27-
<div class="index-pane-trigger"></div>
2847
<button class="sidebar-toggle-btn ${sidebarPinned ? 'open' : ''}"
2948
id="sidebar-toggle-btn"
3049
onclick="toggleSidebar()"
@@ -40,6 +59,7 @@
4059
<main id="main-pane" class="main-pane"></main>
4160
</div>
4261
`;
62+
_applySidebarWidth(sidebarPinned);
4363
updateIndexHeader();
4464
}
4565

@@ -49,6 +69,7 @@
4969

5070
const pane = document.getElementById('index-pane');
5171
const btn = document.getElementById('sidebar-toggle-btn');
72+
_applySidebarWidth(sidebarPinned);
5273
if (pane) pane.classList.toggle('pinned', sidebarPinned);
5374
if (btn) {
5475
btn.classList.toggle('open', sidebarPinned);
@@ -354,6 +375,9 @@
354375
if (archives.length > 1) {
355376
list.classList.add('compare-mode');
356377
list.style.setProperty('--archive-cols', String(archives.length));
378+
// Re-compute panel width if sidebar is already open (archive count may
379+
// have changed when loading a compare report).
380+
if (sidebarPinned) _applySidebarWidth(true);
357381
let html = '';
358382
archives.forEach((archive) => {
359383
html += _renderArchiveColumn(archive, sessions, records, useTree);
@@ -366,6 +390,7 @@
366390
// Single-archive: flat list of session dashboards + records.
367391
list.classList.remove('compare-mode');
368392
list.style.removeProperty('--archive-cols');
393+
if (sidebarPinned) _applySidebarWidth(true);
369394
let html = '';
370395
sessions.forEach((session) => {
371396
html += _renderSessionDashboardLink(session);

0 commit comments

Comments
 (0)