style: one spacing, title and skeleton language across the app

Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
This commit is contained in:
Marc-André Bombeck committed 2026-06-10 16:54:15 +02:00
1 parent 4ca3de5ff3
commit 04765ea8ac
92 files changed
+1433 -1445

No files matched your search

+12 -5
View File
@@ -23,7 +23,12 @@
import { useState } from "react";
import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Loader2, MessageCircleQuestion, NotebookPen, Trash2 } from "lucide-react";
import {
Loader2,
MessageCircleQuestion,
NotebookPen,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -180,9 +185,7 @@ export function AboutMeSection({
maxLength={fieldMaxChars}
placeholder={placeholder}
disabled={disabled}
onChange={(e) =>
setDrafts((d) => ({ ...d, [key]: e.target.value }))
}
onChange={(e) => setDrafts((d) => ({ ...d, [key]: e.target.value }))}
/>
</div>
);
@@ -342,7 +345,11 @@ export function AboutMeSection({
)}
{query.isError && (
<p role="status" aria-live="polite" className="text-destructive text-xs">
<p
role="status"
aria-live="polite"
className="text-destructive text-sm"
>
{t("settings.ai.aboutMe.loadError")}
</p>
)}
+8 -8
View File
@@ -334,9 +334,9 @@ export function AboutSection() {
target="_blank"
rel="noopener noreferrer"
>
<GitBranch className="mr-2 h-4 w-4" />
<GitBranch className="h-4 w-4" />
{t("settings.about.repository")}
<ExternalLink className="ml-2 h-3.5 w-3.5" />
<ExternalLink className="h-3.5 w-3.5" />
</a>
</Button>
<Button asChild variant="outline" size="sm">
@@ -345,16 +345,16 @@ export function AboutSection() {
target="_blank"
rel="noopener noreferrer"
>
<Sparkles className="mr-2 h-4 w-4" />
<Sparkles className="h-4 w-4" />
{t("settings.about.changelog")}
<ExternalLink className="ml-2 h-3.5 w-3.5" />
<ExternalLink className="h-3.5 w-3.5" />
</a>
</Button>
<Button asChild variant="outline" size="sm">
<a href={version.docs} target="_blank" rel="noopener noreferrer">
<BookOpen className="mr-2 h-4 w-4" />
<BookOpen className="h-4 w-4" />
{t("settings.about.docs")}
<ExternalLink className="ml-2 h-3.5 w-3.5" />
<ExternalLink className="h-3.5 w-3.5" />
</a>
</Button>
</div>
@@ -392,9 +392,9 @@ export function AboutSection() {
className="w-full shrink-0 sm:w-auto"
>
{replayingTour ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Compass className="mr-2 h-4 w-4" />
<Compass className="h-4 w-4" />
)}
{t("settings.about.tourReplay")}
</Button>
+12 -12
View File
@@ -587,9 +587,9 @@ export function AccountSection() {
<div className="flex justify-end">
<Button type="submit" disabled={saving}>
{saving ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-2 h-4 w-4" />
<Save className="h-4 w-4" />
)}
{t("common.save")}
</Button>
@@ -623,9 +623,9 @@ export function AccountSection() {
disabled={passkeyLoading}
>
{passkeyLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<KeyRound className="mr-2 h-4 w-4" />
<KeyRound className="h-4 w-4" />
)}
{t("settings.addPasskey")}
</Button>
@@ -703,9 +703,9 @@ export function AccountSection() {
className="w-full shrink-0 sm:w-auto"
>
{tourRestarting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Compass className="mr-2 h-4 w-4" />
<Compass className="h-4 w-4" />
)}
{t("onboarding.tour.restart")}
</Button>
@@ -794,9 +794,9 @@ export function AccountSection() {
<Button type="submit" variant="outline" disabled={passwordSaving}>
{passwordSaving ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-2 h-4 w-4" />
<Save className="h-4 w-4" />
)}
{t("settings.changePassword")}
</Button>
@@ -1180,9 +1180,9 @@ function AvatarSection() {
onClick={() => fileInputRef.current?.click()}
>
{upload.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<ImageUp className="mr-2 h-4 w-4" />
<ImageUp className="h-4 w-4" />
)}
{avatarUrl
? t("settings.avatar.replace")
@@ -1200,9 +1200,9 @@ function AvatarSection() {
}}
>
{remove.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Trash2 className="mr-2 h-4 w-4" />
<Trash2 className="h-4 w-4" />
)}
{t("settings.avatar.remove")}
</Button>
+5 -7
View File
@@ -90,9 +90,7 @@ function researchModeStatusLabel(
if (gate === "off") return t("settings.researchMode.disabledStatus");
if (gate === "stale") return t("settings.researchMode.enabledStaleStatus");
return t("settings.researchMode.acknowledgedOn", {
date: status?.acknowledgedAt
? formatDateTime(status.acknowledgedAt)
: "—",
date: status?.acknowledgedAt ? formatDateTime(status.acknowledgedAt) : "—",
});
}
@@ -306,9 +304,9 @@ function DataResetCard() {
className="w-full shrink-0 sm:w-auto"
>
{deleting ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Trash2 className="mr-1 h-3.5 w-3.5" />
<Trash2 className="h-3.5 w-3.5" />
)}
{t("settings.dangerZone")}
</Button>
@@ -450,9 +448,9 @@ function AccountDeleteCard() {
data-slot="settings-account-delete-trigger"
>
{deleting ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Trash2 className="mr-1 h-3.5 w-3.5" />
<Trash2 className="h-3.5 w-3.5" />
)}
{t("settings.deleteAccountCta")}
</Button>
+19 -21
View File
@@ -201,9 +201,7 @@ export function AiSection() {
{/* v1.11.2 — "What the Coach remembers": durable-fact review +
forget controls. Gated on the Coach being enabled. */}
{coachEnabled && (
<CoachMemorySection isAuthenticated={isAuthenticated} />
)}
{coachEnabled && <CoachMemorySection isAuthenticated={isAuthenticated} />}
{/* v1.15.20 — user-authored "about me" context for the Coach +
daily briefing. Not gated on the Coach toggle: the briefing
@@ -836,9 +834,9 @@ function CodexProviderForm({
disabled={disconnecting}
>
{disconnecting ? (
<Loader2 className="mr-1 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Trash2 className="mr-1 h-4 w-4" />
<Trash2 className="h-4 w-4" />
)}
{t("settings.ai.codex.disconnectButton")}
</Button>
@@ -900,9 +898,9 @@ function CodexProviderForm({
className="w-full sm:w-auto"
>
{devicePolling ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
<Sparkles className="h-4 w-4" />
)}
{t("settings.ai.codex.connectButton")}
</Button>
@@ -1096,9 +1094,9 @@ function OpenAIProviderForm({
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-2 h-4 w-4" />
<Save className="h-4 w-4" />
)}
{t("settings.ai.saveCta")}
</Button>
@@ -1252,9 +1250,9 @@ function AnthropicProviderForm({
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-2 h-4 w-4" />
<Save className="h-4 w-4" />
)}
{t("settings.ai.saveCta")}
</Button>
@@ -1409,9 +1407,9 @@ function LocalProviderForm({
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-2 h-4 w-4" />
<Save className="h-4 w-4" />
)}
{t("settings.ai.saveCta")}
</Button>
@@ -1661,16 +1659,16 @@ function FallbackChainCard({
data-testid="ai-fallback-chain-save"
>
{saveMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-2 h-4 w-4" />
<Save className="h-4 w-4" />
)}
{t("settings.ai.providerChain.saveOrder")}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="outline">
<RotateCcw className="mr-2 h-4 w-4" />
<RotateCcw className="h-4 w-4" />
{t("settings.ai.providerChain.resetDefaults")}
</Button>
</AlertDialogTrigger>
@@ -1734,7 +1732,7 @@ function AddProviderControl({
if (isProviderType(picked)) onAdd(picked);
}}
>
<PlusCircle className="mr-2 h-4 w-4" />
<PlusCircle className="h-4 w-4" />
{t("settings.ai.providerChain.addProvider")}
</Button>
</div>
@@ -1933,9 +1931,9 @@ function RuntimeActionsRow({
data-testid="ai-test-active-provider"
>
{testing ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
<Sparkles className="h-4 w-4" />
)}
{t("settings.ai.testProvider")}
</Button>
@@ -1947,9 +1945,9 @@ function RuntimeActionsRow({
disabled={regen}
>
{regen ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
<RefreshCw className="h-4 w-4" />
)}
{t("settings.regenerateInsights")}
</Button>
+2 -2
View File
@@ -281,7 +281,7 @@ function ApiTokensCard() {
disabled={creating || !newName.trim()}
>
{creating && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.create")}
</Button>
@@ -502,7 +502,7 @@ function ApiTokensCard() {
size="sm"
className="text-destructive border-destructive/30 min-h-11 w-full"
>
<Trash2 className="mr-2 h-3.5 w-3.5" />
<Trash2 className="h-3.5 w-3.5" />
{t("settings.tokenRevoke")}
</Button>
</AlertDialogTrigger>
@@ -216,7 +216,7 @@ export function CoachMemorySection({
<p
role="status"
aria-live="polite"
className="text-destructive text-xs"
className="text-destructive text-sm"
>
{t("settings.ai.coachMemory.loadError")}
</p>
@@ -324,7 +324,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
disabled={resetMutation.isPending}
className="self-end sm:self-auto"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
<RotateCcw className="h-3.5 w-3.5" />
{t("dashboard.layoutReset")}
</Button>
</div>
@@ -512,7 +512,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
disabled={saveMutation.isPending}
>
{saveMutation.isPending && (
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.save")}
</Button>
@@ -637,7 +637,7 @@ function SortableWidgetRow({
// visible GripVertical at 28 px (matches the Switch primitive
// pattern from v1.4.43 W5-H1). v1.4.47 W10 design-M1 — drop
// dnd-kit's CSS transition under prefers-reduced-motion.
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background -m-1 relative inline-flex h-7 w-7 cursor-grab touch-none items-center justify-center rounded transition-colors before:absolute before:inset-[-8px] before:content-[''] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background relative -m-1 inline-flex h-7 w-7 cursor-grab touch-none items-center justify-center rounded transition-colors before:absolute before:inset-[-8px] before:content-[''] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
>
<GripVertical className="h-4 w-4" />
</button>
+12 -12
View File
@@ -289,9 +289,9 @@ function CsvCard({
disabled={busy}
>
{busy ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Download className="mr-1 h-3.5 w-3.5" />
<Download className="h-3.5 w-3.5" />
)}
{t("settings.sections.export.actions.download")}
</Button>
@@ -305,7 +305,7 @@ function CsvCard({
setUntil={setUntil}
/>
{error && (
<p role="alert" className="text-destructive text-xs">
<p role="alert" className="text-destructive text-sm">
{error}
</p>
)}
@@ -376,9 +376,9 @@ function MedicationsCsvCard() {
disabled={busy}
>
{busy ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Download className="mr-1 h-3.5 w-3.5" />
<Download className="h-3.5 w-3.5" />
)}
{t("settings.sections.export.actions.download")}
</Button>
@@ -406,7 +406,7 @@ function MedicationsCsvCard() {
</span>
</label>
{error && (
<p role="alert" className="text-destructive text-xs">
<p role="alert" className="text-destructive text-sm">
{error}
</p>
)}
@@ -465,16 +465,16 @@ function FullBackupCard() {
disabled={busy}
>
{busy ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<FileText className="mr-1 h-3.5 w-3.5" />
<FileText className="h-3.5 w-3.5" />
)}
{t("settings.sections.export.actions.download")}
</Button>
}
>
{error && (
<p role="alert" className="text-destructive text-xs">
<p role="alert" className="text-destructive text-sm">
{error}
</p>
)}
@@ -575,16 +575,16 @@ function CycleExportCard() {
disabled={busy}
>
{busy ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Download className="mr-1 h-3.5 w-3.5" />
<Download className="h-3.5 w-3.5" />
)}
{t("settings.sections.export.actions.download")}
</Button>
}
>
{error && (
<p role="alert" className="text-destructive text-xs">
<p role="alert" className="text-destructive text-sm">
{error}
</p>
)}
@@ -168,8 +168,7 @@ export function HealthRecordExportPanel() {
return;
}
const blob = await res.blob();
const ext =
format === "pdf" ? "pdf" : format === "fhir" ? "json" : "zip";
const ext = format === "pdf" ? "pdf" : format === "fhir" ? "json" : "zip";
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
@@ -306,127 +305,127 @@ export function HealthRecordExportPanel() {
className="animate-insight-in space-y-3"
style={{ animationDuration: "200ms" }}
>
<div className="border-border space-y-2 rounded-lg border p-3">
<p className="text-xs font-semibold">
{t("settings.healthRecord.groupVitals")}
</p>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.weight")}
checked={sections.weight}
onToggle={() => toggle("weight")}
/>
<ToggleRow
label={t("settings.healthRecord.bp")}
checked={sections.bp}
onToggle={() => toggle("bp")}
/>
<ToggleRow
label={t("settings.healthRecord.pulse")}
checked={sections.pulse}
onToggle={() => toggle("pulse")}
/>
<ToggleRow
label={t("settings.healthRecord.oxygenSaturation")}
checked={sections.oxygenSaturation}
onToggle={() => toggle("oxygenSaturation")}
/>
<ToggleRow
label={t("settings.healthRecord.bodyFat")}
checked={sections.bodyFat}
onToggle={() => toggle("bodyFat")}
/>
<ToggleRow
label={t("settings.healthRecord.bodyComposition")}
checked={sections.bodyComposition}
onToggle={() => toggle("bodyComposition")}
/>
</div>
</div>
<div className="border-border space-y-2 rounded-lg border p-3">
<p className="text-xs font-semibold">
{t("settings.healthRecord.groupVitals")}
</p>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.weight")}
checked={sections.weight}
onToggle={() => toggle("weight")}
/>
<ToggleRow
label={t("settings.healthRecord.bp")}
checked={sections.bp}
onToggle={() => toggle("bp")}
/>
<ToggleRow
label={t("settings.healthRecord.pulse")}
checked={sections.pulse}
onToggle={() => toggle("pulse")}
/>
<ToggleRow
label={t("settings.healthRecord.oxygenSaturation")}
checked={sections.oxygenSaturation}
onToggle={() => toggle("oxygenSaturation")}
/>
<ToggleRow
label={t("settings.healthRecord.bodyFat")}
checked={sections.bodyFat}
onToggle={() => toggle("bodyFat")}
/>
<ToggleRow
label={t("settings.healthRecord.bodyComposition")}
checked={sections.bodyComposition}
onToggle={() => toggle("bodyComposition")}
/>
</div>
</div>
<div className="border-border space-y-2 rounded-lg border p-3">
<p className="text-xs font-semibold">
{t("settings.healthRecord.groupCardio")}
</p>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.restingHeartRate")}
checked={sections.restingHeartRate}
onToggle={() => toggle("restingHeartRate")}
/>
<ToggleRow
label={t("settings.healthRecord.hrv")}
checked={sections.hrv}
onToggle={() => toggle("hrv")}
/>
<ToggleRow
label={t("settings.healthRecord.vo2max")}
checked={sections.vo2max}
onToggle={() => toggle("vo2max")}
/>
</div>
</div>
<div className="border-border space-y-2 rounded-lg border p-3">
<p className="text-xs font-semibold">
{t("settings.healthRecord.groupCardio")}
</p>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.restingHeartRate")}
checked={sections.restingHeartRate}
onToggle={() => toggle("restingHeartRate")}
/>
<ToggleRow
label={t("settings.healthRecord.hrv")}
checked={sections.hrv}
onToggle={() => toggle("hrv")}
/>
<ToggleRow
label={t("settings.healthRecord.vo2max")}
checked={sections.vo2max}
onToggle={() => toggle("vo2max")}
/>
</div>
</div>
<div className="border-border space-y-2 rounded-lg border p-3">
<p className="text-xs font-semibold">
{t("settings.healthRecord.groupActivity")}
</p>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.steps")}
checked={sections.steps}
onToggle={() => toggle("steps")}
/>
<ToggleRow
label={t("settings.healthRecord.distance")}
checked={sections.distance}
onToggle={() => toggle("distance")}
/>
<ToggleRow
label={t("settings.healthRecord.sleep")}
checked={sections.sleep}
onToggle={() => toggle("sleep")}
/>
</div>
</div>
<div className="border-border space-y-2 rounded-lg border p-3">
<p className="text-xs font-semibold">
{t("settings.healthRecord.groupActivity")}
</p>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.steps")}
checked={sections.steps}
onToggle={() => toggle("steps")}
/>
<ToggleRow
label={t("settings.healthRecord.distance")}
checked={sections.distance}
onToggle={() => toggle("distance")}
/>
<ToggleRow
label={t("settings.healthRecord.sleep")}
checked={sections.sleep}
onToggle={() => toggle("sleep")}
/>
</div>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.glucose")}
checked={sections.glucose}
onToggle={() => toggle("glucose")}
/>
<ToggleRow
label={t("settings.healthRecord.medications")}
checked={sections.medList && sections.compliance}
onToggle={toggleMedications}
/>
<ToggleRow
label={t("settings.healthRecord.bmi")}
checked={sections.bmi}
onToggle={() => toggle("bmi")}
/>
<ToggleRow
label={t("settings.healthRecord.mood")}
checked={sections.mood}
onToggle={() => toggle("mood")}
/>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<ToggleRow
label={t("settings.healthRecord.glucose")}
checked={sections.glucose}
onToggle={() => toggle("glucose")}
/>
<ToggleRow
label={t("settings.healthRecord.medications")}
checked={sections.medList && sections.compliance}
onToggle={toggleMedications}
/>
<ToggleRow
label={t("settings.healthRecord.bmi")}
checked={sections.bmi}
onToggle={() => toggle("bmi")}
/>
<ToggleRow
label={t("settings.healthRecord.mood")}
checked={sections.mood}
onToggle={() => toggle("mood")}
/>
</div>
{isPdfLike && (
<ToggleRow
label={t("settings.healthRecord.includeCharts")}
checked={includeCharts}
onToggle={() => setIncludeCharts((v) => !v)}
/>
)}
{isPdfLike && (
<ToggleRow
label={t("settings.healthRecord.includeAiSummary")}
checked={includeAiSummary}
onToggle={() => setIncludeAiSummary((v) => !v)}
/>
)}
{isPdfLike && (
<ToggleRow
label={t("settings.healthRecord.includeCharts")}
checked={includeCharts}
onToggle={() => setIncludeCharts((v) => !v)}
/>
)}
{isPdfLike && (
<ToggleRow
label={t("settings.healthRecord.includeAiSummary")}
checked={includeAiSummary}
onToggle={() => setIncludeAiSummary((v) => !v)}
/>
)}
</div>
)}
</fieldset>
@@ -449,9 +448,9 @@ export function HealthRecordExportPanel() {
className="ml-auto min-h-11 sm:min-h-9"
>
{busy ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Download className="mr-2 h-4 w-4" />
<Download className="h-4 w-4" />
)}
{t("settings.healthRecord.generate")}
</Button>
+30 -20
View File
@@ -168,15 +168,21 @@ function AppleHealthImportCard() {
body: form,
});
if (res.status === 429) {
setUploadError(t("settings.sections.export.import.appleHealth.rateLimited"));
setUploadError(
t("settings.sections.export.import.appleHealth.rateLimited"),
);
return;
}
if (res.status === 413) {
setUploadError(t("settings.sections.export.import.appleHealth.tooLarge"));
setUploadError(
t("settings.sections.export.import.appleHealth.tooLarge"),
);
return;
}
if (!res.ok) {
setUploadError(t("settings.sections.export.import.appleHealth.uploadFailed"));
setUploadError(
t("settings.sections.export.import.appleHealth.uploadFailed"),
);
return;
}
const body = (await res.json()).data as {
@@ -184,12 +190,16 @@ function AppleHealthImportCard() {
status: string;
};
if (!body?.jobId) {
setUploadError(t("settings.sections.export.import.appleHealth.uploadFailed"));
setUploadError(
t("settings.sections.export.import.appleHealth.uploadFailed"),
);
return;
}
setJobId(body.jobId);
} catch {
setUploadError(t("settings.sections.export.import.appleHealth.uploadFailed"));
setUploadError(
t("settings.sections.export.import.appleHealth.uploadFailed"),
);
} finally {
setUploading(false);
}
@@ -212,8 +222,7 @@ function AppleHealthImportCard() {
}
const status = statusQuery.data ?? null;
const isRunning =
status !== null && !TERMINAL_STATES.includes(status.status);
const isRunning = status !== null && !TERMINAL_STATES.includes(status.status);
const isDone = status?.status === "done";
const isFailed = status?.status === "failed";
const busy = uploading || isRunning;
@@ -265,7 +274,9 @@ function AppleHealthImportCard() {
type="file"
accept=".zip,application/zip"
className="sr-only"
aria-label={t("settings.sections.export.import.appleHealth.fileInputLabel")}
aria-label={t(
"settings.sections.export.import.appleHealth.fileInputLabel",
)}
onChange={onFileChange}
/>
@@ -292,9 +303,7 @@ function AppleHealthImportCard() {
`settings.sections.export.import.appleHealth.phase.${status?.status ?? "queued"}`,
)}
</p>
{typeof percent === "number" && (
<Progress value={percent} />
)}
{typeof percent === "number" && <Progress value={percent} />}
{typeof status?.progress?.rowsUpserted === "number" && (
<p className="text-muted-foreground text-xs">
{t("settings.sections.export.import.appleHealth.rowsImported", {
@@ -325,7 +334,7 @@ function AppleHealthImportCard() {
{(isFailed || uploadError) && (
<p
role="alert"
className="text-destructive flex items-start gap-2 text-xs"
className="text-destructive flex items-start gap-2 text-sm"
>
<AlertCircle
className="mt-0.5 h-3.5 w-3.5 shrink-0"
@@ -350,9 +359,9 @@ function AppleHealthImportCard() {
data-testid="import-action-apple-health"
>
{busy ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Upload className="mr-1 h-3.5 w-3.5" />
<Upload className="h-3.5 w-3.5" />
)}
{t("settings.sections.export.import.appleHealth.choose")}
</Button>
@@ -485,7 +494,8 @@ function JsonImportCard() {
if (res.status === 422) {
const body = await res.json().catch(() => null);
setError(
body?.error ?? t("settings.sections.export.import.json.invalidPayload"),
body?.error ??
t("settings.sections.export.import.json.invalidPayload"),
);
return;
}
@@ -570,7 +580,7 @@ function JsonImportCard() {
{error && (
<p
role="alert"
className="text-destructive flex items-start gap-2 text-xs"
className="text-destructive flex items-start gap-2 text-sm"
>
<AlertCircle
className="mt-0.5 h-3.5 w-3.5 shrink-0"
@@ -589,7 +599,7 @@ function JsonImportCard() {
onClick={() => fileInputRef.current?.click()}
data-testid="import-json-choose-file"
>
<Upload className="mr-1 h-3.5 w-3.5" />
<Upload className="h-3.5 w-3.5" />
{t("settings.sections.export.import.json.uploadFile")}
</Button>
<Button
@@ -599,7 +609,7 @@ function JsonImportCard() {
onClick={downloadExample}
data-testid="import-json-download-example"
>
<Download className="mr-1 h-3.5 w-3.5" />
<Download className="h-3.5 w-3.5" />
{t("settings.sections.export.import.json.downloadExample")}
</Button>
<Button
@@ -610,9 +620,9 @@ function JsonImportCard() {
data-testid="import-action-json"
>
{busy ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<FileJson className="mr-1 h-3.5 w-3.5" />
<FileJson className="h-3.5 w-3.5" />
)}
{t("settings.sections.export.import.json.import")}
</Button>
@@ -106,8 +106,8 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
// differs from what we last seeded from.
const serverSignature = layoutSignature(layout.tiles);
const [seededSignature, setSeededSignature] = useState(serverSignature);
const [draftTiles, setDraftTiles] = useState<InsightsTileConfig[]>(
() => [...layout.tiles].sort((a, b) => a.order - b.order),
const [draftTiles, setDraftTiles] = useState<InsightsTileConfig[]>(() =>
[...layout.tiles].sort((a, b) => a.order - b.order),
);
if (isSuccess && serverSignature !== seededSignature) {
setSeededSignature(serverSignature);
@@ -215,10 +215,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
<div className="space-y-1">
<h2
id="insights-pill-order-title"
className="text-lg font-semibold"
>
<h2 id="insights-pill-order-title" className="text-lg font-semibold">
{t("insights.pillOrder.title")}
</h2>
<p className="text-muted-foreground text-sm">
@@ -233,7 +230,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
className="self-end sm:self-auto"
>
{saveMutation.isPending && (
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("insights.pillOrder.save")}
</Button>
@@ -320,8 +317,14 @@ function SortablePillRow({
hideLabel: string;
onToggle: (id: string, visible: boolean) => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
useSortable({ id });
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
@@ -165,7 +165,7 @@ function IntegrationErrorMessage({ message }: { message: string }) {
return (
<p
data-testid="integration-error-message"
className="text-destructive flex items-start gap-1.5 text-xs"
className="text-destructive flex items-start gap-1.5 text-sm"
>
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span className="min-w-0 break-words">{message}</span>
@@ -321,7 +321,9 @@ function WithingsCard({
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.withings() });
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
queryClient.invalidateQueries({
queryKey: queryKeys.integrationsStatus(),
});
},
});
@@ -343,7 +345,9 @@ function WithingsCard({
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.withings() });
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
queryClient.invalidateQueries({
queryKey: queryKeys.integrationsStatus(),
});
},
});
@@ -368,7 +372,9 @@ function WithingsCard({
);
setSyncMsgType("success");
void invalidateKeys(queryClient, measurementDependentKeys);
queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus() });
queryClient.invalidateQueries({
queryKey: queryKeys.integrationsStatus(),
});
} else {
setSyncMsg(json.error || t("settings.withingsSyncFailed"));
setSyncMsgType("error");
@@ -445,7 +451,10 @@ function WithingsCard({
title={t("settings.withings")}
description={t("settings.withingsDescription")}
status={
<IntegrationStatusPill state={pillState} lastSyncAt={pillLastSyncAt} />
<IntegrationStatusPill
state={pillState}
lastSyncAt={pillLastSyncAt}
/>
}
/>
@@ -496,7 +505,7 @@ function WithingsCard({
data-testid="withings-parked-banner"
className="border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
>
<span className="text-warning min-w-0 break-words text-xs">
<span className="text-warning min-w-0 text-xs break-words">
{t("settings.integrationPill.parkedReconnect")}
</span>
<Button
@@ -509,9 +518,9 @@ function WithingsCard({
className="min-h-11"
>
{resume.isPending ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Link2 className="mr-1 h-3.5 w-3.5" />
<Link2 className="h-3.5 w-3.5" />
)}
{t("settings.integrationPill.resumeCta")}
</Button>
@@ -520,7 +529,7 @@ function WithingsCard({
{resume.isError && (
<p
role="alert"
className="text-destructive text-xs"
className="text-destructive text-sm"
data-testid="withings-resume-error"
>
{t("settings.integrationPill.resumeError")}
@@ -604,9 +613,9 @@ function WithingsCard({
}
>
{credsSaving ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-1 h-3.5 w-3.5" />
<Save className="h-3.5 w-3.5" />
)}
{t("settings.withingsSaveCredentials")}
</Button>
@@ -639,9 +648,9 @@ function WithingsCard({
disabled={syncing}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.withingsSync")}
</Button>
@@ -654,9 +663,9 @@ function WithingsCard({
disabled={syncing}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.withingsFullSync")}
</Button>
@@ -689,7 +698,7 @@ function WithingsCard({
size="sm"
className="text-destructive min-h-11"
>
<Unlink className="mr-1 h-3.5 w-3.5" />
<Unlink className="h-3.5 w-3.5" />
{t("settings.withingsDisconnect")}
</Button>
</AlertDialogTrigger>
@@ -730,7 +739,7 @@ function WithingsCard({
window.location.href = "/api/withings/connect";
}}
>
<Link2 className="mr-2 h-4 w-4" />
<Link2 className="h-4 w-4" />
{t("settings.withingsConnect")}
</Button>
) : (
@@ -816,7 +825,9 @@ function WhoopCard({
});
const json = await res.json();
if (res.ok) {
setSyncMsg(t("settings.whoopSyncResult", { count: json.data.imported }));
setSyncMsg(
t("settings.whoopSyncResult", { count: json.data.imported }),
);
setSyncMsgType("success");
void invalidateKeys(queryClient, measurementDependentKeys);
queryClient.invalidateQueries({ queryKey: queryKeys.whoop() });
@@ -897,7 +908,10 @@ function WhoopCard({
</>
}
status={
<IntegrationStatusPill state={pillState} lastSyncAt={pillLastSyncAt} />
<IntegrationStatusPill
state={pillState}
lastSyncAt={pillLastSyncAt}
/>
}
/>
@@ -920,7 +934,7 @@ function WhoopCard({
data-testid="whoop-parked-banner"
className="border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
>
<span className="text-warning min-w-0 break-words text-xs">
<span className="text-warning min-w-0 text-xs break-words">
{t("settings.integrationPill.parkedReconnect")}
</span>
<Button
@@ -933,9 +947,9 @@ function WhoopCard({
className="min-h-11"
>
{resume.isPending ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Link2 className="mr-1 h-3.5 w-3.5" />
<Link2 className="h-3.5 w-3.5" />
)}
{t("settings.integrationPill.resumeCta")}
</Button>
@@ -944,7 +958,7 @@ function WhoopCard({
{resume.isError && (
<p
role="alert"
className="text-destructive text-xs"
className="text-destructive text-sm"
data-testid="whoop-resume-error"
>
{t("settings.integrationPill.resumeError")}
@@ -1023,9 +1037,9 @@ function WhoopCard({
}
>
{credsSaving ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-1 h-3.5 w-3.5" />
<Save className="h-3.5 w-3.5" />
)}
{t("settings.whoopSaveCredentials")}
</Button>
@@ -1052,9 +1066,9 @@ function WhoopCard({
disabled={syncing}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.whoopSync")}
</Button>
@@ -1067,9 +1081,9 @@ function WhoopCard({
disabled={syncing}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.whoopFullSync")}
</Button>
@@ -1102,7 +1116,7 @@ function WhoopCard({
size="sm"
className="text-destructive min-h-11"
>
<Unlink className="mr-1 h-3.5 w-3.5" />
<Unlink className="h-3.5 w-3.5" />
{t("settings.whoopDisconnect")}
</Button>
</AlertDialogTrigger>
@@ -1148,7 +1162,7 @@ function WhoopCard({
window.location.href = "/api/whoop/connect";
}}
>
<Link2 className="mr-2 h-4 w-4" />
<Link2 className="h-4 w-4" />
{t("settings.whoopConnect")}
</Button>
) : (
@@ -1344,7 +1358,10 @@ function FitbitCard({
</>
}
status={
<IntegrationStatusPill state={pillState} lastSyncAt={pillLastSyncAt} />
<IntegrationStatusPill
state={pillState}
lastSyncAt={pillLastSyncAt}
/>
}
/>
@@ -1366,7 +1383,7 @@ function FitbitCard({
data-testid="fitbit-parked-banner"
className="border-warning/30 bg-warning/10 flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
>
<span className="text-warning min-w-0 break-words text-xs">
<span className="text-warning min-w-0 text-xs break-words">
{t("settings.integrationPill.parkedReconnect")}
</span>
<Button
@@ -1379,9 +1396,9 @@ function FitbitCard({
className="min-h-11"
>
{resume.isPending ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Link2 className="mr-1 h-3.5 w-3.5" />
<Link2 className="h-3.5 w-3.5" />
)}
{t("settings.integrationPill.resumeCta")}
</Button>
@@ -1390,7 +1407,7 @@ function FitbitCard({
{resume.isError && (
<p
role="alert"
className="text-destructive text-xs"
className="text-destructive text-sm"
data-testid="fitbit-resume-error"
>
{t("settings.integrationPill.resumeError")}
@@ -1469,9 +1486,9 @@ function FitbitCard({
}
>
{credsSaving ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-1 h-3.5 w-3.5" />
<Save className="h-3.5 w-3.5" />
)}
{t("settings.fitbitSaveCredentials")}
</Button>
@@ -1498,9 +1515,9 @@ function FitbitCard({
disabled={syncing}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.fitbitSync")}
</Button>
@@ -1513,9 +1530,9 @@ function FitbitCard({
disabled={syncing}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("settings.fitbitFullSync")}
</Button>
@@ -1548,7 +1565,7 @@ function FitbitCard({
size="sm"
className="text-destructive min-h-11"
>
<Unlink className="mr-1 h-3.5 w-3.5" />
<Unlink className="h-3.5 w-3.5" />
{t("settings.fitbitDisconnect")}
</Button>
</AlertDialogTrigger>
@@ -1594,7 +1611,7 @@ function FitbitCard({
window.location.href = "/api/fitbit/connect";
}}
>
<Link2 className="mr-2 h-4 w-4" />
<Link2 className="h-4 w-4" />
{t("settings.fitbitConnect")}
</Button>
) : (
@@ -92,7 +92,9 @@ export function NotificationStatusCard() {
if (!res.ok) throw new Error("Failed");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.notificationsStatus() });
queryClient.invalidateQueries({
queryKey: queryKeys.notificationsStatus(),
});
},
});
@@ -105,7 +107,9 @@ export function NotificationStatusCard() {
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.notificationsStatus() });
queryClient.invalidateQueries({
queryKey: queryKeys.notificationsStatus(),
});
},
});
@@ -286,9 +290,9 @@ function ChannelRow({
className="min-h-11"
>
{reEnablePending ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RotateCw className="mr-1 h-3.5 w-3.5" />
<RotateCw className="h-3.5 w-3.5" />
)}
{t("settings.notificationStatus.reEnable")}
</Button>
@@ -303,9 +307,9 @@ function ChannelRow({
className="min-h-11"
>
{testPending ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Send className="mr-1 h-3.5 w-3.5" />
<Send className="h-3.5 w-3.5" />
)}
{t("settings.notificationStatus.sendTest")}
</Button>
@@ -328,8 +332,7 @@ function stateBadgeFor(
return {
label: t("settings.notificationStatus.stateActive"),
variant: "default",
className:
"border-success/30 bg-success/15 text-success",
className: "border-success/30 bg-success/15 text-success",
icon: <CheckCircle2 className="h-3 w-3" aria-hidden />,
};
case "auto_disabled":
+1 -1
View File
@@ -166,7 +166,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
className="min-h-11"
>
{save.isPending && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.save")}
</Button>
+3 -3
View File
@@ -337,7 +337,7 @@ export function SourcesSection() {
disabled={resetMutation.isPending || saveMutation.isPending}
className="self-end sm:self-auto"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
<RotateCcw className="h-3.5 w-3.5" />
{t("settings.sections.sources.resetDefaults")}
</Button>
</div>
@@ -523,7 +523,7 @@ export function SourcesSection() {
onClick={resetDeviceTypeAxis}
disabled={saveMutation.isPending}
>
<RotateCcw className="mr-1 h-3 w-3" />
<RotateCcw className="h-3 w-3" />
{t("settings.sections.sources.resetDeviceTypes")}
</Button>
</div>
@@ -597,7 +597,7 @@ export function SourcesSection() {
disabled={saveMutation.isPending}
>
{saveMutation.isPending && (
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.save")}
</Button>
+3 -5
View File
@@ -156,9 +156,7 @@ export function TelegramCard({
<p
role="alert"
className={`text-sm ${
msgType === "success"
? "text-success"
: "text-destructive"
msgType === "success" ? "text-success" : "text-destructive"
}`}
>
{msg}
@@ -172,9 +170,9 @@ export function TelegramCard({
/>
<Button type="submit" disabled={saving} className="min-h-11">
{saving ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-1 h-3.5 w-3.5" />
<Save className="h-3.5 w-3.5" />
)}
{t("common.save")}
</Button>
@@ -88,9 +88,9 @@ export function TestConnectionButton({
disabled={disabled || testing}
>
{testing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Plug className="mr-1 h-3.5 w-3.5" />
<Plug className="h-3.5 w-3.5" />
)}
{testing
? t("settings.testConnection.testing")
@@ -110,7 +110,7 @@ export function TestConnectionButton({
{result?.kind === "error" && (
<p
role="alert"
className="text-destructive flex items-center gap-1.5 text-xs"
className="text-destructive flex items-center gap-1.5 text-sm"
>
<XCircle className="h-3.5 w-3.5" />
{describeError(result.errorCode)}
@@ -138,7 +138,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
onClick={() => resetMutation.mutate(null)}
disabled={resetMutation.isPending}
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
<RotateCcw className="h-3.5 w-3.5" />
{t("thresholds.resetAllAction")}
</Button>
)}
+3 -3
View File
@@ -172,7 +172,7 @@ export function WebPushCard() {
disabled={actionLoading}
>
{actionLoading && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("settings.webPushUnsubscribe")}
</Button>
@@ -185,9 +185,9 @@ export function WebPushCard() {
disabled={actionLoading}
>
{actionLoading ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<BellRing className="mr-1 h-3.5 w-3.5" />
<BellRing className="h-3.5 w-3.5" />
)}
{t("settings.webPushSubscribe")}
</Button>