/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// Labels: zelfstandig naamwoord met hoofdletter, geen dubbele punt, uitleg in een hint, eenheid rechts.
window.UI = (() => {
const esc = (s) => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
const icon = (name, cls) => ``;
const hint = (text) => text ? `${icon('info-circle')}` : '';
function field(id, label, control, o) {
return `
${control}
`;
}
function num(id, label, value, o) {
o = o || {};
const unit = o.unit === undefined ? 'mm' : o.unit;
return field(id, label,
`` +
`${unit}`, o);
}
// Het getal mag buiten het schuifbereik; het model klemt zelf.
function range(id, label, value, o) {
o = o || {};
const unit = o.unit === undefined ? 'mm' : o.unit;
const lim = `min="${o.min}" max="${o.max}" step="${o.step || 10}"`;
return ``;
}
function onRange(id, fn, after) {
const n = document.getElementById(id), r = document.getElementById(id + '-r');
if (!n || !r) return;
r.addEventListener('input', () => { n.value = r.value; fn(parseFloat(r.value)); after && after(); });
n.addEventListener('input', () => {
const v = parseFloat(n.value);
if (!isFinite(v)) return;
r.value = v;
fn(v); after && after();
});
}
function setRange(id, v) {
const n = document.getElementById(id), r = document.getElementById(id + '-r');
if (n && document.activeElement !== n) n.value = v;
if (r) r.value = v;
}
function sel(id, label, options, value, o) {
const opts = Object.entries(options).map(([k, v]) => ``).join('');
return field(id, label, ``, o);
}
function color(id, label, value, o) {
return field(id, label, ``, o);
}
function onColor(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('change', (e) => { fn(e.target.value); after && after(); });
}
function text(id, label, value, o) {
return field(id, label, ``, o);
}
function onText(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('input', (e) => { fn(e.target.value); after && after(); });
}
function toggle(id, label, checked, o) {
return field(id, label,
``, o);
}
const section = (title, iconName, body) =>
`${iconName ? icon(iconName) : ''}${title}
${body}`;
const info = (text) => `${icon('info-circle')}${text}
`;
const warn = (text) => `${icon('exclamation-triangle')}${text}
`;
const grid = (...cols) => `${cols.map(c => `
${c}
`).join('')}
`;
const button = (label, iconName, attrs, cls) =>
``;
const actions = (...btns) => `${btns.join('')}
`;
const activeTabs = {};
function tabs(group, list) {
const active = list.some(t => t.key === activeTabs[group]) ? activeTabs[group] : list[0].key;
return `
${list.map(t => ``).join('')}
${list.map(t => `
${t.html}
`).join('')}
`;
}
function wireTabs(container) {
if (container._uiTabsWired) return;
container._uiTabsWired = true;
container.addEventListener('click', (e) => {
const btn = e.target.closest('.ui-tab');
if (!btn) return;
const wrap = btn.closest('.ui-tabs');
activeTabs[wrap.dataset.tabGroup] = btn.dataset.tab;
wrap.querySelectorAll('.ui-tab').forEach(b => b.classList.toggle('active', b === btn));
wrap.querySelectorAll('.ui-tab-page').forEach(p => p.classList.toggle('active', p.dataset.tabPage === btn.dataset.tab));
});
}
function specCard(title, iconName, rows, total) {
const row = ([label, value, unit]) =>
`${label}${value}${unit ? `${unit}` : ''}
`;
return `${icon(iconName)}${title}
${rows.map(row).join('')}${total ? `
${row(total)}
` : ''}
`;
}
function onNum(id, fn, after, o) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
if (isFinite(v) && (!(o && o.positive) || v > 0)) { fn(v); after && after(); }
});
}
function onSel(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('input', (e) => { fn(e.target.value); after && after(); });
}
function onToggle(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('change', (e) => { fn(e.target.checked); after && after(); });
}
// Weergave van een editor: klassen fullscreen-2d/-3d op het editor-element (de looptocht bewaart en herstelt className).
const VIEWS = [
{ key: 'split', icon: 'layout-split', label: '2D + 3D', key1: '1' },
{ key: '2d', icon: 'square', label: 'Alleen 2D', key1: '2' },
{ key: '3d', icon: 'box', label: 'Alleen 3D', key1: '3' }
];
const viewOf = (el) => el.classList.contains('fullscreen-3d') ? '3d' : el.classList.contains('fullscreen-2d') ? '2d' : 'split';
function setView(el, mode) {
el.classList.toggle('fullscreen-3d', mode === '3d');
el.classList.toggle('fullscreen-2d', mode === '2d');
el.classList.remove('render-only');
el.querySelectorAll('[data-view]').forEach(b => b.classList.toggle('active', b.dataset.view === mode));
}
const tb = (label, iconName, attrs, cls) =>
``;
const tbSep = () => '';
const viewSeg = (mode) => `${VIEWS.map(v =>
tb('', v.icon, `data-view="${v.key}" title="${v.label} (${v.key1})"`, v.key === mode ? 'active' : '')).join('')}
`;
// Uitklappaneel onder een knop; sluit bij klik buiten, Esc of een andere popover.
let openPop = null;
function popover(btn, panel) {
const close = () => { panel.classList.remove('open'); btn.classList.remove('open'); if (openPop === close) openPop = null; };
btn.addEventListener('click', (e) => {
e.stopPropagation();
const was = panel.classList.contains('open');
if (openPop) openPop();
if (!was) { panel.classList.add('open'); btn.classList.add('open'); openPop = close; }
});
panel.addEventListener('click', (e) => e.stopPropagation());
return close;
}
const closePopovers = () => { if (openPop) { openPop(); return true; } return false; };
document.addEventListener('click', () => closePopovers());
// Kaarten verdelen over kolommen van min. minW breed; telkens in de kortste kolom. Opnieuw aanroepen bij een andere breedte.
function masonry(el, minW, gap) {
if (!el) return;
gap = gap === undefined ? 12 : gap;
const cards = el._cards || (el._cards = [...el.children]);
const n = Math.max(1, Math.floor((el.clientWidth + gap) / (minW + gap)));
if (el._cols === n) return;
el._cols = n;
el.classList.add('ui-masonry');
const cols = Array.from({ length: n }, () => { const c = document.createElement('div'); c.className = 'ui-masonry-col'; return c; });
el.replaceChildren(...cols);
cards.forEach(card => cols.reduce((a, b) => b.offsetHeight < a.offsetHeight ? b : a).appendChild(card));
}
return { esc, icon, hint, num, range, onRange, setRange, sel, toggle, color, onColor, text, onText, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle,
VIEWS, viewOf, setView, tb, tbSep, viewSeg, popover, closePopovers, masonry };
})();