import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Box, Text } from 'ink'; export interface LoadingIndicatorProps { /** true にするとローディング表示を開始する */ isLoading: boolean; /** 表示までの遅延時間 (ms)。デフォルトは 300ms */ delay?: number; /** 表示するメッセージ */ message?: string; /** スピナーの更新間隔 (ms)。デフォルトは 80ms */ interval?: number; /** 使用するスピナーフレーム。ASCII のみを想定 */ frames?: string[]; } const DEFAULT_FRAMES = ['|', '/', '-', '\\']; /** * ローディング中に簡易スピナーとメッセージを表示するコンポーネント。 * delay で指定した時間を超えるまでスピナーを表示しないことで、短時間の処理ではちらつきを抑える。 */ export function LoadingIndicator({ isLoading, delay = 300, message = 'Loading... please wait', interval = 80, frames = DEFAULT_FRAMES, }: LoadingIndicatorProps) { const [visible, setVisible] = useState(false); const [frameIndex, setFrameIndex] = useState(0); const delayTimerRef = useRef | null>(null); const intervalRef = useRef | null>(null); // スピナーに使用するフレームをキャッシュ const safeFrames = useMemo(() => (frames.length > 0 ? frames : DEFAULT_FRAMES), [frames]); useEffect(() => { // ローディングが開始したら、delay後に表示を有効化 if (isLoading) { delayTimerRef.current = setTimeout(() => { setVisible(true); }, delay); } else { setVisible(false); setFrameIndex(0); } return () => { if (delayTimerRef.current) { clearTimeout(delayTimerRef.current); delayTimerRef.current = null; } }; }, [isLoading, delay]); useEffect(() => { // 表示中のみスピナーを回転 if (visible && isLoading) { intervalRef.current = setInterval(() => { setFrameIndex((current) => (current + 1) % safeFrames.length); }, interval); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [visible, isLoading, interval, safeFrames.length]); // ローディングが解消されたらタイマーをクリア useEffect(() => { if (!isLoading && delayTimerRef.current) { clearTimeout(delayTimerRef.current); delayTimerRef.current = null; } }, [isLoading]); if (!isLoading || !visible) { return null; } return ( {safeFrames[frameIndex]} {message} ); }