Skip to content

Comments

feat(toast): add useToast hook with stacked notifications, mobile support, and docs#3493

Open
sergiocarracedo wants to merge 31 commits intofeat/dialogfrom
feat/toast
Open

feat(toast): add useToast hook with stacked notifications, mobile support, and docs#3493
sergiocarracedo wants to merge 31 commits intofeat/dialogfrom
feat/toast

Conversation

@sergiocarracedo
Copy link
Collaborator

@sergiocarracedo sergiocarracedo commented Feb 20, 2026

🚪 Why?

Problem

The app needed a toast notification system that could be used by developers through a simple hook API. The system needed to handle multiple simultaneous notifications gracefully (stacking/collapsing), work correctly on mobile viewports, and be well-documented so developers know how to use it.

https://www.figma.com/design/dzE3YAvPHj0bJ2FpHTBixp/%F0%9F%9A%A7-Toast?node-id=1-2831&p=f&t=yjV91SFE637oYGRs-0

Screen.Recording.2026-02-20.at.17.03.48.mov

🔑 What?

Changes

  • useToast hook — imperatively trigger toast notifications with variants (default, success, warning, error), configurable duration, actions (buttons/links), and programmatic control (removeToast, clearAll, custom ids)
  • ToastProvider + ToastsContainer — renders toasts in a fixed bottom-right panel; manages two zones: an active area (oldest N toasts, fully interactive) and a stacked area (overflow toasts collapsed behind the active zone with scale/offset animation; expands on hover to show all)
  • Stacked layout with animations — uses Framer Motion AnimatePresence + layout for smooth promotion of stacked toasts into the active area; suppresses spurious exit animations on promoted items via a ghost element strategy
  • Mobile support — at < 640px all toasts go into a single non-expandable stack (no active area, no hover expansion); the front toast remains fully interactive; container width is w-full sm:w-[350px]
  • useIsDesktop / useIsMobile hooks — thin wrappers around useMediaQuery('(min-width: 640px)') exported from lib/exports.ts
  • Developer documentation (useToast.mdx) — Storybook MDX page covering setup, basic usage, variants, actions, duration/persistence, programmatic control, an interactive demo, and full type reference

Ordering rules

  • items array is oldest-first; active = items.slice(0, minActiveToasts) rendered via flex-col-reverse so oldest sits at the bottom
  • stacked = items.slice(minActiveToasts) — overflow toasts, oldest-first so index 0 is at front of stack (highest z-index)
  • When expanded (hovered), stacked items appear newest-at-top via CSS order

✅ Verification

Tests

  • No existing test suite for toast; no new unit tests added (none expected per codebase conventions for this component)
  • Pre-commit hooks passed: cycle-dependencies ✔, format-react ✔, lint-react (0 warnings, 0 errors) ✔

Manual Verification

  • Toasts appear bottom-right, stack correctly, and expand on hover on desktop
  • On mobile (< 640px) all toasts collapse into a single stack with no hover expansion
  • Active toast timer runs and toast dismisses after duration
  • clearAll, removeToast, and custom id work as expected
  • MDX documentation page renders in Storybook with correct sections and interactive demo

Note

Medium Risk
Adds a new global notification system wired into F0Provider and uses portal + animation/timer logic; regressions could affect app-wide rendering/perf and notification lifecycle, but changes are isolated to new code paths.

Overview
Adds a new toast notification API to packages/react via useToast/ToastProvider, including stacking/promotion animations, hover-to-expand behavior, and mobile-specific limits, with rendering done through a portal.

Introduces an internal F0Toast component (variants, actions, progress/timer auto-dismiss) plus Storybook stories/MDX documentation, and wires ToastProvider into F0Provider so the hook works by default. Also exports new viewport helpers (useIsDesktop/useIsMobile), updates Storybook to include src/internal, tweaks several story titles, and pins the Chromatic GitHub Action to v13.

Written by Cursor Bugbot for commit 6bcd4bc. This will update automatically on new commits. Configure here.

- Add clearAll function to ToastProvider context and useToast hook
- Add Clear All button to playground
- Add entry animation for new stacked items (fade in from top)
- Make expand animation speed dynamic based on item count
- Fix bottom margin for stacked container
- minUncollapsedToasts -> minActiveToasts
- collapsedItems -> stackedItems
- collapsed variable -> stacked
- variant 'collapsed' -> 'stacked'
- Updated comments to use 'stacked' terminology
@sergiocarracedo sergiocarracedo requested a review from a team as a code owner February 20, 2026 16:04
@github-actions github-actions bot added feat react Changes affect packages/react labels Feb 20, 2026
@github-actions
Copy link
Contributor

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3493 to install the package

Use pnpm i github:factorialco/f0#2eeee3c7cf1ca0326a9fc9c68dca12fc81dad0a8 to install this specific commit

@github-actions
Copy link
Contributor

🔍 Visual review for your branch is published 🔍

Here are the links to:

Copy link

@cursor cursor bot left a comment

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 3 potential issues.


const addToast = useCallback((toast: ToastProviderItem) => {
setItems((prev) => [...prev, toast])
}, [])
Copy link

Choose a reason for hiding this comment

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

Duplicate toast IDs not replaced, just appended

High Severity

The addToast function always appends to the items array without checking for an existing toast with the same id. The documentation explicitly promises that "calling with the same id again replaces the existing toast," but the current implementation creates duplicates instead. This breaks the custom-ID deduplication feature and the documented API contract.

Additional Locations (1)

Fix in Cursor Fix in Web

const [remainingTime, setRemainingTime] = useState(duration || 0)
const [isPaused, setIsPaused] = useState(false)
const startTimeRef = useRef<number | null>(null)
const animationFrameRef = useRef<number | null>(null)
Copy link

Choose a reason for hiding this comment

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

Unused refs leftover from previous implementation

Low Severity

startTimeRef and animationFrameRef appear to be leftovers from a previous implementation approach. startTimeRef is assigned a value but never read for any computation. animationFrameRef is checked and cleared but never assigned a value from requestAnimationFrame, so it's always null — making the cancelAnimationFrame call a no-op. Both can be removed to reduce confusion.

Fix in Cursor Fix in Web

*/
variant?: F0ToastVariant
/**

Copy link

Choose a reason for hiding this comment

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

Stray JSDoc opening creates malformed comment block

Low Severity

An orphaned /** on line 18 (likely a leftover from a deleted property's JSDoc) merges with the actual /** on line 20 into a single malformed comment block. The resulting JSDoc for the actions property contains garbled content, which will show a messy tooltip in IDE IntelliSense. Removing the stray /** and blank line fixes it.

Fix in Cursor Fix in Web

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

Labels

feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant