// Shared small primitives for the admin UI kit. // Status tone + tiny SVG icons + labels matching the codebase. const STATUS_LABELS = { queued: 'Inbox', running: 'Running', waiting_human: 'Waiting', waiting_subtasks: 'Subtasks', retry: 'Retry', succeeded: 'Done', failed: 'Failed', cancelled: 'Cancelled', }; const STATUS_TONE = { running: { bg: '#dcfce7', fg: '#166534' }, waiting_human: { bg: '#fef9c3', fg: '#854d0e' }, waiting_subtasks: { bg: '#e0e7ff', fg: '#3730a3' }, failed: { bg: '#fee2e2', fg: '#b91c1c' }, succeeded: { bg: '#dbeafe', fg: '#1e40af' }, retry: { bg: '#fef3c7', fg: '#92400e' }, queued: { bg: '#e2e8f0', fg: '#475569' }, cancelled: { bg: '#e2e8f0', fg: '#475569' }, }; function StatusBadge({ status, small }) { const tone = STATUS_TONE[status] || STATUS_TONE.queued; const label = STATUS_LABELS[status] || status; const style = { background: tone.bg, color: tone.fg, fontSize: small ? 10 : 11, fontWeight: 700, padding: small ? '1px 8px' : '2px 10px', borderRadius: 9999, display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap', }; return {label}; } function StatChip({ label, value, color }) { return (
{label}
{value}
); } function Spinner() { return
; } function PulseDot() { return ; } function IconSearch(props) { return ; } function IconAttach(props) { return ; } function IconClose(props) { return ; } // ---- State primitives: loading / empty / error ---- function SkeletonLine({ width = '100%', height = 10, style }) { return
; } function SkeletonCard({ lines = 2 }) { return (
{Array.from({ length: lines }).map((_, i) => ( ))}
); } function SkeletonList({ count = 5, lines = 2 }) { return (
{Array.from({ length: count }).map((_, i) => )}
); } function EmptyState({ icon, title, hint, action, compact }) { return (
{icon &&
{icon}
} {title &&
{title}
} {hint &&
{hint}
} {action &&
{action}
}
); } function ErrorState({ title = '読み込みに失敗しました', hint, onRetry, compact }) { return (
{title}
{hint &&
{hint}
} {onRetry && ( )}
); } function relativeTime(ms) { const mins = Math.floor((Date.now() - ms) / 60000); if (mins < 1) return 'たった今'; if (mins < 60) return `${mins}分前`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}時間前`; return `${Math.floor(hrs / 24)}日前`; } Object.assign(window, { STATUS_LABELS, STATUS_TONE, StatusBadge, StatChip, Spinner, PulseDot, SkeletonLine, SkeletonCard, SkeletonList, EmptyState, ErrorState, IconSearch, IconAttach, IconClose, relativeTime, });