[object Object]

← back to CelebritySignatures

Minimal cards (signature + name only) + artist details popup: badges/museums/license/links, signature-evolution strip, and the artist's WORKS live-loaded from the Met Open Access API; URL-addressable ?artist=QID deep links (TK-10181)

faaabccc8772a5f2b06a1cfa76a8c99f10bd3efb · 2026-08-03 11:12:44 -0700 · Steve Abrams

Files touched

Diff

commit faaabccc8772a5f2b06a1cfa76a8c99f10bd3efb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 11:12:44 2026 -0700

    Minimal cards (signature + name only) + artist details popup: badges/museums/license/links, signature-evolution strip, and the artist's WORKS live-loaded from the Met Open Access API; URL-addressable ?artist=QID deep links (TK-10181)
---
 public/index.html | 127 +++++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 102 insertions(+), 25 deletions(-)

diff --git a/public/index.html b/public/index.html
index 20c0cea..290dd0d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -45,15 +45,32 @@
   .collage .ch a { margin-left:auto; font-size:13px; color:#1d7a36; text-decoration:none; }
   .collage img { width:100%; display:block; }
   .collage .note { padding:24px 18px; color:var(--muted); font-size:14px; }
-  /* signature-evolution: inline expand-in-flow strip (never a modal) */
-  .evo { margin-top:2px; }
-  .evo summary { cursor:pointer; font-size:11.5px; color:#1d7a36; list-style:none; }
-  .evo summary::-webkit-details-marker { display:none; }
-  .evo[open] summary { color:var(--muted); }
-  .evo-strip { display:flex; flex-wrap:wrap; gap:8px; padding:8px 0 2px; }
+  /* signature-evolution strip (rendered inside the details popup) */
+  .evo-strip { display:flex; flex-wrap:wrap; gap:10px; padding:6px 0 2px; }
   .evo-cell { text-align:center; }
-  .evo-cell img { max-height:34px; max-width:110px; object-fit:contain; background:#fff; border:1px solid var(--line); border-radius:6px; padding:3px; }
+  .evo-cell img { max-height:44px; max-width:140px; object-fit:contain; background:#fff; border:1px solid var(--line); border-radius:6px; padding:3px; }
   .evo-cell .y { font-size:10px; color:var(--muted); }
+  /* minimal card: details chip */
+  .info-chip { display:inline-block; margin-top:2px; font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:2px 9px; cursor:pointer; text-decoration:none; }
+  .info-chip:hover { background:var(--ink); color:#fff; border-color:var(--ink); }
+  /* artist details popup */
+  #artistPop { position:fixed; inset:0; z-index:50; display:flex; align-items:center; justify-content:center; }
+  #artistPop[hidden] { display:none; }
+  .ap-back { position:absolute; inset:0; background:rgba(20,18,14,.55); }
+  .ap-card { position:relative; background:#fff; border-radius:16px; max-width:820px; width:min(94vw,820px); max-height:88vh; overflow:auto; padding:26px 28px; box-shadow:0 24px 80px rgba(0,0,0,.35); }
+  .ap-x { position:absolute; top:12px; right:14px; border:none; background:none; font-size:26px; line-height:1; cursor:pointer; color:var(--muted); }
+  .ap-x:hover { color:var(--ink); }
+  .ap-sig { text-align:center; padding:8px 0 2px; }
+  .ap-sig img { max-height:90px; max-width:80%; object-fit:contain; }
+  .ap-name { font-size:22px; font-weight:650; margin:6px 0 2px; }
+  .ap-life { color:var(--muted); font-size:13px; margin-bottom:10px; }
+  .ap-rowline { display:flex; flex-wrap:wrap; gap:6px; margin:8px 0; }
+  .ap-sec { font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:16px 0 6px; }
+  .ap-sub { font-size:13px; color:var(--muted); }
+  .works { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; }
+  .works a { display:block; border:1px solid var(--line); border-radius:10px; overflow:hidden; background:#faf9f6; }
+  .works img { width:100%; height:150px; object-fit:cover; display:block; }
+  .works .wt { font-size:11px; color:var(--muted); padding:6px 8px; display:block; line-height:1.3; }
 </style>
 </head>
 <body>
@@ -91,6 +108,10 @@
   <div id="gridView"><div class="grid" id="grid"></div><div class="empty" id="empty" hidden>No signatures match these filters.</div></div>
   <div id="collagesView" class="collages" hidden></div>
 </main>
+<div id="artistPop" hidden>
+  <div class="ap-back"></div>
+  <div class="ap-card"><button class="ap-x" aria-label="Close">×</button><div id="apBody"></div></div>
+</div>
 
 <script>
 const LS = 'celebsig.prefs';
@@ -128,19 +149,17 @@ function render() {
 
   $('#count').textContent = `· ${rows.length} of ${DATA.length}`;
   $('#empty').hidden = rows.length>0;
+  // Minimal card: signature + name only. Everything else lives in the
+  // details popup (chip), which also loads the artist's works from the Met.
+  BY_QID = {}; rows.forEach(r => { BY_QID[(r.wikidata||'').split('/').pop()] = r; });
   $('#grid').innerHTML = rows.map(r => {
-    const ub = r.usable_in_commercial_collage;
-    const uClass = ub==='yes'?'use-yes':ub==='permission-needed'?'use-perm':'use-review';
-    const uText = ub==='yes'?'usable':ub==='permission-needed'?'permission':'review';
+    const qid = (r.wikidata||'').split('/').pop();
+    const evoN = (EVO[qid]?.sigs?.length) || 0;
     return `<div class="card">
       <div class="sig"><img loading="lazy" src="${r.signature_image_url}" alt="Signature of ${r.full_name}" onerror="this.parentNode.innerHTML='<span style=color:#bbb;font-size:12px>image unavailable</span>'"></div>
       <div class="meta">
         <div class="name">${r.full_name}</div>
-        <div class="row"><span class="tag">${drill('cat',r.category)}</span><span class="badge b-${r.risk_level}">${r.risk_level}</span><span class="badge ${uClass}">${drill('use',ub,uText)}</span></div>
-        <div class="sub">${r.deceased==='yes'?'† ':'● living · '}${(r.image_license||'').slice(0,28)}</div>
-        ${Array.isArray(r.museums)&&r.museums.length?`<div class="sub" title="${esc(r.museums.join(' · '))}">🏛 ${esc(r.museums.slice(0,2).join(' · '))}${r.museums.length>2?` +${r.museums.length-2}`:''}</div>`:''}
-        ${evoStrip(r)}
-        <div class="sub"><a href="${r.wikidata}" target="_blank" rel="noopener noreferrer">Wikidata</a> · <a href="${r.backup_source}" target="_blank" rel="noopener noreferrer">source</a></div>
+        <a class="info-chip" data-qid="${qid}" href="?artist=${qid}">ⓘ details${evoN>1?` · ✍ ${evoN}`:''}</a>
       </div></div>`;
   }).join('');
 }
@@ -155,6 +174,7 @@ $('#density').oninput = (e) => { prefs.density=+e.target.value; save(); render()
 $('#useFilter').onchange = (e) => { prefs.use=e.target.value; save(); writeURL(true); render(); };
 // a card data point (category / usability) is an href-drill atom → filter in place + reflect into the URL
 document.addEventListener('click', (e) => { const d=e.target.closest('a.drill[data-filter]'); if(!d) return; if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1) return; e.preventDefault(); e.stopPropagation();
+  closeArtist(false); // a drill inside the popup filters the grid behind it
   const dim=d.dataset.filter, v=d.dataset.v, dflt=dim==='cat'?'All':'all';
   prefs[dim] = (prefs[dim]===v?dflt:v); save(); writeURL(true);
   document.querySelectorAll('.chip').forEach(x=>x.classList.toggle('on', x.dataset.c===prefs.cat)); if($('#useFilter')) $('#useFilter').value=prefs.use; render(); });
@@ -191,20 +211,77 @@ function setView(v) {
 }
 document.querySelector('.tabs').onclick = (e) => { const v=e.target.dataset.v; if(v) setView(v); };
 
-// signature evolution: people with multiple signature files over their life.
-// Rendered as an inline <details> strip on the card (expand-in-flow, no modal).
-let EVO = {};
-function evoStrip(r){
-  const qid=(r.wikidata||'').split('/').pop(), e=EVO[qid];
+// ── artist details popup: all metadata + signature evolution + works from the
+// Met Open Access API (free, PD images, CORS-open) loaded on demand ──
+let EVO = {}, BY_QID = {}, WORKS_CACHE = {};
+function evoStripHTML(qid){
+  const e=EVO[qid];
   if(!e||!e.sigs||e.sigs.length<2) return '';
-  const dated=e.sigs.filter(s=>s.year);
-  const label=`✍ ${e.sigs.length} signatures over time${dated.length>=2?` (${dated[0].year}–${dated[dated.length-1].year})`:''}`;
-  return `<details class="evo"><summary>${label}</summary><div class="evo-strip">${
+  return `<div class="ap-sec">✍ Signature over time (${e.sigs.length})</div><div class="evo-strip">${
     e.sigs.map(s=>`<div class="evo-cell"><img loading="lazy" src="${esc(s.url)}" alt="Signature of ${esc(e.name)}${s.year?' ('+s.year+')':''}" onerror="this.closest('.evo-cell').remove()"><div class="y">${s.year||'undated'}</div></div>`).join('')
-  }</div></details>`;
+  }</div>`;
 }
-fetch('/api/signatures').then(r=>r.json()).then(d => { DATA=d; readURL(); buildChips(); render(); setView(prefs.view); });
+async function loadWorks(qid, name){
+  if (WORKS_CACHE[qid]) return WORKS_CACHE[qid];
+  const MET='https://collectionapi.metmuseum.org/public/collection/v1';
+  try {
+    const s = await (await fetch(`${MET}/search?hasImages=true&artistOrCulture=true&q=${encodeURIComponent(name)}`)).json();
+    const ids = (s.objectIDs||[]).slice(0,14);
+    const nl = name.toLowerCase();
+    const works = [];
+    for (const id of ids) {
+      if (works.length >= 6) break;
+      try {
+        const o = await (await fetch(`${MET}/objects/${id}`)).json();
+        if (!o.primaryImageSmall) continue;
+        if (!(o.artistDisplayName||'').toLowerCase().includes(nl.split(' ').pop())) continue;
+        works.push({ img:o.primaryImageSmall, title:o.title||'Untitled', date:o.objectDate||'', url:o.objectURL||('https://www.metmuseum.org/art/collection/search/'+id) });
+      } catch {}
+    }
+    WORKS_CACHE[qid]=works; return works;
+  } catch { return []; }
+}
+function openArtist(qid, push=true){
+  const r = BY_QID[qid] || DATA.find(x=>(x.wikidata||'').endsWith('/'+qid));
+  if (!r) return;
+  const ub=r.usable_in_commercial_collage;
+  const uClass=ub==='yes'?'use-yes':ub==='permission-needed'?'use-perm':'use-review';
+  $('#apBody').innerHTML = `
+    <div class="ap-sig"><img src="${r.signature_image_url}" alt="Signature of ${esc(r.full_name)}"></div>
+    <div class="ap-name">${esc(r.full_name)}</div>
+    <div class="ap-life">${r.deceased==='yes'?'† '+esc(r.death_date||''):'● living'} · rank #${r.rank} · ${esc((r.reason_for_ranking||'').replace('Cross-wiki notability: ',''))}</div>
+    <div class="ap-rowline"><span class="tag">${drill('cat',r.category)}</span><span class="badge b-${r.risk_level}">${r.risk_level}</span><span class="badge ${uClass}">${drill('use',ub,ub==='yes'?'usable':ub)}</span></div>
+    ${Array.isArray(r.museums)&&r.museums.length?`<div class="ap-sec">🏛 In the collections of</div><div class="ap-sub">${esc(r.museums.join(' · '))}</div>`:''}
+    <div class="ap-sec">License & sources</div>
+    <div class="ap-sub">${esc(r.image_license||'')} · <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">Wikidata</a> · <a href="${esc(r.backup_source)}" target="_blank" rel="noopener noreferrer">signature source</a></div>
+    ${evoStripHTML(qid)}
+    <div class="ap-sec">Works — The Met Open Access</div>
+    <div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`;
+  $('#artistPop').hidden = false;
+  document.body.style.overflow = 'hidden';
+  if (push) { const u=new URLSearchParams(location.search); u.set('artist',qid); history.pushState({},'', '?'+u.toString()); }
+  loadWorks(qid, r.full_name).then(ws=>{
+    const el=$('#apWorks'); if(!el) return;
+    el.innerHTML = ws.length ? ws.map(w=>`<a href="${esc(w.url)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="${esc(w.img)}" alt="${esc(w.title)}"><span class="wt">${esc(w.title)}${w.date?' · '+esc(w.date):''}</span></a>`).join('')
+      : `<span class="ap-sub">no openly-licensed works found in the Met's collection — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`;
+  });
+}
+function closeArtist(push=true){
+  if ($('#artistPop').hidden) return;
+  $('#artistPop').hidden = true;
+  document.body.style.overflow = '';
+  if (push) { const u=new URLSearchParams(location.search); u.delete('artist'); const q=u.toString(); history.pushState({},'', q?('?'+q):location.pathname); }
+}
+document.addEventListener('click', (e)=>{
+  const c=e.target.closest('a.info-chip'); if(c){ e.preventDefault(); openArtist(c.dataset.qid); return; }
+  if (e.target.closest('.ap-x')||e.target.classList.contains('ap-back')) closeArtist();
+});
+document.addEventListener('keydown', (e)=>{ if(e.key==='Escape') closeArtist(); });
+function maybeOpenFromURL(){ const q=new URLSearchParams(location.search).get('artist'); if(q&&DATA.length) openArtist(q,false); else if(!q) closeArtist(false); }
+
+fetch('/api/signatures').then(r=>r.json()).then(d => { DATA=d; readURL(); buildChips(); render(); setView(prefs.view); maybeOpenFromURL(); });
 fetch('/api/signature-evolution').then(r=>r.ok?r.json():{}).then(e=>{ EVO=e||{}; if(DATA.length) render(); }).catch(()=>{});
+addEventListener('popstate', maybeOpenFromURL);
 </script>
 </body>
 </html>

← cddb938 signature-evolution data live: 162 people with multiple sign  ·  back to CelebritySignatures  ·  Museum expansion wiring: +10 museums in P195 harvest (LACMA, a7c40c4 →