Skip to content

Commit 5e1dfec

Browse files
committed
Implement common-mermaid
1 parent af2795f commit 5e1dfec

File tree

3 files changed

+180
-0
lines changed

3 files changed

+180
-0
lines changed

typescript/packages/lookslike-high-level/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
"@commontools/lookslike-sagas": "^0.0.1",
6666
"marked": "^14.1.3",
6767
"merkle-reference": "^1.1.0",
68+
"mermaid": "^11.4.1",
69+
"panzoom": "^9.4.3",
6870
"zod": "^3.x.x",
6971
"zod-to-json-schema": "^3.23.3"
7072
},

typescript/packages/lookslike-high-level/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * as CommonShaderLayer from "./shader-layer.js";
1212
export * as CommonDroppable from "./common-droppable.js";
1313
export * as CommonDraggable from "./common-draggable.js";
1414
export * as CommonSpellEditor from "./common-spell-editor.js";
15+
export * as CommonMermaid from "./mermaid.js";
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { LitElement, html, css } from "lit-element";
2+
import { customElement, property } from "lit/decorators.js";
3+
import mermaid from "mermaid";
4+
import panzoom from "panzoom";
5+
6+
@customElement("common-mermaid")
7+
export default class MermaidElement extends LitElement {
8+
@property({ type: String }) diagram = "";
9+
10+
private panzoomInstance: ReturnType<typeof panzoom> | null = null;
11+
12+
static override styles = css`
13+
:host {
14+
display: block;
15+
width: 100%;
16+
height: 100%;
17+
min-height: 300px;
18+
position: relative;
19+
overflow: hidden;
20+
}
21+
22+
.container {
23+
position: absolute;
24+
width: 100%;
25+
height: 100%;
26+
}
27+
28+
.mermaid-content {
29+
position: absolute;
30+
width: fit-content;
31+
height: fit-content;
32+
min-width: 100px;
33+
min-height: 100px;
34+
}
35+
36+
.controls {
37+
position: absolute;
38+
bottom: 10px;
39+
right: 10px;
40+
z-index: 1;
41+
display: flex;
42+
gap: 8px;
43+
}
44+
45+
button {
46+
padding: 8px;
47+
border: none;
48+
background: rgba(0, 0, 0, 0.6);
49+
color: white;
50+
border-radius: 4px;
51+
cursor: pointer;
52+
}
53+
54+
button:hover {
55+
background: rgba(0, 0, 0, 0.8);
56+
}
57+
`;
58+
59+
constructor() {
60+
super();
61+
mermaid.initialize({
62+
startOnLoad: false,
63+
theme: "default",
64+
er: {
65+
useMaxWidth: true
66+
}
67+
});
68+
}
69+
70+
override firstUpdated() {
71+
this.renderMermaid();
72+
}
73+
74+
override updated(changedProperties: Map<string | number | symbol, unknown>) {
75+
if (changedProperties.has("diagram")) {
76+
this.renderMermaid();
77+
}
78+
}
79+
80+
async renderMermaid() {
81+
const container = this.shadowRoot?.querySelector(".mermaid-content");
82+
if (!container || !this.diagram) return;
83+
84+
try {
85+
const { svg } = await mermaid.render("mermaid-graph", this.diagram);
86+
container.innerHTML = svg;
87+
88+
// Ensure the SVG takes up space
89+
const svgElement = container.querySelector('svg');
90+
if (svgElement) {
91+
svgElement.style.width = '100%';
92+
svgElement.style.height = '100%';
93+
}
94+
95+
// Wait for the SVG to be added to the DOM before initializing panzoom
96+
requestAnimationFrame(() => {
97+
this.initializePanzoom();
98+
this.fitContent();
99+
});
100+
} catch (error) {
101+
console.error("Failed to render mermaid diagram:", error);
102+
container.innerHTML = "<p>Failed to render diagram</p>";
103+
}
104+
}
105+
106+
private initializePanzoom() {
107+
const container = this.shadowRoot?.querySelector(".mermaid-content");
108+
if (!container) return;
109+
110+
if (this.panzoomInstance) {
111+
this.panzoomInstance.dispose();
112+
}
113+
114+
this.panzoomInstance = panzoom(container as HTMLElement, {
115+
maxZoom: 5,
116+
minZoom: 0.1,
117+
bounds: true,
118+
boundsPadding: 0.1
119+
});
120+
}
121+
122+
private fitContent() {
123+
const container = this.shadowRoot?.querySelector(".container");
124+
const content = this.shadowRoot?.querySelector(".mermaid-content");
125+
if (!container || !content) return;
126+
127+
const containerRect = container.getBoundingClientRect();
128+
const contentRect = content.getBoundingClientRect();
129+
130+
const scaleX = containerRect.width / contentRect.width;
131+
const scaleY = containerRect.height / contentRect.height;
132+
const scale = Math.min(scaleX, scaleY, 1);
133+
134+
if (this.panzoomInstance) {
135+
this.panzoomInstance.zoomAbs(0, 0, scale * 2.0); // 90% of perfect fit to add some padding
136+
this.panzoomInstance.moveTo(0, 0);
137+
}
138+
}
139+
140+
private resetView() {
141+
if (this.panzoomInstance) {
142+
this.fitContent();
143+
}
144+
}
145+
146+
private zoomIn() {
147+
if (this.panzoomInstance) {
148+
this.panzoomInstance.smoothZoom(0, 0, 1.5);
149+
}
150+
}
151+
152+
private zoomOut() {
153+
if (this.panzoomInstance) {
154+
this.panzoomInstance.smoothZoom(0, 0, 0.667);
155+
}
156+
}
157+
158+
override render() {
159+
return html`
160+
<div class="container">
161+
<div class="mermaid-content"></div>
162+
</div>
163+
<div class="controls">
164+
<button @click=${this.zoomIn}>+</button>
165+
<button @click=${this.zoomOut}>-</button>
166+
<button @click=${this.resetView}>Reset</button>
167+
</div>
168+
`;
169+
}
170+
171+
override disconnectedCallback() {
172+
super.disconnectedCallback();
173+
if (this.panzoomInstance) {
174+
this.panzoomInstance.dispose();
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)