fix(ui): toast cards render on stock themes; gate TDS glow (native-cinny audit 4/N)

LotusToastContainer was styled entirely with --lt-* CSS vars but rendered
unconditionally (not gated on lotusTerminal). Those vars only exist inside the
Lotus Terminal theme's scoped block with no global fallback, so in-app toast
notifications rendered with undefined background/border/colors on every stock
Cinny theme. Now the card uses folds tokens (color.Surface.*/Primary.*,
config.radii/space/borderWidth, color.Other.Shadow) by default, keeping the TDS
--lt-* glow/accents only when lotusTerminal is active. The raw <button> dismiss
control is now a folds IconButton.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:06:33 -04:00
parent 198fd12bb2
commit 127e783f66
+54 -38
View File
@@ -1,6 +1,9 @@
import React, { useEffect, useRef, CSSProperties } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { color, config, Icon, IconButton, Icons } from 'folds';
import { toastQueueAtom, dismissToastAtom, ToastNotif } from '../../state/toast';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
// Inject the keyframe animation once
const STYLE_ID = 'lotus-toast-keyframes';
@@ -29,6 +32,10 @@ type ToastCardProps = {
function ToastCard({ toast }: ToastCardProps) {
const dismiss = useSetAtom(dismissToastAtom);
// Lotus Terminal (TDS) gets its bespoke glow/accents; every other theme uses
// folds tokens so toasts render correctly on stock Cinny themes (the --lt-*
// vars only exist while Terminal mode is active).
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
@@ -56,17 +63,29 @@ function ToastCard({ toast }: ToastCardProps) {
dismiss(toast.id);
};
const accent = toast.sticky ? color.Primary.Main : color.Surface.OnContainer;
const cardStyle: CSSProperties = {
position: 'relative',
background: 'var(--lt-bg-card)',
border: toast.sticky
? '1px solid var(--lt-accent-cyan-border)'
: '1px solid var(--lt-border-color)',
borderRadius: '12px',
padding: '12px 14px',
background: lotusTerminal ? 'var(--lt-bg-card)' : color.Surface.Container,
border: `${config.borderWidth.B300} solid ${
lotusTerminal
? toast.sticky
? 'var(--lt-accent-cyan-border)'
: 'var(--lt-border-color)'
: toast.sticky
? color.Primary.Main
: color.Surface.ContainerLine
}`,
borderRadius: config.radii.R400,
padding: `${config.space.S300} ${config.space.S400}`,
minWidth: '280px',
maxWidth: '340px',
boxShadow: toast.sticky ? 'var(--lt-box-glow-cyan)' : 'var(--lt-box-glow-orange)',
boxShadow: lotusTerminal
? toast.sticky
? 'var(--lt-box-glow-cyan)'
: 'var(--lt-box-glow-orange)'
: `0 8px 24px ${color.Other.Shadow}`,
cursor: 'pointer',
animation: 'lotusToastIn 0.2s ease-out both',
userSelect: 'none',
@@ -75,8 +94,8 @@ function ToastCard({ toast }: ToastCardProps) {
const rowStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: '8px',
marginRight: '20px',
gap: config.space.S200,
marginRight: config.space.S500,
};
const avatarStyle: CSSProperties = {
@@ -91,19 +110,25 @@ function ToastCard({ toast }: ToastCardProps) {
width: '24px',
height: '24px',
borderRadius: '50%',
background: 'var(--lt-accent-orange-dim)',
border: '1px solid var(--lt-accent-orange-border)',
background: lotusTerminal ? 'var(--lt-accent-orange-dim)' : color.Primary.Container,
border: `${config.borderWidth.B300} solid ${
lotusTerminal ? 'var(--lt-accent-orange-border)' : color.Primary.ContainerLine
}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontWeight: 700,
color: 'var(--lt-accent-orange)',
color: lotusTerminal ? 'var(--lt-accent-orange)' : color.Primary.OnContainer,
flexShrink: 0,
};
const nameStyle: CSSProperties = {
color: toast.sticky ? 'var(--lt-accent-cyan)' : 'var(--lt-accent-orange)',
color: lotusTerminal
? toast.sticky
? 'var(--lt-accent-cyan)'
: 'var(--lt-accent-orange)'
: accent,
fontWeight: 600,
fontSize: '0.85rem',
overflow: 'hidden',
@@ -111,22 +136,8 @@ function ToastCard({ toast }: ToastCardProps) {
whiteSpace: 'nowrap',
};
const dismissBtnStyle: CSSProperties = {
position: 'absolute',
top: '8px',
right: '10px',
background: 'none',
border: 'none',
color: 'var(--lt-text-secondary)',
cursor: 'pointer',
fontSize: '14px',
lineHeight: 1,
padding: '2px 4px',
borderRadius: '4px',
};
const bodyStyle: CSSProperties = {
color: 'var(--lt-text-primary)',
color: lotusTerminal ? 'var(--lt-text-primary)' : color.Surface.OnContainer,
fontSize: '0.82rem',
margin: '4px 0 2px',
overflow: 'hidden',
@@ -136,7 +147,7 @@ function ToastCard({ toast }: ToastCardProps) {
};
const roomNameStyle: CSSProperties = {
color: 'var(--lt-text-secondary)',
color: lotusTerminal ? 'var(--lt-text-secondary)' : color.SurfaceVariant.OnContainer,
fontSize: '0.75rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -161,14 +172,19 @@ function ToastCard({ toast }: ToastCardProps) {
}}
aria-label={`Notification from ${toast.displayName} in ${toast.roomName}`}
>
<button
type="button"
style={dismissBtnStyle}
onClick={handleDismiss}
aria-label="Dismiss notification"
>
×
</button>
<span style={{ position: 'absolute', top: config.space.S100, right: config.space.S100 }}>
<IconButton
type="button"
size="300"
radii="300"
variant="Surface"
fill="None"
onClick={handleDismiss}
aria-label="Dismiss notification"
>
<Icon size="100" src={Icons.Cross} />
</IconButton>
</span>
<div style={rowStyle}>
{toast.avatarUrl ? (
<img src={toast.avatarUrl} alt="" style={avatarStyle} aria-hidden="true" />
@@ -201,7 +217,7 @@ export function LotusToastContainer() {
zIndex: 10001,
display: 'flex',
flexDirection: 'column',
gap: '8px',
gap: config.space.S200,
pointerEvents: 'auto',
};