aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-12 12:57:49 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-12 12:57:49 +0200
commit13e7980071ab11b07b5d473e43506c6cb4ab637e (patch)
treec1a76c5125511c810f107dc110f09e7f0c5b9cc2 /packages
parent3324b4eb701e34f64e859dd90937793ccf7f75c5 (diff)
downloadmeshbay-13e7980071ab11b07b5d473e43506c6cb4ab637e.tar.gz
fix: keep the photo lightbox's close button on a phone screen
The bar was 457 px wide and pushed the close button to 397..437 px, off a 360 px phone. Delete leaves the lightbox (it stays on the tile's menu: right-click, or press and hold), and below 768 px the bar drops the file name, the zoom label and 100%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photos-app.js13
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css10
-rwxr-xr-xpackages/meshbay-hub/tests/harness/photo_delete_probe.py32
-rw-r--r--packages/meshbay-hub/tests/test_layout_measured.py51
-rw-r--r--packages/meshbay-hub/tests/test_photo_delete.py15
5 files changed, 81 insertions, 40 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
index df207ab..6dd2310 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
@@ -180,7 +180,7 @@ const ZOOM_MAX = 400;
const SLIDESHOW_MS = 5000;
function Lightbox({
- photos, index, transportRef, gekRef, onClose, onNav, linkFor, onDelete, onPlay,
+ photos, index, transportRef, gekRef, onClose, onNav, linkFor, onPlay,
}) {
const entry = photos[index];
// A video is shown by its thumbnail and played in the group's video
@@ -330,10 +330,6 @@ function Lightbox({
<button class="video-close" onClick=${() => copyLink(linkFor(entry))}
title=${t('link.copy')} aria-label=${t('link.copy')}>
<${Icon} name="link" /></button>`}
- ${onDelete && onDelete(entry) && html`
- <button class="video-close" onClick=${onDelete(entry)}
- title=${t('group.delete')} aria-label=${t('group.delete')}>
- <${Icon} name="trash" /></button>`}
<button class="video-close" onClick=${onClose} title=${t('video.close')}>
<${Icon} name="close" /></button>
</div>
@@ -375,8 +371,9 @@ function AlbumView({
album, entries, transportRef, gekRef, setError, onBack, readOnly, linkFor, onDelete, onPlay,
}) {
const [lightboxIndex, setLightboxIndex] = useState(null);
- // Right-click a photo for its link, or to delete it. A tile has no dots to
- // press, so on a touchscreen both are in the lightbox's bar instead.
+ // Right-click a photo, or press and hold it, for its link or to delete it.
+ // Deleting is here only: the lightbox's bar has no room left for it on a
+ // phone, where it pushed the close button off the screen.
const { menu, openAt, close: closeMenu } = useMenu();
const onTileMenu = (e, entry) => {
const link = linkFor ? linkFor(entry) : null;
@@ -442,7 +439,7 @@ function AlbumView({
<${Lightbox} photos=${album.photos} index=${lightboxIndex}
transportRef=${transportRef} gekRef=${gekRef}
onClose=${() => setLightboxIndex(null)} onNav=${navigate} linkFor=${linkFor}
- onDelete=${onDelete} onPlay=${onPlay} />
+ onPlay=${onPlay} />
`}
${menu && html`<${Menu} ...${menu} onClose=${closeMenu} />`}
`;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index b5996d8..586f36d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -5295,6 +5295,16 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
flex-shrink: 0;
}
+/* A phone has room for the zoom buttons, slideshow, cast, link and close —
+ not for the file name, the zoom label and 100% as well: they pushed the
+ close button off the right of the screen. */
+@media (max-width: 768px) {
+ .photo-lightbox .video-top-bar { padding-left: 12px; padding-right: 12px; gap: 6px; }
+ .photo-lightbox .video-title,
+ .photo-lightbox .photo-zoom-percent,
+ .photo-lightbox .photo-icon-btn-text { display: none; }
+}
+
.photo-lightbox-info {
position: absolute;
bottom: 0;
diff --git a/packages/meshbay-hub/tests/harness/photo_delete_probe.py b/packages/meshbay-hub/tests/harness/photo_delete_probe.py
index a99566d..2c41b14 100755
--- a/packages/meshbay-hub/tests/harness/photo_delete_probe.py
+++ b/packages/meshbay-hub/tests/harness/photo_delete_probe.py
@@ -5,8 +5,8 @@ Deleting a photo from the Photos app, in a real browser.
Mounts the shipped `PhotosApp` on a made-up index, with a transport that
records what it is asked to delete and an index that drops what was deleted,
as a node's does. Right-clicks a tile, picks Delete, answers the page's own
-confirmation (ask.js) one way then the other, and does the same from the
-lightbox's bar.
+confirmation (ask.js) one way then the other, and checks the lightbox's bar
+offers none.
photo_delete_probe.py
@@ -109,16 +109,8 @@ const pick = async (label) => {
await wait(50);
return true;
};
-// The page's own confirmation: what it says, whether it is the topmost thing
-// on screen, and an answer.
+// The page's own confirmation: what it says, and an answer.
const dialog = () => document.querySelector('[role="alertdialog"]');
-const onTop = () => {
- const d = dialog();
- if (!d) return false;
- const r = d.getBoundingClientRect();
- const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
- return Boolean(hit && d.contains(hit));
-};
const answer = async (yes) => {
const d = dialog();
const buttons = [...d.querySelectorAll('button')];
@@ -172,21 +164,12 @@ const answer = async (yes) => {
await closeMenus();
cases.push({ case: 'operator', delete_offered: all });
- // -- from the lightbox: the button, the dialog above it, the next photo --
+ // -- the lightbox: no delete button, the tile is where it is --
await mount({});
document.querySelectorAll('.photo-tile')[0].click();
await frame();
- const title = () => (document.querySelector('.photo-lightbox .video-title') || {}).textContent;
- const before = title();
- const btn = document.querySelector('.photo-lightbox button[aria-label="Delete"]');
- if (btn) btn.click();
- await wait(50);
- const dialogOnTop = onTop();
- if (dialog()) await answer(true);
- await wait(100); await frame();
- cases.push({ case: 'lightbox', button: Boolean(btn), dialog_on_top: dialogOnTop,
- before, after: title() || null, open: Boolean(document.querySelector('.photo-lightbox')),
- deleted: DELETED.map((d) => d[0]) });
+ cases.push({ case: 'lightbox', open: Boolean(document.querySelector('.photo-lightbox')),
+ button: Boolean(document.querySelector('.photo-lightbox button[aria-label="Delete"]')) });
// -- read-only (Search): never --
await mount({ readOnly: true, isNodeAdmin: true });
@@ -196,7 +179,8 @@ const answer = async (yes) => {
await closeMenus();
ro.click(); await frame();
cases.push({ case: 'read only', labels: roLabels,
- lightbox_button: Boolean(document.querySelector('.photo-lightbox button[aria-label="Delete"]')) });
+ lightbox_button: Boolean(
+ document.querySelector('.photo-lightbox button[aria-label="Delete"]')) });
parent.postMessage({ cases, logs: LOGS, origin: location.origin }, '*');
} catch (err) {
diff --git a/packages/meshbay-hub/tests/test_layout_measured.py b/packages/meshbay-hub/tests/test_layout_measured.py
index 298a754..96f035f 100644
--- a/packages/meshbay-hub/tests/test_layout_measured.py
+++ b/packages/meshbay-hub/tests/test_layout_measured.py
@@ -362,3 +362,54 @@ def test_the_name_leaves_the_trigger_on_mobile_only(full_nav, width):
assert shown == (width > 768), (
f"at {width} px the name in the user menu is "
f"{'shown' if shown else 'hidden'}")
+
+
+# The photo lightbox's bar. With the file name, the zoom label, 100% and a
+# delete button it was 457 px wide, and the close button sat at 397..437 px —
+# off a 360 px phone, with no other way out of the lightbox on screen. The
+# fixture is `Lightbox()`'s bar, cast button and copy-link included.
+_ZOOM_ICON = '<svg class="icon photo-icon-btn-icon" viewBox="0 0 24 24"></svg>'
+LIGHTBOX_BAR = textwrap.dedent(f"""
+ <div class="video-overlay photo-lightbox">
+ <div class="video-top-bar">
+ <span class="video-title">IMG_20150510_205012.JPG</span>
+ <div class="photo-zoom-controls">
+ <button class="photo-icon-btn">{_ZOOM_ICON}</button>
+ <span class="photo-zoom-percent">Ajusté</span>
+ <button class="photo-icon-btn">{_ZOOM_ICON}</button>
+ <button class="photo-icon-btn active">{_ZOOM_ICON}</button>
+ <button class="photo-icon-btn photo-icon-btn-text">100%</button>
+ </div>
+ <button class="video-close">{_ICON}</button>
+ <div class="cast-wrapper"><button class="video-close cast-btn">{_ICON}</button></div>
+ <button class="video-close">{_ICON}</button>
+ <button class="video-close lightbox-close">{_ICON}</button>
+ </div>
+ </div>
+""")
+LIGHTBOX_WIDTHS = [320, 360, 412, 768, 1024]
+
+
+@pytest.fixture(scope="module")
+def lightbox_bar(tmp_path_factory):
+ fragment = tmp_path_factory.mktemp("lightbox") / "fragment.html"
+ fragment.write_text(LIGHTBOX_BAR, encoding="utf-8")
+ proc = subprocess.run(
+ ["python3", str(HARNESS), ",".join(str(w) for w in LIGHTBOX_WIDTHS),
+ str(fragment), ".lightbox-close", ".video-title"],
+ capture_output=True, text=True, timeout=180)
+ assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}"
+ out = json.loads(proc.stdout)
+ assert "error" not in out, f"no measurement: {out}"
+ return out
+
+
+@pytest.mark.parametrize("width", LIGHTBOX_WIDTHS)
+def test_the_lightbox_close_button_is_on_screen(lightbox_bar, width):
+ close = lightbox_bar[str(width)]["boxes"][".lightbox-close"]
+ assert close["right"] <= width, (
+ f"at {width} px the lightbox's close button ends at {close['right']} px")
+
+
+def test_the_file_name_stays_in_the_desktop_lightbox(lightbox_bar):
+ assert lightbox_bar["1024"]["boxes"][".video-title"]["width"] > 200
diff --git a/packages/meshbay-hub/tests/test_photo_delete.py b/packages/meshbay-hub/tests/test_photo_delete.py
index bcc943f..bd22cbe 100644
--- a/packages/meshbay-hub/tests/test_photo_delete.py
+++ b/packages/meshbay-hub/tests/test_photo_delete.py
@@ -1,10 +1,11 @@
"""
Deleting a photo from the Photos app (harness/photo_delete_probe.py).
-On a right-clicked tile and in the lightbox's bar, which is how a phone gets
-at it. Offered on the rule Files applies and the node enforces: the node's
-operator, or whoever uploaded the photo. Never without the page's own
-confirmation, and never from Search, which is read-only.
+On a tile's menu — right-click, or press and hold on a phone — and not in the
+lightbox's bar, where it pushed the close button off a phone's screen.
+Offered on the rule Files applies and the node enforces: the node's operator,
+or whoever uploaded the photo. Never without the page's own confirmation, and
+never from Search, which is read-only.
"""
import json
@@ -48,11 +49,9 @@ def test_the_operator_may_delete_any_photo(cases):
assert cases["operator"]["delete_offered"] == [True, True, True]
-def test_the_lightbox_deletes_and_shows_the_next_photo(cases):
+def test_the_lightbox_offers_no_delete(cases):
c = cases["lightbox"]
- assert c["button"] and c["dialog_on_top"]
- assert c["deleted"] == ["p1"]
- assert c["open"] and c["before"] == "beach.jpg" and c["after"] == "dunes.jpg"
+ assert c["open"] and not c["button"]
def test_search_results_offer_no_delete(cases):