- 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.
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import type { CSSProperties, KeyboardEvent as RKeyboardEvent } from "react";
import { wouldExceedLimit } from "../boardState";
import { COLUMN_ACCENT, type BoardState } from "../model";
type Props = {
cardId: string;
cardTitle: string;
board: BoardState;
currentColId: string;
anchorRect: { left: number; top: number; bottom: number };
onMove: (colId: string) => void;
onClose: () => void;
};
function short(text: string, max = 22): string {
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
}
export function MoveMenu({ cardId, cardTitle, board, currentColId, anchorRect, onMove, onClose }: Props) {
const ref = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const w = el.offsetWidth;
const h = el.offsetHeight;
const vw = window.innerWidth;
const vh = window.innerHeight;
const left = Math.min(Math.max(8, anchorRect.left), vw - w - 8);
let top = anchorRect.bottom + 6;
if (top + h > vh - 8) top = Math.max(8, anchorRect.top - h - 6);
setPos({ top, left });
}, [anchorRect]);
useEffect(() => {
const first = ref.current?.querySelector<HTMLButtonElement>("button:not([disabled])");
first?.focus();
}, []);
function onKeyDown(e: RKeyboardEvent<HTMLDivElement>) {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
onClose();
return;
}
if (e.key === "Tab") {
onClose();
return;
}
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
const buttons = Array.from(ref.current?.querySelectorAll<HTMLButtonElement>("button:not([disabled])") ?? []);
if (buttons.length === 0) return;
const idx = buttons.findIndex(b => b === document.activeElement);
const next = e.key === "ArrowDown" ? (idx + 1) % buttons.length : (idx - 1 + buttons.length) % buttons.length;
buttons[next].focus();
}
}
const style: CSSProperties = pos
? { top: pos.top, left: pos.left, opacity: 1 }
: { top: 0, left: 0, opacity: 0, pointerEvents: "none" };
return (
<div
ref={ref}
className="move-menu"
role="menu"
aria-label={`Move “${cardTitle}” to another column`}
style={style}
onKeyDown={onKeyDown}
>
<p className="move-menu-title">
Move “{short(cardTitle)}” to
</p>
{board.columns
.filter(c => c.id !== currentColId)
.map(col => {
const blocked = wouldExceedLimit(board, col.id, cardId);
return (
<button key={col.id} type="button" role="menuitem" disabled={blocked} onClick={() => onMove(col.id)}>
<span className="menu-dot" style={{ background: COLUMN_ACCENT[col.id] ?? "#8A8F98" }} aria-hidden="true" />
<span className="menu-name">{col.title}</span>
<span className="menu-count">
{col.card_ids.length} {col.card_ids.length === 1 ? "card" : "cards"}
</span>
{blocked && <span className="menu-full">Full · WIP limit {col.wip_limit}</span>}
</button>
);
})}
</div>
);
}Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.