TOAST: accent hairline around the capsule that drains with the dismiss timer (pauses on hover) #405

Closed
opened 2026-09-29 05:30:41 +00:00 by kayg · 22 comments
Owner

Request (owner, restated 2026-09-29; first asked 2026-09-28 and dropped)

"the toasts should have a very thin border line (accent colour from the theme) that is around the capsule and it basically runs out from being full to being not there depending on how much time it takes for the toast to dismiss."

Spec

  • A hairline stroke (role token for width, the theme's accent colour) traces the full capsule outline (a rounded-rect path, stroke-dasharray/stroke-dashoffset on an SVG path that follows the capsule's exact radius, or an equivalent that stays sharp at any size).
  • It starts full when the toast appears and drains linearly to nothing over the toast's remaining time; when it reaches zero the toast dismisses (#317 durations: plain confirmations 4 s; action toasts and errors keep their own timing).
  • Hover or keyboard focus pauses it (and resumes from the remaining time, as the existing pause/resume bank in toasts.svelte.ts does); a toast with no timeout (sticky) shows no ring.
  • The ring follows the capsule when the text wraps to two lines or the toast resizes.
  • Reduced motion: no animation; the ring is hidden (or shown static) and the timeout still applies.
  • Toasts use the see-through overlay glass (#402).
  • Reference for the overall toast feel: https://emilkowal.ski/ui/building-a-toast-component.
    Evidence: a frame strip of the drain, hover-pause, two-line and phone widths, light and dark (theme asserted).
## Request (owner, restated 2026-09-29; first asked 2026-09-28 and dropped) "the toasts should have a very thin border line (accent colour from the theme) that is around the capsule and it basically runs out from being full to being not there depending on how much time it takes for the toast to dismiss." ## Spec - A hairline stroke (role token for width, the theme's accent colour) traces the full capsule outline (a rounded-rect path, `stroke-dasharray`/`stroke-dashoffset` on an SVG path that follows the capsule's exact radius, or an equivalent that stays sharp at any size). - It starts full when the toast appears and drains linearly to nothing over the toast's remaining time; when it reaches zero the toast dismisses (#317 durations: plain confirmations 4 s; action toasts and errors keep their own timing). - **Hover or keyboard focus pauses it** (and resumes from the remaining time, as the existing pause/resume bank in toasts.svelte.ts does); a toast with no timeout (sticky) shows no ring. - The ring follows the capsule when the text wraps to two lines or the toast resizes. - Reduced motion: no animation; the ring is hidden (or shown static) and the timeout still applies. - Toasts use the see-through overlay glass (#402). - Reference for the overall toast feel: https://emilkowal.ski/ui/building-a-toast-component. Evidence: a frame strip of the drain, hover-pause, two-line and phone widths, light and dark (theme asserted).
Author
Owner

Starting issue #405 on branch job/toast-ring, based on dev at d0d83a32b41e67b64f6586690ba4946e41878160. I am keeping the implementation to the toast ring and its timing. I will merge the current dev branch before final web gates and keep visual evidence in the worktree.

Starting issue #405 on branch `job/toast-ring`, based on dev at `d0d83a32b41e67b64f6586690ba4946e41878160`. I am keeping the implementation to the toast ring and its timing. I will merge the current dev branch before final web gates and keep visual evidence in the worktree.
Author
Owner

Finding for #405: packages/ui/src/tokens.css has the theme accent color role and the glass hairline color, but no shared hairline-width role. I will add --hairline-width: 1px and use it for the draining accent stroke, so the ring keeps the shared token approach.

Finding for #405: `packages/ui/src/tokens.css` has the theme accent color role and the glass hairline color, but no shared hairline-width role. I will add `--hairline-width: 1px` and use it for the draining accent stroke, so the ring keeps the shared token approach.
Author
Owner

Browser finding for #405: the toast set data-expanded="true" on hover, but getComputedStyle(.cal-toast-ring rect).animationPlayState stayed running. The pause selector targeted the SVG wrapper while the CSS animation runs on its rect. I changed the selector to pause the animated rect. The new production-browser regression test now passes: ring holds its offset while hovered, resumes after exit, and remains tied to the toast timeout.

Browser finding for #405: the toast set `data-expanded="true"` on hover, but `getComputedStyle(.cal-toast-ring rect).animationPlayState` stayed `running`. The pause selector targeted the SVG wrapper while the CSS animation runs on its rect. I changed the selector to pause the animated rect. The new production-browser regression test now passes: ring holds its offset while hovered, resumes after exit, and remains tied to the toast timeout.
Author
Owner

Built

Added an accent hairline ring to timed toast capsules. It follows the Sonner timeout, pauses with hover, keyboard focus, and a hidden page, and resumes from the held offset. Sticky toasts have no ring. Reduced motion hides the ring while preserving the timeout. The SVG follows single- and two-line capsule sizes without runtime measurement.

Files

  • apps/web/src/lib/components/AppToaster.svelte — ring drain and pause rules.
  • apps/web/src/lib/components/ToastBody.svelte — accessible SVG ring overlay.
  • apps/web/src/lib/stores/toasts.svelte.ts and toasts.svelte.test.ts — timeout handoff, replacement reset, and store coverage.
  • packages/ui/src/tokens.css — shared --hairline-width role token.
  • apps/web/e2e/toast-ring.mjs and apps/web/package.json — production-browser hover/resume regression test and command.

Commits and branch

  • 70f49252 — feat(web): add timeout ring to toasts
  • cf77b20d — fix(web): pause toast ring with toast timer
  • Merged dev once at c4651504; merge commit/head: 969670eebe5b6b3a1fe00e3f2d00720e0e851b8a.
  • Pushed job/toast-ring; push reported Everything up-to-date.

The one-time dev merge did not change AppToaster.svelte or packages/ui/src/tokens.css, so there was no toast transparency change to resolve in that merge.

Gate output

bun run check:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/toast-ring/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test summary:

 Test Files  124 passed (124)
      Tests  791 passed (791)
   Start at  09:50:34
   Duration  85.35s (transform 44%, import 20%, environment 20%, tests 11%, setup 4%)

Environment  |component| jsdom was created 38 times · 95.59s total, 30% of tracked time
             create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files)
             learn more: https://vitest.dev/guide/improving-performance#test-environments

cargo fmt --check: exit 0, no output.

cargo clippy --all-targets -- -D warnings: stopped at the four-hour job limit while compiling; no Clippy diagnostic had appeared. Last output:

   Compiling zstd-sys v2.1.0+zstd.1.5.7
    Checking wait-timeout v0.2.1
    Checking unicode_categories v0.1.1
    Checking daachorse v3.0.3

cargo test was not run because Clippy had not completed before the time limit. The web browser regression test passed:

$ bun e2e/toast-ring.mjs
PASS: the ring pauses with Sonner on hover, resumes on exit, and remains tied to the toast timeout.

Vitest also printed jsdom Window.scrollTo() and CSS parser notices; it exited successfully with the summary above.

Visual evidence

The current production web bundle was served with a local proxy to a real throwaway API instance. The theme was asserted before capture. The Browser plugin was unavailable, so Playwright was used.

Decisions not explicit in DESIGN

  • Added the shared --hairline-width: 1px role token because no hairline role existed.
  • Used an SVG rounded rect with pathLength="100", the accent role, and the pill-radius role. Its linear CSS duration uses the same normalized timeout passed to Sonner.
  • Kept the timeout active under reduced motion and hid only the ring. Sticky and non-finite timeouts have no ring.

Cleanup

cargo clean removed 5,325 files (1.7 GiB). Removed apps/web/build, apps/web/.svelte-kit, and target/tmp. Screenshots remain in the ignored artifacts/toast-ring/ directory and are attached above.

## Built Added an accent hairline ring to timed toast capsules. It follows the Sonner timeout, pauses with hover, keyboard focus, and a hidden page, and resumes from the held offset. Sticky toasts have no ring. Reduced motion hides the ring while preserving the timeout. The SVG follows single- and two-line capsule sizes without runtime measurement. ## Files - `apps/web/src/lib/components/AppToaster.svelte` — ring drain and pause rules. - `apps/web/src/lib/components/ToastBody.svelte` — accessible SVG ring overlay. - `apps/web/src/lib/stores/toasts.svelte.ts` and `toasts.svelte.test.ts` — timeout handoff, replacement reset, and store coverage. - `packages/ui/src/tokens.css` — shared `--hairline-width` role token. - `apps/web/e2e/toast-ring.mjs` and `apps/web/package.json` — production-browser hover/resume regression test and command. ## Commits and branch - `70f49252` — `feat(web): add timeout ring to toasts` - `cf77b20d` — `fix(web): pause toast ring with toast timer` - Merged `dev` once at `c4651504`; merge commit/head: `969670eebe5b6b3a1fe00e3f2d00720e0e851b8a`. - Pushed `job/toast-ring`; push reported `Everything up-to-date`. The one-time `dev` merge did not change `AppToaster.svelte` or `packages/ui/src/tokens.css`, so there was no toast transparency change to resolve in that merge. ## Gate output `bun run check`: ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/toast-ring/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` summary: ```text Test Files 124 passed (124) Tests 791 passed (791) Start at 09:50:34 Duration 85.35s (transform 44%, import 20%, environment 20%, tests 11%, setup 4%) Environment |component| jsdom was created 38 times · 95.59s total, 30% of tracked time create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files) learn more: https://vitest.dev/guide/improving-performance#test-environments ``` `cargo fmt --check`: exit 0, no output. `cargo clippy --all-targets -- -D warnings`: stopped at the four-hour job limit while compiling; no Clippy diagnostic had appeared. Last output: ```text Compiling zstd-sys v2.1.0+zstd.1.5.7 Checking wait-timeout v0.2.1 Checking unicode_categories v0.1.1 Checking daachorse v3.0.3 ``` `cargo test` was not run because Clippy had not completed before the time limit. The web browser regression test passed: ```text $ bun e2e/toast-ring.mjs PASS: the ring pauses with Sonner on hover, resumes on exit, and remains tied to the toast timeout. ``` Vitest also printed jsdom `Window.scrollTo()` and CSS parser notices; it exited successfully with the summary above. ## Visual evidence The current production web bundle was served with a local proxy to a real throwaway API instance. The theme was asserted before capture. The Browser plugin was unavailable, so Playwright was used. - Drain strips: [Paper](https://git.kayg.org/attachments/4d76bd28-6286-4ba5-8a55-54e7d143a74a), [Tokyo Night](https://git.kayg.org/attachments/39e1150d-5d44-49ec-8300-c21e68e84a6f) - Hover pause: [Paper](https://git.kayg.org/attachments/312da5e9-6689-40ad-ba7b-d54da4fe9494), [Tokyo Night](https://git.kayg.org/attachments/6a093d07-6688-45e7-ad44-60e6f1738803) - Two-line phone (390 px): [Paper](https://git.kayg.org/attachments/156a1caf-384d-45e2-abdd-249b1c77c029), [Tokyo Night](https://git.kayg.org/attachments/00e4337b-d22d-476b-bc5a-b91691636f31) - Two-line tablet (820 px): [Paper](https://git.kayg.org/attachments/9eade70a-7d9d-46e4-9143-738b26b20438), [Tokyo Night](https://git.kayg.org/attachments/929d3acf-1245-4805-b10b-46e518f1e346) - Two-line desktop (1440 px): [Paper](https://git.kayg.org/attachments/45ec173b-8af5-44db-bf33-443c55b648a1), [Tokyo Night](https://git.kayg.org/attachments/7676ba19-ba65-4602-acca-9148cd78e0d5) - Keyboard focus pause: [Paper](https://git.kayg.org/attachments/dfbc232f-8075-43cb-944b-83a1e7e834f4) - Reduced motion: [Paper](https://git.kayg.org/attachments/01a410c6-19a5-4c37-b664-71b96e1b589b) ## Decisions not explicit in DESIGN - Added the shared `--hairline-width: 1px` role token because no hairline role existed. - Used an SVG rounded rect with `pathLength="100"`, the accent role, and the pill-radius role. Its linear CSS duration uses the same normalized timeout passed to Sonner. - Kept the timeout active under reduced motion and hid only the ring. Sticky and non-finite timeouts have no ring. ## Cleanup `cargo clean` removed 5,325 files (1.7 GiB). Removed `apps/web/build`, `apps/web/.svelte-kit`, and `target/tmp`. Screenshots remain in the ignored `artifacts/toast-ring/` directory and are attached above.
Author
Owner

Starting the visual-review fix on branch job/toast-ring. Original feature base: d0d83a32b4. Current head includes the single required dev merge (969670ee). I am correcting ring geometry, allowing complete messages up to three lines, and adding geometry assertions plus the requested production screenshots.

Starting the visual-review fix on branch job/toast-ring. Original feature base: d0d83a32b41e67b64f6586690ba4946e41878160. Current head includes the single required dev merge (969670ee). I am correcting ring geometry, allowing complete messages up to three lines, and adding geometry assertions plus the requested production screenshots.
Author
Owner

Finding: the first web check reported six nullable-reference diagnostics in ToastBody.svelte at the ResizeObserver callback. The callback now captures the guarded mounted nodes before observing them. The rerun reports: Text sizes use shared role tokens. / svelte-check found 0 errors and 0 warnings. Web tests pass: 124 files, 791 tests.

Finding: the first web check reported six nullable-reference diagnostics in ToastBody.svelte at the ResizeObserver callback. The callback now captures the guarded mounted nodes before observing them. The rerun reports: Text sizes use shared role tokens. / svelte-check found 0 errors and 0 warnings. Web tests pass: 124 files, 791 tests.
Author
Owner

Finding: the first e2e launch stopped before opening the app because the installed sharp binary could not load . The drain-strip builder now uses the browser canvas to combine screenshots captured from the production app, so the review no longer depends on sharp's native runtime.

Finding: the first e2e launch stopped before opening the app because the installed sharp binary could not load . The drain-strip builder now uses the browser canvas to combine screenshots captured from the production app, so the review no longer depends on sharp's native runtime.
Author
Owner

Finding detail: the e2e import failed with ERR_DLOPEN_FAILED because libstdc++.so.6 was absent. The strip compositor now uses the browser canvas and keeps the captured frames from the production app.

Finding detail: the e2e import failed with ERR_DLOPEN_FAILED because libstdc++.so.6 was absent. The strip compositor now uses the browser canvas and keeps the captured frames from the production app.
Author
Owner

Finding: on the first production e2e run, the 12 s drain-strip toast expired before the test sent Escape, after the last 10.8 s frame and screenshot encoding. The strip now uses a 20 s review duration with frames through 17.5 s, and teardown skips keyboard dismissal when the timed toast has already left.

Finding: on the first production e2e run, the 12 s drain-strip toast expired before the test sent Escape, after the last 10.8 s frame and screenshot encoding. The strip now uses a 20 s review duration with frames through 17.5 s, and teardown skips keyboard dismissal when the timed toast has already left.
Author
Owner

Finding: the next e2e run captured the frame sequence, then Playwright tried to focus a toast already marked data-removed=true while its timer elapsed during canvas encoding. Screenshot teardown now waits for natural timeout expiry; the separate hover test still checks pause and resume.

Finding: the next e2e run captured the frame sequence, then Playwright tried to focus a toast already marked data-removed=true while its timer elapsed during canvas encoding. Screenshot teardown now waits for natural timeout expiry; the separate hover test still checks pause and resume.
Author
Owner

Finding: after the real Appearance API response, the new owner’s root stayed on its default mono palette while the screenshot helper expected paper. The matrix now waits for that response, applies the requested existing palette to the production document, and asserts the rendered palette before capture.

Finding: after the real Appearance API response, the new owner’s root stayed on its default mono palette while the screenshot helper expected paper. The matrix now waits for that response, applies the requested existing palette to the production document, and asserts the rendered palette before capture.
Author
Owner

Finding: the strip capture reached desktop paper, then Playwright rejected the canvas-composition call because page.evaluate accepts one argument. The capture now passes its image list and gutter in one object.

Finding: the strip capture reached desktop paper, then Playwright rejected the canvas-composition call because page.evaluate accepts one argument. The capture now passes its image list and gutter in one object.
Author
Owner

Finding: the 20 s drain timer started when the toast was raised, before geometry and three zoom crops; on desktop paper the toast had expired before the final scheduled frame. Review duration is now 30 s, with frames through 25 s and a 1 s bound on each frame lookup.

Finding: the 20 s drain timer started when the toast was raised, before geometry and three zoom crops; on desktop paper the toast had expired before the final scheduled frame. Review duration is now 30 s, with frames through 25 s and a 1 s bound on each frame lookup.
Author
Owner

Merged the current dev head once before final gates: merge commit d8c62e42 includes dev 70fce975. The merge had no conflicts and includes server/API and contract changes, so I am rebuilding the local server for the final production review.

Merged the current dev head once before final gates: merge commit d8c62e42 includes dev 70fce975. The merge had no conflicts and includes server/API and contract changes, so I am rebuilding the local server for the final production review.
Author
Owner

Finding: the dev merge extended the e2e theme helper to verify the server's stored mode. The matrix still used its older save helper signature, so it rendered paper while the server reported system. I switched it to the shared setTheme helper, which persists and asserts the User's mode and palette.

Finding: the dev merge extended the e2e theme helper to verify the server's stored mode. The matrix still used its older save helper signature, so it rendered paper while the server reported system. I switched it to the shared setTheme helper, which persists and asserts the User's mode and palette.
Author
Owner

Post-merge browser evidence found three harness conditions and they are now covered:

  • The real calendar returns 404 for today's journal note when that note is empty. The ring probe accepts only this exact empty-note endpoint and still fails other HTTP errors and failed requests.
  • The 30 second desktop drain toast could expire before its last screenshot after high-DPI captures. The review toast now stays for 45 seconds while the strip samples the first 25 seconds.
  • An app instance can finish its earlier Appearance read after a theme update and restore the old mode. Each matrix cell now leaves the old app, saves the scheme, and loads the production app again.

The post-merge production run passed numeric ring bounds/radius checks at 390, 820 and 1440 px in paper and tokyo-night. It produced 36 four-times crops and two drain strips in the ignored artifacts/toast-ring-review directory.

Post-merge browser evidence found three harness conditions and they are now covered: - The real calendar returns 404 for today's journal note when that note is empty. The ring probe accepts only this exact empty-note endpoint and still fails other HTTP errors and failed requests. - The 30 second desktop drain toast could expire before its last screenshot after high-DPI captures. The review toast now stays for 45 seconds while the strip samples the first 25 seconds. - An app instance can finish its earlier Appearance read after a theme update and restore the old mode. Each matrix cell now leaves the old app, saves the scheme, and loads the production app again. The post-merge production run passed numeric ring bounds/radius checks at 390, 820 and 1440 px in paper and tokyo-night. It produced 36 four-times crops and two drain strips in the ignored artifacts/toast-ring-review directory.
Author
Owner

Final report — Forgejo #405

Built

  • The timeout ring now uses a one-CSS-pixel SVG viewBox measured from the toast border box. A ResizeObserver updates its geometry after a resize or text wrap. The path radius matches the real capsule radius.
  • A one-line toast keeps its capsule radius. A wrapped toast uses the shared glass radius. Messages wrap without a line clamp or ellipsis so the toast grows to show all text.
  • The production browser probe checks all four ring edges, viewBox size and path radius within 0.5 px. It also checks complete one- and two-line messages and timer pause/resume.

Files

  • apps/web/src/lib/components/AppToaster.svelte
  • apps/web/src/lib/components/ToastBody.svelte
  • apps/web/e2e/toast-ring.mjs

Proof

Exact e2e output:

PASS: ring geometry matches toast bounds and corner radius at all 3 widths and 2 themes; 36 4x crops and 2 drain strips saved in /home/kayg/Developer/calternal-wt/toast-ring/artifacts/toast-ring-review.

Web gates

bun run check passed with this output:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/toast-ring/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test passed with this output:

$ vitest run

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

Could not parse CSS stylesheet
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method

 Test Files  126 passed (126)
      Tests  804 passed (804)
   Start at  15:45:25
   Duration  133.48s (transform 57%, environment 16%, import 13%, tests 10%, setup 4%)

  Transform  |component| transforming modules took 315.32s · 53% of tracked time, re-done on every run
             persist transforms across runs with fsModuleCache: true
             learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns

Adversarial round

One real-server ADVERSARIAL_API_ONLY=1 round ran after the required dev merge. It finished with exit code 1 and 123 findings: two non-SLOW findings and 121 SLOW-only reports under shared-host load. The server remained alive. The non-SLOW evidence is filed on the relevant existing issues:

  • #387: 3 MiB Appearance PUT returned 502 local adversarial server is unavailable through the local Node proxy; the direct-backend size-limit probe returned the expected 413. This did not show a Rust server crash.
  • #48: after completing a CalDAV VTODO, its next GET returned 200 without STATUS:COMPLETED and COMPLETED:20260925T000000Z.

The other findings were SLOW load results, including the 120-photo upload burst and DAV/Journal operations. They do not block under the owner rule.

Decisions

docs/DESIGN.md did not select an SVG path or a CSS border mask. I used the SVG path because its measured pixel-sized viewBox preserves a crisp 1 px stroke at every toast size. For a wrapped toast, I used the shared --radius-glass token; for one line, the measured height divided by two gives the true capsule radius. I removed the message clamp so long text remains visible as the toast grows.

No Rust source was changed by this job, so cargo clippy was not run. The server was built for the production e2e and adversarial run. cargo clean completed afterward.

Head SHA: 2a7e4d21b497f78af38c0a02221405693e3c2bcc

Final report — Forgejo #405 ### Built - The timeout ring now uses a one-CSS-pixel SVG viewBox measured from the toast border box. A ResizeObserver updates its geometry after a resize or text wrap. The path radius matches the real capsule radius. - A one-line toast keeps its capsule radius. A wrapped toast uses the shared glass radius. Messages wrap without a line clamp or ellipsis so the toast grows to show all text. - The production browser probe checks all four ring edges, viewBox size and path radius within 0.5 px. It also checks complete one- and two-line messages and timer pause/resume. ### Files - `apps/web/src/lib/components/AppToaster.svelte` - `apps/web/src/lib/components/ToastBody.svelte` - `apps/web/e2e/toast-ring.mjs` ### Proof - Post-merge production e2e passed for 390, 820 and 1440 px, in paper and tokyo-night, with one- and two-line toasts. - It saved 36 4x crops and two drain strips. All 38 post-merge PNGs are attached to this issue: - [Paper drain strip](https://git.kayg.org/attachments/38609526-7905-4255-9056-ace0267578c7) - [Tokyo Night drain strip](https://git.kayg.org/attachments/14e780cc-4713-4925-bfd6-97f4a2fe8613) - one-line, 390 px, paper: [left](https://git.kayg.org/attachments/b7aabf56-9aef-441f-8d72-476706dd9e32) · [right](https://git.kayg.org/attachments/4c1f1206-447c-454a-af7a-b6822143cc22) · [top](https://git.kayg.org/attachments/8eb6f50a-5fda-403d-8860-70cfa23e0354) - one-line, 390 px, tokyo-night: [left](https://git.kayg.org/attachments/800cd1eb-e429-40bf-ad3b-6449a8f67f2c) · [right](https://git.kayg.org/attachments/9bd1fcac-4cb9-4ca2-b7fc-1a370598d8dd) · [top](https://git.kayg.org/attachments/507fdcbe-3bb3-4626-8b98-73dc33879396) - one-line, 820 px, paper: [left](https://git.kayg.org/attachments/bf5a54be-cb69-4af7-89fe-d6f2f469b5ac) · [right](https://git.kayg.org/attachments/a34b501b-9f61-4028-828c-e121cc603550) · [top](https://git.kayg.org/attachments/87652f23-7450-44a1-94f6-5e19f94c76df) - one-line, 820 px, tokyo-night: [left](https://git.kayg.org/attachments/04ed5ce2-5987-4b50-ba5f-77716d571b31) · [right](https://git.kayg.org/attachments/a1e6a42d-85bb-4ec4-8fa5-31c52e079fe4) · [top](https://git.kayg.org/attachments/d4f1e933-066d-4ff5-99a0-c04efee82591) - one-line, 1440 px, paper: [left](https://git.kayg.org/attachments/cd916aef-e989-419a-9663-efd271b4bf52) · [right](https://git.kayg.org/attachments/aaa803e4-637a-41df-928b-8d29566c211b) · [top](https://git.kayg.org/attachments/609567c2-0efe-4cc0-958e-94fd7068d2b4) - one-line, 1440 px, tokyo-night: [left](https://git.kayg.org/attachments/23043131-b690-4e78-870d-4b64989dc090) · [right](https://git.kayg.org/attachments/1110bc93-3699-42b5-8cab-70ac87afeea8) · [top](https://git.kayg.org/attachments/8ebb50fe-e6b8-4125-9ab9-c5b70d190a28) - two-line, 390 px, paper: [left](https://git.kayg.org/attachments/5ca8ea9f-e927-4c43-a888-02cff207d98f) · [right](https://git.kayg.org/attachments/23986439-cc65-47b8-aa54-1e7677bd2e33) · [top](https://git.kayg.org/attachments/c5ecaf44-53c5-41c1-abe1-740c2671a1c8) - two-line, 390 px, tokyo-night: [left](https://git.kayg.org/attachments/315b01e4-fcdf-4f02-b641-1fff103c8e51) · [right](https://git.kayg.org/attachments/8b1d2f17-484d-4f37-95e6-5cfb0431022d) · [top](https://git.kayg.org/attachments/69df5f4c-510f-4111-b44e-2c971f3cd9d2) - two-line, 820 px, paper: [left](https://git.kayg.org/attachments/d20efa70-c0b5-4a56-8f32-009656d451cb) · [right](https://git.kayg.org/attachments/fa2336d3-5073-4c49-9d64-e1c5058a0a2a) · [top](https://git.kayg.org/attachments/6c5cf9ec-829c-43c8-a619-0dd85f8c2aaf) - two-line, 820 px, tokyo-night: [left](https://git.kayg.org/attachments/e59fb00c-c2ec-4140-a247-51289eb18308) · [right](https://git.kayg.org/attachments/e46bde59-43c6-45e1-b915-277312f616a1) · [top](https://git.kayg.org/attachments/cc32a771-4528-4313-bd55-7eb5ea4d1041) - two-line, 1440 px, paper: [left](https://git.kayg.org/attachments/c8cb33e1-853c-42da-85b0-9af6301a8325) · [right](https://git.kayg.org/attachments/70866c29-3abf-49e7-a3ec-46c7932345e5) · [top](https://git.kayg.org/attachments/270835a6-5eb4-4973-9505-11e181c6f0a1) - two-line, 1440 px, tokyo-night: [left](https://git.kayg.org/attachments/2a867833-3e7b-47b2-a578-98085c697724) · [right](https://git.kayg.org/attachments/a981caf5-e887-43ad-80e6-455c95f3fcbd) · [top](https://git.kayg.org/attachments/f689973c-39d3-44bf-9b59-3719686a8cfd) Exact e2e output: ```text PASS: ring geometry matches toast bounds and corner radius at all 3 widths and 2 themes; 36 4x crops and 2 drain strips saved in /home/kayg/Developer/calternal-wt/toast-ring/artifacts/toast-ring-review. ``` ### Web gates `bun run check` passed with this output: ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/toast-ring/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` passed with this output: ```text $ vitest run RUN v5.0.1 /home/kayg/Developer/calternal-wt/toast-ring/apps/web Could not parse CSS stylesheet Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Test Files 126 passed (126) Tests 804 passed (804) Start at 15:45:25 Duration 133.48s (transform 57%, environment 16%, import 13%, tests 10%, setup 4%) Transform |component| transforming modules took 315.32s · 53% of tracked time, re-done on every run persist transforms across runs with fsModuleCache: true learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns ``` ### Adversarial round One real-server `ADVERSARIAL_API_ONLY=1` round ran after the required `dev` merge. It finished with exit code 1 and 123 findings: two non-SLOW findings and 121 SLOW-only reports under shared-host load. The server remained alive. The non-SLOW evidence is filed on the relevant existing issues: - [#387](https://git.kayg.org/kayg/calternal/issues/387): 3 MiB Appearance PUT returned `502 local adversarial server is unavailable` through the local Node proxy; the direct-backend size-limit probe returned the expected 413. This did not show a Rust server crash. - [#48](https://git.kayg.org/kayg/calternal/issues/48): after completing a CalDAV VTODO, its next GET returned 200 without `STATUS:COMPLETED` and `COMPLETED:20260925T000000Z`. The other findings were SLOW load results, including the 120-photo upload burst and DAV/Journal operations. They do not block under the owner rule. ### Decisions `docs/DESIGN.md` did not select an SVG path or a CSS border mask. I used the SVG path because its measured pixel-sized viewBox preserves a crisp 1 px stroke at every toast size. For a wrapped toast, I used the shared `--radius-glass` token; for one line, the measured height divided by two gives the true capsule radius. I removed the message clamp so long text remains visible as the toast grows. No Rust source was changed by this job, so `cargo clippy` was not run. The server was built for the production e2e and adversarial run. `cargo clean` completed afterward. Head SHA: `2a7e4d21b497f78af38c0a02221405693e3c2bcc`
Author
Owner

Starting round 3 of Forgejo #405 on job/toast-ring, current head 2a7e4d21b497f78af38c0a02221405693e3c2bcc, based on dev at 70fce975945c272c7ed7f3e1844949e818ac778f. The last visual review rejected the visible ring and toast crop evidence. I am tracing the production DOM and pixels, then I will correct the probe or rendering, capture the requested 390/1440 light/dark evidence, merge the latest dev once before final gates, and report the gate output.

Starting round 3 of Forgejo #405 on `job/toast-ring`, current head `2a7e4d21b497f78af38c0a02221405693e3c2bcc`, based on dev at `70fce975945c272c7ed7f3e1844949e818ac778f`. The last visual review rejected the visible ring and toast crop evidence. I am tracing the production DOM and pixels, then I will correct the probe or rendering, capture the requested 390/1440 light/dark evidence, merge the latest dev once before final gates, and report the gate output.
Author
Owner

Finding for #405: apps/web/e2e/toast-ring.mjs currently captures every frame and zoom crop with animations: 'disabled' while the accent ring is a finite CSS animation. That capture mode fast-forwards the ring to its end, matching the identical frames in the attached drain-strip-paper.png. The 1440 px Tokyo Night left crop is only 144 × 192 px and shows the Calendar tray pill, not “Link copied”; the helper clips stale viewport coordinates instead of capturing the toast node after its entry transition. I will capture the toast element directly, assert its label and computed accent stroke, and sample the rendered border pixels through the timeout. Production ring visibility remains to be confirmed in the browser.

Finding for #405: `apps/web/e2e/toast-ring.mjs` currently captures every frame and zoom crop with `animations: 'disabled'` while the accent ring is a finite CSS animation. That capture mode fast-forwards the ring to its end, matching the identical frames in the attached `drain-strip-paper.png`. The 1440 px Tokyo Night left crop is only 144 × 192 px and shows the Calendar tray pill, not “Link copied”; the helper clips stale viewport coordinates instead of capturing the toast node after its entry transition. I will capture the toast element directly, assert its label and computed accent stroke, and sample the rendered border pixels through the timeout. Production ring visibility remains to be confirmed in the browser.
Author
Owner

Round 3 production-browser finding, after commit 6cb795d6:

  • The old review strip was captured with Playwright animation fast-forwarding, so its finite ring could drain before paint was sampled. The bad left crop also used a stale viewport coordinate and landed on the Calendar tray.
  • The updated probe captures .cal-toaster [data-sonner-toast] itself and checks that the toast text is present. It reads computed accent, stroke width, opacity, geometry, and dash offset, then samples rendered pixels.
  • Production build evidence before the latest dev merge: six 4x toast-root crops at 390, 820, and 1440 px in paper and Tokyo Night. Contrast measured 5.74:1 or better in paper and 5.93:1 or better in Tokyo Night.
  • Both six-frame strips pass strict monotonic pixel loss. Paper: 3757, 3119, 2271, 1856, 536, 312 accent pixels. Tokyo Night: 3756, 3102, 2269, 1846, 1104, 372. Dash offsets at 0%, 50%, and 90% are about 0.7, 50.7, and 90.7.
  • Crop-only probe toasts use a 10 s test duration to keep the ring visible while browser screenshots finish. The 1440 px strip uses 60 s so frames can be captured at 0%, 20%, 40%, 50%, 70%, and 90%. Product confirmation duration remains 4 s.

The probe passed against the production SPA and a local server binary built from the current dev lineage. I will rerun the production proof after merging dev and attach the final artifacts then.

Round 3 production-browser finding, after commit `6cb795d6`: - The old review strip was captured with Playwright animation fast-forwarding, so its finite ring could drain before paint was sampled. The bad left crop also used a stale viewport coordinate and landed on the Calendar tray. - The updated probe captures `.cal-toaster [data-sonner-toast]` itself and checks that the toast text is present. It reads computed accent, stroke width, opacity, geometry, and dash offset, then samples rendered pixels. - Production build evidence before the latest `dev` merge: six 4x toast-root crops at 390, 820, and 1440 px in paper and Tokyo Night. Contrast measured 5.74:1 or better in paper and 5.93:1 or better in Tokyo Night. - Both six-frame strips pass strict monotonic pixel loss. Paper: 3757, 3119, 2271, 1856, 536, 312 accent pixels. Tokyo Night: 3756, 3102, 2269, 1846, 1104, 372. Dash offsets at 0%, 50%, and 90% are about 0.7, 50.7, and 90.7. - Crop-only probe toasts use a 10 s test duration to keep the ring visible while browser screenshots finish. The 1440 px strip uses 60 s so frames can be captured at 0%, 20%, 40%, 50%, 70%, and 90%. Product confirmation duration remains 4 s. The probe passed against the production SPA and a local server binary built from the current `dev` lineage. I will rerun the production proof after merging `dev` and attach the final artifacts then.
Author
Owner

#405 round 3 complete. Branch: job/toast-ring. HEAD: d2081a1ab92c9af6812472c19f3e712bd283c403. Merged the latest dev once at 7280dbc0.

Built

  • The toast has a 1 CSS px, fully opaque ring in the full-strength theme accent. Its CSS dash uses Sonner's finite timeout and pauses with hover/focus and page-hidden state.
  • The ring viewBox tracks the actual toast box, including its current two-line label height.
  • Preserved dev's two-line clamp and full-text title tooltip. The long-message browser case checks both truncation and tooltip behavior.
  • Updated the production-browser regression probe to sample computed stroke/style and rendered pixels, capture the toast root itself, verify label pixels and 4x scale, check contrast, enforce monotonic drain, and test hover pause.

Files: apps/web/e2e/toast-ring.mjs, apps/web/src/lib/components/AppToaster.svelte, apps/web/src/lib/components/ToastBody.svelte, apps/web/src/lib/stores/toasts.svelte.ts, packages/ui/src/tokens.css.

Production-browser evidence

The Playwright probe ran against the production SPA and a local server built from the current dev lineage. It passed at 390, 820, and 1440 px in paper and Tokyo Night. Six toast-root crops contain the label and accent pixels. Contrast was at least 5.72:1 in paper and 5.93:1 in Tokyo Night.

Paper strip accent-pixel counts at 0%, 20%, 40%, 50%, 70%, 90%: 3624, 3035, 2083, 1743, 988, 200.

Tokyo Night strip accent-pixel counts at 0%, 20%, 40%, 50%, 70%, 90%: 3742, 3061, 2273, 1775, 1135, 368.

Exact browser result:

PASS: full-strength ring geometry and two-line layout match at all 3 widths and 2 themes; six 4x toast crops and two pixel-checked drain strips saved in /home/kayg/Developer/calternal-wt/toast-ring/artifacts/toast-ring-round3.

Attachments:

Gates

bun run check output:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/toast-ring/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test output:

 FAIL  |component| src/lib/components/analytics/widgets/StatRow.svelte.test.ts > StatRow summary cards > toggles all four cards together and remembers the mode
Error: Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ src/lib/components/analytics/widgets/StatRow.svelte.test.ts:97:3

 Test Files  1 failed | 126 passed (127)
      Tests  1 failed | 812 passed (813)
   Duration  260.96s (transform 58%, environment 17%, import 13%, tests 9%, setup 3%)
error: script "test" exited with code 1

The timed-out test is in StatRow.svelte.test.ts, outside the files changed for #405. I did not change its assertion or rerun the gate.

bun run build completed successfully. cargo clean removed 3588 files (1.5 GiB); apps/web/build and apps/web/.svelte-kit were removed. Browser-plugin testing was unavailable in this session, so the repository's Playwright production-browser harness was used.

Decisions

  • DESIGN §28 defines the 4 s plain-confirmation timeout and hover/focus pause, but it does not define long-label behavior. I preserved the latest dev decision: clamp after two lines, keep the full text in the DOM, and set a title only when the clamp cuts text.
  • Crop-only test toasts use 10 s and drain strips use 60 s so browser capture can finish while the ring is visible. Product confirmation timing remains 4 s. The strip samples 0%, 20%, 40%, 50%, 70%, and 90% of its test duration.
  • The ring radius follows half of the measured capsule height to keep the path on the current dev stadium shape after a label wraps.
#405 round 3 complete. Branch: `job/toast-ring`. HEAD: `d2081a1ab92c9af6812472c19f3e712bd283c403`. Merged the latest `dev` once at `7280dbc0`. ## Built - The toast has a 1 CSS px, fully opaque ring in the full-strength theme accent. Its CSS dash uses Sonner's finite timeout and pauses with hover/focus and page-hidden state. - The ring viewBox tracks the actual toast box, including its current two-line label height. - Preserved `dev`'s two-line clamp and full-text title tooltip. The long-message browser case checks both truncation and tooltip behavior. - Updated the production-browser regression probe to sample computed stroke/style and rendered pixels, capture the toast root itself, verify label pixels and 4x scale, check contrast, enforce monotonic drain, and test hover pause. Files: `apps/web/e2e/toast-ring.mjs`, `apps/web/src/lib/components/AppToaster.svelte`, `apps/web/src/lib/components/ToastBody.svelte`, `apps/web/src/lib/stores/toasts.svelte.ts`, `packages/ui/src/tokens.css`. ## Production-browser evidence The Playwright probe ran against the production SPA and a local server built from the current `dev` lineage. It passed at 390, 820, and 1440 px in paper and Tokyo Night. Six toast-root crops contain the label and accent pixels. Contrast was at least 5.72:1 in paper and 5.93:1 in Tokyo Night. Paper strip accent-pixel counts at 0%, 20%, 40%, 50%, 70%, 90%: `3624, 3035, 2083, 1743, 988, 200`. Tokyo Night strip accent-pixel counts at 0%, 20%, 40%, 50%, 70%, 90%: `3742, 3061, 2273, 1775, 1135, 368`. Exact browser result: ```text PASS: full-strength ring geometry and two-line layout match at all 3 widths and 2 themes; six 4x toast crops and two pixel-checked drain strips saved in /home/kayg/Developer/calternal-wt/toast-ring/artifacts/toast-ring-round3. ``` Attachments: - [Paper drain strip](https://git.kayg.org/attachments/b9562dc2-6859-4d72-a035-fae6c2d57ecd) - [Tokyo Night drain strip](https://git.kayg.org/attachments/1a1768ae-cd24-4cca-b094-e437c6359c75) - [390 px paper crop](https://git.kayg.org/attachments/e379f6f3-40bb-47f4-a696-149335da8f39) - [390 px Tokyo Night crop](https://git.kayg.org/attachments/56cde543-ce63-4781-bcd0-49298655b6dd) - [820 px paper crop](https://git.kayg.org/attachments/fc1c66e3-7c7e-4276-ad89-eb8645d8b420) - [820 px Tokyo Night crop](https://git.kayg.org/attachments/97d7edce-5d1e-42ae-b641-b146bc3b4961) - [1440 px paper crop](https://git.kayg.org/attachments/908e586c-6553-4c2d-9fb0-19b495f42e1b) - [1440 px Tokyo Night crop](https://git.kayg.org/attachments/6f6ad47e-4936-4c2a-bdd6-abd5b4f68fa6) ## Gates `bun run check` output: ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/toast-ring/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` output: ```text FAIL |component| src/lib/components/analytics/widgets/StatRow.svelte.test.ts > StatRow summary cards > toggles all four cards together and remembers the mode Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ src/lib/components/analytics/widgets/StatRow.svelte.test.ts:97:3 Test Files 1 failed | 126 passed (127) Tests 1 failed | 812 passed (813) Duration 260.96s (transform 58%, environment 17%, import 13%, tests 9%, setup 3%) error: script "test" exited with code 1 ``` The timed-out test is in `StatRow.svelte.test.ts`, outside the files changed for #405. I did not change its assertion or rerun the gate. `bun run build` completed successfully. `cargo clean` removed 3588 files (1.5 GiB); `apps/web/build` and `apps/web/.svelte-kit` were removed. Browser-plugin testing was unavailable in this session, so the repository's Playwright production-browser harness was used. ## Decisions - DESIGN §28 defines the 4 s plain-confirmation timeout and hover/focus pause, but it does not define long-label behavior. I preserved the latest `dev` decision: clamp after two lines, keep the full text in the DOM, and set a title only when the clamp cuts text. - Crop-only test toasts use 10 s and drain strips use 60 s so browser capture can finish while the ring is visible. Product confirmation timing remains 4 s. The strip samples 0%, 20%, 40%, 50%, 70%, and 90% of its test duration. - The ring radius follows half of the measured capsule height to keep the path on the current `dev` stadium shape after a label wraps.
Author
Owner

Merged into dev and pushed (9bf3d549) after the web gates (816/816) and the relevant crate gates. Deploy follows the #356/#393 Apple GO/NO-GO.

Merged into dev and pushed (9bf3d549) after the web gates (816/816) and the relevant crate gates. Deploy follows the #356/#393 Apple GO/NO-GO.
kayg closed this issue 2026-09-29 15:47:36 +00:00
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#405
No description provided.