- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.1.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 { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import type { BoardState, CardData, Member, UndoEntry } from "./types";
import {
buildInitialState,
buildMembers,
buildMeta,
bundledMembers,
bundledMeta,
parseStoredBoard,
} from "./data";
import {
EMPTY_FILTERS,
type Filters,
UNASSIGNED,
applyMove,
applyPatch,
finalIndexAfterRemoval,
findColumnOf,
filtersActive,
getColumn,
insertIndexForVisibleDrop,
isCardOverdue,
isColumnFull,
newCardId,
visibleCardIds,
} from "./utils";
import { ColumnView } from "./components/ColumnView";
import type { CardCommon, ComposerState } from "./components/ColumnView";
import { CardItem } from "./components/CardItem";
import { CardDialog } from "./components/CardDialog";
import { MoveMenu } from "./components/MoveMenu";
import {
IconAlert,
IconBoard,
IconChevronDown,
IconClose,
IconInfo,
IconMove,
IconReset,
IconSearch,
IconUndo,
LogoMark,
} from "./icons";
const STORAGE_KEY = "boardly.board.v1";
interface PendingDrag {
cardId: string;
fromColumnId: string;
fromIndex: number;
startX: number;
startY: number;
offsetX: number;
offsetY: number;
width: number;
height: number;
pointerId: number;
active: boolean;
}
interface ActiveDrag {
cardId: string;
fromColumnId: string;
fromIndex: number;
width: number;
height: number;
}
interface DropTarget {
columnId: string;
index: number;
}
interface Pickup {
cardId: string;
originColumnId: string;
originIndex: number;
snapshot: BoardState;
grabbed: boolean;
undoDepth: number;
}
interface Toast {
id: number;
message: string;
tone: "info" | "warning" | "success";
}
function loadStoredBoard(): BoardState | null {
try {
return parseStoredBoard(window.localStorage.getItem(STORAGE_KEY));
} catch {
return null;
}
}
export default function App() {
const [meta, setMeta] = useState(() => buildMeta(bundledMeta));
const [members, setMembers] = useState<Member[]>(() => bundledMembers);
const [board, setBoard] = useState<BoardState>(() => loadStoredBoard() ?? buildInitialState());
const [undoStack, setUndoStack] = useState<UndoEntry[]>([]);
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
const [dialogCardId, setDialogCardId] = useState<string | null>(null);
const [moveMenu, setMoveMenu] = useState<{ cardId: string; anchor: HTMLElement } | null>(null);
const [composer, setComposer] = useState<ComposerState | null>(null);
const [activeDrag, setActiveDrag] = useState<ActiveDrag | null>(null);
const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
const [pickup, setPickup] = useState<Pickup | null>(null);
const [toasts, setToasts] = useState<Toast[]>([]);
const [politeMessage, setPoliteMessage] = useState("");
const [alertMessage, setAlertMessage] = useState("");
const [blockedFlashColumn, setBlockedFlashColumn] = useState<string | null>(null);
const boardRef = useRef(board);
boardRef.current = board;
const initialBoardRef = useRef<BoardState | null>(null);
if (initialBoardRef.current === null) initialBoardRef.current = board;
const undoStackRef = useRef(undoStack);
undoStackRef.current = undoStack;
const pickupRef = useRef<Pickup | null>(null);
const activeDragRef = useRef<ActiveDrag | null>(null);
const dropTargetRef = useRef<DropTarget | null>(null);
const pendingRef = useRef<PendingDrag | null>(null);
const suppressClickRef = useRef(false);
const refocusRef = useRef<string | null>(null);
const announceTickRef = useRef(0);
const toastIdRef = useRef(0);
const rafRef = useRef<number | undefined>(undefined);
const ghostPosRef = useRef({ x: 0, y: 0 });
const finishDragRef = useRef<(commit: boolean) => void>(() => {});
const undoRef = useRef<() => void>(() => {});
const storedAtMountRef = useRef<boolean>(false);
const flashTimerRef = useRef<number | undefined>(undefined);
const ghostRef = useRef<HTMLElement | null>(null);
const boardRegionRef = useRef<HTMLElement | null>(null);
const boardScrollRef = useRef<HTMLDivElement | null>(null);
const cardElsRef = useRef(new Map<string, HTMLLIElement>());
const columnElsRef = useRef(new Map<string, HTMLElement>());
const listElsRef = useRef(new Map<string, HTMLUListElement>());
const addButtonElsRef = useRef(new Map<string, HTMLButtonElement>());
const dialogReturnFocusRef = useRef<HTMLElement | null>(null);
const announce = useCallback((message: string) => {
announceTickRef.current = (announceTickRef.current + 1) % 5;
setPoliteMessage(message + "\u200B".repeat(announceTickRef.current));
}, []);
const raiseAlert = useCallback((message: string) => {
announceTickRef.current = (announceTickRef.current + 1) % 5;
setAlertMessage(message + "\u200B".repeat(announceTickRef.current));
}, []);
const pushToast = useCallback((message: string, tone: Toast["tone"] = "info") => {
toastIdRef.current += 1;
const id = toastIdRef.current;
setToasts((list) => [...list.slice(-2), { id, message, tone }]);
window.setTimeout(() => {
setToasts((list) => list.filter((toast) => toast.id !== id));
}, 5200);
}, []);
const flashColumn = useCallback((columnId: string) => {
setBlockedFlashColumn(columnId);
if (flashTimerRef.current) window.clearTimeout(flashTimerRef.current);
flashTimerRef.current = window.setTimeout(() => setBlockedFlashColumn(null), 1800);
}, []);
useEffect(() => {
storedAtMountRef.current = loadStoredBoard() !== null;
}, []);
useEffect(() => {
if (storedAtMountRef.current) return;
let cancelled = false;
const base = import.meta.env.BASE_URL || "/";
Promise.all([
fetch(`${base}data/board.json`)
.then((response) => (response.ok ? response.json() : null))
.catch(() => null),
fetch(`${base}data/members.json`)
.then((response) => (response.ok ? response.json() : null))
.catch(() => null),
]).then(([boardRaw, membersRaw]) => {
if (cancelled) return;
if (boardRef.current !== initialBoardRef.current) return;
if (boardRaw && Array.isArray(boardRaw.columns) && Array.isArray(boardRaw.cards)) {
setBoard(buildInitialState(boardRaw));
setMeta(buildMeta(boardRaw));
}
if (membersRaw) setMembers(buildMembers(membersRaw));
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(board));
} catch {
}
}, [board]);
const memberById = useMemo(() => new Map(members.map((member) => [member.id, member])), [members]);
const memberIndexById = useMemo(
() => new Map(members.map((member, index) => [member.id, index])),
[members],
);
const labelName = useCallback(
(id: string) => meta.labels.find((label) => label.id === id)?.name ?? id,
[meta.labels],
);
const visibleByColumn = useMemo(() => {
const map: Record<string, string[]> = {};
for (const column of board.columns) map[column.id] = visibleCardIds(board, filters, column);
return map;
}, [board, filters]);
const totalVisible = useMemo(
() => Object.values(visibleByColumn).reduce((sum, ids) => sum + ids.length, 0),
[visibleByColumn],
);
const totalCards = Object.keys(board.cards).length;
const isFiltering = filtersActive(filters);
const pushUndo = useCallback((label: string) => {
const snapshot = boardRef.current;
setUndoStack((stack) => [...stack.slice(-49), { state: snapshot, label }]);
}, []);
const performUndo = useCallback(() => {
const stack = undoStackRef.current;
if (stack.length === 0) return;
const entry = stack[stack.length - 1];
setBoard(entry.state);
setUndoStack(stack.slice(0, -1));
pushToast(`Undid last ${entry.label}.`, "info");
announce(`Undo: the board was restored to before the last ${entry.label}.`);
}, [announce, pushToast]);
undoRef.current = performUndo;
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
const isUndo =
(event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "z";
if (!isUndo) return;
const target = event.target as HTMLElement | null;
if (target && target.closest("input, textarea, select, [contenteditable='true']")) return;
event.preventDefault();
undoRef.current();
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
const resetDemo = () => {
setBoard(buildInitialState());
setUndoStack([]);
setFilters(EMPTY_FILTERS);
setComposer(null);
setDialogCardId(null);
setMoveMenu(null);
try {
window.localStorage.removeItem(STORAGE_KEY);
} catch {
}
announce("Demo reset. The board is back to its original state.");
pushToast("Demo reset — the original board is back.", "success");
};
const blockMoveMessage = (cardTitle: string, columnId: string): string => {
const column = getColumn(board, columnId);
const limit = column?.wip_limit ?? 0;
return `Can't move “${cardTitle}” to ${column?.title ?? "that column"} — that column is at its WIP limit of ${limit} cards. Move or finish a card there first.`;
};
const doMove = (
cardId: string,
toColumnId: string,
toIndex: number,
via: "drag" | "keyboard" | "control" | "dialog",
): boolean => {
const location = findColumnOf(board, cardId);
if (!location) return false;
if (location.column.id === toColumnId && location.index === toIndex) return true;
const card = board.cards[cardId];
const title = card?.title ?? "This card";
if (location.column.id !== toColumnId && isColumnFull(board, toColumnId)) {
const message = blockMoveMessage(title, toColumnId);
raiseAlert(message);
pushToast(message, "warning");
flashColumn(toColumnId);
return false;
}
pushUndo("move");
const next = applyMove(board, cardId, toColumnId, toIndex);
setBoard(next);
refocusRef.current = cardId;
const finalIndex = finalIndexAfterRemoval(board, cardId, toColumnId, toIndex);
const column = getColumn(next, toColumnId);
if (via === "keyboard") {
announce(
`Moved “${title}” to ${column?.title}, position ${finalIndex + 1} of ${
column?.card_ids.length ?? 0
}. Space drops the card; Escape cancels and restores the original position.`,
);
} else {
announce(`Moved “${title}” to ${column?.title}, position ${finalIndex + 1}.`);
}
return true;
};
const createCard = (columnId: string, rawTitle: string): boolean => {
const title = rawTitle.trim();
const column = getColumn(board, columnId);
if (!column || !title) return false;
if (isColumnFull(board, columnId)) {
const message = `Can't add a card to ${column.title} — the column is at its WIP limit of ${column.wip_limit} cards. Move or finish a card there first.`;
raiseAlert(message);
pushToast(message, "warning");
flashColumn(columnId);
return false;
}
pushUndo("card creation");
const id = newCardId();
const card: CardData = {
id,
title,
description: "",
assignee: null,
labels: [],
priority: "medium",
due: null,
checklist: [],
};
setBoard({
columns: board.columns.map((item) =>
item.id === columnId ? { ...item, card_ids: [...item.card_ids, id] } : item,
),
cards: { ...board.cards, [id]: card },
});
announce(`Added “${title}” to ${column.title}.`);
pushToast(`“${title}” added to ${column.title}.`, "success");
return true;
};
const deleteCard = (cardId: string) => {
const card = board.cards[cardId];
const location = findColumnOf(board, cardId);
if (!card || !location) return;
pushUndo("card deletion");
setBoard({
columns: board.columns.map((column) => ({
...column,
card_ids: column.card_ids.filter((id) => id !== cardId),
})),
cards: Object.fromEntries(Object.entries(board.cards).filter(([id]) => id !== cardId)),
});
announce(`Deleted “${card.title}” from ${location.column.title}. Use Undo to restore it.`);
pushToast(`“${card.title}” deleted — you can undo this.`, "info");
};
const updateCardField = useCallback((cardId: string, patch: Partial<CardData>) => {
setBoard((state) => applyPatch(state, cardId, patch));
}, []);
const moveCardViaDialog = (cardId: string, toColumnId: string): string | null => {
const location = findColumnOf(board, cardId);
if (!location) return null;
if (location.column.id === toColumnId) return null;
const target = getColumn(board, toColumnId);
if (!target) return null;
const title = board.cards[cardId]?.title ?? "This card";
if (isColumnFull(board, toColumnId)) {
const message = `${target.title} is at its WIP limit of ${target.wip_limit} cards. Move or finish a card there before moving this one in.`;
raiseAlert(`Can't move “${title}” to ${target.title} — the WIP limit is ${target.wip_limit}.`);
return message;
}
pushUndo("move");
setBoard(applyMove(board, cardId, toColumnId, target.card_ids.length));
announce(`Moved “${title}” to ${target.title}.`);
pushToast(`“${title}” moved to ${target.title}.`, "info");
return null;
};
const openComposer = (columnId: string) => {
if (isColumnFull(board, columnId)) {
const column = getColumn(board, columnId);
const message = `${column?.title ?? "That column"} is at its WIP limit of ${
column?.wip_limit
} cards. Move or finish a card there first.`;
raiseAlert(message);
pushToast(message, "warning");
flashColumn(columnId);
return;
}
setComposer({ columnId, value: "", error: null });
};
const submitComposer = () => {
if (!composer) return;
const title = composer.value.trim();
const column = getColumn(board, composer.columnId);
if (!column) {
setComposer(null);
return;
}
if (!title) {
setComposer({ ...composer, error: "Enter a title before adding the card." });
raiseAlert("No card was added. A title is required.");
return;
}
if (isColumnFull(board, composer.columnId)) {
const message = `Can't add a card to ${column.title} — the column is at its WIP limit of ${column.wip_limit} cards. Move or finish a card there first.`;
setComposer({ ...composer, error: message });
raiseAlert(message);
return;
}
createCard(composer.columnId, title);
const willBeFull = column.wip_limit !== null && column.card_ids.length + 1 >= column.wip_limit;
if (willBeFull) setComposer(null);
else setComposer({ columnId: composer.columnId, value: "", error: null });
};
const openDetails = useCallback((cardId: string) => {
if (pickupRef.current) {
pickupRef.current = null;
setPickup(null);
}
dialogReturnFocusRef.current = cardElsRef.current.get(cardId) ?? null;
setDialogCardId(cardId);
setMoveMenu(null);
}, []);
const closeDialog = useCallback((fallback?: HTMLElement | null) => {
setDialogCardId(null);
const target = fallback ?? dialogReturnFocusRef.current;
dialogReturnFocusRef.current = null;
window.requestAnimationFrame(() => {
if (target && document.contains(target)) target.focus();
else boardRegionRef.current?.focus();
});
}, []);
const handleDialogDelete = (cardId: string) => {
const location = findColumnOf(boardRef.current, cardId);
const fallback = location ? addButtonElsRef.current.get(location.column.id) ?? null : null;
deleteCard(cardId);
closeDialog(fallback);
};
const setPickupState = (value: Pickup | null) => {
pickupRef.current = value;
setPickup(value);
};
const beginPickup = (cardId: string, grabbed: boolean): Pickup | null => {
const location = findColumnOf(board, cardId);
if (!location) return null;
const next: Pickup = {
cardId,
originColumnId: location.column.id,
originIndex: location.index,
snapshot: board,
grabbed,
undoDepth: undoStack.length,
};
setPickupState(next);
if (grabbed) {
announce(
`Picked up “${board.cards[cardId]?.title}”. Use the arrow keys to move within the column or to an adjacent column. Space drops the card, Escape cancels.`,
);
}
return next;
};
const dropPickup = () => {
const current = pickupRef.current;
if (!current) return;
setPickupState(null);
const location = findColumnOf(board, current.cardId);
const column = location ? getColumn(board, location.column.id) : null;
announce(
`Dropped “${board.cards[current.cardId]?.title}” in ${column?.title}, position ${
(location?.index ?? 0) + 1
}.`,
);
};
const cancelPickup = (silent: boolean) => {
const current = pickupRef.current;
if (!current) return;
setBoard(current.snapshot);
setUndoStack((stack) => stack.slice(0, current.undoDepth));
refocusRef.current = current.cardId;
setPickupState(null);
if (!silent) {
const column = getColumn(current.snapshot, current.originColumnId);
announce(
`Cancelled. “${current.snapshot.cards[current.cardId]?.title}” returned to ${
column?.title
}, position ${current.originIndex + 1}.`,
);
}
};
const moveByKey = (cardId: string, key: string) => {
const location = findColumnOf(board, cardId);
if (!location) return;
const title = board.cards[cardId]?.title ?? "This card";
if (key === "ArrowUp" || key === "ArrowDown") {
const direction = key === "ArrowUp" ? -1 : 1;
const nextIndex = location.index + direction;
if (nextIndex < 0 || nextIndex >= location.column.card_ids.length) {
announce(
`“${title}” is already at the ${direction < 0 ? "top" : "bottom"} of ${
location.column.title
}.`,
);
return;
}
doMove(cardId, location.column.id, nextIndex, "keyboard");
return;
}
const columnIndex = board.columns.findIndex((column) => column.id === location.column.id);
const direction = key === "ArrowLeft" ? -1 : 1;
const target = board.columns[columnIndex + direction];
if (!target) {
announce(
`“${title}” is already in the ${direction < 0 ? "first" : "last"} column, ${
location.column.title
}.`,
);
return;
}
if (isColumnFull(board, target.id)) {
const message = blockMoveMessage(title, target.id);
raiseAlert(message);
pushToast(message, "warning");
flashColumn(target.id);
return;
}
const insertAt = Math.min(location.index, target.card_ids.length);
doMove(cardId, target.id, insertAt, "keyboard");
};
const handleCardKeyDown = (event: React.KeyboardEvent<HTMLLIElement>, cardId: string) => {
if (event.target !== event.currentTarget) return;
const key = event.key;
if (key === "Enter") {
event.preventDefault();
openDetails(cardId);
return;
}
if (key === " ") {
event.preventDefault();
if (pickupRef.current?.cardId === cardId) dropPickup();
else beginPickup(cardId, true);
return;
}
if (key === "Escape") {
if (pickupRef.current?.cardId === cardId) {
event.preventDefault();
cancelPickup(false);
}
return;
}
if (key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight") {
event.preventDefault();
if (pickupRef.current?.cardId !== cardId) beginPickup(cardId, false);
moveByKey(cardId, key);
}
};
const handleCardFocus = (cardId: string) => {
const current = pickupRef.current;
if (current && current.cardId !== cardId) setPickupState(null);
};
const computeDropTarget = (x: number, y: number, draggedId: string): DropTarget | null => {
let hovered: string | null = null;
let nearest: { id: string; dist: number } | null = null;
for (const column of board.columns) {
const columnEl = columnElsRef.current.get(column.id);
const listEl = listElsRef.current.get(column.id);
if (!columnEl || !listEl) continue;
const rect = columnEl.getBoundingClientRect();
const inside =
x >= rect.left - 6 && x <= rect.right + 6 && y >= rect.top - 8 && y <= rect.bottom + 14;
if (inside) {
hovered = column.id;
break;
}
const center = rect.left + rect.width / 2;
const dist = Math.abs(x - center);
if (!nearest || dist < nearest.dist) nearest = { id: column.id, dist };
}
const columnId = hovered ?? nearest?.id ?? null;
if (!columnId) return null;
const listEl = listElsRef.current.get(columnId);
let index = 0;
if (listEl) {
const nodes = listEl.querySelectorAll<HTMLElement>("[data-card-id]");
nodes.forEach((node) => {
if (node.dataset.cardId === draggedId) return;
const rect = node.getBoundingClientRect();
if (y > rect.top + rect.height / 2) index += 1;
});
}
return { columnId, index };
};
const positionGhost = (x: number, y: number) => {
const pending = pendingRef.current;
if (!pending) return;
ghostPosRef.current = { x: x - pending.offsetX, y: y - pending.offsetY };
if (rafRef.current !== undefined) return;
rafRef.current = window.requestAnimationFrame(() => {
rafRef.current = undefined;
const ghost = ghostRef.current;
if (!ghost) return;
const { x: gx, y: gy } = ghostPosRef.current;
ghost.style.transform = `translate3d(${gx}px, ${gy}px, 0) rotate(1.6deg)`;
});
};
const autoScroll = (x: number, y: number, target: DropTarget | null) => {
const scroller = boardScrollRef.current;
if (scroller) {
const rect = scroller.getBoundingClientRect();
const margin = 70;
if (x < rect.left + margin) scroller.scrollLeft -= 14;
else if (x > rect.right - margin) scroller.scrollLeft += 14;
if (y < rect.top + 40) scroller.scrollTop -= 12;
else if (y > rect.bottom - 40) scroller.scrollTop += 12;
}
if (target) {
const listEl = listElsRef.current.get(target.columnId);
if (listEl && listEl.scrollHeight > listEl.clientHeight + 4) {
const rect = listEl.getBoundingClientRect();
if (y < rect.top + 44) listEl.scrollTop -= 12;
else if (y > rect.bottom - 44) listEl.scrollTop += 12;
}
}
};
const handleWindowPointerMove = (event: PointerEvent) => {
const pending = pendingRef.current;
if (!pending || event.pointerId !== pending.pointerId) return;
if (!pending.active) {
const distance = Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY);
if (distance < 6) return;
pending.active = true;
suppressClickRef.current = true;
document.body.classList.add("is-dragging");
if (pickupRef.current) cancelPickup(true);
const drag: ActiveDrag = {
cardId: pending.cardId,
fromColumnId: pending.fromColumnId,
fromIndex: pending.fromIndex,
width: pending.width,
height: pending.height,
};
activeDragRef.current = drag;
setActiveDrag(drag);
const initialTarget: DropTarget = {
columnId: pending.fromColumnId,
index: pending.fromIndex,
};
dropTargetRef.current = initialTarget;
setDropTarget(initialTarget);
positionGhost(event.clientX, event.clientY);
}
positionGhost(event.clientX, event.clientY);
const nextTarget = computeDropTarget(event.clientX, event.clientY, pending.cardId);
if (
nextTarget &&
(!dropTargetRef.current ||
dropTargetRef.current.columnId !== nextTarget.columnId ||
dropTargetRef.current.index !== nextTarget.index)
) {
dropTargetRef.current = nextTarget;
setDropTarget(nextTarget);
}
autoScroll(event.clientX, event.clientY, nextTarget);
};
const handleWindowPointerUp = () => {
finishDragRef.current(true);
};
const handleWindowPointerCancel = () => {
finishDragRef.current(false);
};
const finishPointerDrag = (commit: boolean) => {
window.removeEventListener("pointermove", handleWindowPointerMove);
window.removeEventListener("pointerup", handleWindowPointerUp);
window.removeEventListener("pointercancel", handleWindowPointerCancel);
const pending = pendingRef.current;
pendingRef.current = null;
if (rafRef.current !== undefined) {
window.cancelAnimationFrame(rafRef.current);
rafRef.current = undefined;
}
document.body.classList.remove("is-dragging");
const drag = activeDragRef.current;
const target = dropTargetRef.current;
activeDragRef.current = null;
dropTargetRef.current = null;
setActiveDrag(null);
setDropTarget(null);
if (pending && pending.active && drag) {
if (commit && target) {
const insertIndex = insertIndexForVisibleDrop(
board,
filters,
target.columnId,
target.index,
drag.cardId,
);
doMove(drag.cardId, target.columnId, insertIndex, "drag");
} else if (!commit) {
const location = findColumnOf(board, drag.cardId);
announce(
`Cancelled the drag. “${board.cards[drag.cardId]?.title}” stayed in ${
location?.column.title
}.`,
);
}
}
window.setTimeout(() => {
suppressClickRef.current = false;
}, 0);
};
finishDragRef.current = finishPointerDrag;
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && activeDragRef.current) {
event.preventDefault();
finishDragRef.current(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
const handleCardPointerDown = (event: React.PointerEvent<HTMLLIElement>, cardId: string) => {
if (event.button !== 0) return;
const target = event.target as HTMLElement;
if (target.closest("button, a, input, select, textarea, [data-no-drag]")) return;
if (event.pointerType === "touch" && !target.closest("[data-drag-handle]")) return;
const element = event.currentTarget;
const location = findColumnOf(board, cardId);
if (!location) return;
element.focus({ preventScroll: true });
const rect = element.getBoundingClientRect();
pendingRef.current = {
cardId,
fromColumnId: location.column.id,
fromIndex: location.index,
startX: event.clientX,
startY: event.clientY,
offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top,
width: rect.width,
height: rect.height,
pointerId: event.pointerId,
active: false,
};
window.addEventListener("pointermove", handleWindowPointerMove);
window.addEventListener("pointerup", handleWindowPointerUp);
window.addEventListener("pointercancel", handleWindowPointerCancel);
};
const suppressClicks = useCallback(() => {
if (suppressClickRef.current) {
suppressClickRef.current = false;
return true;
}
return false;
}, []);
useLayoutEffect(() => {
const cardId = refocusRef.current;
if (!cardId) return;
refocusRef.current = null;
const element = cardElsRef.current.get(cardId);
if (element && document.activeElement !== element) {
element.focus({ preventScroll: true });
}
}, [board]);
const registerCardEl = useCallback((cardId: string, el: HTMLLIElement | null) => {
if (el) cardElsRef.current.set(cardId, el);
else cardElsRef.current.delete(cardId);
}, []);
const registerColumnEl = useCallback((columnId: string, el: HTMLElement | null) => {
if (el) columnElsRef.current.set(columnId, el);
else columnElsRef.current.delete(columnId);
}, []);
const registerListEl = useCallback((columnId: string, el: HTMLUListElement | null) => {
if (el) listElsRef.current.set(columnId, el);
else listElsRef.current.delete(columnId);
}, []);
const registerAddButtonEl = useCallback((columnId: string, el: HTMLButtonElement | null) => {
if (el) addButtonElsRef.current.set(columnId, el);
else addButtonElsRef.current.delete(columnId);
}, []);
const cardCommon: CardCommon = {
memberById,
memberIndexById,
labelName,
suppressClicks,
onOpen: openDetails,
onKeyDown: handleCardKeyDown,
onPointerDown: handleCardPointerDown,
onFocusCard: handleCardFocus,
onMoveClick: (cardId, anchor) => setMoveMenu({ cardId, anchor }),
registerCardEl,
draggedCardId: activeDrag?.cardId ?? null,
grabbedCardId: pickup?.cardId ?? null,
overdueOf: (card, columnId) => isCardOverdue(board, card, columnId, meta.today),
};
const clearFilters = () => setFilters(EMPTY_FILTERS);
const moveMenuCard = moveMenu ? board.cards[moveMenu.cardId] ?? null : null;
const dialogCard = dialogCardId ? board.cards[dialogCardId] ?? null : null;
const dialogColumn = dialogCardId ? findColumnOf(board, dialogCardId) : null;
const keyboardHintCard = pickup ? board.cards[pickup.cardId] : null;
const draggedCard = activeDrag ? board.cards[activeDrag.cardId] ?? null : null;
return (
<div className="app">
<header className="app-header">
<div className="brand" aria-label={meta.product}>
<LogoMark size={28} />
<span className="brand-name">{meta.product}</span>
</div>
<div className="header-divider" aria-hidden="true" />
<div className="workspace-chip">
<IconBoard size={15} />
<span className="workspace-name">{meta.workspace}</span>
</div>
<div className="header-spacer" />
<div className="header-actions">
<button
type="button"
className="btn ghost small has-icon"
onClick={performUndo}
disabled={undoStack.length === 0}
title={
undoStack.length > 0
? `Undo last ${undoStack[undoStack.length - 1].label} (Ctrl+Z)`
: "Nothing to undo yet"
}
>
<IconUndo size={15} />
<span>Undo</span>
</button>
<button
type="button"
className="btn ghost small has-icon"
onClick={resetDemo}
title="Restore the original demo board"
>
<IconReset size={15} />
<span>Reset demo</span>
</button>
</div>
</header>
<div className="toolbar">
<div className="toolbar-title">
<h1>{meta.board}</h1>
<p className="toolbar-meta">
{isFiltering
? `${totalVisible} of ${totalCards} cards match`
: `${totalCards} cards across ${board.columns.length} columns`}
{isFiltering && (
<button type="button" className="link-button" onClick={clearFilters}>
Clear filters
</button>
)}
</p>
</div>
<div className="filter-bar" role="search" aria-label="Filter cards">
<div className="search-field">
<IconSearch size={15} className="search-icon" />
<input
type="search"
className="search-input"
placeholder="Search cards…"
aria-label="Search cards by title or description"
value={filters.query}
onChange={(event) =>
setFilters((current) => ({ ...current, query: event.target.value }))
}
onKeyDown={(event) => {
if (event.key === "Escape" && filters.query) {
event.preventDefault();
event.stopPropagation();
setFilters((current) => ({ ...current, query: "" }));
}
}}
/>
{filters.query !== "" && (
<button
type="button"
className="search-clear"
aria-label="Clear search text"
onClick={() => setFilters((current) => ({ ...current, query: "" }))}
>
<IconClose size={13} />
</button>
)}
</div>
<label className="select-wrap filter-select">
<span className="sr-only">Filter by assignee</span>
<select
aria-label="Filter by assignee"
value={filters.assignee}
onChange={(event) =>
setFilters((current) => ({ ...current, assignee: event.target.value }))
}
>
<option value="all">All assignees</option>
<option value={UNASSIGNED}>Unassigned</option>
{members.map((member) => (
<option key={member.id} value={member.id}>
{member.name}
</option>
))}
</select>
<IconChevronDown size={14} className="select-caret" />
</label>
<label className="select-wrap filter-select">
<span className="sr-only">Filter by label</span>
<select
aria-label="Filter by label"
value={filters.label}
onChange={(event) =>
setFilters((current) => ({ ...current, label: event.target.value }))
}
>
<option value="all">All labels</option>
{meta.labels.map((label) => (
<option key={label.id} value={label.id}>
{label.name}
</option>
))}
</select>
<IconChevronDown size={14} className="select-caret" />
</label>
{isFiltering && (
<button type="button" className="btn ghost small clear-filters" onClick={clearFilters}>
Clear filters
</button>
)}
</div>
</div>
<main
className="board-region"
ref={boardRegionRef}
tabIndex={-1}
aria-label={`${meta.board} board`}
>
{pickup && keyboardHintCard && (
<div className="keyboard-hint">
<IconMove size={14} />
<span>
Moving <strong>“{keyboardHintCard.title}”</strong> — arrow keys position it,
<kbd>Space</kbd> drops it, <kbd>Esc</kbd> cancels.
</span>
</div>
)}
<div className="board-scroll" ref={boardScrollRef}>
<div className="board-grid">
{board.columns.map((column) => {
const visibleIds = visibleByColumn[column.id] ?? [];
const cards = visibleIds
.map((id) => board.cards[id])
.filter((card): card is CardData => Boolean(card));
const isTarget = activeDrag !== null && dropTarget?.columnId === column.id;
let renderIndex = isTarget && dropTarget ? dropTarget.index : null;
if (renderIndex !== null && activeDrag && activeDrag.fromColumnId === column.id) {
const draggedVisibleIndex = cards.findIndex(
(card) => card.id === activeDrag.cardId,
);
if (draggedVisibleIndex !== -1 && draggedVisibleIndex < renderIndex) {
renderIndex += 1;
}
}
const blocked =
isTarget &&
activeDrag !== null &&
activeDrag.fromColumnId !== column.id &&
isColumnFull(board, column.id);
return (
<ColumnView
key={column.id}
column={column}
cards={cards}
totalCount={column.card_ids.length}
filtersOn={isFiltering}
isFull={isColumnFull(board, column.id)}
isDropTarget={isTarget}
dropIndex={renderIndex}
dropBlocked={Boolean(blocked)}
dropHeight={activeDrag?.height ?? 92}
isBlockedFlash={blockedFlashColumn === column.id}
cardCommon={cardCommon}
composer={composer}
onComposerOpen={openComposer}
onComposerChange={(value) =>
setComposer((current) =>
current ? { ...current, value, error: null } : current,
)
}
onComposerSubmit={submitComposer}
onComposerCancel={() => {
const columnId = composer?.columnId;
setComposer(null);
if (columnId) addButtonElsRef.current.get(columnId)?.focus();
}}
registerColumnEl={registerColumnEl}
registerListEl={registerListEl}
registerAddButtonEl={registerAddButtonEl}
/>
);
})}
</div>
</div>
{isFiltering && totalVisible === 0 && (
<div className="empty-state" role="status">
<span className="empty-state-icon">
<IconSearch size={26} />
</span>
<h2>No cards match these filters</h2>
<p>Try a different search term, or loosen the assignee and label filters.</p>
<button type="button" className="btn primary" onClick={clearFilters}>
Clear all filters
</button>
</div>
)}
</main>
<footer className="app-footer">
<p className="fictional-notice">{meta.fictional_notice}</p>
<p className="footer-note">
Local demo — the board is saved in this browser only, and never leaves it.
</p>
</footer>
<div className="sr-only" role="status" aria-live="polite" aria-atomic="true">
{politeMessage}
</div>
<div className="sr-only" role="alert" aria-live="assertive" aria-atomic="true">
{alertMessage}
</div>
<div className="toast-region" aria-hidden="true">
{toasts.map((toast) => (
<div key={toast.id} className={`toast toast-${toast.tone}`}>
<span className="toast-icon">
{toast.tone === "warning" ? <IconAlert size={15} /> : <IconInfo size={15} />}
</span>
<span>{toast.message}</span>
</div>
))}
</div>
{activeDrag && draggedCard && (
<ul
className="drag-ghost"
ref={(el) => {
ghostRef.current = el;
}}
style={{
width: activeDrag.width,
transform: `translate3d(${ghostPosRef.current.x}px, ${ghostPosRef.current.y}px, 0) rotate(1.6deg)`,
}}
>
<CardItem
variant="ghost"
card={draggedCard}
columnTitle=""
member={draggedCard.assignee ? memberById.get(draggedCard.assignee) ?? null : null}
memberIndex={0}
labelName={labelName}
overdue={isCardOverdue(board, draggedCard, activeDrag.fromColumnId, meta.today)}
isDragged={false}
isGrabbed={false}
suppressClicks={suppressClicks}
onOpen={() => {}}
onKeyDown={() => {}}
onPointerDown={() => {}}
onMoveClick={() => {}}
registerEl={() => {}}
/>
</ul>
)}
{moveMenu && moveMenuCard && (
<MoveMenu
card={moveMenuCard}
columns={board.columns}
cardColumnId={findColumnOf(board, moveMenu.cardId)?.column.id ?? ""}
isColumnFull={(columnId) => isColumnFull(board, columnId)}
anchor={moveMenu.anchor}
onClose={() => {
const anchor = moveMenu.anchor;
setMoveMenu(null);
window.requestAnimationFrame(() => anchor.focus());
}}
onChoose={(columnId) => {
const activeCardId = moveMenu.cardId;
const target = getColumn(board, columnId);
setMoveMenu(null);
if (!target) return;
const moved = doMove(activeCardId, columnId, target.card_ids.length, "control");
if (!moved) {
window.requestAnimationFrame(() => cardElsRef.current.get(activeCardId)?.focus());
}
}}
/>
)}
{dialogCard && dialogColumn && (
<CardDialog
card={dialogCard}
columnId={dialogColumn.column.id}
columns={board.columns}
members={members}
labels={meta.labels}
isColumnFull={(columnId) => isColumnFull(board, columnId)}
onChange={(patch) => updateCardField(dialogCard.id, patch)}
onMoveColumn={(columnId) => moveCardViaDialog(dialogCard.id, columnId)}
onDelete={() => handleDialogDelete(dialogCard.id)}
onClose={() => closeDialog()}
/>
)}
</div>
);
}