mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
94b2d6f910
commit
038be6ac57
10 files changed
+112
-16
No files matched your search
@@ -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,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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user