Skip to content

Shell redesign: full-width header, centered Messages/Media tabs, list-only sidebar, no md+ burger#197

Merged
joestump merged 2 commits into
mainfrom
feature-190-shell-redesign
Jul 6, 2026
Merged

Shell redesign: full-width header, centered Messages/Media tabs, list-only sidebar, no md+ burger#197
joestump merged 2 commits into
mainfrom
feature-190-shell-redesign

Conversation

@joestump

@joestump joestump commented Jul 6, 2026

Copy link
Copy Markdown
Owner

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_start renders the header as a full-window row above the daisyUI drawer; below it, the sidebar and #main-content are independent scroll containers. conv-header is now sticky top-0 inside the #main-content scroller, 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: only conversation.html carried 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 unconditional padding-left: 80px under .desktop-chrome, and the whole drawer-state-dependent corner-ownership block (sidebar top strip + drag rules + html.sidebar-collapsed variants) is deleted. The header keeps --wails-draggable: drag; the no-drag child list gained nav (the tab strip's own padding sits between clickable tabs). desktop.js needed no changes — its computed-style check reads the event target, and every interactive child resolves no-drag (verified in-browser).

3. Header contents, left → right

  • below-md drawer burger (md:hidden, keyboard-operable label as before)
  • the contextual title (.NavTitle behavior kept — "msgbrowse" on home, conversation name on transcripts, always links home), width-capped so it can never run under the centered tabs
  • centered tabs: Messages → /, Media → /media, absolutely centered on the window, boosted like all in-app nav, styled as a segmented pair consistent with the settings-subnav aesthetic
  • search pill (hidden below sm) plus a new below-sm icon-only /search link, theme toggle, settings gear

4. 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.js re-derives it from location.pathname on htmx:afterSettle / popstate / htmx:historyRestore — the exact event wiring sidebar.js already 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; /media and /gallery* activate Media; everything else (/search, /settings, /providers, /logs, /status) activates neither.

Route note: GET /media is a new alias that renders the gallery handler directly (single request, no redirect hop). /gallery stays 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 the GET /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-full aside, min-h-0 on the list — kept identical on the #186 OOB copy). The #sidebar-pinned-section / #sidebar-conversations / #sidebar-conversations-count OOB target ids and all sidebar.js behavior 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-collapsed CSS, the sidebar-toggle.js collapse branch and its localStorage key (the new script also removes the stale key), and the related test assertions. cmd/msgbrowse-desktop sets Wails MinWidth: 800 / MinHeight: 600 so 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) alongside make desktop-test.

Two mechanical notes

  • Un-layered vs layered cascade: the hand-written .toolbar-icon-btn display rule (un-layered) outranks Tailwind's layered sm:hidden/md:hidden utilities, so the burger/search-link breakpoint gates are restated un-layered in input.css (caught in live verification, covered by the markup + CSS drift tests).
  • htmx revealed under inner scrolling: htmx 2.0.4 only re-checks revealed sentinels after a window scroll/resize event, and inner-container scrolls don't bubble. shell.js forwards them (capture-phase listener → synthetic window scroll), keeping the transcript/gallery infinite scroll alive without touching the documented hx-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 clean
  • rm -rf .tools && make css twice + once more from clean cache — byte-identical app.css committed
  • Live headless-Chrome pass against the fixture archive: 1000px (no burger, tabs centered, Messages active), transcript scroll-under with gap == 0 between header bottom and conv-header top, boosted tab sync across /media/settings/ → history-back, 500px overlay drawer open/close with aria-expanded sync, desktop-chrome 80px inset + drag/no-drag computed values, inner-scroll → window scroll forwarding

🤖 Posted on behalf of @joestump by Claude.

joestump and others added 2 commits July 6, 2026 07:42
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant