Repository navigation
feat(annotate): hide HTML tools by default, toggle chords for annotate mode and tools, shortcut tooltips on the header controls - #1531
Merged
Conversation
An HTML document is authored to fill the viewport, so a raw-HTML (or live-app) annotate session now opens with the floating chrome over the page removed and the header eye rendering its "Show tools" state from the first paint. DEFAULT_HTML_CHROME_STATE.toolsHidden flips to true and App seeds htmlToolsHidden true to match, so there is no flash of tools before the restore effect runs. A fresh persisted record still wins in both directions: a reviewer who explicitly showed the tools keeps them next session. Hidden can never strand anyone — the eye lives in the header on desktop and the compact Options menu offers "Show tools" — which is the same reasoning that already made restoring a hidden cookie safe.
…r the tools Mod+Shift+A already flipped annotate mode in both directions in the app; the scope described it as a re-arm only. It is now documented as the toggle it is — the answer to "Esc dropped me to Interact, how do I get back?" — and pinned by a test that presses it twice. Adds Mod+Shift+X for the header eye, wired through the same scope so it reaches the help modal and the generated marketing shortcuts page, and mirrored inside the sandboxed iframe by the bridge on the capture phase exactly like the annotate chord (additive message type, no protocol bump), so it works whichever document owns focus. The binding is deliberately un-mnemonic: every mnemonic letter is a browser chord (H Home/history, E/I/J/K/C devtools, B/O bookmarks, V paste-as-plain-text, T reopen tab), and a chord the browser eats is worse than an arbitrary one. The tools chord is not gated on documentReadOnly, matching the eye. Disarming through either path already tears the pending draft down via the bridge's set-annotate-mode(false) handler, so toggling to Interact cannot leave a draft floating over a page that now takes clicks.
The eye, the pen and Refresh leaned on the native title attribute, which never appears on keyboard focus, is unstyled, and cannot show a shortcut legibly. They now use the app's Tooltip (the same primitive the review header and DecisionControl use — no new dependency): two lines, the control's description over its shortcut as keycaps rendered through the registry's platform-aware formatter, so a Mac shows glyphs and everyone else Ctrl rather than a hardcoded Cmd. The description strings are the same host-overridable labels the titles carried, so a @plannotator/ui consumer's overrides keep working; the new shortcuts prop overrides the bindings per control and defaults to the html-annotate scope, so a tooltip cannot claim a chord the app does not dispatch. Because title no longer supplies the accessible name, the pen states an aria-label (defaulting to its description), the eye keeps its sr-only text and Refresh its aria-label, and the shortcut is attached as a persistent aria-describedby span — this Base UI build puts no ARIA on the popup at all, so the tooltip alone would never be announced. Refresh has no chord and renders no keycap row.
This was referenced Sep 15, 2026
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three owner requests for the raw-HTML annotate surface.
1. Tools hidden by default
DEFAULT_HTML_CHROME_STATE.toolsHiddenis nowtrue, andAppseedshtmlToolsHiddentrueto match so there is no flash of floating chrome beforethe restore effect runs. The page gets the whole viewport; the header eye renders
its "Show tools" state from the first paint, and the compact Options menu reads
"Show tools" on a fresh session.
A fresh persisted record still wins in both directions — a reviewer who
explicitly showed the tools keeps them next session — and hidden can never strand
anyone, because the control that flips it back lives in the header (and in the
compact menu), never in the hidden chrome. That is the same reasoning that already
made restoring a hidden cookie safe.
One consequence worth knowing: the version-diff "Show changes" control lives in
that floating cluster, so on a fresh session it is behind the eye. Documented in
AGENTS.md rather than moved.
2. Toggle chords
Mod+Shift+Awas already a real toggle in the app —handleHtmlAnnotateToggleflips
htmlAnnotateArmed, and the bridge forwards the chord from inside theiframe — but the scope described it as a re-arm only and AGENTS.md said the same.
It is now described as the toggle it is and pinned by a test that presses it
twice, so the answer to "Esc dropped me to Interact, how do I get back?" is the
same chord.
New:
Mod+Shift+Xtoggles the tools (the header eye), through the same scope— so it reaches the in-app help modal and the generated marketing shortcuts page —
and mirrored inside the sandboxed iframe by the bridge on the capture phase
exactly like the annotate chord. The bridge message is additive
(
plannotator-bridge-tools-toggle, same token/origin path), so no protocol bump.The tools chord is deliberately not gated on
documentReadOnly, matching theeye, which renders on read-only documents.
3. Tooltips on the header controls
The eye, pen and Refresh leaned on the native
title, which never appears onkeyboard focus, is unstyled, and cannot show a chord legibly. They now use the
app's existing
Tooltip(the same primitive the review header andDecisionControluse — no new dependency): two lines, the control's descriptionover its shortcut as keycaps rendered through the registry's platform-aware
formatter.
The description strings are the same host-overridable
labelsvalues the titlescarried, so a
@plannotator/uiconsumer's overrides keep working. A newshortcutsprop overrides the bindings per control and defaults to thehtml-annotatescope, so a tooltip cannot claim a chord the app does notdispatch. Refresh has no chord and renders no keycap row (the owner did not ask
for one; adding a binding was not free of collision risk, so it was left alone).
Decisions
Live-app scope: yes, same default.
isHtmlSurfacecovers raw-HTML files,linked
.htmldocs and live-app sessions (mode: "annotate-app"), and all threenow open hidden. Same header, same eye, same reasoning — the page gets the
viewport — and one default is simpler to explain than a surface-aware split. No
second cookie was introduced.
Tools binding:
Mod+Shift+X, chosen for being un-mnemonic. Every mnemoniccandidate is claimed by a browser:
His Chrome's Home and Firefox's historylibrary,
E/I/J/K/Care devtools,B/Obookmarks,Vpaste-as-plain-text,
Treopen-tab.Mod+Shift+.would have been the naturalsibling of focus mode (
Mod+.) but the matcher comparesevent.keyexactly, andShift+
.reports>— it would never fire without an engine change.Xisunassigned in Chrome, Firefox, Safari and Edge on every platform, and free in
both plan registries (asserted by a single-claimant test).
Draft on toggle-off: closed, same as Esc. No new code was needed — the
bridge's
set-annotate-mode(false)handler already tears down every pendingaffordance (posts
selection-clear, clears the pending selection, pin, hover andmulti-target). So disarming from the chord, from inside the iframe, or from the
header pen all land in the same state, and a draft can never be left floating over
a page that now takes clicks.
Test plan
DOM_TESTS=1 bun test packages/ui packages/editor— 1616 pass / 4 fail, the4 being the pre-existing order-dependent
DocBadges open-in placementfailures(they pass in isolation: 8/8; baseline on this branch point was the same 4).
Six tests added: the two chords in
App.htmlChrome.test.tsx, the bridgeforwarding in
htmlLiveProtocol.test.tsx, the single-claimant binding guard inshortcuts.test.ts, and two tooltip tests inHtmlSurfaceControls.test.tsx(hover AND keyboard focus; the keycaps are asserted from
formatShortcutBindingTokens, never hardcoded, so the test cannot pass whilelying to non-Mac users).
bun test packages/ui/shortcuts.test.ts— 18 pass.bun run typecheck— clean.bun run --cwd packages/ui smoke:package— packs, resolves and installsexternally.
bun run --cwd apps/review build && bun run build:hook— clean.bun run build:marketing— clean; the generated shortcuts page now lists"Toggle annotate mode" and "Show or hide tools" on both plan surfaces.
Existing tests updated rather than deleted: the
htmlChromedefaults, the Appchrome contract (now "default hidden", plus a new case proving a fresh
toolsHidden: falsecookie still wins), and theHtmlSurfaceControlsstring pin(the deliberate copy pin survives; it now asserts the accessible name that
titleused to supply, which is the regression that would otherwise slip through).
Screenshots
Captured against a live
plannotator annotate sample.htmlsession in Chromium at1440x900 (2x):
no comment/attachments cluster, and the header eye shows its eye-off icon with
aria-pressed="true".Mod+Shift+X— tongue tabs on the left, Images/Comment clustertop-right, eye flipped to open (
aria-pressed="false").⌘ ⇧ Xkeycaps, positioned belowthe button.
⌘ ⇧ A.Docs
AGENTS.md (the eye/pen interaction-model paragraph, the
Mod+Shift+Asentence,the tooltip contract, the
htmlChromedefault),packages/ui/HANDOFF.md(thepublished component's props), and a new "HTML Surface Header Manual Checklist" in
tests/UI-TESTING.md.