chore(release): v1.4.27

v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-15 22:59:10 +02:00
1 parent d98e85cd03
commit 4f09c3f862
314 files changed
+26801 -4419

No files matched your search

@@ -28,4 +28,44 @@ describe("<Input>", () => {
expect(html).not.toContain("lpignore");
expect(html).not.toContain("1p-ignore");
});
it("derives inputMode=decimal from type=number", () => {
const html = renderToStaticMarkup(<Input id="weight" type="number" />);
expect(html).toContain('inputMode="decimal"');
});
it("derives inputMode=email from type=email", () => {
const html = renderToStaticMarkup(
<Input id="email" type="email" autoComplete="email" />,
);
expect(html).toContain('inputMode="email"');
});
it("derives inputMode=url from type=url", () => {
const html = renderToStaticMarkup(<Input id="webhook" type="url" />);
expect(html).toContain('inputMode="url"');
});
it("derives inputMode=tel from type=tel", () => {
const html = renderToStaticMarkup(<Input id="phone" type="tel" />);
expect(html).toContain('inputMode="tel"');
});
it("derives inputMode=search from type=search", () => {
const html = renderToStaticMarkup(<Input id="filter" type="search" />);
expect(html).toContain('inputMode="search"');
});
it("omits inputMode for plain text inputs", () => {
const html = renderToStaticMarkup(<Input id="note" type="text" />);
expect(html).not.toContain("inputMode=");
});
it("respects an explicit inputMode override on type=number", () => {
const html = renderToStaticMarkup(
<Input id="steps" type="number" inputMode="numeric" />,
);
expect(html).toContain('inputMode="numeric"');
expect(html).not.toContain('inputMode="decimal"');
});
});
@@ -0,0 +1,223 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
// `<ResponsiveSheet>` flips between a `<Sheet side="bottom">` and a
// centred `<Dialog>` via `useIsMobile()`. Vitest runs in the Node
// environment without `matchMedia`; the hook returns its SSR-safe
// default (`false`) and is overridden below for the Sheet branch.
//
// shadcn `<Sheet>` / `<Dialog>` both wrap Radix Portals — `renderToStaticMarkup`
// does not materialise portal trees — so we mock the primitives down
// to passthrough wrappers so the rendered shape is reachable in the
// static markup.
let mobile = false;
vi.mock("@/hooks/use-is-mobile", () => ({
useIsMobile: () => mobile,
}));
vi.mock("@/components/ui/sheet", () => ({
Sheet: ({
children,
open,
}: {
children: React.ReactNode;
open: boolean;
}) => (open ? <div data-slot="mock-sheet">{children}</div> : null),
SheetContent: ({
children,
className,
...rest
}: {
children: React.ReactNode;
className?: string;
[key: string]: unknown;
}) => (
<div data-slot="mock-sheet-content" className={className} {...rest}>
{children}
</div>
),
SheetHeader: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div className={className}>{children}</div>,
SheetFooter: ({
children,
className,
...rest
}: {
children: React.ReactNode;
className?: string;
[key: string]: unknown;
}) => (
<div className={className} {...rest}>
{children}
</div>
),
SheetTitle: ({ children }: { children: React.ReactNode }) => (
<h2>{children}</h2>
),
SheetDescription: ({ children }: { children: React.ReactNode }) => (
<p>{children}</p>
),
}));
vi.mock("@/components/ui/dialog", () => ({
Dialog: ({
children,
open,
}: {
children: React.ReactNode;
open: boolean;
}) => (open ? <div data-slot="mock-dialog">{children}</div> : null),
DialogContent: ({
children,
className,
...rest
}: {
children: React.ReactNode;
className?: string;
[key: string]: unknown;
}) => (
<div data-slot="mock-dialog-content" className={className} {...rest}>
{children}
</div>
),
DialogHeader: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div className={className}>{children}</div>,
DialogFooter: ({
children,
className,
...rest
}: {
children: React.ReactNode;
className?: string;
[key: string]: unknown;
}) => (
<div className={className} {...rest}>
{children}
</div>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<h2>{children}</h2>
),
DialogDescription: ({ children }: { children: React.ReactNode }) => (
<p>{children}</p>
),
}));
import { ResponsiveSheet } from "../responsive-sheet";
describe("<ResponsiveSheet>", () => {
it("renders the Dialog branch on `md+` viewports", () => {
mobile = false;
const html = renderToStaticMarkup(
<ResponsiveSheet
open
onOpenChange={() => {}}
title="Add measurement"
description="Log a new reading"
>
<p>body</p>
</ResponsiveSheet>,
);
expect(html).toContain('data-variant="dialog"');
expect(html).toContain("Add measurement");
expect(html).toContain("Log a new reading");
expect(html).toContain("body");
// Sheet branch markers must be absent.
expect(html).not.toContain('data-slot="mock-sheet"');
});
it("renders the bottom Sheet branch on narrow viewports", () => {
mobile = true;
const html = renderToStaticMarkup(
<ResponsiveSheet open onOpenChange={() => {}} title="Add measurement">
<p>body</p>
</ResponsiveSheet>,
);
expect(html).toContain('data-variant="sheet"');
expect(html).toContain('data-slot="mock-sheet"');
expect(html).toContain("Add measurement");
expect(html).toContain("body");
mobile = false;
});
it("renders a sticky-pinned footer on the Sheet branch", () => {
mobile = true;
const html = renderToStaticMarkup(
<ResponsiveSheet
open
onOpenChange={() => {}}
title="Add measurement"
footer={<button type="button">Save</button>}
>
<p>body</p>
</ResponsiveSheet>,
);
expect(html).toContain('data-slot="responsive-sheet-footer"');
expect(html).toContain("sticky");
expect(html).toContain("Save");
mobile = false;
});
it("renders a flow-layout footer on the Dialog branch", () => {
mobile = false;
const html = renderToStaticMarkup(
<ResponsiveSheet
open
onOpenChange={() => {}}
title="Add measurement"
footer={<button type="button">Save</button>}
>
<p>body</p>
</ResponsiveSheet>,
);
expect(html).toContain('data-slot="responsive-sheet-footer"');
expect(html).not.toContain("sticky bottom-0");
expect(html).toContain("Save");
});
it("hides the visual header but keeps the title accessible via sr-only when hideHeader is set", () => {
mobile = false;
const html = renderToStaticMarkup(
<ResponsiveSheet
open
onOpenChange={() => {}}
title="Edit medication"
hideHeader
>
<p>body</p>
</ResponsiveSheet>,
);
expect(html).toContain("sr-only");
expect(html).toContain("Edit medication");
});
it("does not throw when onOpenChange is invoked (controlled-state plumbing)", () => {
mobile = false;
const onOpenChange = vi.fn();
// Mounted closed — Dialog mock returns null, smoke-only render
// confirms no throw + the prop wiring round-trips.
renderToStaticMarkup(
<ResponsiveSheet
open={false}
onOpenChange={onOpenChange}
title="closed"
>
<p>body</p>
</ResponsiveSheet>,
);
onOpenChange(true);
expect(onOpenChange).toHaveBeenCalledWith(true);
});
});