diff --git a/web/app.jsx b/web/app.jsx
index dacef73..262c741 100644
--- a/web/app.jsx
+++ b/web/app.jsx
@@ -682,6 +682,8 @@ function App() {
},
});
},
+
+ archiveItem: async (id) => { try { await flushPendingSave(); } catch (e) { showToast('Save failed: ' + e.message, 'err'); return; } handleArchiveItems([id]); },
};
// Which project does a NEW item belong to? An explicit hint (dialog select
diff --git a/web/styles.css b/web/styles.css
index 54de842..63c2207 100644
--- a/web/styles.css
+++ b/web/styles.css
@@ -1488,3 +1488,26 @@ input { font: inherit; color: inherit; }
background: var(--paper); color: var(--ink);
cursor: pointer;
}
+
+/* ---------- Row context menu ---------- */
+.ctx-menu {
+ position: fixed; z-index: 60;
+ width: 200px; padding: 4px;
+ background: var(--paper); border: 1px solid var(--rule);
+ border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
+}
+.ctx-menu-label {
+ padding: 6px 8px 4px;
+ font-size: 11px; font-weight: 600;
+ color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em;
+}
+.ctx-menu-item {
+ width: 100%; display: flex; align-items: center; gap: 8px;
+ padding: 6px 8px; border-radius: 4px;
+ font-size: 13px; color: var(--ink); text-align: left;
+}
+.ctx-menu-item:hover { background: var(--bg-2); }
+.ctx-menu-item.active { background: var(--accent-soft); color: var(--accent-deep); }
+.ctx-menu-item.danger { color: var(--danger); }
+.ctx-menu-item.danger:hover { background: var(--danger-soft); }
+.ctx-menu-sep { height: 1px; margin: 4px 6px; background: var(--rule-2); }
diff --git a/web/tree.jsx b/web/tree.jsx
index 45db8c8..2dae31c 100644
--- a/web/tree.jsx
+++ b/web/tree.jsx
@@ -45,14 +45,74 @@ function StatusPopover({ value, style, onChange, onClose, anchorRect }) {
);
}
+// Right-click context menu: set status / archive / delete, fixed at the cursor.
+function ItemContextMenu({ item, statusStyle, anchor, onSetStatus, onArchive, onDelete, onClose }) {
+ const ref = useRef(null);
+ const [pos, setPos] = useState({ left: anchor.x, top: anchor.y });
+
+ // Viewport clamp (upstream ItemPeek pattern): flip left / above when overflowing.
+ useEffect(() => {
+ const el = ref.current; if (!el) return;
+ const r = el.getBoundingClientRect();
+ const pad = 12, gap = 14;
+ let left = anchor.x + gap, top = anchor.y + gap;
+ if (left + r.width > window.innerWidth - pad) left = Math.max(pad, anchor.x - r.width - gap);
+ if (top + r.height > window.innerHeight - pad) top = Math.max(pad, anchor.y - r.height - gap);
+ setPos({ left, top });
+ }, [anchor.x, anchor.y, item]);
+
+ // Capture-phase scroll/wheel: any scrollable container must close the cursor-anchored menu.
+ useEffect(() => {
+ const onPointerDown = (e) => { if (!ref.current?.contains(e.target)) onClose(); };
+ const onKeyDown = (e) => { if (e.key === "Escape") onClose(); };
+ const onMove = () => onClose();
+ document.addEventListener("pointerdown", onPointerDown);
+ document.addEventListener("keydown", onKeyDown);
+ document.addEventListener("scroll", onMove, true);
+ document.addEventListener("wheel", onMove, true);
+ window.addEventListener("resize", onMove);
+ return () => {
+ document.removeEventListener("pointerdown", onPointerDown);
+ document.removeEventListener("keydown", onKeyDown);
+ document.removeEventListener("scroll", onMove, true);
+ document.removeEventListener("wheel", onMove, true);
+ window.removeEventListener("resize", onMove);
+ };
+ }, [onClose]);
+
+ return (
+
e.preventDefault()}>
+
Set status
+ {STATUSES.map(s => (
+
+ ))}
+
+
+
+
+ );
+}
+
// Single tree row
function ItemRow({ item, depth, expanded, hasChildren, statusStyle,
- onToggle, onSetStatus, onAddChild, onDelete, onEdit, onSetPriority,
+ onToggle, onSetStatus, onAddChild, onDelete, onArchive, onEdit, onSetPriority,
onMoveUp, onMoveDown, canMoveUp, canMoveDown,
onDragStart, onDragOver, onDrop,
dimmed, query, dropTarget }) {
const [statusOpen, setStatusOpen] = useState(false);
const [priOpen, setPriOpen] = useState(false);
+ const [ctxMenu, setCtxMenu] = useState(null);
const overdue = isOverdue(item);
const isP0 = item.priority === "P0" && item.status !== "done" && item.status !== "cancelled";
@@ -73,10 +133,12 @@ function ItemRow({ item, depth, expanded, hasChildren, statusStyle,
onDragStart?.(e, item)}
+ onDragStart={(e) => { setCtxMenu(null); onDragStart?.(e, item); }}
onDragOver={(e) => onDragOver?.(e, item)}
onDrop={(e) => onDrop?.(e, item)}
+ onContextMenu={(e) => { e.preventDefault(); setStatusOpen(false); setPriOpen(false); setCtxMenu({ x: e.clientX, y: e.clientY }); }}
>
+ {ctxMenu && ReactDOM.createPortal( setCtxMenu(null)}/>, document.body)}