Bernt Christian Egeland пре 2 месеци
родитељ
комит
80514a42a5

+ 4 - 1
src/app/(authenticated)/audit-log/audit-log-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from "@/lib/interactive-row";
 import { useCallback, useState, useTransition } from "react";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
@@ -89,6 +90,7 @@ export function AuditLogClient({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const [selectedLog, setSelectedLog] = useState<AuditLogData["logs"][number] | null>(null);
   const { formatDateTime } = useFormatDate();
   const t = useTranslations("audit");
@@ -136,6 +138,7 @@ export function AuditLogClient({
               navigate({ search: value || undefined });
             }}
             className="h-9 pl-9 pr-9"
+            {...tableNav.searchInputProps}
           />
           {isPending && <Loader2 className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 animate-spin text-muted-foreground" />}
         </div>
@@ -220,7 +223,7 @@ export function AuditLogClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-md border md:block">
+      <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <Table>
           <TableHeader>

+ 4 - 1
src/app/(authenticated)/billing/billing-client.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 import { useCallback, useTransition } from "react";
@@ -104,6 +105,7 @@ export default function BillingClient({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
 
   const createQueryString = useCallback(
     (params: Record<string, string>) => {
@@ -311,6 +313,7 @@ export default function BillingClient({
               value={searchValue}
               onChange={(e) => setSearchValue(e.target.value)}
               className="h-9 w-full pl-9 sm:w-[250px]"
+              {...tableNav.searchInputProps}
             />
           </div>
           <Button
@@ -383,7 +386,7 @@ export default function BillingClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-md border md:block">
+      <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
         <Table className="table-fixed">
           <TableHeader>
             <TableRow>

+ 3 - 1
src/app/(authenticated)/customers/[id]/customer-detail-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useTransition } from "react";
 import Link from "next/link";
@@ -122,6 +123,7 @@ export function CustomerDetailClient({
   const searchParams = useSearchParams();
   const [showEditForm, setShowEditForm] = useState(false);
   const [showVehicleForm, setShowVehicleForm] = useState(false);
+  const tableNav = useTableKeyboardNav();
 
   const tabParam = searchParams.get("tab");
   const activeTab =
@@ -381,7 +383,7 @@ export function CustomerDetailClient({
               </div>
 
               {/* Table (md and up) */}
-              <div className="hidden rounded-lg border md:block">
+              <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
                 <Table>
                   <TableHeader>
                     <TableRow>

+ 4 - 1
src/app/(authenticated)/customers/customers-client.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { interactiveRow } from '@/lib/interactive-row';
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition, useEffect } from "react";
@@ -94,6 +95,7 @@ export function CustomersClient({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const [showForm, setShowForm] = useState(false);
   const [showImport, setShowImport] = useState(false);
   const [editCustomer, setEditCustomer] = useState<Customer | null>(null);
@@ -242,6 +244,7 @@ export function CustomersClient({
                   value={searchInput}
                   onChange={(e) => setSearchInput(e.target.value)}
                   className="h-9 pl-9"
+                  {...tableNav.searchInputProps}
                 />
               </form>
               {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -349,7 +352,7 @@ export function CustomersClient({
       </div>
 
       {/* Table (md and up) - only this scrolls */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <div className="overflow-auto max-h-[calc(100vh-220px)]">
         <Table className="table-fixed">

+ 7 - 3
src/app/(authenticated)/dashboard-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from '@/lib/interactive-row';
 import { useState, useTransition } from "react";
 import { useRouter } from "next/navigation";
@@ -299,6 +300,9 @@ export function DashboardClient({
   const router = useRouter();
   const { formatDate } = useFormatDate();
   const [navigatingId, setNavigatingId] = useState<string | null>(null);
+  const recentNav = useTableKeyboardNav();
+  const activeNav = useTableKeyboardNav();
+  const obsNav = useTableKeyboardNav();
   const [dismissingId, setDismissingId] = useState<string | null>(null);
   const [maintenanceTab, setMaintenanceTab] = useState<"active" | "dismissed">("active");
   const [restoringId, setRestoringId] = useState<string | null>(null);
@@ -1285,7 +1289,7 @@ export function DashboardClient({
                 )}
               </div>
 
-              <div className="hidden md:block">
+              <div className="hidden md:block" {...recentNav.containerProps}>
               <Table>
                 <TableHeader>
                   <TableRow>
@@ -1458,7 +1462,7 @@ export function DashboardClient({
                 )}
               </div>
 
-              <div className="hidden md:block">
+              <div className="hidden md:block" {...activeNav.containerProps}>
               <Table>
                 <TableHeader>
                   <TableRow>
@@ -1669,7 +1673,7 @@ export function DashboardClient({
                 ))}
               </div>
 
-              <div className="hidden md:block">
+              <div className="hidden md:block" {...obsNav.containerProps}>
               <Table>
                 <TableHeader>
                   <TableRow>

+ 4 - 1
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
@@ -158,6 +159,7 @@ export function InspectionsClient({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const [showNewDialog, setShowNewDialog] = useState(false);
   const tcm = useTranslations("common.contextMenu");
   const t = useTranslations("inspections.list");
@@ -238,6 +240,7 @@ export function InspectionsClient({
               value={searchInput}
               onChange={(e) => setSearchInput(e.target.value)}
               className="h-9 pl-9"
+              {...tableNav.searchInputProps}
             />
           </form>
           {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -304,7 +307,7 @@ export function InspectionsClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <Table className="table-fixed">
           <TableHeader>

+ 4 - 1
src/app/(authenticated)/inventory/inventory-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 import { isLow as isLowStock } from '@/features/inventory/Lib/lowStockAlerts';
@@ -144,6 +145,7 @@ export function InventoryClient({
   const searchParams = useSearchParams();
   const t = useTranslations('inventory');
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const [showForm, setShowForm] = useState(false);
   const [editPart, setEditPart] = useState<InventoryPart | null>(null);
   const [showMarkup, setShowMarkup] = useState(false);
@@ -360,6 +362,7 @@ export function InventoryClient({
                 value={searchInput}
                 onChange={(e) => setSearchInput(e.target.value)}
                 className="h-9 pl-9"
+                {...tableNav.searchInputProps}
               />
             </form>
             <Select
@@ -550,7 +553,7 @@ export function InventoryClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <Table>
           <TableHeader>

+ 4 - 1
src/app/(authenticated)/labor-presets/labor-presets-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
@@ -72,6 +73,7 @@ export function LaborPresetsClient({
   const searchParams = useSearchParams();
   const t = useTranslations("laborPresets");
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const [showForm, setShowForm] = useState(false);
   const [editPreset, setEditPreset] = useState<{
     id: string;
@@ -163,6 +165,7 @@ export function LaborPresetsClient({
               value={searchInput}
               onChange={(e) => setSearchInput(e.target.value)}
               className="h-9 pl-9"
+              {...tableNav.searchInputProps}
             />
           </form>
           {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -252,7 +255,7 @@ export function LaborPresetsClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <Table>
           <TableHeader>

+ 4 - 1
src/app/(authenticated)/observations/observations-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from "@/lib/interactive-row";
 import { useCallback, useTransition } from "react";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
@@ -83,6 +84,7 @@ export function ObservationsClient({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const { formatDate } = useFormatDate();
   const t = useTranslations("vehicles.observationsPage");
   const tf = useTranslations("vehicles.findings");
@@ -123,6 +125,7 @@ export function ObservationsClient({
               navigate({ search: value || undefined });
             }}
             className="h-9 pl-9 pr-9"
+            {...tableNav.searchInputProps}
           />
           {isPending && <Loader2 className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 animate-spin text-muted-foreground" />}
         </div>
@@ -197,7 +200,7 @@ export function ObservationsClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-md border md:block">
+      <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <Table>
           <TableHeader>

+ 4 - 1
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -1,6 +1,7 @@
 'use client'
 
 import { interactiveRow } from '@/lib/interactive-row';
+import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition, useEffect } from 'react'
@@ -97,6 +98,7 @@ export function QuotesClient({
   const pathname = usePathname()
   const searchParams = useSearchParams()
   const [isPending, startTransition] = useTransition()
+  const tableNav = useTableKeyboardNav()
   const t = useTranslations('quotes')
   const tcm = useTranslations('common.contextMenu')
 
@@ -191,6 +193,7 @@ export function QuotesClient({
               value={searchInput}
               onChange={(e) => setSearchInput(e.target.value)}
               className="h-9 pl-9"
+              {...tableNav.searchInputProps}
             />
           </form>
           {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -257,7 +260,7 @@ export function QuotesClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         <Table className="table-fixed">
           <TableHeader>

+ 3 - 1
src/app/(authenticated)/vehicles/[id]/notes-table.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from "@/lib/interactive-row";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useCallback, useTransition } from "react";
@@ -74,6 +75,7 @@ export function NotesTable({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const t = useTranslations("vehicles.notes");
 
   const createUrl = useCallback(
@@ -186,7 +188,7 @@ export function NotesTable({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <Table>
           <TableHeader>
             <TableRow>

+ 4 - 1
src/app/(authenticated)/vehicles/[id]/service-records-table.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
@@ -119,6 +120,7 @@ export function ServiceRecordsTable({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const [navigatingId, setNavigatingId] = useState<string | null>(null);
   const [isExporting, setIsExporting] = useState(false);
   const t = useTranslations("vehicles.services");
@@ -198,6 +200,7 @@ export function ServiceRecordsTable({
             value={searchInput}
             onChange={(e) => setSearchInput(e.target.value)}
             className="h-9 pl-9"
+            {...tableNav.searchInputProps}
           />
         </form>
         <div className="flex items-center gap-2 sm:flex-1">
@@ -340,7 +343,7 @@ export function ServiceRecordsTable({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <Table>
           <TableHeader>
             <TableRow>

+ 5 - 2
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { interactiveRow } from '@/lib/interactive-row'
 import { useState, useTransition, useCallback } from 'react'
 import { useRouter, useSearchParams } from 'next/navigation'
@@ -345,6 +346,8 @@ export function VehicleDetailClient({
     [searchParams, router, vehicle.id]
   )
   const modal = useGlassModal()
+  const quotesNav = useTableKeyboardNav()
+  const inspectionsNav = useTableKeyboardNav()
   const [showEditForm, setShowEditForm] = useState(false)
   const [showNoteForm, setShowNoteForm] = useState(false)
   const [editingNote, setEditingNote] = useState<PaginatedNotes['records'][number] | undefined>()
@@ -1265,7 +1268,7 @@ export function VehicleDetailClient({
             </div>
 
             {/* Table (md and up) */}
-            <div className="hidden rounded-lg border md:block">
+            <div className="hidden rounded-lg border md:block" {...quotesNav.containerProps}>
               <Table>
                 <TableHeader>
                   <TableRow>
@@ -1370,7 +1373,7 @@ export function VehicleDetailClient({
             </div>
 
             {/* Table (md and up) */}
-            <div className="hidden rounded-lg border md:block">
+            <div className="hidden rounded-lg border md:block" {...inspectionsNav.containerProps}>
               <Table>
                 <TableHeader>
                   <TableRow>

+ 4 - 1
src/app/(authenticated)/vehicles/vehicles-client.tsx

@@ -1,6 +1,7 @@
 'use client'
 
 import { interactiveRow } from '@/lib/interactive-row'
+import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { useState, useCallback, useTransition, useRef, useEffect } from 'react'
 import { useRouter, usePathname, useSearchParams } from 'next/navigation'
 import Link from 'next/link'
@@ -131,6 +132,7 @@ export function VehiclesClient({
   const [editVehicle, setEditVehicle] = useState<Vehicle | null>(null)
   const [view, setView] = useState<'table' | 'grid' | 'grid6'>(initialView)
   const [archiveTarget, setArchiveTarget] = useState<{ id: string; name: string } | null>(null)
+  const tableNav = useTableKeyboardNav()
   const modal = useGlassModal()
   const confirm = useConfirm()
 
@@ -270,6 +272,7 @@ export function VehiclesClient({
               defaultValue={search}
               onChange={(e) => handleSearchChange(e.target.value)}
               className="h-9 pl-9"
+              {...tableNav.searchInputProps}
             />
           </div>
           {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -406,7 +409,7 @@ export function VehiclesClient({
         </div>
 
         {/* Table (md and up) */}
-        <div className="hidden rounded-lg border md:block">
+        <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
           <TableContextMenuHint />
           <Table className="table-fixed">
             <TableHeader>

+ 4 - 1
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { interactiveRow } from '@/lib/interactive-row';
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition } from "react";
@@ -158,6 +159,7 @@ export function WorkOrdersClient({
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
+  const tableNav = useTableKeyboardNav();
   const t = useTranslations("workOrders.list");
   const [navigatingId, setNavigatingId] = useState<string | null>(null);
   const [showPicker, setShowPicker] = useState(false);
@@ -272,6 +274,7 @@ export function WorkOrdersClient({
               value={searchInput}
               onChange={(e) => setSearchInput(e.target.value)}
               className="h-9 pl-9"
+              {...tableNav.searchInputProps}
             />
           </form>
           {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -350,7 +353,7 @@ export function WorkOrdersClient({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <TableContextMenuHint />
         {/* Low-priority columns drop out at sm/md/lg; the min-width stops what
             is left from being squeezed to a few characters, scrolling the

+ 4 - 1
src/features/admin/Components/admin-users.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav';
 import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
@@ -91,6 +92,7 @@ export function AdminUsers({
   const confirm = useConfirm()
   const { formatDate, formatDateTime } = useFormatDate()
   const [isPending, startTransition] = useTransition()
+  const tableNav = useTableKeyboardNav();
 
   const navigate = useCallback(
     (params: Record<string, string | number | undefined>) => {
@@ -197,6 +199,7 @@ export function AdminUsers({
             value={searchInput}
             onChange={(e) => setSearchInput(e.target.value)}
             className="h-9 pl-9"
+            {...tableNav.searchInputProps}
           />
         </form>
         {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
@@ -296,7 +299,7 @@ export function AdminUsers({
       </div>
 
       {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
+      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
         <Table>
           <TableHeader>
             <TableRow>

+ 3 - 1
src/features/dashboard/Components/CustomTableCard.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from "@/lib/interactive-row";
 import { useEffect, useState } from "react";
 import { useRouter } from "next/navigation";
@@ -41,6 +42,7 @@ export function CustomTableCard({
   const { formatDate } = useFormatDate();
   const [rows, setRows] = useState<CardRow[] | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const tableNav = useTableKeyboardNav();
 
   const configKey = JSON.stringify(widget.config);
   useEffect(() => {
@@ -137,7 +139,7 @@ export function CustomTableCard({
             ))}
           </div>
 
-          <div className="hidden md:block">
+          <div className="hidden md:block" {...tableNav.containerProps}>
           <Table>
             <TableHeader>
               <TableRow>

+ 3 - 1
src/features/status-reports/Components/StatusReportList.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { useTableKeyboardNav } from "@/hooks/use-table-keyboard-nav";
 import { interactiveRow } from "@/lib/interactive-row";
 import { useState } from "react";
 import { useRouter } from "next/navigation";
@@ -97,6 +98,7 @@ export function StatusReportList({
   const t = useTranslations("statusReport.list");
   const router = useRouter();
   const [showCreate, setShowCreate] = useState(false);
+  const tableNav = useTableKeyboardNav();
   const [sendReportId, setSendReportId] = useState<string | null>(null);
   const [deleteReportId, setDeleteReportId] = useState<string | null>(null);
   const [deleting, setDeleting] = useState(false);
@@ -256,7 +258,7 @@ export function StatusReportList({
           </div>
 
           {/* Table (md and up) */}
-          <div className="hidden rounded-md border md:block">
+          <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
             <TableContextMenuHint />
             <Table>
               <TableHeader>

+ 3 - 1
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { interactiveRow } from '@/lib/interactive-row'
 import { useRef, useState, useCallback } from 'react'
 import { useRouter } from 'next/navigation'
@@ -77,6 +78,7 @@ export function MyActiveJobs({
   const [uploading, setUploading] = useState<{ jobId: string; type: 'photo' | 'video' } | null>(
     null
   )
+  const tableNav = useTableKeyboardNav()
   const [imageCounts, setImageCounts] = useState<Record<string, number>>(() =>
     Object.fromEntries(jobs.map((j) => [j.id, j.imageCount]))
   )
@@ -309,7 +311,7 @@ export function MyActiveJobs({
         badge={jobs.length || undefined}
         contentClassName="p-0"
       >
-          <div className="divide-y">
+          <div className="divide-y" {...tableNav.containerProps}>
             {jobs.map((job) => {
               const StatusIcon = STATUS_ICON[job.status] || Wrench
               const statusColor = STATUS_COLOR[job.status] || 'bg-muted text-muted-foreground'

+ 89 - 0
src/hooks/use-table-keyboard-nav.ts

@@ -0,0 +1,89 @@
+"use client";
+
+import { useRef, type KeyboardEvent, type RefObject } from "react";
+
+/**
+ * Keyboard navigation for a list of interactive rows (rows made focusable by
+ * `interactiveRow()` — the hook finds them via their `data-row-interactive`
+ * attribute, so the two compose without any per-row wiring).
+ *
+ *   const nav = useTableKeyboardNav();
+ *   <Input {...nav.searchInputProps} />        // Tab jumps to the first row
+ *   <div {...nav.containerProps}>              // wraps the Table
+ *     <TableRow {...interactiveRow(open)} />   // rows as usual
+ *   </div>
+ *
+ * Inside the container, while focus is on a row:
+ *  - Tab / ArrowDown  → next row
+ *  - Shift+Tab / ArrowUp → previous row
+ *  - Home / End       → first / last row
+ *  - Enter / Space    → activate (handled by interactiveRow itself)
+ * Tab past the last row, or Shift+Tab before the first, falls through to the
+ * browser so the widget never traps focus. Keys pressed on controls *inside*
+ * a row (kebab menus, checkboxes) are left alone.
+ */
+export function useTableKeyboardNav<T extends HTMLElement = HTMLDivElement>() {
+  const containerRef = useRef<T | null>(null);
+
+  const rows = () =>
+    Array.from(
+      containerRef.current?.querySelectorAll<HTMLElement>("[data-row-interactive]") ?? [],
+    );
+
+  const onKeyDown = (e: KeyboardEvent<T>) => {
+    const list = rows();
+    const current = list.indexOf(document.activeElement as HTMLElement);
+    if (current === -1) return; // focus is on an inner control, not a row
+
+    const go = (target: HTMLElement | undefined) => {
+      if (!target) return;
+      e.preventDefault();
+      target.focus();
+    };
+
+    switch (e.key) {
+      case "ArrowDown":
+        e.preventDefault();
+        list[current + 1]?.focus();
+        break;
+      case "ArrowUp":
+        e.preventDefault();
+        list[current - 1]?.focus();
+        break;
+      case "Tab":
+        // Roving Tab inside the list; at either end it falls through, so the
+        // rest of the page stays reachable.
+        if (e.shiftKey) {
+          if (current > 0) go(list[current - 1]);
+        } else if (current < list.length - 1) {
+          go(list[current + 1]);
+        }
+        break;
+      case "Home":
+        go(list[0]);
+        break;
+      case "End":
+        go(list[list.length - 1]);
+        break;
+    }
+  };
+
+  const searchInputProps = {
+    onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => {
+      if (e.key === "Tab" && !e.shiftKey) {
+        const first = rows()[0];
+        if (first) {
+          e.preventDefault();
+          first.focus();
+        }
+      }
+    },
+  };
+
+  return {
+    containerRef: containerRef as RefObject<T | null>,
+    containerProps: { ref: containerRef, onKeyDown },
+    /** Spread on the search input so Tab jumps straight to the first row. */
+    searchInputProps,
+  };
+}