Plan: HTML Toolbar Auto-Hide and Markdown Content Spacing
Summary
Fix two viewer UX issues:
- The HTML viewer toolbar consumes permanent vertical space because it cannot observe scroll direction inside the sandboxed artifact iframe.
- Markdown content begins too close to the shared toolbar because its main container has no top padding.
Use a secure, format-specific interaction model:
- Markdown: retain the existing scroll-down hide and scroll-up reveal behavior.
- HTML: automatically collapse the toolbar after a short delay and reveal it through an accessible top-edge control. Do not inspect or intercept iframe scrolling.
- Markdown spacing: add a scoped
1.25remgap between the toolbar and content card.
Current Behavior and Root Cause
HTML toolbar
The HTML host deliberately prevents its outer document from scrolling. The artifact scrolls inside an iframe using sandbox="".
Scroll and wheel events inside that nested browsing context do not bubble to the parent document. Because the iframe has an opaque sandboxed origin, the outer viewer cannot inspect scrollTop or attach listeners to the artifact document.
The existing viewer-scroll.js also exits unless the body has viewer-markdown-host, and the HTML host CSP retains script-src 'none'. The fixed HTML toolbar is therefore expected behavior under decision D36, not a regression in the Markdown scroll state machine.
Markdown spacing
The shared Markdown main container currently uses:
.viewer-main {
padding: 0 1.25rem 2rem;
}
The zero top padding places the content card immediately below the sticky toolbar.
Chosen UX
Markdown
Keep the existing behavior unchanged:
- Visible near the top of the page.
- Hide after deliberate downward scrolling.
- Reappear after upward scrolling.
- Stay visible while focused or while a menu is open.
- Degrade to an always-visible sticky toolbar if JavaScript is unavailable.
HTML
Use auto-collapse rather than pretending to detect iframe scroll direction:
- Show the toolbar when the page initially loads.
- Start the collapse timer only after the iframe fires
load. - Collapse after approximately 1.5–2 seconds if no toolbar control has focus and no menu is open.
- Reveal when the visitor points at, focuses, or activates a narrow top-edge reveal control.
- Keep it visible while the pointer is over the toolbar, a control has focus, or a version/theme menu is open.
- Restart the collapse timer after toolbar interaction ends.
- Provide a visible compact handle for touch devices.
- If trusted shell JavaScript fails, leave the toolbar visible.
This recovers viewport space without changing the iframe sandbox, mutating published HTML, or enabling author scripts.
Implementation Plan
1. Generalize the trusted toolbar controller
Rename app/static/viewer-scroll.js to app/static/viewer-toolbar.js, or retain the filename while restructuring it into two explicit modes.
The controller should detect:
body.viewer-markdown-host: use the existing scroll-direction state machine.body.viewer-html-host: use an auto-collapse state machine driven by iframe load, timeout, toolbar focus/menu state, and reveal-control interaction.
Keep the Markdown state calculation pure and preserve its existing thresholds.
Add a separate HTML state model with named constants, for example:
HTML_INITIAL_HIDE_DELAY_MS = 1800HTML_REHIDE_DELAY_MS = 1400HTML_HIDDEN_CLASS = "viewer-toolbar-hidden"
Avoid sharing scroll-specific variables with the HTML timer behavior.
2. Add an HTML reveal control
Update app/templates/viewer/html_host.html or the shared toolbar partial to render an HTML-only reveal control adjacent to the toolbar in the trusted parent document.
Requirements:
- Use a real
<button type="button">. - Give it an accessible name such as Show viewer toolbar.
- Keep it fixed at the top edge with a z-index above the iframe.
- On desktop, expose a narrow activation zone across the top edge and a subtle visual handle.
- On touch/coarse-pointer devices, give the visible handle a practical hit target without covering important artifact content.
- Set
aria-expandedaccording to toolbar visibility. - Use
aria-controlspointing to a stable toolbar ID. - Do not place the reveal control inside the iframe.
The activation zone should intercept pointer input only at the very top edge; it must not create a large transparent overlay over the artifact.
3. Add HTML auto-collapse behavior
In the trusted controller:
- Find the shared toolbar, reveal control, and artifact iframe.
- Start the initial timer from the iframe
loadevent. - Do not collapse while:
- The toolbar contains
document.activeElement. - A toolbar
<details>element is open. - The pointer is over the toolbar.
- The reveal control has focus or hover.
- The toolbar contains
- On timeout, add
.viewer-toolbar-hidden. - On reveal-control
pointerenter,focus, orclick, remove the hidden class. - After interaction ends, schedule the shorter re-hide delay.
- Cancel stale timers before scheduling a new one.
- Update
aria-expandedwhenever visibility changes. - Restore a consistent visible state on
pageshow. - Never register wheel or scroll listeners against the iframe element as a substitute for iframe scroll events; those events are not reliable across browsing contexts.
- Never attempt
iframe.contentDocumentoriframe.contentWindow.document.
Do not automatically hide while a visitor is operating the version or theme menu.
4. Extend shared toolbar CSS safely
Refactor the hidden-state CSS so both host types can use the same visual transition:
.viewer-toolbar {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.viewer-toolbar.viewer-toolbar-hidden {
opacity: 0;
transform: translateY(-100%);
pointer-events: none;
}
Then add format-specific positioning:
- Markdown toolbar remains
position: sticky. - HTML toolbar remains
position: fixed. - The HTML iframe must continue occupying the full viewport below the nominal toolbar offset.
- When the HTML toolbar hides, decide whether the iframe should expand upward.
Recommended behavior: expand the visible iframe to the top without resizing its document on every transition. Move the iframe host upward and increase its visible height using a class on body or .viewer-main-iframe, with the same transition duration as the toolbar. Verify this does not trigger disruptive iframe reflow.
Style the reveal control so:
- It remains discoverable but visually quiet.
- It uses theme tokens.
- It has a visible
:focus-visiblering. - It does not overlap toolbar menus when the toolbar is open.
- It remains available when reduced motion is enabled.
Under prefers-reduced-motion: reduce, remove transitions but retain immediate show/hide state changes.
5. Restore Markdown breathing room
Add a body-scoped override after the shared .viewer-main rule:
body.viewer-markdown-host .viewer-main {
padding-top: 1.25rem;
}
Do not change the HTML iframe host padding and do not apply the gap globally to error pages.
Verify that the gap remains visually balanced at desktop and mobile widths. If 1.25rem feels excessive below the 3rem toolbar on small screens, use a responsive token such as clamp(0.875rem, 2vw, 1.25rem).
6. Keep the security boundary intact
The HTML auto-collapse controller runs only in the trusted outer shell.
To load it on HTML host pages:
- Change the HTML host CSP from
script-src 'none'toscript-src 'self'. - Load only the external content-fingerprinted toolbar asset with
defer. - Do not add
'unsafe-inline', nonces for published content, or broad script origins. - Keep the raw HTML response CSP at
script-src 'none'. - Keep the iframe attribute exactly
sandbox="". - Do not inject scripts, markup, or styles into published HTML.
- Keep the iframe source pinned to the immutable resolved
/rawversion.
This permits trusted shell behavior without granting script execution to the artifact.
Record this as an explicit update to D36: trusted same-origin shell JavaScript is permitted for Markdown and HTML host chrome, while /raw and published HTML remain script-blocked.
7. Update templates and asset fingerprinting
Update:
app/templates/viewer/base.htmlor format templates to load the controller for both Markdown and HTML host pages.app/templates/viewer/_toolbar.htmlto add a stable toolbar ID.app/templates/viewer/html_host.htmlto render the HTML-only reveal control.app/viewer/routes.pyto fingerprint the renamed/shared JavaScript asset.- HTML host response headers to use a narrowly scoped trusted-shell CSP.
Markdown and HTML should still share the same toolbar markup. Only the reveal control and fullscreen action remain HTML-specific.
8. Automated tests
State-machine tests
Extend tests/test_viewer_scroll.py or rename it to tests/test_viewer_toolbar.py.
Preserve Markdown tests for:
- Near-top visibility.
- Downward hide threshold.
- Upward reveal threshold.
- Jitter resistance.
- Focus/menu visibility.
Add HTML tests for:
- Toolbar initially visible.
- No timer before iframe load.
- Initial collapse after the configured delay.
- Reveal-control hover/focus/click.
- Re-hide after interaction ends.
- Open version/theme menus preventing collapse.
- Keyboard focus preventing collapse.
- Stale timers being cancelled.
pageshowrestoring a deterministic state.aria-expandedmatching toolbar visibility.
Use fake timers where possible; do not make the suite sleep for real timeout durations.
Template and CSP tests
Verify:
- HTML host includes the trusted toolbar asset.
- HTML host includes the accessible reveal button and stable toolbar ID.
- Markdown includes no HTML reveal control.
- HTML host CSP contains
script-src 'self'and excludes'unsafe-inline'. - Raw HTML retains
script-src 'none'. - The iframe retains
sandbox="". - Published HTML is not present in the parent DOM.
- The iframe and fullscreen links stay pinned to the resolved immutable version.
CSS tests
Verify:
- Both formats share hidden-state opacity/transform rules.
- Markdown receives the new top spacing.
- HTML transition classes expand the visible iframe area correctly.
- The reveal control has focus styling and a bounded activation area.
- Reduced-motion rules remove animation.
9. Browser verification
Perform visual QA in a clean/incognito Chromium session at representative desktop and mobile widths.
Markdown checklist
- There is a clear
1.25rem-class gap below the toolbar. - Downward scrolling hides the toolbar.
- Upward scrolling reveals it.
- Menus and keyboard focus keep it visible.
- The content card does not jump when toolbar visibility changes.
HTML checklist
- The artifact and toolbar render immediately.
- The toolbar collapses after iframe load plus the configured delay.
- The iframe fills the recovered space without a blank 3rem strip.
- Moving to or focusing the top-edge control reveals the toolbar.
- The toolbar remains visible while interacting with version, theme, and fullscreen controls.
- The reveal control works with mouse, keyboard, and touch emulation.
- Artifact scrolling and interaction remain unaffected.
- Historical versions continue using their pinned
/rawbodies.
Verify System, Light, and Dark shell themes in both formats.
10. Documentation and decision updates
Update:
README.mddocs/architecture.mdMEMORY.md- The earlier unified-toolbar plan or its superseding note
Document that:
- Markdown visibility follows outer-document scroll direction.
- HTML visibility uses trusted-shell auto-collapse and an explicit reveal control because iframe scroll is unobservable.
- HTML host shell JavaScript is now narrowly permitted.
- Published HTML remains static, sandboxed, and unable to execute scripts.
- Raw HTML and fullscreen fidelity remain unchanged.
Explicit Non-Goals
Do not:
- Remove or weaken
sandbox="". - Add
allow-scriptsorallow-same-originto the artifact iframe. - Inject a scroll bridge into published HTML.
- Change
/rawbodies or their CSP. - Intercept wheel/touch events with a large overlay.
- Pretend that iframe-element wheel events reliably represent artifact scroll direction.
- Expand into general viewer navigation or editing features.
Acceptance Criteria
- Markdown content has a deliberate, consistent gap below the toolbar.
- Markdown hide-on-scroll behavior continues to work unchanged.
- HTML toolbar automatically collapses after the artifact loads.
- HTML artifact uses the recovered viewport space after collapse.
- HTML toolbar can be revealed with pointer, keyboard, and touch.
- Toolbar menus and focused controls never disappear mid-interaction.
- JavaScript failure leaves an always-visible usable toolbar.
- Reduced-motion preferences are respected.
- The iframe remains
sandbox="". - Raw and published HTML remain unable to execute scripts.
- Immutable version pinning, theme behavior, CSP protections, and API behavior remain intact.
- The complete automated suite and browser verification pass.