From e8c4b11069dc02926d5cd0c96082805661725488 Mon Sep 17 00:00:00 2001 From: ReBotics AI Date: Sat, 1 Aug 2026 21:37:45 -0600 Subject: [PATCH] feat(tasks): make card detail sheet resizable up to half screen --- .../intelligence/projects/ProjectsBoard.tsx | 129 +++++++++++++++++- apps/web/src/lib/storage-keys.ts | 3 + 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/intelligence/projects/ProjectsBoard.tsx b/apps/web/src/components/intelligence/projects/ProjectsBoard.tsx index 300a59e..886a08c 100644 --- a/apps/web/src/components/intelligence/projects/ProjectsBoard.tsx +++ b/apps/web/src/components/intelligence/projects/ProjectsBoard.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type CSSProperties, + type PointerEvent as ReactPointerEvent, +} from "react"; import { DndContext, DragOverlay, @@ -129,6 +137,7 @@ import { type Swimlane, type SwimlaneGroupBy, } from "@/lib/tasks-board-swimlanes"; +import { TASK_SHEET_WIDTH_KEY } from "@/lib/storage-keys"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -247,6 +256,45 @@ function saveCardFaceVisibility(boardKey: string, value: CardFaceVisibility) { } } +/** Compact width matches prior md:max-w-2xl sheet. Max is half the viewport. */ +const MIN_TASK_SHEET_WIDTH = 672; +const DEFAULT_TASK_SHEET_WIDTH = 840; + +function maxTaskSheetWidth(viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1280) { + if (viewportWidth < 640) return viewportWidth; + return Math.floor(viewportWidth * 0.5); +} + +function clampTaskSheetWidth( + width: number, + viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1280 +) { + const max = maxTaskSheetWidth(viewportWidth); + const min = viewportWidth < 640 ? viewportWidth : Math.min(MIN_TASK_SHEET_WIDTH, max); + return Math.max(min, Math.min(max, Math.round(width))); +} + +function readStoredTaskSheetWidth(): number { + if (typeof window === "undefined") return DEFAULT_TASK_SHEET_WIDTH; + try { + const raw = localStorage.getItem(TASK_SHEET_WIDTH_KEY); + if (raw == null) return clampTaskSheetWidth(DEFAULT_TASK_SHEET_WIDTH); + const n = Number.parseFloat(raw); + if (!Number.isFinite(n)) return clampTaskSheetWidth(DEFAULT_TASK_SHEET_WIDTH); + return clampTaskSheetWidth(n); + } catch { + return clampTaskSheetWidth(DEFAULT_TASK_SHEET_WIDTH); + } +} + +function writeStoredTaskSheetWidth(width: number) { + try { + localStorage.setItem(TASK_SHEET_WIDTH_KEY, String(Math.round(width))); + } catch { + /* ignore quota */ + } +} + function normalizeContextObject(raw: string | null): Record { if (!raw) return {}; try { @@ -626,6 +674,8 @@ function CardEditorDialog({ const [githubCommentsError, setGithubCommentsError] = useState( null ); + const [sheetWidth, setSheetWidth] = useState(readStoredTaskSheetWidth); + const [sheetResizing, setSheetResizing] = useState(false); const [composer, setComposer] = useState(""); const [awaitingRunId, setAwaitingRunId] = useState(null); const [agents, setAgents] = useState([]); @@ -1124,12 +1174,87 @@ function CardEditorDialog({ (s) => !attachments.some((a) => a.id === s.id) ); + useEffect(() => { + if (!open) return; + const onViewportResize = () => { + setSheetWidth((w) => clampTaskSheetWidth(w)); + }; + window.addEventListener("resize", onViewportResize); + return () => window.removeEventListener("resize", onViewportResize); + }, [open]); + + const handleSheetWidthResize = (e: ReactPointerEvent) => { + if (window.innerWidth < 640) return; + e.preventDefault(); + e.stopPropagation(); + setSheetResizing(true); + const startX = e.clientX; + const startWidth = sheetWidth; + const onMove = (ev: PointerEvent) => { + setSheetWidth(clampTaskSheetWidth(startWidth + (startX - ev.clientX))); + }; + const onUp = () => { + setSheetResizing(false); + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + setSheetWidth((w) => { + const next = clampTaskSheetWidth(w); + writeStoredTaskSheetWidth(next); + return next; + }); + }; + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp); + document.body.style.cursor = "ew-resize"; + document.body.style.userSelect = "none"; + }; + + const toggleSheetWidth = () => { + if (window.innerWidth < 640) return; + setSheetWidth((w) => { + const max = maxTaskSheetWidth(); + const min = Math.min(MIN_TASK_SHEET_WIDTH, max); + const next = w >= max - 24 ? min : max; + writeStoredTaskSheetWidth(next); + return next; + }); + }; + return ( +