- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.0.0, shown as text in the browser.
Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.
import React, { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import initialBoard from '../public/data/board.json';
import memberData from '../public/data/members.json';
import './styles.css';
type ChecklistItem = { id: string; text: string; done: boolean };
type Card = { id: string; title: string; description: string; assignee: string | null; labels: string[]; priority: string; due: string | null; checklist: ChecklistItem[] };
type Column = { id: string; title: string; wip_limit: number | null; card_ids: string[] };
type Board = { product: string; workspace: string; board: string; today: string; fictional_notice: string; labels: {id:string;name:string}[]; priorities: string[]; columns: Column[]; cards: Card[] };
type Modal = {type:'detail'; id:string} | {type:'create'; columnId:string} | {type:'delete'; id:string};
type DropSpot = {columnId:string; index:number};
const seed = initialBoard as Board;
const members = memberData.members;
const storageKey = 'boardly-website-relaunch-v1';
const clone = (board:Board) => structuredClone(board);
const icon = (name:string, size=16) => {
const shared={width:size,height:size,viewBox:'0 0 24 24',fill:'none',stroke:'currentColor',strokeWidth:1.8,strokeLinecap:'round' as const,strokeLinejoin:'round' as const,'aria-hidden':true as const};
const paths:Record<string,React.ReactNode>={
search:<><circle cx="10.8" cy="10.8" r="6.8"/><path d="m16 16 5 5"/></>,
plus:<path d="M12 5v14M5 12h14"/>,
close:<path d="M5 5l14 14M19 5 5 19"/>,
chevron:<path d="m9 6 6 6-6 6"/>,
undo:<><path d="M9 8H4V3"/><path d="M4 8a8 8 0 1 1-1 7"/></>,
calendar:<><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 3v4M17 3v4M3 10h18"/></>,
check:<path d="m4 12 5 5L20 6"/>,
list:<><path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01"/></>,
sliders:<><path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="2" fill="currentColor" stroke="none"/><circle cx="16" cy="17" r="2" fill="currentColor" stroke="none"/></>,
trash:<><path d="M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14M10 11v6M14 11v6"/></>,
grip:<><circle cx="8" cy="6" r="1"/><circle cx="16" cy="6" r="1"/><circle cx="8" cy="12" r="1"/><circle cx="16" cy="12" r="1"/><circle cx="8" cy="18" r="1"/><circle cx="16" cy="18" r="1"/></>,
arrow:<path d="M5 12h14m-6-6 6 6-6 6"/>,
};
return <svg {...shared}>{paths[name]}</svg>;
};
function loadBoard():Board { try { const value=localStorage.getItem(storageKey); if(value){const parsed=JSON.parse(value); if(parsed?.columns?.length===seed.columns.length && Array.isArray(parsed.cards)) return parsed;} } catch {} return clone(seed); }
function dateLabel(date:string|null) { if(!date) return 'No due date'; return new Intl.DateTimeFormat('en-US',{month:'short',day:'numeric',timeZone:'UTC'}).format(new Date(date+'T12:00:00Z')); }
function memberName(id:string|null) {return members.find(m=>m.id===id)?.name || 'Unassigned';}
function initials(name:string) {return name.split(' ').map(x=>x[0]).slice(0,2).join('').toUpperCase();}
function App(){
const [board,setBoard]=useState<Board>(loadBoard);
const [history,setHistory]=useState<{board:Board;action:string}|null>(null);
const [assignee,setAssignee]=useState('all');
const [label,setLabel]=useState('all');
const [query,setQuery]=useState('');
const [modal,setModal]=useState<Modal|null>(null);
const [draft,setDraft]=useState<Card|null>(null);
const [error,setError]=useState('');
const [notice,setNotice]=useState('');
const [announcement,setAnnouncement]=useState('');
const [dragId,setDragId]=useState<string|null>(null);
const [dropSpot,setDropSpot]=useState<DropSpot|null>(null);
const [keyboardGrab,setKeyboardGrab]=useState<{id:string;origin:Board}|null>(null);
const returnFocus=useRef<string|null>(null);
const dialogRef=useRef<HTMLDivElement>(null);
const dragRef=useRef<string|null>(null);
const dropRef=useRef<DropSpot|null>(null);
useEffect(()=>{localStorage.setItem(storageKey,JSON.stringify(board));},[board]);
useEffect(()=>{if(modal) requestAnimationFrame(()=>dialogRef.current?.querySelector<HTMLElement>('input:not([type=checkbox]), textarea, select, button')?.focus());},[modal?.type]);
const announce=(message:string)=>{setAnnouncement('');setTimeout(()=>setAnnouncement(message),20);};
const tell=(message:string)=>{setNotice(message);announce(message);};
const clearFilters=()=>{setAssignee('all');setLabel('all');setQuery('');};
const filtersActive=assignee!=='all'||label!=='all'||query.trim()!=='';
const visible=(card:Card)=> (assignee==='all'||(assignee==='unassigned'?card.assignee===null:card.assignee===assignee)) && (label==='all'||card.labels.includes(label)) && (!query.trim()||`${card.title} ${card.description}`.toLowerCase().includes(query.trim().toLowerCase()));
const totalVisible=board.cards.filter(visible).length;
const locationOf=(b:Board,id:string)=>b.columns.findIndex(c=>c.card_ids.includes(id));
const limitText=(column:Column)=>`${column.title} is at its ${column.wip_limit}-card limit. Move a card out before adding another.`;
const commit=(next:Board,action:string,origin=board)=>{setHistory({board:clone(origin),action});setBoard(next);setNotice('');};
const move=(id:string,toColumnId:string,targetIndex:number,fromKeyboard=false)=>{
const sourceIndex=locationOf(board,id), destinationIndex=board.columns.findIndex(c=>c.id===toColumnId);
if(sourceIndex<0||destinationIndex<0)return false;
const source=board.columns[sourceIndex], destination=board.columns[destinationIndex];
if(source.id!==destination.id&&destination.wip_limit!==null&&destination.card_ids.length>=destination.wip_limit){tell(limitText(destination));return false;}
const oldIndex=source.card_ids.indexOf(id);
const next=clone(board);
next.columns[sourceIndex].card_ids.splice(oldIndex,1);
const adjusted=sourceIndex===destinationIndex&&oldIndex<targetIndex?targetIndex-1:targetIndex;
next.columns[destinationIndex].card_ids.splice(Math.min(Math.max(adjusted,0),next.columns[destinationIndex].card_ids.length),0,id);
if(sourceIndex===destinationIndex&&oldIndex===adjusted)return false;
if(fromKeyboard){setBoard(next);setNotice('');} else commit(next,`Moved ${board.cards.find(c=>c.id===id)?.title}`);
return true;
};
const undo=()=>{if(!history)return;setBoard(clone(history.board));tell(`Undid: ${history.action}.`);setHistory(null);};
useEffect(()=>{
const handler=(e:KeyboardEvent)=>{const target=e.target as HTMLElement;const editing=target.matches('input, textarea, select, [contenteditable="true"]');if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'&&!editing&&!modal){e.preventDefault();undo();}};
document.addEventListener('keydown',handler);return()=>document.removeEventListener('keydown',handler);
});
const closeModal=()=>{setModal(null);setDraft(null);setError('');const id=returnFocus.current;requestAnimationFrame(()=>{if(id)document.getElementById(id)?.focus();});};
const openDetail=(id:string)=>{returnFocus.current=`card-${id}`;const card=board.cards.find(c=>c.id===id);if(!card)return;setDraft(structuredClone(card));setError('');setModal({type:'detail',id});};
const openCreate=(columnId:string)=>{returnFocus.current=`add-${columnId}`;setDraft({id:`card-${Date.now()}`,title:'',description:'',assignee:null,labels:[],priority:'medium',due:null,checklist:[]});setError('');setModal({type:'create',columnId});};
const saveDraft=()=>{
if(!draft||!modal||modal.type==='delete')return;
if(!draft.title.trim()){setError('Give this card a title to continue.');return;}
const cleaned={...draft,title:draft.title.trim(),description:draft.description.trim(),checklist:draft.checklist.filter(i=>i.text.trim()).map(i=>({...i,text:i.text.trim()}))};
const next=clone(board);
if(modal.type==='create'){
const column=next.columns.find(c=>c.id===modal.columnId)!;
if(column.wip_limit!==null&&column.card_ids.length>=column.wip_limit){setError(limitText(column));tell(limitText(column));return;}
next.cards.push(cleaned);column.card_ids.push(cleaned.id);commit(next,`Created ${cleaned.title}`);
tell(`Created ${cleaned.title} in ${column.title}.`);
} else {const index=next.cards.findIndex(c=>c.id===modal.id);next.cards[index]=cleaned;setBoard(next);tell(`Saved ${cleaned.title}.`);}
closeModal();
};
const deleteCard=()=>{if(!modal||modal.type!=='delete')return;const card=board.cards.find(c=>c.id===modal.id)!;const column=board.columns[locationOf(board,modal.id)];const next=clone(board);next.cards=next.cards.filter(c=>c.id!==modal.id);next.columns.forEach(c=>c.card_ids=c.card_ids.filter(id=>id!==modal.id));commit(next,`Deleted ${card.title}`);tell(`Deleted ${card.title}. You can undo this action.`);returnFocus.current=`add-${column.id}`;closeModal();};
const onCardKey=(e:React.KeyboardEvent,id:string)=>{
if(e.target!==e.currentTarget)return;
if(!keyboardGrab){if(e.key===' '){e.preventDefault();setKeyboardGrab({id,origin:clone(board)});announce(`Picked up ${board.cards.find(c=>c.id===id)?.title}. Use arrow keys to move, Enter or Space to drop, Escape to cancel.`);}else if(e.key==='Enter'){e.preventDefault();openDetail(id);}return;}
if(keyboardGrab.id!==id)return;
if(e.key==='Escape'){e.preventDefault();setBoard(keyboardGrab.origin);setKeyboardGrab(null);announce(`Move cancelled. ${board.cards.find(c=>c.id===id)?.title} returned to its original position.`);requestAnimationFrame(()=>document.getElementById(`card-${id}`)?.focus());return;}
if(e.key==='Enter'||e.key===' '){e.preventDefault();setHistory({board:keyboardGrab.origin,action:`Moved ${board.cards.find(c=>c.id===id)?.title}`});setKeyboardGrab(null);announce(`Dropped ${board.cards.find(c=>c.id===id)?.title}.`);return;}
if(!['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key))return;
e.preventDefault();const ci=locationOf(board,id);const col=board.columns[ci];const index=col.card_ids.indexOf(id);
let nextCi=ci,target=index;
if(e.key==='ArrowUp')target=Math.max(0,index-1);
if(e.key==='ArrowDown')target=Math.min(col.card_ids.length-1,index+1);
if(e.key==='ArrowLeft'){nextCi=Math.max(0,ci-1);target=board.columns[nextCi].card_ids.length;}
if(e.key==='ArrowRight'){nextCi=Math.min(board.columns.length-1,ci+1);target=board.columns[nextCi].card_ids.length;}
const rawTarget=nextCi===ci&&target>index?target+1:target;
if(move(id,board.columns[nextCi].id,rawTarget,true)){
announce(`Moved ${board.cards.find(c=>c.id===id)?.title} to ${board.columns[nextCi].title}, position ${target+1}.`);
requestAnimationFrame(()=>document.getElementById(`card-${id}`)?.focus());
}
};
const onDragStart=(e:React.DragEvent,id:string)=>{dragRef.current=id;setDragId(id);e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',id);};
const onDragOver=(e:React.DragEvent,columnId:string,index:number)=>{if(!dragRef.current)return;e.preventDefault();e.dataTransfer.dropEffect='move';const spot={columnId,index};dropRef.current=spot;setDropSpot(spot);};
const onDrop=(e:React.DragEvent,columnId:string)=>{e.preventDefault();const id=dragRef.current||e.dataTransfer.getData('text/plain');const spot=dropRef.current; if(id&&spot&&spot.columnId===columnId)move(id,columnId,spot.index);dragRef.current=null;dropRef.current=null;setDragId(null);setDropSpot(null);};
const onDragEnd=()=>{dragRef.current=null;dropRef.current=null;setDragId(null);setDropSpot(null);};
const moveFromSelect=(id:string,target:string)=>{if(!target)return;const col=board.columns.find(c=>c.id===target)!;if(move(id,target,col.card_ids.length))tell(`Moved card to ${col.title}.`);};
const completed=board.columns.find(c=>c.id==='done')!.card_ids.length;
return <div className="app-shell">
<aside className="sidebar">
<div className="brand"><span className="brand-mark" aria-hidden="true"><i/><i/><i/></span><span>boardly<span className="brand-dot">.</span></span></div>
<div className="workspace-block"><div className="workspace-avatar">H</div><div><span className="eyebrow">WORKSPACE</span><strong>{board.workspace}</strong></div>{icon('chevron',14)}</div>
<div className="side-label">YOUR SPACE</div>
<div className="side-nav active"><span className="nav-grid" aria-hidden="true"><i/><i/><i/><i/></span> Project board <span className="nav-pill">1</span></div>
<div className="side-spacer"/>
<div className="side-progress"><span className="eyebrow">PROJECT PROGRESS</span><div className="progress-heading"><strong>Moving forward</strong><span>{completed}/{board.cards.length}</span></div><div className="progress-track"><span style={{width:`${board.cards.length?completed/board.cards.length*100:0}%`}}/></div><p>{completed} cards done, one step closer to launch.</p></div>
<div className="sidebar-footer">A clearer way to move work.</div>
</aside>
<main className="main-content">
<header className="topbar"><div className="mobile-brand"><span className="brand-mark small" aria-hidden="true"><i/><i/><i/></span><span className="mobile-brand-text"><strong>boardly<span className="brand-dot">.</span></strong><small>Harbor Studio</small></span></div><div className="breadcrumbs"><span>Harbor Studio</span>{icon('chevron',14)}<strong>Project board</strong></div><div className="topbar-right"><span className="today-pill"><span className="today-dot"/> Demo date · Oct 14, 2026</span><div className="team-avatars" aria-label="Team members">{members.slice(0,4).map(m=><span key={m.id} title={m.name} className={`avatar avatar-${m.id}`}>{initials(m.name)}</span>)}</div></div></header>
<div className="content-inner"><section className="board-heading"><div><div className="section-kicker"><span className="kicker-line"/> PROJECT / BOARD VIEW</div><h1>{board.board}</h1><p>Everything in motion, all in one place.</p><p className="fictional-inline">{board.fictional_notice}</p></div><div className="heading-actions"><button className="subtle-button" onClick={undo} disabled={!history} title={history?`Undo ${history.action}`:'Nothing to undo'}>{icon('undo')}<span>Undo</span></button><button className="subtle-button reset-button" onClick={()=>{setBoard(clone(seed));setHistory(null);clearFilters();tell('Demo reset to its original board.');}}>Reset demo</button><button className="primary-button" onClick={()=>openCreate('backlog')}>{icon('plus')} New card</button></div></section>
<div className="view-tabs"><span className="view-tab active">Board <span>{board.cards.length}</span></span><span className="view-divider"/><span className="view-hint">Track ideas from first thought to finished work</span></div>
<section className="filter-panel" aria-label="Board filters"><div className="filter-heading">{icon('sliders')}<span>Filter board</span></div><label className="search-field">{icon('search')}<span className="sr-only">Search cards</span><input aria-label="Search cards" placeholder="Search cards..." value={query} onChange={e=>setQuery(e.target.value)}/>{query&&<button aria-label="Clear search" onClick={()=>setQuery('')}>{icon('close',14)}</button>}</label><label className="filter-select"><span className="sr-only">Filter by assignee</span><select value={assignee} onChange={e=>setAssignee(e.target.value)} aria-label="Filter by assignee"><option value="all">All assignees</option><option value="unassigned">Unassigned</option>{members.map(m=><option key={m.id} value={m.id}>{m.name}</option>)}</select></label><label className="filter-select"><span className="sr-only">Filter by label</span><select value={label} onChange={e=>setLabel(e.target.value)} aria-label="Filter by label"><option value="all">All labels</option>{board.labels.map(l=><option key={l.id} value={l.id}>{l.name}</option>)}</select></label>{filtersActive&&<button className="clear-filters" onClick={clearFilters}>Clear filters</button>}<span className="filter-count">{totalVisible} shown</span></section>
{notice&&<div className="notice" role="alert"><span>{notice}</span><button aria-label="Dismiss message" onClick={()=>setNotice('')}>{icon('close',14)}</button></div>}
{totalVisible===0&&<div className="empty-results"><div className="empty-icon">{icon('search',26)}</div><h2>No cards found</h2><p>Try a different search or remove a filter to see more work.</p><button className="subtle-button" onClick={clearFilters}>Clear all filters</button></div>}
<section className="board-scroll" aria-label="Kanban board"><div className="columns">
{board.columns.map((column,ci)=>{const ids=column.card_ids.filter(id=>{const card=board.cards.find(c=>c.id===id);return card&&visible(card);});const blocked=dragId&&locationOf(board,dragId)!==ci&&column.wip_limit!==null&&column.card_ids.length>=column.wip_limit;return <div className={`column column-${column.id}${blocked?' limit-blocked':''}`} key={column.id} onDragOver={e=>{if(e.target===e.currentTarget||!(e.target as HTMLElement).closest('.card'))onDragOver(e,column.id,column.card_ids.length);}} onDrop={e=>onDrop(e,column.id)}><div className="column-top"><div className="column-title"><span className="column-indicator"/><h2>{column.title}</h2><span className="column-count">{ids.length}{column.wip_limit!==null&&` / ${column.wip_limit}`}</span></div><button id={`add-${column.id}`} className="add-icon" aria-label={`Add card to ${column.title}`} title={`Add card to ${column.title}`} onClick={()=>openCreate(column.id)}>{icon('plus',17)}</button></div>{column.wip_limit!==null&&<div className="limit-caption">Work in progress limit · {column.wip_limit}</div>}<div className="card-list">
{ids.map(id=>{const card=board.cards.find(c=>c.id===id)!;const fullIndex=column.card_ids.indexOf(id);const overdue=!!card.due&&card.due<board.today&&column.id!=='done';const done=card.checklist.filter(i=>i.done).length;const member=members.find(m=>m.id===card.assignee);const isGrabbed=keyboardGrab?.id===id;return <React.Fragment key={id}>{dropSpot?.columnId===column.id&&dropSpot.index===fullIndex&&dragId&&<div className="drop-placeholder" aria-label="Drop position"/>}<article id={`card-${id}`} className={`card${dragId===id?' dragging':''}${isGrabbed?' keyboard-grabbed':''}`} tabIndex={0} role="button" aria-label={`${card.title}. ${column.title}. Press Enter for details or Space to move.`} draggable onDragStart={e=>onDragStart(e,id)} onDragEnd={onDragEnd} onDragOver={e=>{e.stopPropagation();const rect=e.currentTarget.getBoundingClientRect();onDragOver(e,column.id,fullIndex+(e.clientY>rect.top+rect.height/2?1:0));}} onKeyDown={e=>onCardKey(e,id)} onClick={e=>{if(!(e.target as HTMLElement).closest('select, button')&&!dragId&&!keyboardGrab)openDetail(id);}}><div className="card-topline"><span className={`priority priority-${card.priority}`}>{card.priority}</span><span className="card-grip" aria-hidden="true">{icon('grip',14)}</span></div><h3>{card.title}</h3><div className="card-labels">{card.labels.map(labelId=>{const l=board.labels.find(x=>x.id===labelId);return <span key={labelId} className={`tag tag-${labelId}`}>{l?.name}</span>;})}</div><div className="card-meta"><span className={`card-date${overdue?' overdue':''}`} title={overdue?'Overdue':card.due?'Due date':'No due date'}>{icon('calendar',13)} {dateLabel(card.due)}{overdue&&<b> · Overdue</b>}</span>{card.checklist.length>0&&<span className="check-progress" title="Checklist progress">{icon('check',13)} {done}/{card.checklist.length}</span>}</div><div className="card-bottom"><div className="assigned">{member?<span className={`avatar avatar-${member.id}`} title={member.name}>{initials(member.name)}</span>:<span className="unassigned-icon" aria-hidden="true">?</span>}<span>{member?member.name.split(' ')[0]:'Unassigned'}</span></div><label className="move-control"><span className="sr-only">Move {card.title} to another column</span><select aria-label={`Move ${card.title} to another column`} value="" onClick={e=>e.stopPropagation()} onChange={e=>moveFromSelect(id,e.target.value)}><option value="">Move to…</option>{board.columns.filter(c=>c.id!==column.id).map(c=><option key={c.id} value={c.id}>{c.title}</option>)}</select></label></div></article></React.Fragment>;})}
{dropSpot?.columnId===column.id&&dropSpot.index===column.card_ids.length&&dragId&&<div className="drop-placeholder" aria-label="Drop position"/>}
{ids.length===0&&<div className="column-empty">{filtersActive?'No matching cards here.':'Nothing here yet. Add a card to get started.'}</div>}
<button className="add-card" onClick={()=>openCreate(column.id)}>{icon('plus',15)} Add a card</button>
</div></div>;})}
</div></section>
<footer className="footer"><span>Made for work that moves.</span><span>Harbor Studio · Website Relaunch</span></footer>
</div>
</main>
<div className="sr-only" aria-live="polite" aria-atomic="true">{announcement}</div>
{modal&&<div className="modal-backdrop" onMouseDown={e=>{if(e.target===e.currentTarget)closeModal();}}><div className="dialog" ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="dialog-title" onKeyDown={e=>{if(e.key==='Escape'){e.preventDefault();closeModal();}if(e.key==='Tab'){const nodes=Array.from(dialogRef.current?.querySelectorAll<HTMLElement>('button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled])')||[]).filter(n=>n.offsetParent!==null);if(nodes.length){const first=nodes[0],last=nodes[nodes.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}}}}}>
{modal.type==='delete'?<><div className="dialog-head"><div><span className="dialog-kicker">CONFIRM DELETION</span><h2 id="dialog-title">Delete this card?</h2></div><button className="icon-button" aria-label="Close dialog" onClick={closeModal}>{icon('close')}</button></div><p className="confirm-copy">“{board.cards.find(c=>c.id===modal.id)?.title}” will be removed from the board. You can undo this afterward.</p><div className="dialog-actions"><button className="subtle-button" onClick={closeModal}>Keep card</button><button className="danger-button" onClick={deleteCard}>{icon('trash')} Delete card</button></div></>:
draft&&<><div className="dialog-head"><div><span className="dialog-kicker">{modal.type==='create'?'NEW CARD':'CARD DETAILS'}</span><h2 id="dialog-title">{modal.type==='create'?`Add to ${board.columns.find(c=>c.id===modal.columnId)?.title}`:'Edit card'}</h2></div><button className="icon-button" aria-label="Close dialog" onClick={closeModal}>{icon('close')}</button></div><div className="dialog-body"><label className="field"><span>Title <em>*</em></span><input value={draft.title} onChange={e=>{setDraft({...draft,title:e.target.value});setError('');}} placeholder="What needs to be done?" aria-invalid={!!error&&error.includes('title')}/></label><label className="field"><span>Description</span><textarea rows={3} value={draft.description} onChange={e=>setDraft({...draft,description:e.target.value})} placeholder="Add context for your team..."/></label><div className="field-grid"><label className="field"><span>Assignee</span><select value={draft.assignee||''} onChange={e=>setDraft({...draft,assignee:e.target.value||null})}><option value="">Unassigned</option>{members.map(m=><option value={m.id} key={m.id}>{m.name}</option>)}</select></label><label className="field"><span>Due date</span><input type="date" value={draft.due||''} onChange={e=>setDraft({...draft,due:e.target.value||null})}/></label><label className="field"><span>Priority</span><select value={draft.priority} onChange={e=>setDraft({...draft,priority:e.target.value})}>{board.priorities.map(p=><option value={p} key={p}>{p[0].toUpperCase()+p.slice(1)}</option>)}</select></label><div className="field"><span>Status</span><div className="read-status">{modal.type==='create'?board.columns.find(c=>c.id===modal.columnId)?.title:board.columns[locationOf(board,draft.id)]?.title}</div></div></div><fieldset className="label-field"><legend>Labels</legend><div className="label-options">{board.labels.map(l=><label key={l.id} className={`label-choice tag-${l.id}`}><input type="checkbox" checked={draft.labels.includes(l.id)} onChange={e=>setDraft({...draft,labels:e.target.checked?[...draft.labels,l.id]:draft.labels.filter(x=>x!==l.id)})}/>{l.name}</label>)}</div></fieldset><div className="checklist-section"><div className="checklist-heading"><span>Checklist</span><span>{draft.checklist.filter(i=>i.done).length}/{draft.checklist.length} complete</span></div>{draft.checklist.map((item,i)=><div className="checklist-row" key={item.id}><input aria-label={`Mark ${item.text||'item'} complete`} type="checkbox" checked={item.done} onChange={e=>setDraft({...draft,checklist:draft.checklist.map((x,j)=>j===i?{...x,done:e.target.checked}:x)})}/><input aria-label={`Checklist item ${i+1}`} value={item.text} onChange={e=>setDraft({...draft,checklist:draft.checklist.map((x,j)=>j===i?{...x,text:e.target.value}:x)})}/><button aria-label={`Remove checklist item ${i+1}`} onClick={()=>setDraft({...draft,checklist:draft.checklist.filter((_,j)=>j!==i)})}>{icon('close',14)}</button></div>)}<button className="add-check" onClick={()=>setDraft({...draft,checklist:[...draft.checklist,{id:`item-${Date.now()}`,text:'',done:false}]})}>{icon('plus',14)} Add checklist item</button></div>{error&&<div className="form-error" role="alert">{error}</div>}</div><div className="dialog-actions">{modal.type==='detail'&&<button className="delete-link" onClick={()=>setModal({type:'delete',id:draft.id})}>{icon('trash',15)} Delete card</button>}<div className="action-spacer"/><button className="subtle-button" onClick={closeModal}>Cancel</button><button className="primary-button" onClick={saveDraft}>{modal.type==='create'?'Create card':'Save changes'}</button></div></>}
</div></div>}
</div>;
}
createRoot(document.getElementById('root')!).render(<App/>);