Unify the Viewer Header for Markdown and HTML Pages
Problem
The viewer currently renders two intentionally different shell headers:
- Markdown pages use a centered document header constrained to
52rem, with a large1.75remtitle above the version and theme controls. - HTML pages use a compact, fixed, full-width
3remtoolbar with a1remellipsized title, version and theme controls, and an Open fullscreen action.
This split came from the full-screen HTML viewer design, which preserved Markdown as a traditional readable document. The result is that navigation and viewer-owned controls look like two different products even though both page formats belong to the same viewer.
This is a shell inconsistency, not a Markdown rendering or theme bug.
Desired Outcome
Use one compact top toolbar for both Markdown and HTML pages while preserving their different content behavior:
- Markdown remains a centered, normally scrolling readable document.
- HTML remains a full-viewport, sandboxed iframe.
- HTML alone retains the Open fullscreen action.
- Version selection, theme selection, title styling, focus states, and responsive behavior look consistent across formats.
- While reading Markdown, the toolbar fades and slides out when scrolling down, then reappears when scrolling up.
Important Scrolling Constraint
The hide-on-scroll behavior will apply to Markdown pages.
HTML artifacts scroll inside a sandboxed iframe with an opaque origin. The trusted outer viewer cannot inspect that iframe's scroll position or direction. Attempting to do so would require weakening the iframe sandbox or introducing a cooperation protocol with published content, both of which conflict with the current security model.
The HTML toolbar therefore remains visible and fixed. This preserves the existing sandbox boundary and keeps fullscreen and version controls available. The plan must not use wheel-event interception, sandbox relaxation, or injected code as workarounds.
Implementation Plan
1. Create a shared toolbar partial
Add app/templates/viewer/_toolbar.html containing:
- Page title.
- Shared version-menu partial.
- Shared theme-menu partial.
- Optional fullscreen action when a fullscreen URL is present.
Keep the toolbar server-rendered and semantically structured with <header>, <h1>, and <nav> elements.
2. Adopt the shared toolbar in both viewer templates
Update app/templates/viewer/html_host.html to render the shared partial without changing its iframe structure.
Update app/templates/viewer/markdown.html to:
- Add a Markdown-specific body class such as
viewer-markdown-host. - Replace the large
.viewer-headerand.viewer-titleblock with the shared toolbar. - Keep the existing
.viewer-main,.viewer-content, and.markdown-bodystructure. - Omit the fullscreen URL so Markdown does not display Open fullscreen.
- Load the trusted scroll-behavior asset only on Markdown pages.
This also reduces the prominent duplicate-heading effect when the page title and the first Markdown # heading contain similar text.
3. Refactor viewer CSS around shared chrome
In app/static/viewer.css:
- Make toolbar height, typography, border, spacing, title ellipsis, action alignment, dropdown placement, hover styles, and focus styles common to both formats.
- Keep the toolbar visually identical across formats.
- Let the Markdown toolbar be sticky and remain in normal document flow.
- Preserve fixed positioning and viewport-height calculations for the HTML host.
- Retain the Markdown content width of
52rem, content card, padding, typography, and normal scrolling. - Remove obsolete
.viewer-header,.viewer-header-controls, and.viewer-titlerules after the templates stop using them.
4. Add Markdown scroll-direction behavior
Add a small trusted static asset such as app/static/viewer-scroll.js. It should run only when body.viewer-markdown-host is present.
Behavior:
- Start with the toolbar visible.
- Keep it visible while the page is near the top.
- After a deliberate downward-scroll threshold, add a state class such as
.viewer-toolbar-hidden. - Fade and translate the toolbar upward using CSS, rather than removing it from layout.
- Reveal it promptly when upward scrolling passes a smaller threshold.
- Use hysteresis or accumulated scroll distance so trackpad noise and tiny direction changes do not make the toolbar flicker.
- Use a passive scroll listener and
requestAnimationFrameso scroll handling does not block rendering. - Keep the toolbar visible whenever it contains keyboard focus or either the version/theme
<details>menu is open. - Re-evaluate state on
pageshow, anchor navigation, and viewport changes. - If JavaScript fails or is blocked, degrade to an ordinary always-visible sticky toolbar.
Suggested motion is a short 160–220 ms opacity and transform transition. Under prefers-reduced-motion: reduce, remove the animation; the toolbar may still switch visibility immediately.
5. Keep the CSP change narrow
The current shell CSP uses script-src 'none'. Reliable scroll-direction detection is not broadly achievable with CSS alone, so Markdown shell pages need permission to load the trusted external script.
Update the viewer response policy so:
- Markdown shell pages use
script-src 'self'. - The script is external, content-fingerprinted, and loaded with
defer; do not allow inline scripts or'unsafe-inline'. - HTML host pages may retain
script-src 'none'because their toolbar does not react to iframe scrolling. - Raw published HTML must continue using
script-src 'none'. - No published content is ever able to call or modify the trusted shell script.
Add a content hash for the JavaScript asset in the same spirit as the existing viewer CSS cache-busting hash.
6. Handle responsive layouts explicitly
Verify the shared toolbar at narrow widths:
- The title shrinks first and uses single-line ellipsis.
- Version, theme, and fullscreen controls remain usable.
- Dropdown panels remain within the viewport and open from the appropriate edge.
- No controls wrap over or overlap the content.
- Keyboard focus rings remain visible.
- Revealing the toolbar does not cause layout shift.
If necessary, shorten or selectively hide nonessential action labels at the smallest breakpoint without introducing additional client state.
7. Update automated tests
Update tests/test_viewer_smoke.py:
- Replace the assertion that Markdown must not contain
.viewer-toolbar. - Verify Markdown renders the shared toolbar and retains the content card.
- Verify Markdown does not render the fullscreen action.
- Verify only Markdown includes the scroll-behavior asset.
Update tests/test_viewer_security.py:
- Preserve all HTML iframe, sandbox, immutable raw URL, and safe new-tab assertions.
- Verify the HTML host continues to provide the fullscreen action.
- Verify raw HTML retains
script-src 'none'. - Verify Markdown permits only same-origin shell scripts, with no inline-script allowance.
- Verify HTML hosts do not need broader script permission.
Update tests/test_viewer_css.py and theme tests:
- Verify shared toolbar styling and body-specific positioning.
- Cover hidden/visible state classes, transition rules, and reduced-motion behavior.
- Cover long-title ellipsis and bounded menus.
- Verify System, Light, and Dark selections work from both formats.
- Confirm no iframe or raw HTML palette behavior changes.
Add focused JavaScript behavior tests using a browser-capable test harness, or isolate the scroll-state calculation into a testable function and cover:
- Near-top visibility.
- Downward threshold.
- Upward reveal threshold.
- Jitter resistance.
- Open-menu and focused-control visibility.
- Back-forward restoration.
Run the complete pytest suite and perform browser-based visual QA.
8. Update documentation
Update README.md, docs/architecture.md, and MEMORY.md to explain:
- Markdown and HTML share viewer-owned toolbar chrome.
- Their content layouts remain intentionally different.
- Markdown hides/reveals the toolbar according to outer-document scroll direction.
- The HTML toolbar cannot respond to sandboxed iframe scrolling and stays visible.
- Published HTML still controls its own palette inside the sandbox.
- The previous decision that Markdown would not adopt the toolbar has been superseded.
- Trusted shell JavaScript is narrowly allowed on Markdown pages while published HTML remains script-blocked.
Add a note to docs/full-screen-html-viewer-plan.md or link to a new decision entry so the historical document no longer appears to describe current intended behavior.
Security and Compatibility Constraints
The change must not alter:
- REST or MCP schemas.
- Public or versioned viewer URLs.
- Markdown sanitization.
- HTML iframe sandboxing.
- Raw HTML CSP.
- Immutable version pinning.
- Theme-cookie behavior.
- Raw HTML fidelity.
The shell CSP change is limited to the same-origin Markdown viewer script. It must not enable scripts in published HTML.
Acceptance Criteria
- Markdown and HTML pages display the same compact top toolbar.
- Title, version, and theme controls use identical typography and alignment.
- On Markdown pages, deliberate downward scrolling fades/slides the toolbar away.
- On Markdown pages, upward scrolling reveals the toolbar promptly.
- The toolbar remains visible near the top, while focused, and while a menu is open.
- Tiny scroll-direction changes do not cause flicker.
- Reduced-motion preferences are respected.
- Without JavaScript, Markdown falls back to an always-visible sticky toolbar.
- Markdown remains centered, readable, and normally scrollable.
- HTML remains a borderless full-viewport sandboxed iframe with an always-visible toolbar.
- Open fullscreen appears only for HTML pages.
- Long titles and controls remain usable on mobile widths.
- Light, Dark, and System themes render correctly.
- Raw published HTML remains unable to execute scripts.
- All existing security guarantees and the complete automated test suite pass.