Shell redesign: full-width header, centered Messages/Media tabs, list-only sidebar, no md+ burger#197
Merged
Merged
Conversation
…-only sidebar, no md+ burger (#190) - page_start: the header is now a full-window row ABOVE the drawer (the macOS traffic lights always overlay its left edge in the desktop shell); the body never scrolls — the sidebar and #main-content scroll independently, so conv-header pins at top-0 and the drifting top-[54px] see-through slit is structurally impossible. - Header contents: below-md burger (md:hidden), contextual title, centered Messages (/) / Media (/media) segmented tabs (server-rendered .NavTab on full loads + shell.js location sync across boosted swaps and history moves), search pill (+ below-sm icon-only /search link), theme toggle, settings gear. - GET /media aliases the gallery render; / and /c/* activate Messages, /media and /gallery* activate Media, everything else neither. - Sidebar = filter + Pinned + Conversations only (Search/Media nav links removed); #186 OOB target ids intact; scrolls internally. - #175 lg+ collapse retired wholesale: html.sidebar-collapsed CSS, sidebar-toggle.js collapse branch + localStorage key, test assertions; drawer breakpoint lg→md so ~1000px windows show the sidebar with no burger. - shell.js also forwards inner-container scroll events to window so htmx 2.0.4's hx-trigger="revealed" load-more sentinels keep firing under the new scroll model. - Desktop shell: unconditional 80px traffic-light inset (the drawer-state-dependent block is gone); Wails MinWidth 800 / MinHeight 600 so the app can never reach drawer territory. - Tests updated across shell/desktop-chrome/setup/perf-client; app.css rebuilt from a clean toolchain cache (build verified reproducible). Part of #190 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…190) MAJOR — header tabs reserved no layout space (position:absolute left:50% translateX(-50%)) and painted over / click-intercepted the search pill at 640-~830px, the below-sm search icon at phone widths, and let long titles run under the strip in desktop chrome: - .app-toolbar is now a 3-column grid [left cluster | tabs | right cluster] with the tab strip IN FLOW, so overlap is structurally impossible. Asymmetric side tracks: right is a plain 1fr whose automatic minimum (34px flex-shrink:0 icon buttons + the search pill's 3rem input floor) the grid never collapses below — the tabs shift left instead of overlapping; left is minmax(0,1fr) + a min-width:0 cluster so the nowrap contextual title truncates inside its track (its min-content would otherwise blow the header wider than the window) instead of running under the tabs. The old viewport-based clamp() title cap (which ignored the 80px inset) is gone; the grid divides the padded content box, so the tabs center in AVAILABLE space (macOS-toolbar behavior), inset included. - Search pill flexes (flex-basis 12rem, min 3rem) inside the right cluster at sm-md instead of holding a fixed 12rem input. - Below-sm trims (padding/gap/tab-padding) fit the full header inventory in flow at a 320px viewport. - The desktop-chrome 80px traffic-light inset is gated to md+: the macOS shell window has MinWidth 800 >= md, so the inset can never legitimately coexist with the below-md burger it was crushing. - Verified live (headless Chrome, fixture server) at 320/375/430/640/ 700/800/820/1000 x {browser, desktop-chrome} x {home, transcript, long-title}: 48/48 cells with zero pairwise element overlaps (getBoundingClientRect) and elementFromPoint hitting every search affordance + burger/theme/gear at center. Minors: 1. shell.js saves the #main-content scrollTop to sessionStorage on htmx:beforeHistorySave (keyed by the event's detail.path — NOT location, which htmx's popstate handling has already flipped to the destination, clobbering its saved offset) and restores it on htmx:historyRestore/popstate, so Back into a long transcript lands at the reading position instead of the top. Live-verified via CDP. 2. #main-content carries tabindex="0" in every *_content define (axe scrollable-region-focusable) with an inset :focus-visible accent ring, so PageDown/Space scroll without a pointer. 3. SPEC-0006 REQ-0006-002/003 amended to the shipped #190 shell (full-width header + centered tabs + gear-only Settings; list-only sidebar), IDs stable; layout token line updated to match. 4. Retired top-54px utility can no longer resurrect: the literal class token is reworded out of the conversation.html template comment and the shell_test.go Go comment (Tailwind v4 scans both), app.css rebuilt from a clean cache (second build byte-identical), and TestBuiltCSSCarriesShellComponents now fails on top:54px in the built artifact. Tests: header flow-order + tabindex assertions added; toolbar-cluster classes pinned in the built-CSS guard. CGO_ENABLED=0 go build/vet/test green; gofmt clean; #186 OOB sidebar ids untouched. Part of #190. Addresses the #197 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5e74d60 to
aa86220
Compare
This was referenced Jul 6, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #190
Real-Mac owner feedback round (2026-07-06), all four items as one coherent shell change.
What changed
1. Scroll-through gap — killed structurally
The body never scrolls anymore.
page_startrenders the header as a full-window row above the daisyUI drawer; below it, the sidebar and#main-contentare independent scroll containers.conv-headeris nowsticky top-0inside the#main-contentscroller, so the transcript scrolls under it with zero slit — there is no offset left to drift. The old#main-content { scroll-margin-top: 54px }boosted-nav offset is retired with the body-scroll model (verified: onlyconversation.htmlcarried a sticky offset; the settings sub-nav and gallery filter bar were never sticky).2. Full-width header
<header class="app-toolbar">spans the entire window including over the sidebar. In the desktop shell the macOS traffic lights therefore always overlay the header's left edge: the traffic-light inset is now one unconditionalpadding-left: 80pxunder.desktop-chrome, and the whole drawer-state-dependent corner-ownership block (sidebar top strip + drag rules +html.sidebar-collapsedvariants) is deleted. The header keeps--wails-draggable: drag; the no-drag child list gainednav(the tab strip's own padding sits between clickable tabs).desktop.jsneeded no changes — its computed-style check reads the event target, and every interactive child resolvesno-drag(verified in-browser).3. Header contents, left → right
md:hidden, keyboard-operable label as before).NavTitlebehavior kept — "msgbrowse" on home, conversation name on transcripts, always links home), width-capped so it can never run under the centered tabs/, Media →/media, absolutely centered on the window, boosted like all in-app nav, styled as a segmented pair consistent with the settings-subnav aesthetic/searchlink, theme toggle, settings gear4. Tab active state — mechanism and decision
Mechanism: small static JS (
shell.js), not an OOB fragment. The server renders the correct state on full loads (baseData.NavTab);shell.jsre-derives it fromlocation.pathnameonhtmx:afterSettle/popstate/htmx:historyRestore— the exact event wiringsidebar.jsalready uses for the active conversation row, so the two stay consistent. Chosen over an OOB fragment because it adds zero payload to every boosted response, needs no gating between full and partial renders, and handles back/forward for free. No flash of wrong state: full loads are server-correct, and boosted swaps update the tabs at settle time exactly like the sidebar highlight.Decision (documented in code + tests):
/and every/c/*transcript activate Messages;/mediaand/gallery*activate Media; everything else (/search,/settings,/providers,/logs,/status) activates neither.Route note:
GET /mediais a new alias that renders the gallery handler directly (single request, no redirect hop)./gallerystays the canonical surface — its tab/filter links keep their/gallery?...URLs, which is why/gallery*also reads as Media-active. The exact-path pattern cannot shadow theGET /media/{id}/{path...}attachment route.5. Sidebar = list only
The nav block (Search + Media links) is gone; the sidebar is filter + Pinned + Conversations, scrolling internally (
h-fullaside,min-h-0on the list — kept identical on the #186 OOB copy). The#sidebar-pinned-section/#sidebar-conversations/#sidebar-conversations-countOOB target ids and allsidebar.jsbehavior are preserved.6. Burger removal + #175 collapse retired
Drawer breakpoint moved lg→md, so a ~1000px window shows the pinned sidebar with no burger (verified at exactly 1000px in headless Chrome). Below md the overlay drawer keeps a header burger, keyboard-operable as before. The #175 lg+ persistent collapse is retired wholesale:
html.sidebar-collapsedCSS, thesidebar-toggle.jscollapse branch and its localStorage key (the new script also removes the stale key), and the related test assertions.cmd/msgbrowse-desktopsets WailsMinWidth: 800/MinHeight: 600so the desktop app can never reach drawer territory — the nested cgo module was built successfully in this environment (make desktop-linux, webkit2gtk-4.1 headers present) alongsidemake desktop-test.Two mechanical notes
.toolbar-icon-btndisplay rule (un-layered) outranks Tailwind's layeredsm:hidden/md:hiddenutilities, so the burger/search-link breakpoint gates are restated un-layered ininput.css(caught in live verification, covered by the markup + CSS drift tests).revealedunder inner scrolling: htmx 2.0.4 only re-checksrevealedsentinels after a window scroll/resize event, and inner-container scrolls don't bubble.shell.jsforwards them (capture-phase listener → synthetic window scroll), keeping the transcript/gallery infinite scroll alive without touching the documentedhx-trigger="revealed"contract.Known trade-off
htmx history snapshots restore window scroll only, so back/forward now lands at the top of the content scroller instead of the previous offset. Inherent to independent scroll areas; deemed acceptable against the structural fix for the scroll-through gap.
Verification
CGO_ENABLED=0 go build ./... && go vet ./... && go test ./...green; gofmt cleanrm -rf .tools && make csstwice + once more from clean cache — byte-identicalapp.csscommittedgap == 0between header bottom and conv-header top, boosted tab sync across/media→/settings→/→ history-back, 500px overlay drawer open/close witharia-expandedsync, desktop-chrome 80px inset + drag/no-drag computed values, inner-scroll → window scroll forwarding🤖 Posted on behalf of
@joestumpby Claude.