mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data showed only awake and asleep — the per-session source pick now prefers the source that knows its stages. Tour popovers clamp into the viewport, wrap their footer and fall back to a bottom sheet on narrow screens. Settings and admin sections share one alignment rule — icon column left, everything else flush on the content column — with neutral header icons, no duplicated section titles under the shell heading, and every select on the account-style idiom with a visible chevron. The dashboard's customize icon matches insights, the target-settings link leaves the home charts, and the mood heatmap adopts the damped fills the other mood charts already use.
This commit is contained in:
1 parent
9f2c403f84
commit
024da9fe96
67 files changed
+545
-344
No files matched your search
@@ -33,7 +33,9 @@ import type { LucideIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SettingsCardHeaderProps {
|
||||
/** Lucide icon component; rendered as `text-primary h-5 w-5`. */
|
||||
/** Lucide icon component; rendered as `text-muted-foreground h-5 w-5`.
|
||||
* Neutral by design — primary/purple stays reserved for actions and
|
||||
* highlight states, never for section-header iconography. */
|
||||
icon: LucideIcon;
|
||||
/** Card title — rendered as `<h2 class="text-lg font-semibold">`.
|
||||
* Accepts a node so a card can wrap the title in a link. */
|
||||
@@ -68,7 +70,7 @@ export function SettingsCardHeader({
|
||||
return (
|
||||
<header className={cn("flex items-start gap-2", className)}>
|
||||
<Icon
|
||||
className="text-primary mt-0.5 h-5 w-5 shrink-0"
|
||||
className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* Title + description share one column to the RIGHT of the icon, so the
|
||||
|
||||
@@ -229,7 +229,7 @@ export function AboutMeSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-4 pl-7 sm:grid-cols-2">
|
||||
{structuredField(
|
||||
"conditions",
|
||||
t("settings.ai.aboutMe.conditionsLabel"),
|
||||
@@ -249,7 +249,7 @@ export function AboutMeSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1.5 pl-7">
|
||||
<Label htmlFor="settings-about-me-freetext">
|
||||
{t("settings.ai.aboutMe.freeTextLabel")}
|
||||
</Label>
|
||||
@@ -267,7 +267,7 @@ export function AboutMeSection({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pl-7">
|
||||
<p
|
||||
data-testid="settings-about-me-count"
|
||||
className="text-muted-foreground text-xs tabular-nums"
|
||||
@@ -354,7 +354,7 @@ export function AboutMeSection({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground border-border border-t pt-3 text-xs">
|
||||
<p className="text-muted-foreground border-border border-t pt-3 pl-7 text-xs">
|
||||
{t("settings.ai.aboutMe.hint")} {t("settings.ai.aboutMe.profileHint")}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -238,7 +238,7 @@ export function AboutSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-about-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.about.title")}
|
||||
</h1>
|
||||
@@ -255,14 +255,14 @@ export function AboutSection() {
|
||||
<SettingsCardHeader icon={Info} title="HealthLog" className="mb-4" />
|
||||
|
||||
{isLoading || !version ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 pl-7">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t("common.loading")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<dl className="flex flex-wrap items-baseline gap-x-6 gap-y-3 text-sm">
|
||||
<dl className="flex flex-wrap items-baseline gap-x-6 gap-y-3 pl-7 text-sm">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
||||
{t("settings.about.version")}
|
||||
@@ -327,7 +327,7 @@ export function AboutSection() {
|
||||
title={t("settings.about.linksHeading")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||
<div className="flex flex-col gap-2 pl-7 sm:flex-row sm:flex-wrap">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a
|
||||
href={version.repository}
|
||||
@@ -372,16 +372,16 @@ export function AboutSection() {
|
||||
tour" card so the two surfaces feel consistent. */}
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="text-primary h-5 w-5" />
|
||||
<div className="flex items-start gap-2">
|
||||
<Compass className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.about.tourReplay")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.about.tourReplayHint")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.about.tourReplayHint")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -402,7 +402,7 @@ export function AboutSection() {
|
||||
{tourFeedback && (
|
||||
<p
|
||||
role="alert"
|
||||
className={`mt-2 text-xs ${
|
||||
className={`mt-2 pl-7 text-xs ${
|
||||
tourFeedback.type === "success"
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
|
||||
@@ -383,7 +383,7 @@ export function AccountSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-account-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.account.title")}
|
||||
</h1>
|
||||
@@ -402,7 +402,7 @@ export function AccountSection() {
|
||||
title={t("settings.profile")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<form onSubmit={handleSaveProfile} className="space-y-4">
|
||||
<form onSubmit={handleSaveProfile} className="space-y-4 pl-7">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">{t("settings.username")}</Label>
|
||||
@@ -615,8 +615,10 @@ export function AccountSection() {
|
||||
title={t("settings.passkeys")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<PasskeyListSection isAuthenticated={isAuthenticated} />
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="pl-7">
|
||||
<PasskeyListSection isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end pl-7">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleAddPasskey}
|
||||
@@ -653,16 +655,16 @@ export function AccountSection() {
|
||||
right border by ~48 px). */}
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="text-primary h-5 w-5" />
|
||||
<div className="flex items-start gap-2">
|
||||
<Shield className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.passwordReset")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.changePasswordDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.changePasswordDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -683,16 +685,16 @@ export function AccountSection() {
|
||||
as the password card so both action surfaces look identical. */}
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="text-primary h-5 w-5" />
|
||||
<div className="flex items-start gap-2">
|
||||
<Compass className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("onboarding.tour.restart")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("onboarding.tour.restartHint")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("onboarding.tour.restartHint")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -1156,10 +1158,10 @@ function AvatarSection() {
|
||||
title={t("settings.avatar.title")}
|
||||
className="mb-4"
|
||||
/>
|
||||
<p className="text-muted-foreground mb-4 text-sm">
|
||||
<p className="text-muted-foreground mb-4 pl-7 text-sm">
|
||||
{t("settings.avatar.description")}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4 pl-7">
|
||||
<Avatar className="size-16">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={user.username} />}
|
||||
<AvatarFallback className="text-base">
|
||||
|
||||
@@ -57,7 +57,7 @@ export function AdvancedSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-advanced-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.advanced.title")}
|
||||
</h1>
|
||||
@@ -154,12 +154,12 @@ function ResearchModeCard() {
|
||||
data-slot="settings-research-mode-card"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpenCheck className="text-primary h-5 w-5" />
|
||||
<BookOpenCheck className="text-muted-foreground h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.researchMode.sectionTitle")}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<p className="text-muted-foreground mt-1 pl-7 text-xs">
|
||||
{t("settings.researchMode.subtitle")}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { AboutMeSection } from "@/components/settings/about-me-section";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { CoachMemorySection } from "@/components/settings/coach-memory-section";
|
||||
import { CoachPrefsSection } from "@/components/settings/coach-prefs-section";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -183,7 +185,7 @@ export function AiSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-ai-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.ai.title")}
|
||||
</h1>
|
||||
@@ -199,6 +201,12 @@ export function AiSection() {
|
||||
through provider configuration before finding the toggle. */}
|
||||
<DisableCoachCard isAuthenticated={isAuthenticated} />
|
||||
|
||||
{/* v1.16.1 — Coach preferences (tone, verbosity, data scope)
|
||||
moved here from the in-chat sheet; the chat header gear
|
||||
deep-links to this section. Gated on the Coach being
|
||||
enabled. */}
|
||||
{coachEnabled && <CoachPrefsSection isAuthenticated={isAuthenticated} />}
|
||||
|
||||
{/* v1.11.2 — "What the Coach remembers": durable-fact review +
|
||||
forget controls. Gated on the Coach being enabled. */}
|
||||
{coachEnabled && <CoachMemorySection isAuthenticated={isAuthenticated} />}
|
||||
@@ -447,57 +455,64 @@ function AiInsightsCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-6">
|
||||
{/* v1.4.33 IW7 — the dedicated H2 ("KI-Insights") used to repeat
|
||||
the parent section title ("KI-Auswertungen"). Both are gone now:
|
||||
the section renders "Auswertungen" once at the top, and this
|
||||
card carries only the Sparkles icon + the live provider-status
|
||||
badges, mirroring the description that lives on the section
|
||||
header. The configuration description is moved one level up
|
||||
into the section subtitle so the visual hierarchy stays
|
||||
two-level (section -> form) instead of three-level
|
||||
(section -> card title -> form). */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<Sparkles className="text-primary h-5 w-5" aria-hidden="true" />
|
||||
<ProviderStatusBadges
|
||||
settings={insightsSettings}
|
||||
activeProvider={chainData?.activeProvider ?? null}
|
||||
{/* The card used to render an icon-only header row (Sparkles +
|
||||
status badges, no title), which left the tile unanchored next
|
||||
to its titled siblings. It now follows the shared
|
||||
`SettingsCardHeader` contract: icon column, title + description
|
||||
in the content column, status badges top-right — and the body
|
||||
below indents to the title column (`pl-7`). */}
|
||||
<SettingsCardHeader
|
||||
icon={Sparkles}
|
||||
title={t("settings.ai.providerCardTitle")}
|
||||
description={t("settings.kiInsightsDescription")}
|
||||
status={
|
||||
<ProviderStatusBadges
|
||||
settings={insightsSettings}
|
||||
activeProvider={chainData?.activeProvider ?? null}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-4 pl-7">
|
||||
<ActiveProviderSelect
|
||||
value={selectedProvider}
|
||||
onChange={pickProvider}
|
||||
/>
|
||||
|
||||
<ProviderConfigCard
|
||||
provider={selectedProvider}
|
||||
insightsSettings={insightsSettings}
|
||||
userProvider={userProvider}
|
||||
/>
|
||||
|
||||
<FallbackChainCard
|
||||
chain={chainData?.configuredChain ?? []}
|
||||
selected={selectedProvider}
|
||||
onSelect={pickProvider}
|
||||
/>
|
||||
|
||||
<RuntimeActionsRow
|
||||
provider={selectedProvider}
|
||||
userProvider={userProvider}
|
||||
canRegenerate={
|
||||
insightsSettings?.codexStatus === "connected" ||
|
||||
insightsSettings?.hasAdminKey ||
|
||||
Boolean(userProvider?.provider)
|
||||
}
|
||||
privacyMode={insightsSettings?.privacyMode ?? "aggregated"}
|
||||
lastInsightAt={insightsSettings?.lastInsightAt ?? null}
|
||||
onRegenerated={() =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.insightsRoot(),
|
||||
})
|
||||
}
|
||||
onPrivacyChanged={() =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.insightsRoot(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.kiInsightsDescription")}
|
||||
</p>
|
||||
|
||||
<ActiveProviderSelect value={selectedProvider} onChange={pickProvider} />
|
||||
|
||||
<ProviderConfigCard
|
||||
provider={selectedProvider}
|
||||
insightsSettings={insightsSettings}
|
||||
userProvider={userProvider}
|
||||
/>
|
||||
|
||||
<FallbackChainCard
|
||||
chain={chainData?.configuredChain ?? []}
|
||||
selected={selectedProvider}
|
||||
onSelect={pickProvider}
|
||||
/>
|
||||
|
||||
<RuntimeActionsRow
|
||||
provider={selectedProvider}
|
||||
userProvider={userProvider}
|
||||
canRegenerate={
|
||||
insightsSettings?.codexStatus === "connected" ||
|
||||
insightsSettings?.hasAdminKey ||
|
||||
Boolean(userProvider?.provider)
|
||||
}
|
||||
privacyMode={insightsSettings?.privacyMode ?? "aggregated"}
|
||||
lastInsightAt={insightsSettings?.lastInsightAt ?? null}
|
||||
onRegenerated={() =>
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() })
|
||||
}
|
||||
onPrivacyChanged={() =>
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ApiSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-api-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.api.title")}
|
||||
</h1>
|
||||
@@ -262,7 +262,7 @@ function ApiTokensCard() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{/* The button mirrors the Input's responsive height (h-11 / sm:h-10)
|
||||
so the row reads as one aligned control group instead of a tall
|
||||
input next to a shorter `sm` button. */}
|
||||
|
||||
@@ -139,8 +139,8 @@ export function CoachNudgeCard({
|
||||
aria-live="polite"
|
||||
className={
|
||||
msgType === "error"
|
||||
? "text-destructive mt-3 text-sm"
|
||||
: "text-muted-foreground mt-3 text-sm"
|
||||
? "text-destructive mt-3 pl-7 text-sm"
|
||||
: "text-muted-foreground mt-3 pl-7 text-sm"
|
||||
}
|
||||
>
|
||||
{msg}
|
||||
|
||||
@@ -72,7 +72,7 @@ export function CycleTrackingCard({
|
||||
title={t("settings.cycleTracking.title")}
|
||||
description={t("settings.cycleTracking.description")}
|
||||
/>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<Label htmlFor="cycle-tracking-enable" className="text-sm">
|
||||
{t("settings.cycleTracking.enable")}
|
||||
|
||||
@@ -312,7 +312,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
zurücksetzen" copy clipping the card edge at narrow widths. */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutDashboard className="text-primary h-5 w-5" />
|
||||
<LayoutDashboard className="text-muted-foreground h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("dashboard.customizeTitle")}
|
||||
</h2>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function DashboardSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-dashboard-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.dashboard.title")}
|
||||
</h1>
|
||||
|
||||
@@ -80,7 +80,7 @@ export function ExportSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-export-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.export.title")}
|
||||
</h1>
|
||||
@@ -161,7 +161,7 @@ function ExportCardShell({
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-primary h-5 w-5 shrink-0" />
|
||||
<Icon className="text-muted-foreground h-5 w-5 shrink-0" />
|
||||
<h2 className="text-base font-semibold">{title}</h2>
|
||||
</div>
|
||||
<span className="border-border text-muted-foreground rounded-full border px-2 py-0.5 text-[11px] font-medium tracking-wide uppercase">
|
||||
|
||||
@@ -200,7 +200,7 @@ export function HealthRecordExportPanel() {
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<FileText
|
||||
className="text-primary h-5 w-5 shrink-0"
|
||||
className="text-muted-foreground h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2
|
||||
|
||||
@@ -95,7 +95,7 @@ function ImportCardShell({
|
||||
className="bg-card border-border flex h-full flex-col rounded-xl border p-6"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-primary h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<Icon className="text-muted-foreground h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<h3 className="text-base font-semibold">{title}</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">{description}</p>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function InjectionSitesCard({
|
||||
title={t("settings.globalExcludedInjectionSitesLabel")}
|
||||
description={t("settings.globalExcludedInjectionSitesHint")}
|
||||
/>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 pl-7">
|
||||
{INJECTION_SITE_KEYS.map((site) => (
|
||||
<label key={site} className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
|
||||
@@ -26,7 +26,7 @@ export function InsightsSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-insights-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.insights.title")}
|
||||
</h1>
|
||||
|
||||
@@ -257,7 +257,7 @@ export function IntegrationsSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-integrations-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.integrations.title")}
|
||||
</h1>
|
||||
@@ -463,7 +463,7 @@ function WithingsCard({
|
||||
className="border-border/60 mt-4"
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
|
||||
{/* v1.4.25 W5d — Withings activity-scope reconnect banner.
|
||||
Only paints when (a) the user has an active Withings
|
||||
@@ -920,7 +920,7 @@ function WhoopCard({
|
||||
className="border-border/60 mt-4"
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
|
||||
{/* Parked-integration resume CTA. Surfaces only when the row
|
||||
state is `parked` (>24h of persistent failures). The button
|
||||
@@ -1370,7 +1370,7 @@ function FitbitCard({
|
||||
className="border-border/60 mt-4"
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
|
||||
{/* Parked-integration resume CTA. Surfaces only when the row
|
||||
state is `parked` (>24h of persistent failures). The button
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { SmilePlus } from "lucide-react";
|
||||
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -181,16 +182,16 @@ export function MoodReminderCard({
|
||||
}
|
||||
/>
|
||||
{enabled && (
|
||||
<div className="mt-4 flex min-h-11 items-center gap-3">
|
||||
<div className="mt-4 flex min-h-11 items-center gap-3 pl-7">
|
||||
<label
|
||||
htmlFor="mood-reminder-hour"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
{t("notifications.moodReminder.hourLabel")}
|
||||
</label>
|
||||
<select
|
||||
<NativeSelect
|
||||
id="mood-reminder-hour"
|
||||
className="border-input bg-background h-11 rounded-md border px-3 text-sm"
|
||||
className="w-auto"
|
||||
value={reminderHour}
|
||||
disabled={!isAuthenticated || saving}
|
||||
onChange={(e) => handleHourChange(Number(e.target.value))}
|
||||
@@ -201,7 +202,7 @@ export function MoodReminderCard({
|
||||
{`${h.toString().padStart(2, "0")}:00`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
)}
|
||||
{msg && (
|
||||
@@ -210,8 +211,8 @@ export function MoodReminderCard({
|
||||
aria-live="polite"
|
||||
className={
|
||||
msgType === "error"
|
||||
? "text-destructive mt-3 text-sm"
|
||||
: "text-muted-foreground mt-3 text-sm"
|
||||
? "text-destructive mt-3 pl-7 text-sm"
|
||||
: "text-muted-foreground mt-3 pl-7 text-sm"
|
||||
}
|
||||
>
|
||||
{msg}
|
||||
|
||||
@@ -136,12 +136,12 @@ export function NotificationStatusCard() {
|
||||
data-testid="notification-status-empty"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="text-primary h-5 w-5" />
|
||||
<Bell className="text-muted-foreground h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.notificationStatus.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<p className="text-muted-foreground mt-1 pl-7 text-sm">
|
||||
{t("settings.notificationStatus.emptyDescription")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -152,12 +152,12 @@ export function NotificationStatusCard() {
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<header className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="text-primary h-5 w-5" />
|
||||
<Bell className="text-muted-foreground h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.notificationStatus.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-muted-foreground pl-7 text-xs">
|
||||
{t("settings.notificationStatus.description")}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -77,7 +77,7 @@ export function NotificationsSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-notifications-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.notifications.title")}
|
||||
</h1>
|
||||
|
||||
@@ -91,7 +91,7 @@ export function NtfyCard({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
description={t("settings.ntfyDescription")}
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="ntfy-toggle">{t("settings.ntfyEnable")}</Label>
|
||||
<Switch
|
||||
|
||||
@@ -72,7 +72,7 @@ export function SectionPlaceholder({ slug }: SectionPlaceholderProps) {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id={`settings-section-${slug}-title`}
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{sectionTitle}
|
||||
</h1>
|
||||
|
||||
@@ -301,7 +301,7 @@ export function SourcesSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-sources-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.sources.title")}
|
||||
</h1>
|
||||
@@ -325,7 +325,7 @@ export function SourcesSection() {
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-6">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="text-primary h-5 w-5" />
|
||||
<Layers className="text-muted-foreground h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.sections.sources.cardTitle")}
|
||||
</h2>
|
||||
@@ -342,7 +342,7 @@ export function SourcesSection() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-muted-foreground pl-7 text-xs">
|
||||
{t("settings.sections.sources.help")}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export function TelegramCard({
|
||||
description={t("settings.telegramDescription")}
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
<form onSubmit={handleSave} className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -128,7 +128,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
control affordance. */}
|
||||
<div className="flex items-center justify-between">
|
||||
<SlidersHorizontal
|
||||
className="text-primary h-5 w-5"
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{data && Object.keys(data.overrides).length > 0 && (
|
||||
|
||||
@@ -41,7 +41,7 @@ export function ThresholdsSection() {
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-thresholds-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.thresholds.title")}
|
||||
</h1>
|
||||
|
||||
@@ -208,7 +208,7 @@ export function WebPushCard() {
|
||||
/>
|
||||
|
||||
{(!loading && !isSupported) || (!loading && isDenied) || msg ? (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{!loading && !isSupported ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.webPushNotSupported")}
|
||||
|
||||
Reference in new issue
Block a user