/** * domain-viewers.ts — the v1 viewer palette's actual rendering (musehub#117 DOM_09/DOM_10). * * Each viewer takes only a `viewerType` string and the domain's declared * `dimensions` list (schema-validated server-side — see * musehub/api/routes/musehub/domains.py's DomainCapabilities model) and * renders real SVG. No per-domain executable code runs here — a domain * manifest can only *select* one of these built-in, reviewed viewers and * supply the dimension names/descriptions that parameterize it. * * Hand-rolled SVG via template strings, matching the existing pattern in * src/ts/pages/timeline.ts and the sparkline in src/ts/pages/symbols.ts — * no charting library dependency needed or added. */ import { domainColor } from './domain-palette.ts'; export interface DimensionSpec { name: string; description?: string; } function escapeXml(s: string): string { return s .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** Deterministic pseudo-activity seed from a dimension's name — gives each * track a stable, distinct-looking pattern without any real commit data. */ function hashStr(s: string): number { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h); } /** * symbol_graph — a central "repo" node radiating to one node per dimension, * styled like a lightweight commit/symbol graph (see code-demo.html for the * full-scale reference this palette entry is modeled after). */ export function renderSymbolGraphViewer(container: HTMLElement, dimensions: DimensionSpec[]): void { const color = domainColor('symbol_graph'); const dims = dimensions.length ? dimensions : [{ name: 'state' }]; const W = Math.max(container.clientWidth || 600, 360); const H = Math.max(160, dims.length * 32 + 40); const cx = 56; const cy = H / 2; const nodeX = Math.min(W - 80, 220); const nodes = dims.map((d, i) => { const y = dims.length === 1 ? cy : 24 + (i / (dims.length - 1)) * (H - 48); return { x: nodeX, y, name: d.name }; }); const edges = nodes .map(n => ``) .join(''); const nodeMarks = nodes .map( n => ` ${escapeXml(n.name)}` ) .join(''); container.innerHTML = ` ${edges} repo ${nodeMarks} `; } /** * piano_roll — one horizontal track per dimension with deterministic * activity blocks, DAW-style (see midi-demo.html for the full-scale * reference this palette entry is modeled after). */ export function renderPianoRollViewer(container: HTMLElement, dimensions: DimensionSpec[]): void { const color = domainColor('piano_roll'); const dims = dimensions.length ? dimensions : [{ name: 'track' }]; const rowH = 28; const labelW = 130; const W = Math.max(container.clientWidth || 600, 360); const H = dims.length * rowH + 16; const laneW = W - labelW - 16; const blockCount = 6; const rows = dims .map((d, i) => { const y = 8 + i * rowH; const seed = hashStr(d.name); const blocks = Array.from({ length: blockCount }, (_, j) => { const on = ((seed >> j) & 1) === 1; if (!on) return ''; const x = labelW + j * (laneW / blockCount); const w = laneW / blockCount - 4; return ``; }).join(''); return ` ${escapeXml(d.name)} ${blocks}`; }) .join(''); container.innerHTML = ` ${rows} `; } /** generic — no built-in visual palette entry; list the declared dimensions * as plain text so the fallback is informative, not a blank gray icon. */ export function renderGenericViewer(container: HTMLElement, dimensions: DimensionSpec[]): void { if (!dimensions.length) { container.innerHTML = `

No dimensions declared for this domain yet.

`; return; } const items = dimensions .map(d => `
  • ${escapeXml(d.name)}${d.description ? ' — ' + escapeXml(d.description) : ''}
  • `) .join(''); container.innerHTML = `
      ${items}
    `; } /** Dispatch to the palette entry for `viewerType`, falling back to generic. */ export function renderDomainViewer( container: HTMLElement, viewerType: string, dimensions: DimensionSpec[] ): void { if (viewerType === 'symbol_graph') return renderSymbolGraphViewer(container, dimensions); if (viewerType === 'piano_roll') return renderPianoRollViewer(container, dimensions); return renderGenericViewer(container, dimensions); }