mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Merge pull request #1256 from MBombeck/release/v1.43.10
Release v1.43.10
This commit is contained in:
13 files changed
+79
-14
No files matched your search
@@ -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,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
@@ -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
@@ -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"');
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in new issue
Block a user