Skip to content

feat(annotate): open local image links in an HTML page in the image lightbox - #1676

Merged
backnotprop merged 3 commits into
mainfrom
feat/html-image-link-lightbox
Oct 3, 2026
Merged

backnotprop merged 3 commits into
mainfrom
feat/html-image-link-lightbox

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

What

In a raw-HTML annotate session, a link to a local image (<a href="renders/home-1440.png"><img …></a>) used to toast "Can't open home-1440.png — Only markdown, text and HTML documents open in Plannotator." It now opens in the existing image lightbox.

How

  • resolveHtmlLinkIntent (packages/ui/utils/htmlLinkNavigation.ts) has a new { kind: "image", path, url, label } intent for relative and root-relative links to png jpg jpeg gif webp svg avif bmp ico apng (the code-review preview set, isReviewImagePath). The url is the page's own /api/html-assets/<token>/… route, read from the <base href> the server already installs (htmlAssetRouteFromDocument). So the lightbox reads only what the page itself can load. Nothing new becomes readable.
  • Containment: an image is shown only when it resolves inside the current page's directory (the token root). If it resolves outside (../x.png, or a root-relative path outside a linked page's folder), a toast says "Images open only from this page's own folder." unsupported now carries a reason (type / outside-asset-root / no-asset-root). External http(s) image URLs still open in a new tab.
  • Lightbox: extracted from Viewer.tsx to packages/ui/components/ImageLightbox.tsx. The Viewer and App both mount it. It shows the image only through <img src>. When it opens it takes focus, which pulls focus out of the iframe. While open, it swallows keys on the window capture phase, so:
    • Esc closes it before the HTML Esc ladder runs.
    • Mod+Enter cannot submit a decision behind it. This also fixes the markdown lightbox, which did not block it before.
  • Armed vs Interact: unchanged. The bridge already decides this for every link. Armed clicks pin the element. Interact opens the lightbox.
  • Server: no route changes. .bmp is added to the shared asset content-type table (packages/shared/html-assets.ts, vendored to Pi), so the whole preview set serves. Without it, .bmp returned 415.

Verified

  • Typecheck. Full bun test: the only failure was the DOM allowlist, fixed by adding the new DOM test to test.yml.
  • New tests:
    • The image intent, root containment, encoding, no-asset-root, external images and parsing the asset route.
    • The prefix is pinned against the server constant.
    • A DOM test that the lightbox keeps Esc and Mod+Enter from reaching window listeners.
  • bun run --cwd apps/review build && bun run build:hook, and packages/ui smoke:package.
  • Live: a CLI compiled from this branch (temp PLANNOTATOR_DATA_DIR, headless Chromium):
    • Armed: clicking the thumbnail opened the comment composer and no lightbox.
    • Interact: clicking it showed the full 800×500 PNG from /api/html-assets/<token>/renders/x.png, with focus on the lightbox.
    • With the lightbox open, Meta+Enter and Ctrl+Enter posted no decision.
    • Esc closed the lightbox and the surface stayed in Interact.
    • The ../outside.png link showed the out-of-folder toast.

…ightbox

A link to a local image inside an annotated HTML page (a thumbnail linking
to its full-size render) used to toast "Can't open". It now opens in the
image lightbox the markdown Viewer already has, extracted to a shared
ImageLightbox component.

- resolveHtmlLinkIntent gains an image intent: png jpg jpeg gif webp svg
  avif bmp ico apng (the code-review preview set), resolved to the page's
  own /api/html-assets/<token>/ route and only inside the page's folder.
  Out-of-folder images and pages with no asset route keep a toast that
  says why.
- The lightbox takes focus (out of the iframe) and swallows keys on the
  window capture phase, so Esc closes it before the HTML Esc ladder and
  Mod+Enter cannot submit behind it (also fixes the markdown lightbox).
- Armed clicks still pin; Interact opens the lightbox, same as doc links.
- The asset route also serves .bmp (shared table, vendored to Pi).
htmlAssetRouteFromDocument matched any <base> naming /api/html-assets
anywhere in the page, so a comment or a later base tag could name a
different token than the one the browser actually applies. Skip comments
and take only the first <base> element, as the browser does. Adds tests
for that and for encoded-dot and backslash escapes in image links.
@backnotprop
backnotprop merged commit 8c51508 into main Oct 3, 2026
28 checks passed
@backnotprop
backnotprop deleted the feat/html-image-link-lightbox branch October 3, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant