mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
chore(ui): apply M7 + L1 + L3 + L4 polish across medication form, motion-reduce, details aria, therapy timeline
W10 design-review carry-over items. M7 lifts the medication-form day-of-week buttons, the more-horizontal trigger, and the schedule label-input gaps to the 44 px / space-y-2 standard used by every other settings surface. L1 pairs every animate-spin/pulse/bounce with motion-reduce:animate-none across the component tree so users with prefers-reduced-motion get a still surface. L3 adds the aria-controls + panel-id pairing to the inventory + coach-evidence <details> blocks for parity with the Health Score provenance accordion. L4 inserts a sr-only h4 per therapy-timeline entry so screen-reader users can jump between medication blocks.
This commit is contained in:
1 parent
19b5129306
commit
32a023dfff
71 files changed
+165
-136
No files matched your search
@@ -273,7 +273,7 @@ export default function AchievementsPage() {
|
||||
if (authLoading || isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function LoginPage() {
|
||||
disabled={loading}
|
||||
>
|
||||
{loading && mode === "passkey" ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<KeyRound className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -198,7 +198,7 @@ export default function LoginPage() {
|
||||
disabled={loading}
|
||||
>
|
||||
{loading && mode === "password" ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function RegisterPage() {
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function BugReportPage() {
|
||||
|
||||
<div className="flex items-center justify-end pt-2">
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
<Bug className="mr-2 h-4 w-4" />
|
||||
{t("bugreport.submit")}
|
||||
</Button>
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function InsightsMedikamentePage() {
|
||||
return (
|
||||
<SubPageShell title={t("insights.medicationCompliance")}>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
</SubPageShell>
|
||||
);
|
||||
@@ -250,7 +250,7 @@ function MedicationComplianceCalendar({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<Loader2 className="text-primary h-4 w-4 animate-spin" />
|
||||
<Loader2 className="text-primary h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function InsightsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function MeasurementsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function IntakeHistoryPage({
|
||||
if (authLoading || medLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function MedicationsPage() {
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ export default function MedicationsPage() {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="bg-card border-border flex h-64 items-center justify-center rounded-xl border">
|
||||
@@ -499,7 +499,7 @@ function IntakeImportDialog({
|
||||
onClick={handleImport}
|
||||
disabled={importing || !jsonText.trim()}
|
||||
>
|
||||
{importing && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{importing && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t("common.import")}
|
||||
</Button>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function MoodPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function NotificationsPage() {
|
||||
if (authLoading || isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export default function TargetsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function AiQualitySection() {
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<div className="bg-card border-border flex items-center gap-2 rounded-xl border p-6">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t("admin.aiQuality.loading")}
|
||||
</span>
|
||||
|
||||
@@ -170,7 +170,7 @@ export function ApiTokenOverviewSection() {
|
||||
) : !tokens?.length ? (
|
||||
// v1.4.15 phase-C5: empty state explains where tokens come
|
||||
// from (native client sign-in / Settings → Account) so a
|
||||
// fresh admin doesn't think they need to create them here.
|
||||
// fresh admin doesn motion-reduce:animate-none't think they need to create them here.
|
||||
<EmptyState
|
||||
icon={<Key className="size-6" />}
|
||||
title={t("admin.tokensEmptyTitle")}
|
||||
|
||||
@@ -133,7 +133,7 @@ export function AppLogPreviewSection() {
|
||||
aria-label={t("admin.section.app-logs.refresh")}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`}
|
||||
className={`h-4 w-4 ${isFetching ? "animate-spin" : ""} motion-reduce:animate-none`}
|
||||
/>
|
||||
</Button>
|
||||
</header>
|
||||
@@ -195,7 +195,7 @@ export function AppLogPreviewSection() {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin" />
|
||||
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : events.length === 0 ? (
|
||||
<EmptyState
|
||||
|
||||
@@ -90,7 +90,7 @@ function RestoreRowDialog({
|
||||
className="min-h-11"
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<History className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -119,7 +119,7 @@ export function BugReportSection() {
|
||||
disabled={updateSettings.isPending}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -99,7 +99,7 @@ export function CoachFeedbackSection() {
|
||||
|
||||
{query.isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t("admin.coachFeedback.loading")}
|
||||
</span>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function DangerZoneSection() {
|
||||
disabled={wipeAllData.isPending}
|
||||
>
|
||||
{wipeAllData.isPending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -104,7 +104,7 @@ export function FeedbackInboxSection() {
|
||||
<TabsContent key={s} value={s} className="mt-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t("admin.feedback.loading")}
|
||||
</span>
|
||||
@@ -435,7 +435,7 @@ function FeedbackDetailDialog({
|
||||
onClick={() => update.mutate({ adminNote: note || null })}
|
||||
>
|
||||
{update.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("admin.feedback.saveNote")}
|
||||
</Button>
|
||||
@@ -497,7 +497,7 @@ function FeedbackDetailDialog({
|
||||
className="ml-auto"
|
||||
>
|
||||
{publish.isPending ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<GitPullRequest className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -148,7 +148,7 @@ export function GlitchtipSection() {
|
||||
disabled={testGlitchtip.isPending || updateSettings.isPending}
|
||||
>
|
||||
{testGlitchtip.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.test")}
|
||||
</Button>
|
||||
@@ -158,7 +158,7 @@ export function GlitchtipSection() {
|
||||
disabled={updateSettings.isPending}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -340,7 +340,7 @@ export function LoginOverviewSection() {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin" />
|
||||
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : !entries.length ? (
|
||||
<EmptyState
|
||||
|
||||
@@ -79,7 +79,7 @@ export function RecentAuditPreview() {
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<Loader2
|
||||
className="text-muted-foreground h-4 w-4 animate-spin"
|
||||
className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -177,7 +177,7 @@ export function RemindersSection() {
|
||||
disabled={testNotification.isPending}
|
||||
>
|
||||
{testNotification.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
<Bell className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t("admin.notificationTestSend")}
|
||||
@@ -189,7 +189,7 @@ export function RemindersSection() {
|
||||
disabled={reminderCheck.isPending}
|
||||
>
|
||||
{reminderCheck.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
<Activity className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t("admin.reminderCheckRun")}
|
||||
@@ -220,7 +220,7 @@ export function RemindersSection() {
|
||||
}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -127,7 +127,7 @@ export function SystemStatusSummary() {
|
||||
) : (
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Loader2
|
||||
className="text-muted-foreground h-4 w-4 animate-spin"
|
||||
className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -149,7 +149,7 @@ export function UmamiSection() {
|
||||
disabled={testUmami.isPending || updateSettings.isPending}
|
||||
>
|
||||
{testUmami.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.test")}
|
||||
</Button>
|
||||
@@ -159,7 +159,7 @@ export function UmamiSection() {
|
||||
disabled={updateSettings.isPending}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -407,7 +407,7 @@ export function UserManagementSection() {
|
||||
)
|
||||
) : (
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t("admin.loadingUsers")}
|
||||
</span>
|
||||
@@ -460,7 +460,7 @@ export function UserManagementSection() {
|
||||
}
|
||||
>
|
||||
{updateUser.isPending && (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
@@ -510,7 +510,7 @@ export function UserManagementSection() {
|
||||
}
|
||||
>
|
||||
{resetPw.isPending && (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("admin.reset")}
|
||||
</Button>
|
||||
@@ -562,7 +562,7 @@ export function UserManagementSection() {
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{forceLogout.isPending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("admin.section.users.forceLogout")}
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -135,7 +135,7 @@ export function WebPushVapidSection() {
|
||||
disabled={updateSettings.isPending}
|
||||
>
|
||||
{updateSettings.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -1164,7 +1164,7 @@ export function HealthChart({
|
||||
) : !chartData?.length ? null : (chartData?.length ?? 0) < 3 ? (
|
||||
// v1.4.16 B1a — sparse-data placeholder. <3 daily points is too
|
||||
// few to render a meaningful trend; paint a friendly hint
|
||||
// instead so the dashboard doesn't look broken.
|
||||
// instead so the dashboard doesn motion-reduce:animate-none't look broken.
|
||||
<ChartEmptyState
|
||||
title={t("charts.emptyStateTitle")}
|
||||
description={t("charts.emptyStateDescription")}
|
||||
|
||||
@@ -369,7 +369,7 @@ export function MedicationComplianceChart({
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex h-48 items-center justify-center">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : !hasData ? (
|
||||
<div className="text-muted-foreground flex h-48 items-center justify-center text-sm">
|
||||
|
||||
@@ -623,7 +623,7 @@ export function MoodChart({
|
||||
</div>
|
||||
) : chartData.length < 3 ? (
|
||||
// v1.4.16 B1a — sparse-data placeholder consistent with the
|
||||
// BP/weight/pulse charts. Height tracks the chart strip's
|
||||
// BP/weight/pulse charts. Height tracks the chart strip motion-reduce:animate-none's
|
||||
// shared CHART_HEIGHT_PX so the empty state preserves the
|
||||
// trend-row rhythm (v1.4.27 — was 280, now 240 to match every
|
||||
// other dashboard chart card).
|
||||
|
||||
@@ -430,7 +430,7 @@ export function Glp1Tile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.27 B1 — small segmented-control button used by the tile's
|
||||
* v1.4.27 B1 — small segmented-control button used by the tile motion-reduce:animate-none's
|
||||
* tab strip. Carved into its own component so the active/inactive
|
||||
* styling lives in one place and the test can pin the data-slot
|
||||
* markers per tab.
|
||||
|
||||
@@ -218,7 +218,7 @@ export function CoachInput({
|
||||
className="gap-1.5"
|
||||
>
|
||||
{isStreaming ? (
|
||||
<Loader2 className="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<Loader2 className="size-3.5 animate-spin motion-reduce:animate-none" aria-hidden="true" />
|
||||
) : (
|
||||
<Send className="size-3.5" aria-hidden="true" />
|
||||
)}
|
||||
|
||||
@@ -351,7 +351,7 @@ export function CoachSettingsSheet({
|
||||
data-slot="coach-prefs-save"
|
||||
>
|
||||
{save.isPending ? (
|
||||
<Loader2 className="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<Loader2 className="size-3.5 animate-spin motion-reduce:animate-none" aria-hidden="true" />
|
||||
) : null}
|
||||
{t("insights.coach.settingsSave")}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
Bot,
|
||||
@@ -315,6 +315,9 @@ function ChatBubble({
|
||||
}: ChatBubbleProps) {
|
||||
const { t } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
// v1.4.27 B7 / L3 — pair the evidence `<details>` and its disclosed
|
||||
// list explicitly so screen-readers announce the panel relationship.
|
||||
const evidencePanelId = useId();
|
||||
if (role === "user") {
|
||||
// v1.4.22 B3 — pull the user's Gravatar so the user bubble's
|
||||
// avatar matches the Coach avatar in size and visual weight.
|
||||
@@ -434,6 +437,7 @@ function ChatBubble({
|
||||
>
|
||||
<summary
|
||||
data-slot="coach-evidence-summary"
|
||||
aria-controls={evidencePanelId}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground flex cursor-pointer",
|
||||
"items-center gap-1.5 leading-relaxed",
|
||||
@@ -448,6 +452,7 @@ function ChatBubble({
|
||||
<span>{t("insights.coach.evidenceLabel")}</span>
|
||||
</summary>
|
||||
<ul
|
||||
id={evidencePanelId}
|
||||
data-slot="coach-evidence-list"
|
||||
className="text-foreground mt-2 flex flex-col gap-1"
|
||||
>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function InsightStatusCard({
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-dracula-purple h-5 w-5 animate-spin" />
|
||||
<Loader2 className="text-dracula-purple h-5 w-5 animate-spin motion-reduce:animate-none" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{t("common.loading")}
|
||||
</span>
|
||||
|
||||
@@ -267,7 +267,7 @@ export function RecommendationFeedback({
|
||||
className="text-muted-foreground hover:text-dracula-green disabled:hover:text-muted-foreground focus-visible:ring-ring/50 inline-flex min-h-11 min-w-11 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:opacity-60"
|
||||
>
|
||||
{submittingUp ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" aria-hidden="true" />
|
||||
) : (
|
||||
<ThumbsUp className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
)}
|
||||
@@ -282,7 +282,7 @@ export function RecommendationFeedback({
|
||||
className="text-muted-foreground hover:text-dracula-orange disabled:hover:text-muted-foreground focus-visible:ring-ring/50 inline-flex min-h-11 min-w-11 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:opacity-60"
|
||||
>
|
||||
{submittingDown ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" aria-hidden="true" />
|
||||
) : (
|
||||
<ThumbsDown className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
)}
|
||||
|
||||
@@ -108,6 +108,13 @@ export function TherapyTimeline({ limit }: TherapyTimelineProps) {
|
||||
style={{ backgroundColor: colourForKind(entry.kind) }}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
{/* v1.4.27 B7 / L4 — sr-only drug-name heading per
|
||||
entry so screen-reader users can jump between
|
||||
medication blocks; the visible row keeps the
|
||||
inline <strong> rendering. */}
|
||||
{entry.medicationName && (
|
||||
<h4 className="sr-only">{entry.medicationName}</h4>
|
||||
)}
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{fmt.dateWithWeekday(new Date(entry.date))}
|
||||
</p>
|
||||
|
||||
@@ -89,7 +89,7 @@ export function WeeklyReportView({
|
||||
data-slot="weekly-report-loading"
|
||||
className="flex h-64 items-center justify-center"
|
||||
>
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-dvh items-center justify-center" role="status">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
<span className="sr-only">{t("nav.loadingScreen")}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -88,7 +88,7 @@ export function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="flex h-dvh items-center justify-center" role="status">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
<span className="sr-only">{t("nav.loadingScreen")}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ export function TopBar() {
|
||||
{/* Mobile-only auth section (desktop uses sidebar user section) */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
{isLoading ? (
|
||||
<div className="bg-muted h-4 w-20 animate-pulse rounded" />
|
||||
<div className="bg-muted h-4 w-20 animate-pulse rounded motion-reduce:animate-none" />
|
||||
) : user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
|
||||
@@ -433,7 +433,7 @@ export function MeasurementForm({
|
||||
)}
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -338,7 +338,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : !data?.measurements?.length ? (
|
||||
// v1.4.15 phase-C5: replaces the bare-text empty rectangle.
|
||||
@@ -692,7 +692,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
}
|
||||
>
|
||||
{updateMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
@@ -722,7 +722,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -365,7 +365,7 @@ function GatedPlaceholder({
|
||||
// Three distinguishable states:
|
||||
// - Research Mode is off entirely.
|
||||
// - Research Mode is on but the acknowledged version is stale.
|
||||
// - We don't know yet (server returned null) — still render the
|
||||
// - We don motion-reduce:animate-none't know yet (server returned null) — still render the
|
||||
// opt-in CTA so the user has a path forward.
|
||||
const stale = !!knownState?.enabled && !versionsAligned;
|
||||
return (
|
||||
|
||||
@@ -269,7 +269,7 @@ export function ResearchModeAcknowledgmentDialog({
|
||||
data-slot="research-mode-acknowledge"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("medications.researchMode.dialog.acknowledgeCta")}
|
||||
</Button>
|
||||
|
||||
@@ -151,7 +151,7 @@ export function SchedulingSection({
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground flex items-center gap-2">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
<span>{t("medications.scheduling.loading")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -256,7 +256,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
<span>{t("medications.sideEffects.loading")}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -473,7 +473,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.sideEffects.submitCta")}
|
||||
</Button>
|
||||
|
||||
@@ -119,7 +119,7 @@ export function TitrationSection({ medicationId }: TitrationSectionProps) {
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground flex items-center gap-2">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
<span>{t("medications.titration.loading")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -511,5 +511,5 @@ function siteSuffix(site: InjectionSiteKey): string {
|
||||
.join("");
|
||||
}
|
||||
|
||||
/** Re-export so the parent doesn't need to import describeInjectionSite. */
|
||||
/** Re-export so the parent doesn motion-reduce:animate-none't need to import describeInjectionSite. */
|
||||
export { describeInjectionSite };
|
||||
@@ -370,7 +370,7 @@ export function IntakeHistoryList({
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : !data?.events?.length ? (
|
||||
<div className="text-muted-foreground flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||
@@ -631,7 +631,7 @@ export function IntakeHistoryList({
|
||||
}
|
||||
>
|
||||
{updateMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
@@ -661,7 +661,7 @@ export function IntakeHistoryList({
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
@@ -739,7 +739,7 @@ export function IntakeHistoryList({
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useId, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDown, Loader2, Plus, Trash2 } from "lucide-react";
|
||||
|
||||
@@ -75,6 +75,10 @@ export function InventorySection({
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
// v1.4.27 B7 / L3 — explicit aria-controls between summary and disclosed
|
||||
// panel for parity with the W8e Provenance accordion.
|
||||
const panelId = useId();
|
||||
const pastPanelId = useId();
|
||||
const [dosesTotal, setDosesTotal] = useState<string>(
|
||||
defaultDosesPerUnit ? String(defaultDosesPerUnit) : "",
|
||||
);
|
||||
@@ -247,7 +251,11 @@ export function InventorySection({
|
||||
open={open}
|
||||
onToggle={(e) => setOpen((e.target as HTMLDetailsElement).open)}
|
||||
>
|
||||
<summary className="text-foreground/85 flex cursor-pointer list-none items-center justify-between px-3 py-2 font-medium">
|
||||
<summary
|
||||
className="text-foreground/85 flex cursor-pointer list-none items-center justify-between px-3 py-2 font-medium"
|
||||
aria-controls={panelId}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span>
|
||||
{t("medications.inventory.title")}
|
||||
{liveItems.length > 0 && (
|
||||
@@ -262,10 +270,13 @@ export function InventorySection({
|
||||
}`}
|
||||
/>
|
||||
</summary>
|
||||
<div className="border-border/60 space-y-3 border-t px-3 py-2.5">
|
||||
<div
|
||||
id={panelId}
|
||||
className="border-border/60 space-y-3 border-t px-3 py-2.5"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
<span>{t("medications.inventory.loading")}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -357,12 +368,18 @@ export function InventorySection({
|
||||
|
||||
{pastItems.length > 0 && (
|
||||
<details className="border-border/40 rounded-md border">
|
||||
<summary className="text-muted-foreground cursor-pointer list-none px-2.5 py-1.5 font-medium">
|
||||
<summary
|
||||
className="text-muted-foreground cursor-pointer list-none px-2.5 py-1.5 font-medium"
|
||||
aria-controls={pastPanelId}
|
||||
>
|
||||
{t("medications.inventory.pastTitle", {
|
||||
count: pastItems.length,
|
||||
})}
|
||||
</summary>
|
||||
<ul className="border-border/40 space-y-1 border-t px-2.5 py-2">
|
||||
<ul
|
||||
id={pastPanelId}
|
||||
className="border-border/40 space-y-1 border-t px-2.5 py-2"
|
||||
>
|
||||
{pastItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
@@ -496,7 +513,7 @@ export function InventorySection({
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending && (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.inventory.save")}
|
||||
</Button>
|
||||
|
||||
@@ -567,7 +567,7 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
|
||||
disabled={!!intakeLoading}
|
||||
>
|
||||
{intakeLoading === "take" ? (
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Check className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
@@ -580,7 +580,7 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
|
||||
disabled={!!intakeLoading}
|
||||
>
|
||||
{intakeLoading === "skip" ? (
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<SkipForward className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -768,7 +768,7 @@ export function MedicationForm({
|
||||
</div>
|
||||
|
||||
<div className="mt-1 grid items-end gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-normal">
|
||||
{t("medications.scheduleFrom")}
|
||||
</Label>
|
||||
@@ -786,7 +786,7 @@ export function MedicationForm({
|
||||
maxLength={5}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-normal">
|
||||
{t("medications.scheduleTo")}
|
||||
</Label>
|
||||
@@ -804,7 +804,7 @@ export function MedicationForm({
|
||||
maxLength={5}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-normal">
|
||||
{t("medications.scheduleLabel")}
|
||||
</Label>
|
||||
@@ -816,7 +816,7 @@ export function MedicationForm({
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-normal">
|
||||
{t("medications.dose")}
|
||||
</Label>
|
||||
@@ -838,7 +838,7 @@ export function MedicationForm({
|
||||
{/* Day-of-week selection */}
|
||||
{s.showAdvanced && (
|
||||
<div className="border-border/60 mt-2.5 space-y-2.5 border-t pt-2.5">
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">
|
||||
{t("medications.scheduleInterval")}
|
||||
</Label>
|
||||
@@ -862,7 +862,7 @@ export function MedicationForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">
|
||||
{t("medications.scheduleDays")}
|
||||
</Label>
|
||||
@@ -870,7 +870,7 @@ export function MedicationForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateSchedule(i, "daysOfWeek", [])}
|
||||
className={`h-8 min-w-24 rounded-md border px-3 text-xs font-medium transition-colors ${
|
||||
className={`min-h-11 min-w-24 rounded-md border px-3 text-xs font-medium transition-colors ${
|
||||
s.daysOfWeek.length === 0
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border/70 bg-muted text-foreground/70 hover:bg-accent hover:text-foreground"
|
||||
@@ -885,7 +885,7 @@ export function MedicationForm({
|
||||
key={dayIndex}
|
||||
type="button"
|
||||
onClick={() => toggleDay(i, dayIndex)}
|
||||
className={`h-8 flex-1 rounded-md border text-xs font-medium transition-colors ${
|
||||
className={`min-h-11 flex-1 rounded-md border text-xs font-medium transition-colors ${
|
||||
isSelected
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border/70 bg-muted text-foreground/75 hover:bg-accent hover:text-foreground"
|
||||
@@ -925,7 +925,7 @@ export function MedicationForm({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
className="h-11 w-11"
|
||||
disabled={loading || deleting}
|
||||
aria-label={t("common.moreOptions")}
|
||||
>
|
||||
@@ -1009,7 +1009,7 @@ export function MedicationForm({
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" disabled={loading || deleting}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
{isEdit ? t("common.save") : t("medications.createMedication")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1057,7 +1057,7 @@ export function MedicationForm({
|
||||
disabled={purging}
|
||||
>
|
||||
{purging ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("medications.purgeRecords")}
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -171,7 +171,7 @@ export function PhaseConfigDialog({
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<Loader2 className="h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -253,7 +253,7 @@ export function PhaseConfigDialog({
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -271,7 +271,7 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
|
||||
)}
|
||||
<Button type="submit" disabled={loading || !mood}>
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -429,7 +429,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
{/* Mobile list — v1.4.15 phase-A3 fix #2: previously the row
|
||||
rendered the score TWICE on mobile (the big number in the
|
||||
left badge AND a duplicate in the title line: "2 (schlecht)").
|
||||
Desktop's table version only ever showed one. The badge is
|
||||
Desktop motion-reduce:animate-none's table version only ever showed one. The badge is
|
||||
the visual anchor; next to it the user wants the textual
|
||||
label, not a second copy of the digit. The
|
||||
`data-testid="mood-row"` hook is what the Playwright Pixel-5
|
||||
@@ -650,7 +650,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
}
|
||||
>
|
||||
{updateMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
@@ -680,7 +680,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -240,7 +240,7 @@ export function BaselineForm() {
|
||||
disabled={saving}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{saving ? <Loader2 className="size-4 animate-spin" /> : null}
|
||||
{saving ? <Loader2 className="size-4 animate-spin motion-reduce:animate-none" /> : null}
|
||||
{t("onboarding.baseline.saveCta")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -265,7 +265,7 @@ export function AboutSection() {
|
||||
{/* Updates — proper heading + last-check timestamp + manual button
|
||||
that proxies through `/api/version/check-updates`. The v1.4.2
|
||||
version called `api.github.com` directly from the browser, which
|
||||
the production CSP blocked silently — that's why "nothing
|
||||
the production CSP blocked silently — that motion-reduce:animate-none's why "nothing
|
||||
happened" when the user clicked the button. */}
|
||||
{version && (
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
@@ -284,7 +284,7 @@ export function AboutSection() {
|
||||
disabled={checking}
|
||||
>
|
||||
{checking ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -407,7 +407,7 @@ export function AccountSection() {
|
||||
wide on sm+). Date of birth is the bottom of the
|
||||
biological-profile block; language is the only UI
|
||||
preference on this card. They sit together to close the
|
||||
"single-cell row" gap that broke the form's grid. */}
|
||||
"single-cell row" gap that broke the form motion-reduce:animate-none's grid. */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dob">{t("settings.dateOfBirth")}</Label>
|
||||
@@ -459,7 +459,7 @@ export function AccountSection() {
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={saving}>
|
||||
{saving ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -483,7 +483,7 @@ export function AccountSection() {
|
||||
disabled={passkeyLoading}
|
||||
>
|
||||
{passkeyLoading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<KeyRound className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -728,7 +728,7 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
which on a 393px viewport hid the right-most column —
|
||||
including the destructive delete action. Render the desktop
|
||||
table only at `≥ md`, and at `< md` paint a card-list where
|
||||
every passkey's name, device type, backup status, created
|
||||
every passkey motion-reduce:animate-none's name, device type, backup status, created
|
||||
date, and delete action are all visible without scrolling. */}
|
||||
<div className="border-border mt-3 hidden overflow-x-auto rounded-lg border md:block">
|
||||
<table className="w-full text-sm">
|
||||
|
||||
@@ -281,7 +281,7 @@ function DataResetCard() {
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" disabled={deleting}>
|
||||
{deleting ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -716,7 +716,7 @@ function CodexProviderForm({
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
* OpenAI form — API key + model select + collapsed Base URL override.
|
||||
* Save mutation flips both `aiProvider` (for the legacy single-result
|
||||
* resolver) and `aiOpenaiKeyEncrypted` (the user's key) so an OPENAI
|
||||
* resolver) and `aiOpenaiKeyEncrypted` (the user motion-reduce:animate-none's key) so an OPENAI
|
||||
* pick is visible to every code path that reads the row.
|
||||
* ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -889,7 +889,7 @@ function OpenAIProviderForm({
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -1045,7 +1045,7 @@ function AnthropicProviderForm({
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -1202,7 +1202,7 @@ function LocalProviderForm({
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -1454,7 +1454,7 @@ function FallbackChainCard({
|
||||
data-testid="ai-fallback-chain-save"
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -1659,7 +1659,7 @@ function RuntimeActionsRow({
|
||||
data-testid="ai-test-active-provider"
|
||||
>
|
||||
{testing ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -1673,7 +1673,7 @@ function RuntimeActionsRow({
|
||||
disabled={regen}
|
||||
>
|
||||
{regen ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -210,7 +210,7 @@ function ApiTokensCard() {
|
||||
size="sm"
|
||||
disabled={creating || !newName.trim()}
|
||||
>
|
||||
{creating && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
||||
{creating && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />}
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -241,7 +241,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
|
||||
{isLoading || !layout ? (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
) : (
|
||||
@@ -342,7 +342,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending && (
|
||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -224,7 +224,7 @@ function DoctorReportCard() {
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={busy}
|
||||
>
|
||||
{busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
||||
{busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />}
|
||||
{t("settings.sections.export.cards.doctorReport.action")}
|
||||
</Button>
|
||||
}
|
||||
@@ -358,7 +358,7 @@ function CsvCard({
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Download className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -445,7 +445,7 @@ function MedicationsCsvCard() {
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Download className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -534,7 +534,7 @@ function FullBackupCard() {
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<FileText className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -434,7 +434,7 @@ function WithingsCard({
|
||||
}
|
||||
>
|
||||
{credsSaving ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -463,7 +463,7 @@ function WithingsCard({
|
||||
disabled={syncing}
|
||||
>
|
||||
{syncing ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -473,7 +473,7 @@ function WithingsCard({
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={syncing}>
|
||||
{syncing ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -726,7 +726,7 @@ function MoodLogCard({
|
||||
disabled={saving || (!url.trim() && !apiKey.trim())}
|
||||
size="sm"
|
||||
>
|
||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
@@ -792,7 +792,7 @@ function MoodLogCard({
|
||||
disabled={syncing}
|
||||
onClick={() => handleSync(false)}
|
||||
>
|
||||
{syncing && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{syncing && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t("settings.moodLogSync")}
|
||||
</Button>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function NotificationStatusCard() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ function ChannelRow({
|
||||
className="min-h-11"
|
||||
>
|
||||
{reEnablePending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<RotateCw className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -296,7 +296,7 @@ function ChannelRow({
|
||||
className="min-h-11"
|
||||
>
|
||||
{testPending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Send className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -195,7 +195,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
onClick={() => test.mutate()}
|
||||
>
|
||||
{test.isPending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Send className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
@@ -203,7 +203,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
</Button>
|
||||
<Button type="submit" disabled={save.isPending}>
|
||||
{save.isPending && (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
|
||||
@@ -200,7 +200,7 @@ export function TelegramCard({
|
||||
onClick={handleTest}
|
||||
>
|
||||
{testing ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
@@ -208,7 +208,7 @@ export function TelegramCard({
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving}>
|
||||
{saving ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -87,7 +87,7 @@ export function TestConnectionButton({
|
||||
disabled={disabled || testing}
|
||||
>
|
||||
{testing ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Plug className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -182,7 +182,7 @@ export function WebPushCard() {
|
||||
<div className="mt-4 space-y-3">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : !isSupported ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
@@ -202,7 +202,7 @@ export function WebPushCard() {
|
||||
disabled={actionLoading}
|
||||
>
|
||||
{actionLoading && (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("settings.webPushUnsubscribe")}
|
||||
</Button>
|
||||
@@ -214,7 +214,7 @@ export function WebPushCard() {
|
||||
disabled={actionLoading}
|
||||
>
|
||||
{actionLoading ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<BellRing className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin motion-reduce:animate-none" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user