v0.4.1: UI opgeschoond — tabbladen, iconen, uniforme labels, T-cyclus
- core/ui-kit.js: gedeelde velden/schakelaars/tabs/specificatiekaarten - Bootstrap Icons lokaal (css/vendor, font ingebed) - Woning- en unit-instellingen in tabbladen, labels gestandaardiseerd - Specificaties onder 3D als kaarten - T: standaard -> 3D groot -> alleen render -> standaard Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c690e72d13
commit
3591fad220
@@ -0,0 +1,121 @@
|
||||
/*!
|
||||
* 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,
|
||||
// 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, '>').replace(/"/g, '"');
|
||||
const icon = (name, cls) => `<i class="bi bi-${name}${cls ? ' ' + cls : ''}" aria-hidden="true"></i>`;
|
||||
const hint = (text) => text ? `<span class="ui-hint" title="${esc(text)}">${icon('info-circle')}</span>` : '';
|
||||
|
||||
function field(id, label, control, o) {
|
||||
return `<div class="ui-field${o && o.disabled ? ' is-disabled' : ''}"><label for="${id}">${label}${hint(o && o.hint)}</label><div class="ui-control">${control}</div></div>`;
|
||||
}
|
||||
|
||||
// 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,
|
||||
`<input type="number" id="${id}" value="${value}" step="${o.step || 50}"${o.min !== undefined ? ` min="${o.min}"` : ''}${o.disabled ? ' disabled' : ''}>` +
|
||||
`<span class="ui-unit">${unit}</span>`, o);
|
||||
}
|
||||
|
||||
// Keuzelijst. options: { waarde: 'Label' }.
|
||||
function sel(id, label, options, value, o) {
|
||||
const opts = Object.entries(options).map(([k, v]) => `<option value="${k}"${k === value ? ' selected' : ''}>${v}</option>`).join('');
|
||||
return field(id, label, `<select id="${id}">${opts}</select><span class="ui-unit"></span>`, o);
|
||||
}
|
||||
|
||||
// Aan/uit-schakelaar.
|
||||
function toggle(id, label, checked, o) {
|
||||
return field(id, label,
|
||||
`<label class="ui-switch"><input type="checkbox" id="${id}"${checked ? ' checked' : ''}><span></span></label><span class="ui-unit"></span>`, o);
|
||||
}
|
||||
|
||||
const section = (title, iconName, body) =>
|
||||
`<section class="ui-section"><h2>${iconName ? icon(iconName) : ''}${title}</h2>${body}</section>`;
|
||||
const info = (text) => `<div class="ui-info">${icon('info-circle')}<span>${text}</span></div>`;
|
||||
const warn = (text) => `<div class="ui-warn">${icon('exclamation-triangle')}<span>${text}</span></div>`;
|
||||
const grid = (...cols) => `<div class="ui-grid">${cols.map(c => `<div>${c}</div>`).join('')}</div>`;
|
||||
const button = (label, iconName, attrs, cls) =>
|
||||
`<button type="button" class="ui-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}<span>${label}</span></button>`;
|
||||
const actions = (...btns) => `<div class="ui-actions">${btns.join('')}</div>`;
|
||||
|
||||
// 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 `<div class="ui-tabs" data-tab-group="${group}">
|
||||
<div class="ui-tab-bar">${list.map(t => `<button type="button" class="ui-tab${t.key === active ? ' active' : ''}" data-tab="${t.key}">${t.icon ? icon(t.icon) : ''}<span>${t.label}</span></button>`).join('')}</div>
|
||||
${list.map(t => `<div class="ui-tab-page${t.key === active ? ' active' : ''}" data-tab-page="${t.key}">${t.html}</div>`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
// 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]) =>
|
||||
`<div class="ui-spec-row"><span>${label}</span><span class="ui-spec-val">${value}${unit ? `<small>${unit}</small>` : ''}</span></div>`;
|
||||
return `<div class="ui-spec-card"><h2>${icon(iconName)}${title}</h2>${rows.map(row).join('')}${total ? `<div class="ui-spec-total">${row(total)}</div>` : ''}</div>`;
|
||||
}
|
||||
|
||||
// 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, sel, toggle, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
|
||||
})();
|
||||
@@ -21,7 +21,7 @@ function createViewFilter(toolbar, items, onChange) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'view-filter';
|
||||
wrap.innerHTML = `
|
||||
<button type="button" class="btn-3d-toggle vf-btn">Weergave ▾</button>
|
||||
<button type="button" class="btn-3d-toggle vf-btn"><i class="bi bi-eye"></i>Weergave<i class="bi bi-chevron-down"></i></button>
|
||||
<div class="vf-menu">
|
||||
${items.map(it => `<label class="vf-item"><input type="checkbox" data-key="${it.key}" ${state[it.key] ? 'checked' : ''}> ${it.label}</label>`).join('')}
|
||||
</div>`;
|
||||
|
||||
Reference in New Issue
Block a user