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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-10 23:08:31 -07:00
parent c530b238b4
commit 281d1a3371
5 changed files with 112 additions and 30 deletions

View File

@@ -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.

View File

@@ -261,8 +261,10 @@ Features:
- Bulk selection + batch model updates - 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 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. - 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. - Desktop List view keeps the two-pane table/detail split. Tablet-width and mobile viewports use the single-pane card layout so list controls and quick-add stay full-width; tapping a task opens detail instead of selecting an embedded split pane.
<!-- FNXC:ListContextMenu 2026-06-29-00:00: List context menus are alternate action entry points only; desktop left-click still selects the split-pane detail and mobile tap still opens detail while long-press suppresses the follow-up tap. - List rows and tablet/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.
<!-- FNXC:ListView 2026-07-10-00:00: FN-7809 makes tablet List view use the same single-pane card/detail route as mobile because the desktop split sidebar clipped the primary controls and quick-add composer at 769–1024px.
FNXC:ListContextMenu 2026-06-29-00:00: List context menus are alternate action entry points only; desktop left-click still selects the split-pane detail and mobile tap still opens detail while long-press suppresses the follow-up tap.
FNXC:ListContextMenu 2026-06-30-00:20: Keyboard access is part of the Board/List context-menu contract, so docs must include the context-menu key and Shift+F10 alongside pointer and touch entry points. FNXC:ListContextMenu 2026-06-30-00:20: Keyboard access is part of the Board/List context-menu contract, so docs must include the context-menu key and Shift+F10 alongside pointer and touch entry points.
FNXC:DoneTaskRefine 2026-07-01-00:00: Completed List row/card context menus route Refine to the existing task-detail feedback modal so desktop right-click and mobile long-press share the same refinement flow. FNXC:DoneTaskRefine 2026-07-01-00:00: Completed List row/card context menus route Refine to the existing task-detail feedback modal so desktop right-click and mobile long-press share the same refinement flow.
FNXC:TaskContextMenu 2026-07-01-00:00: Mobile List card long-press action taps must select and dismiss through the same shared TaskContextMenu invariant as Board and Task Detail surfaces. --> FNXC:TaskContextMenu 2026-07-01-00:00: Mobile List card long-press action taps must select and dismiss through the same shared TaskContextMenu invariant as Board and Task Detail surfaces. -->

View File

@@ -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); border-bottom: 1px solid var(--border);
} }
/* === List View Mobile Responsive === */ /* === List View Single-Pane Responsive ===
@media (max-width: 768px) { 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 { .list-toolbar {
padding: var(--space-md); padding: var(--space-md);
flex-wrap: wrap; 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) === */ /* === List View Single-Pane Cards (FN-1140, FN-7809) === */
@media (max-width: 768px) { @media (max-width: 1024px) {
/* Hide the table on mobile — card layout is rendered via JS */ /* Hide the table on mobile — card layout is rendered via JS */
.list-table { .list-table {
display: none; display: none;

View File

@@ -386,6 +386,11 @@ export function ListView({
}); });
const viewportMode = useViewportMode(); const viewportMode = useViewportMode();
const isMobile = viewportMode === "mobile"; 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(); const { confirm, confirmWithChoice } = useConfirm();
useEffect(() => { useEffect(() => {
@@ -507,7 +512,7 @@ export function ListView({
}, [selectedTaskId, tasks]); }, [selectedTaskId, tasks]);
useEffect(() => { useEffect(() => {
if (isMobile || typeof ResizeObserver === "undefined") return; if (useSinglePaneList || typeof ResizeObserver === "undefined") return;
const container = splitLayoutRef.current; const container = splitLayoutRef.current;
if (!container) return; if (!container) return;
@@ -531,10 +536,10 @@ export function ListView({
const observer = new ResizeObserver(applyClamp); const observer = new ResizeObserver(applyClamp);
observer.observe(container); observer.observe(container);
return () => observer.disconnect(); return () => observer.disconnect();
}, [isMobile, sidebarWidth]); }, [sidebarWidth, useSinglePaneList]);
useEffect(() => { useEffect(() => {
if (isMobile || typeof ResizeObserver === "undefined") return; if (useSinglePaneList || typeof ResizeObserver === "undefined") return;
const sidebar = splitSidebarRef.current; const sidebar = splitSidebarRef.current;
const container = splitLayoutRef.current; const container = splitLayoutRef.current;
if (!sidebar || !container) return; if (!sidebar || !container) return;
@@ -561,7 +566,7 @@ export function ListView({
observer.disconnect(); observer.disconnect();
if (saveTimer) clearTimeout(saveTimer); if (saveTimer) clearTimeout(saveTimer);
}; };
}, [isMobile, projectId]); }, [projectId, useSinglePaneList]);
const toggleBulkEdit = useCallback(() => { const toggleBulkEdit = useCallback(() => {
setBulkEditEnabled((prev) => { setBulkEditEnabled((prev) => {
@@ -1701,7 +1706,7 @@ export function ListView({
addToast(getErrorMessage(err), "error"); addToast(getErrorMessage(err), "error");
} }
} : undefined, } : undefined,
onOpenRefine: () => onOpenDetail(task, { origin: isMobile ? "list-mobile" : undefined, initialAction: "refine" }), onOpenRefine: () => onOpenDetail(task, { origin: useSinglePaneList ? "list-mobile" : undefined, initialAction: "refine" }),
onRespecify: async () => { onRespecify: async () => {
const shouldRebuild = await confirm({ const shouldRebuild = await confirm({
title: t("taskDetail.plan.rebuildTitle", "Rebuild Plan"), 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 }); 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)); 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( const contextMenuActions = useMemo(
() => (contextMenuState ? buildListContextMenuActions(contextMenuState.task) : []), () => (contextMenuState ? buildListContextMenuActions(contextMenuState.task) : []),
@@ -1913,7 +1918,7 @@ export function ListView({
return; return;
} }
closeContextMenu(); closeContextMenu();
if (isMobile) { if (useSinglePaneList) {
onOpenDetail(task, { origin: "list-mobile" }); onOpenDetail(task, { origin: "list-mobile" });
return; return;
} }
@@ -1921,7 +1926,7 @@ export function ListView({
setSelectedTaskId(task.id); setSelectedTaskId(task.id);
setSelectedTaskSnapshot(task); setSelectedTaskSnapshot(task);
}, },
[closeContextMenu, isMobile, onOpenDetail] [closeContextMenu, onOpenDetail, useSinglePaneList]
); );
// Debounce detail fetches so rapid keyboard/mouse navigation through a // 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. style={{ width }} — which wins over the grid `auto` track — updates live and persists.
*/ */
const handleSplitResizeStart = useCallback((event: React.PointerEvent<HTMLDivElement>) => { const handleSplitResizeStart = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (isMobile) return; if (useSinglePaneList) return;
const container = splitLayoutRef.current; const container = splitLayoutRef.current;
if (!container) return; if (!container) return;
event.preventDefault(); event.preventDefault();
@@ -2061,13 +2066,13 @@ export function ListView({
window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", teardown); window.addEventListener("pointerup", teardown);
window.addEventListener("pointercancel", 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. // 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?.(), []); useEffect(() => () => splitResizeTeardownRef.current?.(), []);
const handleSplitResizeKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => { const handleSplitResizeKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
if (isMobile) return; if (useSinglePaneList) return;
const measuredWidth = splitLayoutRef.current?.clientWidth ?? 0; const measuredWidth = splitLayoutRef.current?.clientWidth ?? 0;
const fallbackWidth = sidebarWidth / LIST_SIDEBAR_MAX_RATIO + LIST_SIDEBAR_KEYBOARD_STEP; const fallbackWidth = sidebarWidth / LIST_SIDEBAR_MAX_RATIO + LIST_SIDEBAR_KEYBOARD_STEP;
const containerWidth = Math.max(measuredWidth, fallbackWidth); const containerWidth = Math.max(measuredWidth, fallbackWidth);
@@ -2091,7 +2096,7 @@ export function ListView({
event.preventDefault(); event.preventDefault();
setSidebarWidth(maxWidth); setSidebarWidth(maxWidth);
} }
}, [isMobile, sidebarWidth]); }, [sidebarWidth, useSinglePaneList]);
const handleColumnDragOver = useCallback( const handleColumnDragOver = useCallback(
(e: React.DragEvent, column: ColumnId) => { (e: React.DragEvent, column: ColumnId) => {
@@ -2305,7 +2310,7 @@ export function ListView({
className="btn btn-sm list-view-options-toggle" className="btn btn-sm list-view-options-toggle"
onClick={() => setViewOptionsOpen((prev) => !prev)} onClick={() => setViewOptionsOpen((prev) => !prev)}
aria-expanded={viewOptionsOpen} 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"}
> >
<Columns3 size={14} /> <Columns3 size={14} />
{t("listView.viewOptions", "View")} {t("listView.viewOptions", "View")}
@@ -2408,7 +2413,7 @@ export function ListView({
} }
return ( return (
<div className="list-view"> <div className={`list-view${useSinglePaneList ? " list-view--single-pane" : ""}`}>
{contextMenuState && hasContextMenuActions && createPortal( {contextMenuState && hasContextMenuActions && createPortal(
<div <div
ref={contextMenuRef} ref={contextMenuRef}
@@ -2435,7 +2440,7 @@ export function ListView({
addToast={addToast} addToast={addToast}
/> />
)} )}
{isMobile && ( {useSinglePaneList && (
<> <>
<div className="list-toolbar"> <div className="list-toolbar">
{renderWorkflowSelector()} {renderWorkflowSelector()}
@@ -2460,14 +2465,14 @@ export function ListView({
)} )}
<div className="list-table-container"> <div className="list-table-container">
<div className={isMobile ? "" : "list-split-layout"} data-testid={isMobile ? undefined : "list-split-layout"} ref={splitLayoutRef}> <div className={useSinglePaneList ? "" : "list-split-layout"} data-testid={useSinglePaneList ? undefined : "list-split-layout"} ref={splitLayoutRef}>
<div <div
className={isMobile ? "" : "list-split-sidebar"} className={useSinglePaneList ? "" : "list-split-sidebar"}
data-testid={isMobile ? undefined : "list-split-sidebar"} data-testid={useSinglePaneList ? undefined : "list-split-sidebar"}
ref={splitSidebarRef} ref={splitSidebarRef}
style={isMobile ? undefined : { width: `${sidebarWidth}px` }} style={useSinglePaneList ? undefined : { width: `${sidebarWidth}px` }}
> >
{!isMobile && ( {!useSinglePaneList && (
<aside className="list-sidebar-controls" aria-label={t("listView.listControlsLabel", "List controls")}> <aside className="list-sidebar-controls" aria-label={t("listView.listControlsLabel", "List controls")}>
{/* {/*
FNXC:ListView 2026-06-23-23:42: FNXC:ListView 2026-06-23-23:42:
@@ -2536,7 +2541,7 @@ export function ListView({
<div className="list-empty"> <div className="list-empty">
{searchQuery ? t("listView.noTasksMatch", "No tasks match your filter") : t("listView.noTasksYet", "No tasks yet")} {searchQuery ? t("listView.noTasksMatch", "No tasks match your filter") : t("listView.noTasksYet", "No tasks yet")}
</div> </div>
) : isMobile ? ( ) : useSinglePaneList ? (
<div className="list-cards"> <div className="list-cards">
{listColumns.map((columnDef) => { {listColumns.map((columnDef) => {
const column = columnDef.id; const column = columnDef.id;
@@ -2929,7 +2934,7 @@ export function ListView({
</table> </table>
)} )}
</div> </div>
{!isMobile && ( {!useSinglePaneList && (
<> <>
<div <div
className="list-split-resize-handle" className="list-split-resize-handle"

View File

@@ -365,6 +365,21 @@ function mockMobileViewport() {
})); }));
} }
function mockTabletViewport() {
ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: 900, configurable: true });
return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(min-width: 769px) and (max-width: 1024px)",
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
}
function mockDesktopViewport() { function mockDesktopViewport() {
ensureMatchMedia(); ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true }); Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
@@ -1705,6 +1720,40 @@ describe("ListView", () => {
viewportSpy.mockRestore(); viewportSpy.mockRestore();
}); });
it("renders tablet List view as a single full-width pane without split chrome", () => {
const viewportSpy = mockTabletViewport();
const tasks = [createMockTask({ id: "FN-001", title: "Tablet task" })];
const { container } = renderListView({ tasks });
expect(container.querySelector(".list-view--single-pane")).toBeInTheDocument();
expect(screen.queryByTestId("list-split-layout")).toBeNull();
expect(screen.queryByTestId("list-split-sidebar")).toBeNull();
expect(screen.queryByTestId("list-split-resize-handle")).toBeNull();
expect(screen.queryByTestId("list-split-detail")).toBeNull();
expect(container.querySelector(".list-toolbar .list-action-cluster")).toBeInTheDocument();
expect(within(container.querySelector(".list-toolbar .list-action-cluster") as HTMLElement).getByText("+ New Task")).toBeInTheDocument();
expect(container.querySelector(".list-quick-entry-above-table .quick-entry-box")).toBeInTheDocument();
expect(container.querySelector(".list-cards")).toBeInTheDocument();
expect(container.querySelector("table.list-table")).toBeNull();
viewportSpy.mockRestore();
});
it("opens tablet task detail through the single-pane detail route", () => {
const viewportSpy = mockTabletViewport();
const task = createMockTask({ id: "FN-001", title: "Tablet open" });
const onOpenDetail = vi.fn();
const { container } = renderListView({ tasks: [task], onOpenDetail });
fireEvent.click(container.querySelector('.list-card[data-id="FN-001"]') as HTMLElement);
expect(onOpenDetail).toHaveBeenCalledWith(task, { origin: "list-mobile" });
expect(onOpenDetail).toHaveBeenCalledTimes(1);
expect(screen.queryByTestId("list-split-detail-content")).toBeNull();
viewportSpy.mockRestore();
});
it("sorts tasks by ID when ID header is clicked", () => { it("sorts tasks by ID when ID header is clicked", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-003", title: "Third", column: "triage" }), createMockTask({ id: "FN-003", title: "Third", column: "triage" }),
@@ -2100,7 +2149,7 @@ describe("ListView", () => {
const css = readFileSync("app/components/ListView.css", "utf8"); const css = readFileSync("app/components/ListView.css", "utf8");
const actionClusterRule = css.match(/\.list-action-cluster,\s*\n\.list-sidebar-controls__actions\s*\{[^}]*\}/)?.[0] ?? ""; const actionClusterRule = css.match(/\.list-action-cluster,\s*\n\.list-sidebar-controls__actions\s*\{[^}]*\}/)?.[0] ?? "";
const toolbarRule = css.match(/\.list-sidebar-controls__toolbar\s*\{[^}]*\}/)?.[0] ?? ""; const toolbarRule = css.match(/\.list-sidebar-controls__toolbar\s*\{[^}]*\}/)?.[0] ?? "";
const mobileToolbarRule = css.match(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.list-toolbar\s*\{[^}]*padding:\s*var\(--space-sm\) var\(--space-md\);[^}]*\}/)?.[0] ?? ""; const singlePaneToolbarRule = css.match(/@media\s*\(max-width:\s*1024px\)[\s\S]*?\.list-toolbar\s*\{[^}]*padding:\s*var\(--space-sm\) var\(--space-md\);[^}]*\}/)?.[0] ?? "";
expect(actionClusterRule).toContain("flex-wrap: nowrap"); expect(actionClusterRule).toContain("flex-wrap: nowrap");
expect(actionClusterRule).toContain("justify-content: center"); expect(actionClusterRule).toContain("justify-content: center");
@@ -2108,7 +2157,23 @@ describe("ListView", () => {
expect(actionClusterRule).toContain("min-width: max-content"); expect(actionClusterRule).toContain("min-width: max-content");
expect(actionClusterRule).toContain("overflow-x: auto"); expect(actionClusterRule).toContain("overflow-x: auto");
expect(toolbarRule).toContain("justify-content: center"); expect(toolbarRule).toContain("justify-content: center");
expect(mobileToolbarRule).toContain("justify-content: center"); expect(singlePaneToolbarRule).toContain("justify-content: center");
});
it("loads CSS fixture rules that apply single-pane non-clipping layout at tablet while keeping desktop split rules", () => {
const css = loadAllAppCss();
const splitHideRule = css.match(/@media\s*\(max-width:\s*1024px\)[\s\S]*?\.list-split-resize-handle,\s*\n\s*\.list-split-detail\s*\{[^}]*display:\s*none;[^}]*\}/)?.[0] ?? "";
const cardRule = css.match(/@media\s*\(max-width:\s*1024px\)[\s\S]*?\.list-table\s*\{[^}]*display:\s*none;[^}]*\}[\s\S]*?\.list-cards\s*\{[^}]*width:\s*100%;[^}]*\}/)?.[0] ?? "";
const desktopSplitRule = css.match(/\.list-split-layout\s*\{[^}]*grid-template-columns:\s*auto 0 minmax\(0, 1fr\);[^}]*\}/)?.[0] ?? "";
expect(splitHideRule).toContain("max-width: 1024px");
expect(splitHideRule).toContain(".list-split-resize-handle");
expect(splitHideRule).toContain("display: none");
expect(cardRule).toContain(".list-table");
expect(cardRule).toContain("display: none");
expect(cardRule).toContain(".list-cards");
expect(cardRule).toContain("width: 100%");
expect(desktopSplitRule).toContain("grid-template-columns: auto 0 minmax(0, 1fr)");
}); });
it("keeps Bulk Edit, View, and + New Task together in the mobile toolbar controls", () => { it("keeps Bulk Edit, View, and + New Task together in the mobile toolbar controls", () => {