Merge pull request #402 from MBombeck/release/v1.27.1

Release v1.27.1 — iPad layout fixes
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-04 16:21:54 +02:00
commit 6360ba19ee
7 files changed
+119 -19

No files matched your search

+7
View File
@@ -2,6 +2,13 @@
## [Unreleased]
## [1.27.1] — 2026-07-04 — iPad layout fixes
### Fixed
- On tablet-width screens — an iPad held upright — the app now starts with the compact icon sidebar, so the content keeps the width it needs instead of being squeezed into a narrow column next to the full navigation. Expanding the sidebar still works and the choice is remembered, as before.
- The measurements page no longer pushes the whole page into a horizontal scroll on tablet widths; the history table scrolls within its own frame instead.
## [1.27.0] — 2026-07-03 — Fitbit and Pixel Watch through Google Health
### Added
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.27.0
version: 1.27.1
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+70
View File
@@ -0,0 +1,70 @@
import { expect, test } from "@playwright/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
/**
* iPad-width regression guard (portrait, 768×1024).
*
* Two invariants this viewport class kept losing:
*
* 1. No horizontal page overflow — the content column must shrink
* below its children's intrinsic min width (`min-w-0` on the
* shell column); wide children scroll inside their own
* `overflow-x-auto` containers instead of widening the page.
* 2. The content column keeps its room: with no stored sidebar
* preference, tablet widths start on the icon rail (w-16), so
* `<main>` gets ~704 px of the 768 — not the 512 px left over
* next to the expanded 256 px sidebar.
*
* Desktop project only — the assertions are viewport-driven via
* `setViewportSize`, and the Pixel-5 project covers the phone class.
*/
const ROUTES = ["/", "/measurements", "/settings/integrations"] as const;
test.describe("iPad portrait layout (768x1024)", () => {
test.use({ storageState: STORAGE_STATE_PATH });
test.beforeEach(({}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"viewport-driven spec; desktop project only",
);
});
for (const route of ROUTES) {
test(`no horizontal overflow and full-width content on ${route}`, async ({
page,
}) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto(route, { waitUntil: "domcontentloaded" });
await page.waitForLoadState("networkidle");
const dims = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
bodyScrollWidth: document.body.scrollWidth,
innerWidth: window.innerWidth,
mainWidth:
document
.querySelector("main")
?.getBoundingClientRect()
.width.valueOf() ?? 0,
}));
// 1 px tolerance for sub-pixel rounding, matching the Pixel-5 spec.
expect(
dims.scrollWidth,
`page scrollWidth=${dims.scrollWidth}, innerWidth=${dims.innerWidth}`,
).toBeLessThanOrEqual(dims.innerWidth + 1);
expect(
dims.bodyScrollWidth,
`body scrollWidth=${dims.bodyScrollWidth}`,
).toBeLessThanOrEqual(dims.innerWidth + 1);
// Icon rail (64 px) + content: main must keep ≥ 690 px of the 768.
expect(
Math.round(dims.mainWidth),
`main width=${dims.mainWidth}`,
).toBeGreaterThanOrEqual(690);
});
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.27.0",
"version": "1.27.1",
"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",
+6 -1
View File
@@ -268,7 +268,12 @@ export function AuthShell({
</a>
<div className="flex h-dvh flex-col md:flex-row">
<SidebarNav />
<div className="flex min-h-0 flex-1 flex-col">
{/* `min-w-0` lets the content column shrink below its children's
intrinsic min width (e.g. the measurements table); without it
the column widens past the viewport next to the sidebar and
the whole page gains a horizontal scrollbar. Wide children
scroll inside their own `overflow-x-auto` containers. */}
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
{/*
v1.4.43 QoL (M5) — `<OfflineBanner>` paints only when
`navigator.onLine === false`. Sits above the maintainership
+26 -13
View File
@@ -25,6 +25,7 @@ import {
import { cn } from "@/lib/utils";
import { Logo } from "@/components/ui/logo";
import { useAuth, useLogout } from "@/hooks/use-auth";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { useMounted } from "@/hooks/use-mounted";
import { useTheme } from "@/components/providers";
import { useTranslations } from "@/lib/i18n/context";
@@ -252,25 +253,37 @@ export function SidebarNav() {
() => visibleNavDestinations(user?.modules, mounted),
[user?.modules, mounted],
);
const [collapsed, setCollapsed] = useState(() => {
if (typeof window === "undefined") return false;
// Collapsed = the user's stored choice, or — with no stored choice — a
// viewport default: tablet widths (md–lg, e.g. an iPad held upright)
// fall back to the icon rail so the content column keeps its room; a
// 256 px sidebar squeezes a 768 px viewport down to a 512 px column.
// Everything is gated on `mounted` (same pattern as the module filter
// above): SSR and the hydration render both paint the expanded shell,
// the stored pref / viewport default applies on the first client render
// after hydration settles. Branching earlier — localStorage or
// matchMedia inside the initial render — is a React #418 hydration
// mismatch, because the sidebar is only CSS-hidden below `md` and still
// hydrates its DOM there.
const tabletOrBelow = useIsMobile("lg");
const [collapsedPref, setCollapsedPref] = useState<boolean | null>(() => {
if (typeof window === "undefined") return null;
try {
return localStorage.getItem(STORAGE_KEY) === "true";
const stored = localStorage.getItem(STORAGE_KEY);
return stored === null ? null : stored === "true";
} catch {
return false;
return null;
}
});
const collapsed = mounted ? (collapsedPref ?? tabletOrBelow) : false;
function toggleCollapsed() {
setCollapsed((prev) => {
const next = !prev;
try {
localStorage.setItem(STORAGE_KEY, String(next));
} catch {
// Ignore storage errors
}
return next;
});
const next = !collapsed;
setCollapsedPref(next);
try {
localStorage.setItem(STORAGE_KEY, String(next));
} catch {
// Ignore storage errors
}
}
// v1.17.1 (F-1 residue) — the sidebar footer utility links derive from
+8 -3
View File
@@ -36,8 +36,13 @@ import { useSyncExternalStore } from "react";
* need a tighter cut (e.g. the Coach drawer, which flips to a
* bottom-sheet only below `sm` / 640 px) pass an explicit breakpoint.
*/
function getMediaQuery(breakpoint: "sm" | "md"): string {
const maxWidth = breakpoint === "sm" ? "639.98px" : "767.98px";
function getMediaQuery(breakpoint: "sm" | "md" | "lg"): string {
const maxWidth =
breakpoint === "sm"
? "639.98px"
: breakpoint === "md"
? "767.98px"
: "1023.98px";
return `(max-width: ${maxWidth})`;
}
@@ -59,7 +64,7 @@ function getServerSnapshot(): boolean {
return false;
}
export function useIsMobile(breakpoint: "sm" | "md" = "md"): boolean {
export function useIsMobile(breakpoint: "sm" | "md" | "lg" = "md"): boolean {
const query = getMediaQuery(breakpoint);
return useSyncExternalStore(
subscribe(query),