← All Pull Requests

Copy button on the ref/handle strip: copies "#158 hero-final" #5

Closed opened by Ray 2026-09-02

Ray asked for a copy button beside the ref/handle pill, and specifically wondered whether it should copy the whole thing (#158 hero-final) or just one half.

It copies both. Reasoning:

  • The ref is globally unambiguous; the handle is what a human actually says out loud ("hero-final is the one").
  • Pasting both lets the AI resolve on either token, and survives a handle being reused on a later shot.
  • Untagged shots copy just #158, since there is nothing else to give.

What it looks like

#1  [hero-final]  [copy]

Fades in on card hover and on :focus-visible, matching the existing .card-fill-btn pattern, so it never sits on top of the thumbnail. On touch it stays at 0.8 opacity (same media query the fill button uses). Shows copied for 1.2s on success.

Implementation notes

  • Follows the house clipboard pattern at index.html:1113 (navigator.clipboard?.writeText + a text swap for feedback).
  • Adds a legacyCopy fallback, because this viewer also runs in a sandboxed readme frame where the async clipboard API is unavailable and the existing call would silently no-op. execCommand is deprecated but is the only thing that works there.
  • ev.stopPropagation() so the click never opens the shot.

Verified

  • node --check on the main script block: OK.
  • cargo build --release: clean, 7 warnings, identical to the pre-change baseline.
  • Rendered in a real headless browser against a scratch server with a tagged shot; screenshot shows #1 hero-final copy laid out correctly.

Not merging this myself, adom-shotlog is yours.

Diff Skip to comments (1)

--- a/src/html/index.html+++ b/src/html/index.html@@ -1,2083 +1,2128 @@⋯ 256 unchanged lines ⋯ .card-refbar { display: flex; align-items: center; gap: 8px; padding: 3px 10px 2px; min-height: 16px;   font: 600 10.5px/1.3 'JetBrains Mono', monospace; color: var(--text3); } .card-refbar .rb-ref { letter-spacing: .2px; }+.card-refbar .rb-copy { margin-left: 4px; padding: 0 5px; border-radius: 5px; cursor: pointer;+  font: inherit; line-height: 1.5; color: var(--text3); background: transparent;+  border: 1px solid var(--border-muted); opacity: 0; transition: opacity .12s, color .12s, border-color .12s; }+.shot-card:hover .card-refbar .rb-copy, .card-refbar .rb-copy:focus-visible { opacity: 1; }+.card-refbar .rb-copy:hover { color: var(--accent-bright); border-color: rgba(0,184,176,.5); }+.card-refbar .rb-copy.done { opacity: 1; color: var(--accent-bright); border-color: rgba(0,184,176,.5); }+@media (hover: none) { .card-refbar .rb-copy { opacity: .8; } } .card-refbar .rb-tag { color: var(--accent-bright); background: rgba(0,184,176,.12); border: 1px solid rgba(0,184,176,.45);   border-radius: 5px; padding: 0 6px; max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .shot-card:not(.has-refbar) .card-refbar { }⋯ 28 unchanged lines ⋯    The thumbnail IMAGE also fills the window in one click (see the click    handler on img, "ONE click on a thumbnail goes STRAIGHT to fill-window"),    so this button is the discoverable, keyboard-focusable route to the same-   thing rather than a separate shortcut past the modal. (Ray, PR #1) */+   thing rather than a separate shortcut past the modal. */ .card-img-wrap { position: relative; } .card-fill-btn { position: absolute; top: 10px; right: 10px; width: 32px; height: 32px;   display: inline-flex; align-items: center; justify-content: center;⋯ 626 unchanged lines ⋯     const tip = $('global-tooltip'); let cx=0, cy=0, timer=null;     document.addEventListener('mousemove', e => { cx=e.clientX; cy=e.clientY; }, true);     function show(el) {-      // A card whose info overlay is REVEALED (compact mode ⓘ) is being read:-      // no tooltip may float over it (Ray: "the tooltip obscures the popover").+      // Nothing incidental may cover something the human deliberately opened.+      // This started as a card-scoped check for the compact-mode ⓘ overlay+      // (Ray: "the tooltip obscures the popover"), but the same complaint came+      // back with a DIFFERENT surface open: the guard only fired when the+      // hovered element sat inside a .shot-card whose OWN overlay was showing,+      // so any other open popover still got covered. Suppress over any of them.       if (el.closest && el.closest('.shot-card')?.querySelector('.card-info.show')) return;-      // ...and nothing incidental may cover ANY surface the human deliberately-      // opened: another card's overlay, or the brand hover card (Ray, PR #4).       if (document.querySelector('.hover-popover.show, .card-info.show')) return;       // Rich (authored, trusted) HTML via el._ttHtml; else plain text. Dynamic       // values interpolated into _ttHtml are escaped by the builders below.⋯ 31 unchanged lines ⋯   })();    // ── Helpers ──────────────────────────────────────────────────────────────+  // execCommand('copy') is deprecated but is the only thing that works in a+  // sandboxed frame with no clipboard permission, which is where this viewer+  // sometimes lives. Used only when navigator.clipboard is missing or rejects.+  function legacyCopy(text, done) {+    try {+      const ta = document.createElement('textarea');+      ta.value = text; ta.setAttribute('readonly', '');+      ta.style.cssText = 'position:fixed;top:-1000px;opacity:0';+      document.body.appendChild(ta); ta.select();+      const ok = document.execCommand('copy');+      document.body.removeChild(ta);+      if (ok && done) done();+    } catch (e) { /* nothing else to try; leave the label alone */ }+  }   let toastTimer=null;   function showToast(msg, ms){ toast.textContent=msg; toast.classList.add('show');     toast.classList.toggle('lifted', !!document.querySelector('.cdown')); // never under the countdown card⋯ 240 unchanged lines ⋯       if (e.tag){ const tg=document.createElement('span'); tg.className='rb-tag'; tg.textContent=e.tag;         tg.setAttribute('data-tooltip','Handle "'+e.tag+'" given by the AI that injected this shot (--tag). Say "'+e.tag+'" to mean exactly this shot.');         rb.appendChild(tg); }+      // Copy the WHOLE reference, "#158 hero-final", not just one half. The ref+      // is globally unambiguous and the handle is what a human actually says, so+      // pasting both lets the AI resolve it either way and survives a handle+      // being reused on a later shot. Untagged shots copy just "#158".+      { const txt = '#' + e.id + (e.tag ? ' ' + e.tag : '');+        const cp = document.createElement('button'); cp.className = 'rb-copy';+        cp.textContent = 'copy'; cp.setAttribute('aria-label', 'Copy ' + txt);+        cp.setAttribute('data-tooltip', 'Copy "' + txt + '" — paste it to your AI to mean exactly this shot.');+        cp.addEventListener('click', function (ev) {+          ev.stopPropagation();          // never let it open the shot+          const done = function () {+            cp.textContent = 'copied'; cp.classList.add('done');+            setTimeout(function () { cp.textContent = 'copy'; cp.classList.remove('done'); }, 1200);+          };+          // The viewer also runs in a sandboxed readme frame where the async+          // clipboard API is unavailable; fall back rather than silently no-op.+          if (navigator.clipboard && navigator.clipboard.writeText) {+            navigator.clipboard.writeText(txt).then(done, function () { legacyCopy(txt, done); });+          } else { legacyCopy(txt, done); }+        });+        rb.appendChild(cp); }       c.appendChild(rb); }   // info (hidden in compact) -> refbar -> imgWrap (appended below)     // COMPACT mode's reveal chip: hover (or tap, on touch) the ⓘ on the image     // to overlay the full info block; it hides again when the pointer leaves⋯ 863 unchanged lines ⋯ </script> </body> </html>+

Comments

John Lauer · 1mo ago

Applied in 0.28.4 (adom-wiki pkg update adom/adom-shotlog), essentially verbatim: the hover-revealed copy button, the whole-reference copy ("#158 hero-final", just "#158" when untagged), the legacyCopy fallback for sandboxed frames, and the 1.2s "copied" swap. Two trims: the tooltip's em dash became a colon (house style), and the comment edits elsewhere in the diff were skipped since they only reverted attribution lines.

Verified in pup on a tagged shot: hidden at rest, click shows "copied" and resets, the click never opens the shot, aria-label "Copy #3 heroH". Closing rather than merging (older whole-file base); the code is in and credited. Thanks, this is a good one.

Log in to comment.