import { createPlugin, windowTransport, type PageSummary } from '@dorfteich/plugin-sdk'; import de from '../i18n/de.json'; import en from '../i18n/en.json'; /** * Page-index reference plugin (issue #77, plugin-architecture.md §Reference * plugins): lists the pond's pages the viewer may read (`readPond` — the * server filters per page, so permissions are inherently respected), with * label chips to filter by. Clicking a page navigates via `ui.openPage`. * * Strings are bundled (the sandbox CSP blocks runtime fetches); the files in * `i18n/` are the single source and are inlined at build time. */ const STRINGS: Record> = { de, en }; function labelFor(locale: string, key: string): string { const base = locale.split('-')[0] ?? locale; return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key; } const { host } = createPlugin({ transport: windowTransport({ // Window.postMessage's overloads don't structurally match the transport's // minimal shape; this adapter pins the sandbox-safe wildcard origin. target: { postMessage: (message) => window.parent.postMessage(message, '*') }, source: window, }), onRender: (context) => void render(context.locale), }); /** The active label filter; `null` shows every readable page. */ let activeLabel: string | null = null; async function render(locale: string): Promise { let pages: PageSummary[]; try { pages = await host.readPond.listPages(); } catch { document.body.textContent = labelFor(locale, 'error'); return; } document.body.textContent = ''; document.body.className = 'dt-page-index'; const labels = [...new Set(pages.flatMap((page) => page.labels))].sort(); if (labels.length > 0) { document.body.appendChild(buildFilterBar(labels, locale)); } const shown = activeLabel ? pages.filter((page) => page.labels.includes(activeLabel!)) : pages; if (shown.length === 0) { const empty = document.createElement('p'); empty.textContent = labelFor(locale, 'empty'); document.body.appendChild(empty); } else { const list = document.createElement('ul'); for (const page of shown) { const item = document.createElement('li'); const link = document.createElement('a'); link.href = '#'; link.textContent = page.title; link.dataset.pageId = page.id; link.addEventListener('click', (event) => { event.preventDefault(); void host.ui.openPage(page.id); }); item.appendChild(link); if (page.labels.length > 0) { const chips = document.createElement('span'); chips.textContent = ` (${page.labels.join(', ')})`; item.appendChild(chips); } list.appendChild(item); } document.body.appendChild(list); } void host.ui.resize(document.body.scrollHeight + 16); } function buildFilterBar(labels: string[], locale: string): HTMLElement { const bar = document.createElement('p'); bar.className = 'dt-page-index__filters'; const options: Array<{ value: string | null; text: string }> = [ { value: null, text: labelFor(locale, 'all') }, ...labels.map((label) => ({ value: label, text: label })), ]; for (const option of options) { const button = document.createElement('button'); button.type = 'button'; button.textContent = option.text; button.dataset.filter = option.value ?? ''; button.style.fontWeight = option.value === activeLabel ? 'bold' : 'normal'; button.addEventListener('click', () => { activeLabel = option.value; void render(locale); }); bar.appendChild(button); bar.appendChild(document.createTextNode(' ')); } return bar; }