domain-viewers.ts
typescript
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b
Merge branch 'fix/wire-push-external-parent-manifest' into dev
Human
8 days ago
| 1 | /** |
| 2 | * domain-viewers.ts — the v1 viewer palette's actual rendering (musehub#117 DOM_09/DOM_10). |
| 3 | * |
| 4 | * Each viewer takes only a `viewerType` string and the domain's declared |
| 5 | * `dimensions` list (schema-validated server-side — see |
| 6 | * musehub/api/routes/musehub/domains.py's DomainCapabilities model) and |
| 7 | * renders real SVG. No per-domain executable code runs here — a domain |
| 8 | * manifest can only *select* one of these built-in, reviewed viewers and |
| 9 | * supply the dimension names/descriptions that parameterize it. |
| 10 | * |
| 11 | * Hand-rolled SVG via template strings, matching the existing pattern in |
| 12 | * src/ts/pages/timeline.ts and the sparkline in src/ts/pages/symbols.ts — |
| 13 | * no charting library dependency needed or added. |
| 14 | */ |
| 15 | |
| 16 | import { domainColor } from './domain-palette.ts'; |
| 17 | |
| 18 | export interface DimensionSpec { |
| 19 | name: string; |
| 20 | description?: string; |
| 21 | } |
| 22 | |
| 23 | function escapeXml(s: string): string { |
| 24 | return s |
| 25 | .replace(/&/g, '&') |
| 26 | .replace(/</g, '<') |
| 27 | .replace(/>/g, '>') |
| 28 | .replace(/"/g, '"'); |
| 29 | } |
| 30 | |
| 31 | /** Deterministic pseudo-activity seed from a dimension's name — gives each |
| 32 | * track a stable, distinct-looking pattern without any real commit data. */ |
| 33 | function hashStr(s: string): number { |
| 34 | let h = 0; |
| 35 | for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; |
| 36 | return Math.abs(h); |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * symbol_graph — a central "repo" node radiating to one node per dimension, |
| 41 | * styled like a lightweight commit/symbol graph (see code-demo.html for the |
| 42 | * full-scale reference this palette entry is modeled after). |
| 43 | */ |
| 44 | export function renderSymbolGraphViewer(container: HTMLElement, dimensions: DimensionSpec[]): void { |
| 45 | const color = domainColor('symbol_graph'); |
| 46 | const dims = dimensions.length ? dimensions : [{ name: 'state' }]; |
| 47 | const W = Math.max(container.clientWidth || 600, 360); |
| 48 | const H = Math.max(160, dims.length * 32 + 40); |
| 49 | const cx = 56; |
| 50 | const cy = H / 2; |
| 51 | const nodeX = Math.min(W - 80, 220); |
| 52 | |
| 53 | const nodes = dims.map((d, i) => { |
| 54 | const y = dims.length === 1 ? cy : 24 + (i / (dims.length - 1)) * (H - 48); |
| 55 | return { x: nodeX, y, name: d.name }; |
| 56 | }); |
| 57 | |
| 58 | const edges = nodes |
| 59 | .map(n => `<line x1="${cx}" y1="${cy}" x2="${n.x}" y2="${n.y}" stroke="${color}" stroke-opacity="0.35" stroke-width="1.5"/>`) |
| 60 | .join(''); |
| 61 | const nodeMarks = nodes |
| 62 | .map( |
| 63 | n => ` |
| 64 | <circle cx="${n.x}" cy="${n.y}" r="6" fill="${color}"/> |
| 65 | <text x="${n.x + 10}" y="${n.y + 4}" fill="currentColor" font-size="12" font-family="ui-monospace, monospace">${escapeXml(n.name)}</text>` |
| 66 | ) |
| 67 | .join(''); |
| 68 | |
| 69 | container.innerHTML = ` |
| 70 | <svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" class="domain-viewer-svg" role="img" aria-label="Symbol graph preview"> |
| 71 | ${edges} |
| 72 | <circle cx="${cx}" cy="${cy}" r="10" fill="${color}"/> |
| 73 | <text x="${cx}" y="${cy + 26}" fill="currentColor" font-size="11" text-anchor="middle" opacity="0.7">repo</text> |
| 74 | ${nodeMarks} |
| 75 | </svg>`; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * piano_roll — one horizontal track per dimension with deterministic |
| 80 | * activity blocks, DAW-style (see midi-demo.html for the full-scale |
| 81 | * reference this palette entry is modeled after). |
| 82 | */ |
| 83 | export function renderPianoRollViewer(container: HTMLElement, dimensions: DimensionSpec[]): void { |
| 84 | const color = domainColor('piano_roll'); |
| 85 | const dims = dimensions.length ? dimensions : [{ name: 'track' }]; |
| 86 | const rowH = 28; |
| 87 | const labelW = 130; |
| 88 | const W = Math.max(container.clientWidth || 600, 360); |
| 89 | const H = dims.length * rowH + 16; |
| 90 | const laneW = W - labelW - 16; |
| 91 | const blockCount = 6; |
| 92 | |
| 93 | const rows = dims |
| 94 | .map((d, i) => { |
| 95 | const y = 8 + i * rowH; |
| 96 | const seed = hashStr(d.name); |
| 97 | const blocks = Array.from({ length: blockCount }, (_, j) => { |
| 98 | const on = ((seed >> j) & 1) === 1; |
| 99 | if (!on) return ''; |
| 100 | const x = labelW + j * (laneW / blockCount); |
| 101 | const w = laneW / blockCount - 4; |
| 102 | return `<rect x="${x}" y="${y + 4}" width="${Math.max(w, 2)}" height="${rowH - 10}" rx="3" fill="${color}" fill-opacity="0.6"/>`; |
| 103 | }).join(''); |
| 104 | return ` |
| 105 | <text x="4" y="${y + rowH / 2 + 4}" fill="currentColor" font-size="11" font-family="ui-monospace, monospace">${escapeXml(d.name)}</text> |
| 106 | <line x1="${labelW}" y1="${y}" x2="${W - 8}" y2="${y}" stroke="currentColor" stroke-opacity="0.08"/> |
| 107 | ${blocks}`; |
| 108 | }) |
| 109 | .join(''); |
| 110 | |
| 111 | container.innerHTML = ` |
| 112 | <svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" class="domain-viewer-svg" role="img" aria-label="Track preview"> |
| 113 | ${rows} |
| 114 | </svg>`; |
| 115 | } |
| 116 | |
| 117 | /** generic — no built-in visual palette entry; list the declared dimensions |
| 118 | * as plain text so the fallback is informative, not a blank gray icon. */ |
| 119 | export function renderGenericViewer(container: HTMLElement, dimensions: DimensionSpec[]): void { |
| 120 | if (!dimensions.length) { |
| 121 | container.innerHTML = `<p class="domain-viewer-empty">No dimensions declared for this domain yet.</p>`; |
| 122 | return; |
| 123 | } |
| 124 | const items = dimensions |
| 125 | .map(d => `<li>${escapeXml(d.name)}${d.description ? ' — ' + escapeXml(d.description) : ''}</li>`) |
| 126 | .join(''); |
| 127 | container.innerHTML = `<ul class="domain-viewer-list">${items}</ul>`; |
| 128 | } |
| 129 | |
| 130 | /** Dispatch to the palette entry for `viewerType`, falling back to generic. */ |
| 131 | export function renderDomainViewer( |
| 132 | container: HTMLElement, |
| 133 | viewerType: string, |
| 134 | dimensions: DimensionSpec[] |
| 135 | ): void { |
| 136 | if (viewerType === 'symbol_graph') return renderSymbolGraphViewer(container, dimensions); |
| 137 | if (viewerType === 'piano_roll') return renderPianoRollViewer(container, dimensions); |
| 138 | return renderGenericViewer(container, dimensions); |
| 139 | } |
File History
3 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b
Merge branch 'fix/wire-push-external-parent-manifest' into dev
Human
8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226
Merge branch 'fix/two-column-scroll-layout' into dev
Human
8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454
chore: bump version to 0.2.0.dev2 — nightly.2, matching muse
Sonnet 4.6
patch
11 days ago