docs: publish public guides, screenshots and Git-synced portal documentation
Tests / test (push) Failing after 50s
Tests / test (push) Failing after 50s
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.public-doc-links { display:flex; flex-wrap:wrap; gap:12px 24px; margin:0 0 25px; font-size:14px; font-weight:650; }
|
||||
.public-doc-links a { color:var(--petrol); text-decoration:underline; text-underline-offset:4px; }
|
||||
.sidebar .public-doc-link { display:flex; align-items:center; }
|
||||
@@ -0,0 +1,59 @@
|
||||
export function validateCatalog(value) {
|
||||
if (!value || value.currency !== 'CHF' || value.prices !== 'net' || !Number.isFinite(value.vatRate) || value.vatRate < 0 || value.vatRate > 100 || !value.items || typeof value.items !== 'object' || Array.isArray(value.items) || !Object.keys(value.items).length) throw new Error('invalid_catalog');
|
||||
for (const item of Object.values(value.items)) {
|
||||
if (!item || typeof item.name !== 'string' || !item.name || typeof item.sku !== 'string' || !Number.isSafeInteger(item.unitAmount) || item.unitAmount < 0 || !Number.isSafeInteger(item.setupAmount) || item.setupAmount < 0 || (item.termMonths !== null && (!Number.isSafeInteger(item.termMonths) || item.termMonths < 1))) throw new Error('invalid_catalog_item');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const grossAmount = (net, rate) => net + Math.round(net * rate / 100);
|
||||
export const termLabel = months => months === null ? 'Einmalig' : months === 12 ? 'Pro Jahr' : `Pro ${months} Monate`;
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const list = document.getElementById('price-list');
|
||||
const status = document.getElementById('price-status');
|
||||
const refresh = document.getElementById('refresh-prices');
|
||||
let catalog = null;
|
||||
let pending = false;
|
||||
const money = new Intl.NumberFormat('de-CH', { style:'currency', currency:'CHF' });
|
||||
function el(tag, text, className) { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; }
|
||||
function render() {
|
||||
list.replaceChildren();
|
||||
if (!catalog) return;
|
||||
const gross = document.querySelector('[name="tax"]:checked').value === 'gross';
|
||||
const amount = value => money.format((gross ? grossAmount(value, catalog.vatRate) : value) / 100);
|
||||
const table = el('table', undefined, 'price-table');
|
||||
table.append(el('caption', `${gross ? 'Inklusive' : 'Zuzüglich'} ${new Intl.NumberFormat('de-CH').format(catalog.vatRate)} % MwSt. · Preise je Einheit`));
|
||||
const head = el('thead'), row = el('tr');
|
||||
for (const text of ['Produkt', 'Laufzeit', 'Lizenz', 'Ersteinrichtung']) { const th = el('th', text); th.scope = 'col'; row.append(th); }
|
||||
head.append(row); table.append(head);
|
||||
const body = el('tbody');
|
||||
for (const item of Object.values(catalog.items)) {
|
||||
const tr = el('tr'), name = el('th', item.name); name.scope = 'row'; name.append(el('small', item.sku));
|
||||
tr.append(name, el('td', termLabel(item.termMonths)), el('td', amount(item.unitAmount), 'amount'), el('td', amount(item.setupAmount), 'amount'));
|
||||
body.append(tr);
|
||||
}
|
||||
table.append(body);
|
||||
const wrapper = el('div', undefined, 'table-scroll'); wrapper.tabIndex = 0; wrapper.setAttribute('role', 'region'); wrapper.setAttribute('aria-label', 'Aktuelle Preise'); wrapper.append(table); list.append(wrapper);
|
||||
}
|
||||
async function load() {
|
||||
if (pending) return;
|
||||
pending = true; refresh.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/catalog', { cache:'no-store', credentials:'omit', signal:AbortSignal.timeout(10000) });
|
||||
if (!response.ok) throw new Error('catalog_unavailable');
|
||||
catalog = validateCatalog(await response.json());
|
||||
render(); status.classList.remove('error');
|
||||
status.textContent = `Stand ${new Date().toLocaleTimeString('de-CH')} · automatisch alle 30 Sekunden aktualisiert.`;
|
||||
} catch {
|
||||
catalog = null; list.replaceChildren(); status.classList.add('error');
|
||||
status.textContent = 'Aktuelle Preise sind vorübergehend nicht erreichbar. Bitte erneut aktualisieren. Es werden keine veralteten Ersatzpreise angezeigt.';
|
||||
} finally { pending = false; refresh.disabled = false; }
|
||||
}
|
||||
refresh.addEventListener('click', load);
|
||||
document.querySelectorAll('[name="tax"]').forEach(input => input.addEventListener('change', render));
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); });
|
||||
window.addEventListener('pageshow', event => { if (event.persisted) load(); });
|
||||
setInterval(() => { if (!document.hidden) load(); }, 30000);
|
||||
load();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
:root { --ink:#172124; --muted:#59696d; --line:#d8e0de; --paper:#fff; --canvas:#f2f5f4; --petrol:#2f6b66; --lime:#d5df38; --danger:#9a3939; font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; color:var(--ink); background:var(--paper); letter-spacing:0; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; min-width:320px; }
|
||||
a { color:var(--petrol); text-underline-offset:3px; }
|
||||
a:hover { color:#153f3b; }
|
||||
button,input { font:inherit; }
|
||||
button,a,input,summary { -webkit-tap-highlight-color:transparent; }
|
||||
:focus-visible { outline:3px solid #497fbe; outline-offset:3px; }
|
||||
.skip { position:fixed; top:-60px; left:16px; z-index:100; background:#fff; padding:12px; }
|
||||
.skip:focus { top:8px; }
|
||||
.topbar { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; gap:16px; min-height:76px; padding:14px 32px; background:#fff; border-bottom:1px solid var(--line); }
|
||||
.brand { display:flex; align-items:center; gap:10px; text-decoration:none; color:var(--ink); }
|
||||
.brand-mark { display:grid; place-items:center; width:38px; height:38px; border-radius:4px; background:var(--lime); font-weight:850; font-size:23px; }
|
||||
.brand strong { display:block; font-size:24px; line-height:1; }
|
||||
.brand small { display:block; margin-top:5px; font-size:12px; color:var(--muted); }
|
||||
.topbar nav { display:flex; align-items:center; flex-wrap:wrap; gap:24px; font-size:14px; font-weight:650; }
|
||||
.topbar nav a { text-decoration:none; }
|
||||
.portal-link { padding-left:24px; border-left:1px solid var(--line); }
|
||||
.docs-layout { display:grid; grid-template-columns:248px minmax(0,850px) minmax(170px,220px); max-width:1500px; margin:auto; }
|
||||
.sidebar { position:sticky; top:76px; height:calc(100dvh - 76px); overflow-y:auto; padding:26px 20px; border-right:1px solid var(--line); background:var(--canvas); }
|
||||
#docs-menu > summary { display:none; }
|
||||
.sidebar section { margin:24px 0; }
|
||||
.sidebar h2,.contents h2 { font-size:12px; color:var(--muted); font-weight:700; margin:0 0 10px; }
|
||||
.sidebar ul { list-style:none; margin:0; padding:0; }
|
||||
.sidebar li { margin:2px 0; }
|
||||
.sidebar a { display:block; padding:8px 10px; font-size:13px; line-height:1.35; color:#425456; text-decoration:none; border-left:3px solid transparent; overflow-wrap:anywhere; transition:background .15s; }
|
||||
.sidebar a:hover { background:#e5edeb; }
|
||||
.sidebar a[aria-current] { background:#fff; border-left-color:var(--petrol); color:var(--petrol); font-weight:750; }
|
||||
.nav-folder > summary { display:list-item; list-style-position:inside; min-height:36px; padding:8px 10px; font-size:13px; line-height:1.5; color:#425456; font-weight:650; cursor:pointer; overflow-wrap:anywhere; border-left:3px solid transparent; }
|
||||
.nav-folder > summary:hover { background:#e5edeb; }
|
||||
.nav-folder > summary::marker { color:var(--petrol); }
|
||||
.nav-folder[data-active] > summary { color:var(--petrol); border-left-color:var(--petrol); }
|
||||
.nav-folder > ul { margin:4px 0 8px 14px; padding-left:6px; border-left:1px solid #c4d3cd; }
|
||||
.search-label { display:block; font-size:12px; margin-bottom:8px; font-weight:650; }
|
||||
#doc-search { width:100%; border:1px solid #b4c5c0; border-radius:4px; padding:10px; background:#fff; font-size:13px; }
|
||||
#search-result { font-size:12px; color:var(--muted); }
|
||||
main { min-width:0; padding:30px 44px 56px; }
|
||||
.page-meta { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:24px; color:var(--muted); font-size:12px; }
|
||||
.channel { padding:5px 8px; background:#edf3e5; color:#385c28; border-radius:3px; font-weight:650; }
|
||||
.sync-status { margin:16px 0 30px; color:var(--muted); font-size:12px; line-height:1.5; }
|
||||
.sync-status.warning { border-left:3px solid #aa731f; padding-left:10px; color:#80570e; }
|
||||
article { font-size:15px; line-height:1.75; overflow-wrap:anywhere; }
|
||||
article h1 { font-size:34px; line-height:1.18; margin:0 0 20px; font-weight:760; color:#173f3c; }
|
||||
article h2 { font-size:23px; line-height:1.3; margin:38px 0 16px; padding-top:14px; border-top:1px solid var(--line); }
|
||||
article h3 { font-size:18px; line-height:1.4; margin:28px 0 12px; }
|
||||
article h4 { font-size:16px; }
|
||||
article h1,article h2,article h3,article h4 { scroll-margin-top:100px; }
|
||||
article p { margin:12px 0 18px; }
|
||||
article li { margin:7px 0; padding-left:3px; }
|
||||
article ul,article ol { padding-left:23px; }
|
||||
article blockquote { margin:24px 0; padding:4px 18px; border-left:3px solid var(--petrol); background:#eff5f2; color:#314e46; }
|
||||
article pre { overflow:auto; padding:18px; border:1px solid var(--line); background:var(--canvas); font-size:12px; line-height:1.65; border-radius:4px; }
|
||||
article code { font-family:ui-monospace,"SFMono-Regular",Consolas,monospace; font-size:.86em; background:#f0f3f1; padding:2px 4px; border-radius:3px; }
|
||||
article pre code { padding:0; }
|
||||
.heading-anchor { padding-left:8px; opacity:0; font-weight:400; text-decoration:none; }
|
||||
h1:hover .heading-anchor,h2:hover .heading-anchor,h3:hover .heading-anchor,.heading-anchor:focus { opacity:1; }
|
||||
.table-scroll { max-width:100%; overflow-x:auto; margin:20px 0; }
|
||||
table { border-collapse:collapse; width:100%; font-size:13px; line-height:1.55; text-align:left; }
|
||||
th { color:#284a45; background:#eef4f1; font-weight:700; }
|
||||
th,td { padding:12px; border-bottom:1px solid var(--line); vertical-align:top; }
|
||||
article table code { overflow-wrap:normal; }
|
||||
.unpublished { color:inherit; }
|
||||
.doc-image { display:block; margin-top:22px; border:1px solid var(--line); border-radius:4px; overflow:hidden; }
|
||||
.doc-image img { display:block; max-width:100%; width:100%; height:auto; }
|
||||
.image-caption { display:block; margin-top:8px; color:var(--muted); font-size:12px; line-height:1.6; }
|
||||
.contents { position:sticky; top:76px; align-self:start; max-height:calc(100dvh - 76px); overflow:auto; padding:36px 20px 25px 0; }
|
||||
.contents a { display:block; border-left:1px solid var(--line); padding:6px 12px; color:var(--muted); font-size:12px; line-height:1.5; text-decoration:none; overflow-wrap:anywhere; }
|
||||
.contents a:hover { border-left-color:var(--petrol); color:var(--petrol); }
|
||||
footer { margin-top:48px; padding-top:20px; border-top:1px solid var(--line); font-size:12px; color:var(--muted); line-height:1.7; overflow-wrap:anywhere; }
|
||||
footer p { margin:4px 0; }
|
||||
.price-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:16px; justify-content:space-between; }
|
||||
.price-toolbar fieldset { display:flex; gap:14px; border:0; padding:0; margin:0; font-size:14px; }
|
||||
.price-toolbar legend { font-size:12px; color:var(--muted); margin-bottom:5px; }
|
||||
.price-toolbar label { display:flex; align-items:center; gap:6px; }
|
||||
.price-toolbar button { background:#fff; border:1px solid var(--line); border-radius:4px; padding:9px 12px; color:var(--petrol); cursor:pointer; }
|
||||
.price-toolbar button:disabled { opacity:.5; cursor:wait; }
|
||||
.price-table th:not(:first-child),.price-table td:not(:first-child) { text-align:right; }
|
||||
.price-table td:nth-child(2) { white-space:nowrap; }
|
||||
.price-table small { display:block; color:var(--muted); font-weight:400; font-size:11px; }
|
||||
.price-table .amount { white-space:nowrap; }
|
||||
.price-table caption { text-align:left; margin-bottom:12px; font-size:13px; color:var(--muted); }
|
||||
#price-status { font-size:13px; color:var(--muted); }
|
||||
#price-status.error { color:var(--danger); }
|
||||
[hidden] { display:none!important; }
|
||||
@media (max-width:1150px) { .docs-layout { grid-template-columns:228px minmax(0,1fr); } .contents { display:none; } main { padding:28px 36px; } }
|
||||
@media (max-width:720px) { .topbar { position:relative; padding:16px; flex-wrap:wrap; } .topbar nav { gap:20px; font-size:13px; width:100%; justify-content:space-between; } .portal-link { border:0; padding:0; } .docs-layout { display:block; } .sidebar { position:relative; top:0; height:auto; border:0; border-bottom:1px solid var(--line); padding:0 20px; } #docs-menu > summary { display:list-item; padding:15px 0; font-weight:650; cursor:pointer; font-size:14px; } #docs-menu[open] { padding-bottom:20px; } .sidebar nav { display:block; } .sidebar section { margin:16px 0 0; } .sidebar a,.nav-folder > summary { min-height:40px; } main { padding:24px 20px 40px; } article h1 { font-size:29px; } article h2 { font-size:21px; } article { font-size:15px; } .sync-status { margin-bottom:24px; } .price-table { min-width:570px; } article h1,article h2,article h3 { scroll-margin-top:20px; } }
|
||||
@media print { .topbar,.sidebar,.contents,.price-toolbar,.heading-anchor { display:none; } .docs-layout { display:block; } main { padding:0; } .table-scroll { overflow:visible; } article { font-size:11px; } a { color:inherit; } }
|
||||
@media (prefers-reduced-motion:reduce) { * { transition:none!important; scroll-behavior:auto!important; } }
|
||||
@@ -0,0 +1,47 @@
|
||||
const menu = document.getElementById('docs-menu');
|
||||
const mobile = matchMedia('(max-width:720px)');
|
||||
const setMenu = () => { menu.open = !mobile.matches; };
|
||||
setMenu();
|
||||
mobile.addEventListener('change', setMenu);
|
||||
const search = document.getElementById('doc-search');
|
||||
const folders = [...menu.querySelectorAll('.nav-folder')];
|
||||
let beforeSearch = null;
|
||||
const normalize = value => value.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
search.addEventListener('input', () => {
|
||||
const query = normalize(search.value.trim());
|
||||
if (query && !beforeSearch) beforeSearch = new Map(folders.map(folder => [folder, folder.open]));
|
||||
let count = 0;
|
||||
for (const item of document.querySelectorAll('[data-search]')) {
|
||||
item.hidden = !normalize(item.dataset.search).includes(query);
|
||||
if (!item.hidden) count++;
|
||||
}
|
||||
for (const folder of folders) {
|
||||
const matches = [...folder.querySelectorAll('[data-search]')].some(item => !item.hidden);
|
||||
folder.parentElement.hidden = !matches;
|
||||
if (query) folder.open = matches;
|
||||
else if (beforeSearch) folder.open = beforeSearch.get(folder);
|
||||
}
|
||||
if (!query) beforeSearch = null;
|
||||
for (const group of menu.querySelectorAll('section')) group.hidden = ![...group.querySelectorAll('[data-search]')].some(item => !item.hidden);
|
||||
const result = document.getElementById('search-result');
|
||||
result.hidden = !query;
|
||||
result.textContent = count ? `${count} passende Themen` : 'Keine passenden Themen gefunden.';
|
||||
});
|
||||
async function syncStatus() {
|
||||
const label = document.getElementById('sync-status');
|
||||
try {
|
||||
const response = await fetch('/docs-status.json', { cache:'no-store', credentials:'omit', signal:AbortSignal.timeout(8000) });
|
||||
if (!response.ok) throw new Error();
|
||||
const status = await response.json();
|
||||
const date = new Date(status.lastSuccess);
|
||||
if (!Number.isFinite(date.valueOf())) throw new Error();
|
||||
const stale = !status.ok || Date.now() - date.valueOf() > 15 * 60 * 1000;
|
||||
label.classList.toggle('warning', stale);
|
||||
label.textContent = `${stale ? 'Git-Abgleich verzögert. Letzter erfolgreicher Abgleich' : 'Git zuletzt geprüft'}: ${date.toLocaleString('de-CH')}${status.drafts ? ' · neue Anleitungen noch nicht in Git veröffentlicht' : ''}.`;
|
||||
} catch {
|
||||
label.classList.add('warning');
|
||||
label.textContent = 'Der aktuelle Git-Abgleichstatus ist nicht erreichbar. Quellstand dieser Seite siehe unten.';
|
||||
}
|
||||
}
|
||||
syncStatus();
|
||||
setInterval(() => { if (!document.hidden) syncStatus(); }, 60000);
|
||||
Reference in New Issue
Block a user