← back to Paul Conrad Cartoons Rebrand

scripts/artifact-template.html

379 lines

<title>Conrad Cartoon Index</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@300;500;700&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;1,6..72,400&display=swap');

  :root {
    --paper: #eceae1;
    --card: #f5f3ec;
    --ink: #17150f;
    --ink-soft: #57534a;
    --line: #d5d1c5;
    --red: #a11e12;
    --gold: #8a6d24;
    --doc: #2f5d43;
    --arc: #7a5a1e;
    color-scheme: light;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --paper: #16140e;
      --card: #201d15;
      --ink: #ece7d8;
      --ink-soft: #a49d8b;
      --line: #332f25;
      --red: #e0694e;
      --gold: #d0b25f;
      --doc: #6fbf8f;
      --arc: #d9b25f;
      color-scheme: dark;
    }
  }
  :root[data-theme="dark"] {
    --paper: #16140e;
    --card: #201d15;
    --ink: #ece7d8;
    --ink-soft: #a49d8b;
    --line: #332f25;
    --red: #e0694e;
    --gold: #d0b25f;
    --doc: #6fbf8f;
    --arc: #d9b25f;
    color-scheme: dark;
  }

  * { box-sizing: border-box; }
  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: "Newsreader", Georgia, serif;
    font-size: 16px;
    line-height: 1.5;
  }
  .wrap { max-width: 1160px; margin: 0 auto; padding-inline: 20px; }

  /* masthead */
  header.masthead {
    border-bottom: 3px solid var(--ink);
    padding-block: 26px 18px;
    text-align: center;
  }
  .kicker {
    font-family: "Oswald", system-ui, sans-serif;
    font-weight: 500;
    letter-spacing: .28em;
    text-transform: uppercase;
    font-size: 11px;
    color: var(--red);
    margin-bottom: 6px;
  }
  header.masthead h1 {
    font-family: "Oswald", system-ui, sans-serif;
    font-weight: 700;
    letter-spacing: .01em;
    font-size: clamp(30px, 6vw, 56px);
    line-height: .98;
    margin: 0;
    text-transform: uppercase;
    text-wrap: balance;
  }
  header.masthead .life { font-weight: 300; color: var(--ink-soft); }
  header.masthead .dek {
    max-width: 60ch;
    margin: 12px auto 0;
    color: var(--ink-soft);
    font-size: 15px;
  }
  .prizes {
    display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
    margin-top: 14px;
  }
  .prize {
    font-family: "Oswald", system-ui, sans-serif;
    font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
    border: 1px solid var(--gold); color: var(--gold);
    padding: 4px 10px; border-radius: 2px;
  }

  /* controls */
  .toolbar {
    position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    display: flex; flex-wrap: wrap; align-items: center; gap: 14px 18px;
    padding-block: 12px;
  }
  .toolbar label {
    font-family: "Oswald", system-ui, sans-serif;
    font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
    color: var(--ink-soft); margin-right: 6px;
  }
  select, input[type="search"] {
    font-family: "Newsreader", Georgia, serif; font-size: 15px;
    background: var(--card); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px; padding: 6px 9px;
  }
  input[type="search"] { min-width: 190px; flex: 1 1 190px; }
  input[type="range"] { accent-color: var(--red); width: 120px; vertical-align: middle; }
  .count { margin-left: auto; font-family: "Oswald", system-ui, sans-serif; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }

  /* facet chips */
  .facets { display: flex; flex-wrap: wrap; gap: 8px; padding-block: 14px 4px; }
  .chip {
    font-family: "Oswald", system-ui, sans-serif;
    font-size: 12px; letter-spacing: .05em;
    background: none; color: var(--ink-soft);
    border: 1px solid var(--line); border-radius: 999px;
    padding: 4px 12px; cursor: pointer;
  }
  .chip[aria-pressed="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); }
  .chip.clear { border-style: dashed; }

  /* grid */
  main { padding-block: 8px 60px; }
  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--col, 260px), 1fr));
    gap: 16px;
    margin-top: 12px;
  }
  .card {
    background: var(--card);
    border: 1px solid var(--line);
    border-left: 3px solid var(--rail, var(--line));
    border-radius: 3px;
    padding: 14px 16px;
    cursor: pointer;
    display: flex; flex-direction: column; gap: 8px;
    text-align: left;
    color: inherit; font: inherit;
  }
  .card:hover { border-color: var(--ink-soft); }
  .card:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
  .card[data-rt="documented"] { --rail: var(--doc); }
  .card[data-rt="collection-summary"] { --rail: var(--arc); }
  .card[data-award="1"] { --rail: var(--red); }
  .card .date {
    font-family: "Oswald", system-ui, sans-serif;
    font-size: 12px; letter-spacing: .06em; color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
  }
  .card .title { font-size: 18px; font-weight: 600; line-height: 1.25; }
  .card .subj { font-size: 14px; color: var(--ink-soft); flex: 1; }
  .card .tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
  .tag {
    font-family: "Oswald", system-ui, sans-serif;
    font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
    padding: 2px 7px; border-radius: 2px; color: var(--paper);
  }
  .tag.documented { background: var(--doc); }
  .tag.collection-summary { background: var(--arc); }
  .tag.award { background: var(--red); }
  .tag.pub { background: none; color: var(--ink-soft); border: 1px solid var(--line); }
  .empty { padding: 50px 0; text-align: center; color: var(--ink-soft); font-style: italic; }

  /* modal */
  .backdrop {
    position: fixed; inset: 0; background: rgba(0,0,0,.55);
    display: none; align-items: center; justify-content: center; padding: 20px; z-index: 40;
  }
  .backdrop.open { display: flex; }
  .modal {
    background: var(--card); color: var(--ink);
    border: 1px solid var(--line); border-top: 4px solid var(--red);
    border-radius: 4px; max-width: 640px; width: 100%;
    max-height: 86vh; overflow-y: auto; padding: 24px 26px; position: relative;
  }
  .modal .x {
    position: absolute; top: 12px; right: 14px; background: none; border: none;
    font-size: 24px; line-height: 1; cursor: pointer; color: var(--ink-soft);
  }
  .modal .m-kicker { font-family: "Oswald", system-ui, sans-serif; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--red); }
  .modal h2 { font-family: "Oswald", system-ui, sans-serif; font-weight: 700; font-size: 26px; margin: 4px 0 2px; text-wrap: balance; }
  .modal .m-date { color: var(--ink-soft); font-size: 14px; margin-bottom: 16px; font-variant-numeric: tabular-nums; }
  .modal dl { display: grid; grid-template-columns: 120px 1fr; gap: 8px 16px; margin: 0; font-size: 15px; }
  .modal dt { font-family: "Oswald", system-ui, sans-serif; font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); padding-top: 2px; }
  .modal dd { margin: 0; }
  .modal a { color: var(--red); }
  .modal .rights { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line); font-size: 13px; font-style: italic; color: var(--ink-soft); }

  footer { border-top: 1px solid var(--line); padding-block: 20px 30px; color: var(--ink-soft); font-size: 13px; }
  footer a { color: var(--red); }

  @media (max-width: 520px) {
    .modal dl { grid-template-columns: 1fr; gap: 2px; }
    .modal dt { margin-top: 8px; }
  }
</style>

<header class="masthead">
  <div class="wrap">
    <div class="kicker">Catalogue of an Editorial Cartoonist</div>
    <h1>Paul Conrad <span class="life">1924&ndash;2010</span></h1>
    <p class="dek" id="dek"></p>
    <div class="prizes" id="prizes"></div>
  </div>
</header>

<div class="wrap">
  <div class="toolbar">
    <span><label for="q">Search</label><input type="search" id="q" placeholder="title, subject, year&hellip;"></span>
    <span><label for="sort">Sort</label>
      <select id="sort">
        <option value="year-asc">Year &uarr;</option>
        <option value="year-desc">Year &darr;</option>
        <option value="title">Title A&ndash;Z</option>
        <option value="award">Prizes first</option>
      </select>
    </span>
    <span><label for="density">Density</label><input type="range" id="density" min="200" max="380" step="20" value="260"></span>
    <span class="count" id="count"></span>
  </div>
  <div class="facets" id="facets"></div>
</div>

<main class="wrap">
  <div class="grid" id="grid"></div>
  <div class="empty" id="empty" hidden>No entries match these filters.</div>
</main>

<footer class="wrap">
  <p><strong>On the images.</strong> No public-domain or open-license Paul Conrad cartoon exists &mdash; every work is copyright the Los Angeles Times or the Conrad estate. This index therefore hosts no cartoon images; each entry links to the holding archive or retrospective where the work can be viewed.</p>
  <p id="sources"></p>
</footer>

<div class="backdrop" id="backdrop"><div class="modal" id="modal" role="dialog" aria-modal="true"></div></div>

<script>
const DATA = /*__DATA__*/{};
const BIO = DATA.bio || {};
const RECORDS = (DATA.records || []).map((r, i) => ({
  id: r.id || ('r'+i),
  title: r.title || 'Untitled',
  date: r.date || r.date_range || '',
  year: (String(r.date || r.date_range || '').match(/\d{4}/) || ['—'])[0],
  publication: r.publication || '',
  subject: r.subject || '',
  award: r.award || '',
  record_type: r.record_type || 'documented',
  extent: r.extent || '',
  rights_note: r.rights_note || '',
  citation_url: r.citation_url || ''
}));

const esc = s => String(s==null?'':s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const el = id => document.getElementById(id);

let filters = { year:null, publication:null, record_type:null, awardOnly:false };

// masthead
el('dek').textContent = BIO.career
  ? (BIO.career + '. ' + (BIO.honors ? BIO.honors.split(';')[0] + '.' : ''))
  : 'Three-time Pulitzer Prize–winning editorial cartoonist.';
el('prizes').innerHTML = ['1964','1971','1984'].map(y => '<span class="prize">Pulitzer '+y+'</span>').join('');

// facets
function facetValues(field) {
  const m = new Map();
  for (const r of RECORDS) { const v = r[field]; if (v) m.set(v, (m.get(v)||0)+1); }
  return [...m.entries()].sort((a,b)=> b[1]-a[1] || String(a[0]).localeCompare(String(b[0])));
}
function renderFacets() {
  const parts = [];
  parts.push('<button class="chip" data-f="awardOnly" aria-pressed="'+(filters.awardOnly?'true':'false')+'">Pulitzer years</button>');
  for (const rt of ['documented','collection-summary']) {
    const n = RECORDS.filter(r=>r.record_type===rt).length; if (!n) continue;
    const label = rt==='documented' ? 'Documented' : 'Archive holdings';
    parts.push('<button class="chip" data-f="record_type" data-v="'+rt+'" aria-pressed="'+(filters.record_type===rt?'true':'false')+'">'+label+' · '+n+'</button>');
  }
  for (const [v,n] of facetValues('publication')) {
    parts.push('<button class="chip" data-f="publication" data-v="'+esc(v)+'" aria-pressed="'+(filters.publication===v?'true':'false')+'">'+esc(v)+' · '+n+'</button>');
  }
  if (filters.year || filters.publication || filters.record_type || filters.awardOnly)
    parts.push('<button class="chip clear" data-f="clear">Clear</button>');
  const f = el('facets'); f.innerHTML = parts.join('');
  f.querySelectorAll('.chip').forEach(c => c.addEventListener('click', () => {
    const k = c.dataset.f;
    if (k === 'clear') filters = { year:null, publication:null, record_type:null, awardOnly:false };
    else if (k === 'awardOnly') filters.awardOnly = !filters.awardOnly;
    else filters[k] = filters[k] === c.dataset.v ? null : c.dataset.v;
    renderFacets(); render();
  }));
}

function applied() {
  const q = el('q').value.trim().toLowerCase();
  return RECORDS.filter(r => {
    if (filters.publication && r.publication !== filters.publication) return false;
    if (filters.record_type && r.record_type !== filters.record_type) return false;
    if (filters.awardOnly && !r.award) return false;
    if (q) {
      const hay = [r.title, r.subject, r.publication, r.award, r.date].join(' ').toLowerCase();
      if (!hay.includes(q)) return false;
    }
    return true;
  });
}
function sorted(list) {
  const a = [...list], m = el('sort').value;
  if (m==='year-asc') a.sort((x,y)=> x.year.localeCompare(y.year) || x.title.localeCompare(y.title));
  else if (m==='year-desc') a.sort((x,y)=> y.year.localeCompare(x.year) || x.title.localeCompare(y.title));
  else if (m==='title') a.sort((x,y)=> x.title.localeCompare(y.title));
  else if (m==='award') a.sort((x,y)=> (y.award?1:0)-(x.award?1:0) || x.year.localeCompare(y.year));
  return a;
}

function render() {
  const list = sorted(applied());
  el('count').textContent = list.length + ' of ' + RECORDS.length;
  const g = el('grid');
  el('empty').hidden = list.length !== 0;
  g.innerHTML = list.map(r => {
    const tags = [
      '<span class="tag '+r.record_type+'">'+(r.record_type==='documented'?'Documented':'Archive')+'</span>',
      r.award ? '<span class="tag award">Pulitzer</span>' : '',
      r.publication ? '<span class="tag pub">'+esc(r.publication)+'</span>' : ''
    ].join('');
    return '<button class="card" data-id="'+r.id+'" data-rt="'+r.record_type+'" data-award="'+(r.award?1:0)+'">'
      + '<span class="date">'+esc(r.date || r.year)+'</span>'
      + '<span class="title">'+esc(r.title)+'</span>'
      + '<span class="subj">'+esc(r.subject)+'</span>'
      + '<span class="tags">'+tags+'</span></button>';
  }).join('');
  g.querySelectorAll('.card').forEach(c => c.addEventListener('click', () => openModal(c.dataset.id)));
}

function openModal(id) {
  const r = RECORDS.find(x => x.id === id); if (!r) return;
  el('modal').innerHTML =
    '<button class="x" id="mx" aria-label="Close">&times;</button>'
    + '<div class="m-kicker">'+(r.record_type==='documented'?'Cartoon':'Archive holding')+(r.award?' · Pulitzer':'')+'</div>'
    + '<h2>'+esc(r.title)+'</h2>'
    + '<div class="m-date">'+esc(r.date || r.year)+(r.publication?' · '+esc(r.publication):'')+'</div>'
    + '<dl>'
    + '<dt>Subject</dt><dd>'+esc(r.subject||'—')+'</dd>'
    + (r.award?'<dt>Award</dt><dd>'+esc(r.award)+'</dd>':'')
    + (r.extent?'<dt>Extent</dt><dd>'+esc(r.extent)+'</dd>':'')
    + '<dt>Source</dt><dd>'+(r.citation_url?'<a href="'+esc(r.citation_url)+'" target="_blank" rel="noopener noreferrer">'+esc(r.citation_url.replace(/^https?:\/\//,'').split('/')[0])+' &nearr;</a>':'—')+'</dd>'
    + '</dl>'
    + (r.rights_note?'<div class="rights">'+esc(r.rights_note)+'</div>':'');
  el('mx').addEventListener('click', closeModal);
  el('backdrop').classList.add('open');
}
function closeModal(){ el('backdrop').classList.remove('open'); }

el('backdrop').addEventListener('click', e => { if (e.target.id==='backdrop') closeModal(); });
document.addEventListener('keydown', e => { if (e.key==='Escape') closeModal(); });
el('q').addEventListener('input', render);
el('sort').addEventListener('change', render);
el('density').addEventListener('input', () => document.documentElement.style.setProperty('--col', el('density').value + 'px'));
document.documentElement.style.setProperty('--col', el('density').value + 'px');

// sources line
const srcHosts = [...new Set(RECORDS.map(r=>r.citation_url).filter(Boolean).map(u=>u.replace(/^https?:\/\//,'').split('/')[0]))];
el('sources').innerHTML = 'Compiled from ' + srcHosts.map(h=>esc(h)).join(' &middot; ') + '.';

renderFacets();
render();
</script>