sync: update from private repo (9a86f49b)
CI / build-and-test (push) Successful in 9m8s
CI / build-and-test (pull_request) Successful in 9m42s

This commit is contained in:
oss-sync
2026-07-12 23:51:48 +00:00
parent a67c40d33c
commit ed3dc5529a
159 changed files with 11696 additions and 1600 deletions
+47 -12
View File
@@ -1,24 +1,59 @@
import { useState, useEffect, useCallback } from 'react';
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
export type ToastVariant = 'success' | 'error';
export type ToastVariant = 'success' | 'error' | 'info';
export interface ToastState {
id: string;
message: string;
variant: ToastVariant;
title?: string;
actionLabel?: string;
onAction?: () => void;
visual?: ReactNode;
}
export interface ShowToastOptions {
/** 同じ ID の通知は更新し、重複して積まない。 */
id?: string;
title?: string;
actionLabel?: string;
onAction?: () => void;
visual?: ReactNode;
}
let nextToastId = 0;
/**
* 画面内通知のキュー。既存の `toast` は後方互換のため最新1件を返す。
* 同じ ID は置き換えるため、ポーリング由来の重複通知を表示しない。
*/
export function useToast(durationMs = 3500) {
const [state, setState] = useState<ToastState | null>(null);
const [toasts, setToasts] = useState<ToastState[]>([]);
const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
useEffect(() => {
if (!state) return;
const id = setTimeout(() => setState(null), durationMs);
return () => clearTimeout(id);
}, [state, durationMs]);
const showToast = useCallback((message: string, variant: ToastVariant = 'success') => {
setState({ message, variant });
const dismissToast = useCallback((id: string) => {
const timer = timers.current.get(id);
if (timer) clearTimeout(timer);
timers.current.delete(id);
setToasts(current => current.filter(toast => toast.id !== id));
}, []);
return { toast: state, showToast };
const showToast = useCallback((message: string, variant: ToastVariant = 'success', options: ShowToastOptions = {}) => {
const id = options.id ?? `toast-${++nextToastId}`;
const timer = timers.current.get(id);
if (timer) clearTimeout(timer);
const toast: ToastState = { id, message, variant, title: options.title, actionLabel: options.actionLabel, onAction: options.onAction, visual: options.visual };
setToasts(current => [...current.filter(item => item.id !== id), toast]);
timers.current.set(id, setTimeout(() => dismissToast(id), durationMs));
}, [dismissToast, durationMs]);
useEffect(() => () => {
timers.current.forEach(clearTimeout);
timers.current.clear();
}, []);
const latest = toasts.at(-1);
// Legacy consumers only expect these two properties.
const toast = latest ? { message: latest.message, variant: latest.variant } : null;
return { toast, toasts, showToast, dismissToast };
}