Skip to content

feat(emcn/toast): toast redesign — intent variants, stacking, hover reveal, dismiss-all#4909

Open
andresdjasso wants to merge 1 commit into
stagingfrom
improvement/emcn-toast-redesign
Open

feat(emcn/toast): toast redesign — intent variants, stacking, hover reveal, dismiss-all#4909
andresdjasso wants to merge 1 commit into
stagingfrom
improvement/emcn-toast-redesign

Conversation

@andresdjasso

Copy link
Copy Markdown

Re-cut cleanly off latest staging (the original #4907 was accidentally branched off improvement/platform, which is now merged into staging — hence its conflicts). This is the same toast work as commit b92ea33, applied conflict-free on staging.

What changed

  • Variants default/info/success/warning/error, each with a distinct outline icon (CircleAlert/TriangleAlert/CircleCheck/Info/Bell) rendered inline with the message in a neutral color — no badge; intent reads from icon + copy.
  • Stacking (Sonner/Base-UI style): collapsed pile that fans open upward only when the cards are hovered; one fixed-duration expo-out tween so rapid arrivals move in unison. Cards arrive collapsed.
  • Title vs subtext hierarchy (medium primary title + lighter description); notifyBlockError splits block name (title) / error (subtext).
  • Hover text reveal: truncated cards reveal hidden lines on hover — only the hidden lines blur in; card height tracks content so the action button stays pinned. Larger bottom gradient fade.
  • Concentric corner radius (16px = chip 8px + 8px padding); single-line cards use a tighter 12px.
  • Dismiss-all control: small circular chip just outside the stack's bottom-left at 2+ toasts — linear auto-dismiss ring (restarts on each new arrival), pauses on hover, click to clear all; spring "pop" entrance.
  • Fixes: route-scoped clearing, dedup of the add/update double-fire, persistent actionable toasts.
  • Demo: EMCN playground (/playground → Toast section).

The playground/page.tsx diff looks large but is 107 real lines (import + ToastProvider wrap + the demo section) — the rest is re-indentation from the wrapper. Use "Hide whitespace" to review.

🤖 Generated with Claude Code

…k, hover text reveal, dismiss-all

Component (apps/sim/components/emcn/components/toast/toast.tsx):
- Variants default/info/success/warning/error, each with a distinct outline
  icon (CircleAlert/TriangleAlert/CircleCheck/Info/Bell) rendered inline with
  the message in a neutral color — no badge; intent reads from icon + copy.
- Stacking modeled on Sonner/Base-UI: a collapsed pile that fans open upward
  only when the cards are hovered. One fixed-duration expo-out tween drives all
  cards so rapid arrivals move in unison (no lagging card); cards arrive
  collapsed (expand is scoped to a wrapper around the cards, not the dismiss
  control).
- Title vs subtext hierarchy: message is a medium, primary-color title; the
  optional description is lighter/smaller subtext.
- Truncated text reveals its hidden lines on hover (RevealText): only the
  previously hidden lines blur in; the card height tracks the content so the
  action button stays pinned (no clipping). Larger bottom gradient fade hints
  at more text.
- Concentric corner radius (16px = chip 8px + 8px padding); single-line cards
  use a tighter 12px so they don't read as pills.
- Dismiss-all control: a small circular chip just outside the stack's
  bottom-left, shown at 2+ toasts. Linear auto-dismiss ring that restarts on
  each new arrival, pauses on hover, click to clear all; spring 'pop' entrance.
- Bug fixes: route-scoped clearing (toasts no longer trail across navigation),
  dedup of the add/update double-fire, actionable toasts persist by default.

Source/usage:
- stores/terminal/console/store.ts: notifyBlockError now passes the block name
  as the title and the error as the description (title/subtext), plus the dedup
  window.
- app/playground/page.tsx: Toast section added to the EMCN gallery.
- New EMCN icons: circle-alert, circle-check, info, triangle-alert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 8, 2026 7:40pm

Request Review

@cursor

cursor Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large UI/behavior change to a global notification surface and workflow block-error toasts; route clearing and dedup alter when users see errors, but no auth or data-path changes.

Overview
Redesigns the EMCN toast with intent variants (info / warning plus existing types), neutral inline leading icons, and toast.info / toast.warning helpers. Stacking is Sonner-style: up to three cards collapse with scale/offset, fan open on card hover/focus, and framer-motion drives enter/reshuffle; hovering pauses auto-dismiss.

Interaction and lifecycle changes: truncated title/description use hover reveal (RevealText); at 2+ toasts a dismiss-all control with a linear ring countdown replaces per-toast timers (and the old countdown ring). Action toasts stay until dismissed unless duration is set; navigation clears the stack. Block error toasts in the terminal console store use block name as title / error as description, with ~1.5s dedup for add/update double-fire.

The playground wraps content in ToastProvider and adds a Toast demo section (variants, actions, stacking).

Reviewed by Cursor Bugbot for commit d47c3ae. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d47c3ae. Configure here.

reduceMotion={reduceMotion}
resetKey={toasts[toasts.length - 1]?.id ?? ''}
onDismiss={dismissAllToasts}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stack timer clears actionable toasts

Medium Severity

When two or more toasts are visible, StackDismiss always auto-calls dismissAllToasts after STACK_DISMISS_MS, even if a toast was created with an action and duration: 0 to stay until dismissed. Block error “Fix in Copilot” toasts can vanish while the user still has other notifications in the stack.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d47c3ae. Configure here.

const dedupKey = `${displayName}: ${errorMessage}`
const lastShownAt = recentErrorNotifications.get(dedupKey)
if (lastShownAt !== undefined && now - lastShownAt < NOTIFY_DEDUP_WINDOW_MS) return
recentErrorNotifications.set(dedupKey, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dedup key skips error normalization

Medium Severity

notifyBlockError builds its dedup key from String(error), while addConsole passes createdEntry.error after normalizeConsoleError. The same failure from addConsole and updateConsole can produce different keys or descriptions, so duplicate error toasts can still appear within the dedup window.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d47c3ae. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the previous toast implementation with a fully custom component: intent variants (default/info/success/warning/error) with inline outline icons, Sonner-style collapsed stacking that fans open on hover, a "hover-reveal" for truncated text, a dismiss-all chip with an animated linear countdown ring, and persistent actionable toasts. The store layer gains a 1 500 ms dedup window so the add/update double-fire from block errors produces only one notification.

  • toast.tsx is a self-contained client component with its own context + module-level singleton bridge; it handles stacking geometry, per-toast timers, route-scoped clearing, and the StackDismiss countdown entirely in React state/effects.
  • Four new icon components (CircleAlert, CircleCheck, Info, TriangleAlert) follow the existing SVG icon pattern and are exported through the barrel index.
  • store.ts adds recentErrorNotifications dedup and surfaces block name as the toast title with the error as the description, matching the new hierarchy design.

Confidence Score: 4/5

Safe to merge; the core toast logic is sound and the store dedup is a clear improvement over the previous double-fire behavior.

The implementation is well-thought-out: timers, route-scoped clearing, reduced-motion support, and the singleton bridge all behave correctly. The evicted-toast heights leak is a bounded memory accumulation (one stale number per evicted card, never accessed for layout), not a functional defect. The resetKey restart-on-dismiss is a UX ambiguity rather than wrong behavior. No auth, data, or crash risks are introduced.

toast.tsx deserves a second look — specifically the heights cleanup on STACK_LIMIT eviction and the StackDismiss resetKey semantics.

Important Files Changed

Filename Overview
apps/sim/components/emcn/components/toast/toast.tsx New toast system: stacking, hover-reveal, dismiss-all, intent icons; well-structured with one minor heights-map leak on eviction and a non-obvious resetKey restart on dismiss.
apps/sim/stores/terminal/console/store.ts Adds dedup window for block-error notifications and splits block name / error into title/description for the new toast; clean and well-guarded.
apps/sim/components/emcn/icons/index.ts Adds four new icon exports (CircleAlert, CircleCheck, Info, TriangleAlert) to the barrel index.
apps/sim/app/playground/page.tsx Wraps the playground in ToastProvider and adds an interactive Toast demo section; re-indentation from the wrapper is the bulk of the diff.
apps/sim/components/emcn/icons/circle-alert.tsx New CircleAlert SVG icon for error intent; correct path data and aria-hidden.
apps/sim/components/emcn/icons/circle-check.tsx New CircleCheck SVG icon for success intent; correct path data and aria-hidden.
apps/sim/components/emcn/icons/info.tsx New Info SVG icon for info intent; correct path data and aria-hidden.
apps/sim/components/emcn/icons/triangle-alert.tsx New TriangleAlert SVG icon for warning intent; correct triangle path and aria-hidden.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["toast.error / toast.success / etc."] --> B{globalToast set?}
    B -- No --> C[throw Error]
    B -- Yes --> D["ToastProvider.addToast"]
    D --> E["setToasts: append + slice -STACK_LIMIT"]
    E --> F{"toasts.length >= threshold?"}
    F -- Yes --> G["Clear per-toast timers — StackDismiss takes over"]
    F -- No --> H["Set per-toast setTimeout for each toast"]
    G --> I["StackDismiss ring counts down 6s"]
    I -->|"complete or click"| J["dismissAllToasts: clear all"]
    H -->|"timer fires"| K["dismissToast id"]
    K --> L["Clear timer + filter toasts + cleanup heights"]
    E --> N["Portal renders motion.ol with layout geometry"]
    N --> O{"stack hovered?"}
    O -- Yes --> P["Fan open: full heights + cumulative y offsets"]
    O -- No --> Q["Collapsed pile: depth scale + offset"]
    P & Q --> R["ToastItem renders with geometry"]
    R --> S["useLayoutEffect measures height via ResizeObserver"]
    S -->|"onMeasure"| N
    R --> T["hover: RevealText tail blurs in"]
Loading

Reviews (1): Last reviewed commit: "feat(emcn/toast): redesign toast — inten..." | Re-trigger Greptile

Comment on lines 588 to 606
@@ -293,9 +593,15 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
description: input.description,
variant: input.variant ?? 'default',
action: input.action,
duration: input.duration ?? AUTO_DISMISS_MS,
/**
* Actionable toasts persist until dismissed (`duration: 0`) so the
* action — e.g. "Fix in Copilot" — can't disappear before the user
* reacts. An explicit `duration` always wins. Everything else auto-
* dismisses after the default window.
*/
duration: input.duration ?? (input.action ? 0 : AUTO_DISMISS_MS),
}
setToasts((prev) => [...prev, data].slice(-MAX_VISIBLE))
setToasts((prev) => [...prev, data].slice(-STACK_LIMIT))
return id
}, [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Evicted toasts leak their heights entry. When addToast slices the oldest toast off via .slice(-STACK_LIMIT), dismissToast is never called for it, so its measured height stays in the heights map indefinitely. Over a session with many rapid arrivals the map accumulates stale entries. The fix is to clean up evicted IDs inside addToast's state setter.

Suggested change
const addToast = useCallback((input: ToastInput): string => {
const id = generateId()
const data: ToastData = {
id,
message: input.message,
description: input.description,
variant: input.variant ?? 'default',
action: input.action,
/**
* Actionable toasts persist until dismissed (`duration: 0`) so the
* action e.g. "Fix in Copilot" can't disappear before the user
* reacts. An explicit `duration` always wins. Everything else auto-
* dismisses after the default window.
*/
duration: input.duration ?? (input.action ? 0 : AUTO_DISMISS_MS),
}
setToasts((prev) => {
const next = [...prev, data].slice(-STACK_LIMIT)
if (prev.length >= STACK_LIMIT) {
const evictedIds = new Set(next.map((t) => t.id))
const dropped = prev.filter((t) => !evictedIds.has(t.id))
if (dropped.length > 0) {
setHeights((h) => {
const cleaned = { ...h }
for (const t of dropped) delete cleaned[t.id]
return cleaned
})
}
}
return next
})
return id
}, [])

Comment on lines +750 to +754
paused={expanded}
reduceMotion={reduceMotion}
resetKey={toasts[toasts.length - 1]?.id ?? ''}
onDismiss={dismissAllToasts}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 resetKey restarts countdown on last-toast dismiss, not only on new arrivals. resetKey is set to toasts[toasts.length - 1]?.id, so it changes whenever the tail of the array changes — including when the currently-last toast is dismissed. For example: with three toasts [A, B, C], dismissing C leaves [A, B] and changes resetKey to B.id, restarting the 6-second ring even though no new toast arrived. If that's intentional (giving users a fresh window when actively managing the stack) a brief code comment would prevent future confusion.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant