/* 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 (
);
}
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 });