diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b8742..32daadb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,11 @@ All notable user-visible changes to this project are documented in this file. - After an SSE reconnect the viewer refetches the selected session, so snippets and comments that arrived during the gap can no longer be silently missing from a live-looking board. - +- The viewer layout no longer breaks at phone widths: below 700px the + sidebar collapses into a drawer behind a slim top bar (hamburger toggle, + unread dot), the stream takes the full width, and hover-only actions + (card open/delete, session delete) stay visible on narrow or touch + screens. - Comments not attached to a snippet (e.g. `sideshow comment` without `--snippet`) were stored and delivered to agents but never shown in the viewer; they now render in the session thread. diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index 9a29ae8..6e55c31 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -183,6 +183,31 @@ test("activity in an unselected session badges the tab title until viewed", asyn await expect(page).toHaveTitle("sideshow"); }); +test("at phone width the sidebar collapses into a drawer and actions stay visible", async ({ + page, + server, +}) => { + await publish(server.url, { html: "
m
", title: "Mobile", agent: "e2e" }); + + await page.setViewportSize({ width: 375, height: 667 }); + await page.goto(server.url); + + // the sidebar is off-canvas and the stream gets the full width + const card = page.locator(".card:not(#sessionThread)"); + await expect(card).toBeVisible(); + await expect(page.locator("aside")).not.toBeInViewport(); + expect((await card.boundingBox())!.width).toBeGreaterThan(300); + + // hover-only card actions are always visible at narrow widths + await expect(card.locator(".act.open")).toHaveCSS("opacity", "1"); + + // the menu button opens the drawer; picking a session closes it again + await page.locator("#menuBtn").click(); + await expect(page.locator("aside")).toBeInViewport(); + await page.locator(".sess").click(); + await expect(page.locator("aside")).not.toBeInViewport(); +}); + test("version select appears live after an update", async ({ page, server }) => { const snippet = await publish(server.url, { html: "v1
", title: "Doc", agent: "e2e" }); diff --git a/viewer/index.html b/viewer/index.html index 8db15fa..50bc45a 100644 --- a/viewer/index.html +++ b/viewer/index.html @@ -73,14 +73,14 @@ font-weight: 500; letter-spacing: 0.01em; } - #live { + .livedot { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); transition: background 0.3s; } - #live.on { + .livedot.on { background: #4caf78; } #sessionList { @@ -426,6 +426,123 @@ color: var(--text); } + /* phone widths: the sidebar becomes an off-canvas drawer behind a slim + top bar; the stream keeps the full width */ + .topbar { + display: none; + } + #scrim { + display: none; + } + @media (max-width: 700px) { + #app { + flex-direction: column; + } + .topbar { + display: flex; + align-items: center; + gap: 4px; + flex: none; + padding: 8px 10px; + background: var(--panel); + border-bottom: 0.5px solid var(--border); + } + .topbar .brand { + padding: 0; + } + .topbar .menu { + position: relative; + font-size: 17px; + line-height: 1; + color: var(--muted); + background: none; + border: none; + border-radius: 8px; + padding: 6px 9px; + cursor: pointer; + font-family: inherit; + } + .topbar .menu:hover { + color: var(--text); + background: var(--hover); + } + .topbar .menu .dot { + position: absolute; + top: 3px; + right: 4px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); + display: none; + } + .topbar .menu .dot.show { + display: block; + } + aside { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 30; + width: min(280px, 84vw); + transform: translateX(-105%); + transition: transform 0.2s ease; + } + body.nav-open aside { + transform: none; + box-shadow: 0 0 32px rgba(0, 0, 0, 0.25); + } + #scrim { + display: block; + position: fixed; + inset: 0; + z-index: 25; + background: rgba(0, 0, 0, 0.35); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + } + body.nav-open #scrim { + opacity: 1; + pointer-events: auto; + } + main { + min-height: 0; + } + .session-head { + padding: 12px 16px 10px; + } + #stream { + padding: 16px 14px 120px; + } + #onboard { + padding: 40px 18px; + } + .card-title { + min-width: 0; + flex: 0 1 auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + /* narrow or touch: hover-revealed actions must stay reachable */ + @media (max-width: 700px), (hover: none) { + .card-head .act { + opacity: 1; + } + .sess .x { + opacity: 1; + } + .sess-title { + padding-right: 44px; + } + .sess .dot { + right: 32px; + } + } + #toast { position: fixed; left: 50%; @@ -454,8 +571,14 @@