Toasts: dot-to-capsule morph, burst coalescing with rolling count, in-place honest Undo (keep Sonner) #1201

Open
opened 2026-10-06 11:17:53 +00:00 by kayg · 1 comment
Owner

Owner request (via Aside, 2026-10-06): dot→capsule toast morph with burst coalescing and in-place Undo — keep Sonner

Reference: metalforge.xyz/interactions#component=toast ("A dot that opens into the message; a second one rolls the count; Undo morphs it in place"). Behavioural decode from the live preview (source is Pro-only; do not claim copied code; map colours to calternal tokens, never the reference hex values):

  • A compact status dot expands into a fully rounded message capsule (~44 px, shadow) rather than a rectangular slide-in.
  • Repeated compatible triggers coalesce into ONE toast: 12 clicks → "Message sent ×12"; the count rolls (digit roll incl. 9→10 width change) while the message stays anchored.
  • Undo on the ×12 toast morphs the SAME toast to "Unsent", removing Undo and count; the reference emits undo {message, count}.
  • Text changes per character for a local transition, but assistive tech gets ONE readable string in one role=status region (no letter-by-letter announcements).
  • Reference defaults: stays ~2.9 s; Top or Bottom placement; Speed/Bounce/Blur/Press-grow tuning. Exact springs/timings not verified — tune against the preview.

Architecture (must)

  • Keep svelte-sonner (installed 1.2.1) as the owner of lifecycle, stacking, swipe, focus, dismissal and timers. Verify its API in node_modules (custom component toasts; updating an existing toast by id; promise states) before choosing. Put the capsule morph + content animation in ONE reusable Svelte renderer used via the shared adapter (apps/web/src/lib/stores/toasts.svelte.ts / AppToaster.svelte). Never override Sonner's outer transform; no second queue or competing timers.
  • All existing call sites keep going through the shared adapter, preserving error/info/warning/loading/action semantics and the #1142 rules (toast only for actionable or awaited background results; persisted to Notifications per policy) and #1194 (no notification-panel actions as toasts).

Aggregation and Undo policy (safety first)

  • Coalesce by a stable operation/group key (operation type + target scope + undo contract), never by message text alone. Compatible repeats update the same Sonner toast id: rolling count, refreshed readable lifetime. Different entities/contexts, errors, different actions or undo contracts stay separate. The aggregation window is bounded; state is cleaned up on dismiss; a new burst after expiry starts at 1.
  • Honest Undo: group only operations whose real undo tokens can all be reversed as a group (the adapter holds every token; Undo runs them all, transactionally where the domain supports it). Otherwise keep separate toasts or label "Undo last" explicitly. Never advertise batch Undo and undo one. While Undo is pending, disable repeats; morph to "Undone" only after real success; on failure/refusal show readable error + Retry (keep the existing Money import refusal handling inline, #1196).
  • Loading toasts persist until resolution and morph loading→success/error in place. Long errors and actionable details are not crushed into a capsule: the capsule grows to a readable card (wrapping, max width, description/action, safe areas on mobile).

Quality

  • Hover/focus pause and hidden-tab pause (verify Sonner's Svelte port; implement in the adapter if missing), swipe/close, keyboard access to actions, one meaningful live announcement per burst state (not per frame), reduced motion = static update, contrast in every theme, RTL/localisation, counts 9→10→99→100 without layout jumps. No per-frame layout reads, no persistent blur, no rAF after settle; retarget mid-flight from current geometry; measure on content change and font load.
  • Tests/evidence: 12+ rapid compatible events → exactly one toast with the exact count; mixed operations stay separate; expiry resets the count; bursts during entrance/exit; Undo during a burst; pending / failed / partial batch Undo; promise rejection; long messages; resize/touch; keyboard; reduced motion; Chromium + WebKit. Short screen recordings (WebM) side by side with the reference behaviour, and a perf trace. bun run check + bun run test pass. Commit; comment with head SHA. Do not push or deploy.

Coordination

#1142 (toast policy) is merged; #1194 (notification-panel loop, keyboard rows) is running in the same adapter — this job starts after #1194 lands on its branch and builds on it. #1200 owns SegmentedControl; #1189 owns PillGroup.

## Owner request (via Aside, 2026-10-06): dot→capsule toast morph with burst coalescing and in-place Undo — keep Sonner Reference: metalforge.xyz/interactions#component=toast ("A dot that opens into the message; a second one rolls the count; Undo morphs it in place"). Behavioural decode from the live preview (source is Pro-only; do not claim copied code; map colours to calternal tokens, never the reference hex values): - A compact status dot expands into a fully rounded message capsule (~44 px, shadow) rather than a rectangular slide-in. - Repeated compatible triggers coalesce into ONE toast: 12 clicks → "Message sent ×12"; the count rolls (digit roll incl. 9→10 width change) while the message stays anchored. - Undo on the ×12 toast morphs the SAME toast to "Unsent", removing Undo and count; the reference emits undo {message, count}. - Text changes per character for a local transition, but assistive tech gets ONE readable string in one role=status region (no letter-by-letter announcements). - Reference defaults: stays ~2.9 s; Top or Bottom placement; Speed/Bounce/Blur/Press-grow tuning. Exact springs/timings not verified — tune against the preview. ### Architecture (must) - Keep **svelte-sonner (installed 1.2.1)** as the owner of lifecycle, stacking, swipe, focus, dismissal and timers. Verify its API in node_modules (custom component toasts; updating an existing toast by `id`; promise states) before choosing. Put the capsule morph + content animation in ONE reusable Svelte renderer used via the shared adapter (apps/web/src/lib/stores/toasts.svelte.ts / AppToaster.svelte). Never override Sonner's outer transform; no second queue or competing timers. - All existing call sites keep going through the shared adapter, preserving error/info/warning/loading/action semantics and the #1142 rules (toast only for actionable or awaited background results; persisted to Notifications per policy) and #1194 (no notification-panel actions as toasts). ### Aggregation and Undo policy (safety first) - Coalesce by a stable **operation/group key** (operation type + target scope + undo contract), never by message text alone. Compatible repeats update the same Sonner toast id: rolling count, refreshed readable lifetime. Different entities/contexts, errors, different actions or undo contracts stay separate. The aggregation window is bounded; state is cleaned up on dismiss; a new burst after expiry starts at 1. - **Honest Undo:** group only operations whose real undo tokens can all be reversed as a group (the adapter holds every token; Undo runs them all, transactionally where the domain supports it). Otherwise keep separate toasts or label "Undo last" explicitly. Never advertise batch Undo and undo one. While Undo is pending, disable repeats; morph to "Undone" only after real success; on failure/refusal show readable error + Retry (keep the existing Money import refusal handling inline, #1196). - Loading toasts persist until resolution and morph loading→success/error in place. Long errors and actionable details are not crushed into a capsule: the capsule grows to a readable card (wrapping, max width, description/action, safe areas on mobile). ### Quality - Hover/focus pause and hidden-tab pause (verify Sonner's Svelte port; implement in the adapter if missing), swipe/close, keyboard access to actions, one meaningful live announcement per burst state (not per frame), reduced motion = static update, contrast in every theme, RTL/localisation, counts 9→10→99→100 without layout jumps. No per-frame layout reads, no persistent blur, no rAF after settle; retarget mid-flight from current geometry; measure on content change and font load. - Tests/evidence: 12+ rapid compatible events → exactly one toast with the exact count; mixed operations stay separate; expiry resets the count; bursts during entrance/exit; Undo during a burst; pending / failed / partial batch Undo; promise rejection; long messages; resize/touch; keyboard; reduced motion; Chromium + WebKit. Short screen recordings (WebM) side by side with the reference behaviour, and a perf trace. `bun run check` + `bun run test` pass. Commit; comment with head SHA. Do not push or deploy. ### Coordination #1142 (toast policy) is merged; #1194 (notification-panel loop, keyboard rows) is running in the same adapter — this job starts after #1194 lands on its branch and builds on it. #1200 owns SegmentedControl; #1189 owns PillGroup.
Author
Owner

Also (orchestrator review of #1194 screenshots): Notification rows put the Open action on its own line under the row, doubling each row's height. Put Open inline at the row end (shared Pill, small) next to time/unread dot; hover/focus reveals Delete; keep 44px touch targets on coarse pointers.

Also (orchestrator review of #1194 screenshots): Notification rows put the Open action on its own line under the row, doubling each row's height. Put Open inline at the row end (shared Pill, small) next to time/unread dot; hover/focus reveals Delete; keep 44px touch targets on coarse pointers.
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#1201
No description provided.