bpmn-js-markdown-documentation-panel
Version:
A comprehensive documentation management plugin for Camunda Modeler with markdown support, element linking, and coverage tracking
1,484 lines (1,407 loc) • 83.2 kB
JavaScript
import { marked } from "marked";
//#region ../../node_modules/.pnpm/bpmn-js@13.2.2/node_modules/bpmn-js/lib/util/ModelUtil.js
/**
* @typedef { import('../model/Types').Element } Element
* @typedef { import('../model/Types').ModdleElement } ModdleElement
*/
/**
* Is an element of the given BPMN type?
*
* @param {Element|ModdleElement} element
* @param {string} type
*
* @return {boolean}
*/
function is(element, type) {
var bo = getBusinessObject(element);
return bo && typeof bo.$instanceOf === "function" && bo.$instanceOf(type);
}
/**
* Return the business object for a given element.
*
* @param {Element|ModdleElement} element
*
* @return {ModdleElement}
*/
function getBusinessObject(element) {
return element && element.businessObject || element;
}
//#endregion
//#region src/extension/managers/AutocompleteManager.ts
var AutocompleteManager = class {
_callbacks;
_selectedIndex = -1;
constructor(options) {
this._callbacks = options.callbacks;
}
setupAutocompleteEventListeners() {
setTimeout(() => {
const textarea = document.getElementById("doc-textarea");
if (!textarea) return;
textarea.addEventListener("input", () => {
this.handleAutocomplete();
});
textarea.addEventListener("keydown", (event) => {
this._handleAutocompleteKeydown(event);
});
document.addEventListener("click", (event) => {
const dropdown = document.getElementById("autocomplete-dropdown");
const textarea$1 = document.getElementById("doc-textarea");
if (dropdown && !(event.target instanceof Node && dropdown.contains(event.target)) && event.target !== textarea$1) this.hideAutocomplete();
});
}, 100);
}
handleAutocomplete() {
const textarea = document.getElementById("doc-textarea");
if (!textarea) return;
const cursorPos = textarea.selectionStart;
const text = textarea.value;
let hashPos = -1;
for (let i = cursorPos - 1; i >= 0; i--) {
if (text[i] === "#") {
hashPos = i;
break;
}
if (text[i] === " " || text[i] === "\n" || text[i] === " ") break;
}
if (hashPos >= 0) {
if (hashPos > 0 && text[hashPos - 1] === "(") {
let foundClosingBracket = false;
for (let i = hashPos - 2; i >= 0; i--) {
if (text[i] === "]") {
foundClosingBracket = true;
break;
}
if (text[i] === "\n" || text[i] === "\r") break;
}
if (foundClosingBracket) {
const searchText = text.substring(hashPos + 1, cursorPos);
if (!searchText.includes(" ") && !searchText.includes("\n")) {
this._showAutocomplete(searchText, hashPos);
return;
}
}
}
}
this.hideAutocomplete();
}
hideAutocomplete() {
const dropdown = document.getElementById("autocomplete-dropdown");
if (!dropdown) return;
dropdown.classList.remove("visible");
this._selectedIndex = -1;
}
destroy() {
this.hideAutocomplete();
}
_showAutocomplete(searchText, hashPos) {
const dropdown = document.getElementById("autocomplete-dropdown");
const autocompleteList = document.getElementById("autocomplete-list");
const textarea = document.getElementById("doc-textarea");
if (!dropdown || !autocompleteList || !textarea) return;
const allElements = this._getAllElements();
const filteredElements = allElements.filter((element) => element.id.toLowerCase().includes(searchText.toLowerCase()) || element.name.toLowerCase().includes(searchText.toLowerCase()));
if (filteredElements.length === 0) {
this.hideAutocomplete();
return;
}
autocompleteList.innerHTML = "";
filteredElements.slice(0, 10).forEach((element) => {
const item = document.createElement("div");
item.className = "autocomplete-item";
item.innerHTML = `
<div class="autocomplete-item-id">${element.id}</div>
<div class="autocomplete-item-name">${element.name}</div>
<div class="autocomplete-item-type">${element.type}</div>
`;
item.addEventListener("click", () => {
this._selectAutocompleteItem(element.id, hashPos);
});
autocompleteList.appendChild(item);
});
this._positionAutocomplete(textarea, hashPos);
dropdown.classList.add("visible");
this._selectedIndex = 0;
this._updateAutocompleteSelection(Array.from(autocompleteList.children));
}
_positionAutocomplete(textarea, hashPos) {
const dropdown = document.getElementById("autocomplete-dropdown");
if (!dropdown) return;
const textareaRect = textarea.getBoundingClientRect();
const style = window.getComputedStyle(textarea);
const tempSpan = document.createElement("span");
tempSpan.style.visibility = "hidden";
tempSpan.style.position = "absolute";
tempSpan.style.top = "-9999px";
tempSpan.style.fontFamily = style.fontFamily;
tempSpan.style.fontSize = style.fontSize;
tempSpan.style.fontWeight = style.fontWeight;
tempSpan.style.letterSpacing = style.letterSpacing;
tempSpan.style.whiteSpace = "pre";
const textUpToHash = textarea.value.substring(0, hashPos);
const linesUpToHash = textUpToHash.split("\n");
const currentLine = linesUpToHash[linesUpToHash.length - 1];
tempSpan.textContent = currentLine;
this._callbacks.getCanvasContainer().appendChild(tempSpan);
this._callbacks.getCanvasContainer().removeChild(tempSpan);
const left = textareaRect.left + 10;
const top = textareaRect.top + 100;
dropdown.style.setProperty("left", `${left}px`, "important");
dropdown.style.setProperty("top", `${top}px`, "important");
dropdown.style.setProperty("position", "fixed", "important");
dropdown.style.setProperty("z-index", "10001", "important");
}
_getAllElements() {
const elements = [];
const seenIds = /* @__PURE__ */ new Set();
const allElements = this._callbacks.getAllElements();
allElements.forEach((element) => {
if (element.businessObject?.id) {
const bo = element.businessObject;
const elementId = bo.id;
if (seenIds.has(elementId)) return;
seenIds.add(elementId);
elements.push({
id: elementId,
name: bo.name || "Unnamed",
type: this._callbacks.getElementTypeName(element)
});
}
});
return elements.sort((a, b) => a.id.localeCompare(b.id));
}
_handleAutocompleteKeydown(event) {
const dropdown = document.getElementById("autocomplete-dropdown");
if (!dropdown || !dropdown.classList.contains("visible")) return;
const items = Array.from(dropdown.querySelectorAll(".autocomplete-item"));
if (event.key === "ArrowDown") {
event.preventDefault();
this._selectedIndex = Math.min(this._selectedIndex + 1, items.length - 1);
this._updateAutocompleteSelection(items);
} else if (event.key === "ArrowUp") {
event.preventDefault();
this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
this._updateAutocompleteSelection(items);
} else if (event.key === "Enter") {
event.preventDefault();
if (this._selectedIndex >= 0 && items[this._selectedIndex]) {
const selectedId = items[this._selectedIndex].querySelector(".autocomplete-item-id")?.textContent;
const textarea = document.getElementById("doc-textarea");
if (!textarea) return;
const cursorPos = textarea.selectionStart;
const text = textarea.value;
let hashPos = -1;
for (let i = cursorPos - 1; i >= 0; i--) if (text[i] === "#") {
hashPos = i;
break;
}
if (hashPos >= 0 && selectedId) this._selectAutocompleteItem(selectedId, hashPos);
}
} else if (event.key === "Escape") {
event.preventDefault();
this.hideAutocomplete();
}
}
_updateAutocompleteSelection(items) {
const dropdown = document.getElementById("autocomplete-dropdown");
const autocompleteList = document.getElementById("autocomplete-list");
if (!dropdown || !autocompleteList) return;
Array.from(items).forEach((item, index) => {
if (index === this._selectedIndex) {
item.classList.add("selected");
const itemTop = item.offsetTop;
const itemBottom = itemTop + item.offsetHeight;
const dropdownTop = dropdown.scrollTop;
const dropdownBottom = dropdownTop + dropdown.clientHeight;
if (itemTop < dropdownTop) dropdown.scrollTop = itemTop;
else if (itemBottom > dropdownBottom) dropdown.scrollTop = itemBottom - dropdown.clientHeight;
} else item.classList.remove("selected");
});
}
_selectAutocompleteItem(elementId, hashPos) {
const textarea = document.getElementById("doc-textarea");
if (!textarea) return;
const text = textarea.value;
const cursorPos = textarea.selectionStart;
const beforeHash = text.substring(0, hashPos + 1);
const afterCursor = text.substring(cursorPos);
const newText = beforeHash + elementId + afterCursor;
textarea.value = newText;
const newCursorPos = hashPos + 1 + elementId.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
this.hideAutocomplete();
this._callbacks.updatePreview();
this._callbacks.saveDocumentationLive();
textarea.focus();
}
};
//#endregion
//#region src/extension/utils/MarkdownRenderer.ts
/**
* Enhanced markdown renderer with GitHub-style alerts and improved code block rendering
*/
var MarkdownRenderer = class MarkdownRenderer {
renderer;
constructor() {
this.renderer = new marked.Renderer();
this.setupRenderer();
}
setupRenderer() {
this.renderer.blockquote = (quote) => {
let quoteStr = "";
if (typeof quote === "string") quoteStr = quote;
else if (quote && typeof quote === "object") if (quote.text) quoteStr = quote.text;
else if (quote.tokens && Array.isArray(quote.tokens)) quoteStr = quote.tokens.map((token) => {
if (typeof token === "string") return token;
if (token?.text) return token.text;
if (token?.raw) return token.raw;
return "";
}).join("");
else quoteStr = quote ? `Unsupported quote format: ${JSON.stringify(quote)}` : "";
else quoteStr = quote ? String(quote) : "";
let alertMatch = quoteStr.match(/<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]<\/p>\s*<p>([\s\S]*?)<\/p>/);
if (!alertMatch) alertMatch = quoteStr.match(/<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s+([\s\S]*?)<\/p>/);
if (!alertMatch) alertMatch = quoteStr.match(/\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s+([\s\S]*?)$/);
if (alertMatch) {
const [, type, content] = alertMatch;
const alertType = type.toLowerCase();
const icon = this.getAlertIcon(alertType);
return `
<div class="markdown-alert markdown-alert-${alertType}">
<p class="markdown-alert-title">
${icon}
${type}
</p>
<div class="markdown-alert-content">${content}</div>
</div>
`;
}
const alertMarkerMatch = quoteStr.match(/GITHUB_ALERT_(NOTE|TIP|IMPORTANT|WARNING|CAUTION):([\s\S]*)$/);
if (alertMarkerMatch) {
const [, type, content] = alertMarkerMatch;
const alertType = type.toLowerCase();
const icon = this.getAlertIcon(alertType);
const cleanContent = content.trim();
return `
<div class="markdown-alert markdown-alert-${alertType}">
<p class="markdown-alert-title">
${icon}
${type}
</p>
${cleanContent && cleanContent !== "No content" ? `<div class="markdown-alert-content">${this.escapeHtml(cleanContent)}</div>` : ""}
</div>
`;
}
return `<blockquote>${quoteStr}</blockquote>`;
};
this.renderer.code = (code, language, _escaped, _meta) => {
let codeStr = "";
if (typeof code === "string") codeStr = code;
else if (code && typeof code === "object") if (code.text) codeStr = code.text;
else if (code.raw) codeStr = code.raw;
else codeStr = code ? `Unsupported code format: ${JSON.stringify(code)}` : "";
else codeStr = code ? String(code) : "";
const validLanguage = language || "";
let title = "";
let actualLanguage = validLanguage;
const blockIdMatch = codeStr.match(/<!-- BLOCK_ID:([^>]+) -->/);
if (blockIdMatch) {
const blockId = blockIdMatch[1];
const blockData = MarkdownRenderer.codeBlockData.get(blockId);
if (blockData) {
title = blockData.title;
actualLanguage = blockData.language;
}
codeStr = codeStr.replace(/<!-- BLOCK_ID:[^>]+ -->\s*/, "");
}
const displayLanguage = actualLanguage || validLanguage || "text";
const escapedCode = this.escapeHtml(codeStr);
const codeId = `code_${Math.random().toString(36).substr(2, 9)}`;
return `<div class="markdown-code-block">
<div class="markdown-code-header">
<div class="markdown-code-info">
${title ? `<span class="markdown-code-title">${this.escapeHtml(title)}</span>` : ""}
<span class="markdown-code-language">${displayLanguage}</span>
</div>
<button class="markdown-code-copy" onclick="copyCodeBlock('${codeId}')" title="Copy code">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
</svg>
</button>
</div>
<pre><code id="${codeId}" class="language-${displayLanguage}">${escapedCode}</code></pre>
</div>`;
};
this.renderer.codespan = (code) => {
return `<code class="markdown-inline-code">${this.escapeHtml(code)}</code>`;
};
}
getAlertIcon(type) {
const icons = {
note: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
</svg>`,
tip: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/>
</svg>`,
important: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
</svg>`,
warning: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
</svg>`,
caution: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
</svg>`
};
return icons[type] || icons.note;
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
/**
* Store for code block titles and languages (static storage during rendering)
*/
static codeBlockData = /* @__PURE__ */ new Map();
/**
* Renders markdown content with GitHub-style alerts and enhanced code blocks
*/
async render(markdown) {
console.log("🚀 MARKDOWN RENDERER CALLED WITH:", JSON.stringify(markdown.substring(0, 100)));
if (!markdown?.trim()) return "<em>No documentation.</em>";
MarkdownRenderer.codeBlockData.clear();
let processedMarkdown = markdown;
processedMarkdown = this.preprocessIncompleteCodeBlocks(processedMarkdown);
processedMarkdown = processedMarkdown.replace(/```([a-zA-Z0-9_+-]+):([^`\n]+)\n([\s\S]*?)```/g, (_match, language, title, code) => {
const blockId = `cb_${Math.random().toString(36).substr(2, 9)}`;
MarkdownRenderer.codeBlockData.set(blockId, {
title: title.trim(),
language
});
return `\`\`\`${language}\n<!-- BLOCK_ID:${blockId} -->\n${code}\n\`\`\``;
});
processedMarkdown = this.preprocessAlerts(processedMarkdown);
try {
const rendered = await Promise.resolve(marked(processedMarkdown, {
renderer: this.renderer,
gfm: true,
breaks: true
}));
return typeof rendered === "string" ? rendered : "";
} catch (error) {
console.error("Markdown rendering error:", error);
return `<div class="markdown-error">Error rendering markdown: ${error instanceof Error ? error.message : "Unknown error"}</div>`;
}
}
/**
* Pre-processes incomplete code blocks - only renders if properly closed
*/
preprocessIncompleteCodeBlocks(markdown) {
return markdown.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]+)$/m, (match, _language, content) => {
if (!content.includes("```")) {
console.log("FOUND INCOMPLETE CODE BLOCK - treating as regular text");
return content;
}
return match;
});
}
/**
* Pre-processes alerts (alerts are handled in the blockquote renderer)
*/
preprocessAlerts(markdown) {
return markdown;
}
};
//#endregion
//#region src/extension/managers/ExportManager.ts
var ExportManager = class {
_elementRegistry;
_markdownRenderer;
_moddle;
_canvas;
constructor(elementRegistry, moddle, canvas) {
this._elementRegistry = elementRegistry;
this._moddle = moddle;
this._canvas = canvas;
this._markdownRenderer = new MarkdownRenderer();
}
setupExportEventListeners() {
setTimeout(() => {
const exportBtn = document.getElementById("export-btn");
if (exportBtn) {
exportBtn.replaceWith(exportBtn.cloneNode(true));
const newBtn = document.getElementById("export-btn");
newBtn?.addEventListener("click", () => {
this.handleExport();
});
}
}, 100);
}
handleExport() {
this.exportDocumentation().catch((error) => {
console.error("Export failed:", error);
this._showNotification("Export failed", "error");
});
}
/**
* Export documentation in HTML format
*/
async exportDocumentation() {
try {
const processInfo = this._getProcessInfo();
const elements = this._getAllElementsWithDocumentation();
const documentedElements = elements.filter((el) => el.hasDocumentation);
if (documentedElements.length === 0) {
this._showNotification("No documented elements found to export", "warning");
return;
}
const htmlContent = await this._generateHTMLExport(elements, processInfo);
const processName = processInfo.name || processInfo.id || "Process";
const filename = `${processName}_Documentation.html`;
this._downloadFile(htmlContent, filename, "text/html");
this._showNotification(`Documentation exported successfully (${documentedElements.length} elements)`, "success");
} catch (error) {
console.error("Export failed:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
this._showNotification(`Export failed: ${errorMessage}`, "error");
}
}
/**
* Get BPMN diagram as SVG
*/
async _getDiagramSVG() {
try {
const canvasContainer = this._canvas.getContainer();
const svgElement = canvasContainer.querySelector("svg");
if (svgElement) {
const svgCopy = svgElement.cloneNode(true);
const allElements = this._elementRegistry.getAll();
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
allElements.forEach((element) => {
if (element.x !== void 0 && element.y !== void 0 && element.width !== void 0 && element.height !== void 0) {
minX = Math.min(minX, element.x);
minY = Math.min(minY, element.y);
maxX = Math.max(maxX, element.x + element.width);
maxY = Math.max(maxY, element.y + element.height);
}
});
const padding = 50;
const diagramWidth = maxX - minX;
const diagramHeight = maxY - minY;
const viewBoxX = minX - padding;
const viewBoxY = minY - padding;
const viewBoxWidth = diagramWidth + padding * 2;
const viewBoxHeight = diagramHeight + padding * 2;
const viewBox = `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`;
svgCopy.setAttribute("viewBox", viewBox);
const aspectRatio = viewBoxWidth / viewBoxHeight;
let svgWidth = 800;
let svgHeight = 600;
if (aspectRatio > svgWidth / svgHeight) svgHeight = svgWidth / aspectRatio;
else svgWidth = svgHeight * aspectRatio;
svgCopy.setAttribute("width", Math.round(svgWidth).toString());
svgCopy.setAttribute("height", Math.round(svgHeight).toString());
const background = document.createElementNS("http://www.w3.org/2000/svg", "rect");
background.setAttribute("x", viewBoxX.toString());
background.setAttribute("y", viewBoxY.toString());
background.setAttribute("width", viewBoxWidth.toString());
background.setAttribute("height", viewBoxHeight.toString());
background.setAttribute("fill", "#ffffff");
svgCopy.insertBefore(background, svgCopy.firstChild);
return svgCopy.outerHTML;
}
return "";
} catch (error) {
console.error("Error getting diagram SVG:", error);
return "";
}
}
/**
* Get all elements with their documentation status
*/
_getAllElementsWithDocumentation() {
const elements = [];
const seenIds = /* @__PURE__ */ new Set();
const allElements = this._elementRegistry.getAll();
allElements.forEach((element) => {
if (element.businessObject?.id) {
const bo = element.businessObject;
const elementId = bo.id;
if (seenIds.has(elementId)) return;
seenIds.add(elementId);
const documentation = this._getElementDocumentation(element);
elements.push({
id: elementId,
name: bo.name || "Unnamed",
type: this._getElementTypeName(element),
hasDocumentation: !!documentation?.trim(),
documentation: documentation || "",
element
});
}
});
return elements.sort((a, b) => a.id.localeCompare(b.id));
}
/**
* Get documentation for a specific element
*/
_getElementDocumentation(element) {
if (!element || !element.businessObject) return "";
const bo = element.businessObject;
if (bo.documentation && bo.documentation.length > 0) return bo.documentation[0].text || "";
return "";
}
/**
* Get element type name for display
*/
_getElementTypeName(element) {
if (!element || !element.businessObject) return "Unknown";
const bo = element.businessObject;
const type = bo.$type || "";
if (type.includes(":")) {
const typeName = type.split(":")[1];
return typeName.replace(/([A-Z])/g, " $1").trim();
}
return type || "Unknown";
}
/**
* Generate HTML export content
*/
async _generateHTMLExport(elements, processInfo) {
const totalElements = elements.length;
const documentedCount = elements.filter((el) => el.hasDocumentation).length;
const undocumentedCount = totalElements - documentedCount;
const coveragePercentage = totalElements > 0 ? Math.round(documentedCount / totalElements * 100) : 0;
const processTitle = processInfo.name || processInfo.id || "BPMN Process";
const processDocumentation = processInfo.element ? this._getElementDocumentation(processInfo.element) : "";
const diagramSVG = await this._getDiagramSVG();
const tocItems = elements.map((el) => `<li><a href="#element-${el.id}" class="toc-link">${el.name} (${el.id})</a></li>`).join("");
const elementSections = await Promise.all(elements.map(async (el) => {
const markdownContent = el.documentation || "";
let htmlContent = markdownContent ? await this._markdownRenderer.render(markdownContent) : "<p class='no-documentation'><em>No documentation available</em></p>";
htmlContent = this._fixElementLinksForExport(htmlContent, elements);
return `
<div class="element-section" id="element-${el.id}">
<div class="element-header">
<div class="element-title-info">
<h2 class="element-title">${el.name}</h2>
<div class="element-meta">
<span class="element-id">${el.id}</span>
</div>
</div>
</div>
<div class="element-content">
${htmlContent}
</div>
</div>
`;
})).then((sections) => sections.join(""));
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${this._escapeHtml(processTitle)} - Documentation</title>
<style>
${this._generateStyles()}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1 class="main-title">${this._escapeHtml(processTitle)}</h1>
${processDocumentation ? `<p class="subtitle">${this._escapeHtml(processDocumentation)}</p>` : ""}
<div class="export-info">
<span class="export-date">Generated on ${(/* @__PURE__ */ new Date()).toLocaleDateString()}</span>
</div>
</header>
<div class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">${totalElements}</div>
<div class="stat-label">Total Elements</div>
</div>
<div class="stat-card documented">
<div class="stat-number">${documentedCount}</div>
<div class="stat-label">Documented</div>
</div>
<div class="stat-card undocumented">
<div class="stat-number">${undocumentedCount}</div>
<div class="stat-label">Undocumented</div>
</div>
<div class="stat-card coverage">
<div class="stat-number">${coveragePercentage}%</div>
<div class="stat-label">Coverage</div>
</div>
</div>
</div>
${diagramSVG ? `
<div class="diagram-section">
<h2 class="section-title">Process Diagram</h2>
<div class="diagram-container">
${diagramSVG}
</div>
</div>
` : ""}
<div class="toc-section">
<h2 class="section-title">Table of Contents</h2>
<div class="toc-container">
<ul class="toc-list">
${tocItems}
</ul>
</div>
</div>
<div class="documentation-section">
<h2 class="section-title">Element Documentation</h2>
${elementSections}
</div>
<div class="back-to-top">
<button onclick="window.scrollTo({top: 0, behavior: 'smooth'})" class="back-to-top-btn">
↑ Back to Top
</button>
</div>
</div>
<script>
// Add smooth scrolling for table of contents links
document.querySelectorAll('.toc-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// Show/hide back to top button
window.addEventListener('scroll', function() {
const backToTop = document.querySelector('.back-to-top');
if (window.scrollY > 300) {
backToTop.style.display = 'block';
} else {
backToTop.style.display = 'none';
}
});
<\/script>
</body>
</html>`;
}
/**
* Fix element links in HTML content for export by adding element- prefix
*/
_fixElementLinksForExport(htmlContent, elements) {
const elementIds = new Set(elements.map((el) => el.id));
return htmlContent.replace(/href="#([^"]+)"/g, (match, elementId) => {
if (elementIds.has(elementId)) return `href="#element-${elementId}"`;
return match;
});
}
/**
* Generate CSS styles for the HTML export
*/
_generateStyles() {
return `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #262c33;
background: #f8f9fa;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 20px auto;
padding: 0;
background: white;
border: 1px solid #dae2ec;
border-radius: 8px;
overflow: hidden;
}
.header {
background: white;
color: #262c33;
padding: 60px 40px;
text-align: center;
border-bottom: 1px solid #dae2ec;
}
.main-title {
font-size: 2.5em;
font-weight: 600;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
margin-bottom: 20px;
}
.export-date {
font-size: 0.9em;
opacity: 0.8;
background: #fafafa;
padding: 4px 12px;
border-radius: 4px;
display: inline-block;
border: 1px solid #dae2ec;
}
.stats-section {
padding: 40px;
background: #fafafa;
border-bottom: 1px solid #dae2ec;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: white;
padding: 25px 20px;
border-radius: 4px;
text-align: center;
border: 1px solid #dae2ec;
}
.stat-number {
font-size: 2.2em;
font-weight: 600;
color: #262c33;
display: block;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.diagram-section {
padding: 40px;
background: white;
border-bottom: 1px solid #dae2ec;
}
.section-title {
font-size: 1.8em;
color: #262c33;
margin-bottom: 25px;
display: inline-block;
}
.diagram-container {
background: #fafafa;
border-radius: 4px;
padding: 20px;
text-align: center;
border: 1px solid #dae2ec;
}
.diagram-container svg {
max-width: 100%;
height: auto;
border-radius: 4px;
}
.toc-section {
padding: 40px;
background: #fafafa;
border-bottom: 1px solid #dae2ec;
}
.toc-container {
background: white;
border-radius: 4px;
padding: 30px;
border: 1px solid #dae2ec;
}
.toc-list {
list-style: none;
columns: 2;
column-gap: 30px;
column-fill: balance;
}
.toc-list li {
break-inside: avoid;
margin-bottom: 8px;
position: relative;
}
.toc-link {
color: #262c33;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
.toc-link:hover {
color: #666;
}
.documentation-section {
padding: 40px;
background: white;
}
.element-section {
margin-bottom: 30px;
background: white;
border-radius: 4px;
border: 1px solid #dae2ec;
overflow: hidden;
}
.element-header {
background: white;
color: #262c33;
padding: 20px 25px;
display: flex;
align-items: center;
gap: 15px;
border-bottom: 1px solid #dae2ec;
}
.element-title-info {
flex: 1;
}
.element-title {
font-size: 1.3em;
font-weight: 600;
margin-bottom: 8px;
}
.element-meta {
display: flex;
gap: 15px;
align-items: center;
}
.element-id {
font-size: 0.9em;
opacity: 0.8;
font-family: 'Monaco', 'Consolas', monospace;
}
.element-content {
padding: 25px;
background: white;
}
.element-content h1,
.element-content h2,
.element-content h3,
.element-content h4,
.element-content h5,
.element-content h6 {
color: #262c33;
margin-top: 20px;
margin-bottom: 12px;
font-weight: 600;
}
.element-content p {
margin-bottom: 15px;
line-height: 1.7;
}
.element-content ul,
.element-content ol {
margin-bottom: 15px;
padding-left: 25px;
}
.element-content li {
margin-bottom: 8px;
line-height: 1.6;
}
.element-content code {
background: #fafafa;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.9em;
color: #262c33;
}
.element-content pre {
background: #fafafa;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 15px;
border-left: 3px solid #bfcbd9;
}
.element-content blockquote {
border-left: 3px solid #bfcbd9;
padding-left: 15px;
margin: 15px 0;
color: #666;
font-style: italic;
background: #fafafa;
padding: 12px 15px;
border-radius: 0 4px 4px 0;
}
.element-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
border: 1px solid #dae2ec;
border-radius: 4px;
overflow: hidden;
}
.element-content th,
.element-content td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #dae2ec;
}
.element-content th {
background: #fafafa;
color: #262c33;
font-weight: 600;
}
.element-content tr:hover {
background: #fafafa;
}
.element-content a {
color: #262c33;
text-decoration: none;
font-weight: 500;
}
.element-content a:hover {
color: #666;
text-decoration: underline;
}
.no-documentation {
color: #666;
font-style: italic;
text-align: center;
padding: 15px;
background: #fafafa;
border-radius: 4px;
}
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
display: none;
z-index: 1000;
}
.back-to-top-btn {
background: white;
color: #262c33;
border: 1px solid #dae2ec;
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
}
.back-to-top-btn:hover {
background: #fafafa;
}
@media (max-width: 768px) {
.container {
margin: 0;
box-shadow: none;
}
.header {
padding: 40px 20px;
}
.main-title {
font-size: 2.2em;
}
.stats-section,
.diagram-section,
.toc-section,
.documentation-section {
padding: 20px;
}
.stats-grid {
grid-template-columns: 1fr;
}
.toc-list {
columns: 1;
}
.element-header {
padding: 20px;
flex-direction: column;
text-align: center;
gap: 10px;
}
.element-meta {
justify-content: center;
}
.element-content {
padding: 20px;
}
}
@media print {
body {
background: white;
}
.container {
box-shadow: none;
}
.back-to-top {
display: none;
}
.element-section {
page-break-inside: avoid;
break-inside: avoid;
}
}
`;
}
/**
* Escape HTML special characters
*/
_escapeHtml(unsafe) {
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
/**
* Get process information from BPMN diagram
*/
_getProcessInfo() {
try {
const rootElement = this._canvas.getRootElement();
if (rootElement?.businessObject) {
const bo = rootElement.businessObject;
return {
name: bo.name || null,
id: bo.id || null,
filename: "process.bpmn",
element: rootElement
};
}
return {
name: null,
id: null,
filename: "process.bpmn",
element: null
};
} catch (error) {
console.error("Error getting process info:", error);
return {
name: null,
id: null,
filename: "process.bpmn",
element: null
};
}
}
/**
* Download file to user's system
*/
_downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Show notification to user
*/
_showNotification(message, type) {
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
Object.assign(notification.style, {
position: "fixed",
top: "20px",
right: "20px",
padding: "12px 16px",
borderRadius: "4px",
color: "white",
fontSize: "14px",
fontWeight: "500",
zIndex: "10000",
maxWidth: "300px",
wordWrap: "break-word",
backgroundColor: type === "success" ? "#28a745" : type === "warning" ? "#ffc107" : "#dc3545",
boxShadow: "0 2px 8px rgba(0,0,0,0.2)"
});
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) notification.parentNode.removeChild(notification);
}, 3e3);
}
destroy() {}
};
//#endregion
//#region src/extension/managers/OverviewManager.ts
var OverviewManager = class {
_callbacks;
_currentFilter = "all";
_currentSearchTerm = "";
constructor(options) {
this._callbacks = options.callbacks;
}
setupOverviewEventListeners() {
setTimeout(() => {
document.getElementById("overview-search")?.addEventListener("input", (event) => {
this.filterOverviewList(event.target.value);
});
document.getElementById("show-all")?.addEventListener("click", () => {
this.setOverviewFilter("all");
});
document.getElementById("show-documented")?.addEventListener("click", () => {
this.setOverviewFilter("documented");
});
document.getElementById("show-undocumented")?.addEventListener("click", () => {
this.setOverviewFilter("undocumented");
});
}, 100);
}
refreshOverview() {
this._updateFilterButtonStates();
this._updateCoverageStats();
this._updateOverviewList();
}
filterOverviewList(searchTerm) {
this._currentSearchTerm = searchTerm;
this._updateOverviewList();
}
setOverviewFilter(filter) {
this._currentFilter = filter;
const sidebar = this._callbacks.getSidebar();
if (!sidebar) return;
sidebar.querySelectorAll(".btn-small").forEach((btn) => {
btn.classList.remove("active");
});
const activeButton = sidebar.querySelector(`#show-${filter}`);
if (activeButton) activeButton.classList.add("active");
this._updateOverviewList();
}
destroy() {}
_updateFilterButtonStates() {
const sidebar = this._callbacks.getSidebar();
if (!sidebar) return;
sidebar.querySelectorAll(".btn-small").forEach((btn) => {
btn.classList.remove("active");
});
const activeButton = sidebar.querySelector(`#show-${this._currentFilter}`);
if (activeButton) activeButton.classList.add("active");
}
_updateCoverageStats() {
const sidebar = this._callbacks.getSidebar();
if (!sidebar) return;
const elements = this._getAllElementsWithDocumentation();
const documentedCount = elements.filter((el) => el.hasDocumentation).length;
const totalCount = elements.length;
const percentage = totalCount > 0 ? Math.round(documentedCount / totalCount * 100) : 0;
const documentedCountEl = sidebar.querySelector("#documented-count");
if (documentedCountEl) documentedCountEl.textContent = documentedCount.toString();
const totalCountEl = sidebar.querySelector("#total-count");
if (totalCountEl) totalCountEl.textContent = totalCount.toString();
const coveragePercentageEl = sidebar.querySelector("#coverage-percentage");
if (coveragePercentageEl) coveragePercentageEl.textContent = `${percentage}%`;
const progressBar = sidebar.querySelector("#coverage-progress");
if (progressBar) progressBar.style.width = `${percentage}%`;
}
_getAllElementsWithDocumentation() {
const elements = [];
const seenIds = /* @__PURE__ */ new Set();
const allElements = this._callbacks.getAllElements();
allElements.forEach((element) => {
if (element.businessObject?.id) {
const bo = element.businessObject;
const elementId = bo.id;
if (seenIds.has(elementId)) return;
seenIds.add(elementId);
const documentation = this._callbacks.getElementDocumentation(element);
elements.push({
id: elementId,
name: bo.name || "Unnamed",
type: this._callbacks.getElementTypeName(element),
hasDocumentation: !!documentation?.trim(),
documentation: documentation || "",
element
});
}
});
return elements.sort((a, b) => a.id.localeCompare(b.id));
}
_updateOverviewList() {
const sidebar = this._callbacks.getSidebar();
if (!sidebar) return;
const overviewList = sidebar.querySelector("#overview-list");
if (!overviewList) return;
const elements = this._getAllElementsWithDocumentation();
let filteredElements = elements;
if (this._currentFilter === "documented") filteredElements = elements.filter((el) => el.hasDocumentation);
else if (this._currentFilter === "undocumented") filteredElements = elements.filter((el) => !el.hasDocumentation);
if (this._currentSearchTerm) {
const searchTerm = this._currentSearchTerm.toLowerCase();
filteredElements = filteredElements.filter((el) => el.id.toLowerCase().includes(searchTerm) || el.name.toLowerCase().includes(searchTerm) || el.documentation.toLowerCase().includes(searchTerm));
}
if (filteredElements.length === 0) {
overviewList.innerHTML = "<div class=\"overview-loading\">No elements found</div>";
return;
}
overviewList.innerHTML = filteredElements.map((element) => {
const statusClass = element.hasDocumentation ? "documented" : "undocumented";
const statusText = element.hasDocumentation ? "documented" : "undocumented";
return `
<div class="element-item ${statusClass}" data-element-id="${element.id}">
<div class="element-header">
<span class="element-id">${element.id}</span>
<span class="element-status ${statusClass}">${statusText}</span>
</div>
<div class="element-info">
<span>${element.name}</span>
<span>•</span>
<span>${element.type}</span>
</div>
</div>
`;
}).join("");
overviewList.querySelectorAll(".element-item").forEach((card) => {
card.addEventListener("click", () => {
const elementId = card.getAttribute("data-element-id");
if (elementId) {
this._callbacks.selectElementById(elementId);
this._callbacks.switchToElementTab();
}
});
});
}
};
//#endregion
//#region src/extension/managers/SidebarManager.ts
var SidebarManager = class {
_canvas;
_htmlGenerator;
_onSidebarReady;
_sidebar = null;
_resizeObserver = null;
_cleanupRaf = null;
_isResizing = false;
_resizeStartX = 0;
_resizeStartWidth = 0;
_isVerticalResizing = false;
_resizeStartY = 0;
_resizeStartHeight = 0;
_customWidth = null;
_wasVisible = false;
_isMinimized = false;
_minimizedIcon = null;
constructor(options) {
this._canvas = options.canvas;
this._htmlGenerator = options.htmlGenerator;
this._onSidebarReady = options.onSidebarReady;
this._isMinimized = this._getMinimizedPreference();
}
initializeSidebar() {
const existingSidebar = document.getElementById("documentation-sidebar");
if (existingSidebar) existingSidebar.remove();
const existingHandle = document.getElementById("horizontal-resize-handle");
if (existingHandle) existingHandle.remove();
const existingHelpPopover = document.getElementById("help-popover");
if (existingHelpPopover) existingHelpPopover.remove();
const sidebar = document.createElement("div");
sidebar.id = "documentation-sidebar";
sidebar.className = "documentation-sidebar";
sidebar.style.display = "none";
sidebar.innerHTML = this._htmlGenerator.generateSidebarHTML();
const canvasContainer = this._getCanvasContainer();
const currentPosition = window.getComputedStyle(canvasContainer).position;
if (currentPosition === "static") canvasContainer.style.position = "relative";
canvasContainer.appendChild(sidebar);
this._sidebar = sidebar;
const helpPopoverDiv = document.createElement("div");
helpPopoverDiv.innerHTML = this._htmlGenerator.generateHelpPopoverHTML();
const helpPopover = helpPopoverDiv.firstElementChild;
canvasContainer.appendChild(helpPopover);
const horizontalResizeHandle = document.createElement("div");
horizontalResizeHandle.id = "horizontal-resize-handle";
horizontalResizeHandle.className = "horizontal-resize-handle";
canvasContainer.appendChild(horizontalResizeHandle);
if (this._onSidebarReady) setTimeout(() => {
this._onSidebarReady?.(sidebar);
}, 10);
setTimeout(() => {
this.updateSidebarPosition();
this.setupResizeObserver();
this.setupResizeHandles();
}, 100);
}
showSidebar() {
if (this._sidebar && !this._sidebar.parentElement) {
this.initializeSidebar();
setTimeout(() => {
this.showSidebar();
}, 10);
return;
}
if (this._isMinimized) {
this._showMinimizedIcon();
return;
}
this._hideMinimizedIcon();
this.updateSidebarPosition();
if (this._sidebar) {
this._sidebar.style.display = "flex";
this._sidebar.classList.add("visible");
}
const horizontalHandle = document.getElementById("horizontal-resize-handle");
if (horizontalHandle) horizontalHandle.style.display = "block";
this._wasVisible = true;
}
hideSidebar() {
if (this._sidebar) {
this._wasVisible = this._sidebar.classList.contains("visible");
this._sidebar.classList.remove("visible");
this._sidebar.style.display = "none";
}
this._hideMinimizedIcon();
const horizontalHandle = document.getElementById("horizontal-resize-handle");
if (horizontalHandle) horizontalHandle.style.display = "none";
}
updateSidebarPosition() {
const canvasContainer = this._getCanvasContainer();
const containerRect = canvasContainer.getBoundingClientRect();
const propertiesPanel = document.querySelector(".bio-properties-panel-container") || document.querySelector(".djs-properties-panel") || document.querySelector("[data-tab=\"properties\"]") || document.querySelector(".properties-panel");
if (propertiesPanel) {
const panelRect = propertiesPanel.getBoundingClientRect();
const topOffset = Math.max(panelRect.top - containerRect.top, 0);
const bottomOffset = Math.max(containerRect.bottom - panelRect.bottom, 0);
const availableHeight = containerRect.height - topOffset - bottomOffset;
if (this._sidebar) {
this._sidebar.style.top = `${topOffset}px`;
this._sidebar.style.height = `${Math.max(availableHeight, 300)}px`;
if (!this._isResizing) {
const width = this._customWidth ? `${this._customWidth}px` : "350px";
this._sidebar.style.setProperty("width", width, "important");
const horizontalHandle = document.getElementById("horizontal-resize-handle");
if (horizontalHandle) {
const sidebarWidth = this._customWidth || 350;
horizontalHandle.style.right = `${sidebarWidth}px`;
horizontalHandle.style.top = `${topOffset}px`;
horizontalHandle.style.height = `${Math.max(availableHeight, 900)}px`;
}
}
}
} else if (this._sidebar) {
this._sidebar.style.right = "0px";
this._sidebar.style.top = "0px";
this._sidebar.style.height = "100%";
if (!this._isResizing) {
const width = this._customWidth ? `${this._customWidth}px` : "350px";
this._sidebar.style.setProperty("width", width, "important");
const horizontalHandle = document.getElementById("horizontal-resize-handle");
if (horizontalHandle) {
const sidebarWidth = this._customWidth || 350;
horizontalHandle.style.right = `${sidebarWidth}px`;
horizontalHandle.style.top = "0px";
horizontalHandle.style.height = "100%";
}
}
}
}
setupResizeObserver() {
if (this._resizeObserver) this._resizeObserver.disconnect();
const propertiesPanel = document.querySelector(".bio-properties-panel-container") || document.querySelector(".djs-properties-panel") || document.querySelector("[data-tab=\"properties\"]") || document.querySelector(".properties-panel");
if (propertiesPanel && window.ResizeObserver) {
this._resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
this.updateSidebarPosition();
});
});
this._resizeObserver.observe(propertiesPanel);
const parentContainer = propertiesPanel.parentElement;
if (parentContainer) this._resizeObserver.observe(parentContainer);