← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.25.2"+version = "0.26.0" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.25.2"+version = "0.26.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1
@@ -1 +1 @@-0.25.2+0.26.0
bin/adom-shotlog
⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+14
@@ -391,3 +391,17 @@ the remembered last value (lastKeep/lastPrune in JS). max_shots=0 now means UNLIMITED - the janitor guards `cap > 0` at BOTH enforcement points (recovery trim + add_entry trim) and the prefs handler accepts 0 or 10-5000. retention=0 already meant "never". The .pref-control.off class greys + disables the field.++## Compact mode (0.26.0, John's daily-driver request) - image-first cards+Pref `compact` (default TRUE - the new default). The card's text (why/desc ++meta chip row) is wrapped in ONE .card-info container; body.compact hides it+and a ⓘ chip (top-left on the image, mirroring the ⛶ top-right) reveals it as+an absolute overlay (.card-info.show) on hover (mouseenter) or tap (click+toggles - the touch fallback); card mouseleave hides. Images render at NATURAL+aspect in compact (height:auto, max-height 420px - no fixed-height letterbox),+which is what gets ~90% of a skinny webview showing thumbnails (measured 89%).+The ⛶ fill button is ALWAYS visible in compact (opacity .85), not hover-only -+it is John's most-used control. <body> ships class="compact" so the default+renders without a flash; boot + renderPrefs sync it from the pref. Classic+mode (toggle off) is byte-identical to the old layout - .card-info is display+-block static, .card-info-hot display:none.
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.25.2",+  "version": "0.26.0",   "type": "app",   "title": "Shotlog - Screenshot Log",   "description": "See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Bridge child-window arrays via --meta.",
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.25.2",+  "version": "0.26.0",   "type": "app",   "title": "Shotlog - Screenshot Log",   "description": "See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Bridge child-window arrays via --meta.",
src/html/index.html+69−5
@@ -180,6 +180,37 @@ header {   overflow: hidden; transition: border-color .15s; cursor: zoom-in;   display: flex; flex-direction: column; flex-shrink: 0; } /* never compress in the flex-column feed */ .shot-card:hover, .shot-card:active { border-color: rgba(0,184,176,.5); }+/* The ⓘ reveal chip on the image: only exists in compact mode */+.card-info-hot { display: none; }++/* ── COMPACT MODE (the default): images ARE the gallery ─────────────────────+   The .card-info block (headline/desc + meta chips) hides; hovering/tapping+   the ⓘ chip on the image overlays it. Images render at natural aspect (no+   letterbox) so ~90% of a skinny webview is thumbnails. */+body.compact .feed { gap: 8px; padding: 8px; }+body.compact .shot-card { position: relative; }+body.compact .card-info { display: none; }+body.compact .card-info.show { display: block; position: absolute; top: 0; left: 0; right: 0;+  z-index: 6; background: rgba(13,17,23,.95); backdrop-filter: blur(3px);+  border-bottom: 1px solid var(--border); border-radius: 10px 10px 0 0;+  padding-bottom: 9px; max-height: 82%; overflow-y: auto; }+/* natural aspect: the browser sizes the box from width x aspect (no fixed+   height = no letterbox bars), capped so one giant portrait can't eat the feed */+body.compact .shot-card-img { height: auto; max-height: 420px; object-fit: contain; }+/* his most-used control stays visible, not hover-revealed */+body.compact .card-fill-btn { opacity: .85; }+/* the ⓘ reveal chip, top-left over the image (mirrors the ⛶ top-right) */+body.compact .card-info-hot { display: inline-flex; position: absolute; top: 10px; left: 10px;+  width: 32px; height: 32px; align-items: center; justify-content: center;+  background: rgba(13,17,23,.72); color: var(--text); border: 1px solid rgba(255,255,255,.18);+  border-radius: 8px; cursor: help; backdrop-filter: blur(3px);+  font: 600 15px/1 'JetBrains Mono', monospace; padding: 0;+  opacity: 0; transition: opacity .15s, background .15s, border-color .15s; }+body.compact .shot-card:hover .card-info-hot, body.compact .card-info-hot:focus-visible { opacity: 1; }+body.compact .card-info-hot:hover { background: rgba(0,184,176,.22); border-color: rgba(0,184,176,.6); color: var(--accent-bright); }+@media (hover: none) { body.compact .card-info-hot { opacity: .9; } }+/* child-window strip stays (it's image content); tighten its caption */+body.compact .card-popups .ph { padding-top: 4px; } /* WHY-headline for auto-captures: the prompt IS the title (same gutter as a    description), the filename demoted to a small mono sub-line under it. */ .shot-card-why { padding: 10px 13px 0; font: 500 13.5px/1.45 'Satoshi', sans-serif; color: var(--text); overflow-wrap: anywhere; }@@ -483,7 +514,7 @@ header {   font-size: 10px; color: var(--text3); margin-top: 8px; word-break: break-all; } </style> </head>-<body tabindex="0">+<body tabindex="0" class="compact">  <div id="global-tooltip"></div> @@ -569,6 +600,14 @@ header {       </div>       <button class="toggle" id="prefAutoswitch" role="switch" aria-checked="true" data-tooltip="Auto-switch the in-app channel tab when a new shot arrives for it."></button>     </div>+    <div class="pref-row">+      <div class="pref-info">+        <div class="pref-name">Compact cards (image-first)</div>+        <div class="pref-desc">The gallery shows mostly image thumbnails: the info text (headline, time, sizes, provenance chips) hides behind the ⓘ chip on each image - hover it to reveal. The ⛶ fullscreen button stays on every thumbnail. Turn off for the classic text-above-image cards.</div>+        <div class="ai-chip"><span class="lbl">or say to your AI</span>turn off shotlog compact mode</div>+      </div>+      <button class="toggle" id="prefCompact" role="switch" aria-checked="true" data-tooltip="On (default): image-first cards, info on hover. Off: classic cards with the text always visible."></button>+    </div>     <div class="pref-row">       <div class="pref-info">         <div class="pref-name">Default canvas</div>@@ -868,6 +907,11 @@ header {    function card(e) {     const c=document.createElement('div'); c.className='shot-card'; c.dataset.id=e.id;+    // ALL the info text (headline/desc + the meta chip row) lives in ONE+    // .card-info container: normal mode renders it above the image as always;+    // COMPACT mode hides it and reveals it as a hover overlay (the ⓘ chip on+    // the image), so the gallery is ~90% thumbnails.+    const info=document.createElement('div'); info.className='card-info';     // For auto-captures the WHY is the headline: show the prompt the thread     // was serving, and demote the generic "auto-captured: file.png" to a     // small mono sub-line. Deliberate injects keep their description first.@@ -876,15 +920,15 @@ header {       const why=document.createElement('div'); why.className='shot-card-why';       why.textContent=e.context;       why.setAttribute('data-tooltip','The prompt this AI thread was working on when it read the image - the REASON this capture exists.');-      c.appendChild(why);+      info.appendChild(why);       const sub=document.createElement('div'); sub.className='shot-card-sub';       sub.textContent='auto-captured: '+(e.filename||'').replace(/^auto-captured-screenshot-claude-read-for-analysis-/,'');-      c.appendChild(sub);+      info.appendChild(sub);     } else {       const desc=document.createElement('div');       if (e.description) { desc.className='shot-card-desc'; desc.textContent=e.description; }       else { desc.className='shot-card-desc no-desc'; desc.textContent='⚠ No description — inject with -d "what and why"'; }-      c.appendChild(desc);+      info.appendChild(desc);     }     const meta=document.createElement('div'); meta.className='shot-card-meta';     const ts=document.createElement('span'); ts.className='ts'; ts.textContent=fmtDate(e.timestamp);@@ -915,11 +959,22 @@ header {       chip=document.createElement('span'); chip.className='badge-nerd'; chip.textContent='ⓘ meta';       chip.setAttribute('data-tooltip','Tap to reveal EVERYTHING shotlog knows about this shot: the full entry record (thread, context, source, sizes), all capture metadata (coordMap, DPI, screen rect, child windows), and the raw JSON.');       meta.appendChild(chip); }-    c.appendChild(meta);+    info.appendChild(meta);+    c.appendChild(info);     const imgWrap=document.createElement('div'); imgWrap.className='card-img-wrap';     const img=document.createElement('img'); img.className='shot-card-img'; img.src=imgUrl(e); img.alt=e.description||e.filename;     img.addEventListener('click', ()=>openModal(e));   // 1st click still opens the modal (unchanged)     imgWrap.appendChild(img);+    // 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+    // the card. Hidden entirely in normal mode (the info is already visible).+    const infoHot=document.createElement('button'); infoHot.className='card-info-hot'; infoHot.textContent='ⓘ';+    infoHot.setAttribute('aria-label','Show shot info');+    infoHot.setAttribute('data-tooltip','Hover to reveal this shot\'s info: the headline, time, sizes, and provenance chips. (Compact mode hides them so images get the room.)');+    infoHot.addEventListener('mouseenter', ()=>info.classList.add('show'));+    infoHot.addEventListener('click', ev=>{ ev.stopPropagation(); info.classList.toggle('show'); });+    c.addEventListener('mouseleave', ()=>info.classList.remove('show'));+    imgWrap.appendChild(infoHot);     // Additional path: jump STRAIGHT to fullscreen, no modal first. The click     // is the user gesture the Fullscreen API needs.     const fillBtn=document.createElement('button'); fillBtn.className='card-fill-btn'; fillBtn.setAttribute('aria-label','Fullscreen this shot');@@ -1223,6 +1278,9 @@ header {   // Boot: seed tabs from the broker's channel overview, then activate.   (async function bootApp(){     try{ APREFS=await fetch('api/prefs').then(r=>r.json()); }catch{}+    // Apply the display mode at boot (the <body> ships class="compact" so the+    // default renders without a flash; this corrects it if the pref is off).+    document.body.classList.toggle('compact', APREFS.compact!==false);     try{       const rows=await fetch('api/channels').then(r=>r.json());       if(Array.isArray(rows)) for(const r of rows){@@ -1436,6 +1494,8 @@ header {       $('prefAlerts').setAttribute('aria-checked', String(p.update_alerts!==false));       $('prefAutoswitch').setAttribute('aria-checked', String(p.update_autoswitch!==false));       $('prefAutosize').setAttribute('aria-checked', String(p.webview_autosize===true));+      $('prefCompact').setAttribute('aria-checked', String(p.compact!==false));+      document.body.classList.toggle('compact', p.compact!==false);       APREFS=p;       seg.querySelectorAll('button').forEach(b=>b.classList.toggle('on', b.dataset.v===p.default_canvas));       $('prefWidth').value = p.webview_width ?? 20;@@ -1473,6 +1533,10 @@ header {       const now=$('prefAutosize').getAttribute('aria-checked')==='true';       patch({webview_autosize: !now}, !now?'auto-size ON: the webview column snaps to your width on every open.':'auto-size OFF: shotlog leaves the webview column at the width you set.');     });+    $('prefCompact').addEventListener('click',()=>{+      const now=$('prefCompact').getAttribute('aria-checked')==='true';+      patch({compact: !now}, !now?'compact ON: image-first cards - hover the ⓘ on a thumbnail for its info.':'compact OFF: classic cards with the info text always visible.');+    });     seg.addEventListener('click',e=>{       const b=e.target.closest('button[data-v]'); if(!b) return;       patch({default_canvas: b.dataset.v}, 'channels now default to '+b.dataset.v+'.');
src/server.rs+4
@@ -524,6 +524,7 @@ struct PrefsPatch {     webview_width: Option<u8>,     update_autoswitch: Option<bool>,     webview_autosize: Option<bool>,+    compact: Option<bool>, }  /// Patch global preferences from the viewer's Preferences dialog.@@ -566,6 +567,9 @@ async fn set_prefs(     if let Some(a) = patch.webview_autosize {         p.webview_autosize = a;     }+    if let Some(a) = patch.compact {+        p.compact = a;+    }     if let Some(w) = patch.webview_width {         if !(10..=50).contains(&w) {             return Json(serde_json::json!({"ok": false, "error": "webview_width must be 10-50 (percent)"}));
src/store.rs+7−1
@@ -130,6 +130,12 @@ pub struct AppPrefs {     /// open snaps the column back to the pref width.     #[serde(default = "default_false")]     pub webview_autosize: bool,+    /// COMPACT cards (the default): the gallery is image-first - the info text+    /// (headline, description, meta chips) hides behind a hover chip so ~90%+    /// of a skinny webview shows thumbnails. Off = the classic text-above-image+    /// cards.+    #[serde(default = "default_true")]+    pub compact: bool, } fn default_canvas() -> String { "webview".into() } fn default_true() -> bool { true }@@ -141,7 +147,7 @@ impl Default for AppPrefs {     fn default() -> Self { Self { default_canvas: default_canvas(), read_hook: true,         max_shots: default_max_shots(), retention_days: default_retention_days(),         update_alerts: true, webview_width: default_webview_width(),-        update_autoswitch: true, webview_autosize: false } }+        update_autoswitch: true, webview_autosize: false, compact: true } } }  /// The flag file the read-hook script checks; present = hook disabled.