// 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 (
);
}
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,
});