Stage 307: PR #1758 — feat(composer): click pasted/attached image thumbnails to lightbox-zoom them by @nesquena-hermes

This commit is contained in:
test
2026-05-06 20:01:54 +00:00
3 changed files with 115 additions and 4 deletions
+2 -1
View File
@@ -894,7 +894,8 @@
.attach-chip--audio,.attach-chip--video{max-width:260px;}
.attach-media-icon{display:inline-flex;align-items:center;color:var(--accent-text);}
.attach-chip-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.attach-thumb{width:56px;height:56px;object-fit:cover;border-radius:4px;display:block;cursor:default;}
.attach-thumb{width:56px;height:56px;object-fit:cover;border-radius:4px;display:block;cursor:zoom-in;transition:filter .12s ease, transform .12s ease;}
.attach-thumb:hover{filter:brightness(1.05);transform:scale(1.04);}
textarea#msg{width:100%;background:transparent;border:none;outline:none;color:var(--text);font-size:16px;line-height:1.65;padding:12px 16px 6px;resize:none;min-height:44px;max-height:200px;font-family:inherit;}
textarea#msg::placeholder{color:var(--muted);}
.composer-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 10px 10px;position:relative;container-type:inline-size;container-name:composer-footer;}
+13 -3
View File
@@ -296,9 +296,19 @@ function _closeImgLightbox(lb) {
}
document.addEventListener('click', e => {
const img = e.target && e.target.closest ? e.target.closest('.msg-media-img') : null;
if(!img) return;
_openImgLightbox(img.src, img.alt);
if(!e.target || !e.target.closest) return;
// Message-attached images (already wired since v0.50.x).
let img = e.target.closest('.msg-media-img');
if(img){ _openImgLightbox(img.src, img.alt); return; }
// Composer attach-tray image thumbnails — click any pasted/dropped image
// chip to lightbox-zoom it before sending. Excludes audio/video chips,
// which keep their inline media controls. SVG thumbnails (.attach-thumb--svg)
// are still images visually, so they qualify.
img = e.target.closest('.attach-thumb');
if(img && img.tagName === 'IMG'){
_openImgLightbox(img.src, img.alt || img.title || 'Attached image');
return;
}
});
const _IMAGE_EXTS=/\.(png|jpg|jpeg|gif|webp|bmp|ico|avif)$/i;
+100
View File
@@ -0,0 +1,100 @@
"""Regression tests for composer attach-thumb lightbox click behaviour.
User pasted/dropped/picked an image and wants to verify the right one
attached before sending. Clicking the thumbnail in the composer's
attach-tray should open the existing image lightbox (the same one
that's wired to message-attached images).
This file pins the wiring at the source level the document-level
delegated click handler must:
- Continue handling .msg-media-img (existing v0.50.x behaviour).
- Also handle .attach-thumb on IMG elements (new in this PR).
- NOT trigger on the chip's × remove button (sibling element).
- NOT trigger on audio/video chips (those have native controls).
It also pins the CSS cursor affordance so users discover the feature.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI = ROOT / "static" / "ui.js"
STYLE = ROOT / "static" / "style.css"
class TestComposerChipLightboxDelegate:
def test_delegate_handles_attach_thumb_clicks(self):
"""The document click handler must pick up clicks on .attach-thumb
(composer image chips) and route them to _openImgLightbox().
Previously the handler only looked for .msg-media-img.
"""
src = UI.read_text(encoding="utf-8")
assert "e.target.closest('.attach-thumb')" in src, (
"Document click delegate must also match .attach-thumb"
)
# And it must call _openImgLightbox in that path.
# Use a tighter anchor block to ensure both branches are wired.
anchor = (
"img = e.target.closest('.attach-thumb');\n"
" if(img && img.tagName === 'IMG'){\n"
)
assert anchor in src
def test_delegate_still_handles_message_attached_images(self):
"""Existing .msg-media-img wiring must not regress."""
src = UI.read_text(encoding="utf-8")
# The message-image branch must come first (so _openImgLightbox
# fires for them without falling through to the .attach-thumb check).
msg_branch = "let img = e.target.closest('.msg-media-img');\n if(img){ _openImgLightbox(img.src, img.alt); return; }"
assert msg_branch in src
def test_delegate_excludes_audio_video_chips(self):
"""Audio/video chips have their own inline controls (native <audio>
/ <video>) they don't get a thumbnail .attach-thumb at all, so
the handler can't possibly trigger on them. Pin that the chip
renderer uses .attach-chip--audio / .attach-chip--video sibling
classes (no IMG with class attach-thumb in those branches).
"""
src = UI.read_text(encoding="utf-8")
# Audio chip block — uses <audio>, no .attach-thumb img
assert "<audio controls preload=\"metadata\"" in src
# Video chip block — uses <video>, no .attach-thumb img
assert "<video controls preload=\"metadata\"" in src
# The .attach-thumb img tag is only generated in the image / svg branches.
# Quick structural check: every chip-rendering line that emits
# `class="attach-thumb"` has either `<img class="attach-thumb"` or
# `attach-thumb attach-thumb--svg`. Both are images.
for line in src.splitlines():
if 'class="attach-thumb' in line:
assert "<img " in line, (
"Every .attach-thumb emission should be an <img> tag, "
f"got: {line.strip()[:120]}"
)
class TestComposerChipCursorAffordance:
def test_attach_thumb_cursor_is_zoom_in(self):
"""`cursor: zoom-in` signals to the user that the thumbnail is
clickable for zoom the most discoverable affordance for this UX.
Previously it was `cursor: default` which silently advertised
non-interactivity.
"""
src = STYLE.read_text(encoding="utf-8")
# The .attach-thumb rule must declare cursor:zoom-in
# Use a substring search resilient to other property additions.
for line in src.splitlines():
if line.strip().startswith(".attach-thumb{"):
assert "cursor:zoom-in" in line, (
f".attach-thumb cursor must be 'zoom-in', got: {line.strip()[:120]}"
)
break
else:
raise AssertionError(".attach-thumb selector not found in style.css")
def test_attach_thumb_has_hover_emphasis(self):
"""Subtle hover emphasis (brightness + scale) reinforces the
zoom-in cursor by giving instant visual feedback before click.
"""
src = STYLE.read_text(encoding="utf-8")
assert ".attach-thumb:hover{" in src or ".attach-thumb:hover {" in src