gabriel / musehub public
domain-viewers.ts typescript
139 lines 5.5 KB
Raw
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, '&lt;')
27 .replace(/>/g, '&gt;')
28 .replace(/"/g, '&quot;');
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