Files
deepseek-harness/packages/client/ui-primitives/src/Toast.tsx
T
creatixchu 87e3c95027 fix(web): review-round attachment refinements
Body-portal the lightbox and toast so transformed ancestors cannot trap
their fixed positioning (a lightbox opened from a chat message covered only
the chat column); make the toast pointer-transparent; observe the rail
element's own size instead of window resizes; consume vertical wheel ticks
exclusively via a non-passive listener with LINE/PAGE delta normalization;
keep the start position when the rail mounts over an existing draft; honor
prefers-reduced-motion for the toast, remove-control, and paging; retry
loads through the guarded load effect; note the deliberate promptError
re-announce; pin the intake toast in the assembled snapshot; sync the
superseded multimodal note and package docs.
2026-08-11 17:45:11 +08:00

42 lines
1.5 KiB
TypeScript

import { useEffect } from 'react'
import type { ReactNode } from 'react'
import { createPortal } from 'react-dom'
import css from './Toast.module.css'
/** Full-opacity hold before the fade starts. Must agree with the stylesheet's
* toast-fade delay (Toast.module.css) or the banner unmounts mid-fade. */
const HOLD_MS = 3000
/** Fade duration. Must agree with the stylesheet's toast-fade duration. */
const FADE_MS = 1000
/**
* Transient top-center banner: slides in, holds at full opacity, fades out,
* then reports done so the owner can unmount it. Re-showing the same text
* restarts the cycle when the owner remounts the component (key it by a
* per-show sequence). Rendered through a body portal so an owner inside a
* transformed or filtered ancestor cannot trap the fixed banner in that
* ancestor's box.
*
* @param props.text - resolved banner copy; the owner passes localized text.
* @param props.icon - optional leading glyph (e.g. a warning icon).
* @param props.onDone - called once the fade completes; unmount the toast here.
* @returns the floating banner.
*/
export function Toast({ text, icon, onDone }: {
text: string
icon?: ReactNode
onDone: () => void
}) {
useEffect(() => {
const timer = setTimeout(onDone, HOLD_MS + FADE_MS)
return () => { clearTimeout(timer) }
}, [onDone])
return createPortal(
<div className={css.toast} role="alert">
{icon !== undefined && <span className={css.icon} aria-hidden>{icon}</span>}
<span className={css.text}>{text}</span>
</div>,
document.body,
)
}