Repository navigation
Open every picture and file in one Slack-style viewer, with rotate, zoom, Previous and Next, and Download - #881
Merged
Merged
Conversation
…h a message's files with it
Chat's pictures opened in assistant-ui's copied gallery dialog, and document and decision-card
pictures in the editor's own lightbox: two looks and two keyboards for one job, neither with
rotate or a zoom slider, both on a flat ink ground, with Download at the top beside Copy and Close.
Pressing a file sent with a message downloaded it at once.
Every caller now opens the editor's viewer (`ImageLightbox`), loaded only when a picture opens,
with its own stylesheet. It is laid out as Slack's: the whole window over the picture itself,
blurred and dimmed; along the top who sent it (their face and handle, chopin for the Planner's
pictures), then "3 hours ago in <document> – <name>" ("in a thread in <document>" for a thread's
messages), or for a document's picture the document's title and the picture's name, with Close at
the top end; the picture fitted with a margin; Rotate, then Zoom out, the zoom slider and Zoom in
at the bottom start; Download alone at the bottom end, with nothing to its right. Several pictures
step with Previous and Next at the sides and the arrow keys, and say "2 of 5". The keys, the wheel,
a pinch and a double-click zoom; a zoomed picture pans under a drag. Download reads the original
as bytes, so a signed-in upload saves as a file under its name; a picture whose host refuses the
read opens in a new tab without opener or referrer. Focus is trapped and returned; it fades in and
out, and not at all under reduced motion.
A message's files join its viewer after its pictures: a text file shows as its text, and Rotate
and zoom are not drawn for it. The shared tooltip draws itself inside an open modal dialog, so the
viewer's controls say their names. The document's title reaches the viewer through a small
`ImagePlace` context, and the viewer's styles leave the editor's stylesheet.
The new and changed viewer specs fail on 38716a9 (10 of them) and pass here.
Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types, bun run ci
Assistant-verification: bun test packages/editor/src/widgets packages/icons apps/web/src/chat apps/web/src/icon-tooltip.test.ts (1103 pass)
Assistant-verification: chat-media, chat-images, chat-images-gallery, chat-attachments, images, decision-option-previews on aaa1d6c (95 passed), and harness "images the Planner makes" (1 passed)
Assistant-verification: the changed chat-media and images specs on 38716a9 (10 failed)
Assistant-verification: bun run design:behavior (104 passed; cursors and emoji picker specs flaked under load and passed on their own, 20 passed)
Assistant-verification: bun run build (initial JS 259,958 B raw / 82,482 B gzip, initial CSS 184,551 B raw / 32,672 B gzip)
User-preference: the image click view should look something like Slack's, but remove everything to the right of the download button on the bottom right hand corner; the gallery for multiple images or documents in one message should have Next and Previous arrows
Co-authored-by: Alex Lavaee <lavaman131@github.com>
…round The viewer's stylesheet drew its own focus outline and suppressed the dialog's, which apps/web/src/focus.test.ts refuses: focus geometry belongs to the theme. The viewer now uses the theme's rule like everything else. On its dark ground petrol is under 3:1, so the theme gives the viewer's controls the page's colour for `--focus-ring-color`, beside the boundary offset it already sets. The dialog itself no longer takes focus; Close does on opening, as the old lightbox did, and Tab and Shift+Tab still wrap inside the viewer. Assistant-model: Claude Opus 5.5 Assistant-workflow: inline (subagent) Assistant-verification: bun test apps/web/src (1662 pass, 2 skip), bun test packages/editor (1004 pass), bun test scripts packages/icons packages/visuals (284 pass) Assistant-verification: bun run types, bun run ci Assistant-verification: chat-media, chat-images, chat-images-gallery, chat-attachments, images, decision-option-previews on 8634eef (95 passed after the images spec's colour serialization fix) Assistant-verification: bun run design:behavior e2e/design/chat-media.e2e.ts e2e/design/cursors.e2e.ts (22 passed) Assistant-verification: bun run build (initial JS 259,958 B raw / 82,484 B gzip, initial CSS 184,636 B raw / 32,692 B gzip) User-preference: the image click view should look something like Slack's, but remove everything to the right of the download button on the bottom right hand corner Co-authored-by: Alex Lavaee <lavaman131@github.com>
lavaman131
force-pushed
the
image-viewer-slack
branch
from
November 12, 2026 23:32
8dded4d to
ef9d362
Compare
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.
What the maintainer asked for
And, while this was in progress:
What was there
Two viewers did the same job:
<Dialog>, on flat ink. It had the sender and the time at the top, Copy, Download and Close at the top end, and the arrows at the sides. It had no zoom and no rotate.Pressing a file sent with a message downloaded it at once. A message's files could not be stepped through.
What changed
Every picture now opens in one viewer: Chat images, a message's files, document images and decision-card visuals. It is the editor's
ImageLightbox, rebuilt in place (packages/editor/src/widgets/image-lightbox.tsx), with its own stylesheet. It loads only when a picture opens.The layout follows the reference:
--color-gray-900over it.3 hours ago in Weekly review – chart.png. For a thread's messages it readsin a thread in Weekly review. A document's picture has no sender, so the header is the document's title, then the picture's name.How it works
2 of 5, as a status.+=-keys, and0to fit. The wheel and a trackpad pinch zoom about the pointer, and so does a two-finger pinch on a phone. A zoomed picture pans under a drag, only as far as its edges. A double-click switches between fitted and the picture's own pixels.<dialog>(icon-tooltip.tsx). Before this, it was drawn under the dialog's top layer, where nobody could see it.--duration-base(200ms) while the picture settles from 96%. It fades out over--duration-fast(120ms). Zoom and rotate ease over--duration-base. A drag, a pinch or the wheel follow the hand with no easing. Under reduced motion there is no animation and no transition.Smaller changes
ImagePlacecontext (@chopin/editor/image-place). It is provided where decision contexts are, and the thread panel turns it into "a thread in …".chat/image.cssare gone.RotateIconjoins Chopin's line icons. The design contract's exceptions forline.tsxandquestion-markdown.tsxare renewed: a new hash, and one new case for the icon's prop forwarding.Contrast (measured)
I measured contrast from screenshots with a script,
./contrast.md. For each element it takes the worst ground pixel around it and compares it with the element's computed colour.--color-gray-300)2 of 5The focus ring is a 2px white outline, so it has the icons' contrast. A disabled control (Zoom out when fitted) is exempt. On a phone, Previous and Next sit over the picture itself. With the controls' 12% white fill they measured 1.03:1 over a white picture, so they now carry a 62% dark circle of their own.
Impeccable passes
impeccable contexton the lightbox. The incumbent system is the authority; this is an Operate surface. As a subagent, I worked from the brief instead of an interview.bun run ci(Impeccablecheck-design) reports no new findings.Full notes:
critique.md.Checks
bun run types: passes.bun run ci: passes. dprint, oxlint, tokens, the design contract, the design record, and Impeccable report no new findings.bun test packages/editor/src/widgets packages/icons apps/web/src/chat apps/web/src/icon-tooltip.test.ts: 1103 pass. The lightbox state tests cover fit, rotation, slider scale, steps, double-click, pan limits, zoom about a point, pinch, relative time, the header line, file names and download names.--workers=2): chat-media, chat-images, chat-images-gallery, chat-attachments, images and decision-option-previews: 95 passed. The harness spec "images the Planner makes reach Chat" also passed.bun run design:behavior --workers=2(withCHOKIDAR_USEPOLLING=1, because this shared machine is out of inotify instances): 104 passed and 2 failed. The two failures arecursors.e2e.ts"shadcn's menu… emoji picker's tabs" andemoji.e2e.ts"every emoji is text…". In both, the emoji picker did not open in time under load, and neither touches the viewer. Rerun on their own, both files passed (20 passed). An earlier full run also found a real bug: in development, React's double effect closed the viewer as it opened. That is fixed, anddesign/chat-media.e2e.tscovers it.bun run build: initial JavaScript is 259,958 B raw and 82,482 B gzip; on main it was 259,958 B and 82,478 B. The budget is 260,500 and 83,450. The initial stylesheet is 184,551 B raw and 32,672 B gzip, the same as main. The budget is 185,000 and 33,000. The viewer is its own lazy chunk (image-lightbox-*.jsandimage-lightbox-*.css).Judgement calls
ImageGallery<Dialog>. Now Chat opens Chopin's own viewer, and the copied element keeps its bytes. The tiles still come from it, and its own dialog is still shown on the design-audit plate. The copy's dialog could not take a slider, rotate or a blurred ground without patching it, and the request was one design for every caller.--duration-base,--duration-fastand--ease-outrather than adding a token.Left open