mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Merge pull request #364 from MBombeck/release/v1.21.1
release/v1.21.1 — dialog footer reachability
This commit is contained in:
6 files changed
+94
-17
No files matched your search
@@ -2,6 +2,14 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.21.1] — 2026-06-26 — Dialog footer reachability
|
||||
|
||||
A patch release. No migrations, no breaking changes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pop-up forms now scroll only their body on desktop, keeping the action row in view. A tall form — or one made tall by browser zoom, display scaling, or a longer-language label — could push the primary button below the fold and force a scroll to reach it; the centred dialog now matches the bottom sheet, which already pinned its footer. Every form the app mounts through the shared sheet surface inherits the fix.
|
||||
|
||||
## [1.21.0] — 2026-06-26 — Date-format preference, a far more connected Coach, and broad correctness work
|
||||
|
||||
A feature release. The Coach reaches every data domain on demand, surfaces the cross-metric patterns the analytics tier already discovers, opens in context from any screen, and speaks with a warmer, forward-looking voice on one shared set of safety thresholds. Dates render in your chosen format everywhere. Two additive migrations (`0193` rollup x-rescale, `0192` date format); no breaking changes.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.21.0
|
||||
version: 1.21.1
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
@@ -87,4 +87,48 @@ test.describe("v1.4.27 — ResponsiveSheet branch", () => {
|
||||
await expect(content).toBeVisible({ timeout: 10_000 });
|
||||
await expect(content).toHaveAttribute("data-variant", "dialog");
|
||||
});
|
||||
|
||||
// v1.21.1 — the desktop Dialog branch must scroll only its body and
|
||||
// keep the footer (the primary action) inside the viewport when the
|
||||
// form is taller than the available height. A short viewport stands in
|
||||
// for the real-world triggers — browser zoom, OS display scaling, long
|
||||
// locale strings — that push a form past `max-h-[calc(100dvh-2rem)]`.
|
||||
test("Desktop Chrome: footer stays in viewport when the form overflows", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name !== "chromium-desktop",
|
||||
"desktop-only branch",
|
||||
);
|
||||
|
||||
// Deliberately short so the Add-Measurement form exceeds the dialog
|
||||
// height cap and forces the body to scroll.
|
||||
await page.setViewportSize({ width: 1280, height: 480 });
|
||||
|
||||
await page.goto("/measurements?add=WEIGHT", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
const content = page.locator('[data-slot="responsive-sheet-content"]');
|
||||
await expect(content).toBeVisible({ timeout: 10_000 });
|
||||
await expect(content).toHaveAttribute("data-variant", "dialog");
|
||||
|
||||
// The body owns the scroll — its content is taller than its box.
|
||||
const body = page.locator('[data-slot="responsive-sheet-body"]');
|
||||
await expect(body).toBeVisible();
|
||||
const bodyScrolls = await body.evaluate(
|
||||
(el) => el.scrollHeight > el.clientHeight + 1,
|
||||
);
|
||||
expect(bodyScrolls).toBe(true);
|
||||
|
||||
// The footer (primary action) is reachable without scrolling: its
|
||||
// bottom edge sits within the viewport.
|
||||
const footer = page.locator('[data-slot="responsive-sheet-footer"]');
|
||||
await expect(footer).toBeVisible();
|
||||
const box = await footer.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
const viewport = page.viewportSize();
|
||||
expect(viewport).not.toBeNull();
|
||||
expect(box!.y + box!.height).toBeLessThanOrEqual(viewport!.height + 1);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.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",
|
||||
|
||||
+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.21.0";
|
||||
/* @sw-version-fallback */ "v1.21.1";
|
||||
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/*`
|
||||
|
||||
@@ -53,10 +53,9 @@ export interface ResponsiveSheetProps {
|
||||
* child and breaks that responsive stacking. This split is a
|
||||
* decision, not drift — don't unify the two branches.
|
||||
*
|
||||
* The Sheet branch sticky-pins the footer to the
|
||||
* bottom of the bottom-sheet so the primary CTA stays reachable
|
||||
* once the body scrolls; the Dialog branch lets the footer flow
|
||||
* normally beneath the body.
|
||||
* Both branches keep the footer reachable once the body scrolls:
|
||||
* the Sheet branch sticky-pins it to the bottom of the bottom-sheet,
|
||||
* and the Dialog branch holds it `shrink-0` below the scrolling body.
|
||||
*/
|
||||
footer?: React.ReactNode;
|
||||
/** Class name applied to the content surface (sheet / dialog). */
|
||||
@@ -100,12 +99,15 @@ const CONTENT_WIDTH_CLASS: Record<
|
||||
* centred `<Dialog>` on tablet/desktop. Picks the breakpoint via
|
||||
* the `useIsMobile()` hook (Tailwind `md` — 768 px).
|
||||
*
|
||||
* On the Sheet branch we cap the content at `90dvh`, scroll the
|
||||
* body on overflow, and sticky-pin the footer so Save / Cancel
|
||||
* stay reachable when the keyboard pushes the bottom of the sheet
|
||||
* up. The Dialog branch consumes the primitive's `max-h-[calc(100dvh-2rem)]`
|
||||
* default so long forms scroll inside the dialog instead of
|
||||
* spilling off-screen on tablet portrait.
|
||||
* Both branches use the same layout contract: cap the surface
|
||||
* height, scroll ONLY the body on overflow, and keep the footer
|
||||
* reachable. The Sheet branch caps at `90dvh` and sticky-pins the
|
||||
* footer so Save / Cancel stay reachable when the keyboard pushes
|
||||
* the bottom of the sheet up. The Dialog branch caps at the
|
||||
* primitive's `max-h-[calc(100dvh-2rem)]`, runs as a flex column,
|
||||
* and pins its header + footer (`shrink-0`) so the body — not the
|
||||
* whole grid — is what scrolls; the primary action never slides
|
||||
* below the fold under long forms, browser zoom, or OS scaling.
|
||||
*
|
||||
* Consumers swap `<Dialog>` → `<ResponsiveSheet>` and feed the
|
||||
* existing form markup through `children` / `footer`. The rest of
|
||||
@@ -194,7 +196,21 @@ export function ResponsiveSheet({
|
||||
showCloseButton={showCloseButton}
|
||||
data-slot="responsive-sheet-content"
|
||||
data-variant="dialog"
|
||||
className={cn(CONTENT_WIDTH_CLASS[contentWidth], className)}
|
||||
// v1.21.1 — flip the desktop Dialog from the primitive's single
|
||||
// `overflow-y-auto` grid (where header + body + footer all scroll
|
||||
// together, so the primary CTA can slide below the fold once the
|
||||
// form, a long locale string, browser zoom, or OS display scaling
|
||||
// pushes the content past `max-h-[calc(100dvh-2rem)]`) to a flex
|
||||
// column that scrolls ONLY the body. This brings the Dialog branch
|
||||
// to parity with the Sheet branch, which already pins its footer.
|
||||
// The header + footer stay shrink-0 so the body absorbs every
|
||||
// height shortfall and the action row is always reachable without
|
||||
// scrolling. `overflow-hidden` makes the body the lone scroll port.
|
||||
className={cn(
|
||||
"flex flex-col overflow-hidden",
|
||||
CONTENT_WIDTH_CLASS[contentWidth],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{hideHeader ? (
|
||||
<DialogHeader className="sr-only">
|
||||
@@ -204,7 +220,10 @@ export function ResponsiveSheet({
|
||||
) : null}
|
||||
</DialogHeader>
|
||||
) : (
|
||||
<DialogHeader data-slot="responsive-sheet-header">
|
||||
<DialogHeader
|
||||
data-slot="responsive-sheet-header"
|
||||
className="shrink-0"
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{description ? (
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
@@ -213,12 +232,18 @@ export function ResponsiveSheet({
|
||||
)}
|
||||
<div
|
||||
data-slot="responsive-sheet-body"
|
||||
className={cn("flex flex-col gap-4", bodyClassName)}
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto",
|
||||
bodyClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{footer ? (
|
||||
<DialogFooter data-slot="responsive-sheet-footer">
|
||||
<DialogFooter
|
||||
data-slot="responsive-sheet-footer"
|
||||
className="shrink-0"
|
||||
>
|
||||
{footer}
|
||||
</DialogFooter>
|
||||
) : null}
|
||||
|
||||
Reference in new issue
Block a user