diff --git a/e2e/day-doors.spec.ts b/e2e/day-doors.spec.ts
index 357e2773c..c650f5c81 100644
--- a/e2e/day-doors.spec.ts
+++ b/e2e/day-doors.spec.ts
@@ -266,7 +266,7 @@ test.describe("the dashboard and the list of all values", () => {
);
});
- test("the docked day moves the Coach button off its footer", async ({
+ test("the docked day, open or collapsed, moves the Coach button off it", async ({
page,
}, testInfo) => {
test.skip(isPhone(testInfo), "docked from 1280 px");
@@ -290,6 +290,16 @@ test.describe("the dashboard and the list of all values", () => {
return b.x + b.width;
})
.toBeLessThanOrEqual(panelBox.x);
+
+ // Collapsed, the day keeps a narrow edge; the launcher stays clear of it.
+ await page.locator('[data-slot="day-close"]').click();
+ const edge = (await page.locator('[data-slot="day-rail"]').boundingBox())!;
+ await expect
+ .poll(async () => {
+ const b = (await fab.boundingBox())!;
+ return b.x + b.width;
+ })
+ .toBeLessThanOrEqual(edge.x);
});
});
diff --git a/e2e/day-view.spec.ts b/e2e/day-view.spec.ts
index 6a04b16f0..679e57884 100644
--- a/e2e/day-view.spec.ts
+++ b/e2e/day-view.spec.ts
@@ -21,11 +21,14 @@ import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
* - `?day=` opens the layer on any page, Back and Escape close it, a date in
* the future is dropped;
* - stepping to the neighbouring days never adds history entries;
+ * - docked, the day collapses to a narrow edge that brings it back on any
+ * page; a sheet closes for good;
* - the dashboard's today area offers no door; its charts below do.
*
* Stable data attributes only: `day-panel` (+ `data-shell`), `day-link`,
* `chart-plot[data-day-links]`, `chart-tooltip-open-day`, `day-prev` /
- * `day-next` / `day-close`.
+ * `day-next` / `day-close`, and the collapsed edge `day-rail` (+ `data-day`)
+ * with its `day-expand`.
*/
const panel = (page: Page) => page.locator('[data-slot="day-panel"]');
@@ -117,6 +120,68 @@ test.describe("the day view", () => {
await expect(page).toHaveURL(/\/mood$/);
});
+ test("the docked day collapses to an edge and comes back on another page", async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "docked from 1280 px",
+ );
+ const day = isoDaysAgo(3);
+ await page.goto(`/mood?day=${day}`);
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+
+ await page.locator('[data-slot="day-close"]').click();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/mood$/);
+ // A narrow edge stays, holding the day, and focus waits on its button.
+ const rail = page.locator('[data-slot="day-rail"]');
+ const expand = page.locator('[data-slot="day-expand"]');
+ await expect(rail).toHaveAttribute("data-day", day);
+ await expect(expand).toBeFocused();
+ await expect(expand).toHaveAttribute("aria-expanded", "false");
+ expect(await expand.getAttribute("aria-label")).toMatch(/^Show /);
+ expect((await rail.boundingBox())!.width).toBeLessThanOrEqual(48);
+
+ // Another page: the edge is still there and brings the same day back,
+ // from the keyboard too.
+ await page.goto("/labs");
+ await expect(rail).toHaveAttribute("data-day", day);
+ await expand.focus();
+ await page.keyboard.press("Enter");
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day,
+ );
+ await expect(page).toHaveURL(new RegExp(`/labs\\?day=${day}$`));
+ await expect(panel(page).locator("h2").first()).toBeFocused();
+ await expect(rail).toHaveCount(0);
+ });
+
+ test("a sheet closes for good and leaves no edge behind", async ({
+ page,
+ }, testInfo) => {
+ const desktop = testInfo.project.name === "chromium-desktop";
+ // Below 1280 px the day is a sheet from the right; on a phone, from the
+ // bottom. Neither collapses.
+ if (desktop) await page.setViewportSize({ width: 1024, height: 800 });
+ const day = isoDaysAgo(3);
+ await page.goto(`/mood?day=${day}`);
+ await expect(panel(page)).toHaveAttribute(
+ "data-shell",
+ desktop ? "sheet" : "bottom",
+ );
+ await expect(page.locator('[data-slot="day-close"]')).toHaveAttribute(
+ "aria-label",
+ "Close day",
+ );
+ await page.locator('[data-slot="day-close"]').click();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/mood$/);
+ await expect(page.locator('[data-slot="day-rail"]')).toHaveCount(0);
+ });
+
test("a future or malformed ?day= is dropped without a word", async ({
page,
}) => {
diff --git a/messages/de.json b/messages/de.json
index b7cc77b50..84b126d51 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -12207,6 +12207,8 @@
"previousDay": "Vortag",
"nextDay": "Folgetag",
"close": "Tag schließen",
+ "hidePanel": "Tag ausblenden",
+ "showPanel": "{date} einblenden",
"openDay": "Tag öffnen",
"viewWholeDay": "Ganzen Tag ansehen",
"captureForDay": "Für diesen Tag erfassen",
diff --git a/messages/en.json b/messages/en.json
index bdea7e6a7..ad96b368e 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -12207,6 +12207,8 @@
"previousDay": "Previous day",
"nextDay": "Next day",
"close": "Close day",
+ "hidePanel": "Hide day",
+ "showPanel": "Show {date}",
"openDay": "Open day",
"viewWholeDay": "View the whole day",
"captureForDay": "Add for this day",
diff --git a/messages/es.json b/messages/es.json
index 3e9806ff9..349a397ba 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -12207,6 +12207,8 @@
"previousDay": "Día anterior",
"nextDay": "Día siguiente",
"close": "Cerrar día",
+ "hidePanel": "Ocultar día",
+ "showPanel": "Mostrar {date}",
"openDay": "Abrir día",
"viewWholeDay": "Ver el día completo",
"captureForDay": "Registrar para este día",
diff --git a/messages/fr.json b/messages/fr.json
index 4ffa3c91a..9e484dfbf 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -12207,6 +12207,8 @@
"previousDay": "Jour précédent",
"nextDay": "Jour suivant",
"close": "Fermer le jour",
+ "hidePanel": "Masquer le jour",
+ "showPanel": "Afficher {date}",
"openDay": "Ouvrir le jour",
"viewWholeDay": "Voir toute la journée",
"captureForDay": "Saisir pour ce jour",
diff --git a/messages/it.json b/messages/it.json
index 7fb859048..3cd58750b 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -12207,6 +12207,8 @@
"previousDay": "Giorno precedente",
"nextDay": "Giorno successivo",
"close": "Chiudi giorno",
+ "hidePanel": "Nascondi giorno",
+ "showPanel": "Mostra {date}",
"openDay": "Apri giorno",
"viewWholeDay": "Vedi l’intera giornata",
"captureForDay": "Registra per questo giorno",
diff --git a/messages/ko.json b/messages/ko.json
index 3405b2f39..5d5956d00 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -12207,6 +12207,8 @@
"previousDay": "전날",
"nextDay": "다음 날",
"close": "하루 닫기",
+ "hidePanel": "하루 숨기기",
+ "showPanel": "{date} 보기",
"openDay": "하루 열기",
"viewWholeDay": "하루 전체 보기",
"captureForDay": "이 날에 기록하기",
diff --git a/messages/pl.json b/messages/pl.json
index 7100d719b..c8d6bc240 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -12207,6 +12207,8 @@
"previousDay": "Poprzedni dzień",
"nextDay": "Następny dzień",
"close": "Zamknij dzień",
+ "hidePanel": "Ukryj dzień",
+ "showPanel": "Pokaż {date}",
"openDay": "Otwórz dzień",
"viewWholeDay": "Zobacz cały dzień",
"captureForDay": "Dodaj dla tego dnia",
diff --git a/src/components/day/__tests__/day-view.test.tsx b/src/components/day/__tests__/day-view.test.tsx
index 310db7133..038aa97ee 100644
--- a/src/components/day/__tests__/day-view.test.tsx
+++ b/src/components/day/__tests__/day-view.test.tsx
@@ -146,6 +146,7 @@ async function render(
date: string;
today: string;
focus: unknown;
+ shell: "docked" | "sheet" | "bottom";
}> = {},
) {
const { I18nProvider } = await import("@/lib/i18n/context");
@@ -156,7 +157,7 @@ async function render(
date={props.date ?? "2026-01-03"}
today={props.today ?? "2026-10-08"}
focus={(props.focus as never) ?? null}
- shell="docked"
+ shell={props.shell ?? "docked"}
onClose={() => undefined}
onStep={() => undefined}
Title={(p) =>
}
@@ -232,6 +233,19 @@ describe("", () => {
expect(leave).not.toContain("FAMILY");
});
+ it("collapses the docked day and closes a sheet, and says which", async () => {
+ const docked = await render();
+ const hide = docked.match(/