diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index 5b671b3dec..6650c5ef8e 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -146,22 +146,38 @@ } .github-import-workspace__resize-handle { - flex: 0 0 var(--space-xs); + position: relative; + flex: 0 0 var(--space-sm); align-self: stretch; cursor: col-resize; touch-action: none; border-radius: var(--radius-sm); +} + +.github-import-workspace__resize-handle::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: var(--space-xs); + transform: translateX(-50%); + border-radius: var(--radius-sm); background: color-mix(in srgb, var(--border) 35%, transparent); transition: background var(--transition-fast); } -.github-import-workspace__resize-handle:hover { +.github-import-workspace__resize-handle:hover::before, +.github-import-workspace__resize-handle:active::before { background: color-mix(in srgb, var(--todo) 50%, transparent); } .github-import-workspace__resize-handle:focus-visible { outline: var(--focus-ring-strong); outline-offset: 0; +} + +.github-import-workspace__resize-handle:focus-visible::before { background: color-mix(in srgb, var(--todo) 60%, transparent); } diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 3ef7233dd4..52c57f8d2d 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -26,8 +26,9 @@ interface GitHubImportModalProps { projectId?: string; } -// Mobile breakpoint in pixels +// Mobile and two-pane breakpoints in pixels const MOBILE_BREAKPOINT = 640; +const TWO_PANE_BREAKPOINT = 860; const GITHUB_IMPORT_LIST_PANE_MIN_WIDTH = 240; const GITHUB_IMPORT_LIST_PANE_MAX_WIDTH = 640; const GITHUB_IMPORT_LIST_PANE_DEFAULT_WIDTH = 360; @@ -82,8 +83,9 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId useModalResizePersist(modalRef, isOpen, "fusion:github-modal-size"); const overlayDismissProps = useOverlayDismiss(onClose); - // Mobile view state + // Responsive view state const [isMobile, setIsMobile] = useState(false); + const [canResizePanes, setCanResizePanes] = useState(false); const [mobileView, setMobileView] = useState<"list" | "preview">("list"); const [listPaneWidth, setListPaneWidth] = useState(() => { if (typeof window === "undefined") { @@ -288,20 +290,21 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId return () => document.removeEventListener("keydown", handleKey); }, [isOpen, onClose]); - // Detect mobile viewport + // Detect responsive viewport bands useEffect(() => { if (!isOpen) return; - - const checkMobile = () => { + + const checkViewportBands = () => { setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT); + setCanResizePanes(window.innerWidth > TWO_PANE_BREAKPOINT); }; - + // Check initially - checkMobile(); - + checkViewportBands(); + // Listen for resize - window.addEventListener("resize", checkMobile); - return () => window.removeEventListener("resize", checkMobile); + window.addEventListener("resize", checkViewportBands); + return () => window.removeEventListener("resize", checkViewportBands); }, [isOpen]); useEffect(() => { @@ -316,7 +319,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId }, [listPaneWidth]); const handleListPaneResizeStart = useCallback((event: ReactPointerEvent) => { - if (isMobile) { + if (!canResizePanes) { return; } @@ -340,10 +343,10 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId document.addEventListener("pointermove", handlePointerMove); document.addEventListener("pointerup", handlePointerUp); - }, [isMobile, listPaneWidth]); + }, [canResizePanes, listPaneWidth]); const handleListPaneResizeKeyDown = useCallback((event: ReactKeyboardEvent) => { - if (isMobile) { + if (!canResizePanes) { return; } @@ -371,7 +374,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId event.preventDefault(); setListPaneWidth(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH); } - }, [isMobile]); + }, [canResizePanes]); // Handle issue selection - switch to preview view on mobile const handleIssueSelect = useCallback((issueNumber: number) => { @@ -625,7 +628,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId {/* Left pane: Issue/PR list */}
@@ -763,7 +766,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
- {!isMobile && ( + {canResizePanes && (
{ return rendered; }; + const renderWithEmptyIssues = async () => { + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([]); + + const rendered = render(); + + await waitFor(() => { + expect(screen.getByText("No open issues found")).toBeTruthy(); + }); + + return rendered; + }; + + const renderWithPulls = async () => { + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([]); + vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce([ + { number: 7, title: "Resize Test Pull", body: "Pull body", html_url: "https://github.com/owner/repo/pull/7", headBranch: "feature", baseBranch: "main" }, + ]); + + const rendered = render(); + + fireEvent.click(screen.getByRole("tab", { name: /Pull Requests/i })); + + await waitFor(() => { + expect(screen.getByText("Resize Test Pull")).toBeTruthy(); + }); + + return rendered; + }; + + const stubPointerCapture = (handle: HTMLElement) => { + handle.setPointerCapture = vi.fn(); + handle.releasePointerCapture = vi.fn(); + handle.hasPointerCapture = vi.fn(() => true); + }; + + const dragHandle = (handle: HTMLElement, startX: number, endX: number) => { + stubPointerCapture(handle); + fireEvent.pointerDown(handle, { pointerId: 1, clientX: startX }); + fireEvent.pointerMove(document, { pointerId: 1, clientX: endX }); + fireEvent.pointerUp(document, { pointerId: 1, clientX: endX }); + }; + beforeEach(() => { window.localStorage.removeItem("fusion:github-import-list-pane-width"); setViewportWidth(1200); @@ -897,17 +941,62 @@ describe("GitHubImportModal", () => { setViewportWidth(originalInnerWidth); }); - it("renders handle on desktop and hides it on mobile", async () => { + it("renders handle only in the side-by-side two-pane band", async () => { await renderWithIssues(); expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy(); + expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px"); + + setViewportWidth(800); + + await waitFor(() => { + expect(screen.queryByTestId("github-import-resize-handle")).toBeNull(); + }); + expect(screen.getByTestId("github-import-list-pane").getAttribute("style") ?? "").not.toContain("flex: 0 0"); setViewportWidth(480); await waitFor(() => { expect(screen.queryByTestId("github-import-resize-handle")).toBeNull(); }); + expect(screen.getByTestId("github-import-list-pane").getAttribute("style") ?? "").not.toContain("flex: 0 0"); }); + it("resizes the list pane with pointer drags and clamps to bounds", async () => { + await renderWithIssues(); + const handle = screen.getByTestId("github-import-resize-handle"); + const listPane = screen.getByTestId("github-import-list-pane"); + + dragHandle(handle, 100, 160); + expect(handle.getAttribute("aria-valuenow")).toBe("420"); + expect(listPane.getAttribute("style")).toContain("flex: 0 0 420px"); + + dragHandle(handle, 160, 120); + expect(handle.getAttribute("aria-valuenow")).toBe("380"); + expect(listPane.getAttribute("style")).toContain("flex: 0 0 380px"); + + dragHandle(handle, 120, -200); + expect(handle.getAttribute("aria-valuenow")).toBe("240"); + expect(listPane.getAttribute("style")).toContain("flex: 0 0 240px"); + + dragHandle(handle, -200, 700); + expect(handle.getAttribute("aria-valuenow")).toBe("640"); + expect(listPane.getAttribute("style")).toContain("flex: 0 0 640px"); + }); + + it("renders the desktop handle regardless of list content or active tab", async () => { + const mounted = await renderWithEmptyIssues(); + expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy(); + expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px"); + mounted.unmount(); + + vi.clearAllMocks(); + window.localStorage.removeItem("fusion:github-import-list-pane-width"); + setViewportWidth(1200); + + await renderWithPulls(); + expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy(); + expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px"); + }); it.each([ [{ key: "ArrowRight" }, 370], [{ key: "ArrowLeft" }, 350],