) : onAddFirst ? (
) : undefined
@@ -598,77 +595,79 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
{data.entries.map((entry) => {
const isSelected = selectedIds.has(entry.id);
return (
-
-
- onToggleRow(entry.id)}
- aria-label={t("dataList.selectRow")}
- />
-
-
- {entry.score}{" "}
-
- (
- {MOOD_LABEL_KEYS[entry.mood]
- ? t(MOOD_LABEL_KEYS[entry.mood])
- : entry.mood}
- )
-
-
-
-
- {entry.tags.length > 0 ? entry.tags.join(", ") : "-"}
-
- {entry.note && (
-
-
-
-
- {entry.note}
-
-
-
-
- {entry.note}
-
-
-
-
- )}
-
-
- {formatDateTime(entry.moodLoggedAt)}
-
-
- {entry.source !== "MANUAL" && (
-
- {formatMoodSource(entry.source, t)}
-
- )}
-
-
-
-
-
deleteMutation.mutate(entry.id)}
- title={t("mood.deleteConfirmTitle")}
- description={t("mood.deleteConfirmDescription")}
+
+
+ onToggleRow(entry.id)}
+ aria-label={t("dataList.selectRow")}
/>
-
-
-
+
+
+ {entry.score}{" "}
+
+ (
+ {MOOD_LABEL_KEYS[entry.mood]
+ ? t(MOOD_LABEL_KEYS[entry.mood])
+ : entry.mood}
+ )
+
+
+
+
+ {entry.tags.length > 0
+ ? entry.tags.join(", ")
+ : "-"}
+
+ {entry.note && (
+
+
+
+
+ {entry.note}
+
+
+
+
+ {entry.note}
+
+
+
+
+ )}
+
+
+ {formatDateTime(entry.moodLoggedAt)}
+
+
+ {entry.source !== "MANUAL" && (
+
+ {formatMoodSource(entry.source, t)}
+
+ )}
+
+
+
+
+
deleteMutation.mutate(entry.id)}
+ title={t("mood.deleteConfirmTitle")}
+ description={t("mood.deleteConfirmDescription")}
+ />
+
+
+
);
})}
@@ -688,87 +687,87 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
{data.entries.map((entry) => {
const isSelected = selectedIds.has(entry.id);
return (
-
-
- {/* v1.15.13 MEDIUM-1 — a Radix Checkbox renders a 16px
+
+
+ {/* v1.15.13 MEDIUM-1 — a Radix Checkbox renders a 16px
`
-
- {/* Phase A5 mobile audit: bumped from h-8 w-8 (32px)
+
+ {/* Phase A5 mobile audit: bumped from h-8 w-8 (32px)
to min-h-11 min-w-11 (44px) so the per-row edit
and delete actions meet WCAG 2.5.5 on touch
devices. The desktop table keeps its denser
h-8 w-8 since pointer targets allow it. */}
-
startEdit(entry)}
- aria-label={t("common.edit")}
- >
-
-
-
deleteMutation.mutate(entry.id)}
- iconClassName="h-4 w-4"
- title={t("mood.deleteConfirmTitle")}
- description={t("mood.deleteConfirmDescription")}
- />
+ startEdit(entry)}
+ aria-label={t("common.edit")}
+ >
+
+
+ deleteMutation.mutate(entry.id)}
+ iconClassName="h-4 w-4"
+ title={t("mood.deleteConfirmTitle")}
+ description={t("mood.deleteConfirmDescription")}
+ />
+
-
);
})}
@@ -833,205 +832,196 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
title={t("mood.editEntry")}
footer={
}
>
- {editing && (
-
)}
@@ -449,9 +448,9 @@ export function HealthRecordExportPanel() {
className="ml-auto min-h-11 sm:min-h-9"
>
{busy ? (
-
+
) : (
-
+
)}
{t("settings.healthRecord.generate")}
diff --git a/src/components/settings/import-panel.tsx b/src/components/settings/import-panel.tsx
index 5ee3dfa88..f132faf5e 100644
--- a/src/components/settings/import-panel.tsx
+++ b/src/components/settings/import-panel.tsx
@@ -168,15 +168,21 @@ function AppleHealthImportCard() {
body: form,
});
if (res.status === 429) {
- setUploadError(t("settings.sections.export.import.appleHealth.rateLimited"));
+ setUploadError(
+ t("settings.sections.export.import.appleHealth.rateLimited"),
+ );
return;
}
if (res.status === 413) {
- setUploadError(t("settings.sections.export.import.appleHealth.tooLarge"));
+ setUploadError(
+ t("settings.sections.export.import.appleHealth.tooLarge"),
+ );
return;
}
if (!res.ok) {
- setUploadError(t("settings.sections.export.import.appleHealth.uploadFailed"));
+ setUploadError(
+ t("settings.sections.export.import.appleHealth.uploadFailed"),
+ );
return;
}
const body = (await res.json()).data as {
@@ -184,12 +190,16 @@ function AppleHealthImportCard() {
status: string;
};
if (!body?.jobId) {
- setUploadError(t("settings.sections.export.import.appleHealth.uploadFailed"));
+ setUploadError(
+ t("settings.sections.export.import.appleHealth.uploadFailed"),
+ );
return;
}
setJobId(body.jobId);
} catch {
- setUploadError(t("settings.sections.export.import.appleHealth.uploadFailed"));
+ setUploadError(
+ t("settings.sections.export.import.appleHealth.uploadFailed"),
+ );
} finally {
setUploading(false);
}
@@ -212,8 +222,7 @@ function AppleHealthImportCard() {
}
const status = statusQuery.data ?? null;
- const isRunning =
- status !== null && !TERMINAL_STATES.includes(status.status);
+ const isRunning = status !== null && !TERMINAL_STATES.includes(status.status);
const isDone = status?.status === "done";
const isFailed = status?.status === "failed";
const busy = uploading || isRunning;
@@ -265,7 +274,9 @@ function AppleHealthImportCard() {
type="file"
accept=".zip,application/zip"
className="sr-only"
- aria-label={t("settings.sections.export.import.appleHealth.fileInputLabel")}
+ aria-label={t(
+ "settings.sections.export.import.appleHealth.fileInputLabel",
+ )}
onChange={onFileChange}
/>
@@ -292,9 +303,7 @@ function AppleHealthImportCard() {
`settings.sections.export.import.appleHealth.phase.${status?.status ?? "queued"}`,
)}
- {typeof percent === "number" && (
-
- )}
+ {typeof percent === "number" && }
{typeof status?.progress?.rowsUpserted === "number" && (
{t("settings.sections.export.import.appleHealth.rowsImported", {
@@ -325,7 +334,7 @@ function AppleHealthImportCard() {
{(isFailed || uploadError) && (
{busy ? (
-
+
) : (
-
+
)}
{t("settings.sections.export.import.appleHealth.choose")}
@@ -485,7 +494,8 @@ function JsonImportCard() {
if (res.status === 422) {
const body = await res.json().catch(() => null);
setError(
- body?.error ?? t("settings.sections.export.import.json.invalidPayload"),
+ body?.error ??
+ t("settings.sections.export.import.json.invalidPayload"),
);
return;
}
@@ -570,7 +580,7 @@ function JsonImportCard() {
{error && (
fileInputRef.current?.click()}
data-testid="import-json-choose-file"
>
-
+
{t("settings.sections.export.import.json.uploadFile")}
-
+
{t("settings.sections.export.import.json.downloadExample")}
{busy ? (
-
+
) : (
-
+
)}
{t("settings.sections.export.import.json.import")}
diff --git a/src/components/settings/insights-pill-order-section.tsx b/src/components/settings/insights-pill-order-section.tsx
index f6add9fad..14004bf24 100644
--- a/src/components/settings/insights-pill-order-section.tsx
+++ b/src/components/settings/insights-pill-order-section.tsx
@@ -106,8 +106,8 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
// differs from what we last seeded from.
const serverSignature = layoutSignature(layout.tiles);
const [seededSignature, setSeededSignature] = useState(serverSignature);
- const [draftTiles, setDraftTiles] = useState(
- () => [...layout.tiles].sort((a, b) => a.order - b.order),
+ const [draftTiles, setDraftTiles] = useState(() =>
+ [...layout.tiles].sort((a, b) => a.order - b.order),
);
if (isSuccess && serverSignature !== seededSignature) {
setSeededSignature(serverSignature);
@@ -215,10 +215,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
>
-
+
{t("insights.pillOrder.title")}
@@ -233,7 +230,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
className="self-end sm:self-auto"
>
{saveMutation.isPending && (
-
+
)}
{t("insights.pillOrder.save")}
@@ -320,8 +317,14 @@ function SortablePillRow({
hideLabel: string;
onToggle: (id: string, visible: boolean) => void;
}) {
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
- useSortable({ id });
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
diff --git a/src/components/settings/integrations-section.tsx b/src/components/settings/integrations-section.tsx
index 4d3999ee6..5aa4c9cfc 100644
--- a/src/components/settings/integrations-section.tsx
+++ b/src/components/settings/integrations-section.tsx
@@ -165,7 +165,7 @@ function IntegrationErrorMessage({ message }: { message: string }) {
return (
{message}
@@ -321,7 +321,9 @@ function WithingsCard({
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.withings() });
- queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.integrationsStatus(),
+ });
},
});
@@ -343,7 +345,9 @@ function WithingsCard({
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.withings() });
- queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.integrationsStatus(),
+ });
},
});
@@ -368,7 +372,9 @@ function WithingsCard({
);
setSyncMsgType("success");
void invalidateKeys(queryClient, measurementDependentKeys);
- queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.integrationsStatus(),
+ });
} else {
setSyncMsg(json.error || t("settings.withingsSyncFailed"));
setSyncMsgType("error");
@@ -445,7 +451,10 @@ function WithingsCard({
title={t("settings.withings")}
description={t("settings.withingsDescription")}
status={
-
+
}
/>
@@ -496,7 +505,7 @@ function WithingsCard({
data-testid="withings-parked-banner"
className="border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
>
-
+
{t("settings.integrationPill.parkedReconnect")}
{resume.isPending ? (
-
+
) : (
-
+
)}
{t("settings.integrationPill.resumeCta")}
@@ -520,7 +529,7 @@ function WithingsCard({
{resume.isError && (
{t("settings.integrationPill.resumeError")}
@@ -604,9 +613,9 @@ function WithingsCard({
}
>
{credsSaving ? (
-
+
) : (
-
+
)}
{t("settings.withingsSaveCredentials")}
@@ -639,9 +648,9 @@ function WithingsCard({
disabled={syncing}
>
{syncing ? (
-
+
) : (
-
+
)}
{t("settings.withingsSync")}
@@ -654,9 +663,9 @@ function WithingsCard({
disabled={syncing}
>
{syncing ? (
-
+
) : (
-
+
)}
{t("settings.withingsFullSync")}
@@ -689,7 +698,7 @@ function WithingsCard({
size="sm"
className="text-destructive min-h-11"
>
-
+
{t("settings.withingsDisconnect")}
@@ -730,7 +739,7 @@ function WithingsCard({
window.location.href = "/api/withings/connect";
}}
>
-
+
{t("settings.withingsConnect")}
) : (
@@ -816,7 +825,9 @@ function WhoopCard({
});
const json = await res.json();
if (res.ok) {
- setSyncMsg(t("settings.whoopSyncResult", { count: json.data.imported }));
+ setSyncMsg(
+ t("settings.whoopSyncResult", { count: json.data.imported }),
+ );
setSyncMsgType("success");
void invalidateKeys(queryClient, measurementDependentKeys);
queryClient.invalidateQueries({ queryKey: queryKeys.whoop() });
@@ -897,7 +908,10 @@ function WhoopCard({
>
}
status={
-
+
}
/>
@@ -920,7 +934,7 @@ function WhoopCard({
data-testid="whoop-parked-banner"
className="border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
>
-
+
{t("settings.integrationPill.parkedReconnect")}
{resume.isPending ? (
-
+
) : (
-
+
)}
{t("settings.integrationPill.resumeCta")}
@@ -944,7 +958,7 @@ function WhoopCard({
{resume.isError && (
{t("settings.integrationPill.resumeError")}
@@ -1023,9 +1037,9 @@ function WhoopCard({
}
>
{credsSaving ? (
-
+
) : (
-
+
)}
{t("settings.whoopSaveCredentials")}
@@ -1052,9 +1066,9 @@ function WhoopCard({
disabled={syncing}
>
{syncing ? (
-
+
) : (
-
+
)}
{t("settings.whoopSync")}
@@ -1067,9 +1081,9 @@ function WhoopCard({
disabled={syncing}
>
{syncing ? (
-
+
) : (
-
+
)}
{t("settings.whoopFullSync")}
@@ -1102,7 +1116,7 @@ function WhoopCard({
size="sm"
className="text-destructive min-h-11"
>
-
+
{t("settings.whoopDisconnect")}
@@ -1148,7 +1162,7 @@ function WhoopCard({
window.location.href = "/api/whoop/connect";
}}
>
-
+
{t("settings.whoopConnect")}
) : (
@@ -1344,7 +1358,10 @@ function FitbitCard({
>
}
status={
-
+
}
/>
@@ -1366,7 +1383,7 @@ function FitbitCard({
data-testid="fitbit-parked-banner"
className="border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
>
-
+
{t("settings.integrationPill.parkedReconnect")}
{resume.isPending ? (
-
+
) : (
-
+
)}
{t("settings.integrationPill.resumeCta")}
@@ -1390,7 +1407,7 @@ function FitbitCard({
{resume.isError && (
{t("settings.integrationPill.resumeError")}
@@ -1469,9 +1486,9 @@ function FitbitCard({
}
>
{credsSaving ? (
-
+
) : (
-
+
)}
{t("settings.fitbitSaveCredentials")}
@@ -1498,9 +1515,9 @@ function FitbitCard({
disabled={syncing}
>
{syncing ? (
-
+
) : (
-
+
)}
{t("settings.fitbitSync")}
@@ -1513,9 +1530,9 @@ function FitbitCard({
disabled={syncing}
>
{syncing ? (
-
+
) : (
-
+
)}
{t("settings.fitbitFullSync")}
@@ -1548,7 +1565,7 @@ function FitbitCard({
size="sm"
className="text-destructive min-h-11"
>
-
+
{t("settings.fitbitDisconnect")}
@@ -1594,7 +1611,7 @@ function FitbitCard({
window.location.href = "/api/fitbit/connect";
}}
>
-
+
{t("settings.fitbitConnect")}
) : (
diff --git a/src/components/settings/notification-status-card.tsx b/src/components/settings/notification-status-card.tsx
index b0609ac8e..fe9b5c835 100644
--- a/src/components/settings/notification-status-card.tsx
+++ b/src/components/settings/notification-status-card.tsx
@@ -92,7 +92,9 @@ export function NotificationStatusCard() {
if (!res.ok) throw new Error("Failed");
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: queryKeys.notificationsStatus() });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.notificationsStatus(),
+ });
},
});
@@ -105,7 +107,9 @@ export function NotificationStatusCard() {
}
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: queryKeys.notificationsStatus() });
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.notificationsStatus(),
+ });
},
});
@@ -286,9 +290,9 @@ function ChannelRow({
className="min-h-11"
>
{reEnablePending ? (
-
+
) : (
-
+
)}
{t("settings.notificationStatus.reEnable")}
@@ -303,9 +307,9 @@ function ChannelRow({
className="min-h-11"
>
{testPending ? (
-
+
) : (
-
+
)}
{t("settings.notificationStatus.sendTest")}
@@ -328,8 +332,7 @@ function stateBadgeFor(
return {
label: t("settings.notificationStatus.stateActive"),
variant: "default",
- className:
- "border-success/30 bg-success/15 text-success",
+ className: "border-success/30 bg-success/15 text-success",
icon: ,
};
case "auto_disabled":
diff --git a/src/components/settings/ntfy-card.tsx b/src/components/settings/ntfy-card.tsx
index af1c34142..dc0007425 100644
--- a/src/components/settings/ntfy-card.tsx
+++ b/src/components/settings/ntfy-card.tsx
@@ -166,7 +166,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
className="min-h-11"
>
{save.isPending && (
-
+
)}
{t("common.save")}
diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx
index e3ba990ef..0dc3585d2 100644
--- a/src/components/settings/sources-section.tsx
+++ b/src/components/settings/sources-section.tsx
@@ -337,7 +337,7 @@ export function SourcesSection() {
disabled={resetMutation.isPending || saveMutation.isPending}
className="self-end sm:self-auto"
>
-
+
{t("settings.sections.sources.resetDefaults")}
@@ -523,7 +523,7 @@ export function SourcesSection() {
onClick={resetDeviceTypeAxis}
disabled={saveMutation.isPending}
>
-
+
{t("settings.sections.sources.resetDeviceTypes")}
@@ -597,7 +597,7 @@ export function SourcesSection() {
disabled={saveMutation.isPending}
>
{saveMutation.isPending && (
-
+
)}
{t("common.save")}
diff --git a/src/components/settings/telegram-card.tsx b/src/components/settings/telegram-card.tsx
index ec66489d0..e886663ae 100644
--- a/src/components/settings/telegram-card.tsx
+++ b/src/components/settings/telegram-card.tsx
@@ -156,9 +156,7 @@ export function TelegramCard({
{msg}
@@ -172,9 +170,9 @@ export function TelegramCard({
/>
{saving ? (
-
+
) : (
-
+
)}
{t("common.save")}
diff --git a/src/components/settings/test-connection-button.tsx b/src/components/settings/test-connection-button.tsx
index e6e5ea84f..7a66580c0 100644
--- a/src/components/settings/test-connection-button.tsx
+++ b/src/components/settings/test-connection-button.tsx
@@ -88,9 +88,9 @@ export function TestConnectionButton({
disabled={disabled || testing}
>
{testing ? (
-
+
) : (
-
+
)}
{testing
? t("settings.testConnection.testing")
@@ -110,7 +110,7 @@ export function TestConnectionButton({
{result?.kind === "error" && (
{describeError(result.errorCode)}
diff --git a/src/components/settings/thresholds-editor-section.tsx b/src/components/settings/thresholds-editor-section.tsx
index 4b33495a3..4ac8bf765 100644
--- a/src/components/settings/thresholds-editor-section.tsx
+++ b/src/components/settings/thresholds-editor-section.tsx
@@ -138,7 +138,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
onClick={() => resetMutation.mutate(null)}
disabled={resetMutation.isPending}
>
-
+
{t("thresholds.resetAllAction")}
)}
diff --git a/src/components/settings/web-push-card.tsx b/src/components/settings/web-push-card.tsx
index 9e4491cfe..99928905f 100644
--- a/src/components/settings/web-push-card.tsx
+++ b/src/components/settings/web-push-card.tsx
@@ -172,7 +172,7 @@ export function WebPushCard() {
disabled={actionLoading}
>
{actionLoading && (
-
+
)}
{t("settings.webPushUnsubscribe")}
@@ -185,9 +185,9 @@ export function WebPushCard() {
disabled={actionLoading}
>
{actionLoading ? (
-
+
) : (
-
+
)}
{t("settings.webPushSubscribe")}
diff --git a/src/components/targets/target-edit-sheet.tsx b/src/components/targets/target-edit-sheet.tsx
index baa25561f..6174acf4d 100644
--- a/src/components/targets/target-edit-sheet.tsx
+++ b/src/components/targets/target-edit-sheet.tsx
@@ -131,9 +131,7 @@ function TargetEditSheetBody({
// to canonical mg/dL before the PUT — otherwise a `5.5 mmol/L` target
// is rejected by the 40–400 mg/dL bounds or stored verbatim as 5.5.
const isGlucoseMmol =
- metric != null &&
- metric.startsWith("BLOOD_GLUCOSE") &&
- unit === "mmol/L";
+ metric != null && metric.startsWith("BLOOD_GLUCOSE") && unit === "mmol/L";
const toDisplay = (mgdl: number) =>
isGlucoseMmol ? convertGlucose(mgdl, "mmol/L") : mgdl;
const toCanonical = (displayValue: number) =>
@@ -374,7 +372,7 @@ function TargetEditSheetBody({
disabled={busy}
data-slot="target-edit-reset"
>
-
+
{t("targets.edit.resetToDefault")}
) : (
@@ -414,88 +412,88 @@ function TargetEditSheetBody({
{!isDerivedMetric && primaryBounds && (
-
-
- {isBp && secondaryBounds && (
- )}
+
+ {isBp && secondaryBounds && (
+
+ )}
{t("targets.edit.boundsHint", {
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 0e3df88f6..bcf43539b 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -18,9 +18,9 @@ import { cn } from "@/lib/utils";
* ```tsx
*
* {loading ? (
- *
+ *
* ) : (
- *
+ *
* )}
* {label}
*
diff --git a/src/components/ui/responsive-sheet.tsx b/src/components/ui/responsive-sheet.tsx
index 77c4683b2..0099f2a85 100644
--- a/src/components/ui/responsive-sheet.tsx
+++ b/src/components/ui/responsive-sheet.tsx
@@ -42,7 +42,18 @@ export interface ResponsiveSheetProps {
*/
hideHeader?: boolean;
/**
- * Footer slot. The Sheet branch sticky-pins the footer to the
+ * Footer slot. Pass the action ``s directly (a fragment is
+ * fine) — never a wrapper ``. The two
+ * branches deliberately stack differently and both rely on owning
+ * the flex context: the Sheet branch pins a `flex-row justify-end`
+ * bar so Cancel/Save sit side by side above the keyboard, while the
+ * Dialog branch defers to `DialogFooter`'s `flex-col-reverse
+ * sm:flex-row` so narrow dialogs stack the primary action on top.
+ * A consumer-supplied wrapper div collapses both into a single flex
+ * child and breaks that responsive stacking. This split is a
+ * decision, not drift — don't unify the two branches.
+ *
+ * The Sheet branch sticky-pins the footer to the
* bottom of the bottom-sheet so the primary CTA stays reachable
* once the body scrolls; the Dialog branch lets the footer flow
* normally beneath the body.
@@ -167,7 +178,7 @@ export function ResponsiveSheet({
{footer ? (
{footer}