# 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:

- Keep the artifact iframe at `sandbox=""`.
- Keep published HTML and `/raw` at `script-src 'none'`.
- Do not inject a scroll bridge into artifact HTML.
- Do not inspect `iframe.contentDocument` or `iframe.contentWindow.document`.
- Do not intercept iframe wheel or touch events with an overlay.

## What was observed

### Markdown reference

- The toolbar is visible near the top.
- Deliberate downward scrolling hides the complete toolbar.
- Upward scrolling reveals it immediately.
- The document content does not jump when toolbar visibility changes.

### HTML reference

- The parent toolbar waits for the iframe to finish loading.
- It then waits an additional `1800ms` before collapsing.
- Scrolling down or up inside the artifact does not affect toolbar state.
- Recalling the toolbar starts a separate `1400ms` re-hide delay.
- The iframe height changes from `calc(100dvh - 3rem)` to `100dvh`, which can cause artifact viewport reflow during the transition.

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:

```js
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:

```text
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:

```html
<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:

```css
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:

- Visible near the top.
- Hide after `64px` of accumulated downward scrolling.
- Reveal after `32px` of accumulated upward scrolling.
- Remain visible while a toolbar control has focus or a menu is open.
- Keep the existing content spacing and non-jumping document layout.

### HTML

Adopt a secure overlay model:

- Render the toolbar immediately with the shell.
- Collapse it within approximately one second of the shell’s first paint.
- Do not wait for iframe `load` or remote artifact resources.
- Keep the iframe at a constant full-viewport size in both toolbar states.
- Overlay the toolbar on top of the artifact instead of reserving and recovering iframe height.
- Reveal the toolbar through a narrow top-edge activation zone.
- Keep it visible while the pointer or focus is inside the toolbar or a menu is open.
- Collapse shortly after interaction ends.
- Keep JavaScript failure behavior safe: toolbar remains visible and usable.

## 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:

```js
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:

```js
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:

- `visible`
- `collapsed`
- `INITIAL_TIMEOUT`
- `REVEAL_START`
- `REVEAL_END`
- `TOOLBAR_INTERACTION_START`
- `TOOLBAR_INTERACTION_END`
- `MENU_OPENED`
- `MENU_CLOSED`
- `PAGESHOW`

State application must be the only place that changes:

- `.viewer-toolbar-hidden`
- `aria-hidden`
- toolbar `inert`
- reveal-control `hidden`
- reveal-control `aria-expanded`

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:

```css
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:

- `padding-top`
- iframe `height`
- collapsed iframe height overrides

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:

- Make the activation zone span the top edge with a maximum height of approximately `6–8px`.
- Keep it visually transparent while idle.
- Display the centered line only on hover or `:focus-visible`.
- Do not cover meaningful artifact content below the activation strip.

For coarse-pointer devices:

- Retain a visible centered handle with a practical touch target.
- Keep its visual footprint small.
- Do not make the entire top edge a large invisible touch overlay.

Accessibility requirements:

- Keep `type="button"`.
- Keep `aria-controls="viewer-toolbar"`.
- Set `aria-expanded="true"` only while the toolbar is visible.
- Keep a clear `:focus-visible` ring.
- Clicking or focusing the control must move focus into the toolbar without trapping it.
- The hidden toolbar must be `inert` and `aria-hidden="true"`.

### 5. Preserve interaction guards

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

- The pointer is over the toolbar.
- The toolbar contains `document.activeElement`.
- The version `<details>` menu is open.
- The theme `<details>` menu is open.
- The reveal control is hovered or focused.

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:

- Assert the new initial and re-hide delays.
- Remove tests that require iframe `load` to arm collapse.
- Verify initialization schedules collapse without iframe load.
- Verify initial timeout collapses only when no guard is active.
- Verify reveal hover, focus, and click show the toolbar.
- Verify interaction cancels stale timers.
- Verify leaving interaction schedules exactly one re-hide timer.
- Verify open menus and toolbar focus prevent collapse.
- Verify `pageshow` restores visible state and schedules a fresh timer.
- Verify class, `aria-hidden`, `inert`, and `aria-expanded` stay synchronized.

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:

- HTML iframe height is always `100vh` and `100dvh`.
- HTML main has no toolbar-dependent padding.
- `.viewer-toolbar-collapsed` does not change iframe height.
- Only toolbar opacity/transform animate.
- The reveal activation zone is bounded.
- Fine-pointer and coarse-pointer reveal styles are distinct.
- Reduced-motion removes toolbar/reveal animation without disabling state changes.

#### Template and security tests

Preserve assertions that:

- HTML uses `sandbox=""` exactly.
- Published HTML is absent from the parent DOM.
- `/raw` retains `script-src 'none'`.
- The host shell permits only trusted same-origin toolbar JavaScript.
- The iframe source and fullscreen link remain pinned to the immutable resolved version.
- Markdown does not render the HTML reveal control.

### 8. Browser verification

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

#### HTML checklist

- Toolbar and artifact paint immediately.
- Toolbar collapses within approximately one second of shell paint.
- Slow or failed remote images do not postpone collapse.
- The complete viewer toolbar slides out; no title, version, theme, or fullscreen control remains.
- The iframe dimensions do not change during show/hide.
- Artifact scroll position is preserved.
- Responsive artifact layout does not reflow during toolbar transitions.
- Top-edge hover reveals the toolbar on desktop.
- Keyboard focus and activation reveal it.
- Touch activation works on coarse-pointer emulation.
- Menus and focused controls remain visible during interaction.
- Re-hide occurs shortly after interaction ends.
- JavaScript-disabled behavior leaves the toolbar visible.
- The artifact’s own fixed controls remain untouched.

#### Markdown regression checklist

- Near-top toolbar remains visible.
- Downward scrolling hides it at the existing threshold.
- Upward scrolling reveals it at the existing threshold.
- Menus and focus keep it visible.
- Content spacing and scroll position do not change.

## Documentation changes

Update:

- `MEMORY.md` decision D36.
- `docs/architecture.md` viewer UX description.
- `AGENTS.md` HTML viewer summary if timing or overlay wording changes.

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:

- Add `allow-scripts` or `allow-same-origin` to the artifact iframe.
- Change `/raw` to permit scripts.
- Inject a scroll bridge into published HTML.
- Use `iframe.contentDocument` or inspect the artifact DOM.
- Intercept iframe wheel/touch gestures with a large overlay.
- Remove or restyle controls authored inside published HTML.
- Change Markdown thresholds or layout.
- Expand the viewer into editing or artifact navigation features.

## Acceptance criteria

The work is complete when:

- HTML toolbar collapse begins from the trusted shell rather than iframe `load`.
- HTML toolbar collapses within approximately one second even when artifact resources load slowly.
- HTML toolbar visibility never changes iframe height or viewport dimensions.
- The entire viewer toolbar disappears; only the appropriate top-edge reveal affordance remains.
- Toolbar reveal works with pointer, keyboard, and touch.
- Toolbar menus and focused controls never disappear mid-interaction.
- The reference artifact’s sun/moon pill is correctly recognized as artifact-owned content.
- Markdown scroll behavior is unchanged.
- `sandbox=""`, raw `script-src 'none'`, CSP, immutable version pinning, and fullscreen behavior remain intact.
- Automated tests and Safari/Chromium verification pass.

## 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.
