From 2e3e7b38505e50e8900cb1407f8ada09782564ff Mon Sep 17 00:00:00 2001
From: Developer
Date: Fri, 26 Jun 2026 12:43:09 +0000
Subject: [PATCH] Mobile Media table: stacked cards + mobile pagination (Slice
3)
Below md, the Media DataTable renders as MobileCardRow cards: title as
primary, plus size/HDR/library/year (3-5 fields, null-safe). Card tap
navigates to /files?path=... (same handleRowClick as desktop). The TanStack
column-visibility toggle is absent below md (the card picks the fields).
Pagination is preserved via a standalone MediaMobilePagination component
that mirrors DataTablePagination semantics (rows count, page-size select,
page indicator, prev/next with correct disabled states) off the raw
PaginationState. The duplication is flagged tech debt -- extracting a shared
TablePagination is a follow-up, out of scope for this slice.
Desktop (md+) is byte-for-byte identical: the isMobile===false branch
renders the same DataTable with the same props. enableRowSelection state is
vestigial (no batch consumer on either path); navigation is the correct
primary mobile interaction.
Tests: 5 new covering mobile cards + hidden column toggle + pagination +
card-tap navigation, and desktop DataTable + column toggle. matchMedia
mocked per-breakpoint. 94 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/ (spec R3, tasks slice 3).
---
frontend/src/pages/Media.tsx | 192 +++++++++++++++++---
frontend/src/pages/__tests__/Media.test.tsx | 93 ++++++++++
2 files changed, 258 insertions(+), 27 deletions(-)
diff --git a/frontend/src/pages/Media.tsx b/frontend/src/pages/Media.tsx
index e6dd536..3bbf835 100644
--- a/frontend/src/pages/Media.tsx
+++ b/frontend/src/pages/Media.tsx
@@ -9,6 +9,10 @@ import type {
} from "@tanstack/react-table";
import { DataTable } from "@/components/ui/data-table";
+import {
+ MobileCardRow,
+ type MobileCardField,
+} from "@/components/ui/mobile-card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -31,6 +35,7 @@ import {
useForceStopBuildIndex,
} from "../hooks/useMedia";
import { usePersistentState } from "../hooks/usePersistentState";
+import { useIsMobile } from "../hooks/useIsMobile";
import type { MediaItem } from "../types";
import { useServiceInstances } from "../hooks/useServices";
import { useCounts, useLibraries } from "../hooks/useDashboard";
@@ -75,6 +80,116 @@ function getMediaRowId(row: MediaItem): string {
return row.path;
}
+// Mobile card fields (spec R3.2): the card picks the 3-5 most important fields.
+// Title is the primary identifier; size/HDR/library/year give the at-a-glance
+// tech + context info a user scanning the library on a phone needs. Runtime,
+// bitrate, resolution, codec etc. live on the desktop table only.
+const mediaCardFields: MobileCardField[] = [
+ { key: "title", label: "Title", render: (r) => r.title, primary: true },
+ { key: "size", label: "Size", render: (r) => r.size || "-" },
+ {
+ key: "hdr",
+ label: "HDR",
+ render: (r) => r.hdr || "-",
+ },
+ { key: "library", label: "Library", render: (r) => r.library || "-" },
+ {
+ key: "year",
+ label: "Year",
+ render: (r) => (r.year != null ? String(r.year) : "-"),
+ },
+];
+
+// Standalone pagination for the mobile card layout. The DataTable renders its
+// own pagination internally; this mirrors that UI (rows count, page-size
+// select, page indicator, prev/next) but works off the raw pagination state
+// instead of a TanStack table instance. See spec R3.3.
+function MediaMobilePagination({
+ pageIndex,
+ pageSize,
+ pageSizeOptions,
+ totalRows,
+ pageCount,
+ onPaginationChange,
+}: {
+ pageIndex: number;
+ pageSize: number;
+ pageSizeOptions: number[];
+ totalRows: number;
+ pageCount: number;
+ onPaginationChange: OnChangeFn;
+}) {
+ return (
+