A11Y: modal surfaces leave the app background live despite their inert contract #848

Open
opened 2026-10-02 13:29:28 +00:00 by kayg · 21 comments
Owner

Context

Source accessibility review of origin/dev at c4a61e8cf090170f35b1bed3350d9de20c83ecd5, job rev-a11y, assigned #427. Shared modal surfaces claim an inert app background in their comments, but the app shell does not apply that state.

Evidence

  • apps/web/src/lib/overlay/state.svelte.ts:3–5 says the layout sets inert while an overlay is open. Its open() at :50 only updates a count and dismisses transient UI.
  • apps/web/src/routes/+layout.svelte:796–821 renders the app frame, main header and route-content without an inert binding. The only consumers of overlay.active that remove controls from interaction are the bottom Tab Bar and Primary Pill (:907–908).
  • apps/web/src/lib/components/app-sidebar.svelte:451 makes the desktop sidebar inert only when collapsed, independently of modal state.
  • packages/ui/src/components/OverlaySurface.svelte:713–750 portals its surface and declares aria-modal="true". The portal (packages/ui/src/actions/portal.ts:24–37) moves the branch, but does not isolate siblings.
  • apps/web/src/lib/a11y/focusTrap.ts:228–229 listens for keydown only inside the surface. It has no document focusin guard. A background control that receives programmatic focus is outside that listener and can keep subsequent keyboard input.

The scrim and a correct Tab loop do not make the app background inert. The ARIA modal-dialog pattern requires underlying windows to be inert. The implementation also contradicts its own focus-restore comments. This is a confirmed source contract gap; a full app assistive-technology run is still needed to identify which readers expose the background despite aria-modal.

Expected and exact fix

Restore the shared modal-background contract. Mark the app content behind a modal inert through its full open/close lifetime. Preserve the top modal, its nested menus and the explicitly supported toast action path. Restore prior inert states and focus on the last close. Apply the same policy to stacked dialogs so a lower dialog cannot take focus through its own effects. Keep modal state in one shared owner, and update the stale comments together with the fix. Do not add per-component focus policing.

Test idea

Open Search over a route with focusable content and an expanded sidebar. Assert that app background controls are inert and cannot take focus. Trigger a delayed background focus effect and ensure focus stays in the modal. Open a confirmation over Settings and check that only the upper dialog accepts focus; close it and check restoration to Settings, then close Settings and check the opener. Check nested menus, the toast action path, Escape and reduced motion. Complete a production screen-reader browse/touch exploration run at each required device width.

Duplicate search

Searches: inert and "modal background" (the latter had no results). #740 records hidden focus candidates and initial focus; it does not restore the shell's modal background state. One issue is filed for the modal shell family.

# Context Source accessibility review of `origin/dev` at `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`, job rev-a11y, assigned #427. Shared modal surfaces claim an inert app background in their comments, but the app shell does not apply that state. # Evidence - `apps/web/src/lib/overlay/state.svelte.ts:3–5` says the layout sets `inert` while an overlay is open. Its `open()` at `:50` only updates a count and dismisses transient UI. - `apps/web/src/routes/+layout.svelte:796–821` renders the app frame, main header and route-content without an inert binding. The only consumers of `overlay.active` that remove controls from interaction are the bottom Tab Bar and Primary Pill (`:907–908`). - `apps/web/src/lib/components/app-sidebar.svelte:451` makes the desktop sidebar inert only when collapsed, independently of modal state. - `packages/ui/src/components/OverlaySurface.svelte:713–750` portals its surface and declares `aria-modal="true"`. The portal (`packages/ui/src/actions/portal.ts:24–37`) moves the branch, but does not isolate siblings. - `apps/web/src/lib/a11y/focusTrap.ts:228–229` listens for keydown only inside the surface. It has no document focusin guard. A background control that receives programmatic focus is outside that listener and can keep subsequent keyboard input. The scrim and a correct Tab loop do not make the app background inert. The ARIA modal-dialog pattern requires underlying windows to be inert. The implementation also contradicts its own focus-restore comments. This is a confirmed source contract gap; a full app assistive-technology run is still needed to identify which readers expose the background despite aria-modal. # Expected and exact fix Restore the shared modal-background contract. Mark the app content behind a modal inert through its full open/close lifetime. Preserve the top modal, its nested menus and the explicitly supported toast action path. Restore prior inert states and focus on the last close. Apply the same policy to stacked dialogs so a lower dialog cannot take focus through its own effects. Keep modal state in one shared owner, and update the stale comments together with the fix. Do not add per-component focus policing. # Test idea Open Search over a route with focusable content and an expanded sidebar. Assert that app background controls are inert and cannot take focus. Trigger a delayed background focus effect and ensure focus stays in the modal. Open a confirmation over Settings and check that only the upper dialog accepts focus; close it and check restoration to Settings, then close Settings and check the opener. Check nested menus, the toast action path, Escape and reduced motion. Complete a production screen-reader browse/touch exploration run at each required device width. # Duplicate search Searches: `inert` and `"modal background"` (the latter had no results). #740 records hidden focus candidates and initial focus; it does not restore the shell's modal background state. One issue is filed for the modal shell family.
Author
Owner

Final audit probe, exit 0, using one Chromium browser and no product build:

REPRODUCED #740: hidden explicit initial target leaves focus outside the trap.
REPRODUCED #740: a hidden last candidate lets Tab leave the trap.
REPRODUCED #848 supporting behavior: the shared trap permits programmatic background focus.
REPRODUCED #831: a category label with a space resolves no error-description ID references.

The #848 case tests the actual shared focusTrap action with visible controls, not the complete overlay. Source evidence in the issue establishes the missing shell inert binding. The #831 case checks browser ID-reference parsing with a neutral label, not the full category editor. These probes support the findings and do not replace production acceptance checks.

Final audit probe, exit 0, using one Chromium browser and no product build: ```text REPRODUCED #740: hidden explicit initial target leaves focus outside the trap. REPRODUCED #740: a hidden last candidate lets Tab leave the trap. REPRODUCED #848 supporting behavior: the shared trap permits programmatic background focus. REPRODUCED #831: a category label with a space resolves no error-description ID references. ``` The #848 case tests the actual shared focusTrap action with visible controls, not the complete overlay. Source evidence in the issue establishes the missing shell inert binding. The #831 case checks browser ID-reference parsing with a neutral label, not the full category editor. These probes support the findings and do not replace production acceptance checks.
Author
Owner

Starting implementation on branch job/a11yfix2, based on c4a61e8cf0 (origin/dev). Scope: shared modal background inertness for #848, with the related round 7b component findings in this job prompt being mapped against their existing shared components and tests.

Starting implementation on branch job/a11yfix2, based on c4a61e8cf090170f35b1bed3350d9de20c83ecd5 (origin/dev). Scope: shared modal background inertness for #848, with the related round 7b component findings in this job prompt being mapped against their existing shared components and tests.
Author
Owner

#846 phone repro found a touch/hover collision in the shared Live Photo control. On a 390 px touch context, the sequence was pointerenter → pointerdown → pointerup → mouseenter → click; the compatibility mouseenter started playback, then the button click toggled it off (aria-pressed=false, video paused). The stage's hold listener also received presses that began on the badge.

I added a red/green regression test for both starting and stopping from touch, then changed the shared control to use non-touch pointer hover and to ignore badge-originated stage holds. Focused component tests now pass (3 tests). The production Photos screenshot matrix is running against the rebuilt SPA; I will report its result in the final update.

#846 phone repro found a touch/hover collision in the shared Live Photo control. On a 390 px touch context, the sequence was `pointerenter → pointerdown → pointerup → mouseenter → click`; the compatibility `mouseenter` started playback, then the button click toggled it off (`aria-pressed=false`, video paused). The stage's hold listener also received presses that began on the badge. I added a red/green regression test for both starting and stopping from touch, then changed the shared control to use non-touch pointer hover and to ignore badge-originated stage holds. Focused component tests now pass (3 tests). The production Photos screenshot matrix is running against the rebuilt SPA; I will report its result in the final update.
Author
Owner

#848 status report — partial; stopped at the owner’s four-hour job limit

Branch: job/a11yfix2
Head: c29b72ef39f6d95db219dc7ea1e46aaba240e868

Built

  • #848: made the app background inert for shared modal surfaces, including the skip link and nested overlay ownership.
  • #830: added keyboard and named-button image panning. Forwarded viewer shortcuts from the dialog root because pointer zoom can leave focus there.
  • #831: generated stable category error IDs for duplicate display labels.
  • #845: added the full-size Activity heatmap touch selector/readout and a screen-reader table alternative.
  • #846: kept the LIVE badge at 28 px with a 44 px touch target. Fixed touch-generated hover events that started playback immediately before the badge click toggled it off.

Key files: packages/ui/src/components/OverlaySurface.svelte, packages/ui/src/components/viewer/{QuickLook,ImageView,LiveMotion}.svelte, apps/web/src/lib/overlay/state.svelte.ts, apps/web/src/lib/components/analytics/{BklitAnalytics,BklitAnalyticsHeatmapMarks,BklitHeatmapTouchReadout}.tsx, apps/web/src/lib/components/money/AssignedCell.svelte, their component tests, and apps/web/e2e/{modal-inert-848,photos,analytics,money}.mjs.

Verification output

bun run build exited 0:

✓ built in 17.35s

Focused component tests after the last UI change:

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/a11yfix2/apps/web

Not implemented: Window's scrollTo() method

 Test Files  2 passed (2)
      Tests  3 passed (3)
   Start at  21:07:54
   Duration  7.70s (transform 69%, environment 16%, tests 5%, import 4%)

The modal E2E passed all six viewport/theme cases:

modal inert e2e passed: 390, 820 and 1440 px in light and dark with macOS platform emulation; screenshots saved in /home/kayg/Developer/calternal-wt/a11yfix2/artifacts/a11yfix2/modal-inert-848

The Photos production screenshot E2E passed the pan and LIVE badge matrices:

PHOTOS SCREENSHOTS PASSED
CSP REPORTS photos: 0 across 2 pages

The Analytics screenshot-only run produced artifacts but failed four layout/readout checks:

4 check(s) failed:
- 820px paper Year heatmap layout: the time heatmap has visible weekday labels: {"dayLabels":[],"hourLabels":[],"calendarShellWidth":0,"calendarInnerWidth":0,"calendarSvgWidth":0,"calendarCellWidth":0}
- 390px paper Year heatmap layout: waitFor: Timeout 30000ms exceeded.
Call log:
  - waiting for locator('.bklit-time-heatmap').getByRole('combobox', { name: 'Choose a value in Activity heatmap' }) to be visible

- 820px tokyo-night Year heatmap layout: waitFor: Timeout 30000ms exceeded.
Call log:
  - waiting for locator('.bklit-time-heatmap').getByRole('combobox', { name: 'Choose a value in Activity heatmap' }) to be visible

- 390px tokyo-night Year heatmap layout: waitFor: Timeout 30000ms exceeded.
Call log:
  - waiting for locator('.bklit-time-heatmap').getByRole('combobox', { name: 'Choose a value in Activity heatmap' }) to be visible

UX gaps closed

Modal background keyboard/screen-reader access is blocked while overlays are open; image pan works with keyboard, buttons and drag; duplicate Money labels retain unique error descriptions; the heatmap exposes touch values and table data; and touch taps can start and stop Live Photo playback.

Remaining work and known gaps

  • Diagnose and fix or correct the Analytics year-view screenshot/layout failures above.
  • Run bun run check, the full bun run test, and the Money E2E.
  • Run and record the Analytics and Photos performance profiles.
  • Attach the generated screenshots to this issue. Current local evidence is under artifacts/a11yfix2/{modal-inert-848,photos,analytics}; the Money evidence set was not run.
  • Run the requested cleanup (cargo clean and remove web build output).

Decisions

  • The shared reduced-motion token is 0.001ms (1e-6s as computed by Chromium), so the pan assertion accepts that shared near-zero value.
  • Touch compatibility mouse events do not count as hover. The badge is excluded from the stage hold gesture so one tap remains one button action.
  • The screenshot tests emulate macOS via the shared Playwright helper.

No pushes, deploys, merges, or issue closure were performed.

#848 status report — partial; stopped at the owner’s four-hour job limit Branch: `job/a11yfix2` Head: `c29b72ef39f6d95db219dc7ea1e46aaba240e868` ## Built - #848: made the app background inert for shared modal surfaces, including the skip link and nested overlay ownership. - #830: added keyboard and named-button image panning. Forwarded viewer shortcuts from the dialog root because pointer zoom can leave focus there. - #831: generated stable category error IDs for duplicate display labels. - #845: added the full-size Activity heatmap touch selector/readout and a screen-reader table alternative. - #846: kept the LIVE badge at 28 px with a 44 px touch target. Fixed touch-generated hover events that started playback immediately before the badge click toggled it off. Key files: `packages/ui/src/components/OverlaySurface.svelte`, `packages/ui/src/components/viewer/{QuickLook,ImageView,LiveMotion}.svelte`, `apps/web/src/lib/overlay/state.svelte.ts`, `apps/web/src/lib/components/analytics/{BklitAnalytics,BklitAnalyticsHeatmapMarks,BklitHeatmapTouchReadout}.tsx`, `apps/web/src/lib/components/money/AssignedCell.svelte`, their component tests, and `apps/web/e2e/{modal-inert-848,photos,analytics,money}.mjs`. ## Verification output `bun run build` exited 0: ```text ✓ built in 17.35s ``` Focused component tests after the last UI change: ```text RUN v5.0.1 /home/kayg/Developer/calternal-wt/a11yfix2/apps/web Not implemented: Window's scrollTo() method Test Files 2 passed (2) Tests 3 passed (3) Start at 21:07:54 Duration 7.70s (transform 69%, environment 16%, tests 5%, import 4%) ``` The modal E2E passed all six viewport/theme cases: ```text modal inert e2e passed: 390, 820 and 1440 px in light and dark with macOS platform emulation; screenshots saved in /home/kayg/Developer/calternal-wt/a11yfix2/artifacts/a11yfix2/modal-inert-848 ``` The Photos production screenshot E2E passed the pan and LIVE badge matrices: ```text PHOTOS SCREENSHOTS PASSED CSP REPORTS photos: 0 across 2 pages ``` The Analytics screenshot-only run produced artifacts but failed four layout/readout checks: ```text 4 check(s) failed: - 820px paper Year heatmap layout: the time heatmap has visible weekday labels: {"dayLabels":[],"hourLabels":[],"calendarShellWidth":0,"calendarInnerWidth":0,"calendarSvgWidth":0,"calendarCellWidth":0} - 390px paper Year heatmap layout: waitFor: Timeout 30000ms exceeded. Call log: - waiting for locator('.bklit-time-heatmap').getByRole('combobox', { name: 'Choose a value in Activity heatmap' }) to be visible - 820px tokyo-night Year heatmap layout: waitFor: Timeout 30000ms exceeded. Call log: - waiting for locator('.bklit-time-heatmap').getByRole('combobox', { name: 'Choose a value in Activity heatmap' }) to be visible - 390px tokyo-night Year heatmap layout: waitFor: Timeout 30000ms exceeded. Call log: - waiting for locator('.bklit-time-heatmap').getByRole('combobox', { name: 'Choose a value in Activity heatmap' }) to be visible ``` ## UX gaps closed Modal background keyboard/screen-reader access is blocked while overlays are open; image pan works with keyboard, buttons and drag; duplicate Money labels retain unique error descriptions; the heatmap exposes touch values and table data; and touch taps can start and stop Live Photo playback. ## Remaining work and known gaps - Diagnose and fix or correct the Analytics year-view screenshot/layout failures above. - Run `bun run check`, the full `bun run test`, and the Money E2E. - Run and record the Analytics and Photos performance profiles. - Attach the generated screenshots to this issue. Current local evidence is under `artifacts/a11yfix2/{modal-inert-848,photos,analytics}`; the Money evidence set was not run. - Run the requested cleanup (`cargo clean` and remove web build output). ## Decisions - The shared reduced-motion token is `0.001ms` (`1e-6s` as computed by Chromium), so the pan assertion accepts that shared near-zero value. - Touch compatibility mouse events do not count as hover. The badge is excluded from the stage hold gesture so one tap remains one button action. - The screenshot tests emulate macOS via the shared Playwright helper. No pushes, deploys, merges, or issue closure were performed.
Author
Owner

Starting the #848 send-back round on job/a11yfix2. Current HEAD: c29b72ef39. Current merge base with origin/dev: 440e19dce2. I will investigate the blank Analytics cards, category label overlap, focus ring mismatch, and missing evidence, then commit each fix separately.

Starting the #848 send-back round on job/a11yfix2. Current HEAD: c29b72ef39f6d95db219dc7ea1e46aaba240e868. Current merge base with origin/dev: 440e19dce23040ac8ebaae88f0469b6535b1afcb. I will investigate the blank Analytics cards, category label overlap, focus ring mismatch, and missing evidence, then commit each fix separately.
Author
Owner

Resuming on branch job/a11yfix2 after the build VM reboot. HEAD: c29b72ef39. Merge base with origin/dev: 440e19dce2. Continuing the latest send-back checklist: diagnose Analytics year-view failures, inspect Money category label overlap and focus ring mismatch, and complete required evidence and focused gates. The #848 modal implementation and its six-case screenshot run are already complete.

Resuming on branch job/a11yfix2 after the build VM reboot. HEAD: c29b72ef39f6d95db219dc7ea1e46aaba240e868. Merge base with origin/dev: 440e19dce23040ac8ebaae88f0469b6535b1afcb. Continuing the latest send-back checklist: diagnose Analytics year-view failures, inspect Money category label overlap and focus ring mismatch, and complete required evidence and focused gates. The #848 modal implementation and its six-case screenshot run are already complete.
Author
Owner

Finding during resumed verification: is absent, and neither assigned job target nor the shared target contains a binary. The #848/Analytics real-server screenshot repro therefore cannot use the shared prebuilt server; I am building the current branch's in the assigned job target with the required 3 build jobs before continuing.

Finding during resumed verification: is absent, and neither assigned job target nor the shared target contains a binary. The #848/Analytics real-server screenshot repro therefore cannot use the shared prebuilt server; I am building the current branch's in the assigned job target with the required 3 build jobs before continuing.
Author
Owner

Correction to the preceding progress note: the shell interpreted its Markdown backticks and omitted the names. The shared-server CURRENT file at /mnt/hdd/targets/shared-server/CURRENT is absent, and neither assigned job target nor shared target contains the calternal-server binary. I am building the current branch's server in the assigned job target with 3 build jobs before continuing the real-server screenshot repro.

Correction to the preceding progress note: the shell interpreted its Markdown backticks and omitted the names. The shared-server CURRENT file at /mnt/hdd/targets/shared-server/CURRENT is absent, and neither assigned job target nor shared target contains the calternal-server binary. I am building the current branch's server in the assigned job target with 3 build jobs before continuing the real-server screenshot repro.
Author
Owner

Finding: Money month rows sit inside a table with overflow:hidden, while AssignedCell used the outer focus-ring offset. I added the shared inset token to AssignedCell and expanded the real screenshot E2E to check the ring and category/Assigned column bounds at 390, 820 and 1440 px in both themes. Focused AssignedCell tests pass (4/4); the production E2E checks remain to run.

Finding: Money month rows sit inside a table with overflow:hidden, while AssignedCell used the outer focus-ring offset. I added the shared inset token to AssignedCell and expanded the real screenshot E2E to check the ring and category/Assigned column bounds at 390, 820 and 1440 px in both themes. Focused AssignedCell tests pass (4/4); the production E2E checks remain to run.
Author
Owner

Build finding: the required local server build stops in the existing Mail Plugin code. cache.rs imports mail_read_marking, but cache/store.rs gates that definition with #[cfg(test)], so cargo build -p calternal-server fails before producing a server. I am checking origin/dev and call sites before deciding whether a minimal build-only workaround is safe.

Build finding: the required local server build stops in the existing Mail Plugin code. cache.rs imports mail_read_marking, but cache/store.rs gates that definition with #[cfg(test)], so cargo build -p calternal-server fails before producing a server. I am checking origin/dev and call sites before deciding whether a minimal build-only workaround is safe.
Author
Owner

Additional evidence: origin/dev has a production mail_read_marking getter, while this branch refactored it to cfg(test) for the #667 Undo tests and left it in the production pub use list. The build error is caused by that stale re-export on this branch. I will remove only the production re-export and keep the cfg(test) test re-export; this does not change Mail route behavior or its contract.

Additional evidence: origin/dev has a production mail_read_marking getter, while this branch refactored it to cfg(test) for the #667 Undo tests and left it in the production pub use list. The build error is caused by that stale re-export on this branch. I will remove only the production re-export and keep the cfg(test) test re-export; this does not change Mail route behavior or its contract.
Author
Owner

Build finding update: after removing the stale Mail export, the server reaches final link but fails because CARGO_NET_OFFLINE=true leaves ort-sys without libonnxruntime (undefined OrtGetApiBase). I am checking the existing host ORT cache to avoid a redundant download and will retry the build with its library path if present.

Build finding update: after removing the stale Mail export, the server reaches final link but fails because CARGO_NET_OFFLINE=true leaves ort-sys without libonnxruntime (undefined OrtGetApiBase). I am checking the existing host ORT cache to avoid a redundant download and will retry the build with its library path if present.
Author
Owner

ORT cache finding: the host already has the matching x86_64 static archive at /home/kayg/.cache/ort.pyke.io/dfbin/x86_64-unknown-linux-gnu/e454f710f8a49f53aa5b4ff51e3454ae1835777e431c6c35c5255ce6f205fd68/libonnxruntime.a. I will point ORT_LIB_PATH at that existing directory for the server build; no repository or dependency change is needed.

ORT cache finding: the host already has the matching x86_64 static archive at /home/kayg/.cache/ort.pyke.io/dfbin/x86_64-unknown-linux-gnu/e454f710f8a49f53aa5b4ff51e3454ae1835777e431c6c35c5255ce6f205fd68/libonnxruntime.a. I will point ORT_LIB_PATH at that existing directory for the server build; no repository or dependency change is needed.
Author
Owner

Analytics finding: the production screenshot run captured React error #310 in BklitAnalyticsHeatmapMarks. The component returned before its final useMemo while containerRef or marks were empty, then called that hook after the chart became ready. This violates React hook ordering and leaves Year heatmaps as Chart unavailable. I will move the memo before the conditional return and verify all required viewport/theme screenshots against the real server.

Analytics finding: the production screenshot run captured React error #310 in BklitAnalyticsHeatmapMarks. The component returned before its final useMemo while containerRef or marks were empty, then called that hook after the chart became ready. This violates React hook ordering and leaves Year heatmaps as Chart unavailable. I will move the memo before the conditional return and verify all required viewport/theme screenshots against the real server.
Author
Owner

Analytics follow-up: the earlier 390 px Paper selector timeout did not reproduce on the same real-server production screenshot path. A fresh run passed all six viewport/theme cases (390, 820 and 1440 px; Paper and Tokyo Night), including the touch selector, its 44 px target, selection/readout update and real-value table checks. I added timeout diagnostics to the focused E2E so a future failure reports the selector box, computed visibility, coarse-pointer state, chart mount state and hidden ancestor. Screenshot evidence is under artifacts/a11yfix2/analytics-diagnostic-resume.

Analytics follow-up: the earlier 390 px Paper selector timeout did not reproduce on the same real-server production screenshot path. A fresh run passed all six viewport/theme cases (390, 820 and 1440 px; Paper and Tokyo Night), including the touch selector, its 44 px target, selection/readout update and real-value table checks. I added timeout diagnostics to the focused E2E so a future failure reports the selector box, computed visibility, coarse-pointer state, chart mount state and hidden ancestor. Screenshot evidence is under `artifacts/a11yfix2/analytics-diagnostic-resume`.
Author
Owner

Money screenshot E2E finding: the first run stopped before screenshots because applyScheme sent { mode, location: null } to the Appearance API and received 422. AutoSchemeSettings in crates/calternal-server/src/appearance.rs has deny_unknown_fields and now contains only mode; location belongs to the separate Location settings. I am removing the obsolete location field from the test request and keeping the existing expected 200 status unchanged.

Money screenshot E2E finding: the first run stopped before screenshots because `applyScheme` sent `{ mode, location: null }` to the Appearance API and received 422. `AutoSchemeSettings` in `crates/calternal-server/src/appearance.rs` has `deny_unknown_fields` and now contains only `mode`; location belongs to the separate Location settings. I am removing the obsolete `location` field from the test request and keeping the existing expected 200 status unchanged.
Author
Owner

Money focus-ring E2E finding: after the Appearance payload fix, the label overlap assertion passed, but the ring check saw outline: none and data-input=pointer behavior after Playwright .fill() plus Enter. The shared input-modality policy deliberately treats Enter on an editable field as text entry and does not enable keyboard focus styling. I am updating the E2E to use Tab and Shift+Tab before checking the still-invalid Assigned field, so it measures the real keyboard-navigation state without changing the expected ring or error behavior.

Money focus-ring E2E finding: after the Appearance payload fix, the label overlap assertion passed, but the ring check saw `outline: none` and `data-input=pointer` behavior after Playwright `.fill()` plus Enter. The shared input-modality policy deliberately treats Enter on an editable field as text entry and does not enable keyboard focus styling. I am updating the E2E to use Tab and Shift+Tab before checking the still-invalid Assigned field, so it measures the real keyboard-navigation state without changing the expected ring or error behavior.
Author
Owner

Production screenshots from the real app build and local server. Playwright reports macOS platform values (MacIntel / macOS). The captures cover phone 390 px, tablet 820 px and desktop 1440 px in light and dark themes.

Modal inertness

Photos keyboard pan, Live Photo touch target, and scrubber states

Analytics week, month and year views

Money category validation and Assigned focus ring

Production screenshots from the real app build and local server. Playwright reports macOS platform values (`MacIntel` / `macOS`). The captures cover phone 390 px, tablet 820 px and desktop 1440 px in light and dark themes. ## Modal inertness - [desktop-1440-paper-light.png](https://git.kayg.org/attachments/c087b5e9-9050-43eb-979c-377e8872f13a) - [desktop-1440-tokyo-night-dark.png](https://git.kayg.org/attachments/e5a2bf51-9681-4d9a-92c4-3eca77f91d8a) - [phone-390-paper-light.png](https://git.kayg.org/attachments/cec8d731-ab2b-4cdc-9360-6bdafabc3e77) - [phone-390-tokyo-night-dark.png](https://git.kayg.org/attachments/0b8982a8-2736-4e66-a9d9-e3e65365be47) - [tablet-820-paper-light.png](https://git.kayg.org/attachments/79b7068e-53a8-40bb-8ee2-266f63a06c92) - [tablet-820-tokyo-night-dark.png](https://git.kayg.org/attachments/23451fee-45c9-4100-b93c-404f3c1d0a94) ## Photos keyboard pan, Live Photo touch target, and scrubber states - [image-pan-1440-paper-white.png](https://git.kayg.org/attachments/4d0aed7f-528e-432c-a849-2cd513d56f3e) - [image-pan-1440-tokyo-night.png](https://git.kayg.org/attachments/d36061f1-bcca-4505-ace6-b3ef08cf062c) - [image-pan-390-paper-white.png](https://git.kayg.org/attachments/6fa5749e-2b93-47de-9862-ec514965e2f7) - [image-pan-390-tokyo-night.png](https://git.kayg.org/attachments/fccccb30-99fd-4f62-9539-d357ce0bc86d) - [image-pan-820-paper-white.png](https://git.kayg.org/attachments/fbfeaf60-c967-4ca9-9393-3ac701e01a9b) - [image-pan-820-tokyo-night.png](https://git.kayg.org/attachments/cfa024cc-41cf-459f-9cdb-475609d0d1fe) - [live-photo-badge-1440-paper-white.png](https://git.kayg.org/attachments/326c104f-a91b-4290-8c7c-59eb3e096381) - [live-photo-badge-1440-tokyo-night.png](https://git.kayg.org/attachments/bb0b385a-4d12-4984-ac35-e50571a62645) - [live-photo-badge-390-paper-white.png](https://git.kayg.org/attachments/50da9ed8-b9b8-4055-8c48-02827d0193c1) - [live-photo-badge-390-tokyo-night.png](https://git.kayg.org/attachments/cb119cd7-fbcb-4067-b831-ca43e75ceebe) - [live-photo-badge-820-paper-white.png](https://git.kayg.org/attachments/97f634fd-c12a-4eed-8911-74a35e1cfd72) - [live-photo-badge-820-tokyo-night.png](https://git.kayg.org/attachments/f0da4890-c511-48ca-9510-8410fdbd6b4b) - [photos-scrubber-more-1440-paper-white.png](https://git.kayg.org/attachments/d84a8493-6346-48ee-8797-8867aa249ab1) - [photos-scrubber-more-1440-tokyo-night.png](https://git.kayg.org/attachments/b49d8316-0ba9-49c8-9145-e8061587ca9c) - [photos-scrubber-more-390-paper-white.png](https://git.kayg.org/attachments/856cc137-a0b9-468e-942f-c5d9b50a2798) - [photos-scrubber-more-390-tokyo-night.png](https://git.kayg.org/attachments/cc782501-2a65-4b74-9438-d00b357a31bd) - [photos-scrubber-more-820-paper-white.png](https://git.kayg.org/attachments/1748dd85-2ce8-425d-b369-c71c07eb2464) - [photos-scrubber-more-820-tokyo-night.png](https://git.kayg.org/attachments/7c7b354d-f877-4949-b7ae-622a9d31daa9) - [photos-scrubber-selected-1440-paper-white.png](https://git.kayg.org/attachments/6bccca2b-e3aa-4b50-8813-49ebb8fa9f63) - [photos-scrubber-selected-1440-tokyo-night.png](https://git.kayg.org/attachments/8f3dad70-30b5-447d-bbb6-3124da066b1d) - [photos-scrubber-selected-390-paper-white.png](https://git.kayg.org/attachments/dc08d03f-08d9-4d8c-b872-0343842bf115) - [photos-scrubber-selected-390-tokyo-night.png](https://git.kayg.org/attachments/91d3bdbd-5a2c-4b64-a94d-28c431d47692) - [photos-scrubber-selected-820-paper-white.png](https://git.kayg.org/attachments/26473b3d-e4c2-4387-9427-cdab98ba253d) - [photos-scrubber-selected-820-tokyo-night.png](https://git.kayg.org/attachments/c1cc5b33-8d9f-43ac-a6c5-de187a4ef398) ## Analytics week, month and year views - [analytics-month-paper-1440.png](https://git.kayg.org/attachments/e376ba43-f555-4578-b324-8a1402e940e7) - [analytics-month-paper-390.png](https://git.kayg.org/attachments/3a4679c6-0dda-4160-bf92-d99550ee306d) - [analytics-month-paper-820.png](https://git.kayg.org/attachments/0307f484-0c8d-4fc2-89b5-57d687eb78f2) - [analytics-month-tokyo-night-1440.png](https://git.kayg.org/attachments/e0c118e7-0148-4f07-b1c5-482bcd40c3cf) - [analytics-month-tokyo-night-390.png](https://git.kayg.org/attachments/51e3225f-d71a-4c66-9157-ad081ddb758e) - [analytics-month-tokyo-night-820.png](https://git.kayg.org/attachments/cbef568f-14b7-4866-bcc8-619102d1d486) - [analytics-week-paper-1440.png](https://git.kayg.org/attachments/1a66d0f3-8f6b-4836-ad95-9dbbff82093a) - [analytics-week-paper-390.png](https://git.kayg.org/attachments/8a5c61d7-0e9e-48a2-94f3-843e78d91d62) - [analytics-week-paper-820.png](https://git.kayg.org/attachments/0c3f657f-5aa1-4d35-8dca-a7536ced3e6f) - [analytics-week-tokyo-night-1440.png](https://git.kayg.org/attachments/a24be0d5-2f16-4d03-883f-23e975cfd7cb) - [analytics-week-tokyo-night-390.png](https://git.kayg.org/attachments/2b4c9c11-1dc2-401e-bec3-ad8278fa6527) - [analytics-week-tokyo-night-820.png](https://git.kayg.org/attachments/cc9b439e-9703-4ce8-8ae2-5637c1d3b6c8) - [analytics-year-paper-1440.png](https://git.kayg.org/attachments/9d30025c-d94e-44e3-aa49-42c49dc72088) - [analytics-year-paper-390.png](https://git.kayg.org/attachments/ca1e51c9-7351-448b-a9b8-9cd2eb56dc6a) - [analytics-year-paper-820.png](https://git.kayg.org/attachments/16509453-05d6-4baf-b4c0-ab3ff8b8deb8) - [analytics-year-tokyo-night-1440.png](https://git.kayg.org/attachments/73f9b7c8-049d-45cc-90d2-727fdfab0292) - [analytics-year-tokyo-night-390.png](https://git.kayg.org/attachments/998dd072-08cd-4538-8d1f-7f0edf025642) - [analytics-year-tokyo-night-820.png](https://git.kayg.org/attachments/600cf664-a1e3-40a8-9f95-8ecd0d8b1448) ## Money category validation and Assigned focus ring - [money-category-error-dark-1440.png](https://git.kayg.org/attachments/c0ea2a61-b883-4c4d-9b19-425712b138b4) - [money-category-error-dark-390.png](https://git.kayg.org/attachments/af0938d3-1411-47a5-b787-2fd0cfaa162b) - [money-category-error-dark-820.png](https://git.kayg.org/attachments/12499944-efcd-49e4-8b68-de1a25364e6c) - [money-category-error-light-1440.png](https://git.kayg.org/attachments/2f4e3c04-e585-4577-8513-b0ccf8ce1b34) - [money-category-error-light-390.png](https://git.kayg.org/attachments/f5889a3e-499a-463f-a7eb-80283b73bcb3) - [money-category-error-light-820.png](https://git.kayg.org/attachments/db9311a5-53f0-4001-883f-f622d2f0f0cf)
Author
Owner

Web gate findings from this round:

  • bun run check exits 1 in check-focus-tokens.mjs: it reports local focus-ring declarations in apps/web/src/routes/settings/connected-accounts/ConnectedAccountsSection.svelte:792 and packages/ui/src/components/TextInput.svelte:32. Both files are unchanged from the merged base (a518546ca).
  • Running bunx --no-install svelte-check --tsconfig ./tsconfig.json directly also exits 1 on apps/web/src/routes/search/+page.svelte:15 and :19 because to may be null. That file is unchanged from the merged base.
  • cargo fmt --check passes. The affected Mail crate Clippy and test gates passed earlier in this resume.

I am leaving the unrelated baseline files unchanged and will include the gate output in the final report.

Web gate findings from this round: - `bun run check` exits 1 in `check-focus-tokens.mjs`: it reports local focus-ring declarations in `apps/web/src/routes/settings/connected-accounts/ConnectedAccountsSection.svelte:792` and `packages/ui/src/components/TextInput.svelte:32`. Both files are unchanged from the merged base (`a518546ca`). - Running `bunx --no-install svelte-check --tsconfig ./tsconfig.json` directly also exits 1 on `apps/web/src/routes/search/+page.svelte:15` and `:19` because `to` may be null. That file is unchanged from the merged base. - `cargo fmt --check` passes. The affected Mail crate Clippy and test gates passed earlier in this resume. I am leaving the unrelated baseline files unchanged and will include the gate output in the final report.
Author
Owner

Comment review finding: two changed component docs named competing products (YNAB in AssignedCell and the Photos app in LiveMotion). I am removing those names from the comments while keeping the documented Money keyboard behavior and Live Photo pointer/touch rules intact.

Comment review finding: two changed component docs named competing products (`YNAB` in AssignedCell and the Photos app in LiveMotion). I am removing those names from the comments while keeping the documented Money keyboard behavior and Live Photo pointer/touch rules intact.
Author
Owner

Final report — #848

Branch: job/a11yfix2
Head SHA: d7d5ffa5424a94f80b3fc43d1c17746459bf3394

Built

  • Added one shared modal stack owner. It makes the skip link and app frame inert behind open surfaces, keeps lower surfaces inert, restores prior inert states, and lets only the top surface own focus.
  • Added Quick Look image panning by Shift+Arrow and named buttons. Bare arrows still change the selected item.
  • Kept the Live Photo badge compact while giving it a 44 px touch target. Touch compatibility events no longer start hover playback or trigger the stage hold.
  • Added an Activity heatmap value selector, live readout, and screen-reader table. Fixed React hook ordering while the year chart mounts. The Analytics E2E now prints mount and visibility details if the touch control is missing.
  • Kept Money category error IDs unique and moved the Assigned focus ring inside the clipped row. Updated the Money screenshot setup to send the current Appearance request shape and to enter keyboard modality before measuring the ring.
  • Removed the stale production-only Mail getter re-export so the local server builds; Mail routes still read the preference value and revision together.

Files

Modal state and focus: apps/web/src/lib/overlay/state.svelte.ts, apps/web/src/lib/a11y/focusTrap.ts, apps/web/src/routes/+layout.svelte, packages/ui/src/components/OverlaySurface.svelte, and their focused tests and apps/web/e2e/modal-inert-848.mjs.

Photos: packages/ui/src/components/viewer/{QuickLook,ImageView,LiveMotion}.svelte, apps/web/src/lib/shortcuts/registry.ts, their focused tests, and apps/web/e2e/photos.mjs.

Analytics: apps/web/src/lib/components/analytics/{BklitAnalytics,BklitAnalyticsHeatmapMarks,BklitHeatmapTouchReadout}.tsx, its focused test, and apps/web/e2e/analytics.mjs.

Money and build support: apps/web/src/lib/components/money/AssignedCell.svelte, its focused test, apps/web/e2e/money.mjs, and crates/plugins/mail/src/cache.rs.

UX gaps closed

Modal background controls and lower dialogs cannot take focus while a higher surface is active. Quick Look supports keyboard and button panning. A Live Photo tap is one action. Heatmap values work through a full-size touch control and have a complete table alternative. Money labels do not overlap Assigned inputs, duplicate labels keep distinct error descriptions, and keyboard navigation shows the shared inset ring.

The production screenshots use a real local server, macOS Playwright platform values, 390/820/1440 px, and light/dark themes. 54 PNGs are attached to this issue and linked in the screenshot comment. No screenshot files are committed.

UX gaps left

A real screen-reader browse and touch exploration on macOS was not run in this job. The merge round should check modal browse order and focus restoration with VoiceOver and the other supported assistive technology clients.

Gate output (verbatim excerpts)

cargo fmt --check exited 0 with no stdout.

cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings exited 0:

Finished dev profile [unoptimized + debuginfo] target(s) in 21m 50s

cargo test -p calternal-plugin-mail exited 0:

test result: ok. 48 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Focused modal Vitest run:

 Test Files  3 passed (3)
      Tests  18 passed (18)

Focused Analytics Vitest run: 3 files passed, 15 tests passed. Focused AssignedCell Vitest run: 1 file passed, 4 tests passed. bun run build exited 0.

Production E2Es exited 0:

modal inert e2e passed: 390, 820 and 1440 px in light and dark with macOS platform emulation; screenshots saved in /home/kayg/Developer/calternal-wt/a11yfix2/artifacts/a11yfix2/modal-inert-848
PHOTOS SCREENSHOTS PASSED
CSP REPORTS photos: 0 across 2 pages
analytics e2e: all checks passed
PASS the User flow wrote exact Markdown and the screens show the derived numbers
PASS repeated category labels retain unique screen-reader error descriptions
PASS captured 48 Money screenshots in /home/kayg/Developer/calternal-wt/a11yfix2/artifacts/money

bun run check exited 1. Output:

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-focus-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
Focus ring styles must use the shared tokens:
apps/web/src/routes/settings/connected-accounts/ConnectedAccountsSection.svelte:792: local focus ring styles are not allowed (outline: var(--focus-ring); outline-offset: 2px)
packages/ui/src/components/TextInput.svelte:32: local focus ring styles are not allowed (outline: var(--focus-ring); outline-offset: 1px)
error: script "check" exited with code 1

The two files reported by the focus-token check are unchanged from merge commit a518546ca. Running bunx --no-install svelte-check --tsconfig ./tsconfig.json directly also exited 1 on the unchanged Search route:

/home/kayg/Developer/calternal-wt/a11yfix2/apps/web/src/routes/search/+page.svelte:15:7
Error: 'to' is possibly 'null'. (ts)
/home/kayg/Developer/calternal-wt/a11yfix2/apps/web/src/routes/search/+page.svelte:19:22
Error: 'to' is possibly 'null'. (ts)
svelte-check found 2 errors and 0 warnings in 1 file

cargo clean output:

Removed 10997 files, 6.1GiB total

The generated apps/web/build and apps/web/.svelte-kit directories were removed. git diff --check passes and the worktree is clean.

Decisions

No new product decision was needed. The Money E2E now uses Tab/Shift+Tab because typing and Enter on an editable input do not change the shared input-modality state. The Appearance test request sends only mode, as required by the current server type; Location settings remain separate. The earlier 390 px Analytics selector timeout did not reproduce, so I added test diagnostics and made no layout change for it.

Known gaps and merge round

The web check remains blocked by the unchanged baseline focus-token findings and Search type errors above. The full Vitest suite, full E2E matrix, full adversarial matrix, real Mac client/VoiceOver check, and performance measurements were not run here under the 2026-10-02 verification policy. This issue does not change an API route or route contract. Performance profiles remain available in apps/web/e2e/analytics.mjs --perf and apps/web/e2e/photos-perf.mjs; do not measure them unless this is handled as a performance issue.

For the merge round, run cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace to verify the combined Rust tree; run bun run test and bun run test:e2e from apps/web; and run tests/adversarial/run.sh once against the combined real server to cover cross-User, authorization, hostile-input, and concurrency matrices. Also rerun bun run test:e2e:modal-inert-848, bun run test:e2e:photos, bun run test:e2e:analytics, and bun run test:e2e:money against the merged production build. The macOS check must use the locked Mac VM and prove real assistive-technology browse isolation and focus restoration.

No push, deployment, issue closure, or additional merge was performed.

# Final report — #848 Branch: `job/a11yfix2` Head SHA: `d7d5ffa5424a94f80b3fc43d1c17746459bf3394` ## Built - Added one shared modal stack owner. It makes the skip link and app frame inert behind open surfaces, keeps lower surfaces inert, restores prior inert states, and lets only the top surface own focus. - Added Quick Look image panning by Shift+Arrow and named buttons. Bare arrows still change the selected item. - Kept the Live Photo badge compact while giving it a 44 px touch target. Touch compatibility events no longer start hover playback or trigger the stage hold. - Added an Activity heatmap value selector, live readout, and screen-reader table. Fixed React hook ordering while the year chart mounts. The Analytics E2E now prints mount and visibility details if the touch control is missing. - Kept Money category error IDs unique and moved the Assigned focus ring inside the clipped row. Updated the Money screenshot setup to send the current Appearance request shape and to enter keyboard modality before measuring the ring. - Removed the stale production-only Mail getter re-export so the local server builds; Mail routes still read the preference value and revision together. ## Files Modal state and focus: `apps/web/src/lib/overlay/state.svelte.ts`, `apps/web/src/lib/a11y/focusTrap.ts`, `apps/web/src/routes/+layout.svelte`, `packages/ui/src/components/OverlaySurface.svelte`, and their focused tests and `apps/web/e2e/modal-inert-848.mjs`. Photos: `packages/ui/src/components/viewer/{QuickLook,ImageView,LiveMotion}.svelte`, `apps/web/src/lib/shortcuts/registry.ts`, their focused tests, and `apps/web/e2e/photos.mjs`. Analytics: `apps/web/src/lib/components/analytics/{BklitAnalytics,BklitAnalyticsHeatmapMarks,BklitHeatmapTouchReadout}.tsx`, its focused test, and `apps/web/e2e/analytics.mjs`. Money and build support: `apps/web/src/lib/components/money/AssignedCell.svelte`, its focused test, `apps/web/e2e/money.mjs`, and `crates/plugins/mail/src/cache.rs`. ## UX gaps closed Modal background controls and lower dialogs cannot take focus while a higher surface is active. Quick Look supports keyboard and button panning. A Live Photo tap is one action. Heatmap values work through a full-size touch control and have a complete table alternative. Money labels do not overlap Assigned inputs, duplicate labels keep distinct error descriptions, and keyboard navigation shows the shared inset ring. The production screenshots use a real local server, macOS Playwright platform values, 390/820/1440 px, and light/dark themes. 54 PNGs are attached to this issue and linked in the screenshot comment. No screenshot files are committed. ## UX gaps left A real screen-reader browse and touch exploration on macOS was not run in this job. The merge round should check modal browse order and focus restoration with VoiceOver and the other supported assistive technology clients. ## Gate output (verbatim excerpts) `cargo fmt --check` exited 0 with no stdout. `cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings` exited 0: ```text Finished dev profile [unoptimized + debuginfo] target(s) in 21m 50s ``` `cargo test -p calternal-plugin-mail` exited 0: ```text test result: ok. 48 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Focused modal Vitest run: ```text Test Files 3 passed (3) Tests 18 passed (18) ``` Focused Analytics Vitest run: 3 files passed, 15 tests passed. Focused AssignedCell Vitest run: 1 file passed, 4 tests passed. `bun run build` exited 0. Production E2Es exited 0: ```text modal inert e2e passed: 390, 820 and 1440 px in light and dark with macOS platform emulation; screenshots saved in /home/kayg/Developer/calternal-wt/a11yfix2/artifacts/a11yfix2/modal-inert-848 PHOTOS SCREENSHOTS PASSED CSP REPORTS photos: 0 across 2 pages analytics e2e: all checks passed PASS the User flow wrote exact Markdown and the screens show the derived numbers PASS repeated category labels retain unique screen-reader error descriptions PASS captured 48 Money screenshots in /home/kayg/Developer/calternal-wt/a11yfix2/artifacts/money ``` `bun run check` exited 1. Output: ```text $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-focus-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. Focus ring styles must use the shared tokens: apps/web/src/routes/settings/connected-accounts/ConnectedAccountsSection.svelte:792: local focus ring styles are not allowed (outline: var(--focus-ring); outline-offset: 2px) packages/ui/src/components/TextInput.svelte:32: local focus ring styles are not allowed (outline: var(--focus-ring); outline-offset: 1px) error: script "check" exited with code 1 ``` The two files reported by the focus-token check are unchanged from merge commit `a518546ca`. Running `bunx --no-install svelte-check --tsconfig ./tsconfig.json` directly also exited 1 on the unchanged Search route: ```text /home/kayg/Developer/calternal-wt/a11yfix2/apps/web/src/routes/search/+page.svelte:15:7 Error: 'to' is possibly 'null'. (ts) /home/kayg/Developer/calternal-wt/a11yfix2/apps/web/src/routes/search/+page.svelte:19:22 Error: 'to' is possibly 'null'. (ts) svelte-check found 2 errors and 0 warnings in 1 file ``` `cargo clean` output: ```text Removed 10997 files, 6.1GiB total ``` The generated `apps/web/build` and `apps/web/.svelte-kit` directories were removed. `git diff --check` passes and the worktree is clean. ## Decisions No new product decision was needed. The Money E2E now uses Tab/Shift+Tab because typing and Enter on an editable input do not change the shared input-modality state. The Appearance test request sends only `mode`, as required by the current server type; Location settings remain separate. The earlier 390 px Analytics selector timeout did not reproduce, so I added test diagnostics and made no layout change for it. ## Known gaps and merge round The web check remains blocked by the unchanged baseline focus-token findings and Search type errors above. The full Vitest suite, full E2E matrix, full adversarial matrix, real Mac client/VoiceOver check, and performance measurements were not run here under the 2026-10-02 verification policy. This issue does not change an API route or route contract. Performance profiles remain available in `apps/web/e2e/analytics.mjs --perf` and `apps/web/e2e/photos-perf.mjs`; do not measure them unless this is handled as a performance issue. For the merge round, run `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace` to verify the combined Rust tree; run `bun run test` and `bun run test:e2e` from `apps/web`; and run `tests/adversarial/run.sh` once against the combined real server to cover cross-User, authorization, hostile-input, and concurrency matrices. Also rerun `bun run test:e2e:modal-inert-848`, `bun run test:e2e:photos`, `bun run test:e2e:analytics`, and `bun run test:e2e:money` against the merged production build. The macOS check must use the locked Mac VM and prove real assistive-technology browse isolation and focus restoration. No push, deployment, issue closure, or additional merge was performed.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#848
No description provided.