From f349c2bbfaf6622f8444546644b7183a7f771972 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Mon, 10 Aug 2026 21:53:46 -0700 Subject: [PATCH] Refine Stream narrow navigation and hover states. --- spec/ui.md | 2 +- src/web/inspector.ts | 17 +++++++++-------- test/inspector.test.ts | 10 ++++++++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/spec/ui.md b/spec/ui.md index b3a7e90..ef63ad4 100644 --- a/spec/ui.md +++ b/spec/ui.md @@ -23,7 +23,7 @@ Deterministic transforms are labeled **rules**, not agents. The interface states Filters stay collapsed above the feed until requested. The current controls cover source, activity kind, processing state, and text. -On narrow screens, the private interface behaves as an app shell rather than a compressed desktop document. The title stays in one compact sticky header, the feed runs edge to edge inside safe-area padding, and the primary views move to a fixed bottom navigation bar with touch-sized controls. Detail selection replaces the feed in the same viewport and preserves the existing back affordance. The page publishes standalone-app metadata and an exact private manifest/icon route; it does not cache private HTML, JSON, media, or Jazz-derived content offline. +On narrow screens, the private interface behaves as an app shell rather than a compressed desktop document. A compact sticky header keeps the uppercase `STREAM` wordmark visible above one horizontally scrollable row of touch-sized pill controls. The selected view uses a white pill with black text; inactive views use black pills with thin gray outlines and muted text. The feed runs edge to edge inside safe-area padding. Detail selection replaces the feed in the same viewport and preserves the existing back affordance. The page publishes standalone-app metadata and an exact private manifest/icon route; it does not cache private HTML, JSON, media, or Jazz-derived content offline. Bluesky avatars and embed thumbnails are rendered through one authenticated same-origin media route because the private web boundary intentionally denies arbitrary third-party image loads. The route accepts only HTTPS `cdn.bsky.app/img/*` URLs, follows no redirects, applies strict byte, timeout, content-type, and image-magic bounds, and returns no upstream detail on failure. Full-size post links remain ordinary external navigation. A failed image is removed from the card rather than leaving a large broken placeholder. diff --git a/src/web/inspector.ts b/src/web/inspector.ts index eba4efc..4bba87a 100644 --- a/src/web/inspector.ts +++ b/src/web/inspector.ts @@ -894,7 +894,7 @@ export function renderInspectorHtml(): string { .section-head { display:flex; justify-content:space-between; padding:14px 0 7px; color:var(--muted); font-size:12px } .item { width:100%; min-width:0; display:block; border:0; border-bottom:1px solid var(--line); border-radius:0; margin:0; padding:18px 0; background:transparent; color:inherit; text-align:left; font:inherit; cursor:pointer; overflow-wrap:anywhere } .item:first-child { border-top:1px solid var(--line) } - .item:hover { background:var(--accent-soft) } .item:focus-visible { outline:2px solid var(--accent); outline-offset:3px } .item.active { background:var(--accent-soft) } .type { color:var(--accent); font-size:13px; overflow-wrap:anywhere } .summary { min-width:0; margin:7px 0 9px; font:15px/1.55 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; overflow-wrap:anywhere; white-space:pre-wrap } .meta { color:var(--muted); font-size:11px; overflow-wrap:anywhere } + .item:hover,.item.active { background:transparent } .item:hover .telegram-inlay,.item.active .telegram-inlay,.item:hover .bluesky-inlay,.item.active .bluesky-inlay { background:var(--accent-soft) } .item:focus-visible { outline:2px solid var(--accent); outline-offset:3px } .type { color:var(--accent); font-size:13px; overflow-wrap:anywhere } .summary { min-width:0; margin:7px 0 9px; font:15px/1.55 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; overflow-wrap:anywhere; white-space:pre-wrap } .meta { color:var(--muted); font-size:11px; overflow-wrap:anywhere } .consumer { min-width:0; margin:8px 0 2px; padding-left:10px; border-left:2px solid var(--line-strong); color:var(--secondary); font-size:12px; overflow-wrap:anywhere } .consumer strong { color:var(--accent) } .badge { display:inline-block; border:1px solid var(--line); padding:1px 5px; margin-left:5px; color:var(--amber) } .failed { color:var(--red) } #detail-pane { display:none } main.detail-selected #list-pane { display:none } main.detail-selected #detail-pane { display:block } #detail { padding:8px 0 24px } .detail-back { border:0; background:transparent; color:var(--cyan); padding:4px 0 18px; font:inherit; cursor:pointer } @@ -978,14 +978,15 @@ export function renderInspectorHtml(): string { .bsky-stats { display:flex; flex-wrap:wrap; gap:12px; margin-top:9px; color:var(--muted); font-size:11px } @keyframes spin { to { transform:rotate(360deg) } } @media (prefers-reduced-motion:reduce) { .spinner { animation:none; border-color:var(--cyan) } } - @media(max-width:700px){ + @media(max-width:900px){ body { overscroll-behavior-y:none } - header { position:sticky; top:0; z-index:20; width:100%; padding:max(14px,env(safe-area-inset-top)) 16px 11px; border-bottom:1px solid var(--line); background:color-mix(in srgb,var(--bg) 92%,transparent); backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px) } - h1 { margin:0; font-size:25px } - main { width:100%; padding:0 16px calc(76px + env(safe-area-inset-bottom)) } - nav { position:fixed; z-index:30; left:0; right:0; bottom:0; display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:0; padding:0 4px env(safe-area-inset-bottom); border-top:1px solid var(--line); background:color-mix(in srgb,var(--bg) 96%,transparent); box-shadow:0 -8px 22px rgba(0,0,0,.04); backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px) } - nav button { min-width:0; min-height:56px; padding:5px 1px 3px; overflow:hidden; border:0; border-top:2px solid transparent; font-size:10px; line-height:1.15; text-overflow:ellipsis; white-space:nowrap } - nav button.active { border-top-color:var(--accent); border-bottom:0 } + header { position:sticky; top:0; z-index:20; display:grid; gap:12px; width:100%; padding:max(14px,env(safe-area-inset-top)) 16px 12px; border-bottom:1px solid var(--line); background:color-mix(in srgb,var(--bg) 96%,transparent); backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px) } + h1 { margin:0; font-size:23px; letter-spacing:.04em; text-transform:uppercase } + main { width:100%; padding:0 16px max(36px,env(safe-area-inset-bottom)) } + nav { display:flex; flex-wrap:nowrap; gap:8px; margin:0 -16px; padding:0 16px 3px; overflow-x:auto; overscroll-behavior-x:contain; scrollbar-width:none; -webkit-overflow-scrolling:touch } + nav::-webkit-scrollbar { display:none } + nav button { min-width:max-content; min-height:36px; flex:0 0 auto; padding:8px 14px; border:1px solid #343434; border-radius:999px; background:#050505; color:#8a8a8a; font-size:11px; line-height:1; white-space:nowrap } + nav button.active { border-color:#fff; background:#fff; color:#000 } .section-head { padding-top:16px } .candidate-grid,.human-grid,.suggestion-compare { grid-template-columns:1fr } .human-card.wide { grid-column:auto } diff --git a/test/inspector.test.ts b/test/inspector.test.ts index 43fb1df..2b34735 100644 --- a/test/inspector.test.ts +++ b/test/inspector.test.ts @@ -103,8 +103,12 @@ describe("thought stream inspector", () => { expect(pageHtml).toContain('src:url("assets/around-regular.woff2")'); expect(pageHtml).toContain('rel="manifest" href="manifest.webmanifest"'); expect(pageHtml).toContain('name="apple-mobile-web-app-title" content="Stream"'); - expect(pageHtml).toContain("grid-template-columns:repeat(6,minmax(0,1fr))"); - expect(pageHtml).toContain("padding:0 16px calc(76px + env(safe-area-inset-bottom))"); + expect(pageHtml).toContain("@media(max-width:900px)"); + expect(pageHtml).toContain("text-transform:uppercase"); + expect(pageHtml).toContain("border-radius:999px; background:#050505"); + expect(pageHtml).toContain("nav button.active { border-color:#fff; background:#fff; color:#000 }"); + expect(pageHtml).toContain("overflow-x:auto"); + expect(pageHtml).not.toContain("grid-template-columns:repeat(6,minmax(0,1fr))"); expect(pageHtml).toContain("await loadResource('snapshot')"); expect(pageHtml).not.toContain("Promise.all([fetch('api/snapshot')"); expect(pageHtml).not.toContain("fetch('/api/snapshot')"); @@ -137,6 +141,8 @@ describe("thought stream inspector", () => { expect(pageHtml).toContain("api/inlays/bluesky?"); expect(pageHtml).toContain("telegram-inlay"); expect(pageHtml).toContain("bluesky-inlay"); + expect(pageHtml).toContain(".item:hover,.item.active { background:transparent }"); + expect(pageHtml).toContain(".item:hover .telegram-inlay"); expect(pageHtml).toContain("cursor:pointer; overflow-wrap:anywhere"); expect(pageHtml).toContain("font:15px/1.55 Inter"); expect(page.headers.get("content-security-policy")).toContain("frame-ancestors 'none'"); -- 2.51.2