Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/src/components/Notice.tsx

tsx79 行查看原文
import { CircleAlert, Info, TriangleAlert, X } from "lucide-react";
import { useEffect, useState } from "react";

/** Where the notice lives decides its layout and lifetime, not the caller. */
export type NoticeScope = "input" | "turn" | "window" | "config";
export type NoticeTone = "error" | "warning" | "info";

export interface NoticeAction {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

export interface NoticeProps {
  scope: NoticeScope;
  tone?: NoticeTone;
  message: string;
  /** Callers with frequently changing text may provide a separate, stable live announcement. */
  announce?: boolean;
  action?: NoticeAction;
  onDismiss?: () => void;
  dismissLabel?: string;
}

const icons = { error: CircleAlert, warning: TriangleAlert, info: Info } as const;

export function Notice({
  scope,
  tone = "error",
  message,
  announce = true,
  action,
  onDismiss,
  dismissLabel,
}: NoticeProps) {
  const [hovered, setHovered] = useState(false);
  const [focused, setFocused] = useState(false);
  useEffect(() => {
    // Only window notices are transient; input, turn and config notices stay until resolved.
    if (scope !== "window" || !onDismiss || hovered || focused) return;
    const timer = setTimeout(onDismiss, 5000);
    return () => clearTimeout(timer);
  }, [scope, message, hovered, focused, onDismiss]);

  const Icon = icons[tone];
  return (
    // biome-ignore lint/a11y/noStaticElementInteractions: Hover only pauses the window timer; callers can own the live announcement.
    <div
      className={`notice notice-${scope} notice-${tone}`}
      data-notice-scope={scope}
      role={announce ? (tone === "error" ? "alert" : "status") : undefined}
      onPointerEnter={() => setHovered(true)}
      onPointerLeave={() => setHovered(false)}
      onFocus={() => setFocused(true)}
      onBlur={(event) => {
        if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false);
      }}
    >
      <Icon size={16} aria-hidden="true" />
      <span className="notice-message">{message}</span>
      {action && (
        <button type="button" className="notice-action" disabled={action.disabled} onClick={action.onClick}>
          {action.label}
        </button>
      )}
      {onDismiss && (
        <button
          type="button"
          className="notice-dismiss"
          aria-label={dismissLabel ?? (tone === "error" ? "关闭错误" : "关闭提示")}
          onClick={onDismiss}
        >
          <X size={14} aria-hidden="true" />
        </button>
      )}
    </div>
  );
}