From 05408fd1ce0468a4d914942f5a646ef3b41051f6 Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 8 Jun 2026 22:59:19 -0500 Subject: [PATCH] =?UTF-8?q?feat(emcn/toast):=20toast=20redesign=20?= =?UTF-8?q?=E2=80=94=20intent=20variants,=20stacking,=20hover=20reveal,=20?= =?UTF-8?q?dismiss-all=20(#4909)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(emcn/toast): redesign toast — intent variants, Sonner-style stack, 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 * fix(emcn): rename Info icon to CircleInfo to resolve barrel collision The toast redesign added an `Info` icon, but the emcn barrel already re-exports an `Info` component (`export * from './components'` + `export * from './icons'`), so the top-level `@/components/emcn` had a duplicate `Info` export (TS2308). Rename the icon to CircleInfo — matching its circle-shaped siblings (CircleAlert, CircleCheck) and resolving the collision. The icon is consumed only by toast.tsx. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(emcn/toast): keep persistent toasts alive in a stack; prune stale heights H1 (functional): a stack of 2+ toasts arms the StackDismiss ring, whose 6s countdown called dismissAllToasts() — wiping persistent (duration<=0) actionable toasts like 'Fix in Copilot' before the user could react. Add an `autoDismiss` flag (false when any toast is persistent) that suppresses the auto-countdown while keeping the manual dismiss-all button. H2 (memory): stack-limit eviction (slice) dropped the oldest toast without clearing its `heights` entry, leaking entries over a session. Reconcile heights to live toast ids, mirroring the timer effect's stale-entry cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(emcn/toast): tint variant icons; polish clear-all, countdown reset, teardown - Req 6: per-variant icon tint (error/warning/success/info) from the shared intent palette, so error vs info is distinguishable pre-attentively in a mixed stack instead of only by reading the copy. Default stays neutral. - H3: wrap the stack in AnimatePresence so clear-all / route-change fades the frozen stack out instead of cutting abruptly; per-card exits still play for single dismissals. No pointer-events change (no hover regression). - Countdown reset now keys on a monotonic arrival counter, so dismissing the front card no longer restarts the whole stack's auto-dismiss timer. - Provider teardown only nulls the global toast bindings if they're still its own, guarding against an out-of-order unmount with a second provider. Co-Authored-By: Claude Opus 4.8 (1M context) * fix: keep modal errors inside the modal instead of a screen-corner toast Per the toast spec (modal errors stay in the modal; only page errors go to the corner), route two in-modal failures inline: - add-people-modal: failures now render via ChipModalError in the modal body (the modal stays open with the failed emails), not toast.error. - slack setup wizard: a clipboard-copy failure shows inline beneath the copy button (mirroring the existing 'copied' state), not a toast. Left as-is (verified correct, not violations): credential-detail save error (a routed page, not a modal — page toast is correct) and import-csv success (fires as the dialog closes — inline isn't possible). Co-Authored-By: Claude Opus 4.8 (1M context) * fix(data-drains): surface CreateDrainModal submit error inline Completes the modal-error migration: the create-drain form's submit failure now renders via ChipModalError in the modal body (the modal stays open) instead of a toast.error that escaped to the screen corner. The success-on-close toast and the page-level drain-row action toasts are unchanged (correct). Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(emcn/toast): trim comments to concise TSDoc per project convention Remove inline // and JSX {/* */} comments (the project documents with TSDoc only) and condense multi-paragraph TSDoc to 1-3 lines across toast.tsx, plus the stray inline comments in the console store and slack wizard. Behavior and code are unchanged — verified the non-comment source is byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(emcn/toast): inset on the /w index route; stronger, unified intent tints - Positioning: the workflow-list index route /workspace/[id]/w renders the panel + terminal but failed includes('/w/'), so a toast there rendered under the panel. Match /\/w(\/|$)/ so both the editor and the index inset. - Tint: unify all four intent icons onto the badge palette and use --badge-success-text (a darker green) so success isn't ~1.9:1 / washed out on the light card; error/success now match warning/info's token family. - Gate the card enter animation on reduceMotion for consistency with the rest of the file (behaviour was already correct via duration 0). Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(upgrade): surface billing errors via toast instead of native alert() The upgrade flow used native alert() for upgrade / switch-plan / switch-interval failures. Route them through the unified toast (toast.error) like the rest of the app — it's inside the ToastProvider tree. Co-Authored-By: Claude Opus 4.8 (1M context) * chore(emcn): drop CountdownRing barrel export + dead toast keyframes Completes removing what the framer-motion toast redesign orphaned: the CountdownRing re-export and the zero-reference notification-/toast-* keyframes. * fix(console): normalize error in notifyBlockError so dedup keys match addConsole passes a normalizeConsoleError'd error while updateConsole passed the raw update.error, so the dedup key (String(error)) differed between the two paths and the duplicate block-error toast could still slip through. Normalize inside notifyBlockError so both paths produce the same key and description. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(emcn/toast): expire timed toasts stacked beside a persistent one Suppressing the stack auto-countdown when a persistent toast is present (so it can't clear the action) also stopped timed toasts in that stack from expiring. Fall back to per-toast timers whenever the stack countdown isn't auto-firing, so timed toasts auto-dismiss individually while the persistent one stays. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(add-people): keep the specific add-failure reason in the inline error The inline ChipModalError dropped the server/validation detail the toast used to show via description. Fold getErrorMessage(firstError) back into the message so users still see why the add failed. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(emcn/toast): don't evict a persistent toast at the stack cap slice(-STACK_LIMIT) dropped the oldest card on a 4th arrival even if it was a persistent (actionable) error meant to stay until dismissed. Evict the oldest auto-dismissable toast instead, falling back to the oldest only when every toast is persistent. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(console): scope error-dedup to the block execution The dedup keyed on block+message for 1.5s, but the toast stack clears on navigation — so a genuinely new same-block error within that window (e.g. a re-run, or after navigating) was suppressed with no replacement toast. Key the dedup on the block execution (getBlockExecutionKey) so only the same execution's addConsole/updateConsole double-fire collapses; a different execution always toasts. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(emcn/toast): let global status toasts survive navigation (persistAcrossRoutes) The route-clear dismissed every toast on navigation, including the persistent realtime connection/reconnect status toast — which then never re-showed (the provider's id ref short-circuited). Add a persistAcrossRoutes flag: the navigation clear now keeps flagged toasts and only drops route-scoped ones, and the WorkspacePermissionsProvider status toasts set it. Page-scoped toasts (including actionable block errors) still clear on navigation as before. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(emcn/toast): reset expanded when the stack empties The hover wrapper unmounts without firing mouse-leave when the last toast goes (dismiss / clear-all / navigation), so expanded could stay true and stop the next toasts from auto-dismissing. Force expanded false whenever the stack is empty. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: andresdjasso Co-authored-by: Claude Opus 4.8 Co-authored-by: waleed --- apps/sim/app/_styles/globals.css | 53 - apps/sim/app/playground/page.tsx | 1673 +++++++++-------- .../components/add-people-modal.tsx | 14 +- .../workspace-permissions-provider.tsx | 3 +- .../upgrade/hooks/use-upgrade-state.ts | 7 +- .../[workspaceId]/upgrade/upgrade.tsx | 4 +- .../slack-setup-wizard/slack-setup-wizard.tsx | 11 +- apps/sim/components/emcn/components/index.ts | 1 - .../emcn/components/toast/countdown-ring.tsx | 37 - .../emcn/components/toast/toast.tsx | 738 ++++++-- .../components/emcn/icons/circle-alert.tsx | 27 + .../components/emcn/icons/circle-check.tsx | 26 + .../sim/components/emcn/icons/circle-info.tsx | 29 + apps/sim/components/emcn/icons/index.ts | 4 + .../components/emcn/icons/triangle-alert.tsx | 27 + .../components/data-drains-settings.tsx | 6 +- apps/sim/stores/terminal/console/store.ts | 40 +- 17 files changed, 1617 insertions(+), 1083 deletions(-) delete mode 100644 apps/sim/components/emcn/components/toast/countdown-ring.tsx create mode 100644 apps/sim/components/emcn/icons/circle-alert.tsx create mode 100644 apps/sim/components/emcn/icons/circle-check.tsx create mode 100644 apps/sim/components/emcn/icons/circle-info.tsx create mode 100644 apps/sim/components/emcn/icons/triangle-alert.tsx diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index ab696279ff..2048673ec5 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -844,59 +844,6 @@ input[type="search"]::-ms-clear { } } -@keyframes notification-enter { - from { - opacity: 0; - transform: translateX(calc(var(--stack-offset, 0px) - 8px)) scale(0.97); - } - to { - opacity: 1; - transform: translateX(var(--stack-offset, 0px)) scale(1); - } -} - -@keyframes notification-countdown { - from { - stroke-dashoffset: 0; - } - to { - stroke-dashoffset: 34.56; - } -} - -@keyframes notification-exit { - from { - opacity: 1; - transform: translateX(var(--stack-offset, 0px)) scale(1); - } - to { - opacity: 0; - transform: translateX(calc(var(--stack-offset, 0px) + 8px)) scale(0.97); - } -} - -@keyframes toast-enter { - from { - opacity: 0; - transform: translateX(var(--stack-offset, 0px)) translateY(8px) scale(0.97); - } - to { - opacity: 1; - transform: translateX(var(--stack-offset, 0px)) translateY(0) scale(1); - } -} - -@keyframes toast-exit { - from { - opacity: 1; - transform: translateX(var(--stack-offset, 0px)) translateY(0) scale(1); - } - to { - opacity: 0; - transform: translateX(var(--stack-offset, 0px)) translateY(8px) scale(0.97); - } -} - /* WandPromptBar status indicator */ @keyframes smoke-pulse { 0%, diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index 1cc5ddf50b..bd5a2ebb64 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -78,9 +78,11 @@ import { type TagItem, Textarea, TimePicker, + ToastProvider, Tooltip, Trash, Trash2, + toast, Undo, Wrap, ZoomIn, @@ -164,809 +166,910 @@ export default function PlaygroundPage() { } return ( - -
-
- - - - - Go back - -
-
- - - - - {isDarkMode ? 'Light mode' : 'Dark mode'} - -
-
-
-

- EMCN Component Playground -

-

- All emcn UI components and their variants -

+ + +
+
+ + + + + Go back +
- - {/* Button */} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - {/* ButtonGroup */} -
- - - cURL - Python - JavaScript - - - - - Option 1 - Option 2 - - - - - Option 1 - Option 2 - - - - - Option 1 - Option 2 - - - - - Only Option - - -
- - {/* Badge */} -
- - Default - - - Outline - - - Type - - - Green - - With Dot - - - - Red - - With Dot - - - - Blue - - With Dot - - - - Blue Secondary - - - Purple - - - Orange - - - Amber - - - Teal - - - Cyan - - - Gray - - - Gray Secondary - - - Small - Medium - Large - -
- - {/* Input */} -
- - - - - - -
- - {/* TagInput */} -
- -
- { - const isValid = value.includes('@') && value.includes('.') - setTagItems((prev) => [...prev, { value, isValid }]) - return isValid - }} - onRemove={(_, index) => { - setTagItems((prev) => prev.filter((_, i) => i !== index)) - }} - placeholder='Enter emails...' - placeholderWithTags='Add another' - /> -
-
- -
- true} - onRemove={() => {}} - placeholder='Add tags' - placeholderWithTags='Add another' - triggerKeys={['Enter', ',']} - /> -
-
- -
- false} - onRemove={() => {}} - placeholder='Disabled input' - disabled - /> -
-
-
- - {/* Textarea */} -
-