Merge pull request #1256 from MBombeck/release/v1.43.10

Release v1.43.10
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-10-21 10:04:15 +02:00
commit 421aacf457
13 files changed
+79 -14

No files matched your search

+13
View File
@@ -1,5 +1,18 @@
# Changelog
## [1.43.10] — 2026-10-21
### Changed
- **Range and view switches look the way the chart tabs used to.** Since
1.43.8 every row of mutually exclusive choices shares one control, and it
drew them on a grey track with the chosen option raised. Above a chart that
read heavier than the chart itself. The shared control now draws what the
dashboard's range tabs always had: no track, the chosen option filled in
the highlight colour. The range rows above charts are back to their smaller
type. Keyboard navigation and the touch size the shared control brought
stay as they are.
## [1.43.9] — 2026-10-21
### Fixed
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.43.9
version: 1.43.10
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.43.9",
"version": "1.43.10",
"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",
+1 -1
View File
@@ -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.43.9";
/* @sw-version-fallback */ "v1.43.10";
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/*`
@@ -51,6 +51,7 @@ export function ChartRangeTabs({
const { t } = useTranslations();
return (
<Segmented
size="sm"
mode="toggle"
label={t("charts.rangeLabel")}
optionSlot={() => "chart-range-tab"}
@@ -140,6 +140,7 @@ export function CustomMetricChart({
<div className="space-y-3">
<div className="flex justify-end">
<Segmented
size="sm"
options={RANGE_DAYS.map((r) => ({
value: r.key,
label: t(`customMetrics.chart.range.${r.key}`),
@@ -141,6 +141,7 @@ export function MoodDimensionTrends({
return (
<div className="space-y-3" data-slot="mood-dimension-trends">
<Segmented
size="sm"
options={MOOD_DIMENSION_WINDOWS.map((option) => ({
value: String(option),
label: t("insights.mood.dimensions.window", { days: option }),
@@ -177,6 +177,7 @@ export function PeriodNarrativeCard({
<div className="flex items-start justify-between gap-2">
{/* Calm segmented control, no chart. */}
<Segmented
size="sm"
mode="toggle"
label={t("insights.narrativeTitle")}
slot="period-narrative-period"
@@ -392,6 +392,7 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) {
{/* The page's other range controls read "7T / 30T"; this one
printed an English "7d" beside them, hence `daysNLabel`. */}
<Segmented
size="sm"
mode="toggle"
label={t("charts.rangeLabel")}
slot="sleep-stage-window-toggle"
@@ -254,6 +254,7 @@ export function LabBiomarkerChart({
<div className="space-y-3">
<div className="flex justify-end">
<Segmented
size="sm"
options={RANGE_DAYS.map((r) => ({
value: r.key,
label: t(`labs.chart.range.${r.key}`),
@@ -260,6 +260,7 @@ function TargetCard({
<div className="space-y-3">
<div className="flex justify-end">
<Segmented
size="sm"
mode="toggle"
value={scaleMode}
options={[
@@ -61,6 +61,35 @@ describe("<Segmented>", () => {
expect(out).toContain("min-h-9");
});
it("fills the chosen segment in the primary colour, on no track", () => {
const out = html();
const segments = out.match(/<button[^>]*>/g) ?? [];
expect(segments).toHaveLength(2);
const chosen = segments.filter((b) => b.includes('aria-checked="true"'));
expect(chosen).toHaveLength(1);
expect(chosen[0]).toContain("bg-primary text-primary-foreground");
const other = segments.find((b) => b.includes('aria-checked="false"'));
expect(other).not.toContain("bg-primary");
const group = out.match(/<div[^>]*role="radiogroup"[^>]*>/)?.[0];
expect(group).not.toContain("bg-muted");
});
it("sets a chart header's range row in the small type", () => {
expect(html()).toContain("text-sm");
const small = html({ size: "sm" });
expect(small).toContain("text-xs");
expect(small).not.toContain("text-sm");
expect(small.match(/min-h-11/g)?.length).toBe(2);
});
it("keeps a disabled chosen segment on the readable token pair", () => {
const chosen = html({ disabled: true })
.match(/<button[^>]*>/g)
?.find((b) => b.includes('aria-checked="true"'));
expect(chosen).toContain("disabled:bg-primary-disabled");
expect(chosen).toContain("disabled:opacity-100");
});
it("renders pressed buttons in toggle mode", () => {
const out = html({ mode: "toggle" });
expect(out).toContain('role="group"');
+27 -11
View File
@@ -1,10 +1,17 @@
"use client";
/**
* The segmented control: a short row of mutually exclusive choices on one
* track (zoom level, chronicle grouping, cards or table, recent or all,
* Paperless-ngx or Papra). One look everywhere: a muted track, the chosen
* segment raised on the background colour, never coloured.
* The segmented control: a short row of mutually exclusive choices (zoom
* level, chronicle grouping, cards or table, recent or all,
* Paperless-ngx or Papra). One look everywhere, the one the dashboard chart
* tabs always had: no track, the chosen segment filled in the primary colour,
* the others plain until hovered. A muted track with a raised segment was
* tried and read as heavier than the card around it.
*
* Two sizes: `md` (default) for a choice that is the content of its area (a
* view switch, a form sheet, a detail page's sections), `sm` for a range row
* in a chart header, where the segments sit beside the chart's title and
* must not outweigh it.
*
* Two semantics, one chrome:
*
@@ -19,8 +26,9 @@
* setter that saves a preference is not called twice.
*
* Touch: every segment is at least 44 px on a coarse pointer and 36 px under
* a mouse, the same floor `Button` carries. The four controls this replaced
* sat at 36, 36, 44 and 44.
* a mouse, the same floor `Button` carries; a `sm` range row keeps 44 px
* under a mouse too, as the chart tabs always had. The four controls this
* replaced sat at 36, 36, 44 and 44.
*
* An option with an `icon` and `iconOnly` on the control renders the icon and
* names the segment through `aria-label` and `title`.
@@ -59,6 +67,8 @@ export interface SegmentedProps<T extends string> {
iconOnly?: boolean;
/** Spread the segments over the full width. */
stretch?: boolean;
/** `sm` for a range row in a chart header; see the module comment. */
size?: "sm" | "md";
disabled?: boolean;
className?: string;
/**
@@ -78,6 +88,7 @@ export function Segmented<T extends string>({
mode = "radio",
iconOnly = false,
stretch = false,
size = "md",
disabled = false,
className,
wrapOption,
@@ -100,7 +111,7 @@ export function Segmented<T extends string>({
aria-disabled={disabled || undefined}
data-slot={slot}
className={cn(
"bg-muted inline-flex shrink-0 items-center gap-0.5 rounded-lg p-0.5",
"inline-flex shrink-0 items-center gap-1",
stretch && "flex w-full",
className,
)}
@@ -135,14 +146,19 @@ export function Segmented<T extends string>({
onChange(option.value);
}}
className={cn(
"focus-visible:ring-ring/50 inline-flex items-center justify-center gap-1.5 rounded-md text-sm font-medium transition-colors outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none",
"focus-visible:ring-ring/50 inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none",
size === "sm" ? "text-xs" : "text-sm",
iconOnly
? "size-9 pointer-coarse:size-11"
: "min-h-9 px-3 pointer-coarse:min-h-11",
: size === "sm"
? "min-h-11 px-2 sm:px-3 pointer-coarse:min-w-11"
: "min-h-9 px-3 pointer-coarse:min-h-11",
stretch && "flex-1",
checked
? "bg-background text-foreground shadow-sm"
: "text-foreground/75 hover:text-foreground dark:text-tab-inactive dark:hover:text-foreground",
? // A faded fill reads ~2:1 in light mode; disabled swaps to
// the token pair `Button` uses instead of the opacity.
"bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-primary-disabled disabled:text-primary-disabled-foreground disabled:opacity-100"
: "text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
)}
>
{Icon ? (