fix(toast): sticky toasts + improve update notification visibility (P5-40)

Add sticky?: boolean to ToastNotif — sticky toasts skip the 4s auto-dismiss
timer entirely, staying until the user clicks or manually dismisses. Sticky
toasts also use cyan accent/glow (vs orange for messages) and allow the body
to wrap rather than truncate, so longer action-oriented copy is fully readable.

Update the Tauri update toast to: sticky: true, ⬆ prefix on the title,
"Click to install and restart" as explicit call to action.

Fixes: auto-dismiss before user noticed it, no visual distinction from
a regular message notification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 21:04:49 -04:00
parent af58f7a32c
commit 950b8a8128
3 changed files with 14 additions and 8 deletions
+10 -6
View File
@@ -32,13 +32,14 @@ function ToastCard({ toast }: ToastCardProps) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
if (toast.sticky) return;
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
dismiss(toast.id); dismiss(toast.id);
}, 4000); }, 4000);
return () => { return () => {
if (timerRef.current !== null) clearTimeout(timerRef.current); if (timerRef.current !== null) clearTimeout(timerRef.current);
}; };
}, [dismiss, toast.id]); }, [dismiss, toast.id, toast.sticky]);
const handleCardClick = () => { const handleCardClick = () => {
if (toast.onClick) { if (toast.onClick) {
@@ -58,12 +59,14 @@ function ToastCard({ toast }: ToastCardProps) {
const cardStyle: CSSProperties = { const cardStyle: CSSProperties = {
position: 'relative', position: 'relative',
background: 'var(--lt-bg-card)', background: 'var(--lt-bg-card)',
border: '1px solid var(--lt-border-color)', border: toast.sticky
? '1px solid var(--lt-accent-cyan-border)'
: '1px solid var(--lt-border-color)',
borderRadius: '12px', borderRadius: '12px',
padding: '12px 14px', padding: '12px 14px',
minWidth: '280px', minWidth: '280px',
maxWidth: '340px', maxWidth: '340px',
boxShadow: 'var(--lt-box-glow-orange)', boxShadow: toast.sticky ? 'var(--lt-box-glow-cyan)' : 'var(--lt-box-glow-orange)',
cursor: 'pointer', cursor: 'pointer',
animation: 'lotusToastIn 0.2s ease-out both', animation: 'lotusToastIn 0.2s ease-out both',
userSelect: 'none', userSelect: 'none',
@@ -100,7 +103,7 @@ function ToastCard({ toast }: ToastCardProps) {
}; };
const nameStyle: CSSProperties = { const nameStyle: CSSProperties = {
color: 'var(--lt-accent-orange)', color: toast.sticky ? 'var(--lt-accent-cyan)' : 'var(--lt-accent-orange)',
fontWeight: 600, fontWeight: 600,
fontSize: '0.85rem', fontSize: '0.85rem',
overflow: 'hidden', overflow: 'hidden',
@@ -127,8 +130,9 @@ function ToastCard({ toast }: ToastCardProps) {
fontSize: '0.82rem', fontSize: '0.82rem',
margin: '4px 0 2px', margin: '4px 0 2px',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', ...(toast.sticky
whiteSpace: 'nowrap', ? { whiteSpace: 'normal', lineHeight: 1.4 }
: { textOverflow: 'ellipsis', whiteSpace: 'nowrap' }),
}; };
const roomNameStyle: CSSProperties = { const roomNameStyle: CSSProperties = {
+3 -2
View File
@@ -459,11 +459,12 @@ function TauriUpdateFeature() {
firedRef.current = status.version; firedRef.current = status.version;
setToast({ setToast({
id: `tauri-update-${status.version}`, id: `tauri-update-${status.version}`,
displayName: 'Update Available', displayName: 'Update Available',
body: `Lotus Chat ${status.version} is ready to install.`, body: `Lotus Chat ${status.version} is ready. Click to install and restart.`,
roomName: 'System', roomName: 'System',
roomId: '', roomId: '',
onClick: install, onClick: install,
sticky: true,
}); });
}, [status, setToast, install]); }, [status, setToast, install]);
+1
View File
@@ -9,6 +9,7 @@ export type ToastNotif = {
roomId: string; roomId: string;
hashPath?: string; // overrides window.location.hash navigation when set hashPath?: string; // overrides window.location.hash navigation when set
onClick?: () => void; // custom click handler; skips hash navigation when set onClick?: () => void; // custom click handler; skips hash navigation when set
sticky?: boolean; // when true, does not auto-dismiss — use for action toasts that require a click
}; };
const baseAtom = atom<ToastNotif[]>([]); const baseAtom = atom<ToastNotif[]>([]);