/* global React, TOOLS, TOOL_BY_ID, useI18n, useTheme, useHistory, useHashRoute, navigate, Icon, formatRelativeTime, clearHistory */
/* ============================================================
shell.jsx — App shell (top bar, breadcrumbs) + Hub page
============================================================ */
const { useState: useState_S, useEffect: useEffect_S, useMemo: useMemo_S } = React;
// ---------- TopBar ----------
function TopBar({ tweaks, setTweaks }) {
const { lang, setLang, t } = useI18n();
const { theme, toggle } = useTheme();
const route = useHashRoute();
let crumbs = null;
if (route.name === 'tool') {
const tool = TOOL_BY_ID[route.toolId];
crumbs = (
);
} else {
crumbs = (
{t('crumb_hub')}
);
}
return (
);
}
// ---------- Hub ----------
function Hub() {
const { t } = useI18n();
const [query, setQuery] = useState_S('');
const { items: history, reload } = useHistory();
const filtered = useMemo_S(() => {
if (!query.trim()) return TOOLS;
const q = query.toLowerCase();
return TOOLS.filter(tool =>
t(tool.nameKey).toLowerCase().includes(q) ||
t(tool.descKey).toLowerCase().includes(q) ||
tool.id.includes(q)
);
}, [query, t]);
const tweaks = window.__cv_tweaks || {};
const cardStyle = tweaks.cardStyle || 'soft';
return (
{t('hub_eyebrow')}
{t('hub_title_pre')} {t('hub_title_em')}
{t('hub_sub')}
{t('section_tools')}
{filtered.length.toString().padStart(2, '0')} · {t('section_tools_meta')}
{filtered.map(tool => (
))}
{t('section_history')}
{history.length.toString().padStart(2, '0')} · {t('section_history_meta')}
{history.length > 0 && (
)}
{history.length === 0 ? (
{t('history_empty')}
) : (
{history.slice(0, 12).map(entry => {
const tool = TOOL_BY_ID[entry.toolId];
if (!tool) return null;
return (
navigate(entry.payload ? `${entry.toolId}?h=${entry.id}` : entry.toolId)}
style={{ '--card-hue': `var(--hue-${tool.hue})` }}
>
{tool.initials}
{entry.title || t('untitled')}
{entry.size != null && {window.fmtBytes(entry.size)}}
{t(tool.nameKey)}
{formatRelativeTime(entry.ts, t)}
);
})}
)}
);
}
function ToolCard({ tool, styleVariant }) {
const { t } = useI18n();
const cls = `tool-card style-${styleVariant || 'soft'}`;
return (
navigate(tool.id)}
style={{ '--card-hue': `var(--hue-${tool.hue})` }}
>
{tool.initials}
{t(tool.nameKey)}
{t(tool.descKey)}
{t(tool.badgeKey)}
→
);
}
// ---------- Common Tool Page chrome ----------
function ToolHead({ tool, actions }) {
const { t } = useI18n();
return (
{tool.initials}
{t(tool.nameKey)}
{t(tool.descKey)}
{actions}
);
}
function StatusBar({ status, left, right }) {
const { t } = useI18n();
const pillClass = status === 'busy' ? 'busy' : status === 'err' ? 'err' : 'ok';
const pillText = status === 'busy' ? t('common_busy') : status === 'err' ? t('common_error') : t('common_ready');
return (
);
}
// ---------- Dropzone ----------
function Dropzone({ accept, multiple, files, onFiles, onRemove, styleVariant = 'default', title, sub, busy, busyText }) {
const { t } = useI18n();
const { dragover, bind, inputProps } = window.useDropzone({
accept, multiple,
onFiles: (newFiles) => {
if (multiple) onFiles([...(files || []), ...newFiles]);
else onFiles(newFiles);
},
});
const cls = `dropzone style-${styleVariant} ${dragover ? 'dragover' : ''}`;
const hasFiles = files && files.length > 0;
return (
{busy ? (
<>
{busyText || t('common_busy')}
>
) : (
<>
{dragover ? '↓' : '+'}
{dragover ? t('common_drop_here') : title}
{sub}
{hasFiles ? (
{files.map((f, i) => (
e.stopPropagation()}>
{f.name} · {window.fmtBytes(f.size)}
{ e.stopPropagation(); onRemove?.(i); }} title={t('common_remove')}>×
))}
) : (
{t('common_browse')}
)}
>
)}
);
}
// ---------- Common option controls ----------
function OptCheckbox({ checked, onChange, label, hint }) {
return (
);
}
function OptRadio({ value, onChange, label, options }) {
return (
{label}
{options.map(o => (
))}
);
}
function OptDivider() { return ; }
// ---------- Empty state ----------
function EmptyOutput({ title, sub }) {
return (
);
}
// ---------- Export to window ----------
Object.assign(window, {
TopBar, Hub, ToolHead, StatusBar, Dropzone,
OptCheckbox, OptRadio, OptDivider,
EmptyOutput,
});