diff --git a/spec/ui.md b/spec/ui.md index 9c00c90..bab8a25 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. 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. +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. diff --git a/src/web/inspector.ts b/src/web/inspector.ts index b2e8290..e8cfa1c 100644 --- a/src/web/inspector.ts +++ b/src/web/inspector.ts @@ -949,9 +949,10 @@ 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 button { min-width:max-content; min-height:44px; flex:0 0 auto; border:1px solid var(--line-strong); border-radius:var(--radius-control); background:var(--surface-control); color:var(--muted); padding:9px 16px; cursor:pointer; transition:border-color var(--motion),background var(--motion),color var(--motion),transform var(--motion) } - .primary-nav button:hover { border-color:var(--accent); color:var(--text) } - .primary-nav button.active { border-color:var(--selected-bg); background:var(--selected-bg); color:var(--selected-text) } + .primary-nav { 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 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 } @@ -963,7 +964,7 @@ export function renderInspectorHtml(): string { .suggestion-actions { display:grid; gap:10px; border-top:1px solid var(--line); padding-top:14px; margin-top:16px } .suggestion-buttons { display:flex; flex-wrap:wrap; gap:8px } .suggestion-buttons button { border:1px solid var(--accent); background:var(--accent-soft); color:var(--text); padding:9px 12px; font:inherit; cursor:pointer } .suggestion-buttons button.reject { border-color:var(--red) } .spinner { display:inline-block; width:10px; height:10px; margin-right:7px; border:1px solid var(--line); border-top-color:var(--link); border-radius:50%; animation:spin .8s linear infinite; vertical-align:-1px } .retry { min-height:40px; margin-top:12px; border:1px solid var(--red); border-radius:var(--radius-control); background:transparent; color:var(--text); padding:8px 13px; cursor:pointer } - .observation-head h2 { margin-bottom:4px } .observation-card { margin:15px 0; border:0; background:transparent; padding:0; font:15px/1.58 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; overflow-wrap:anywhere } .observation-card p { margin:0; white-space:pre-wrap } .observation-links { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px } .observation-links a { border:1px solid var(--line); border-radius:var(--radius-control); color:var(--link); padding:7px 10px; text-decoration:none; font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace } .processing-state { display:flex; align-items:center; gap:8px; margin:15px 0 4px; color:var(--muted); font-size:12px } .status-dot { width:7px; height:7px; border-radius:50%; background:var(--line-strong); flex:none } .technical-record { margin-top:26px } .technical-record > summary { padding:3px 0 8px } .technical-section { margin-top:16px } .technical-section h3 { margin-top:0 } + .observation-head h2 { margin-bottom:4px } .observation-card { margin:15px 0; border:0; background:transparent; padding:0; font:15px/1.58 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; overflow-wrap:anywhere } .observation-card .bluesky-inlay { border:0; border-radius:0; background:transparent; padding:0 } .observation-card p { margin:0; white-space:pre-wrap } .observation-links { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px } .observation-links a { border:1px solid var(--line); border-radius:var(--radius-control); color:var(--link); padding:7px 10px; text-decoration:none; font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace } .processing-state { display:flex; align-items:center; gap:8px; margin:15px 0 4px; color:var(--muted); font-size:12px } .status-dot { width:7px; height:7px; border-radius:50%; background:var(--line-strong); flex:none } .technical-record { margin-top:26px } .technical-record > summary { padding:3px 0 8px } .technical-section { margin-top:16px } .technical-section h3 { margin-top:0 } .source-event-list { display:grid; gap:8px; margin:8px 0 16px } .source-event { width:100%; border:1px solid var(--line); border-radius:12px; background:var(--surface); color:var(--text); padding:11px 12px; text-align:left; cursor:pointer } .source-event:hover { border-color:var(--accent); background:var(--accent-soft) } .source-event .source-event-title { color:var(--accent-strong); margin-bottom:3px } .source-event .source-event-body { overflow:hidden; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; font:13px/1.48 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif } .source-overview { margin-top:12px } .source-state { border-top:2px solid var(--line-strong) } .source-state.good { border-top-color:var(--accent) } .source-state.waiting { border-top-color:var(--amber) } .feed-processing { margin-top:12px; padding-top:9px } .feed-processing > summary { width:max-content; min-height:32px; display:flex; align-items:center; gap:6px; border:1px solid transparent; border-radius:var(--radius-control); padding:4px 8px; color:var(--muted); list-style:none; transition:border-color var(--motion),color var(--motion) } .feed-processing > summary::-webkit-details-marker { display:none } .feed-processing > summary::before { content:'›'; display:inline-block; color:var(--accent); transform:rotate(0); transition:transform var(--motion) } .feed-processing[open] > summary::before { transform:rotate(90deg) } .feed-processing > summary:hover { border-color:var(--line); color:var(--text) } .feed-links { display:flex; flex-wrap:wrap; gap:8px; margin:10px 0 0 } .feed-links a { color:var(--link); text-decoration:none; border-bottom:1px solid var(--line-strong); font-size:12px } .activity-kind { margin-bottom:9px; color:var(--muted); font-size:13px } @@ -1011,13 +1012,18 @@ export function renderInspectorHtml(): string { @keyframes sheet-in { from { opacity:0; transform:translateY(18px) scale(.98) } to { opacity:1; transform:none } } .filter-dialog[open] { animation:sheet-in var(--motion) both } @media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto!important; transition-duration:.01ms!important; animation-duration:.01ms!important; animation-iteration-count:1!important } .spinner { animation:none; border-color:var(--link) } } - @media(max-width:900px){ + body.detail-selected .app-header { padding-bottom:10px } + body.detail-selected .app-header h1 { margin-bottom:0 } + body.detail-selected .primary-nav,body.detail-selected .context-nav { display:none } + @media(max-width:640px){ body { overscroll-behavior-y:none; font-size:15px } .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 button { min-height:44px; padding:9px 16px; font-size:13px; line-height:1 } + .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 } @@ -1159,7 +1165,7 @@ function sourceItem(source){ function suggestionItem(item){return ''} function reviewItem(item){const status=item.decision?item.decision.disposition:'unreviewed';return ''} async function select(kind,id,options={}){ - state.selected={kind,id};document.querySelector('main').classList.add('detail-selected');if(!options.preserveScroll)window.scrollTo({top:0,behavior:'smooth'}); + state.selected={kind,id};document.querySelector('main').classList.add('detail-selected');document.body.classList.add('detail-selected');if(!options.preserveScroll)window.scrollTo({top:0,behavior:'smooth'}); if(kind==='source'){await showSource(id);return} if(kind==='suggestion'){const item=state.proposals.items.find(value=>value.eventId===id);document.querySelector('#detail').innerHTML=renderSuggestion(item);bindSuggestionForm(item);return} if(kind==='review'){const item=state.reviews.items.find(value=>value.id===id);document.querySelector('#detail').innerHTML=renderReview(item);bindReviewForm(item);return} @@ -1291,8 +1297,8 @@ function renderArtifact(data){ function humanLabel(value){return String(value??'').replace(/[-_.:]+/g,' ').replace(/\b\w/g,char=>char.toUpperCase())} function humanMediaType(value){return ({'text/markdown':'Markdown','text/plain':'Plain text','application/json':'JSON document','application/yaml':'YAML document','text/yaml':'YAML document','image/png':'PNG image','image/jpeg':'JPEG image'})[value]??humanLabel(value)} function formatBytes(value){const n=Number(value);if(!Number.isFinite(n))return 'Unknown';if(n<1024)return n+' bytes';if(n<1024*1024)return (n/1024).toFixed(n<10240?1:0)+' KB';return (n/(1024*1024)).toFixed(1)+' MB'} -function closeDetail(){state.selected=null;document.querySelector('main').classList.remove('detail-selected');document.querySelector('#detail').innerHTML='';renderList();window.scrollTo({top:0,behavior:'smooth'})} -function switchTab(tab){state.tab=tab;state.selected=null;document.querySelector('main').classList.remove('detail-selected');document.querySelector('#detail').innerHTML='';renderList();window.scrollTo({top:0,behavior:'smooth'})} +function closeDetail(){state.selected=null;document.querySelector('main').classList.remove('detail-selected');document.body.classList.remove('detail-selected');document.querySelector('#detail').innerHTML='';renderList();window.scrollTo({top:0,behavior:'smooth'})} +function switchTab(tab){state.tab=tab;state.selected=null;document.querySelector('main').classList.remove('detail-selected');document.body.classList.remove('detail-selected');document.querySelector('#detail').innerHTML='';renderList();window.scrollTo({top:0,behavior:'smooth'})} document.querySelector('#detail-back').addEventListener('click',closeDetail); document.querySelector('#events-tab').addEventListener('click',()=>switchTab('events')); document.querySelector('#review-tab').addEventListener('click',()=>switchTab(state.tab==='suggestions'||state.tab==='reviews'?state.tab:'suggestions')); diff --git a/test/inspector.test.ts b/test/inspector.test.ts index 935b648..3de1839 100644 --- a/test/inspector.test.ts +++ b/test/inspector.test.ts @@ -103,7 +103,7 @@ 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("@media(max-width:900px)"); + expect(pageHtml).toContain("@media(max-width:640px)"); expect(pageHtml).toContain("text-transform:uppercase"); expect(pageHtml).toContain('class="primary-nav" aria-label="Stream destinations"'); expect(pageHtml).toContain(''); @@ -112,7 +112,7 @@ describe("thought stream inspector", () => { expect(pageHtml).toContain(''); expect(pageHtml).toContain('id="review-nav" class="context-nav"'); expect(pageHtml).toContain('id="system-nav" class="context-nav"'); - expect(pageHtml).toContain(".primary-nav button { min-width:max-content; min-height:44px"); + expect(pageHtml).toContain(".primary-nav button { min-width:max-content; min-height:32px"); expect(pageHtml).toContain(".primary-nav button.active { border-color:var(--selected-bg); background:var(--selected-bg); color:var(--selected-text) }"); expect(pageHtml).toContain(".telegram-inlay { border:1px solid var(--line); border-radius:20px; background:var(--surface-control)"); expect(pageHtml).toContain("overflow-x:auto"); @@ -150,6 +150,9 @@ describe("thought stream inspector", () => { expect(pageHtml).not.toContain('id="count"'); expect(pageHtml).not.toContain("review enabled"); expect(pageHtml).toContain("main.detail-selected #list-pane"); + expect(pageHtml).toContain("body.detail-selected .primary-nav,body.detail-selected .context-nav { display:none }"); + expect(pageHtml).toContain("document.body.classList.add('detail-selected')"); + expect(pageHtml).toContain(".observation-card .bluesky-inlay { border:0; border-radius:0; background:transparent; padding:0 }"); expect(pageHtml).toContain('class="item event-item" tabindex="0" data-kind="event"'); expect(pageHtml).toContain("feed-processing"); expect(pageHtml).toContain("renderSourceInlay");