mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(charts): persist per-chart overlay prefs (default off; clean line)
Hook the overlay-controls dropdown into a TanStack Query mutation so
each chart's three toggle states (7-day trend / Trend arrow / Target
range) save per user and survive reloads. The prefs piggy-back on the
existing User.dashboardWidgetsJson blob with a `chartOverlayPrefs`
keyed by chart id, mirroring the v1.4.16 phase B8 comparison-baseline
pattern — no Prisma migration needed.
Wires up:
- GET /api/dashboard/widgets surfaces the resolved prefs in its
response so a fresh dashboard load knows which overlays to paint.
- PUT /api/dashboard/chart-overlay-prefs replaces a single chart's
prefs without round-tripping the whole layout.
- Existing PUT /api/dashboard/widgets preserves any chartOverlayPrefs
the client doesn't send, so saving widget visibility doesn't wipe
chart prefs.
- useChartOverlayPrefs hook reads + optimistically updates the cache.
- HealthChart, MoodChart, MedicationComplianceChart all consume the
hook, render the controls dropdown next to the range tabs, and gate
their three overlays behind the persisted toggles.
Default state for every chart, every flag: false. The clean line is
the new default; users opt into overlays they care about per chart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
75f340e82b
commit
d70a9b1ee9
17 files changed
+761
-132
No files matched your search
@@ -41,6 +41,14 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
}),
|
||||
isLoading: false,
|
||||
}),
|
||||
// v1.4.18 — stubs for the overlay-prefs hook.
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
@@ -23,7 +23,13 @@ const sampleData = vi.hoisted(() => {
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: sampleData, isLoading: false }),
|
||||
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: vi.fn(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
@@ -90,11 +96,20 @@ describe("<HealthChart mini>", () => {
|
||||
|
||||
it("renders the range tabs when NOT in mini mode (regression guard)", () => {
|
||||
const html = render(<HealthChart types={["WEIGHT"]} title="Weight" />);
|
||||
// Default render still shows the toggle row.
|
||||
expect(html).toMatch(/7-day trend/i);
|
||||
// Default render still shows the range tabs. The v1.4.18 overlay
|
||||
// toggles moved into a popover dropdown that's only visible when
|
||||
// chartKey is wired; without one the chart skips controls and
|
||||
// stays at the clean-line default.
|
||||
expect(html).toMatch(/data-slot="chart-range-tab"/);
|
||||
});
|
||||
|
||||
it("renders the overlay-controls trigger when chartKey is supplied", () => {
|
||||
const html = render(
|
||||
<HealthChart types={["WEIGHT"]} title="Weight" chartKey="weight" />,
|
||||
);
|
||||
expect(html).toContain("chart-overlay-controls-trigger");
|
||||
});
|
||||
|
||||
it("accepts windowOverride and applies the matching range-points", () => {
|
||||
const html = render(
|
||||
<HealthChart
|
||||
|
||||
@@ -52,6 +52,15 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
}),
|
||||
isLoading: false,
|
||||
}),
|
||||
// Hook-side stubs so the v1.4.18 overlay-prefs hook
|
||||
// (`useChartOverlayPrefs`) doesn't blow up under SSR / test render.
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
@@ -4,9 +4,10 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
/**
|
||||
* v1.4.18 — Medication compliance chart clean-line revert.
|
||||
*
|
||||
* Rolls back B1a's gradient fill. The animation, target / threshold
|
||||
* lines, rich tooltip, and 7-day trend chip stay (those weren't
|
||||
* rejected).
|
||||
* Rolls back B1a's gradient fill. The animation, rich tooltip, and
|
||||
* 7-day trend chip stay (those weren't rejected); the threshold /
|
||||
* goal reference lines are now opt-in via the Target-Range overlay
|
||||
* toggle.
|
||||
*/
|
||||
|
||||
const sampleData = vi.hoisted(() =>
|
||||
@@ -19,6 +20,13 @@ const sampleData = vi.hoisted(() =>
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: sampleData, isLoading: false }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
@@ -17,6 +17,13 @@ import {
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: [], isLoading: false }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
@@ -124,6 +124,13 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
data: { entries: __longSeries, summary: null },
|
||||
isLoading: false,
|
||||
}),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
@@ -33,6 +33,13 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
data: { entries: sampleMoodData, summary: null },
|
||||
isLoading: false,
|
||||
}),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
@@ -15,13 +15,11 @@ import {
|
||||
ReferenceArea,
|
||||
} from "recharts";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, useMemo, useId } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { RichChartTooltip, type RichTooltipRow } from "./chart-tooltip";
|
||||
import { ChartEmptyState } from "./chart-empty-state";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatDateShort } from "@/lib/format";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import {
|
||||
@@ -38,7 +36,12 @@ import {
|
||||
type DataWindow,
|
||||
} from "./mini-window";
|
||||
import { shiftDailySeriesForward } from "@/lib/charts/comparison-shift";
|
||||
import type { ComparisonBaseline } from "@/lib/dashboard-layout";
|
||||
import type {
|
||||
ChartOverlayKey,
|
||||
ComparisonBaseline,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import { ChartOverlayControls } from "./chart-overlay-controls";
|
||||
import { useChartOverlayPrefs } from "@/hooks/use-chart-overlay-prefs";
|
||||
|
||||
const TIME_RANGES_KEYS = [
|
||||
{
|
||||
@@ -114,6 +117,14 @@ interface HealthChartProps {
|
||||
* regression for users who keep the toggle off.
|
||||
*/
|
||||
compareBaseline?: ComparisonBaseline;
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-prefs key. When supplied, the chart
|
||||
* mounts an overlay-controls dropdown in the header and reads its
|
||||
* three toggle states from the persisted user prefs. When omitted
|
||||
* (mini mode, ad-hoc usage) the chart renders without controls and
|
||||
* its toggles default to OFF (clean line).
|
||||
*/
|
||||
chartKey?: ChartOverlayKey;
|
||||
}
|
||||
|
||||
interface ChartDataPoint {
|
||||
@@ -289,6 +300,7 @@ export function HealthChart({
|
||||
mini = false,
|
||||
windowOverride,
|
||||
compareBaseline = "none",
|
||||
chartKey,
|
||||
}: HealthChartProps) {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { t } = useTranslations();
|
||||
@@ -300,12 +312,16 @@ export function HealthChart({
|
||||
? resolveMiniRangePoints(windowOverride)
|
||||
: 30;
|
||||
const [rangePoints, setRangePoints] = useState(initialRangePoints);
|
||||
const [showMA, setShowMA] = useState(false);
|
||||
const [showTrend, setShowTrend] = useState(false);
|
||||
const [showBands, setShowBands] = useState(false);
|
||||
const maToggleId = useId();
|
||||
const trendToggleId = useId();
|
||||
const bandsToggleId = useId();
|
||||
|
||||
// v1.4.18 — three overlay toggles (showTrendIndicator / showTrendArrow
|
||||
// / showTargetRange) are persisted per chart via the new
|
||||
// `useChartOverlayPrefs` hook. When the chart isn't bound to a
|
||||
// persistent key (mini mode / ad-hoc render) the prefs fall through
|
||||
// to the clean-line default (every flag false).
|
||||
const overlayPrefs = useChartOverlayPrefs(chartKey ?? "bp");
|
||||
const showMA = chartKey ? overlayPrefs.prefs.showTrendIndicator : false;
|
||||
const showTrend = chartKey ? overlayPrefs.prefs.showTrendArrow : false;
|
||||
const showBands = chartKey ? overlayPrefs.prefs.showTargetRange : false;
|
||||
|
||||
const bmiDivisor =
|
||||
valueMode === "bmi" && user?.heightCm ? (user.heightCm / 100) ** 2 : null;
|
||||
@@ -868,7 +884,7 @@ export function HealthChart({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-1">
|
||||
<div className="flex flex-wrap items-center justify-end gap-1">
|
||||
{TIME_RANGES_KEYS.map((r) => (
|
||||
<Button
|
||||
key={r.labelKey}
|
||||
@@ -882,6 +898,16 @@ export function HealthChart({
|
||||
{t(r.labelKey)}
|
||||
</Button>
|
||||
))}
|
||||
{/* v1.4.18 — overlay-controls dropdown anchored next to
|
||||
the range tabs. Only painted when the chart is bound
|
||||
to a persistent chartKey; ad-hoc usages keep the
|
||||
clean-line default. */}
|
||||
{chartKey ? (
|
||||
<ChartOverlayControls
|
||||
prefs={overlayPrefs.prefs}
|
||||
onChange={overlayPrefs.setPrefs}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -892,46 +918,6 @@ export function HealthChart({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mini && (
|
||||
<div className="text-muted-foreground mb-3 flex items-center gap-4 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
id={maToggleId}
|
||||
checked={showMA}
|
||||
onCheckedChange={setShowMA}
|
||||
/>
|
||||
<Label htmlFor={maToggleId} className="cursor-pointer text-xs">
|
||||
{t("charts.movingAverage7d")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
id={trendToggleId}
|
||||
checked={showTrend}
|
||||
onCheckedChange={setShowTrend}
|
||||
/>
|
||||
<Label htmlFor={trendToggleId} className="cursor-pointer text-xs">
|
||||
{t("charts.trend")}
|
||||
</Label>
|
||||
</div>
|
||||
{valueBands?.length || targetZones?.length ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
id={bandsToggleId}
|
||||
checked={showBands}
|
||||
onCheckedChange={setShowBands}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={bandsToggleId}
|
||||
className="cursor-pointer text-xs"
|
||||
>
|
||||
{t("charts.targetRanges")}
|
||||
</Label>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mini && showTrend && trendInfo.length > 0 ? (
|
||||
<div className="text-muted-foreground mb-3 flex flex-wrap gap-3 text-xs">
|
||||
{trendInfo.map((info) => (
|
||||
|
||||
@@ -52,7 +52,9 @@ import { formatDateShort } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RichChartTooltip, type RichTooltipRow } from "./chart-tooltip";
|
||||
import { ChartEmptyState } from "./chart-empty-state";
|
||||
import { ChartOverlayControls } from "./chart-overlay-controls";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { useChartOverlayPrefs } from "@/hooks/use-chart-overlay-prefs";
|
||||
|
||||
interface DailyCompliancePoint {
|
||||
/** Berlin calendar day, "YYYY-MM-DD". */
|
||||
@@ -172,6 +174,13 @@ export function MedicationComplianceChart({
|
||||
const fmt = useFormatters();
|
||||
const [days, setDays] = useState<RangeDays>(30);
|
||||
|
||||
// v1.4.18 — three overlay toggles persisted per chart. The 7-day
|
||||
// trend chip and the goal/threshold reference lines used to render
|
||||
// unconditionally; both now key off the persisted prefs.
|
||||
const overlayPrefs = useChartOverlayPrefs("medications");
|
||||
const showTrendChip = overlayPrefs.prefs.showTrendIndicator;
|
||||
const showTargetRange = overlayPrefs.prefs.showTargetRange;
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["medication-compliance-chart", days],
|
||||
queryFn: async (): Promise<DailyCompliancePoint[]> => {
|
||||
@@ -241,10 +250,10 @@ export function MedicationComplianceChart({
|
||||
<div className="flex items-center gap-2">
|
||||
<Pill className="text-muted-foreground h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">{displayTitle}</h3>
|
||||
{/* v1.4.16 A6 — 7-day trend chip (mirror of the BP/weight
|
||||
chart's bucket-chip). Painted only when the trend is
|
||||
computable (>= 2 daily points). */}
|
||||
{trend && TrendIcon ? (
|
||||
{/* v1.4.18 — 7-day trend chip is now opt-in via the
|
||||
"7-day trend" overlay toggle. Default OFF; the user
|
||||
activates it from the chart settings popover. */}
|
||||
{showTrendChip && trend && TrendIcon ? (
|
||||
<span
|
||||
className={cn(
|
||||
"bg-muted/40 inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium tracking-wide uppercase",
|
||||
@@ -261,7 +270,7 @@ export function MedicationComplianceChart({
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-1">
|
||||
<div className="flex flex-wrap items-center justify-end gap-1">
|
||||
{RANGE_DAYS.map((r) => (
|
||||
<Button
|
||||
key={r}
|
||||
@@ -273,6 +282,12 @@ export function MedicationComplianceChart({
|
||||
{r}T
|
||||
</Button>
|
||||
))}
|
||||
{/* v1.4.18 — overlay-controls dropdown next to the range
|
||||
tabs. */}
|
||||
<ChartOverlayControls
|
||||
prefs={overlayPrefs.prefs}
|
||||
onChange={overlayPrefs.setPrefs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -384,24 +399,29 @@ export function MedicationComplianceChart({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* v1.4.16 A6 — minimum-acceptable threshold (yellow,
|
||||
paler dash) and 100 % goal line (green, solid-ish).
|
||||
Two lines together visualise "target range" the way
|
||||
HealthChart's targetZone band does for BP/weight. */}
|
||||
<ReferenceLine
|
||||
y={80}
|
||||
stroke={COLOR_THRESHOLD}
|
||||
strokeDasharray="3 5"
|
||||
strokeOpacity={0.6}
|
||||
data-slot="medication-threshold-line"
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={100}
|
||||
stroke={COLOR_GOAL}
|
||||
strokeDasharray="5 5"
|
||||
strokeOpacity={0.85}
|
||||
data-slot="medication-goal-line"
|
||||
/>
|
||||
{/* v1.4.18 — minimum-acceptable threshold + goal line
|
||||
are now opt-in via the "Target range" overlay
|
||||
toggle. Default OFF; chart renders as a clean line
|
||||
until the user activates the overlay from the
|
||||
settings popover. */}
|
||||
{showTargetRange ? (
|
||||
<>
|
||||
<ReferenceLine
|
||||
y={80}
|
||||
stroke={COLOR_THRESHOLD}
|
||||
strokeDasharray="3 5"
|
||||
strokeOpacity={0.6}
|
||||
data-slot="medication-threshold-line"
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={100}
|
||||
stroke={COLOR_GOAL}
|
||||
strokeDasharray="5 5"
|
||||
strokeOpacity={0.85}
|
||||
data-slot="medication-goal-line"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{/* v1.4.18 — gradient Area removed; clean line only. */}
|
||||
<Line
|
||||
type="monotone"
|
||||
|
||||
@@ -14,10 +14,8 @@ import {
|
||||
ReferenceLine,
|
||||
} from "recharts";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, useMemo, useId } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { formatDateShort } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -31,7 +29,12 @@ import { RichChartTooltip, type RichTooltipRow } from "./chart-tooltip";
|
||||
import { ChartEmptyState } from "./chart-empty-state";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { shiftDailySeriesForward } from "@/lib/charts/comparison-shift";
|
||||
import type { ComparisonBaseline } from "@/lib/dashboard-layout";
|
||||
import type {
|
||||
ChartOverlayKey,
|
||||
ComparisonBaseline,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import { ChartOverlayControls } from "./chart-overlay-controls";
|
||||
import { useChartOverlayPrefs } from "@/hooks/use-chart-overlay-prefs";
|
||||
|
||||
// --- Types ---
|
||||
|
||||
@@ -71,6 +74,12 @@ interface MoodChartProps {
|
||||
* a single metric so only one comparison line is drawn.
|
||||
*/
|
||||
compareBaseline?: ComparisonBaseline;
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-prefs key. When supplied, the chart
|
||||
* mounts the overlay-controls dropdown and reads its three toggle
|
||||
* states from the persisted user prefs. Default: "mood".
|
||||
*/
|
||||
chartKey?: ChartOverlayKey;
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
@@ -261,6 +270,7 @@ export function MoodChart({
|
||||
mini = false,
|
||||
windowOverride,
|
||||
compareBaseline = "none",
|
||||
chartKey = "mood",
|
||||
}: MoodChartProps) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t } = useTranslations();
|
||||
@@ -268,12 +278,13 @@ export function MoodChart({
|
||||
? MINI_RANGE_POINTS[windowOverride]
|
||||
: 30;
|
||||
const [rangePoints, setRangePoints] = useState(initialRangePoints);
|
||||
const [showMA, setShowMA] = useState(false);
|
||||
const [showTrend, setShowTrend] = useState(false);
|
||||
const [showBands, setShowBands] = useState(false);
|
||||
const maToggleId = useId();
|
||||
const trendToggleId = useId();
|
||||
const bandsToggleId = useId();
|
||||
|
||||
// v1.4.18 — three overlay toggles persisted per chart. Mini-mode
|
||||
// renders without controls and stays at the clean-line default.
|
||||
const overlayPrefs = useChartOverlayPrefs(chartKey);
|
||||
const showMA = !mini && overlayPrefs.prefs.showTrendIndicator;
|
||||
const showTrend = !mini && overlayPrefs.prefs.showTrendArrow;
|
||||
const showBands = !mini && overlayPrefs.prefs.showTargetRange;
|
||||
|
||||
const displayTitle = title ?? t("charts.mood");
|
||||
|
||||
@@ -536,7 +547,7 @@ export function MoodChart({
|
||||
)}
|
||||
</div>
|
||||
{!mini && (
|
||||
<div className="flex gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{TIME_RANGES_KEYS.map((r) => (
|
||||
<Button
|
||||
key={r.labelKey}
|
||||
@@ -550,49 +561,15 @@ export function MoodChart({
|
||||
{t(r.labelKey)}
|
||||
</Button>
|
||||
))}
|
||||
{/* v1.4.18 — overlay-controls dropdown next to the
|
||||
range tabs. */}
|
||||
<ChartOverlayControls
|
||||
prefs={overlayPrefs.prefs}
|
||||
onChange={overlayPrefs.setPrefs}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!mini && (
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
id={maToggleId}
|
||||
checked={showMA}
|
||||
onCheckedChange={setShowMA}
|
||||
/>
|
||||
<Label htmlFor={maToggleId} className="cursor-pointer text-xs">
|
||||
{t("charts.moodMA")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
id={trendToggleId}
|
||||
checked={showTrend}
|
||||
onCheckedChange={setShowTrend}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={trendToggleId}
|
||||
className="cursor-pointer text-xs"
|
||||
>
|
||||
{t("charts.trend")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
id={bandsToggleId}
|
||||
checked={showBands}
|
||||
onCheckedChange={setShowBands}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={bandsToggleId}
|
||||
className="cursor-pointer text-xs"
|
||||
>
|
||||
{t("charts.targetRanges")}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* v1.4.18 — gradient defs removed; clean line only. */}
|
||||
|
||||
Reference in new issue
Block a user