From fcd4bbcb7f207d4237a54b7977f4f2900e7f97ec Mon Sep 17 00:00:00 2001 From: Tridhatri Vallamkondu Date: Thu, 25 Jun 2026 12:51:47 -0400 Subject: [PATCH 1/2] Tighten mobile viewer layout --- e2e/fixtures.ts | 46 +++- e2e/uploads.spec.ts | 139 +++++++++- e2e/viewer.spec.ts | 111 +++++++- viewer/src/SessionTimeline.tsx | 23 +- viewer/src/embed.tsx | 1 + viewer/src/styles.css | 451 +++++++++++++++++++++++++++++++-- 6 files changed, 734 insertions(+), 37 deletions(-) diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index 7f168ff..260cde3 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -1,4 +1,4 @@ -import { test as base } from "@playwright/test"; +import { expect, test as base, type Locator, type Page } from "@playwright/test"; import { type ChildProcess, spawn } from "node:child_process"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -67,7 +67,7 @@ export const publicReadTest = base.extend<{ publicReadServer: PublicReadServer } }, }); -export { expect } from "@playwright/test"; +export { expect }; export async function publish( serverUrl: string, @@ -127,3 +127,45 @@ export async function publishParts( if (!res.ok) throw new Error(`publishParts failed: ${res.status}`); return res.json() as Promise<{ id: string; sessionId: string; version: number }>; } + +export async function expectNoHorizontalOverflow(page: Page, selector: string) { + await expect.poll(() => page.locator(selector).count()).toBeGreaterThan(0); + await expect + .poll(() => + page + .locator(selector) + .evaluateAll((elements) => + Math.max(0, ...elements.map((el) => Math.ceil(el.scrollWidth - el.clientWidth))), + ), + ) + .toBeLessThanOrEqual(1); +} + +export async function expectIframesNoHorizontalOverflow(page: Page, container: Locator) { + const frameUrls = await container + .locator("iframe") + .evaluateAll((frames) => frames.map((frame) => (frame as HTMLIFrameElement).src)); + expect(frameUrls.length).toBeGreaterThan(0); + + await expect + .poll(async () => { + const childFrames = frameUrls + .map((url) => page.frames().find((frame) => frame.url() === url)) + .filter((frame) => frame !== undefined); + if (childFrames.length < frameUrls.length) return Number.POSITIVE_INFINITY; + const overflows = await Promise.all( + childFrames.map((frame) => + frame.evaluate(() => { + if (document.readyState === "loading") return Number.POSITIVE_INFINITY; + const doc = document.documentElement; + const body = document.body; + const scrollWidth = Math.max(doc.scrollWidth, body?.scrollWidth ?? 0); + const clientWidth = Math.max(doc.clientWidth, body?.clientWidth ?? 0); + return Math.ceil(scrollWidth - clientWidth); + }), + ), + ); + return Math.max(0, ...overflows); + }) + .toBeLessThanOrEqual(1); +} diff --git a/e2e/uploads.spec.ts b/e2e/uploads.spec.ts index 4c0be9f..0ea158f 100644 --- a/e2e/uploads.spec.ts +++ b/e2e/uploads.spec.ts @@ -1,4 +1,13 @@ -import { expect, publish, publishParts, test, TINY_PNG_B64, upload } from "./fixtures.ts"; +import { + expect, + expectIframesNoHorizontalOverflow, + expectNoHorizontalOverflow, + publish, + publishParts, + test, + TINY_PNG_B64, + upload, +} from "./fixtures.ts"; test("an image part renders an served from /a/:id", async ({ page, server }) => { const asset = await upload(server.url, { @@ -42,7 +51,7 @@ test("a trace part renders a step timeline with expandable detail", async ({ pag }); await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await expect(card.locator(".trace-title")).toHaveText("What I did"); await expect(card.locator(".trace-step")).toHaveCount(2); await expect(card.locator(".trace-kind").first()).toHaveText("tool"); @@ -72,13 +81,137 @@ test("a trace part backed by an uploaded file offers a download and renders step }); await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await expect(card.locator(".trace-dl")).toHaveAttribute("href", `/a/${asset.id}`); // steps are fetched from the asset and rendered await expect(card.locator(".trace-step")).toHaveCount(2); await expect(card.locator(".trace-label").first()).toHaveText("step one"); }); +test("a trace part stays readable on an iPhone-sized viewport", async ({ page, server }) => { + await publishParts(server.url, { + title: "Trace on mobile", + agent: "e2e", + parts: [ + { + kind: "trace", + title: "Long trace heading that should truncate cleanly on mobile", + steps: [ + { + label: + "ran a very long shell command with flags --workspace=/tmp/sideshow-mobile --include-traces --verify-sidebar", + kind: "shell", + ts: "2026-06-25T12:00:00Z", + detail: + "stdout: a-long-token-that-should-wrap-instead-of-forcing-horizontal-scroll ".repeat( + 6, + ), + }, + { + label: "noted how the sidebar drawer and trace card read together on a phone", + kind: "say", + ts: "2026-06-25T12:00:03Z", + }, + ], + }, + ], + }); + + await page.setViewportSize({ width: 393, height: 852 }); + await page.goto(server.url); + + const card = page.locator(".card:not(#whatsNew)"); + await expect(card.locator(".trace-title")).toBeVisible(); + await expect(card.locator(".trace-step")).toHaveCount(2); + await card.locator(".trace-row.clickable").first().click(); + await expect(card.locator(".trace-detail")).toBeVisible(); + + await expectNoHorizontalOverflow(page, "main"); + await expectNoHorizontalOverflow(page, ".card"); + await expectNoHorizontalOverflow(page, ".tracepart"); +}); + +test("all native surface primitives fit the iPhone 14 Pro viewer", async ({ page, server }) => { + const asset = await upload(server.url, { + data: TINY_PNG_B64, + contentType: "image/png", + filename: "mobile-primitive.png", + kind: "image", + }); + await publishParts(server.url, { + title: "Every primitive on mobile", + agent: "e2e", + session: asset.sessionId, + parts: [ + { kind: "html", html: "

HTML

Interactive surface shell.

" }, + { kind: "markdown", markdown: "## Markdown\n\n- readable prose\n- list item" }, + { + kind: "diff", + patch: + "diff --git a/app.ts b/app.ts\n--- a/app.ts\n+++ b/app.ts\n@@ -1 +1 @@\n-old mobile spacing\n+new mobile spacing\n", + }, + { + kind: "terminal", + text: "\u001b[32mPASS\u001b[0m mobile primitive check\n$ sideshow trace-sync --all", + title: "terminal", + }, + { kind: "image", assetId: asset.id, caption: "uploaded image primitive" }, + { kind: "mermaid", mermaid: "flowchart LR\n A[Agent] --> B[sideshow]\n B --> C[Phone]" }, + { + kind: "json", + data: { + status: "ok", + primitives: [ + "html", + "markdown", + "diff", + "terminal", + "image", + "mermaid", + "json", + "code", + "trace", + ], + }, + }, + { + kind: "code", + language: "ts", + title: "mobile.ts", + code: "export const mobilePrimitive = (kind: string) => `${kind}: ok`;\n", + }, + { + kind: "trace", + title: "Trace primitive", + steps: [ + { + kind: "tool", + label: "verified every native primitive on iPhone 14 Pro", + detail: "html markdown diff terminal image mermaid json code trace", + }, + ], + }, + ], + }); + + await page.setViewportSize({ width: 393, height: 852 }); + await page.goto(server.url); + + const card = page.locator(".card:not(#whatsNew)"); + await expect(card).toBeVisible(); + await expect(card.locator("iframe").first()).toBeVisible(); + await expectIframesNoHorizontalOverflow(page, card); + await expect(card.locator(".asset-img")).toBeVisible(); + await expect(card.locator(".jsonpart")).toContainText("primitives"); + await expect(card.locator(".trace-step")).toHaveCount(1); + await card.locator(".trace-row.clickable").click(); + await expect(card.locator(".trace-detail")).toBeVisible(); + + await expectNoHorizontalOverflow(page, "main"); + await expectNoHorizontalOverflow(page, ".card"); + await expectNoHorizontalOverflow(page, ".tracepart"); +}); + test("an uploaded image embeds by URL inside an html part under the CSP", async ({ page, server, diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index a3664c3..bc91cfc 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -1,4 +1,11 @@ -import { expect, publish, test, update } from "./fixtures.ts"; +import { + expect, + expectNoHorizontalOverflow, + publish, + publishParts, + test, + update, +} from "./fixtures.ts"; test("the sidebar groups sessions by recency and sinks empty ones to the bottom", async ({ page, @@ -105,7 +112,7 @@ test("comment typed in the composer round-trips to the API", async ({ page, serv const snippet = await publish(server.url, { html: "

v1

", title: "Doc", agent: "e2e" }); await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await card.locator(".act.comment").click(); const input = card.locator(".composer input"); await input.fill("ship it"); @@ -138,7 +145,7 @@ test("a comment's copy button puts an agent-ready paste block on the clipboard", } await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await card.locator(".act.comment").click(); const input = card.locator(".composer input"); await input.fill("tighten the spacing"); @@ -165,7 +172,7 @@ test("a failed comment send restores the input instead of losing the message", a await publish(server.url, { html: "

x

", title: "Doc", agent: "e2e" }); await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await page.route("**/api/comments", (route) => route.request().method() === "POST" ? route.abort() : route.fallback(), ); @@ -192,7 +199,7 @@ test("a comment echoes immediately, before the SSE round-trip confirms it", asyn await publish(server.url, { html: "

x

", title: "Doc", agent: "e2e" }); await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); // hold the POST open so only the optimistic echo can render await page.route("**/api/comments", async (route) => { if (route.request().method() !== "POST") return route.fallback(); @@ -220,7 +227,7 @@ test("a comment containing raw HTML is sandboxed and escaped, never a live node" await publish(server.url, { html: "

x

", title: "Doc", agent: "e2e" }); await page.goto(server.url); - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await card.locator(".act.comment").click(); const input = card.locator(".composer input"); await input.fill(" hi"); @@ -306,12 +313,34 @@ test("at phone width the sidebar collapses into a drawer and actions stay visibl server, }) => { await publish(server.url, { html: "

m

", title: "Mobile", agent: "e2e" }); + const longSession = (await ( + await fetch(`${server.url}/api/sessions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + agent: "e2e", + title: "A deliberately long mobile sidebar session title that should not shove controls", + }), + }) + ).json()) as { id: string }; + await publish(server.url, { + html: "

long session row

", + title: "Long title mobile", + agent: "e2e", + session: longSession.id, + }); - await page.setViewportSize({ width: 375, height: 667 }); - await page.goto(server.url); + await fetch(`${server.url}/api/sessions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ agent: "idle", title: "Empty session still shown in drawer" }), + }); + + await page.setViewportSize({ width: 393, height: 852 }); + await page.goto(`${server.url}/session/${longSession.id}`); // the sidebar is off-canvas and the stream gets the full width - const card = page.locator(".card"); + const card = page.locator(".card:not(#whatsNew)"); await expect(card).toBeVisible(); await expect(page.locator("aside")).not.toBeInViewport(); expect((await card.boundingBox())!.width).toBeGreaterThan(300); @@ -322,10 +351,72 @@ test("at phone width the sidebar collapses into a drawer and actions stay visibl // 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 expectNoHorizontalOverflow(page, "main"); + await expectNoHorizontalOverflow(page, "aside"); + const longSessionTitle = page.getByText("A deliberately long mobile sidebar", { exact: false }); + const longSessionRow = page.locator('[role="button"]').filter({ has: longSessionTitle }); + await expect(longSessionRow).toBeVisible(); + const deleteLongSession = longSessionRow.getByRole("button", { name: /^Delete session/ }); + await expect(deleteLongSession).toBeVisible(); + await deleteLongSession.click({ trial: true }); + await longSessionRow.click(); await expect(page.locator("aside")).not.toBeInViewport(); }); +test("timeline traces wrap cleanly at iPhone 14 Pro width", async ({ page, server }) => { + const surface = await publishParts(server.url, { + title: "Timeline anchor", + agent: "e2e", + parts: [ + { kind: "markdown", markdown: "## Timeline card\n\nThe trace wraps around this card." }, + ], + }); + await fetch(`${server.url}/api/sessions/${surface.sessionId}/trace`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + steps: [ + { + kind: "prompt", + label: "prompt-" + "unbrokenprompttoken".repeat(8), + detail: + "A longer prompt detail that should expand without creating horizontal document scroll.", + }, + { + kind: "say", + label: "response-" + "unbrokenresponsetoken".repeat(8), + }, + { + kind: "shell", + label: + "npm run trace-check -- --device=iPhone14Pro --case=long-command-label-without-spaces", + detail: + "command output: " + + "unbroken-token-for-overflow-regression-".repeat(8) + + "\nsecond line with normal words", + }, + { kind: "say", label: "The timeline remains readable on a phone." }, + ], + reset: true, + }), + }); + + await page.setViewportSize({ width: 393, height: 852 }); + await page.goto(`${server.url}/session/${surface.sessionId}`); + await page.locator(".view-toggle button", { hasText: "Timeline" }).click(); + + await expect(page.locator(".timeline")).toBeVisible(); + await expect(page.getByText("prompt-unbrokenprompttoken", { exact: false })).toBeVisible(); + await page.getByRole("button", { name: "Show 1 work step" }).click(); + await expect(page.getByRole("button", { name: "Hide 1 work step" })).toBeVisible(); + await page.getByText("npm run trace-check", { exact: false }).click(); + await expect( + page.getByText("unbroken-token-for-overflow-regression", { exact: false }), + ).toBeVisible(); + await expectNoHorizontalOverflow(page, "main"); + await expectNoHorizontalOverflow(page, ".timeline"); +}); + test("the Connect Claude Code modal shows the plugin install commands", async ({ page, server, diff --git a/viewer/src/SessionTimeline.tsx b/viewer/src/SessionTimeline.tsx index af8089d..daf8269 100644 --- a/viewer/src/SessionTimeline.tsx +++ b/viewer/src/SessionTimeline.tsx @@ -107,7 +107,7 @@ function TurnBlock(props: { turn: Turn }) { events().filter((_, i) => i !== intentIdx() && i !== outcomeIdx()), ); return ( - <> +
{(p) => } = 0}> @@ -118,7 +118,7 @@ function TurnBlock(props: { turn: Turn }) { = 0}> - +
); } @@ -128,12 +128,23 @@ function TurnBlock(props: { turn: Turn }) { function WorkFold(props: { steps: TraceStep[] }) { const [open, setOpen] = createSignal(false); const n = () => props.steps.length; + const label = () => `${open() ? "Hide" : "Show"} ${n()} work ${n() === 1 ? "step" : "steps"}`; + const desktopLabel = () => (open() ? `··· hide ${n()} steps ···` : `··· ${n()} steps ···`); return ( <>
-
setOpen(!open())}> - {open() ? `··· hide ${n()} steps ···` : `··· ${n()} steps ···`} -
+
@@ -178,7 +189,7 @@ function CommandRow(props: { step: TraceStep }) {
more() && setOpen(!open())} > diff --git a/viewer/src/embed.tsx b/viewer/src/embed.tsx index 2c2336e..223b3e6 100644 --- a/viewer/src/embed.tsx +++ b/viewer/src/embed.tsx @@ -39,6 +39,7 @@ const EMBED_BASE_CSS = ` position: relative; background: var(--bg); color: var(--text); + -webkit-text-size-adjust: 100%; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .ss-engine-root { position: absolute; inset: 0; } diff --git a/viewer/src/styles.css b/viewer/src/styles.css index e77ab8a..9a51484 100644 --- a/viewer/src/styles.css +++ b/viewer/src/styles.css @@ -13,6 +13,8 @@ --accent-bg: #ddf4ff; --hover: #eaeef2; --danger: #cf222e; + --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1); + --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* Terminal chrome — fallback values for the pre-theme paint. The active theme overrides these from its DARK palette (see themes.ts termVars), so the terminal adopts the theme's hue but stays dark in both color schemes: a @@ -50,6 +52,7 @@ body { margin: 0; background: var(--bg); color: var(--text); + -webkit-text-size-adjust: 100%; font: 14px/1.5 -apple-system, BlinkMacSystemFont, @@ -538,12 +541,14 @@ iframe { margin-bottom: 6px; } .trace-title { + min-width: 0; font-weight: 500; font-size: 12.5px; color: var(--muted); } .trace-dl { margin-left: auto; + flex: none; font-size: 12px; color: var(--accent); text-decoration: none; @@ -590,6 +595,7 @@ iframe { padding: 1px 5px; } .trace-label { + min-width: 0; color: var(--text); } .trace-ts { @@ -930,7 +936,6 @@ iframe { .connect-btn:hover { border-color: var(--muted); } - /* the "Connect Claude Code" integrations modal */ .modal-backdrop { position: fixed; @@ -1031,12 +1036,15 @@ iframe { align-items: center; gap: 4px; flex: none; - padding: 8px 10px; + min-height: 49px; + padding: calc(6px + env(safe-area-inset-top, 0px)) 10px 6px; background: var(--panel); border-bottom: 0.5px solid var(--border); + z-index: 20; } .topbar .brand { padding: 0; + min-width: 0; } .topbar .menu { position: relative; @@ -1049,11 +1057,18 @@ iframe { padding: 6px 9px; cursor: pointer; font-family: inherit; + transition: + background-color 120ms ease, + color 120ms ease, + transform 120ms var(--ease-out-strong); } .topbar .menu:hover { color: var(--text); background: var(--hover); } + .topbar .menu:active { + transform: scale(0.96); + } .topbar .menu .dot { position: absolute; top: 3px; @@ -1074,10 +1089,48 @@ iframe { left: 0; z-index: 30; width: min(280px, 84vw); + padding-top: env(safe-area-inset-top, 0px); + padding-bottom: env(safe-area-inset-bottom, 0px); transform: translateX(-105%); - transition: transform 0.2s ease; + transition: transform 220ms var(--ease-drawer); + will-change: transform; + } + aside > .brand { + min-height: 48px; + padding-top: 12px; + padding-bottom: 10px; + } + #sessionList { + padding: 6px 8px 10px; + } + .sess { + min-height: 52px; + padding: 10px 58px 10px 11px; + } + .sess-title { + padding-right: 0; } - body.nav-open aside { + .sess-meta { + min-width: 0; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .sess .x { + top: 11px; + right: 8px; + min-width: 28px; + min-height: 28px; + } + .sess .dot { + top: 15px; + right: 44px; + } + .aside-foot { + padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); + } + :is(body.nav-open, :host(.nav-open)) aside { transform: none; box-shadow: 0 0 32px rgba(0, 0, 0, 0.25); } @@ -1089,9 +1142,9 @@ iframe { background: rgba(0, 0, 0, 0.35); opacity: 0; pointer-events: none; - transition: opacity 0.2s; + transition: opacity 220ms var(--ease-drawer); } - body.nav-open #scrim { + :is(body.nav-open, :host(.nav-open)) #scrim { opacity: 1; pointer-events: auto; } @@ -1099,21 +1152,171 @@ iframe { min-height: 0; } .session-head { - padding: 12px 16px 10px; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: start; + gap: 2px; + padding: 8px 12px 9px; + box-shadow: 0 1px 0 color-mix(in srgb, var(--border) 70%, transparent); + } + #sessTitle { + display: block; + min-width: 0; + max-width: 100%; + overflow: hidden; + padding: 0; + text-overflow: ellipsis; + white-space: nowrap; + } + .session-head .meta { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .session-head .head-sp { + display: none; + } + .view-toggle { + width: min(100%, 360px); + margin-top: 4px; + } + .view-toggle button { + flex: 1; + min-height: 30px; + padding: 3px 10px; } #stream { - padding: 16px 14px 120px; + padding: 12px 8px calc(112px + env(safe-area-inset-bottom, 0px)); + } + .standalone-main { + padding: 12px 8px calc(48px + env(safe-area-inset-bottom, 0px)); } #onboard { padding: 40px 18px; } +} + +/* phone card chrome: keep cards scannable without changing desktop density */ +@media (max-width: 700px) { + .card { + border-radius: 10px; + margin-bottom: 14px; + } + .card-head { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 8px; + padding: 9px 10px; + } .card-title { + grid-column: 1 / -1; min-width: 0; - flex: 0 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .card-head .sp { + display: none; + } + .vslot { + min-width: 0; + } + .card-meta { + justify-self: end; + } +} + +/* phone native parts: prevent each built-in renderer from forcing page overflow */ +@media (max-width: 700px) { + .imagepart, + .tracepart, + .jsonpart, + .part-unsupported, + .cmts { + padding-left: 10px; + padding-right: 10px; + } + .trace-head { + align-items: flex-start; + } + .trace-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .trace-row { + align-items: flex-start; + flex-wrap: wrap; + gap: 4px 7px; + } + .trace-label { + flex: 1 1 160px; + overflow-wrap: anywhere; + } + .trace-ts { + width: 100%; + margin-left: 0; + } + .trace-detail { + max-width: 100%; + overflow-wrap: anywhere; + } + .json-children { + padding-left: 14px; + } +} + +/* phone card footer: comments and actions become touch targets */ +@media (max-width: 700px) { + .cmt .copy { + opacity: 1; + } + .card-actions { + min-height: 48px; + padding: 7px 8px; + } + .actbar { + gap: 4px; + } + .card-actions .act { + min-width: 36px; + height: 36px; + } + .card-actions .composer { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 6px; + } + .card-actions .composer input { + min-height: 38px; + } + .card-actions .composer button { + min-height: 34px; + width: 100%; + } + #newPill { + bottom: calc(22px + env(safe-area-inset-bottom, 0px)); + } +} +@media (max-width: 430px) { + #stream, + .standalone-main { + padding-left: 0; + padding-right: 0; + } + .card { + border-left: 0; + border-right: 0; + border-radius: 0; + } + .standalone-foot { + padding: 0 12px; + } + .view-toggle { + width: 100%; + } } /* narrow or touch: hover-revealed actions must stay reachable */ @media (max-width: 700px), (hover: none) { @@ -1123,11 +1326,13 @@ iframe { .sess .x { opacity: 1; } - .sess-title { - padding-right: 44px; - } .sess .dot { - right: 32px; + right: 44px; + } +} +@media (hover: none) and (min-width: 701px) { + .sess-title { + padding-right: 52px; } } @@ -1155,6 +1360,12 @@ iframe { transform: translateX(-50%) translateY(0); pointer-events: auto; } +@media (max-width: 430px) { + #toast { + width: calc(100vw - 24px); + max-width: none; + } +} /* ---- session view toggle (Stream / Timeline) ---- */ .session-head .head-sp { @@ -1180,6 +1391,20 @@ iframe { background: var(--accent-bg); color: var(--accent); } +@media (prefers-reduced-motion: reduce) { + .topbar .menu, + aside, + #scrim, + .tl-notes-fold > .body, + .tl-cmd-inline { + transition: none; + } + .topbar .menu:active, + .tl-notes-fold > .body:active, + .tl-cmd-inline:active { + transform: none; + } +} /* ---- treatment E: session timeline ---- */ /* One spine down the middle. Surfaces are opaque cards that cover the line; @@ -1209,6 +1434,10 @@ iframe { .tl-rail-pad { padding-left: 28px; } +.tl-turn { + position: relative; + z-index: 1; +} .tl-surface { position: relative; z-index: 1; @@ -1267,15 +1496,40 @@ iframe { font-size: 12px; color: var(--faint); } +.tl-fold-button { + appearance: none; + display: inline; + margin: 0; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + line-height: inherit; + text-align: left; +} +.tl-fold-caret { + display: none; +} +.tl-fold-label-mobile { + display: none; +} /* an expanded tool call, a faint mono line in the fold */ .tl-cmd-row > .body { - display: flex; - align-items: center; - gap: 8px; padding-left: 14px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--faint); + min-width: 0; +} +.tl-cmd-inline { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} +.tl-cmd-inline > span:last-child { + min-width: 0; } .tl-cmd-row .knd { flex: 0 0 auto; @@ -1298,6 +1552,7 @@ iframe { border: 0.5px solid var(--border); border-radius: 8px; white-space: pre-wrap; + overflow-x: auto; } .tl-tail { padding-bottom: 80px; @@ -1306,3 +1561,167 @@ iframe { font-size: 12px; color: var(--faint); } + +@media (max-width: 700px) { + .timeline { + padding: 0 10px 0; + } + .timeline::before { + display: none; + } + .tl-turn { + margin: 10px 0 12px; + padding: 11px 12px 12px; + background: color-mix(in srgb, var(--surface) 78%, var(--bg)); + border: 0.5px solid var(--border); + border-radius: 8px; + box-shadow: 0 1px 0 color-mix(in srgb, var(--border) 70%, transparent); + } + .tl-turn::before { + content: ""; + position: absolute; + inset: 12px auto 12px 0; + width: 3px; + border-radius: 999px; + background: var(--accent); + } + .tl-row { + padding: 0; + } + .tl-marker { + display: none; + } + .tl-marker.prompt { + display: none; + } + .tl-surface { + padding-left: 0; + margin: 12px 0 14px; + } + .tl-surface .tl-node { + display: none; + } + .tl-prompt { + margin: 0 0 8px; + } + .tl-prompt > .body { + font-size: 14px; + line-height: 1.42; + font-weight: 650; + letter-spacing: 0; + overflow-wrap: anywhere; + } + .tl-prompt > .body::before { + content: "Prompt"; + display: block; + margin-bottom: 5px; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + font-size: 10px; + font-weight: 700; + line-height: 1; + text-transform: uppercase; + color: var(--accent); + } + .tl-response > .body { + font-size: 13.5px; + line-height: 1.5; + color: var(--muted); + overflow-wrap: anywhere; + } + .tl-response + .tl-notes-fold, + .tl-notes-fold + .tl-response, + .tl-row + .tl-row { + margin-top: 9px; + } + .tl-notes-fold > .body { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 36px; + padding: 0 10px 0 12px; + color: var(--text); + background: var(--bg); + border: 0.5px solid var(--border); + border-radius: 8px; + transition: + transform 140ms var(--ease-out-strong), + border-color 160ms ease, + background-color 160ms ease; + } + .tl-notes-fold > .body:active { + transform: scale(0.99); + } + .tl-fold-caret { + display: inline-grid; + width: 22px; + height: 22px; + place-items: center; + border-radius: 999px; + color: var(--accent); + background: var(--accent-bg); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 14px; + line-height: 1; + } + .tl-fold-label-desktop { + display: none; + } + .tl-fold-label-mobile { + display: inline; + } + .tl-cmd-row > .body { + padding-left: 0; + color: var(--muted); + } + .tl-cmd-inline { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + gap: 4px 8px; + padding: 9px 10px; + background: var(--bg); + border: 0.5px solid var(--border); + border-radius: 8px; + transition: + transform 140ms var(--ease-out-strong), + border-color 160ms ease, + background-color 160ms ease; + } + .tl-cmd-inline:active { + transform: scale(0.99); + } + .tl-cmd-row .knd { + margin-top: 1px; + padding: 1px 5px; + background: var(--accent-bg); + border-radius: 4px; + opacity: 1; + } + .tl-cmd-inline > span:last-child { + color: var(--muted); + line-height: 1.45; + overflow-wrap: anywhere; + } + .tl-detail { + margin: 6px 0 2px; + padding: 9px 10px; + font-size: 11.5px; + line-height: 1.5; + background: var(--bg); + border-color: var(--border-2); + overflow-wrap: anywhere; + } + .tl-tail { + padding: 8px 10px 72px; + } + .tl-tail > .body { + padding-left: 0; + } +} From 46d94fa85b5426240e7e23e0a05db43eed8cecd5 Mon Sep 17 00:00:00 2001 From: Tridhatri Vallamkondu Date: Thu, 25 Jun 2026 12:55:54 -0400 Subject: [PATCH 2/2] chore: add mobile viewer changeset --- .changeset/mobile-viewer-ui.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mobile-viewer-ui.md diff --git a/.changeset/mobile-viewer-ui.md b/.changeset/mobile-viewer-ui.md new file mode 100644 index 0000000..a1d0b9d --- /dev/null +++ b/.changeset/mobile-viewer-ui.md @@ -0,0 +1,5 @@ +--- +"sideshow": patch +--- + +Improve the mobile viewer layout for phone-sized screens, including sidebar ergonomics, native surface primitives, and timeline trace readability.