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:
Marc-André Bombeck committed 2026-05-15 17:13:42 +02:00
1 parent 19b5129306
commit 32a023dfff
71 files changed
+165 -136

No files matched your search

+1 -1
View File
@@ -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>
);
}
+2 -2
View File
@@ -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" />
)}
+1 -1
View File
@@ -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" />
)}
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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>
);
}
+1 -1
View File
@@ -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>
);
}
+1 -1
View File
@@ -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>
);
}
+1 -1
View File
@@ -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>
);
}
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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>
);
}
+1 -1
View File
@@ -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>
);
}
+1 -1
View File
@@ -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>
);
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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" />
)}
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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" />
)}
+2 -2
View File
@@ -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">
+3 -3
View File
@@ -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">
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -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>
);
}
+2 -2
View File
@@ -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>
);
+1 -1
View File
@@ -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" />
)}
+11 -11
View File
@@ -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>
+1 -1
View File
@@ -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" />
)}
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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" />
)}
+4 -4
View File
@@ -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">
+1 -1
View File
@@ -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" />
)}
+7 -7
View File
@@ -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" />
)}
+1 -1
View File
@@ -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>
+4 -4
View File
@@ -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" />
)}
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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" />
)}
+3 -3
View File
@@ -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" />
)}
+1 -1
View File
@@ -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={
{