feat(ui): stabilize laboratory viewer primitives

This commit is contained in:
Codex
2026-08-24 22:31:35 +03:00
parent c7e136cc14
commit 6e7255ecdb
17 changed files with 590 additions and 30 deletions
+23 -12
View File
@@ -1,4 +1,4 @@
import { useEffect, type HTMLAttributes } from "react";
import { useEffect, useRef, type HTMLAttributes } from "react";
import { createPortal } from "react-dom";
import { cn } from "./cn.js";
import { Icon, type IconName } from "./Icon.js";
@@ -21,6 +21,8 @@ const toastIcons: Record<ToastTone, IconName> = {
loading: "refresh",
};
const DEFAULT_TOAST_DURATION_MS = 10_000;
export interface ToastCardProps extends HTMLAttributes<HTMLDivElement> {
item: ToastItem;
onDismiss?: (id: string) => void;
@@ -48,21 +50,30 @@ export function ToastCard({ item, onDismiss, className, ...props }: ToastCardPro
);
}
export function ToastStack({ items, onDismiss }: { items: ToastItem[]; onDismiss: (id: string) => void }) {
useEffect(() => {
const timers = items.flatMap((item) => {
const duration = item.durationMs === undefined ? 4200 : item.durationMs;
return typeof duration === "number" && duration > 0
? [window.setTimeout(() => onDismiss(item.id), duration)]
: [];
});
return () => timers.forEach((timer) => window.clearTimeout(timer));
}, [items, onDismiss]);
function TimedToastCard({ item, onDismiss }: { item: ToastItem; onDismiss: (id: string) => void }) {
const dismissRef = useRef(onDismiss);
useEffect(() => {
dismissRef.current = onDismiss;
}, [onDismiss]);
useEffect(() => {
const duration = item.durationMs === undefined
? item.tone === "loading" ? null : DEFAULT_TOAST_DURATION_MS
: item.durationMs;
if (typeof duration !== "number" || duration <= 0) return undefined;
const timer = window.setTimeout(() => dismissRef.current(item.id), duration);
return () => window.clearTimeout(timer);
}, [item.durationMs, item.id, item.tone]);
return <ToastCard item={item} onDismiss={onDismiss} />;
}
export function ToastStack({ items, onDismiss }: { items: ToastItem[]; onDismiss: (id: string) => void }) {
if (typeof document === "undefined" || items.length === 0) return null;
return createPortal(
<div className="nodedc-toast-viewport nodedc-ui-root" aria-live="polite" aria-relevant="additions removals">
{items.map((item) => <ToastCard key={item.id} item={item} onDismiss={onDismiss} />)}
{items.map((item) => <TimedToastCard key={item.id} item={item} onDismiss={onDismiss} />)}
</div>,
document.body,
);