Back

tasks.papp

All 73 lines. No libraries, nothing hidden. Read it before you run it.

tasks.papp
<!doctype html>
<meta charset="utf-8">
<meta name="papp-write" content="./*.md">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 1.5 64 64'><g fill='%23111114'><rect x='4' y='10' width='16' height='13' rx='2.5'/><rect x='4' y='27' width='16' height='13' rx='2.5'/><rect x='4' y='44' width='16' height='13' rx='2.5'/></g><g fill='%23ff4f00'><rect x='24' y='27' width='16' height='13' rx='2.5'/><rect x='24' y='44' width='16' height='13' rx='2.5'/></g><rect x='44' y='44' width='16' height='13' rx='2.5' fill='%23c4c7d4'/></svg>">
<title>Tasks</title>
<style>
  :root { color-scheme: light dark; --paper: light-dark(#fff, #0f0f11); --well: light-dark(#f4f4f5, #1c1c20); --ink: light-dark(#0f0f11, #f2f2f3); --soft: light-dark(#6b6b72, #9b9ba4); --rule: light-dark(#e4e4e7, #2b2b31); --accent: #ff4f00; }
  body { margin: 0; padding: 28px; background: var(--paper); color: var(--ink); font: 14px/1.45 system-ui, sans-serif; }
  h1 { margin: 0 0 20px; font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
  main { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
  section { min-height: 70vh; padding: 14px; border: 1px solid transparent; border-radius: 14px; background: var(--well); transition: border-color 0.15s; }
  section.over { border-color: var(--accent); }
  section:not(:has(article))::after { content: 'Drop a card here'; display: block; padding: 24px 0; color: var(--soft); text-align: center; }
  h2 { display: flex; gap: 8px; align-items: center; margin: 0 0 12px; color: var(--soft); font-size: 12px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
  h2::after { content: attr(data-count); margin-left: auto; font-variant-numeric: tabular-nums; }
  [data-status='doing'] h2::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
  article { margin-bottom: 8px; padding: 12px 14px; border: 1px solid var(--rule); border-radius: 10px; background: var(--paper); font-weight: 500; cursor: grab; transition: translate 0.15s, box-shadow 0.15s; }
  article:hover { translate: 0 -1px; box-shadow: 0 6px 16px -10px rgb(0 0 0 / 0.5); }
  [data-status='done'] article { color: var(--soft); text-decoration: line-through; }
  small { display: block; margin-top: 4px; color: var(--soft); font: 12px ui-monospace, monospace; text-decoration: none; }
</style>
<h1>Tasks</h1>
<main id="board">
  <section data-status="todo"><h2>To do</h2></section>
  <section data-status="doing"><h2>Doing</h2></section>
  <section data-status="done"><h2>Done</h2></section>
</main>
<script type="module">
  // Each .md file in this folder is a card. Its column is the status line at the top, which a drop rewrites:
  // ---
  // status: doing
  // ---
  const columns = [...board.children];
  const statusLine = /^---\nstatus: (\w+)\n---\n/;

  for (const column of columns) {
    column.ondragover = event => event.preventDefault();
    column.ondragenter = () => column.classList.add('over');
    column.ondragleave = event => {
      if (!column.contains(event.relatedTarget)) column.classList.remove('over');
    };
    column.ondrop = async event => {
      column.classList.remove('over');
      const name = event.dataTransfer.getData('text/plain');
      const text = await papp.readFile(name);
      await papp.writeFile(name, `---\nstatus: ${column.dataset.status}\n---\n${text.replace(statusLine, '')}`);
    };
  }

  // Draws every card at once, so a watch event that lands mid-draw can't leave a card twice.
  async function draw() {
    const cards = { todo: [], doing: [], done: [] };
    for (const name of await papp.readdir('.')) {
      if (!name.endsWith('.md')) continue;
      const text = await papp.readFile(name);
      const card = document.createElement('article');
      card.draggable = true;
      card.ondragstart = event => event.dataTransfer.setData('text/plain', name);
      card.textContent = text.match(/^# (.+)$/m)?.[1] ?? name;
      card.append(Object.assign(document.createElement('small'), { textContent: name }));
      (cards[text.match(statusLine)?.[1]] ?? cards.todo).push(card);
    }
    for (const column of columns) {
      const heading = column.firstElementChild;
      heading.dataset.count = cards[column.dataset.status].length;
      column.replaceChildren(heading, ...cards[column.dataset.status]);
    }
  }

  await draw();
  // A drop above, or a save in another editor, redraws the board.
  await papp.watch('.', draw);
</script>
Download tasks.papp