- Delete DynamicReport* components and pivot types (PivotConfig, PivotResult, PivotFieldId, etc.) - Remove getDynamicReportData/getDynamicFilterValues from reportService - Strip pivotConfig/pivotResult from useReports hook and ReportsPage - Drop "dynamic" from ReportTab union - Remove reports.pivot.* and reports.dynamic i18n keys in FR and EN - Add skeletons for /reports/highlights, /trends, /compare, /category pages - Register the 4 new sub-routes in App.tsx - Add reports.hub, reports.viewMode, reports.empty, common.underConstruction keys - New shared ContextMenu component with click-outside + Escape handling - Refactor ChartContextMenu to compose generic ContextMenu - New ViewModeToggle with localStorage persistence via storageKey - New Sparkline (Recharts LineChart) for compact trends - Unit tests for readViewMode helper Fixes #69 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { BarChart3, Table } from "lucide-react";
|
|
|
|
export type ViewMode = "chart" | "table";
|
|
|
|
export interface ViewModeToggleProps {
|
|
value: ViewMode;
|
|
onChange: (mode: ViewMode) => void;
|
|
/** localStorage key used to persist the preference per section. */
|
|
storageKey?: string;
|
|
}
|
|
|
|
export function readViewMode(storageKey: string, fallback: ViewMode = "chart"): ViewMode {
|
|
if (typeof localStorage === "undefined") return fallback;
|
|
const saved = localStorage.getItem(storageKey);
|
|
return saved === "chart" || saved === "table" ? saved : fallback;
|
|
}
|
|
|
|
export default function ViewModeToggle({ value, onChange, storageKey }: ViewModeToggleProps) {
|
|
const { t } = useTranslation();
|
|
|
|
useEffect(() => {
|
|
if (storageKey) localStorage.setItem(storageKey, value);
|
|
}, [value, storageKey]);
|
|
|
|
const options: { mode: ViewMode; icon: React.ReactNode; label: string }[] = [
|
|
{ mode: "chart", icon: <BarChart3 size={14} />, label: t("reports.viewMode.chart") },
|
|
{ mode: "table", icon: <Table size={14} />, label: t("reports.viewMode.table") },
|
|
];
|
|
|
|
return (
|
|
<div className="inline-flex gap-1" role="group" aria-label={t("reports.viewMode.chart")}>
|
|
{options.map(({ mode, icon, label }) => (
|
|
<button
|
|
key={mode}
|
|
type="button"
|
|
onClick={() => onChange(mode)}
|
|
aria-pressed={value === mode}
|
|
className={`inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
value === mode
|
|
? "bg-[var(--primary)] text-white"
|
|
: "bg-[var(--card)] border border-[var(--border)] text-[var(--foreground)] hover:bg-[var(--muted)]"
|
|
}`}
|
|
>
|
|
{icon}
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|