/** * — Chronicle-of-self timeline, rendered as a canvas shape. * * Hits the server-rendered `/:space/rpast/render` endpoint via iframe for * the actual timeline/calendar visualization. The chip bar drives query * params; the iframe reloads on every change. * * Attribute: space — the space slug */ import '../../../shared/components/rstack-markwhen-view'; type ViewMode = 'timeline' | 'calendar'; interface ModuleInfo { module: string; label: string; icon: string; color?: string; } export class RpastViewer extends HTMLElement { static get observedAttributes() { return ['space']; } #space = ''; #selected: Set | null = null; // null = "all" #mode: ViewMode = 'timeline'; #modules: ModuleInfo[] = []; #shadow: ShadowRoot; constructor() { super(); this.#shadow = this.attachShadow({ mode: 'open' }); this.#shadow.innerHTML = `
Loading…Download .mw
`; } set space(v: string) { this.#space = v; void this.#init(); } get space() { return this.#space; } attributeChangedCallback(name: string, _old: string | null, next: string | null) { if (name === 'space') this.space = next ?? ''; } connectedCallback() { void this.#init(); } async #init() { if (!this.#space) return; try { // The modules endpoint is space-agnostic; any space path works. const res = await fetch(`/${this.#space}/rpast/api/modules`); this.#modules = await res.json(); } catch (err) { console.error('[rpast-viewer] failed to load module list', err); this.#modules = []; } this.#paintBar(); await this.#reload(); } #paintBar() { const bar = this.#shadow.querySelector('.bar')!; bar.innerHTML = ''; const allChip = document.createElement('span'); allChip.className = 'chip'; allChip.setAttribute('aria-pressed', String(this.#selected === null)); allChip.textContent = '✨ All'; allChip.addEventListener('click', () => { this.#selected = null; this.#paintBar(); void this.#reload(); }); bar.appendChild(allChip); for (const m of this.#modules) { const chip = document.createElement('span'); chip.className = 'chip'; const active = this.#selected !== null && this.#selected.has(m.module); chip.setAttribute('aria-pressed', String(active)); chip.textContent = `${m.icon} ${m.label}`; chip.addEventListener('click', () => this.#toggle(m.module)); bar.appendChild(chip); } const spacer = document.createElement('div'); spacer.className = 'spacer'; bar.appendChild(spacer); const toggle = document.createElement('div'); toggle.className = 'toggle'; for (const mode of ['timeline', 'calendar'] as const) { const btn = document.createElement('button'); btn.textContent = mode[0].toUpperCase() + mode.slice(1); btn.setAttribute('aria-pressed', String(this.#mode === mode)); btn.addEventListener('click', () => { this.#mode = mode; this.#paintBar(); void this.#reload(); }); toggle.appendChild(btn); } bar.appendChild(toggle); } #toggle(module: string) { if (this.#selected === null) this.#selected = new Set([module]); else if (this.#selected.has(module)) { this.#selected.delete(module); if (this.#selected.size === 0) this.#selected = null; } else this.#selected.add(module); this.#paintBar(); void this.#reload(); } #buildUrl(base: string): string { const params = new URLSearchParams(); if (this.#selected) params.set('modules', [...this.#selected].join(',')); params.set('view', this.#mode); return `/${this.#space}/rpast/${base}?${params.toString()}`; } async #reload() { const view = this.#shadow.querySelector('rstack-markwhen-view') as HTMLElement & { src?: string; view?: ViewMode; }; view.view = this.#mode; view.src = this.#buildUrl('render'); const dl = this.#shadow.querySelector('.download') as HTMLAnchorElement; dl.href = this.#buildUrl('api/chronicle.mw'); dl.download = `${this.#space}-rpast.mw`; try { const res = await fetch(this.#buildUrl('api/chronicle')); const data = await res.json() as { count: number; sections: { id: string; count: number }[] }; const foot = this.#shadow.querySelector('.count') as HTMLElement; foot.textContent = `${data.count.toLocaleString()} creation${data.count === 1 ? '' : 's'} across ${data.sections.length} module${data.sections.length === 1 ? '' : 's'}`; } catch (err) { console.error('[rpast-viewer] count fetch failed', err); } } } if (!customElements.get('rpast-viewer')) { customElements.define('rpast-viewer', RpastViewer); }