Skip to content

Put the figure download over the picture, and let a figure be opened - #218

Merged
MikeAlhayek merged 1 commit into
mainfrom
ma/chat-figure-hover-download-and-zoom
Sep 22, 2026
Merged

MikeAlhayek merged 1 commit into
mainfrom
ma/chat-figure-hover-download-and-zoom

Conversation

@MikeAlhayek

Copy link
Copy Markdown
Member

Follows #215, which moved the figure download onto the picture and added click-to-enlarge — but did both inline in the MVC chat sample's own view. The shared chat UI that every other surface renders with (ai-chat.js, chat-interaction.js, ai-chat.css, chat-widget.css) kept the old shape: a button on a row of its own beneath the picture, and a thumbnail that could not be opened. This brings the same two behaviours to the package hosts actually consume.

The download

Drawn over the picture's lower corner and revealed by pointing at the figure, instead of a block under it. A figure is capped to a fraction of the viewport so a stack of them does not push the conversation off screen — that cap is what makes every figure a thumbnail, and in the chat widget, 380px wide, more so. Offering the download on a row of its own cost a band of vertical space per figure.

The wrapping <span class="mt-2 d-block"> is gone and the <a> is now a direct child of the figure. Everything about the link itself is unchanged — same address, same download attribute, same title, same classes — so the existing data-URI interception still names a saved file exactly the way it did.

Details worth naming:

  • The button keeps a pale ground of its own, because it sits on whatever the drawing happens to put in that corner.
  • The outline button it is built from inverts on hover, which against that pale ground leaves a white icon on white. Both colours are stated for hover, focus, focus-visible and active rather than only the resting state.
  • :focus-within reveals it too. A control that is invisible until it takes focus, and cannot be brought into view, cannot be used at all.
  • @media (hover: none) always shows it. A touch screen has no hover to reveal it with, and the widget is the surface most likely to be read on one.

The enlarging

Wired to medium-zoom on the same deferred pass as the broken-image handler, because a picture arrives after the markdown is handed back — the view writes it, and a streamed answer rewrites it on every chunk — so it cannot be attached once at load. data-zoom-wired marks a picture already attached so a re-render does not bind it twice.

The library is the host's to load. wireImageZoom returns immediately when mediumZoom is not a function, so a host that has not registered it sees exactly what it sees today: the picture shown, the download working, no enlarging. Nothing regresses for a host that does nothing.

The opened picture is matched as img.medium-zoom-image--opened rather than through the figure, because the library appends the copy to the document body — a descendant rule never reaches it. That copy carries the thumbnail's classes, so the frame and the viewport cap are taken off there, and only there, leaving the thumbnail in the conversation exactly as it was. chat-widget.css repeats that one rule unscoped for the same reason: a page that mounts only the widget loads only that file, and the copy lands outside the widget entirely.

Scope

Deliberately not ported from #215: the SVG-to-PNG conversion on click and the filename derived from the alt text. Both are specific to the tabular previews that PR introduced, and both would change what every host's figures download as. The sample keeps them.

wwwroot/ is the gulp output, rebuilt with npm run build. npm run test:unit passes (112 tests).

🤖 Generated with Claude Code

The MVC chat sample moved its download onto the picture and gained
click-to-enlarge in #215, but it did both inline in its own view. The shared
chat UI every other surface renders with -- the chat page, the widget and the
chat-interaction page -- kept the old shape: a button on a row of its own
beneath the picture, and a thumbnail that could not be opened.

A figure is capped to a fraction of the viewport so a stack of them does not
push the conversation off screen, which makes every one of them a thumbnail;
in the widget, 380px wide, more so. So the two things a reader wants are to
see it larger and to keep a copy, and the second was costing a band of
vertical space under every figure to offer.

The download is now drawn over the picture's lower corner and revealed by
pointing at the figure, on a pale ground of its own because it sits on
whatever the drawing puts in that corner. focus-within keeps it reachable by
keyboard, and a touch screen -- which has no hover to reveal it with -- simply
always shows it. The link is otherwise untouched: same address, same download
attribute, so the existing data-URI interception still names the saved file
the way it did.

Enlarging is wired to medium-zoom on the same deferred pass as the
broken-image handler, because a picture arrives after the markdown is handed
back and a streamed answer rewrites it on every chunk. The library is the
host's to load: without it the picture is still shown and still downloadable,
and only the enlarging is missing, so a host that has not registered it loses
nothing it had. The opened copy is matched on its own class, since the library
appends it to the body rather than inside the figure -- the widget stylesheet
repeats that one rule unscoped for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MikeAlhayek
MikeAlhayek merged commit 183c309 into main Sep 22, 2026
9 checks passed
@MikeAlhayek
MikeAlhayek deleted the ma/chat-figure-hover-download-and-zoom branch September 22, 2026 16:22
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