/* board.jsx — Card, Column, Board with pointer drag-and-drop. Exported to window. */ function Tag({ kind, value }) { const { Ic, Store } = window; const meta = kind === "cat" ? Store.CATEGORIES[value] : Store.PRIORITIES[value]; if (!meta) return null; const I = meta.icon ? Ic[meta.icon] : null; return ( {I && }{meta.label} ); } function CardCover({ images, art, isStatic, onOpenImages }) { const { Ic, FX } = window; const stripRef = React.useRef(null); const [edge, setEdge] = React.useState({ left: false, right: false }); const realPhotos = images && images.length > 0; const list = realPhotos ? images : (art ? [art] : []); const stop = (e) => e.stopPropagation(); const openFull = (i, e) => { if (isStatic || !onOpenImages) return; if (e) e.stopPropagation(); FX.sfx.play("click"); onOpenImages(i); }; const upd = () => { const el = stripRef.current; if (!el) return; setEdge({ left: el.scrollLeft > 4, right: el.scrollLeft + el.clientWidth < el.scrollWidth - 4 }); }; React.useEffect(() => { const t = setTimeout(upd, 60); return () => clearTimeout(t); }, [list.length]); if (!list.length) return null; // single photo or themed art → one full cover (complete image, blurred fill) if (!realPhotos || list.length === 1) { return (
{realPhotos &&
} openFull(0, e)} />
); } // multiple photos → responsive horizontal thumbnail strip const scroll = (d, e) => { e.stopPropagation(); const el = stripRef.current; if (!el) return; el.scrollBy({ left: d * el.clientWidth * 0.8, behavior: "smooth" }); setTimeout(upd, 320); }; return (
{list.map((u, i) => ( ))}
{list.length} fotos
{!isStatic && edge.left && ( )} {!isStatic && edge.right && ( )}
); } function ProgressBar({ pct }) { const full = pct >= 100; return (
{pct}%
); } const Card = React.forwardRef(function Card( { task, pct, onPointerDown, onOpenImages, selected, popping, isStatic }, ref) { const { Ic, FX } = window; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onPointerDown && onPointerDown({ _kbd: true, currentTarget: e.currentTarget }); } }}>
{task.title}
{task.priority === "alta" && }
onOpenImages(task, i) : null} />
); }); function Congrats() { const { Ic } = window; return (

Tudo limpo!

Você zerou a coluna "A Fazer". Aproveite a vitória — ou crie a próxima missão.

); } function Board({ tasks, query, catFilter, prioFilter, onOpen, onOpenImages, onMove, onAdd, selectedId, popId }) { const { Store, FX } = window; const [drag, setDrag] = React.useState(null); const dragRef = React.useRef(null); const colBodies = React.useRef({}); const bumpRef = React.useRef({}); const [bumps, setBumps] = React.useState({}); // filtered + grouped const filtered = React.useMemo(() => { const q = query.trim().toLowerCase(); return tasks.filter(t => { if (q && !(t.title.toLowerCase().includes(q) || t.desc.toLowerCase().includes(q))) return false; if (catFilter && t.category !== catFilter) return false; if (prioFilter && t.priority !== prioFilter) return false; return true; }); }, [tasks, query, catFilter, prioFilter]); const byCol = React.useMemo(() => { const m = { todo: [], doing: [], done: [] }; filtered.forEach(t => m[t.status].push(t)); return m; }, [filtered]); // true counts (unfiltered) for headers const counts = React.useMemo(() => { const m = { todo: 0, doing: 0, done: 0 }; tasks.forEach(t => m[t.status]++); return m; }, [tasks]); // animate count bump when a column count changes const prevCounts = React.useRef(counts); React.useEffect(() => { Store.COLUMNS.forEach(c => { if (prevCounts.current[c.id] !== undefined && counts[c.id] > prevCounts.current[c.id]) { setBumps(b => ({ ...b, [c.id]: (b[c.id] || 0) + 1 })); setTimeout(() => setBumps(b => ({ ...b, [c.id]: 0 })), 420); } }); prevCounts.current = counts; }, [counts]); const filterActive = query.trim() || catFilter || prioFilter; /* ---- drag computation ---- */ const computeDrop = (px, py, dragId) => { let col = null; for (const c of Store.COLUMNS) { const el = colBodies.current[c.id]; if (!el) continue; const r = el.getBoundingClientRect(); if (px >= r.left && px <= r.right) { col = c.id; break; } } if (!col) { // nearest by x center let best = null, bestD = 1e9; for (const c of Store.COLUMNS) { const el = colBodies.current[c.id]; if (!el) continue; const r = el.getBoundingClientRect(); const cx = (r.left + r.right) / 2; const d = Math.abs(px - cx); if (d < bestD) { bestD = d; best = c.id; } } col = best; } const body = colBodies.current[col]; const cards = [...body.querySelectorAll("[data-card]")].filter(el => el.dataset.card !== dragId); let index = cards.length; for (let i = 0; i < cards.length; i++) { const r = cards[i].getBoundingClientRect(); if (py < r.top + r.height / 2) { index = i; break; } } return { col, index }; }; const onCardPointerDown = (e, task) => { if (e._kbd) { onOpen(task); return; } if (e.button !== undefined && e.button !== 0) return; const cardEl = e.currentTarget; const rect = cardEl.getBoundingClientRect(); const start = { x: e.clientX, y: e.clientY }; const offset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; dragRef.current = { id: task.id, task, started: false, start, offset, w: rect.width }; try { cardEl.setPointerCapture(e.pointerId); } catch (err) {} const move = (ev) => { const d = dragRef.current; if (!d) return; if (!d.started) { if (Math.hypot(ev.clientX - start.x, ev.clientY - start.y) < 6) return; d.started = true; FX.sfx.play("pick"); } const drop = computeDrop(ev.clientX, ev.clientY, d.id); setDrag({ id: d.id, task: d.task, w: d.w, gx: ev.clientX - d.offset.x, gy: ev.clientY - d.offset.y, col: drop.col, index: drop.index }); }; const up = (ev) => { const d = dragRef.current; window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); try { cardEl.releasePointerCapture(ev.pointerId); } catch (err) {} if (d && d.started) { const drop = computeDrop(ev.clientX, ev.clientY, d.id); onMove(d.id, drop.col, drop.index); FX.sfx.play("drop"); } else if (d) { onOpen(d.task); } dragRef.current = null; setDrag(null); }; window.addEventListener("pointermove", move); window.addEventListener("pointerup", up); }; return (
{Store.COLUMNS.map(col => { const items = byCol[col.id]; const showCongrats = col.id === "todo" && items.length === 0 && !filterActive; const isOver = drag && drag.col === col.id; return (
{col.title} {counts[col.id]}
(colBodies.current[col.id] = el)}> {showCongrats && } {items.map((t, i) => ( {isOver && drag.index === i &&
} onCardPointerDown(e, t)} /> ))} {isOver && drag.index >= items.length &&
} {!showCongrats && items.length === 0 && !drag && (
{filterActive ? "Nenhuma tarefa" : "Vazio"}
)}
); })}
{drag && }
); } function GhostCard({ drag }) { const { Store } = window; return (
{drag.task.title}
{drag.task.priority === "alta" && }
); } Object.assign(window, { Board, Card, Tag, ProgressBar, Congrats, GhostCard });