diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-12 12:57:49 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-12 12:57:49 +0200 |
| commit | 13e7980071ab11b07b5d473e43506c6cb4ab637e (patch) | |
| tree | c1a76c5125511c810f107dc110f09e7f0c5b9cc2 /packages | |
| parent | 3324b4eb701e34f64e859dd90937793ccf7f75c5 (diff) | |
| download | meshbay-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')
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): |