The vault 146 files

Demo / the vault

Tools/review-surface/index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Review surface</title>
<style>
  :root {
    --ink: #1b1a17;
    --mute: #6b6862;
    --line: #e0ddd6;
    --paper: #faf9f6;
    --card: #ffffff;
    --accent: #2f5d50;
    --warn: #8a5a1e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--paper); color: var(--ink);
    font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
  }
  main { max-width: 760px; margin: 0 auto; padding: 40px 20px 120px; }
  header { border-bottom: 1px solid var(--line); padding-bottom: 18px; margin-bottom: 28px; }
  h1 { font-size: 21px; margin: 0 0 6px; letter-spacing: -0.01em; }
  .sub { color: var(--mute); font-size: 14px; }
  .sub code { font-size: 13px; background: #f0eee8; padding: 1px 5px; border-radius: 3px; }
  .queuepick { margin-top: 12px; font-size: 14px; }
  .queuepick select { font: inherit; font-size: 14px; padding: 4px 6px; border: 1px solid var(--line); border-radius: 5px; background: #fff; }

  .card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 18px 20px; margin-bottom: 14px; }
  .card.done { background: #f6f5f1; border-color: #ded9cd; }
  .eyebrow { font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--mute); margin-bottom: 8px; }
  .ask { font-size: 17px; font-weight: 600; margin: 0 0 10px; }
  .context { color: var(--mute); font-size: 14.5px; margin: 0 0 12px; white-space: pre-wrap; }
  .handles { margin-bottom: 14px; }
  .handle { display: inline-block; font: 12.5px ui-monospace, SFMono-Regular, Menlo, monospace;
            background: #f0eee8; color: #4a4741; padding: 2px 7px; border-radius: 4px; margin-right: 6px; }

  .row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  button { font: inherit; font-size: 14.5px; padding: 7px 14px; border-radius: 6px; border: 1px solid var(--line);
           background: #fff; color: var(--ink); cursor: pointer; }
  button:hover { border-color: #bdb8ab; }
  button.yes { background: var(--accent); border-color: var(--accent); color: #fff; }
  button.yes:hover { background: #26493f; }
  button.link { border: none; background: none; color: var(--mute); padding: 4px 0; text-decoration: underline; font-size: 13.5px; }

  .defer { margin-top: 12px; display: none; }
  .defer.open { display: block; }
  input[type=text], textarea { width: 100%; font: inherit; font-size: 14.5px; padding: 8px 10px;
    border: 1px solid var(--line); border-radius: 6px; background: #fff; color: var(--ink); }
  textarea { min-height: 64px; resize: vertical; }
  label.f { display: block; font-size: 13px; color: var(--mute); margin: 10px 0 4px; }

  .answered { font-size: 14px; }
  .answered b { color: var(--accent); }
  .answered .warn { color: var(--warn); }
  .meta { color: var(--mute); font-size: 13px; margin-top: 4px; }
  .meta code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
  .empty { color: var(--mute); padding: 40px 0; }
  .err { color: #8a1e1e; font-size: 14px; margin-top: 8px; }
</style>
</head>
<body>
<main>
  <header>
    <h1 id="title">Review surface</h1>
    <div class="sub" id="sub"></div>
    <div class="queuepick" id="pick"></div>
  </header>
  <div id="list"></div>
</main>
<script>
let STATE = [], CURRENT = 0;

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

async function load() {
  STATE = await (await fetch('/api/state')).json();
  render();
}

function render() {
  const q = STATE[CURRENT];
  if (!q) {
    document.getElementById('sub').textContent = '';
    document.getElementById('list').innerHTML = '<div class="empty">No disposition queue in Efforts/dispositions/. Hobbs writes one with each brief and each weekly review.</div>';
    return;
  }
  const done = q.asks.filter((a) => a.answered).length;
  document.getElementById('title').textContent = q.title || q.kind;
  document.getElementById('sub').innerHTML =
    esc(q.date) + ' &middot; ' + done + ' of ' + q.asks.length + ' answered &middot; <code>' + esc(q.source) + '</code>';
  document.getElementById('pick').innerHTML = STATE.length > 1
    ? 'Queue: <select onchange="CURRENT=+this.value;render()">' +
      STATE.map((s, i) => '<option value="' + i + '"' + (i === CURRENT ? ' selected' : '') + '>' + esc(s.file) + '</option>').join('') +
      '</select>'
    : '';
  document.getElementById('list').innerHTML = q.asks.map(card).join('');
}

function card(a) {
  const h = (a.tasks || []).map((t) => '<span class="handle">' + esc(t) + '</span>').join('');
  if (a.answered) {
    const r = a.answered;
    const eff = (r.effects && r.effects.done || []);
    const ref = (r.effects && r.effects.refused || []);
    return '<div class="card done">' +
      '<div class="eyebrow">' + esc(a.section) + '</div>' +
      '<p class="ask">' + esc(a.ask) + '</p>' +
      '<div class="answered"><b>' + esc(r.answer) + '</b>' +
        (r.trigger ? ' &mdash; trigger: &ldquo;' + esc(r.trigger) + '&rdquo;' : '') +
        (r.note ? ' &mdash; note: &ldquo;' + esc(r.note) + '&rdquo;' : '') + '</div>' +
      '<div class="meta">' +
        (eff.length ? esc(eff.join('; ')) + ' &middot; ' : '') +
        (r.intention ? 'intention: <code>' + esc(r.intention) + '</code> &middot; ' : '') +
        'commit <code>' + esc(r.commit || '—') + '</code>' +
        (ref.length ? '<div class="err">refused: ' + esc(ref.join('; ')) + '</div>' : '') +
      '</div>' +
      '<button class="link" onclick="reopen(\'' + a.id + '\')">change</button>' +
      '</div>';
  }
  return '<div class="card" id="c-' + a.id + '">' +
    '<div class="eyebrow">' + esc(a.section) + '</div>' +
    '<p class="ask">' + esc(a.ask) + '</p>' +
    (a.context ? '<p class="context">' + esc(a.context) + '</p>' : '') +
    (h ? '<div class="handles">' + h + '</div>' : '') +
    '<div class="row">' +
      '<button class="yes" onclick="send(\'' + a.id + '\',\'yes\')">' + esc(a.yes_label || 'Yes') + '</button>' +
      '<button onclick="send(\'' + a.id + '\',\'no\')">' + esc(a.no_label || 'No') + '</button>' +
      '<button onclick="openDefer(\'' + a.id + '\')">Defer&hellip;</button>' +
      '<button class="link" onclick="openNote(\'' + a.id + '\')">add a note</button>' +
    '</div>' +
    '<div class="defer" id="d-' + a.id + '">' +
      '<label class="f">What brings it back? A date, or the thing that has to happen first.</label>' +
      '<input type="text" id="t-' + a.id + '" placeholder="e.g. once Pike is granted or refused">' +
      '<div class="row" style="margin-top:10px"><button class="yes" onclick="send(\'' + a.id + '\',\'defer\')">Defer</button></div>' +
    '</div>' +
    '<div class="defer" id="n-' + a.id + '">' +
      '<label class="f">Note &mdash; your words, handed to Hobbs as an intention.</label>' +
      '<textarea id="x-' + a.id + '"></textarea>' +
    '</div>' +
    '<div class="err" id="e-' + a.id + '"></div>' +
    '</div>';
}

const openDefer = (id) => document.getElementById('d-' + id).classList.toggle('open');
const openNote = (id) => document.getElementById('n-' + id).classList.toggle('open');

function reopen(id) {
  const q = STATE[CURRENT];
  q.asks = q.asks.map((a) => a.id === id ? { ...a, answered: null } : a);
  render();
}

async function send(id, choice) {
  const q = STATE[CURRENT];
  const trigger = (document.getElementById('t-' + id) || {}).value || '';
  const note = (document.getElementById('x-' + id) || {}).value || '';
  const err = document.getElementById('e-' + id);
  err.textContent = '';
  document.querySelectorAll('#c-' + id + ' button').forEach((b) => b.disabled = true);
  const res = await fetch('/api/answer', {
    method: 'POST', headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ file: q.file, ask: id, answer: choice, trigger, note }),
  });
  const body = await res.json();
  if (!res.ok) {
    err.textContent = body.error;
    document.querySelectorAll('#c-' + id + ' button').forEach((b) => b.disabled = false);
    return;
  }
  await load();
}

load();
</script>
</body>
</html>