From 281d1a33714d721e648b61ca3a1cab34995ee48f Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 10 Jul 2026 23:08:31 -0700 Subject: [PATCH] FN-7809: collapse List view to single-pane layout on tablet-narrow widths Fixes the List view's two-pane split layout clipping primary controls and the expanded quick-add composer on tablet-width viewports (769-1024px) by switching those widths to the same single-pane card/detail layout already used on mobile. - Add `useSinglePaneList` gate in ListView.tsx (`viewportMode === "mobile" || "tablet"`) driving split-vs-single-pane structure, detail routing, and resize-handle wiring, while touch-only long-press behavior stays on `isMobile`. - Extend the mobile-only responsive CSS breakpoints in ListView.css from `max-width: 768px` to `max-width: 1024px` so tablet gets the same toolbar/card scaffolding as mobile, while desktop split rules remain unchanged above that tier. - Add `list-view--single-pane` root class and route tablet clicks through the single-pane `onOpenDetail` path instead of the desktop split-pane selection. - Update docs/dashboard-guide.md to describe the tablet single-pane behavior and add an FNXC:ListView comment recording the FN-7809 rationale. - Add regression tests covering tablet single-pane rendering, tablet detail-open routing, and updated CSS-fixture assertions for the widened breakpoint. - Add a patch changeset describing the fix for @runfusion/fusion release notes. Files changed: .changeset/fn-7809-list-tablet-single-pane.md | 7 +++ docs/dashboard-guide.md | 6 +- packages/dashboard/app/components/ListView.css | 11 ++-- packages/dashboard/app/components/ListView.tsx | 49 ++++++++------- .../app/components/__tests__/ListView.test.tsx | 69 +++++++++++++++++++++- 5 files changed, 112 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-7809 Fusion-Task-Lineage: 9e5f8bb7-13ee-4a58-81b1-eb5a9911bd4e Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7809-list-tablet-single-pane.md | 7 ++ docs/dashboard-guide.md | 6 +- .../dashboard/app/components/ListView.css | 11 +-- .../dashboard/app/components/ListView.tsx | 49 +++++++------ .../components/__tests__/ListView.test.tsx | 69 ++++++++++++++++++- 5 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 .changeset/fn-7809-list-tablet-single-pane.md diff --git a/.changeset/fn-7809-list-tablet-single-pane.md b/.changeset/fn-7809-list-tablet-single-pane.md new file mode 100644 index 0000000000..26382ca1ca --- /dev/null +++ b/.changeset/fn-7809-list-tablet-single-pane.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the List view controls and quick-add box being cut off on tablet-width screens. +category: fix +dev: ListView collapses to a single-pane layout at the `useViewportMode()` "tablet" tier (769–1024px) instead of the desktop two-pane split, which lacked horizontal room and clipped the primary action cluster and expanded QuickEntryBox. Split-vs-single now keys off a shared narrow gate; touch-only long-press stays gated on mobile. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 4d0a172e49..3abc71f55f 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -261,8 +261,10 @@ Features: - Bulk selection + batch model updates - Bulk Pause / Unpause / Archive actions from the selection toolbar (`Pause selected`, `Unpause selected`, `Archive selected`) for fast batch task state management. - Bulk delete from the selection toolbar (`Delete selected`): archived selections are skipped automatically, and dependency-conflict failures can be force-deleted per task after a danger confirmation that removes dependency references. -- List rows and mobile cards support the same task context menu as Board cards from right-click, keyboard context menu / Shift+F10, or touch long-press without changing ordinary row selection or tap-to-open behavior. Selecting an action applies that exact action once and dismisses the menu, including **Refine** for completed tasks. - diff --git a/packages/dashboard/app/components/ListView.css b/packages/dashboard/app/components/ListView.css index 7cc2ad4a42..89bf5f06e7 100644 --- a/packages/dashboard/app/components/ListView.css +++ b/packages/dashboard/app/components/ListView.css @@ -961,8 +961,11 @@ In the split sidebar the title cell must allow the title to wrap to two lines (h border-bottom: 1px solid var(--border); } -/* === List View Mobile Responsive === */ -@media (max-width: 768px) { +/* === List View Single-Pane Responsive === +FNXC:ListView 2026-07-10-00:00 (FN-7809): +Tablet-width List view shares the mobile single-pane scaffolding so the full-width toolbar and QuickEntryBox are not clipped by the desktop split sidebar. Mobile behavior stays unchanged; desktop split rules remain active above the tablet tier. +*/ +@media (max-width: 1024px) { .list-toolbar { padding: var(--space-md); flex-wrap: wrap; @@ -1025,8 +1028,8 @@ In the split sidebar the title cell must allow the title to wrap to two lines (h } } -/* === List View Mobile Cards (FN-1140) === */ -@media (max-width: 768px) { +/* === List View Single-Pane Cards (FN-1140, FN-7809) === */ +@media (max-width: 1024px) { /* Hide the table on mobile — card layout is rendered via JS */ .list-table { display: none; diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index f17e08a139..a164aa4f8f 100644 --- a/packages/dashboard/app/components/ListView.tsx +++ b/packages/dashboard/app/components/ListView.tsx @@ -386,6 +386,11 @@ export function ListView({ }); const viewportMode = useViewportMode(); const isMobile = viewportMode === "mobile"; + /* + FNXC:ListView 2026-07-10-00:00 (FN-7809): + Tablet-width List view must use the same single-pane layout as mobile because the desktop two-pane sidebar leaves too little horizontal room and clips the primary actions plus expanded QuickEntryBox. Keep touch-only long-press behavior on `isMobile`; this gate only controls split-vs-single-pane structure and detail routing. + */ + const useSinglePaneList = viewportMode === "mobile" || viewportMode === "tablet"; const { confirm, confirmWithChoice } = useConfirm(); useEffect(() => { @@ -507,7 +512,7 @@ export function ListView({ }, [selectedTaskId, tasks]); useEffect(() => { - if (isMobile || typeof ResizeObserver === "undefined") return; + if (useSinglePaneList || typeof ResizeObserver === "undefined") return; const container = splitLayoutRef.current; if (!container) return; @@ -531,10 +536,10 @@ export function ListView({ const observer = new ResizeObserver(applyClamp); observer.observe(container); return () => observer.disconnect(); - }, [isMobile, sidebarWidth]); + }, [sidebarWidth, useSinglePaneList]); useEffect(() => { - if (isMobile || typeof ResizeObserver === "undefined") return; + if (useSinglePaneList || typeof ResizeObserver === "undefined") return; const sidebar = splitSidebarRef.current; const container = splitLayoutRef.current; if (!sidebar || !container) return; @@ -561,7 +566,7 @@ export function ListView({ observer.disconnect(); if (saveTimer) clearTimeout(saveTimer); }; - }, [isMobile, projectId]); + }, [projectId, useSinglePaneList]); const toggleBulkEdit = useCallback(() => { setBulkEditEnabled((prev) => { @@ -1701,7 +1706,7 @@ export function ListView({ addToast(getErrorMessage(err), "error"); } } : undefined, - onOpenRefine: () => onOpenDetail(task, { origin: isMobile ? "list-mobile" : undefined, initialAction: "refine" }), + onOpenRefine: () => onOpenDetail(task, { origin: useSinglePaneList ? "list-mobile" : undefined, initialAction: "refine" }), onRespecify: async () => { const shouldRebuild = await confirm({ title: t("taskDetail.plan.rebuildTitle", "Rebuild Plan"), @@ -1791,7 +1796,7 @@ export function ListView({ actions.push({ id: model.reviewAction.id, label: model.reviewAction.label, disabled: model.reviewAction.disabled, onSelect: model.reviewAction.onSelect }); } return actions.filter((action) => action.tone === "note" || action.disabled === true || Boolean(action.onSelect)); - }, [addToast, autoMerge, columnFlagsById, confirm, getListColumnLabel, handleListContextCheckPrStatus, handleListContextEnableGithubTracking, handleListContextMove, handleListTaskArchive, handleListTaskDelete, handleListTaskRevert, isMobile, listContextMenuColumns, mergeStrategy, onDuplicateTask, onMergeTask, onOpenDetail, onPauseTask, onResetTask, onRetryTask, onUnpauseTask, onArchiveTask, onRevertTask, onTasksUpdated, projectId, t]); + }, [addToast, autoMerge, columnFlagsById, confirm, getListColumnLabel, handleListContextCheckPrStatus, handleListContextEnableGithubTracking, handleListContextMove, handleListTaskArchive, handleListTaskDelete, handleListTaskRevert, isMobile, listContextMenuColumns, mergeStrategy, onDuplicateTask, onMergeTask, onOpenDetail, onPauseTask, onResetTask, onRetryTask, onUnpauseTask, onArchiveTask, onRevertTask, onTasksUpdated, projectId, t, useSinglePaneList]); const contextMenuActions = useMemo( () => (contextMenuState ? buildListContextMenuActions(contextMenuState.task) : []), @@ -1913,7 +1918,7 @@ export function ListView({ return; } closeContextMenu(); - if (isMobile) { + if (useSinglePaneList) { onOpenDetail(task, { origin: "list-mobile" }); return; } @@ -1921,7 +1926,7 @@ export function ListView({ setSelectedTaskId(task.id); setSelectedTaskSnapshot(task); }, - [closeContextMenu, isMobile, onOpenDetail] + [closeContextMenu, onOpenDetail, useSinglePaneList] ); // Debounce detail fetches so rapid keyboard/mouse navigation through a @@ -2020,7 +2025,7 @@ export function ListView({ style={{ width }} — which wins over the grid `auto` track — updates live and persists. */ const handleSplitResizeStart = useCallback((event: React.PointerEvent) => { - if (isMobile) return; + if (useSinglePaneList) return; const container = splitLayoutRef.current; if (!container) return; event.preventDefault(); @@ -2061,13 +2066,13 @@ export function ListView({ window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", teardown); window.addEventListener("pointercancel", teardown); - }, [isMobile]); + }, [useSinglePaneList]); // FNXC:ListView 2026-06-22-18:00: Tear down any in-flight resize drag on unmount so window pointer listeners never leak. useEffect(() => () => splitResizeTeardownRef.current?.(), []); const handleSplitResizeKeyDown = useCallback((event: React.KeyboardEvent) => { - if (isMobile) return; + if (useSinglePaneList) return; const measuredWidth = splitLayoutRef.current?.clientWidth ?? 0; const fallbackWidth = sidebarWidth / LIST_SIDEBAR_MAX_RATIO + LIST_SIDEBAR_KEYBOARD_STEP; const containerWidth = Math.max(measuredWidth, fallbackWidth); @@ -2091,7 +2096,7 @@ export function ListView({ event.preventDefault(); setSidebarWidth(maxWidth); } - }, [isMobile, sidebarWidth]); + }, [sidebarWidth, useSinglePaneList]); const handleColumnDragOver = useCallback( (e: React.DragEvent, column: ColumnId) => { @@ -2305,7 +2310,7 @@ export function ListView({ className="btn btn-sm list-view-options-toggle" onClick={() => setViewOptionsOpen((prev) => !prev)} aria-expanded={viewOptionsOpen} - aria-controls={isMobile ? "list-view-options-panel-mobile" : "list-view-options-panel"} + aria-controls={useSinglePaneList ? "list-view-options-panel-mobile" : "list-view-options-panel"} > {t("listView.viewOptions", "View")} @@ -2408,7 +2413,7 @@ export function ListView({ } return ( -
+
{contextMenuState && hasContextMenuActions && createPortal(
)} - {isMobile && ( + {useSinglePaneList && ( <>
{renderWorkflowSelector()} @@ -2460,14 +2465,14 @@ export function ListView({ )}
-
+
- {!isMobile && ( + {!useSinglePaneList && (