This commit is contained in:
root
2026-09-19 20:21:47 +02:00
commit 0798933b05
62 changed files with 7658 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
// Tiny DOM helpers: element builder, toasts and a confirm dialog.
/** Build an element. Props: class, on<Event> handlers, booleans, attributes. */
export function h(tag, props, ...kids) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(props ?? {})) {
if (v == null || v === false) continue;
if (k === 'class') el.className = v;
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
else if (v === true) el.setAttribute(k, '');
else el.setAttribute(k, v);
}
el.append(...kids.flat(Infinity).filter((c) => c != null && c !== false));
return el;
}
export function clear(el) {
el.replaceChildren();
return el;
}
export function toast(message, kind = 'info') {
const host = document.getElementById('toasts');
const t = h('div', { class: `toast ${kind}`, role: kind === 'err' ? 'alert' : null }, message);
host.append(t);
setTimeout(() => {
t.classList.add('out');
setTimeout(() => t.remove(), 400);
}, kind === 'err' ? 8000 : 4500);
}
/** Resolve true if the user confirms. */
export function confirmDialog({ title, body, confirmText = 'CONFIRM', cancelText = 'ABORT', danger = false }) {
const dlg = document.getElementById('dialog');
return new Promise((resolve) => {
const finish = (v) => {
dlg.close();
resolve(v);
};
clear(dlg).append(
h('div', { class: `panel dialog ${danger ? 'danger' : ''}` },
h('div', { class: 'panel-title' }, title),
h('div', { class: 'panel-body' }, ...[].concat(body).map((p) => h('p', null, p))),
h('div', { class: 'actions' },
h('button', { class: 'btn', type: 'button', onclick: () => finish(false) }, cancelText),
h('button', { class: `btn ${danger ? 'danger' : 'primary'}`, type: 'button', onclick: () => finish(true) }, confirmText),
),
),
);
dlg.oncancel = () => resolve(false);
dlg.showModal();
});
}
export function download(filename, bytes, type = 'application/octet-stream') {
const url = URL.createObjectURL(new Blob([bytes], { type }));
const a = h('a', { href: url, download: filename });
document.body.append(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
export function pad(n, width, ch = '0') {
return String(n).padStart(width, ch);
}
export function plural(n, one, many = `${one}S`) {
return `${n} ${n === 1 ? one : many}`;
}