Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-ai

# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 11:29:56 +02:00
commit cf7302b50a
38 files changed
+1118 -268

No files matched your search

@@ -5,6 +5,8 @@ import { Activity, Smile } from "lucide-react";
import { TrendCard } from "../trend-card";
import { I18nProvider } from "@/lib/i18n/context";
import { primeLocales } from "@/__tests__/helpers/prime-locales";
import { summarize } from "@/lib/analytics/trends";
import { summaryToTrend7Delta } from "@/lib/analytics/trend-delta";
primeLocales("de");
@@ -166,3 +168,42 @@ describe("<TrendCard> 7-day trend label", () => {
expect(html).not.toContain("text-warning");
});
});
/**
* The blood-pressure tile read "7-Tage-Trend: 125 (−11.412)" when the
* week's only readings were one sitting minutes apart: the regression's
* slope per day exploded and the tile printed seven days of it. The tile is
* fed the way `strip-tiles.tsx` feeds it, from `summarize()` through
* `summaryToTrend7Delta()`, so the delta it renders is the one the
* dashboard would.
*/
describe("<TrendCard> blood-pressure tile after one sitting", () => {
it("renders the 7-day mean, not a thousands-of-mmHg trend", () => {
const MINUTE = 60 * 1000;
const summary = summarize([
{ date: new Date(Date.now() - 10 * MINUTE), value: 130 },
{ date: new Date(Date.now() - 8 * MINUTE), value: 124 },
{ date: new Date(Date.now() - 6 * MINUTE), value: 118 },
]);
const html = render(
<TrendCard
label="BD (SYS)"
latest={summary.latest}
unit="mmHg"
fractionDigits={0}
avg7={summary.avg7}
avg30={summary.avg30}
slope30={summary.slope30}
trend7Delta={summaryToTrend7Delta(summary)}
icon={Activity}
directionSentiment="up-bad"
/>,
"de",
);
expect(html).not.toContain('data-slot="trend7-delta"');
expect(html).not.toContain("7-Tage-Trend");
expect(html).toContain("7T");
// No grouped thousands anywhere on the tile.
expect(html).not.toMatch(/\d\.\d{3}/);
});
});
@@ -12,6 +12,10 @@ import { readSource } from "@/__tests__/helpers/source-files";
import {
MAIN_SCROLL_CLASS,
RAIL_ACCOUNT_BAND,
RAIL_WIDTH_COLLAPSED,
RAIL_WIDTH_EXPANDED,
SHELL_HEADER_BAND,
SHELL_ROOT_CLASS,
SUB_SHELL_GRID_FLOOR,
} from "../shell-metrics";
@@ -137,3 +141,53 @@ describe("the bottom edge is cleared in both shells", () => {
);
});
});
/**
* The side insets. An iPad in a Safari tab laid the shell out 10 px in from
* the left edge in portrait; the shell padded that inset and its strip showed
* the page colour beside the rail, from the logo band to the account row.
* Desktop browsers report no side inset, so `e2e/tablet-shell.spec.ts`
* simulates one through `--safe-area-left` and measures the result; these
* hold the decisions that route the inset to the rail.
*/
describe("the side insets belong to what meets them", () => {
const css = () => readSource("src/app/globals.css", ".shell-safe-area {");
const rail = () =>
readSource(
"src/components/layout/sidebar-nav.tsx",
"export function SidebarNav",
);
it("are env() behind one property each, which the shell and the rail read", () => {
const root = readSource("src/app/globals.css", "--safe-area-left");
expect(root).toMatch(
/--safe-area-left:\s*env\(safe-area-inset-left,\s*0px\);/,
);
expect(root).toMatch(
/--safe-area-right:\s*env\(safe-area-inset-right,\s*0px\);/,
);
expect(css()).not.toMatch(/padding-(?:left|right):\s*env\(/);
});
it("the desktop shell pads no side, the phone shell pads both", () => {
const block = /\.shell-safe-area\s*\{([\s\S]*?)\n \}/.exec(css())![1];
expect(block).toMatch(/padding-left:\s*var\(--safe-area-left\);/);
expect(block).toMatch(/padding-right:\s*var\(--safe-area-right\);/);
expect(block).toMatch(
/@variant shell-desktop\s*\{\s*padding-right:\s*0;\s*padding-left:\s*0;\s*\}/,
);
});
it("the rail takes the left inset inside its own background", () => {
const source = rail();
expect(source).toMatch(/<aside[\s\S]*?pl-\(--safe-area-left\)/);
expect(RAIL_WIDTH_COLLAPSED).toBe("w-[calc(4rem+var(--safe-area-left))]");
expect(RAIL_WIDTH_EXPANDED).toBe("w-[calc(16rem+var(--safe-area-left))]");
});
it("the account band mirrors the logo band", () => {
const height = (value: string) => /\bh-(\d+)\b/.exec(value)?.[1];
expect(height(RAIL_ACCOUNT_BAND)).toBe(height(SHELL_HEADER_BAND));
expect(RAIL_ACCOUNT_BAND).toContain("border-t");
});
});
@@ -56,6 +56,7 @@ vi.mock("@/hooks/use-mounted", () => ({
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@/lib/i18n/context";
import { TooltipProvider } from "@/components/ui/tooltip";
import { RAIL_WIDTH_COLLAPSED } from "../shell-metrics";
import {
SIDEBAR_EXPANDED_CONTENT_WIDTH,
SidebarCollapseToggle,
@@ -559,7 +560,7 @@ describe("<SidebarNav> width transition never overflows the nav", () => {
});
mockMountedRef.value = true;
const html = render();
expect(asideClass(html).split(" ")).toContain("w-16");
expect(asideClass(html).split(" ")).toContain(RAIL_WIDTH_COLLAPSED);
expect(columnClass(html).split(" ")).not.toContain(
SIDEBAR_EXPANDED_CONTENT_WIDTH,
);
+49
View File
@@ -91,3 +91,52 @@ export const MAIN_SCROLL_CLASS =
* widths in both the expanded and the collapsed rail.
*/
export const SHELL_HEADER_BAND = "h-16 border-b";
/**
* The rail's account band, the bottom counterpart of its logo band: the same
* height, the border on the other side, the avatar centred in it. Padded on
* its own (`p-3`, `p-2` on short windows) it came out 61 to 69 px tall with
* 18 px above the avatar and more below, and its divider sat at no height
* the logo band shares. The home-indicator inset stays under the band (the
* rail's `pb-(--safe-area-bottom)`), so the band itself never shifts.
*/
export const RAIL_ACCOUNT_BAND = "h-16 shrink-0 border-t";
/**
* The rail's width, collapsed (4rem) and expanded (16rem), plus the window's
* left inset. The rail runs to the window's left edge and takes that inset as
* padding inside its own background (`pl-(--safe-area-left)` on the
* `<aside>`), so its content keeps the 4rem / 16rem it was laid out for and
* stays clear of the inset. The shell leaves the left side to the rail
* (`.shell-safe-area`, globals.css): an iPad in a Safari tab laid the shell
* out 10 px in from the left, and that strip showed the page colour beside
* the rail. Zero inset: exactly `w-16` / `w-64`.
*/
export const RAIL_WIDTH_COLLAPSED = "w-[calc(4rem+var(--safe-area-left))]";
export const RAIL_WIDTH_EXPANDED = "w-[calc(16rem+var(--safe-area-left))]";
/**
* A rail band that draws a border across the rail (the logo band, the footer
* group's scroll edge, the account band) reaches back over the left inset, so
* its line runs to the window's edge like the top bar's does, and adds the
* inset to its own left padding, so its content stays where it was.
*/
const RAIL_BAND_BLEED = "-ml-(--safe-area-left)";
/** The logo band's and the footer group's horizontal padding, inset included. */
export const RAIL_BAND_INSET = {
header: {
collapsed: `${RAIL_BAND_BLEED} pr-3 pl-[calc(0.75rem+var(--safe-area-left))]`,
expanded: `${RAIL_BAND_BLEED} pr-6 pl-[calc(1.5rem+var(--safe-area-left))]`,
},
footer: {
collapsed: `${RAIL_BAND_BLEED} pr-1.5 pl-[calc(0.375rem+var(--safe-area-left))]`,
expanded: `${RAIL_BAND_BLEED} pr-3 pl-[calc(0.75rem+var(--safe-area-left))]`,
},
} as const;
/** The account band, collapsed (the avatar centred) and expanded. */
export const RAIL_ACCOUNT_BAND_CLASS = {
collapsed: `${RAIL_ACCOUNT_BAND} ${RAIL_BAND_BLEED} border-sidebar-border flex items-center justify-center pl-(--safe-area-left)`,
expanded: `${RAIL_ACCOUNT_BAND} ${RAIL_BAND_BLEED} border-sidebar-border flex items-center pr-3 pl-[calc(0.75rem+var(--safe-area-left))]`,
} as const;
+20 -12
View File
@@ -26,7 +26,13 @@ import {
visibleNavDestinations,
visibleUtilityDestinations,
} from "@/components/layout/nav-model";
import { SHELL_HEADER_BAND } from "@/components/layout/shell-metrics";
import {
RAIL_ACCOUNT_BAND_CLASS,
RAIL_BAND_INSET,
RAIL_WIDTH_COLLAPSED,
RAIL_WIDTH_EXPANDED,
SHELL_HEADER_BAND,
} from "@/components/layout/shell-metrics";
import {
RailNavList,
useNavOverflows,
@@ -268,10 +274,8 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
if (collapsed) {
return (
<div
className={cn(
"border-sidebar-border border-t p-3",
"[@media(max-height:940px)]:p-2",
)}
data-slot="sidebar-account"
className={RAIL_ACCOUNT_BAND_CLASS.collapsed}
>
<div className="flex items-center justify-center">
<DropdownMenu>
@@ -294,8 +298,11 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
}
return (
<div className="border-sidebar-border border-t p-3">
<div className="flex items-center gap-3 px-2 py-1">
<div
data-slot="sidebar-account"
className={RAIL_ACCOUNT_BAND_CLASS.expanded}
>
<div className="flex w-full min-w-0 items-center gap-3 px-2 py-1">
{/* The avatar + name in the expanded footer routes straight
into the account settings; the kebab to the right still opens
the rest of the user menu. */}
@@ -343,7 +350,8 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
*
* Steps (rows are full-width, so the click target is the whole row):
* - default: 40 px rows, 4 px apart
* - max-height 940 px: gaps close (40 px pitch), list and user padding tighten
* - max-height 940 px: gaps close (40 px pitch), the list padding tightens
* (the account band keeps the logo band's height, `RAIL_ACCOUNT_BAND`)
* - max-height 820 px: 36 px rows
* - max-height 740 px: 32 px rows — the one step under 40 px, taken only
* where the alternative is rows hidden behind a scroll edge
@@ -627,8 +635,8 @@ export function SidebarNav() {
<aside
aria-label={t("nav.sidebar")}
className={cn(
"bg-sidebar border-sidebar-border shell-desktop:flex shell-desktop:flex-col relative hidden h-full flex-shrink-0 overflow-hidden border-r pb-(--safe-area-bottom) transition-[width] duration-200 motion-reduce:transition-none",
collapsed ? "w-16" : "w-64",
"bg-sidebar border-sidebar-border shell-desktop:flex shell-desktop:flex-col relative hidden h-full flex-shrink-0 overflow-hidden border-r pb-(--safe-area-bottom) pl-(--safe-area-left) transition-[width] duration-200 motion-reduce:transition-none",
collapsed ? RAIL_WIDTH_COLLAPSED : RAIL_WIDTH_EXPANDED,
)}
>
{/* The rail animates its width; the content does not. Expanded, this
@@ -656,7 +664,7 @@ export function SidebarNav() {
className={cn(
"border-sidebar-border",
SHELL_HEADER_BAND,
collapsed ? "px-3" : "px-6",
RAIL_BAND_INSET.header[collapsed ? "collapsed" : "expanded"],
)}
>
<Link
@@ -755,7 +763,7 @@ export function SidebarNav() {
className={cn(
RAIL_LIST_GAP,
"pb-1",
collapsed ? "px-1.5" : "px-3",
RAIL_BAND_INSET.footer[collapsed ? "collapsed" : "expanded"],
navOverflows && "border-sidebar-border border-t pt-1",
)}
>