diff --git a/spec/ui.md b/spec/ui.md index bab8a25..ab044d5 100644 --- a/spec/ui.md +++ b/spec/ui.md @@ -25,7 +25,7 @@ Filters stay collapsed above the feed until requested. The current controls cove On phone 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 four touch-sized destinations: **Feed**, **Review**, **Artifacts**, and **System**. Review contains local **Suggestions** and **Comparisons** views; System contains local **Runs** and **Sources** views. These local choices appear as one segmented control only after their destination is selected, rather than expanding the global navigation. The selected destination uses a high-contrast pill; inactive destinations use quiet outlined pills. The navigation may scroll horizontally on compact phones without making the document overflow. Medium and desktop widths retain the site's quieter text-navigation grammar rather than scaling the phone pills into oversized chrome. Selecting any detail hides global and local navigation until the reader returns to the list, so the object itself owns the viewport. -Feed and Sources expose filtering as an app control, not an expanded form embedded in the document. A compact Filter button opens a modal sheet with full-width, touch-sized fields. On phones the sheet is anchored to the bottom safe area; wider screens use a centered modal. Active filters return to the main view as individually removable chips and a numeric count. 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. +Feed and Sources expose filtering as an app control, not an expanded form or a separate title/toolbar row embedded in the document. The active navigation already names the view, so the Filter control lives at the far right of the global navigation line on medium/desktop widths and becomes the next horizontally scrollable phone control. It opens a modal sheet with full-width, touch-sized fields. On phones the sheet is anchored to the bottom safe area; wider screens use a centered modal. Active filters return to the main view as individually removable chips and a numeric count; zero active filters render no count. 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 e8cfa1c..4eb282f 100644 --- a/src/web/inspector.ts +++ b/src/web/inspector.ts @@ -893,13 +893,10 @@ export function renderInspectorHtml(): string { h1 { margin:0 0 16px; font:400 32px/1 Around,Inter,-apple-system,sans-serif; font-synthesis:none; letter-spacing:.025em; text-transform:uppercase } .muted { color:var(--muted) } main { width:min(640px,calc(100% - 32px)); margin:0 auto; padding-bottom:64px } section { min-width:0 } - .section-head { display:flex; min-height:52px; align-items:center; justify-content:space-between; gap:12px; padding:14px 0 10px; color:var(--muted); font-size:13px } - .view-title { margin:0; color:var(--muted); font-size:13px; font-weight:400; letter-spacing:0 } - .section-action { min-height:40px; border:1px solid var(--line-strong); border-radius:var(--radius-control); background:var(--surface-control); color:var(--secondary); padding:8px 14px; cursor:pointer; transition:border-color var(--motion),background var(--motion),color var(--motion),transform var(--motion) } - .section-action:hover { border-color:var(--accent); color:var(--text) } - .section-action:active { transform:scale(.98) } + .visually-hidden { position:absolute!important; width:1px!important; height:1px!important; padding:0!important; margin:-1px!important; overflow:hidden!important; clip:rect(0,0,0,0)!important; white-space:nowrap!important; border:0!important } .filter-count { display:inline-grid; min-width:19px; height:19px; place-items:center; margin-left:6px; border-radius:50%; background:var(--accent); color:#111; font-size:10px } - .filter-chips { display:flex; gap:7px; margin:-2px 0 12px; overflow-x:auto; scrollbar-width:none } + .filter-count[hidden] { display:none } + .filter-chips { display:flex; gap:7px; margin:0 0 12px; overflow-x:auto; scrollbar-width:none } .filter-chips::-webkit-scrollbar { display:none } .filter-chip { min-height:32px; flex:0 0 auto; border:1px solid var(--line); border-radius:var(--radius-control); background:var(--surface); color:var(--secondary); padding:6px 10px; font-size:11px; cursor:pointer } .filter-chip:hover { border-color:var(--accent); color:var(--text) } @@ -949,10 +946,11 @@ export function renderInspectorHtml(): string { details { border-top:1px solid var(--line); margin-top:14px; padding-top:10px } summary { color:var(--muted); cursor:pointer; font-size:12px } details pre { margin-top:8px } .primary-nav,.context-nav { display:flex; align-items:center; gap:8px; overflow-x:auto; scrollbar-width:none } .primary-nav::-webkit-scrollbar,.context-nav::-webkit-scrollbar { display:none } - .primary-nav { gap:20px } + .primary-nav { width:100%; gap:20px } .primary-nav button { min-width:max-content; min-height:32px; flex:0 0 auto; border:0; border-bottom:1px solid transparent; border-radius:0; background:transparent; color:var(--muted); padding:0; cursor:pointer; transition:border-color var(--motion),color var(--motion),transform var(--motion) } .primary-nav button:hover { color:var(--text) } .primary-nav button.active { border-bottom-color:var(--accent); background:transparent; color:var(--text) } + .primary-nav .filter-nav { margin-left:auto } .primary-nav button:active,.context-nav button:active { transform:scale(.98) } .primary-nav button:focus-visible,.context-nav button:focus-visible { outline:2px solid var(--accent); outline-offset:2px } .context-nav { width:max-content; max-width:100%; margin-top:11px; gap:3px; border:1px solid var(--line); border-radius:var(--radius-control); background:var(--surface); padding:3px } @@ -1020,13 +1018,13 @@ export function renderInspectorHtml(): string { .app-header { position:sticky; top:0; z-index:20; width:100%; padding:max(16px,env(safe-area-inset-top)) 16px 13px; border-bottom:1px solid var(--line); background:color-mix(in srgb,var(--bg) 94%,transparent); backdrop-filter:blur(22px) saturate(1.15); -webkit-backdrop-filter:blur(22px) saturate(1.15) } h1 { margin:0 0 14px; font-size:24px; letter-spacing:.05em } main { width:100%; padding:0 16px max(44px,env(safe-area-inset-bottom)) } - .primary-nav { gap:8px; margin:0 -16px; padding:0 16px 2px; overscroll-behavior-x:contain; -webkit-overflow-scrolling:touch } + .primary-nav { width:auto; gap:8px; margin:0 -16px; padding:0 16px 2px; overscroll-behavior-x:contain; -webkit-overflow-scrolling:touch } .primary-nav button { min-height:44px; padding:9px 16px; border:1px solid var(--line-strong); border-radius:var(--radius-control); background:var(--surface-control); font-size:13px; line-height:1 } .primary-nav button:hover { border-color:var(--accent) } .primary-nav button.active { border-color:var(--selected-bg); background:var(--selected-bg); color:var(--selected-text) } .context-nav { margin-top:10px } .context-nav button { min-height:44px; padding:9px 15px } - .section-head { padding-top:14px } + .primary-nav .filter-nav { margin-left:0 } .candidate-grid,.human-grid,.suggestion-compare { grid-template-columns:1fr } .human-card.wide { grid-column:auto } .item { padding:18px 0 } @@ -1043,8 +1041,8 @@ export function renderInspectorHtml(): string {
-