diff options
| author | Adam Soltys <[email protected]> | 2026-09-12 07:17:58 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-12 07:17:58 -0700 |
| commit | d4576229565d9b4411d0d9fc44780da03ef0b47b (patch) | |
| tree | 3773d0bc0c8e42c0fc71b8ec07c66ab5152f7ca1 /src/app.js | |
| parent | c3befb79b6393e2cf6c3744484699707817fea28 (diff) | |
| download | coinosv3-d4576229565d9b4411d0d9fc44780da03ef0b47b.tar.gz coinosv3-d4576229565d9b4411d0d9fc44780da03ef0b47b.zip | |
A picture in a post opens full screen, and the paperclip is a button again.
The attach control was a .btn-sm holding a 16px icon, wedged between two
full-height buttons — a sliver to aim at on a phone. It's a 48px square the
height of the row now, with a 20px clip in it.
And tapping an image opens it full screen: the image centred on a dark
ground, tap anywhere (or Escape) to close. Posts crop images to 320px so a
feed stays a feed, which makes a photograph a thumbnail — this is where you
actually look at one.
Two details in the viewer. The page underneath stops scrolling while it's
open, and the scrollbar gutter the page keeps reserved stands down with it:
fixed positioning is sized to the viewport WITHOUT that gutter, so an
"inset: 0" backdrop leaves a pale stripe down the side of an otherwise
full-screen picture.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 20 |
1 files changed, 19 insertions, 1 deletions
@@ -458,6 +458,22 @@ let _bootDeciding = true; // the welcome screen held back forever. Nothing legitimate is still deciding // four seconds in — the store wait alone caps at 800ms. setTimeout(() => { if (_bootDeciding) { _bootDeciding = false; render(); } }, 4000); +// A picture, full screen. Posts show images cropped to a few hundred pixels +// so a feed stays a feed; this is where you actually look at one. Tap +// anywhere (or Escape, or Back) to close — no chrome, nothing to aim at. +function imageViewer() { + if (!ui.lightbox) return null; + const close = () => { ui.lightbox = null; render(); }; + return h('div', { + class: 'lightbox', onClick: close, + role: 'dialog', 'aria-modal': 'true', + }, h('img', { src: ui.lightbox, alt: '', onClick: (e) => e.stopPropagation() }), + h('button', { class: 'lightbox-x', 'aria-label': t('close'), onClick: close }, '\u00d7')); +} +if (typeof window !== 'undefined') { + window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && ui.lightbox) { ui.lightbox = null; render(); } }); +} + function render() { if (_publicChatMode) return; // A background render mid carousel-drag rebuilds the strip; the morph then @@ -567,7 +583,8 @@ function renderInner() { ui.navAnimSkip = false; } applyAnim(screen, 'anim-page', (performance.now() - _navAt) < 340 ? performance.now() - _navAt : -1); - morphChildren(root, [screen, footer()]); + morphChildren(root, [screen, footer(), ...(ui.lightbox ? [imageViewer()] : [])]); + try { document.documentElement.classList.toggle('no-scroll', !!ui.lightbox); } catch {} if (fpath) { const el = nodeAtPath(fpath); if (el && el !== a && /^(INPUT|SELECT|TEXTAREA)$/.test(el.tagName)) { @@ -5027,6 +5044,7 @@ const ctx = { // in fiat the typed figure is money, so it needs today's price to become sats parseAmount: (v, u) => parseAmount(v, u, rateNow()), worthLine, + openImage: (src) => { ui.lightbox = src; render(); }, // "The next balance isn't a change, it's the first real value" — state // arriving late (the ark store opening past boot's patience) must not read // as money landing: without this the balance counts up from 0 in green, |
