diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 93d60ed88..363e22769 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -80936,7 +80936,6 @@ components: - workouts - cycle - environment - - lifestyle - lifeEvents description: Each day in the window that holds anything, keyed `YYYY-MM-DD`, with the sections it holds. Days without entries are absent. @@ -81087,7 +81086,6 @@ components: - workouts - cycle - environment - - lifestyle - lifeEvents id: type: string @@ -81190,7 +81188,6 @@ components: - workouts - cycle - environment - - lifestyle - lifeEvents additionalProperties: type: object @@ -81231,7 +81228,6 @@ components: - restMode - cyclePhase - travel - - lifestyle - lifeEvent section: type: string @@ -81252,7 +81248,6 @@ components: - workouts - cycle - environment - - lifestyle - lifeEvents id: type: string @@ -81265,8 +81260,12 @@ components: - type: "null" description: A second line, already formatted (a dose, a destination). since: - type: string - description: A local calendar date, `YYYY-MM-DD`. + anyOf: + - type: string + description: A local calendar date, `YYYY-MM-DD`. + - type: "null" + description: The record's own start date. Null when the record holds none (a medication filed without a start date); the + entry's creation date is never put in its place. until: anyOf: - type: string @@ -81279,7 +81278,7 @@ components: exclusiveMinimum: 0 maximum: 9007199254740991 - type: "null" - description: Day n of the period, counting `since` as day 1. + description: Day n of the period, counting `since` as day 1; null when `since` is null. dayCount: anyOf: - type: integer @@ -81405,7 +81404,6 @@ components: - workouts - cycle - environment - - lifestyle - lifeEvents id: type: string 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 795904f21..bf34146de 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12209,6 +12209,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 a30ac986b..9d38f2574 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12209,6 +12209,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 c46dc490e..4979a7936 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12209,6 +12209,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 3b61b97e9..c2e16bf6e 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12209,6 +12209,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 62db0cedd..f4cb777dc 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12209,6 +12209,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 244aa0850..b570216a0 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12209,6 +12209,8 @@ "previousDay": "전날", "nextDay": "다음 날", "close": "하루 닫기", + "hidePanel": "하루 숨기기", + "showPanel": "{date} 보기", "openDay": "하루 열기", "viewWholeDay": "하루 전체 보기", "captureForDay": "이 날에 기록하기", diff --git a/messages/pl.json b/messages/pl.json index 2c66a69ef..46f29666b 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12209,6 +12209,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 db80cd99f..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) =>
} @@ -190,6 +191,61 @@ describe("