From 81cba15bed2bdc9c1981895e5d8715e93ef78023 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 03:50:45 +0000 Subject: [PATCH 1/3] chore(life): #5228 follow-up cleanup and caption year artifacts Remove dead macOS yearPage helper, fix yearsPerPage sync comment, and reserve Life variation chip space only while Mod page-jump hints are visible. Add narrow sidebar web artifact captures for the 20-year caption grid. Co-authored-by: Tyler Dane --- .../CompassKit/Life/DatePickerCaption.swift | 6 -- .../DatePicker/caption-picker-year-page.ts | 2 +- .../src/views/Life/LifeSidebarContent.tsx | 11 ++- e2e/timed/caption-year-grid-artifact.spec.ts | 68 +++++++++++++++++++ 4 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 e2e/timed/caption-year-grid-artifact.spec.ts diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Life/DatePickerCaption.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Life/DatePickerCaption.swift index 405bc2d3b7..2eef42dfc6 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Life/DatePickerCaption.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Life/DatePickerCaption.swift @@ -28,12 +28,6 @@ public enum DatePickerCaption { return (start ..< start + yearsPerPage).map { $0 } } - /// Decade-aligned pages (1980–1999 for 1993); legacy sidebar grids. - public static func yearPage(containing year: Int) -> [Int] { - let start = year - ((year % yearsPerPage) + yearsPerPage) % yearsPerPage - return (start ..< start + yearsPerPage).map { $0 } - } - public static let monthLabels = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ] diff --git a/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts b/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts index dabf4a9719..76e3decd3e 100644 --- a/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts +++ b/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts @@ -1,4 +1,4 @@ -/** Keep in sync with `DatePickerCaption.reactYearsPerPage` on macOS. */ +/** Keep in sync with `DatePickerCaption.yearsPerPage` on macOS. */ export const CAPTION_YEARS_PER_PAGE = 20; export type YearPageBounds = { readonly start: number; readonly end: number }; diff --git a/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx b/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx index 47af1b1c56..d7542f6286 100644 --- a/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx +++ b/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx @@ -14,6 +14,10 @@ import { ID_LIFE_BIRTH_DATE } from "@web/common/constants/web.constants"; import { ArrowButton } from "@web/components/Button/ArrowButton"; import { NumberInput } from "@web/components/NumberInput/NumberInput"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; +import { + selectPageJumpHintsVisible, + usePageJumpHintStore, +} from "@web/shortcuts/page-jump/page-jump.store"; import { pageJumpAttrs, pageJumpChipAttrs, @@ -46,6 +50,7 @@ export function LifeSidebarContent({ today, onPreferencesChange, }: LifeSidebarContentProps) { + const pageJumpHintsVisible = usePageJumpHintStore(selectPageJumpHintsVisible); const [isBirthDatePickerOpen, setIsBirthDatePickerOpen] = useState(false); const [lifespanInput, setLifespanInput] = useState(() => String(preferences.lifespan), @@ -124,9 +129,9 @@ export function LifeSidebarContent({ /> - {/* Portaled Mod chips do not affect layout; reserve space so chip 3 - sits between the arrows and the title without covering the label. */} - + {pageJumpHintsVisible ? ( + + ) : null}

{LIFE_VARIATIONS[preferences.variation].label}

diff --git a/e2e/timed/caption-year-grid-artifact.spec.ts b/e2e/timed/caption-year-grid-artifact.spec.ts new file mode 100644 index 0000000000..8e1ff0f23a --- /dev/null +++ b/e2e/timed/caption-year-grid-artifact.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from "@playwright/test"; + +const ARTIFACT_PREFIX = "/opt/cursor/artifacts/caption-year-grid"; + +async function openLifeBirthYearGrid(page: import("@playwright/test").Page) { + await page.addInitScript(() => { + localStorage.setItem("compass.onboarding.has-seen-life-view-intro", "true"); + localStorage.setItem("compass.view.sidebar-open", "true"); + localStorage.setItem( + "compass.life.preferences", + JSON.stringify({ + birthDate: "1993-09-14", + lifespan: 79, + variation: "average", + }), + ); + }); + + await page.goto("/life", { waitUntil: "domcontentloaded" }); + await expect(page.getByRole("heading", { name: "Life" })).toBeVisible(); + + const openSidebar = page.getByRole("button", { name: "Open sidebar" }); + if (await openSidebar.isVisible()) { + await openSidebar.click(); + } + + const birth = page.getByRole("textbox", { name: "Date of birth" }); + await birth.click(); + await birth.press("ArrowDown"); + + const popper = page.locator( + ".react-datepicker-popper:has(.lifeBirthDatePicker)", + ); + await expect(popper).toBeVisible(); + + const caption = popper.getByRole("button", { + name: "Choose month and year", + }); + await caption.click(); + await caption.click(); + + await expect(popper.getByText("Years 1981 to 2000")).toBeVisible(); + const years = await popper.getByRole("option").allTextContents(); + expect(years.filter((y) => /^\d{4}$/.test(y.trim()))).toHaveLength(20); + expect( + years.map((y) => Number.parseInt(y.trim(), 10)).filter(Boolean), + ).toEqual(expect.arrayContaining([1981, 2000])); +} + +for (const { name, stored } of [ + { name: "light", stored: "light-beach" }, + { name: "dark", stored: "dark-abyss" }, +] as const) { + test(`artifact: caption year grid (${name}, narrow sidebar)`, { + tag: "@artifact", + }, async ({ page }) => { + await page.addInitScript((theme) => { + localStorage.setItem("compass.theme", theme); + }, stored); + + await page.setViewportSize({ width: 360, height: 720 }); + await openLifeBirthYearGrid(page); + + await page.locator("#sidebar").screenshot({ + path: `${ARTIFACT_PREFIX}-web-${name}-360.png`, + }); + }); +} From 8879ebee3588d5620670e27d031918a8680ebdd6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 04:29:46 +0000 Subject: [PATCH 2/3] fix(life): #5228 follow-up review fixes Reserve Mod chip space invisibly to avoid title layout shift, remove dead mac year-page web helpers, add LifeView layout assertion, and replace artifact-only e2e with narrow light/dark year-grid checks. Co-authored-by: Tyler Dane --- .../caption-picker-year-page.test.ts | 5 - .../DatePicker/caption-picker-year-page.ts | 20 ---- .../src/views/Life/LifeSidebarContent.tsx | 11 ++- .../src/views/Life/LifeView.test.tsx | 10 ++ e2e/life/caption-year-grid-narrow.spec.ts | 99 +++++++++++++++++++ e2e/timed/caption-year-grid-artifact.spec.ts | 68 ------------- 6 files changed, 117 insertions(+), 96 deletions(-) create mode 100644 e2e/life/caption-year-grid-narrow.spec.ts delete mode 100644 e2e/timed/caption-year-grid-artifact.spec.ts diff --git a/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.test.ts b/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.test.ts index 46fda33455..dc5752588f 100644 --- a/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.test.ts +++ b/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.test.ts @@ -1,5 +1,4 @@ import { - macYearRangeCaption, reactDatepickerYearPageBounds, shiftYearPageBounds, yearRangeCaptionFromBounds, @@ -33,8 +32,4 @@ describe("caption-picker-year-page", () => { expect(shiftYearPageBounds(page, -1)).toEqual({ start: 1961, end: 1980 }); expect(shiftYearPageBounds(page, 1)).toEqual({ start: 2001, end: 2020 }); }); - - it("keeps mac decade alignment separate", () => { - expect(macYearRangeCaption(1993)).toBe("Years 1980 to 1999"); - }); }); diff --git a/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts b/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts index 76e3decd3e..6aea14f809 100644 --- a/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts +++ b/apps/calendar-web/src/components/DatePicker/caption-picker-year-page.ts @@ -30,23 +30,3 @@ export const yearRangeCaptionFromBounds = (bounds: YearPageBounds): string => { const { start, end } = bounds; return start === end ? `Year ${start}` : `Years ${start} to ${end}`; }; - -/** macOS decade-aligned pages (0, 20, 40, …) for non-react grids. */ -export const macYearPageContaining = ( - year: number, - pageSize = CAPTION_YEARS_PER_PAGE, -): readonly number[] => { - const start = year - (((year % pageSize) + pageSize) % pageSize); - return Array.from({ length: pageSize }, (_, index) => start + index); -}; - -export const macYearRangeCaption = ( - anchorYear: number, - pageSize = CAPTION_YEARS_PER_PAGE, -): string => { - const page = macYearPageContaining(anchorYear, pageSize); - const first = page[0]; - const last = page[page.length - 1]; - if (first === undefined || last === undefined) return "Select year"; - return first === last ? `Year ${first}` : `Years ${first} to ${last}`; -}; diff --git a/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx b/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx index d7542f6286..05d726abe2 100644 --- a/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx +++ b/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx @@ -129,9 +129,14 @@ export function LifeSidebarContent({ /> - {pageJumpHintsVisible ? ( - - ) : null} +

{LIFE_VARIATIONS[preferences.variation].label}

diff --git a/apps/calendar-web/src/views/Life/LifeView.test.tsx b/apps/calendar-web/src/views/Life/LifeView.test.tsx index 7fd3b4981c..7b5437b533 100644 --- a/apps/calendar-web/src/views/Life/LifeView.test.tsx +++ b/apps/calendar-web/src/views/Life/LifeView.test.tsx @@ -13,6 +13,7 @@ import { pressKey } from "@web/__tests__/utils/keyboard.test.util"; import { mockModuleForFile } from "@web/__tests__/utils/mock-module.test.util"; import { STORAGE_KEYS } from "@web/common/constants/storage.constants"; import { viewActions } from "@web/events/stores/view.store"; +import { pageJumpHintActions } from "@web/shortcuts/page-jump/page-jump.store"; import { LifeView } from "./LifeView"; import { afterAll, @@ -413,6 +414,15 @@ describe("LifeView", () => { expect(randomAge).toBeLessThanOrEqual(100); }); + it("does not shift the life variation title when Mod page-jump hints appear", async () => { + await renderLifeViewWithSidebar(); + const title = screen.getByRole("heading", { level: 2, name: "Average" }); + const before = title.getBoundingClientRect().left; + act(() => pageJumpHintActions.setHintsVisible(true)); + expect(title.getBoundingClientRect().left).toBe(before); + act(() => pageJumpHintActions.setHintsVisible(false)); + }); + it("cycles life variations with J and K", async () => { await renderLifeViewWithSidebar(); ( diff --git a/e2e/life/caption-year-grid-narrow.spec.ts b/e2e/life/caption-year-grid-narrow.spec.ts new file mode 100644 index 0000000000..6628265960 --- /dev/null +++ b/e2e/life/caption-year-grid-narrow.spec.ts @@ -0,0 +1,99 @@ +import { expect, test } from "@playwright/test"; + +async function openLifeBirthYearGrid(page: import("@playwright/test").Page) { + await page.goto("/life", { waitUntil: "domcontentloaded" }); + await expect(page.getByRole("heading", { name: "Life" })).toBeVisible(); + + const birth = page.getByRole("textbox", { name: "Date of birth" }); + await birth.click(); + await birth.press("ArrowDown"); + + const popper = page.locator( + ".react-datepicker-popper:has(.lifeBirthDatePicker)", + ); + await expect(popper).toBeVisible(); + + const caption = popper.getByRole("button", { + name: "Choose month and year", + }); + await caption.click(); + await caption.click(); + + await expect(popper.getByText("Years 1981 to 2000")).toBeVisible(); +} + +for (const { name, stored } of [ + { name: "light", stored: "light-beach" }, + { name: "dark", stored: "dark-abyss" }, +] as const) { + test(`life birth date picker year grid fits 360px sidebar (${name})`, async ({ + page, + }) => { + await page.addInitScript((theme) => { + localStorage.setItem("compass.theme", theme); + localStorage.setItem( + "compass.onboarding.has-seen-life-view-intro", + "true", + ); + localStorage.setItem("compass.view.sidebar-open", "true"); + localStorage.setItem( + "compass.life.preferences", + JSON.stringify({ + birthDate: "1993-09-14", + lifespan: 79, + variation: "average", + }), + ); + }, stored); + + await page.setViewportSize({ width: 360, height: 720 }); + await openLifeBirthYearGrid(page); + + const sidebar = page.locator("#sidebar"); + const popper = page.locator( + ".react-datepicker-popper:has(.lifeBirthDatePicker)", + ); + + const yearLabels = await popper + .getByRole("option") + .filter({ hasText: /^\d{4}$/ }) + .allTextContents(); + expect(yearLabels).toHaveLength(20); + expect(yearLabels.map((y) => Number.parseInt(y, 10))).toEqual( + expect.arrayContaining([1981, 2000]), + ); + + const fitsSidebar = await page.evaluate(() => { + const sidebarEl = document.querySelector("#sidebar"); + const popperEl = document.querySelector( + ".react-datepicker-popper:has(.lifeBirthDatePicker)", + ); + if ( + !(sidebarEl instanceof HTMLElement) || + !(popperEl instanceof HTMLElement) + ) { + return { ok: false, reason: "missing nodes" }; + } + const sb = sidebarEl.getBoundingClientRect(); + const caption = popperEl.querySelector( + ".react-datepicker__current-month, [class*='caption']", + ); + const grid = popperEl.querySelector( + ".react-datepicker__year", + ); + const check = (el: HTMLElement | null, name: string) => { + if (!el) return `${name} missing`; + const b = el.getBoundingClientRect(); + if (b.left < sb.left - 1 || b.right > sb.right + 1) { + return `${name} overflows sidebar (${b.left.toFixed(0)}..${b.right.toFixed(0)} vs ${sb.left.toFixed(0)}..${sb.right.toFixed(0)})`; + } + return null; + }; + const err = check(caption, "caption") ?? check(grid, "year grid") ?? null; + return err ? { ok: false as const, reason: err } : { ok: true as const }; + }); + + expect(fitsSidebar).toEqual({ ok: true }); + await expect(sidebar.getByText("Years 1981 to 2000")).toBeVisible(); + }); +} diff --git a/e2e/timed/caption-year-grid-artifact.spec.ts b/e2e/timed/caption-year-grid-artifact.spec.ts deleted file mode 100644 index 8e1ff0f23a..0000000000 --- a/e2e/timed/caption-year-grid-artifact.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { expect, test } from "@playwright/test"; - -const ARTIFACT_PREFIX = "/opt/cursor/artifacts/caption-year-grid"; - -async function openLifeBirthYearGrid(page: import("@playwright/test").Page) { - await page.addInitScript(() => { - localStorage.setItem("compass.onboarding.has-seen-life-view-intro", "true"); - localStorage.setItem("compass.view.sidebar-open", "true"); - localStorage.setItem( - "compass.life.preferences", - JSON.stringify({ - birthDate: "1993-09-14", - lifespan: 79, - variation: "average", - }), - ); - }); - - await page.goto("/life", { waitUntil: "domcontentloaded" }); - await expect(page.getByRole("heading", { name: "Life" })).toBeVisible(); - - const openSidebar = page.getByRole("button", { name: "Open sidebar" }); - if (await openSidebar.isVisible()) { - await openSidebar.click(); - } - - const birth = page.getByRole("textbox", { name: "Date of birth" }); - await birth.click(); - await birth.press("ArrowDown"); - - const popper = page.locator( - ".react-datepicker-popper:has(.lifeBirthDatePicker)", - ); - await expect(popper).toBeVisible(); - - const caption = popper.getByRole("button", { - name: "Choose month and year", - }); - await caption.click(); - await caption.click(); - - await expect(popper.getByText("Years 1981 to 2000")).toBeVisible(); - const years = await popper.getByRole("option").allTextContents(); - expect(years.filter((y) => /^\d{4}$/.test(y.trim()))).toHaveLength(20); - expect( - years.map((y) => Number.parseInt(y.trim(), 10)).filter(Boolean), - ).toEqual(expect.arrayContaining([1981, 2000])); -} - -for (const { name, stored } of [ - { name: "light", stored: "light-beach" }, - { name: "dark", stored: "dark-abyss" }, -] as const) { - test(`artifact: caption year grid (${name}, narrow sidebar)`, { - tag: "@artifact", - }, async ({ page }) => { - await page.addInitScript((theme) => { - localStorage.setItem("compass.theme", theme); - }, stored); - - await page.setViewportSize({ width: 360, height: 720 }); - await openLifeBirthYearGrid(page); - - await page.locator("#sidebar").screenshot({ - path: `${ARTIFACT_PREFIX}-web-${name}-360.png`, - }); - }); -} From 44c09dfc7ce6e6a363a06d3965f70c37aed818c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 04:42:55 +0000 Subject: [PATCH 3/3] fix(life): lint class order and stabilize year-grid e2e Sort Tailwind classes for biome and match react-datepicker year-text selectors. Co-authored-by: Tyler Dane --- .../src/views/Life/LifeSidebarContent.tsx | 2 +- e2e/life/caption-year-grid-narrow.spec.ts | 62 +++++++------------ 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx b/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx index 05d726abe2..1fd0793240 100644 --- a/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx +++ b/apps/calendar-web/src/views/Life/LifeSidebarContent.tsx @@ -134,7 +134,7 @@ export function LifeSidebarContent({ className={ pageJumpHintsVisible ? "inline-block w-7 shrink-0" - : "inline-block w-7 shrink-0 invisible" + : "invisible inline-block w-7 shrink-0" } />

diff --git a/e2e/life/caption-year-grid-narrow.spec.ts b/e2e/life/caption-year-grid-narrow.spec.ts index 6628265960..8e1308987b 100644 --- a/e2e/life/caption-year-grid-narrow.spec.ts +++ b/e2e/life/caption-year-grid-narrow.spec.ts @@ -4,22 +4,28 @@ async function openLifeBirthYearGrid(page: import("@playwright/test").Page) { await page.goto("/life", { waitUntil: "domcontentloaded" }); await expect(page.getByRole("heading", { name: "Life" })).toBeVisible(); + const openSidebar = page.getByRole("button", { name: "Open sidebar" }); + if (await openSidebar.isVisible()) { + await openSidebar.click(); + } + const birth = page.getByRole("textbox", { name: "Date of birth" }); - await birth.click(); + await birth.focus(); await birth.press("ArrowDown"); const popper = page.locator( ".react-datepicker-popper:has(.lifeBirthDatePicker)", ); - await expect(popper).toBeVisible(); + await expect(popper).toBeVisible({ timeout: 15000 }); const caption = popper.getByRole("button", { name: "Choose month and year", }); await caption.click(); - await caption.click(); - await expect(popper.getByText("Years 1981 to 2000")).toBeVisible(); + await expect(popper.getByText("Years 1981 to 2000")).toBeVisible({ + timeout: 15000, + }); } for (const { name, stored } of [ @@ -49,51 +55,25 @@ for (const { name, stored } of [ await page.setViewportSize({ width: 360, height: 720 }); await openLifeBirthYearGrid(page); - const sidebar = page.locator("#sidebar"); const popper = page.locator( ".react-datepicker-popper:has(.lifeBirthDatePicker)", ); - const yearLabels = await popper - .getByRole("option") - .filter({ hasText: /^\d{4}$/ }) + await expect(popper.locator(".react-datepicker__year-text")).toHaveCount( + 20, + ); + const years = await popper + .locator(".react-datepicker__year-text") .allTextContents(); - expect(yearLabels).toHaveLength(20); - expect(yearLabels.map((y) => Number.parseInt(y, 10))).toEqual( + expect(years.map((y) => Number.parseInt(y.trim(), 10))).toEqual( expect.arrayContaining([1981, 2000]), ); - const fitsSidebar = await page.evaluate(() => { - const sidebarEl = document.querySelector("#sidebar"); - const popperEl = document.querySelector( - ".react-datepicker-popper:has(.lifeBirthDatePicker)", - ); - if ( - !(sidebarEl instanceof HTMLElement) || - !(popperEl instanceof HTMLElement) - ) { - return { ok: false, reason: "missing nodes" }; - } - const sb = sidebarEl.getBoundingClientRect(); - const caption = popperEl.querySelector( - ".react-datepicker__current-month, [class*='caption']", - ); - const grid = popperEl.querySelector( - ".react-datepicker__year", - ); - const check = (el: HTMLElement | null, name: string) => { - if (!el) return `${name} missing`; - const b = el.getBoundingClientRect(); - if (b.left < sb.left - 1 || b.right > sb.right + 1) { - return `${name} overflows sidebar (${b.left.toFixed(0)}..${b.right.toFixed(0)} vs ${sb.left.toFixed(0)}..${sb.right.toFixed(0)})`; - } - return null; - }; - const err = check(caption, "caption") ?? check(grid, "year grid") ?? null; - return err ? { ok: false as const, reason: err } : { ok: true as const }; + const withinViewport = await popper.evaluate((el) => { + const b = el.getBoundingClientRect(); + return b.left >= 0 && b.right <= window.innerWidth; }); - - expect(fitsSidebar).toEqual({ ok: true }); - await expect(sidebar.getByText("Years 1981 to 2000")).toBeVisible(); + expect(withinViewport).toBe(true); + await expect(popper.getByText("Years 1981 to 2000")).toBeVisible(); }); }