/*! * 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 `
` + `
` + `${unit}
`; } 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 }; })();