chore(release): v1.12.11 — unify settings notice colours and callout style

Route every Settings callout, badge and status note through the semantic
colour tokens (success / warning / info / primary) instead of raw palette
classes, so the same meaning reads identically across cards and stays
legible in light mode. Add a single SettingsInfoTile primitive — icon +
tone + text on one fixed surface — and adopt it for the threshold-override
caution note, replacing a bespoke box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-05 18:42:18 +02:00
1 parent 94b2d6f910
commit 038be6ac57
10 files changed
+112 -16

No files matched your search

+6
View File
@@ -2,6 +2,12 @@
## [Unreleased]
## [1.12.11] — 2026-06-05 — settings notices read the same everywhere
### Changed
- **Settings notices and status colours are unified.** Every callout, badge and status note in Settings now draws from the same set of meaning-based colours — success, caution, info — so a green "connected" or an amber "needs attention" looks identical from one card to the next and stays legible in both light and dark themes. Caution notes share one consistent boxed style with a leading icon.
## [1.12.10] — 2026-06-05 — a "taken" tap no longer back-fills the wrong dose
### Fixed
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.12.10
version: 1.12.11
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.12.10",
"version": "1.12.11",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "AGPL-3.0-only",
"homepage": "https://healthlog.dev",
+90
View File
@@ -0,0 +1,90 @@
"use client";
/**
* `<SettingsInfoTile>` — the one callout shape for Settings.
*
* Before this primitive, every section hand-rolled its own notice box:
* `border-l-2`/`border-l-4`, `rounded-md`/`rounded-lg`, mismatched
* border/background tone intensities, and raw palette colours used as
* text (which fail AA in light mode). The tile fixes the surface to a
* single contract — icon left, short text right, a tone-coloured left
* border — and routes every tone through the semantic tokens so the
* same notice reads identically in light and dark.
*
* Tones map to the project's semantic colour tokens:
* - `info` → `--info` (neutral guidance)
* - `warning` → `--warning` (caution / override / reconnect)
* - `success` → `--success` (confirmation)
* - `primary` → `--primary` (brand-accented instruction)
* - `neutral` → muted surface (low-emphasis aside)
*/
import * as React from "react";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export type SettingsInfoTileTone =
| "info"
| "warning"
| "success"
| "primary"
| "neutral";
const TONE_SURFACE: Record<SettingsInfoTileTone, string> = {
info: "border-info/40 bg-info/10",
warning: "border-warning/40 bg-warning/10",
success: "border-success/40 bg-success/10",
primary: "border-primary/40 bg-primary/10",
neutral: "border-border bg-muted/30",
};
const TONE_ICON: Record<SettingsInfoTileTone, string> = {
info: "text-info",
warning: "text-warning",
success: "text-success",
primary: "text-primary",
neutral: "text-muted-foreground",
};
export interface SettingsInfoTileProps {
/** Lucide icon component; rendered in the tone colour. */
icon: LucideIcon;
/** Visual tone — routes through the semantic colour tokens. */
tone?: SettingsInfoTileTone;
/** Optional emphasised first line. */
title?: React.ReactNode;
/** Body content — muted text. */
children?: React.ReactNode;
/** Optional extra classes on the outer wrapper. */
className?: string;
}
export function SettingsInfoTile({
icon: Icon,
tone = "info",
title,
children,
className,
}: SettingsInfoTileProps) {
return (
<div
className={cn(
"flex items-start gap-2 rounded-md border-l-4 p-3 text-sm",
TONE_SURFACE[tone],
className,
)}
>
<Icon
className={cn("mt-0.5 h-4 w-4 shrink-0", TONE_ICON[tone])}
aria-hidden="true"
/>
<div className="min-w-0 space-y-1">
{title ? <p className="text-foreground font-medium">{title}</p> : null}
{children ? (
<div className="text-muted-foreground">{children}</div>
) : null}
</div>
</div>
);
}
+3 -3
View File
@@ -156,7 +156,7 @@ function ResearchModeCard() {
data-slot="settings-research-mode-card"
>
<div className="flex items-center gap-2">
<BookOpenCheck className="text-dracula-purple h-5 w-5" />
<BookOpenCheck className="text-primary h-5 w-5" />
<h2 className="text-lg font-semibold">
{t("settings.researchMode.sectionTitle")}
</h2>
@@ -349,7 +349,7 @@ function DataResetCard() {
{msg && (
<p
role="alert"
className={`mt-3 text-sm ${msgType === "success" ? "text-dracula-green" : "text-destructive"}`}
className={`mt-3 text-sm ${msgType === "success" ? "text-success" : "text-destructive"}`}
>
{msg}
</p>
@@ -493,7 +493,7 @@ function AccountDeleteCard() {
{msg && (
<p
role="alert"
className={`mt-3 text-sm ${msgType === "success" ? "text-dracula-green" : "text-destructive"}`}
className={`mt-3 text-sm ${msgType === "success" ? "text-success" : "text-destructive"}`}
>
{msg}
</p>
+3 -3
View File
@@ -520,7 +520,7 @@ function ProviderStatusBadges({
{settings?.codexStatus !== "connected" &&
settings?.hasAdminKey &&
activeProvider !== "codex" && (
<Badge className="border-dracula-purple/30 bg-dracula-purple/15 text-dracula-purple">
<Badge className="border-primary/30 bg-primary/15 text-primary">
{t("settings.ai.adminAiActiveBadge")}
</Badge>
)}
@@ -837,7 +837,7 @@ function CodexProviderForm({
{t("settings.ai.codex.disconnectButton")}
</Button>
) : deviceCode ? (
<div className="border-dracula-purple bg-dracula-purple/5 space-y-3 rounded-lg border-l-4 p-4">
<div className="border-primary bg-primary/5 space-y-3 rounded-lg border-l-4 p-4">
<p className="text-sm font-medium">
{t("settings.ai.deviceCodeHeading")}
</p>
@@ -848,7 +848,7 @@ function CodexProviderForm({
href={deviceCode.verificationUrl}
target="_blank"
rel="noopener noreferrer"
className="text-dracula-purple font-medium underline"
className="text-primary font-medium underline"
>
{deviceCode.verificationUrl}
</a>
@@ -201,7 +201,7 @@ export function HealthRecordExportPanel() {
>
<div className="mb-2 flex items-center gap-2">
<FileText
className="text-dracula-purple h-5 w-5 shrink-0"
className="text-primary h-5 w-5 shrink-0"
aria-hidden="true"
/>
<h2
@@ -324,7 +324,7 @@ function stateBadgeFor(
label: t("settings.notificationStatus.stateActive"),
variant: "default",
className:
"border-dracula-green/30 bg-dracula-green/15 text-dracula-green",
"border-success/30 bg-success/15 text-success",
icon: <CheckCircle2 className="h-3 w-3" aria-hidden />,
};
case "auto_disabled":
@@ -338,7 +338,7 @@ function stateBadgeFor(
return {
label: t("settings.notificationStatus.stateSendingPaused"),
variant: "outline",
className: "border-dracula-orange/40 text-dracula-orange",
className: "border-warning/40 text-warning",
icon: <Clock className="h-3 w-3" aria-hidden />,
};
case "manually_disabled":
@@ -100,7 +100,7 @@ export function TestConnectionButton({
{result?.kind === "ok" && (
<p
role="status"
className="text-dracula-green flex items-center gap-1.5 text-xs"
className="text-success flex items-center gap-1.5 text-xs"
>
<CheckCircle2 className="h-3.5 w-3.5" />
{t("settings.testConnection.ok", { latency: result.latency })}
@@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { SettingsInfoTile } from "./_info-tile";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import {
@@ -350,10 +351,9 @@ function MetricRow({
defaultRange &&
(override!.min < defaultRange.greenMin * 0.7 ||
override!.max > defaultRange.greenMax * 1.3) && (
<div className="border-dracula-orange/50 bg-dracula-orange/5 flex items-start gap-2 rounded-md border-l-2 p-2 text-xs">
<AlertTriangle className="text-dracula-orange mt-0.5 h-3 w-3 shrink-0" />
<span>{t("thresholds.overrideWarning")}</span>
</div>
<SettingsInfoTile tone="warning" icon={AlertTriangle}>
{t("thresholds.overrideWarning")}
</SettingsInfoTile>
)}
</>
)}