a2c8992650
Self-review of the Batches 9-15 changes surfaced two style inconsistencies (no behavior change): - Toaster.tsx: combine the icon + tone classes with mergeClasses instead of a template-literal string, matching how the rest of the codebase composes Griffel classes. - App.tsx: move the PERF8 React.lazy() view declarations below the import block (they were interleaved between import statements). Verified: typecheck (node+web) + 279 tests + eslint + production build green (code-split chunks intact); touched files prettier-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import {
|
|
Caption1,
|
|
Button,
|
|
makeStyles,
|
|
mergeClasses,
|
|
tokens,
|
|
shorthands
|
|
} from '@fluentui/react-components'
|
|
import {
|
|
DismissRegular,
|
|
ErrorCircleFilled,
|
|
CheckmarkCircleFilled,
|
|
InfoFilled
|
|
} from '@fluentui/react-icons'
|
|
import { useToasts, type ToastTone } from '../../store/toasts'
|
|
import { Z, ELEVATION, RADIUS, ICON } from './tokens'
|
|
|
|
/**
|
|
* Renders the transient-toast stack (UX6/UX9) bottom-center. Deliberately NOT a
|
|
* Fluent Toaster/portal — this app avoids portal-based overlays (they blank/flicker
|
|
* on the dev GPU; see Select.tsx / CommandPalette.tsx) — so it's a plain fixed
|
|
* stack. No enter/exit animation, matching the app's conservative motion policy on
|
|
* this machine.
|
|
*/
|
|
const useStyles = makeStyles({
|
|
stack: {
|
|
position: 'fixed',
|
|
left: '50%',
|
|
bottom: '24px',
|
|
transform: 'translateX(-50%)',
|
|
zIndex: Z.tooltip,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
// Let clicks fall through the gaps between toasts to the app underneath.
|
|
pointerEvents: 'none'
|
|
},
|
|
toast: {
|
|
pointerEvents: 'auto',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '10px',
|
|
maxWidth: 'min(520px, 90vw)',
|
|
padding: '10px 8px 10px 12px',
|
|
backgroundColor: tokens.colorNeutralBackground1,
|
|
border: `1px solid ${tokens.colorNeutralStroke2}`,
|
|
...shorthands.borderRadius(RADIUS.surface),
|
|
boxShadow: ELEVATION.overlay
|
|
},
|
|
icon: {
|
|
flexShrink: 0,
|
|
fontSize: `${ICON.control}px`,
|
|
display: 'flex'
|
|
},
|
|
info: { color: tokens.colorNeutralForeground3 },
|
|
error: { color: tokens.colorStatusDangerForeground1 },
|
|
success: { color: tokens.colorStatusSuccessForeground1 },
|
|
message: {
|
|
flexGrow: 1,
|
|
minWidth: 0
|
|
}
|
|
})
|
|
|
|
const ICONS: Record<ToastTone, React.JSX.Element> = {
|
|
info: <InfoFilled />,
|
|
error: <ErrorCircleFilled />,
|
|
success: <CheckmarkCircleFilled />
|
|
}
|
|
|
|
export function Toaster(): React.JSX.Element | null {
|
|
const styles = useStyles()
|
|
const toasts = useToasts((s) => s.toasts)
|
|
const dismiss = useToasts((s) => s.dismiss)
|
|
const toneClass: Record<ToastTone, string> = {
|
|
info: styles.info,
|
|
error: styles.error,
|
|
success: styles.success
|
|
}
|
|
|
|
if (toasts.length === 0) return null
|
|
return (
|
|
<div className={styles.stack} role="status" aria-live="polite">
|
|
{toasts.map((t) => (
|
|
<div key={t.id} className={styles.toast}>
|
|
<span className={mergeClasses(styles.icon, toneClass[t.tone])}>{ICONS[t.tone]}</span>
|
|
<Caption1 className={styles.message}>{t.message}</Caption1>
|
|
<Button
|
|
size="small"
|
|
appearance="subtle"
|
|
icon={<DismissRegular />}
|
|
onClick={() => dismiss(t.id)}
|
|
aria-label="Dismiss notification"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|