agent-pages

Plan: Fix HTML Viewer Toolbar Collapse and Reveal UX

Download

Fix HTML Viewer Toolbar Collapse and Reveal UX

Summary

The HTML viewer toolbar is currently behaving according to its timer-based implementation, but that behavior feels delayed and visually inconsistent with the Markdown viewer.

The Markdown viewer can observe its own document scroll position, so it hides after deliberate downward scrolling and reappears after upward scrolling. HTML artifacts scroll inside an opaque sandbox="" iframe. The trusted parent viewer cannot inspect the iframe scroll position or direction, and iframe scroll events do not bubble into the parent.

The safe v1 fix is therefore not to imitate scroll-direction detection. Instead, make the HTML toolbar a fast, deterministic overlay that collapses independently of iframe loading, does not resize the artifact, and can be recalled through a minimal accessible top-edge activation area.

This plan keeps the existing security boundary intact:

What was observed

Markdown reference

HTML reference

The sun/moon pill visible at the top-right after collapse is not part of the agent-pages toolbar. It is an author-owned theme switch inside the published HTML artifact. The viewer-owned remnant is the small centered top-edge reveal handle.

Root cause

1. HTML visibility is timer-driven

app/static/viewer-toolbar.js defines:

var HTML_INITIAL_HIDE_DELAY_MS = 1800;
var HTML_REHIDE_DELAY_MS = 1400;

The initial timer is armed only by markIframeLoaded(). Because iframe load can wait for artifact resources such as remote images, actual collapse time is:

artifact load duration + 1800ms

This is why the toolbar appears slow to respond.

2. The parent cannot detect artifact scroll direction

The artifact is loaded through:

<iframe sandbox="" ...></iframe>

The empty sandbox gives the document an opaque origin and prevents scripts from running. The parent cannot safely read the artifact scroll position or attach a listener to its document. Scroll and wheel activity inside the nested browsing context does not bubble to the parent shell.

Therefore, exact Markdown-style “scroll down to hide, scroll up to reveal” cannot be implemented under the locked v1 security model.

3. Toolbar state changes resize the iframe

app/static/viewer.css currently changes both the host padding and iframe height:

body.viewer-html-host .viewer-main.viewer-main-iframe {
  padding: var(--toolbar-height) 0 0;
}

body.viewer-html-host .viewer-iframe {
  height: calc(100dvh - var(--toolbar-height));
}

body.viewer-html-host.viewer-toolbar-collapsed .viewer-main.viewer-main-iframe {
  padding-top: 0;
}

body.viewer-html-host.viewer-toolbar-collapsed .viewer-iframe {
  height: 100dvh;
}

Changing the iframe viewport height can re-run responsive layout inside the artifact and makes the transition feel less stable.

4. Existing tests encode the current implementation

The HTML tests assert the exact 1800ms and 1400ms constants and use source-string checks for load handling. They do not exercise the full visible → collapsed → revealed → collapsed interaction as browser behavior.

Desired outcome

Markdown

Leave Markdown behavior unchanged:

HTML

Adopt a secure overlay model:

Implementation plan

1. Replace iframe-load-gated initialization

Update app/static/viewer-toolbar.js.

Remove iframeLoaded as a prerequisite for initial collapse and interaction re-hide. The HTML controller should arm from trusted parent-shell initialization.

Recommended constants:

var HTML_INITIAL_HIDE_DELAY_MS = 900;
var HTML_REHIDE_DELAY_MS = 600;

After showToolbar():

  1. Wait until the parent has completed at least one paint using requestAnimationFrame.
  2. Schedule the initial hide timer.
  3. Apply the existing visibility guards before hiding.

Conceptually:

showToolbar();
requestAnimationFrame(function () {
  scheduleHide(HTML_INITIAL_HIDE_DELAY_MS);
});

Do not make iframe load restart or postpone this timer. The listener can be removed entirely unless it serves a separately documented recovery purpose.

pageshow should restore a deterministic visible state and schedule the same parent-owned initial timer.

2. Introduce an explicit HTML toolbar state model

Keep the Markdown scroll reducer unchanged.

For HTML, centralize transitions around these states and events:

State application must be the only place that changes:

This prevents timers, pointer handlers, and focus handlers from producing contradictory DOM state.

3. Keep the HTML iframe viewport constant

Update app/static/viewer.css so the HTML iframe is always full viewport:

body.viewer-html-host .viewer-main.viewer-main-iframe {
  position: fixed;
  inset: 0;
  max-width: none;
  margin: 0;
  padding: 0;
}

body.viewer-html-host .viewer-iframe {
  display: block;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  margin: 0;
  border: 0;
  border-radius: 0;
}

Remove the HTML-specific transitions that animate:

Keep the toolbar fixed above the iframe with its opaque themed surface. It may cover the top 3rem of the artifact while visible. When hidden, only the toolbar itself moves; the artifact viewport and scroll position remain unchanged.

The body-level collapsed class may be retained for reveal-control styling, but it must no longer resize the iframe.

4. Refine the top-edge reveal control

Keep the HTML-only semantic button in app/templates/viewer/html_host.html.

For fine-pointer devices:

For coarse-pointer devices:

Accessibility requirements:

5. Preserve interaction guards

The toolbar must not collapse while any of these is true:

When the final guard ends, cancel any stale timer and schedule the shorter re-hide delay.

The auto-hide controller must not close menus itself.

6. Separate viewer chrome from artifact-owned UI

Do not attempt to remove or restyle the sun/moon pill in the reference HTML artifact. It exists inside the sandboxed /raw document and is part of the published page.

Add a neutral HTML viewer fixture without a built-in fixed header or theme control. Use that fixture for host-toolbar browser screenshots and acceptance checks.

If the reference artifact should no longer contain its own theme pill, update that artifact’s source and publish a new page version as a separate content change.

7. Update automated tests

JavaScript state tests

Update tests/test_viewer_toolbar.py to:

Use fake timers or a pure reducer/controller harness. Do not make tests wait for real timeout durations.

CSS tests

Update tests/test_viewer_css.py to verify:

Template and security tests

Preserve assertions that:

8. Browser verification

Test in clean Safari and Chromium sessions at desktop and mobile-sized viewports.

HTML checklist

Markdown regression checklist

Documentation changes

Update:

Document the HTML behavior as:

The HTML host uses a parent-shell overlay toolbar that collapses shortly after the shell paints and is recalled through a top-edge control. The iframe remains full viewport and is never resized by toolbar visibility. Scroll direction inside the opaque sandboxed artifact remains unobservable.

Do not claim that HTML visibility follows artifact scroll direction.

Explicit non-goals

Do not:

Acceptance criteria

The work is complete when:

Future decision if exact scroll parity is required

Exact “scroll down inside HTML hides, scroll up reveals” requires cooperation from code executing inside the artifact browsing context. Achieving that would require a trusted scroll bridge, a different sandbox capability set, transformed artifact responses, and a new message-validation/security model.

That is a material architecture and security change which conflicts with the locked v1 decisions. It should be evaluated separately as a v1.1 proposal and must not be introduced as part of this toolbar UX fix.