Skip to content

fix(narratives): portal the toast to body and pin it top-right - #579

Closed
pablonoel wants to merge 3 commits into
datacommonsorg:mainfrom
pablonoel:narratives_ux-response_spacing
Closed

pablonoel wants to merge 3 commits into
datacommonsorg:mainfrom
pablonoel:narratives_ux-response_spacing

Conversation

@pablonoel

@pablonoel pablonoel commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Move the toast to a body portal so it can appear above stacking contexts, animate its enter and exit, and use theme utility classes for its styling.

Overview

The PDF export toast was rendered inside the answer panel. Some of that panel's ancestors create their own stacking context, so the toast's z-50 only counted within that context and sticky layers (z-[5]) could cover it at some viewport sizes.

The toast now renders through createPortal(..., document.body), the same way tooltip.tsx already does. This takes it out of the panel's stacking contexts, so z-50 places it above the page. Tooltips (z-index: 60) still layer over it. It moves from bottom-center above the prompt box to bottom-right on desktop, and spans the width at the bottom on mobile.

It now fades in sliding up and fades out sliding down, instead of appearing and disappearing abruptly. The component stays mounted until the exit animation has finished, then calls onDismiss.

While in the file, the inline style objects and hard-coded colour/font constants were replaced with Tailwind theme utilities, per narratives/FRONTEND.md.

Related Issues

Fixes # (issue number)

Changes Made

  • narratives/ui/src/components/toast.tsx
    • Render via createPortal into document.body.
    • Position: fixed bottom-3 left-4 right-4 sm:left-auto sm:right-6 sm:max-w-xl z-50, with the text centred (was left-1/2 bottom-24 with a translateX(-50%)). Only standard utilities, no arbitrary values.
    • Motion: enters with the existing animate-enter token (300ms fade up) and leaves with a new animate-exit token (200ms, M3 standard accelerate). Both are gated with motion-safe:. Dismissal is timer-based and checks useReducedMotion() from motion/react, so reduced-motion users dismiss immediately rather than waiting on an animationend that never fires.
    • The 5s visible timer no longer restarts when the parent re-renders. Previously it depended on the inline onDismiss arrow, so the countdown reset on every render of the answer panel.
    • Removed the unused actionLabel / onAction props and their button. The only caller, the PDF export, never passed them.
    • Styling moved from inline style to utility classes: bg-theme-primary, text-on-theme-primary, text-body-medium, rounded, min-h-12, and padding equivalents. Removed COLOR_SURFACE, COLOR_TEXT and FONT_STACK.
    • Visual change: the background is now the theme primary with white text, instead of the M3 inverse surface (#322F35).
    • data-non-print="true" is kept, so the print stylesheet still hides the portalled node.
  • narratives/ui/src/index.css
    • New --color-on-theme-primary token: var(--theme-on-primary, #FFFFFF). Instances with a light primary can override it to keep 4.5:1 contrast.
    • New --animate-exit token and exit keyframes, the reverse of enter.
  • narratives/ui/src/components/toast.test.tsx (new)
    • EXIT_MS matches the --animate-exit duration in index.css, following the existing HANDOFF_MS check in data_agent.test.tsx.
    • The exit animation plays before onDismiss is called.
    • Under reduced motion, onDismiss fires without waiting for the exit.
  • UI screenshots: TODO — before/after at desktop and mobile widths, plus a recording of the enter/exit animation.

Testing Done

From narratives/ (Node v24.22.1 per .nvmrc):

  • pnpm lint:ui: tsc --noEmit clean; Biome checked 59 files, no issues.
  • pnpm test:ui: 33 files, 356 tests passed.
  • pnpm build:ui: built successfully.

Reviewer repro: pnpm dev:ui, ask a question, wait for the answer to finish, and click Export PDF. "PDF downloaded" should slide up into the bottom-right (full width on mobile) above the page, stay about 5s, then slide down and disappear. With the OS "reduce motion" setting on, it should appear and disappear without animating.

  • Unit tests passed
  • Integration tests passed
  • Manual verification
  • Any updated goldens or fixtures were reviewed and are intentional

Risk & Rollback

None. This is a UI-only change to one component with a single caller (the PDF export confirmation). To roll back, revert the commit.

Follow-ups

None.

Checklist

  • I have read AGENTS.md and followed CODING_GUIDELINES.md, plus FRONTEND.md for UI changes.
  • I have run the app's lint, test, and build commands, as documented in that application's guide.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings.

Note: Only Maintainers can approve and merge PRs. Expected initial review time: 3 business days.

Move the toast to a body portal so it can appear above stacking contexts, and use theme utility classes for its styling.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request refactors the Toast component to use React Portals, rendering it into the document body to avoid stacking context issues, and replaces inline styles with Tailwind CSS classes. The review feedback correctly points out a violation of the style guide regarding arbitrary Tailwind values and warns about potential server-side rendering (SSR) issues when accessing 'document.body' directly.

Comment on lines +55 to 63
return createPortal(
<div
role="status"
aria-live="polite"
data-non-print="true"
className="fixed left-1/2 bottom-24 z-50 flex items-center shadow-lg"
style={{
transform: 'translateX(-50%)',
background: COLOR_SURFACE,
color: COLOR_TEXT,
borderRadius: 4,
padding: actionLabel ? '6px 8px 6px 16px' : '14px 16px',
gap: 8,
minHeight: 48,
maxWidth: 'min(560px, calc(100vw - 32px))',
fontFamily: FONT_STACK,
fontSize: 14,
lineHeight: '20px',
}}
className={`fixed top-3 right-6 z-50 flex items-center gap-2 min-h-12 max-w-[min(560px,calc(100vw-2rem))] rounded bg-theme-primary text-white text-body-medium shadow-lg ${
actionLabel ? 'py-1.5 pl-4 pr-2' : 'py-3.5 px-4'
}`}
>

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.

medium

[WARNING] Avoid arbitrary Tailwind classes and ensure SSR safety.

Explanation

  1. Arbitrary Tailwind Class: The class max-w-[min(560px,calc(100vw-2rem))] on line 60 uses an arbitrary value, which violates the repository style guide for the narratives/ application (Section 11, line 211). We can achieve the same responsive layout using standard Tailwind classes like left-4 right-4 sm:left-auto sm:right-6 sm:max-w-xl.
  2. SSR Safety: Accessing document.body directly on line 78 during server-side rendering (SSR) will throw a ReferenceError: document is not defined because document is not available on the server. Adding a guard clause if (typeof window === 'undefined') return null; prevents this error.

References

  • Repository Style Guide (Section 11 - Narratives): "Standard utilities only; avoid arbitrary values (e.g. h-[3px])."
  if (typeof window === 'undefined') {
    return null;
  }

  return createPortal(
    <div
      role="status"
      aria-live="polite"
      data-non-print="true"
      className={`fixed top-3 left-4 right-4 sm:left-auto sm:right-6 z-50 flex items-center gap-2 min-h-12 sm:max-w-xl rounded bg-theme-primary text-white text-body-medium shadow-lg ${
        actionLabel ? 'py-1.5 pl-4 pr-2' : 'py-3.5 px-4'
      }`}
    >
References
  1. Standard utilities only; avoid arbitrary values (e.g. h-[3px]). (link)

@pablonoel pablonoel changed the title Portal toast notifications above page layers fix(narratives): portal the toast to body and pin it top-right Nov 1, 2026
Use inset spacing on narrow screens so toasts fit within the viewport, and restore right alignment with a wider maximum width at the `sm` breakpoint.
Keep the toast mounted through its exit animation, dismiss immediately when reduced motion is enabled, and add tests for timing. Move it to the bottom-right and use a theme-aware text color.
@pablonoel pablonoel closed this Nov 1, 2026
@pablonoel
pablonoel deleted the narratives_ux-response_spacing branch November 1, 2026 05:59
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