/* ===== Shared UI components ===== */
const { useState, useEffect, useRef, useCallback } = React;
/* ---- Icon ---- */
function Icon({ name, size, className }) {
const p = window.ICONS[name] || '';
return (
);
}
/* ---- ScaledFrame: renders a section in an isolated iframe, scaled to fit ----
mode 'crop' -> fills a fixed box, top-aligned (for cards)
mode 'fit' -> shows the whole section, height follows content (for detail) */
function ScaledFrame({ section, theme, logicalWidth, mode, maxScale }) {
const wrapRef = useRef(null);
const frameRef = useRef(null);
const [scale, setScale] = useState(0.26);
const [contentH, setContentH] = useState(section.height || 480);
const lw = logicalWidth || 1280;
const html = window.buildSectionHTML(section);
// If the section has a user-uploaded thumbnail, show it in card/crop contexts
// (grid cards, manage rows, category previews) for a consistent representation.
const hasThumb = !!section.thumb;
const measure = useCallback(() => {
const el = wrapRef.current; if (!el) return;
let s = el.clientWidth / lw;
if (maxScale) s = Math.min(s, maxScale);
setScale(s);
}, [lw, maxScale]);
useEffect(() => {
measure();
const ro = new ResizeObserver(measure);
if (wrapRef.current) ro.observe(wrapRef.current);
return () => ro.disconnect();
}, [measure]);
const onLoad = () => {
try {
const doc = frameRef.current.contentDocument;
const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
if (h && Math.abs(h - contentH) > 2) setContentH(h);
} catch (e) {}
};
if (mode === 'fit') {
return (
);
}
if (hasThumb) {
return (
);
}
return (
);
}
/* ---- CatDot ---- */
function CatDot({ cat }) {
return ;
}
/* ---- MultiCatSelect ---- multi-select category dropdown */
function MultiCatSelect({ cats, setCats }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
}, [open]);
const toggle = (id) => setCats(cats.includes(id) ? cats.filter(c => c !== id) : [...cats, id]);
const label = cats.length === 0 ? 'All categories'
: cats.length === 1 ? window.catById(cats[0]).label
: cats.length + ' categories';
return (
{open && (
Filter by category
{cats.length > 0 && }
{window.CATEGORIES.map(c => {
const on = cats.includes(c.id);
return (
);
})}
)}
);
}
/* ---- SectionCard ---- */
function SectionCard({ section, fav, onFav, onOpen }) {
const cat = window.catById(section.category);
return (
onOpen(section)}>
{section.name}
{section.pro && Pro}
{cat.label}
{section.frameworks.includes('html') && HTML}
{section.frameworks.includes('react') && React}
{(section.downloads / 1000).toFixed(1)}k
);
}
/* ---- StatCard (dashboard) ---- */
function StatCard({ icon, value, label, delta, tint }) {
return (
);
}
/* ---- Category tile ---- */
function CatTile({ cat, onOpen }) {
return (
);
}
/* ---- CatRow (dashboard: vertical category list) ---- */
function CatRow({ cat, onOpen }) {
const n = window.catCount(cat.id);
return (
);
}
/* ---- SectionRow (dashboard: tabbed feed — grid card, thumbnail + title only) ---- */
function SectionRow({ section, onOpen, rank }) {
return (
onOpen(section)}>
{rank ? {rank} : null}
{section.name}
);
}
/* ---- CodePanel ---- */
function CodePanel({ section }) {
const [tab, setTab] = useState('html');
const [copied, setCopied] = useState(false);
const codeRef = useRef(null);
const code = tab === 'html' ? window.buildSectionHTML(section) : window.buildSectionReact(section);
const fileName = tab === 'html' ? section.id + '.html' : pascalName(section.id) + '.jsx';
const copy = () => {
window.copyToClipboard(code).then((ok) => {
setCopied(ok ? 'ok' : 'fail');
if (!ok && codeRef.current) {
const r = document.createRange(); r.selectNodeContents(codeRef.current);
const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
}
setTimeout(() => setCopied(false), 1800);
});
};
const download = () => {
if (window.bumpDownloads) window.bumpDownloads();
const blob = new Blob([code], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = fileName; a.click();
URL.revokeObjectURL(url);
};
return (
{section.frameworks.includes('html') &&
}
{section.frameworks.includes('react') &&
}
);
}
function pascalName(id) {
return id.split(/[-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
}
function initials(name) {
return (name || '?').trim().split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase();
}
/* Opens a section's rendered HTML in a brand-new browser tab (full view). */
function openFullPreview(section) {
try {
const html = window.buildSectionHTML(section);
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const w = window.open(url, '_blank', 'noopener');
setTimeout(() => URL.revokeObjectURL(url), 60000);
if (!w) { // popup blocked — fall back to a data navigation
const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener'; a.click();
}
} catch (e) {}
}
Object.assign(window, { Icon, ScaledFrame, CatDot, MultiCatSelect, SectionCard, StatCard, CatTile, CatRow, SectionRow, CodePanel, pascalName, openFullPreview, initials });