refactor(tracker): remove dead code and unreferenced files (plan A1)

- Delete unreachable in-file dialogs: MonthlyStateDialog block (setShowMbs
  never called) and inline PaymentModal in TrackerRow + MobileTrackerRow
  (setEditPayment only ever null; editing works via PaymentLedgerDialog).
- Delete fully-unreferenced files: tracker/EditableCell.tsx,
  ui/confirm-dialog.tsx, ui/separator.tsx (+ orphaned @radix-ui/react-separator).
- Simplify OverdueCommandCenter dead branch (early-returns null when empty).
- Correct stale roadmap/FUTURE docs: tracker keyboard nav is implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-05 14:35:13 -05:00
parent db1be4b0bd
commit 3973560e92
11 changed files with 17 additions and 557 deletions

View File

@ -143,16 +143,16 @@ The `payments` table has a `method` column (free-text) but no way to see "how mu
### 🔵 No Keyboard Navigation or Shortcuts — LOW
**Added:** 2026-05-11 by Ripley
**Status:** Partial — Esc closes modals ✅, Cmd+K opens command palette ✅, arrow key tracker navigation ❌
**Status:** Done — Esc closes modals ✅, Cmd+K opens command palette ✅, arrow key tracker navigation ✅
**Description:**
Only a skip link exists for keyboard accessibility. No `Cmd+K` to find a bill, no `Esc` to close modals, no arrow keys to navigate the tracker grid.
Skip link, command palette, and tracker-row keyboard navigation are all implemented.
**Implementation Notes:**
- ✅ `Esc` closes any open modal/dialog (via Radix Dialog default)
- ✅ `Cmd+K` / `Ctrl+K` opens command palette (`CommandPalette.jsx`)
- ❌ Arrow keys navigate tracker rows when grid is focused
- Remaining effort: 1-2 hours
- ✅ `Cmd+K` / `Ctrl+K` opens command palette (`CommandPalette.tsx`)
- ✅ Arrow keys / `j`/`k` navigate tracker rows, `Enter` edits, `p` toggles paid (`TrackerRow.tsx` `handleRowKeyDown`, focusable `[data-tracker-row]`)
- Nice-to-have: a `?`-triggered shortcut legend for discoverability
---

View File

@ -1,104 +0,0 @@
import { useState, useRef } from 'react';
import { toast } from 'sonner';
import { api } from '@/api';
import { createPaymentOrConfirm } from '@/lib/paymentActions';
import { cn, fmt, fmtDate, errMessage } from '@/lib/utils';
import { Input } from '@/components/ui/input';
import type { Dollars } from '@/lib/money';
import type { TrackerRow } from '@/types';
// `threshold` = actual_amount ?? expected_amount for this bill/month
interface EditableCellProps {
row: TrackerRow;
field: 'amount' | 'date';
threshold: Dollars;
defaultPaymentDate: string;
refresh: () => void;
}
export function EditableCell({ row, field, threshold, defaultPaymentDate, refresh }: EditableCellProps) {
const [editing, setEditing] = useState(false);
const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const displayVal = field === 'amount'
? (row.total_paid > 0 ? fmt(row.total_paid) : '—')
: (row.last_paid_date ? fmtDate(row.last_paid_date) : '—');
const isEmpty = field === 'amount' ? row.total_paid <= 0 : !row.last_paid_date;
// Mismatch when paid amount differs from the effective threshold for this month
const mismatch = field === 'amount' && row.total_paid > 0 && row.total_paid !== threshold;
function startEdit() {
if (editing) return;
setValue(field === 'amount'
? (row.total_paid > 0 ? String(row.total_paid) : '')
: (row.last_paid_date || ''));
setEditing(true);
setTimeout(() => { inputRef.current?.focus(); inputRef.current?.select(); }, 0);
}
async function commit() {
setEditing(false);
const val = value.trim();
if (!val) return;
try {
if (row.payments && row.payments.length > 0) {
const update: { amount?: number; paid_date?: string } = {};
if (field === 'amount') update.amount = parseFloat(val);
if (field === 'date') update.paid_date = val;
await api.updatePayment(row.payments[0]!.id, update);
toast.success('Saved');
refresh();
} else {
await createPaymentOrConfirm(
{
bill_id: row.id,
amount: field === 'amount' ? parseFloat(val) : threshold,
paid_date: field === 'date' ? val : defaultPaymentDate,
},
() => { toast.success('Saved'); refresh(); },
);
}
} catch (err) {
toast.error(errMessage(err));
}
}
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') inputRef.current?.blur();
if (e.key === 'Escape') { setValue(''); setEditing(false); }
}
if (editing) {
return (
<Input
ref={inputRef}
type={field === 'date' ? 'date' : 'number'}
step={field === 'amount' ? '0.01' : undefined}
min={field === 'amount' ? '0' : undefined}
value={value}
onChange={e => setValue(e.target.value)}
onBlur={commit}
onKeyDown={onKeyDown}
className="h-7 w-28 text-right font-mono text-sm bg-background/80 border-border/60"
/>
);
}
return (
<span
onClick={startEdit}
title={`Click to edit ${field === 'amount' ? 'payment amount' : 'paid date'}`}
className={cn(
'cursor-pointer rounded-md px-1.5 py-0.5 text-sm font-mono',
'transition-all duration-150 hover:bg-accent hover:ring-1 hover:ring-border',
isEmpty && 'text-muted-foreground',
mismatch && 'text-amber-500',
!isEmpty && !mismatch && 'text-emerald-500',
)}
>
{displayVal}
</span>
);
}

View File

@ -11,7 +11,6 @@ import {
AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
} from '@/components/ui/alert-dialog';
import PaymentModal from '@/components/tracker/PaymentModal';
import { paymentDateForTrackerMonth, paymentSummary, ROW_STATUS_CLS } from '@/lib/trackerUtils';
import { useQuickPay, useTogglePaid } from '@/hooks/usePaymentActions';
import { StatusBadge } from '@/components/tracker/StatusBadge';
@ -20,7 +19,7 @@ import { LowerThisMonthButton } from '@/components/tracker/LowerThisMonthButton'
import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog';
import { NotesCell } from '@/components/tracker/NotesCell';
import { AutopaySuggestionActions } from '@/components/tracker/AutopaySuggestionActions';
import type { Payment, TrackerRow } from '@/types';
import type { TrackerRow } from '@/types';
function MiniSparkline({ values }: { values?: number[] | null }) {
if (!values || values.length < 2) return null;
@ -78,7 +77,6 @@ interface MobileTrackerRowProps {
export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill, moveControls, dragProps, isDrifted = false }: MobileTrackerRowProps) {
const amountRef = useRef<HTMLInputElement>(null);
const [editPayment, setEditPayment] = useState<Payment | null>(null);
const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false);
const [confirmUnpay, setConfirmUnpay] = useState(false);
const [suggestionLoading, setSuggestionLoading] = useState(false);
@ -378,15 +376,6 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill,
</div>
</motion.div>
{editPayment && (
<PaymentModal
payment={editPayment}
autopayEnabled={!!row.autopay_enabled}
onClose={() => setEditPayment(null)}
onSave={refresh}
/>
)}
{paymentLedgerOpen && (
<PaymentLedgerDialog
row={row}

View File

@ -1,146 +0,0 @@
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { api } from '@/api';
import { fmt, errMessage } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from '@/components/ui/dialog';
import type { TrackerRow } from '@/types';
const MONTHS = [
'January','February','March','April','May','June',
'July','August','September','October','November','December',
];
interface MonthlyStateDialogProps {
row: TrackerRow;
year: number;
month: number;
open: boolean;
onOpenChange: (open: boolean) => void;
onSaved: () => void;
}
function MonthlyStateDialog({ row, year, month, open, onOpenChange, onSaved }: MonthlyStateDialogProps) {
const [actualAmount, setActualAmount] = useState('');
const [notes, setNotes] = useState('');
const [isSkipped, setIsSkipped] = useState(false);
const [saving, setSaving] = useState(false);
// Populate from current row state when dialog opens
useEffect(() => {
if (open) {
setActualAmount(row.actual_amount != null ? String(row.actual_amount) : '');
setNotes(row.monthly_notes || '');
setIsSkipped(!!row.is_skipped);
}
}, [open, row]);
async function handleSave(e: React.FormEvent) {
e.preventDefault();
const amt = actualAmount.trim() ? parseFloat(actualAmount) : null;
if (amt !== null && (isNaN(amt) || amt < 0)) {
toast.error('Amount must be a positive number or empty');
return;
}
setSaving(true);
try {
await api.saveBillMonthlyState(row.id, {
year,
month,
actual_amount: amt,
notes: notes.trim() || null,
is_skipped: isSkipped,
});
toast.success(`${MONTHS[month - 1]} state saved`);
onSaved();
onOpenChange(false);
} catch (err) {
toast.error(errMessage(err));
} finally {
setSaving(false);
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-sm border-border/60 bg-card/95 backdrop-blur-xl">
<DialogHeader>
<DialogTitle className="text-base font-semibold tracking-tight">
{row.name}
<span className="text-muted-foreground font-normal ml-2">
{MONTHS[month - 1]} {year}
</span>
</DialogTitle>
<p className="text-[11px] text-muted-foreground">
Monthly overrides changes only affect {MONTHS[month - 1]}
</p>
</DialogHeader>
<form id="mbs-form" onSubmit={handleSave} className="space-y-4 py-1">
{/* Actual amount this month */}
<div className="space-y-1.5">
<Label htmlFor="mbs-amount" className="text-xs uppercase tracking-wider text-muted-foreground">
Actual Amount ($)
</Label>
<Input
id="mbs-amount"
type="number" min="0" step="0.01"
placeholder={String(row.expected_amount)}
value={actualAmount}
onChange={e => setActualAmount(e.target.value)}
className="font-mono bg-background/50 border-border/60"
/>
<p className="text-[11px] text-muted-foreground">
Leave blank to use the template default ({fmt(row.expected_amount)}).
</p>
</div>
{/* Monthly notes */}
<div className="space-y-1.5">
<Label htmlFor="mbs-notes" className="text-xs uppercase tracking-wider text-muted-foreground">
Notes (this month only)
</Label>
<Input
id="mbs-notes"
value={notes}
onChange={e => setNotes(e.target.value)}
placeholder="e.g. higher than usual, double-billed…"
className="bg-background/50 border-border/60"
/>
</div>
{/* Skip this month */}
<label className="flex items-start gap-3 cursor-pointer select-none">
<input
type="checkbox"
checked={isSkipped}
onChange={e => setIsSkipped(e.target.checked)}
className="mt-0.5 h-4 w-4 rounded border-border accent-primary"
/>
<div>
<p className="text-sm font-medium leading-tight">Skip this month</p>
<p className="text-[11px] text-muted-foreground mt-0.5">
Excludes this bill from {MONTHS[month - 1]} totals. Other months are unchanged.
</p>
</div>
</label>
</form>
<DialogFooter className="mt-2">
<Button type="button" variant="ghost" disabled={saving} onClick={() => onOpenChange(false)} className="text-xs">
Cancel
</Button>
<Button type="submit" form="mbs-form" disabled={saving} className="text-xs">
{saving ? 'Saving…' : 'Save'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default MonthlyStateDialog;

View File

@ -193,15 +193,11 @@ export default function OverdueCommandCenter({ rows, year, month, refresh, onPay
<div className="flex items-center gap-2.5">
<AlertCircle className="h-4 w-4 shrink-0 text-rose-400" />
<span className="text-sm font-semibold text-foreground">
{overdueRows.length === 0
? 'No active overdue bills'
: `${overdueRows.length} overdue ${overdueRows.length === 1 ? 'bill' : 'bills'}`}
{`${overdueRows.length} overdue ${overdueRows.length === 1 ? 'bill' : 'bills'}`}
</span>
<span className="font-mono text-sm text-rose-400 dark:text-rose-300">
{fmt(asDollars(totalOverdue))}
</span>
{overdueRows.length > 0 && (
<span className="font-mono text-sm text-rose-400 dark:text-rose-300">
{fmt(asDollars(totalOverdue))}
</span>
)}
</div>
<div className="flex items-center gap-3">
{snoozedRows.length > 0 && (

View File

@ -14,8 +14,6 @@ import {
AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
} from '@/components/ui/alert-dialog';
import MonthlyStateDialog from '@/components/tracker/MonthlyStateDialog';
import PaymentModal from '@/components/tracker/PaymentModal';
import { paymentDateForTrackerMonth, paymentSummary, ROW_STATUS_CLS } from '@/lib/trackerUtils';
import { useQuickPay, useTogglePaid } from '@/hooks/usePaymentActions';
import { DEFAULT_TRACKER_TABLE_COLUMNS } from '@/lib/trackerTableColumns';
@ -24,7 +22,7 @@ import { PaymentProgress } from '@/components/tracker/PaymentProgress';
import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog';
import { NotesCell } from '@/components/tracker/NotesCell';
import { AutopaySuggestionActions } from '@/components/tracker/AutopaySuggestionActions';
import type { Payment, TrackerRow as TrackerRowData } from '@/types';
import type { TrackerRow as TrackerRowData } from '@/types';
import type { DragProps, MoveControls } from '@/components/tracker/MobileTrackerRow';
// Cast type for the HTML5 drag handlers, which conflict with framer-motion's own
@ -46,9 +44,7 @@ interface TrackerRowProps {
export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveControls, dragProps, isDrifted, visibleColumns = DEFAULT_TRACKER_TABLE_COLUMNS }: TrackerRowProps) {
const amountRef = useRef<HTMLInputElement>(null);
const [editPayment, setEditPayment] = useState<Payment | null>(null);
const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false);
const [showMbs, setShowMbs] = useState(false);
const [confirmUnpay, setConfirmUnpay] = useState(false);
const [suggestionLoading, setSuggestionLoading] = useState(false);
const [optimisticActual, setOptimisticActual] = useState<Dollars | undefined>(undefined);
@ -690,15 +686,6 @@ export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveC
)}
</TableRow>
{editPayment && (
<PaymentModal
payment={editPayment}
autopayEnabled={!!row.autopay_enabled}
onClose={() => setEditPayment(null)}
onSave={refresh}
/>
)}
{paymentLedgerOpen && (
<PaymentLedgerDialog
row={row}
@ -711,17 +698,6 @@ export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveC
/>
)}
{showMbs && (
<MonthlyStateDialog
row={row}
year={year}
month={month}
open={showMbs}
onOpenChange={setShowMbs}
onSaved={refresh}
/>
)}
<AlertDialog open={confirmUnpay} onOpenChange={setConfirmUnpay}>
<AlertDialogContent>
<AlertDialogHeader>

View File

@ -1,181 +0,0 @@
import * as React from 'react';
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
type ConfirmVariant = 'default' | 'destructive';
interface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title?: string;
description?: React.ReactNode;
confirmLabel?: string;
cancelLabel?: string;
variant?: ConfirmVariant;
onConfirm?: () => void | Promise<void>;
loading?: boolean;
}
/* ConfirmDialog
Reusable confirmation dialog that replaces window.confirm. */
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
variant = 'default',
onConfirm,
loading = false,
}: ConfirmDialogProps) {
const handleConfirm = async () => {
await onConfirm?.();
};
const handleCancel = () => {
if (!loading) onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={loading ? undefined : onOpenChange}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description && (
<DialogDescription>{description}</DialogDescription>
)}
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={handleCancel}
disabled={loading}
>
{cancelLabel}
</Button>
<Button
variant={variant === 'destructive' ? 'destructive' : 'default'}
onClick={handleConfirm}
disabled={loading}
className={cn(loading && 'cursor-not-allowed')}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/* useConfirm hook
Imperative API for ConfirmDialog.
Usage:
const { confirm, ConfirmDialogComponent } = useConfirm();
const ok = await confirm({ title, description, confirmLabel, variant });
// Render <ConfirmDialogComponent /> anywhere in the tree. */
interface ConfirmState {
open: boolean;
title: string;
description: React.ReactNode;
confirmLabel: string;
cancelLabel: string;
variant: ConfirmVariant;
loading: boolean;
}
interface ConfirmOptions {
title?: string;
description?: React.ReactNode;
confirmLabel?: string;
cancelLabel?: string;
variant?: ConfirmVariant;
}
export function useConfirm() {
const [state, setState] = React.useState<ConfirmState>({
open: false,
title: '',
description: '',
confirmLabel: 'Confirm',
cancelLabel: 'Cancel',
variant: 'default',
loading: false,
});
// Keep a stable ref to the resolve callback so it survives re-renders
const resolveRef = React.useRef<((value: boolean) => void) | null>(null);
const confirm = React.useCallback(
({
title = 'Are you sure?',
description = '',
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
variant = 'default',
}: ConfirmOptions = {}): Promise<boolean> => {
return new Promise((resolve) => {
resolveRef.current = resolve;
setState({
open: true,
title,
description,
confirmLabel,
cancelLabel,
variant,
loading: false,
});
});
},
[]
);
const handleConfirm = React.useCallback(async () => {
setState((s) => ({ ...s, loading: true }));
resolveRef.current?.(true);
resolveRef.current = null;
setState((s) => ({ ...s, open: false, loading: false }));
}, []);
const handleOpenChange = React.useCallback((open: boolean) => {
if (!open) {
resolveRef.current?.(false);
resolveRef.current = null;
setState((s) => ({ ...s, open: false }));
}
}, []);
const ConfirmDialogComponent = React.useCallback(
() => (
<ConfirmDialog
open={state.open}
onOpenChange={handleOpenChange}
title={state.title}
description={state.description}
confirmLabel={state.confirmLabel}
cancelLabel={state.cancelLabel}
variant={state.variant}
loading={state.loading}
onConfirm={handleConfirm}
/>
),
[state, handleConfirm, handleOpenChange]
);
return { confirm, ConfirmDialogComponent };
}

View File

@ -1,21 +0,0 @@
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import type { ComponentProps } from 'react';
import { cn } from '@/lib/utils';
function Separator({ className, orientation = 'horizontal', decorative = true, ref, ...props }: ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
);
}
export { Separator };

47
package-lock.json generated
View File

@ -16,7 +16,6 @@
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
@ -4512,52 +4511,6 @@
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
"integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.4"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",

View File

@ -32,7 +32,6 @@
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",

View File

@ -107,20 +107,19 @@ The `payments` table has a `method` column (free-text) but no way to see "how mu
---
### 🔵 No Keyboard Navigation or Shortcuts — LOW
**Status:** Partially implemented
### 🔵 No Keyboard Navigation or Shortcuts — DONE
**Status:** Implemented
**Description:**
Only a skip link exists for keyboard accessibility. No `Cmd+K` to find a bill, no `Esc` to close modals, no arrow keys to navigate the tracker grid.
A skip link, command palette, and tracker-row keyboard navigation all exist.
**Implementation Status:**
- ✅ `Esc` closes any open modal/dialog (via Radix Dialog default)
- ✅ `Cmd+K` / `Ctrl+K` opens command palette (`CommandPalette.jsx`)
- ❌ Arrow keys navigate tracker rows when grid is focused
- ✅ `Cmd+K` / `Ctrl+K` opens command palette (`CommandPalette.tsx`)
- ✅ Arrow keys / `j`/`k` navigate tracker rows, `Enter` edits, `p` toggles paid (`TrackerRow.tsx` `handleRowKeyDown`, focusable `[data-tracker-row]`)
**Remaining Work:**
- Implement arrow key navigation for tracker rows
- Estimated effort: 1-2 hours
- None. A `?`-triggered shortcut legend for discoverability is a possible nice-to-have.
---