diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac0870ba3..b5d019022 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
## [Unreleased]
+## [1.30.6] — 2026-07-18
+
+Accessibility pass.
+
+- **Heatmaps read to a screen reader.** The mood and medication-compliance grids now expose each recorded day — its date and value — through a hidden day list, so the per-day figures are no longer reachable by pointer only. The chart looks exactly the same.
+- **Small labels meet the minimum size.** Muted meta text across settings, admin, the coach panel, and the insights cards is lifted off the sub-`text-xs` sizes; tabular figures and chart chrome are unchanged.
+- **A couple of section headings.** Two metric-page sections became proper headings, so heading navigation lands where it should.
+
+No migration. No breaking changes.
+
## [1.30.5] — 2026-07-18
Making a shipped feature findable, and a sync-status fix.
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index baf24f46d..ebfc5cc5d 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
- version: 1.30.5
+ version: 1.30.6
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
diff --git a/package.json b/package.json
index 1ee141df0..32c841c77 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "healthlog",
- "version": "1.30.5",
+ "version": "1.30.6",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
diff --git a/public/sw.js b/public/sw.js
index b79a2e67b..9cb1e51b4 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
- /* @sw-version-fallback */ "v1.30.5";
+ /* @sw-version-fallback */ "v1.30.6";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
diff --git a/src/components/admin/__tests__/api-token-overview-responsive.test.tsx b/src/components/admin/__tests__/api-token-overview-responsive.test.tsx
index 16a1eeff5..1991a9957 100644
--- a/src/components/admin/__tests__/api-token-overview-responsive.test.tsx
+++ b/src/components/admin/__tests__/api-token-overview-responsive.test.tsx
@@ -153,14 +153,14 @@ describe("ApiTokenOverviewSection — responsive", () => {
const html = render();
const mobileMatch = html.match(/
]*md:hidden[^>]*>([\s\S]*?)<\/ul>/);
expect(mobileMatch).not.toBeNull();
- // The Last-used + Created lines use — no
- // whitespace-nowrap so a long German date+time can flow onto a
+ // The Last-used + Created lines are muted
meta —
+ // no whitespace-nowrap so a long German date+time can flow onto a
// second line within the card. The shadcn `` primitive
// does carry `whitespace-nowrap`, which is fine for the short
// permission strings we render — those are scoped to a flex-wrap
// container that line-breaks at the badge boundary.
expect(mobileMatch![1]).toMatch(
- /]*text-\[11px\][^>]*>(?![^<]*whitespace-nowrap)/,
+ /
]*text-muted-foreground[^>]*text-xs[^>]*>(?![^<]*whitespace-nowrap)/,
);
});
});
diff --git a/src/components/admin/api-token-overview-section.tsx b/src/components/admin/api-token-overview-section.tsx
index 93044b04d..2e7c3d49a 100644
--- a/src/components/admin/api-token-overview-section.tsx
+++ b/src/components/admin/api-token-overview-section.tsx
@@ -328,13 +328,13 @@ export function ApiTokenOverviewSection() {
))}
-
+
{t("admin.tokenLastUsed")}:{" "}
{token.lastUsedAt
? formatDateTime(token.lastUsedAt)
: t("admin.tokenNeverUsed")}
-
+
{t("admin.tokenCreated")}: {formatDate(token.createdAt)}
diff --git a/src/components/admin/backups-section.tsx b/src/components/admin/backups-section.tsx
index 5b73bbbdf..24bbfbf30 100644
--- a/src/components/admin/backups-section.tsx
+++ b/src/components/admin/backups-section.tsx
@@ -578,11 +578,11 @@ export function BackupsSection() {
{row.username}
-
+
{formatBackupType(row.type, t)}
-
+
{t("admin.section.backups.colSize")}:{" "}
{formatBytes(row.sizeBytes, fmt)} ·{" "}
{fmt.dateTime(row.createdAt)}
diff --git a/src/components/admin/user-management-section.tsx b/src/components/admin/user-management-section.tsx
index b73d63835..91a38518f 100644
--- a/src/components/admin/user-management-section.tsx
+++ b/src/components/admin/user-management-section.tsx
@@ -411,7 +411,7 @@ export function UserManagementSection() {
variant={
u.role === "ADMIN" ? "default" : "secondary"
}
- className="text-[10px]"
+ className="text-xs"
>
{u.role}
@@ -419,7 +419,7 @@ export function UserManagementSection() {
{u.email || "—"}
-
+
{t("admin.userPasskeys")}: {u.passkeyCount} ·{" "}
{formatDate(u.createdAt)}
diff --git a/src/components/charts/__tests__/compliance-heatmap-a11y.test.tsx b/src/components/charts/__tests__/compliance-heatmap-a11y.test.tsx
index 5e5de3c15..28ea8a862 100644
--- a/src/components/charts/__tests__/compliance-heatmap-a11y.test.tsx
+++ b/src/components/charts/__tests__/compliance-heatmap-a11y.test.tsx
@@ -5,17 +5,29 @@ import { I18nProvider } from "@/lib/i18n/context";
import { ComplianceHeatmap } from "../compliance-heatmap";
/**
- * 2026-07-17 a11y audit (M2) — medication-compliance heatmap day cells
- * were pointer-only SVG ``s: no `tabIndex`, no per-day `aria-label`.
- * This pins the keyboard-reachability fix on top of the pre-existing
- * aggregate `role="img"` summary on the SVG.
+ * 2026-07-17 a11y audit (M2) — medication-compliance heatmap day cells are
+ * pointer-only SVG ``s. The keyboard/screen-reader path to the
+ * per-day values is a visually-hidden day list beside the grid, not
+ * focusable rects (a cell subtree nested under the SVG's `role="img"` is
+ * pruned from the a11y tree, and per-cell tab stops would flood the
+ * keyboard order). This pins that list plus the aggregate `role="img"`
+ * summary, and that the rects stay a pure pointer affordance.
*/
+// The grid window is the last `days` days ending today, so seed relative
+// dates (UTC-sliced, matching the component) rather than fixed ones that
+// would slide out of the window over time.
+const dayKey = (offset: number) =>
+ new Date(Date.now() - offset * 86_400_000).toISOString().slice(0, 10);
+const D0 = dayKey(0);
+const D1 = dayKey(1);
+
function render() {
return renderToStaticMarkup(
@@ -23,20 +35,33 @@ function render() {
);
}
-describe(" — keyboard-reachable day cells", () => {
+describe(" — keyboard-reachable day values", () => {
it("keeps the aggregate role=img summary on the SVG", () => {
const html = render();
expect(html).toMatch(/]*role="img"[^>]*aria-label="[^"]+"/);
});
- it("makes every day cell focusable with its own aria-label", () => {
+ it("keeps the day cells a pure pointer affordance (no tab stops)", () => {
const html = render();
const rectMatches = html.match(/]*>/g) ?? [];
expect(rectMatches.length).toBeGreaterThan(0);
for (const rect of rectMatches) {
- expect(rect).toContain('tabindex="0"');
- expect(rect).toContain('role="img"');
- expect(rect).toMatch(/aria-label="[^"]+"/);
+ expect(rect).not.toContain("tabindex");
}
});
+
+ it("exposes each active day through a visually-hidden day list", () => {
+ const html = render();
+ // The sr-only list carries one entry per day that had a scheduled or
+ // taken dose — here the two seeded days.
+ const listMatch = html.match(
+ /]*data-slot="compliance-heatmap-day-list"[^>]*>([\s\S]*?)<\/ul>/,
+ );
+ expect(listMatch).not.toBeNull();
+ const items = listMatch![1].match(/]*>/g) ?? [];
+ expect(items.length).toBe(2);
+ // Each entry pairs the day with its taken/expected + rate string.
+ expect(listMatch![1]).toContain("2/2 (100%)");
+ expect(listMatch![1]).toContain("1/2 (50%)");
+ });
});
diff --git a/src/components/charts/__tests__/mood-heatmap-a11y.test.tsx b/src/components/charts/__tests__/mood-heatmap-a11y.test.tsx
index 61db1fc72..938254e8c 100644
--- a/src/components/charts/__tests__/mood-heatmap-a11y.test.tsx
+++ b/src/components/charts/__tests__/mood-heatmap-a11y.test.tsx
@@ -5,17 +5,29 @@ import { I18nProvider } from "@/lib/i18n/context";
import { MoodHeatmap } from "../mood-heatmap";
/**
- * 2026-07-17 a11y audit (M2) — mood heatmap day cells were pointer-only
- * SVG ``s: no `tabIndex`, no per-day `aria-label`. This pins the
- * keyboard-reachability fix (each cell focusable + self-describing) on
- * top of the pre-existing aggregate `role="img"` summary on the SVG.
+ * 2026-07-17 a11y audit (M2) — mood heatmap day cells are pointer-only SVG
+ * ``s. The keyboard/screen-reader path to the per-day values is a
+ * visually-hidden day list beside the grid, not focusable rects (a cell
+ * subtree nested under the SVG's `role="img"` is pruned from the a11y
+ * tree, and per-cell tab stops would flood the keyboard order). This pins
+ * that list plus the aggregate `role="img"` summary, and that the rects
+ * stay a pure pointer affordance.
*/
+// The grid window is the last `days` days ending today, so seed relative
+// dates (UTC-sliced, matching the component) rather than fixed ones that
+// would slide out of the window over time.
+const dayKey = (offset: number) =>
+ new Date(Date.now() - offset * 86_400_000).toISOString().slice(0, 10);
+const D0 = dayKey(0);
+const D1 = dayKey(1);
+
function render() {
return renderToStaticMarkup(
@@ -23,20 +35,31 @@ function render() {
);
}
-describe(" — keyboard-reachable day cells", () => {
+describe(" — keyboard-reachable day values", () => {
it("keeps the aggregate role=img summary on the SVG", () => {
const html = render();
expect(html).toMatch(/]*role="img"[^>]*aria-label="[^"]+"/);
});
- it("makes every day cell focusable with its own aria-label", () => {
+ it("keeps the day cells a pure pointer affordance (no tab stops)", () => {
const html = render();
const rectMatches = html.match(/]*>/g) ?? [];
expect(rectMatches.length).toBeGreaterThan(0);
for (const rect of rectMatches) {
- expect(rect).toContain('tabindex="0"');
- expect(rect).toContain('role="img"');
- expect(rect).toMatch(/aria-label="[^"]+"/);
+ expect(rect).not.toContain("tabindex");
}
});
+
+ it("exposes each logged day through a visually-hidden day list", () => {
+ const html = render();
+ const listMatch = html.match(
+ /]*data-slot="mood-heatmap-day-list"[^>]*>([\s\S]*?)<\/ul>/,
+ );
+ expect(listMatch).not.toBeNull();
+ const items = listMatch![1].match(/]*>/g) ?? [];
+ // One entry per logged day — the two seeded days, not every cell.
+ expect(items.length).toBe(2);
+ expect(listMatch![1]).toContain("4.2");
+ expect(listMatch![1]).toContain("2.0");
+ });
});
diff --git a/src/components/charts/chart-overlay-controls.tsx b/src/components/charts/chart-overlay-controls.tsx
index e759a8009..93e8c19d4 100644
--- a/src/components/charts/chart-overlay-controls.tsx
+++ b/src/components/charts/chart-overlay-controls.tsx
@@ -248,7 +248,7 @@ export function ChartOverlayControlsBody({
prefs.comparisonBaseline === value ? "default" : "outline"
}
size="sm"
- className={`min-h-11 px-2 text-[11px] sm:min-h-9 ${isGreyed ? "opacity-50" : ""}`.trim()}
+ className={`min-h-11 px-2 text-xs sm:min-h-9 ${isGreyed ? "opacity-50" : ""}`.trim()}
onClick={() => setComparisonBaseline(value)}
aria-pressed={prefs.comparisonBaseline === value}
aria-disabled={isGreyed || undefined}
@@ -263,7 +263,7 @@ export function ChartOverlayControlsBody({
{comparisonGreyOut && (
{disabledTooltip}
diff --git a/src/components/charts/compliance-heatmap.tsx b/src/components/charts/compliance-heatmap.tsx
index b4a506449..7eec7a959 100644
--- a/src/components/charts/compliance-heatmap.tsx
+++ b/src/components/charts/compliance-heatmap.tsx
@@ -269,6 +269,32 @@ export function ComplianceHeatmap({
const svgWidth = labelWidth + weeks * cellSize + Math.max(0, weeks - 1) * GAP;
const svgHeight = headerHeight + 7 * cellSize + 6 * GAP;
+ // Per-day text alternative, shared by the pointer tooltip and the
+ // visually-hidden day list below the grid (M2). One source of truth so
+ // the sr-only list never drifts from what a pointer user reads.
+ const describeCell = (cell: (typeof cells)[number]): string => {
+ const rate =
+ cell.data.expected > 0
+ ? Math.min(
+ 100,
+ Math.round((cell.data.taken / cell.data.expected) * 100),
+ )
+ : 0;
+ const hasTimingData =
+ cell.data.onTime !== undefined ||
+ cell.data.late !== undefined ||
+ cell.data.veryLate !== undefined;
+ const timingInfo = hasTimingData
+ ? ` | ${cell.data.onTime ?? 0} ${t("charts.heatmapOnTime")}, ${cell.data.late ?? 0} ${t("charts.heatmapLate")}, ${cell.data.veryLate ?? 0} ${t("charts.heatmapVeryLate")}`
+ : "";
+ return `${formatDay(cell.dateKey)}: ${cell.data.taken}/${cell.data.expected} (${rate}%)${timingInfo}`;
+ };
+ // Only days that carried a scheduled or taken dose go into the sr-only
+ // list; empty days are covered by the aggregate summary on the SVG.
+ const activeCells = cells.filter(
+ (cell) => cell.data.expected > 0 || cell.data.taken > 0,
+ );
+
return (
{/* v1.4.27 MB7 / CF-10 — `overflow-x-auto` on ` {
- const buildText = (): string => {
- const rate =
- cell.data.expected > 0
- ? Math.min(
- 100,
- Math.round((cell.data.taken / cell.data.expected) * 100),
- )
- : 0;
- const hasTimingData =
- cell.data.onTime !== undefined ||
- cell.data.late !== undefined ||
- cell.data.veryLate !== undefined;
- const timingInfo = hasTimingData
- ? ` | ${cell.data.onTime ?? 0} ${t("charts.heatmapOnTime")}, ${cell.data.late ?? 0} ${t("charts.heatmapLate")}, ${cell.data.veryLate ?? 0} ${t("charts.heatmapVeryLate")}`
- : "";
- return `${formatDay(cell.dateKey)}: ${cell.data.taken}/${cell.data.expected} (${rate}%)${timingInfo}`;
- };
+ const buildText = (): string => describeCell(cell);
return (
{
// Touch enter fires synthetically immediately before
// `pointerdown`; skip it so the pinned tooltip below
@@ -394,23 +403,24 @@ export function ComplianceHeatmap({
pinned: true,
});
}}
- onFocus={(e) => {
- const box = e.currentTarget.getBoundingClientRect();
- setTooltip({
- x: box.left + box.width / 2,
- y: box.top,
- text: buildText(),
- });
- }}
- onBlur={() => {
- setTooltip((prev) => (prev?.pinned ? prev : null));
- }}
/>
);
})}
+ {/* 2026-07-17 a11y audit (M2) — visually-hidden per-day list. The SVG
+ carries an aggregate `role="img"` summary; this list is the
+ keyboard/screen-reader path to the same granular values the pointer
+ tooltip shows, without adding a tab stop per cell. */}
+ {activeCells.length > 0 && (
+
+ {activeCells.map((cell) => (
+ {describeCell(cell)}
+ ))}
+
+ )}
+
{/* Tooltip */}
{tooltip && (
{
+ if (!cell.cell) {
+ return `${formatDay(cell.dateKey)}: ${t("insights.mood.heatmapNoEntry")}`;
+ }
+ const labelKey = moodLabelKeyForScore(Math.round(cell.cell.score));
+ const moodLabel = labelKey ? t(labelKey) : "";
+ return `${formatDay(cell.dateKey)}: ${cell.cell.score.toFixed(1)}${moodLabel ? ` · ${moodLabel}` : ""}`;
+ };
+ // Only the logged days go into the sr-only list; the empty-tinted cells
+ // are already covered by the aggregate summary on the SVG, so a rotor
+ // user hears the days that carry a value rather than a wall of "no entry".
+ const loggedCells = cells.filter((cell) => cell.cell !== null);
+
return (
{
- const buildText = (): string => {
- if (!cell.cell) {
- return `${formatDay(cell.dateKey)}: ${t("insights.mood.heatmapNoEntry")}`;
- }
- const labelKey = moodLabelKeyForScore(
- Math.round(cell.cell.score),
- );
- const moodLabel = labelKey ? t(labelKey) : "";
- return `${formatDay(cell.dateKey)}: ${cell.cell.score.toFixed(1)}${moodLabel ? ` · ${moodLabel}` : ""}`;
- };
+ const buildText = (): string => describeCell(cell);
return (
{
if (e.pointerType === "touch") return;
setTooltip({
@@ -330,23 +336,24 @@ export function MoodHeatmap({
pinned: true,
});
}}
- onFocus={(e) => {
- const box = e.currentTarget.getBoundingClientRect();
- setTooltip({
- x: box.left + box.width / 2,
- y: box.top,
- text: buildText(),
- });
- }}
- onBlur={() => {
- setTooltip((prev) => (prev?.pinned ? prev : null));
- }}
/>
);
})}
+ {/* 2026-07-17 a11y audit (M2) — visually-hidden per-day list. The SVG
+ carries an aggregate `role="img"` summary; this list is the
+ keyboard/screen-reader path to the same granular values the pointer
+ tooltip shows, without adding a tab stop per cell. */}
+ {loggedCells.length > 0 && (
+
+ {loggedCells.map((cell) => (
+ {describeCell(cell)}
+ ))}
+
+ )}
+
{tooltip && (
-
+
{label}
diff --git a/src/components/cycle/cycle-phase-crosstab.tsx b/src/components/cycle/cycle-phase-crosstab.tsx
index f2badb1aa..8e0f4d693 100644
--- a/src/components/cycle/cycle-phase-crosstab.tsx
+++ b/src/components/cycle/cycle-phase-crosstab.tsx
@@ -187,7 +187,7 @@ export function CyclePhaseCrosstab({
diff --git a/src/components/dashboard/__tests__/dashboard-header.test.tsx b/src/components/dashboard/__tests__/dashboard-header.test.tsx
index 7746aaf6a..9b45f6014 100644
--- a/src/components/dashboard/__tests__/dashboard-header.test.tsx
+++ b/src/components/dashboard/__tests__/dashboard-header.test.tsx
@@ -43,4 +43,15 @@ describe(" — greeting", () => {
// The title itself stays.
expect(html).toContain("Dashboard");
});
+
+ // 2026-07-17 a11y audit (M4) — the dashboard is the app's landing surface
+ // and must expose a page-level ``. It comes from the shared
+ // `PageHeader` the header renders, so a screen-reader user navigating by
+ // heading lands on a real page anchor (not the Today hero's muted ``
+ // micro-label). No separate sr-only heading is added — that would double
+ // the `h1`.
+ it("renders a real page-level h1 with the dashboard title", () => {
+ const html = renderSSR( undefined} />);
+ expect(html).toMatch(/]*>Dashboard<\/h1>/);
+ });
});
diff --git a/src/components/insights/coach-panel/coach-conversation.tsx b/src/components/insights/coach-panel/coach-conversation.tsx
index 629046bc0..e186a7b13 100644
--- a/src/components/insights/coach-panel/coach-conversation.tsx
+++ b/src/components/insights/coach-panel/coach-conversation.tsx
@@ -1040,11 +1040,11 @@ export function CoachConversation({
{docScopeText}
-
+
{t("insights.coach.attach.fencedHint")}
{anyPendingUnindexed ? (
-
+
{t("insights.coach.attach.indexingHint")}
) : null}
@@ -1143,9 +1143,7 @@ export function CoachConversation({
{renderDescription ? (
renderDescription(tagline)
) : (
-
- {tagline}
-
+
{tagline}
)}
)}
renderDescription={(tagline) => (
-
+
{tagline}
)}
diff --git a/src/components/insights/coach-panel/guided-dialog-bubbles.tsx b/src/components/insights/coach-panel/guided-dialog-bubbles.tsx
index 12f998855..fd6913839 100644
--- a/src/components/insights/coach-panel/guided-dialog-bubbles.tsx
+++ b/src/components/insights/coach-panel/guided-dialog-bubbles.tsx
@@ -133,7 +133,7 @@ export function GuidedQuestionBubble({
<>
{t("insights.coach.guided.progress", {
current: progress.current,
diff --git a/src/components/insights/coach-panel/source-chips.tsx b/src/components/insights/coach-panel/source-chips.tsx
index 3b6352a7c..af884ffd0 100644
--- a/src/components/insights/coach-panel/source-chips.tsx
+++ b/src/components/insights/coach-panel/source-chips.tsx
@@ -88,7 +88,7 @@ export function SourceChips({ provenance, className }: SourceChipsProps) {
className={cn(
"border-info/25 text-info/90",
"inline-flex items-center gap-1 rounded-full border bg-transparent",
- "px-2 py-0.5 text-[11px] leading-none",
+ "px-2 py-0.5 text-xs leading-none",
)}
>
diff --git a/src/components/insights/confidence-meter.tsx b/src/components/insights/confidence-meter.tsx
index c254a7fe0..7f33106ea 100644
--- a/src/components/insights/confidence-meter.tsx
+++ b/src/components/insights/confidence-meter.tsx
@@ -76,7 +76,7 @@ function DraftPill({ ariaLabel }: { ariaLabel: string }) {
data-confidence-band="draft"
role="img"
aria-label={ariaLabel}
- className="bg-destructive/10 text-destructive border-destructive/25 inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase"
+ className="bg-destructive/10 text-destructive border-destructive/25 inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium tracking-wide uppercase"
>
{t("insights.recommendation.confidenceDraft")}
diff --git a/src/components/insights/derived/coach-read-strip.tsx b/src/components/insights/derived/coach-read-strip.tsx
index 8971fd4ea..94a847fee 100644
--- a/src/components/insights/derived/coach-read-strip.tsx
+++ b/src/components/insights/derived/coach-read-strip.tsx
@@ -123,6 +123,9 @@ export function CoachReadStrip({
diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index 41d4eec69..aec8c9079 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -316,6 +316,9 @@ function TargetReferencePanel({
diff --git a/src/components/insights/mood/mood-factor-metric-crosstab.tsx b/src/components/insights/mood/mood-factor-metric-crosstab.tsx
index af67d7f05..c4c1460e7 100644
--- a/src/components/insights/mood/mood-factor-metric-crosstab.tsx
+++ b/src/components/insights/mood/mood-factor-metric-crosstab.tsx
@@ -138,7 +138,7 @@ export function MoodFactorMetricCrosstab({
diff --git a/src/components/insights/mood/mood-tag-influence.tsx b/src/components/insights/mood/mood-tag-influence.tsx
index 1830c7e6a..619c2d699 100644
--- a/src/components/insights/mood/mood-tag-influence.tsx
+++ b/src/components/insights/mood/mood-tag-influence.tsx
@@ -101,7 +101,7 @@ export function MoodTagInfluence({ rows }: { rows: MoodTagInfluenceRow[] }) {
diff --git a/src/components/insights/mood/mood-tag-metric-crosstab.tsx b/src/components/insights/mood/mood-tag-metric-crosstab.tsx
index 035445e31..147b312a9 100644
--- a/src/components/insights/mood/mood-tag-metric-crosstab.tsx
+++ b/src/components/insights/mood/mood-tag-metric-crosstab.tsx
@@ -137,7 +137,7 @@ export function MoodTagMetricCrosstab({
diff --git a/src/components/insights/trend-annotation.tsx b/src/components/insights/trend-annotation.tsx
index b2495195b..5ce48d013 100644
--- a/src/components/insights/trend-annotation.tsx
+++ b/src/components/insights/trend-annotation.tsx
@@ -194,7 +194,7 @@ export function TrendAnnotation({
{t(CONFIDENCE_LABEL_KEY[confidence])}
diff --git a/src/components/medications/scheduling/schedule-history-timeline.tsx b/src/components/medications/scheduling/schedule-history-timeline.tsx
index f500ccf7c..4fbe35ac0 100644
--- a/src/components/medications/scheduling/schedule-history-timeline.tsx
+++ b/src/components/medications/scheduling/schedule-history-timeline.tsx
@@ -201,7 +201,7 @@ export function ScheduleHistoryTimeline({
: t("medications.detail.zeitplan.history.noTimes")}
{revision.source === "MANUAL" && (
{t("medications.detail.zeitplan.history.manualChip")}
diff --git a/src/components/medications/side-effects-section.tsx b/src/components/medications/side-effects-section.tsx
index ade1b2c43..91c111521 100644
--- a/src/components/medications/side-effects-section.tsx
+++ b/src/components/medications/side-effects-section.tsx
@@ -492,7 +492,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
{value}
-
+
{t(`medications.sideEffects.severity.${label}`)}
@@ -517,7 +517,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
// px-3 py-2 is too tall inside the side-effect modal.
className="px-2 py-1.5"
/>
-
+
{notes.length} / {NOTES_MAX}
diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx
index b8622fcc0..76f1f4686 100644
--- a/src/components/mood/mood-list.tsx
+++ b/src/components/mood/mood-list.tsx
@@ -1107,7 +1107,7 @@ function MoodNoteText({
aria-expanded={expanded}
aria-controls={noteId}
data-testid="mood-note-toggle"
- className="text-primary cursor-pointer text-[11px] font-medium"
+ className="text-primary cursor-pointer text-xs font-medium"
>
{expanded ? t("mood.noteCollapse") : t("mood.noteExpand")}
diff --git a/src/components/onboarding/source-card-grid.tsx b/src/components/onboarding/source-card-grid.tsx
index a05e60e47..234f942a4 100644
--- a/src/components/onboarding/source-card-grid.tsx
+++ b/src/components/onboarding/source-card-grid.tsx
@@ -323,7 +323,7 @@ function SourceCardItem({
{titleLabel}
{recommendedLabel ? (
-
+
{recommendedLabel}
) : null}
diff --git a/src/components/settings/api-section.tsx b/src/components/settings/api-section.tsx
index bc6ef36ff..7e930bd03 100644
--- a/src/components/settings/api-section.tsx
+++ b/src/components/settings/api-section.tsx
@@ -119,7 +119,7 @@ function ApiEndpointsCard() {
className="bg-muted/30 border-border space-y-1.5 rounded-lg border p-3 text-xs"
>
-
+
{endpoint.method}
{endpoint.path}
@@ -457,11 +457,11 @@ function ApiTokensCard() {
{tok.name}
{isExpired ? (
-
+
{t("settings.tokenExpired")}
) : (
-
+
{t("settings.tokenActive")}
)}
diff --git a/src/components/settings/mcp-section.tsx b/src/components/settings/mcp-section.tsx
index 534dc2621..d4edba7b5 100644
--- a/src/components/settings/mcp-section.tsx
+++ b/src/components/settings/mcp-section.tsx
@@ -124,7 +124,7 @@ function McpConnectionsCard() {
{conn.clientName}
-
+
{t("settings.mcp.connectionConnected")}
@@ -438,11 +438,11 @@ function McpTokensCard() {
{tok.name}
{isExpired ? (
-
+
{t("settings.tokenExpired")}
) : (
-
+
{t("settings.tokenActive")}
)}
diff --git a/src/components/settings/sharing-section.tsx b/src/components/settings/sharing-section.tsx
index 549d217d1..e029b9f5f 100644
--- a/src/components/settings/sharing-section.tsx
+++ b/src/components/settings/sharing-section.tsx
@@ -125,13 +125,13 @@ function ShareLinksCard() {
{link.label}
-
+
{t("settings.sharing.statusActive")}
{link.protected && (
@@ -141,7 +141,7 @@ function ShareLinksCard() {
{link.documentCount > 0 && (
)}
{link.allowFhirApi && (
-
+
FHIR
)}
@@ -240,7 +240,7 @@ function ShareLinksCard() {
{link.label}
-
+
{link.revokedAt
? t("settings.sharing.statusRevoked")
: t("settings.sharing.statusExpired")}