/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // ============================================================ // core/ui-kit.js — gedeelde UI-bouwstenen (window.UI): iconen (Bootstrap // Icons, css/vendor), invoervelden, schakelaars, secties, tabbladen, // inklapbare secties, schuiven, specificatie-kaarten en de 3-staps // weergave-cyclus (sneltoets T). // Eén plek voor opmaak en woordgebruik, zodat alle panelen gelijk ogen. // // Woordgebruik labels: zelfstandig naamwoord met hoofdletter, geen dubbele // punt, geen uitleg tussen haakjes (die gaat in een hint), eenheid rechts // van het veld. Bijvoorbeeld "Afstand min." i.p.v. "Lampen min. h.o.h.". // Plain script (geen ES-module). // ============================================================ 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}
`; } // Getal. o: { step, unit ('mm' standaard, '' = geen), hint, min, disabled } function num(id, label, value, o) { o = o || {}; const unit = o.unit === undefined ? 'mm' : o.unit; return field(id, label, `` + `${unit}`, o); } // Schuif met gekoppeld getal (voor maten met een logisch bereik, zoals // hoogtes en hellingen). o: { min, max, step, unit ('mm' standaard), hint }. // 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}
`; } // fn(v) bij schuiven en typen; set(id, v) zet beide zonder event. 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; } // Keuzelijst. options: { waarde: 'Label' }. function sel(id, label, options, value, o) { const opts = Object.entries(options).map(([k, v]) => ``).join(''); return field(id, label, ``, o); } // Kleurkiezer (waarde '#rrggbb'); onColor reageert pas bij loslaten. 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(); }); } // Aan/uit-schakelaar. 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('')}
`; // Tabbladen. group = sleutel waaronder het actieve tabblad onthouden wordt // (bijv. 'woning', 'unit'); tabs = [{ key, label, icon, html }]. 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('')}
`; } // Eenmalig per container: klik op een tab wisselt de pagina. 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)); }); } // Specificaties: kaart met regels [label, waarde, eenheid]. 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)}
` : ''}
`; } // Invoer koppelen: onNum/onSel/onToggle(root, id, fn, after). 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-cyclus 3D (sneltoets T): standaard -> 3D groot -> alleen de // render (ook het info-/specificatiedeel eronder weg) -> standaard. function cycle3D(el) { el.classList.remove('fullscreen-2d'); if (!el.classList.contains('fullscreen-3d')) el.classList.add('fullscreen-3d'); else if (!el.classList.contains('render-only')) el.classList.add('render-only'); else el.classList.remove('fullscreen-3d', 'render-only'); } function toggle2D(el) { el.classList.remove('fullscreen-3d', 'render-only'); el.classList.toggle('fullscreen-2d'); } return { esc, icon, hint, num, range, onRange, setRange, sel, toggle, color, onColor, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D }; })();