<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Minutes Decision Board</title>
<style>
:root{color-scheme:dark;font:14px ui-monospace,SFMono-Regular,Consolas,monospace;background:#141719;color:#eff4ee;letter-spacing:0}*{box-sizing:border-box}body{margin:0}button,input,textarea,select{font:inherit;color:inherit;background:#212629;border:1px solid #586264;border-radius:4px;padding:9px}button{cursor:pointer}button:hover{background:#333d3d}button:disabled{opacity:.45;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid #a4f377;outline-offset:3px}header{padding:20px 24px;border-bottom:1px solid #424b4c;display:flex;align-items:center;gap:14px;flex-wrap:wrap}h1{font-size:20px;margin:0;overflow-wrap:anywhere;flex:1;min-width:150px}.mark{color:#a4f377;font-weight:bold;font-size:22px}#status{color:#b9d4d1;font-size:12px}#notice{min-height:36px;padding:10px 24px;color:#ffb19e}main{padding:6px 24px 24px;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr));gap:22px;align-items:start}.column{min-width:0;border-top:3px solid #8cd467}.column:nth-child(3n+2){border-color:#70c5ce}.column:nth-child(3n){border-color:#e69d86}.column-head{display:flex;align-items:center;gap:6px;padding:14px 0}.column-head h2{font-size:15px;flex:1;margin:0;overflow-wrap:anywhere}.icon{width:34px;height:34px;padding:4px;flex-shrink:0}.card{border:1px solid #465152;border-radius:5px;background:#1c2224;margin:0 0 12px;padding:14px;min-width:0}.card.selected{border-color:#a4f377;box-shadow:inset 3px 0 #a4f377}.card h3{font-size:15px;margin:0 0 10px;overflow-wrap:anywhere}.card p{color:#c9d2d0;line-height:1.6;margin:0 0 14px;white-space:pre-wrap;overflow-wrap:anywhere}.card-actions{display:flex;gap:7px;flex-wrap:wrap}.card-actions select{max-width:100%;min-width:0;flex:1}.select-card{border:0;padding:0;background:transparent;text-align:left;width:100%;color:inherit}.select-card:hover{background:transparent;color:#a4f377}.empty{color:#829795;padding:20px 0}.add{width:100%;border-style:dashed;background:transparent}dialog{background:#1c2224;color:inherit;border:1px solid #647371;border-radius:6px;padding:22px;width:500px;max-width:calc(100% - 32px)}dialog::backdrop{background:#0009}dialog h2{font-size:18px;margin:0 0 20px}label{display:block;margin:12px 0}input,textarea{display:block;width:100%;margin-top:7px}textarea{min-height:120px;resize:vertical}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}.primary{background:#a4f377;color:#182119;border-color:#a4f377}.primary:hover{background:#baf799}.badge{font-size:11px;color:#a4f377;margin-bottom:8px}#dialog-error{color:#ffb19e;overflow-wrap:anywhere}footer{padding:8px 24px 24px;color:#92aaa7;font-size:12px}@media(max-width:600px){header{padding:16px}main{padding:0 16px 16px;gap:20px}button,select{min-height:44px}.icon{width:44px;height:44px}.column-head{flex-wrap:wrap}#notice{padding:10px 16px}h1{flex-basis:70%}}
</style></head><body>
<header><span class="mark" aria-hidden="true">M_</span><h1 id="title">Decision Board</h1><span id="status" role="status">Connecting</span><button id="undo" class="icon" title="Undo latest change" aria-label="Undo latest change">↶</button><button id="column">Add column</button></header>
<div id="notice" role="status" aria-live="polite"></div><main id="board" aria-label="Decision board"></main><footer id="selection"></footer>
<dialog id="editor"><form id="form"><h2 id="heading">Edit idea</h2><label>Title<input id="edit-title" required maxlength="180"></label><label id="body-label">Notes<textarea id="edit-body" maxlength="3000"></textarea></label><label id="merge-label" hidden>Merge with<select id="merge-target"></select></label><div id="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" id="cancel">Cancel</button><button type="submit" class="primary">Save</button></div></form></dialog>
<script>
'use strict';
const token=location.hash.slice(1);history.replaceState(null,'',location.pathname);
let state=null,busy=false,editing=null;
const $=id=>document.getElementById(id), el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n};
async function request(path,body){const headers={'X-Minutes-Token':token};if(body)headers['X-Minutes-Change']=btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(body))));const r=await fetch(path,{method:body?'POST':'GET',headers,body:body?'':undefined,cache:'no-store',signal:AbortSignal.timeout(5000)});const v=await r.json();if(!r.ok)throw Error(v.error||'Board unavailable');return v}
function accept(v){const changed=!state||state.revision!==v.revision||state.selected_card_id!==v.selected_card_id;state=v;$('status').textContent='Saved / revision '+v.revision;if(changed)render()}
async function refresh(){if(busy)return;try{accept(await request('/state'))}catch(e){$('status').textContent='Disconnected';$('notice').textContent=e.message}}
async function change(change,revision=state.revision){if(busy)return false;busy=true;try{accept(await request('/edit',{revision,change}));$('notice').textContent='';return true}catch(e){$('notice').textContent=e.message;return false}finally{busy=false}}
async function selectCard(id){if(busy)return;busy=true;try{accept(await request('/edit',{revision:state.revision,select_card_id:id}))}catch(e){$('notice').textContent=e.message}finally{busy=false}}
function button(text,title,fn,cls){const b=el('button',text,cls);b.type='button';b.title=title;b.setAttribute('aria-label',title);b.onclick=fn;return b}
function edit(mode,target){if(!state)return;editing={mode,target,revision:state.revision};$('heading').textContent={card:'Edit idea',add:'Add idea',column:'Rename column',newColumn:'Add column',merge:'Merge ideas'}[mode];$('edit-title').value=target?.title||'';$('edit-body').value=target?.body||'';$('body-label').hidden=['column','newColumn'].includes(mode);$('merge-label').hidden=mode!=='merge';$('dialog-error').textContent='';$('merge-target').replaceChildren();if(mode==='merge'){for(const c of state.columns.flatMap(c=>c.cards).filter(c=>c.id!==target.id)){const o=el('option',c.title);o.value=c.id;$('merge-target').append(o)}const combined=()=>{const other=state.columns.flatMap(c=>c.cards).find(c=>c.id===$('merge-target').value);$('edit-body').value=[target.body,other?.body].filter(Boolean).join('\n\n')};$('merge-target').onchange=combined;combined()}$('editor').showModal();$('edit-title').focus()}
function render(){
$('title').textContent=state.title;document.title=state.title+' | Minutes';$('board').replaceChildren();$('column').disabled=false;$('undo').disabled=!state.undoable_changes.length;
for(const [index,column] of state.columns.entries()){
const section=el('section',undefined,'column'),head=el('div',undefined,'column-head');head.append(el('h2',column.title+' / '+column.cards.length));
head.append(button('\u270e','Rename '+column.title,()=>edit('column',column),'icon'));
for(const [direction,label]of[[-1,'Move column left'],[1,'Move column right']]){const b=button(direction<0?'\u2190':'\u2192',label,()=>{const ids=state.columns.map(c=>c.id);[ids[index],ids[index+direction]]=[ids[index+direction],ids[index]];change({operation:'reorder_columns',column_ids:ids})},'icon');b.disabled=index+direction<0||index+direction>=state.columns.length;head.append(b)}section.append(head);
for(const card of column.cards){const article=el('article',undefined,'card'+(state.selected_card_id===card.id?' selected':''));article.dataset.cardId=card.id;if(state.selected_card_id===card.id)article.append(el('div','Selected','badge'));const pick=button(card.title,'Select '+card.title,()=>selectCard(card.id),'select-card');const h=el('h3');h.append(pick);article.append(h,el('p',card.body));const actions=el('div',undefined,'card-actions');actions.append(button('Edit','Edit '+card.title,()=>edit('card',card)));const merge=button('Merge','Merge '+card.title,()=>edit('merge',card));merge.disabled=state.columns.flatMap(c=>c.cards).length<2;actions.append(merge);const move=el('select');move.setAttribute('aria-label','Move '+card.title);for(const col of state.columns){const o=el('option',col.title);o.value=col.id;o.selected=col.id===column.id;move.append(o)}move.onchange=()=>change({operation:'move_card',card_id:card.id,column_id:move.value});actions.append(move);article.append(actions);section.append(article)}
if(!column.cards.length)section.append(el('div','No ideas','empty'));section.append(button('Add idea','Add idea to '+column.title,()=>edit('add',{id:column.id}),'add'));$('board').append(section);
}
const selected=state.columns.flatMap(c=>c.cards).find(c=>c.id===state.selected_card_id);$('selection').textContent=selected?'Selected: '+selected.title:'';
}
$('column').onclick=()=>edit('newColumn');$('undo').onclick=()=>change({operation:'undo',change_id:state.undoable_changes.at(-1)});$('cancel').onclick=()=>$('editor').close();
$('form').onsubmit=async e=>{e.preventDefault();const {mode,target,revision}=editing,title=$('edit-title').value,body=$('edit-body').value;const changes={card:{operation:'edit_card',card_id:target?.id,title,body},add:{operation:'add_card',column_id:target?.id,title,body},column:{operation:'rename_column',column_id:target?.id,title},newColumn:{operation:'add_column',title},merge:{operation:'merge_cards',card_id:target?.id,other_card_id:$('merge-target').value,title,body}};if(await change(changes[mode],revision))$('editor').close();else $('dialog-error').textContent=$('notice').textContent};
$('column').disabled=true;$('undo').disabled=true;
if(!/^[a-f0-9]{64}$/.test(token)){$('status').textContent='Access unavailable';$('notice').textContent='Reopen this board from Minutes.'}else{refresh();setInterval(refresh,1200)}
</script></body></html>