mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Merge pull request #402 from MBombeck/release/v1.27.1
Release v1.27.1 — iPad layout fixes
This commit is contained in:
7 files changed
+119
-19
No files matched your search
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user