646 lines
18 KiB
HTML
646 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PlantUML → Isometric 3D Renderer</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
|
||
--bg: #ffffff;
|
||
--bg2: #f5f4f0;
|
||
--bg3: #eeecea;
|
||
--txt: #1a1a1a;
|
||
--txts: #555550;
|
||
--txt3: #888780;
|
||
--border: rgba(0,0,0,0.12);
|
||
--border2: rgba(0,0,0,0.22);
|
||
--info-bg: #e6f1fb;
|
||
--info-txt: #0c447c;
|
||
--info-bdr: #378add;
|
||
--sk: rgba(0,0,0,0.13);
|
||
--top-gray: #D3D1C7; --left-gray: #888780; --right-gray: #5F5E5A;
|
||
--top-blue: #B5D4F4; --left-blue: #378ADD; --right-blue: #185FA5;
|
||
--top-teal: #9FE1CB; --left-teal: #1D9E75; --right-teal: #0F6E56;
|
||
--top-amber: #FAC775; --left-amber: #BA7517; --right-amber: #854F0B;
|
||
--top-purple:#CECBF6; --left-purple:#7F77DD; --right-purple:#534AB7;
|
||
--top-coral: #F5C4B3; --left-coral: #D85A30; --right-coral: #993C1D;
|
||
--radius: 8px;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--bg: #1e1e1e;
|
||
--bg2: #2a2a2a;
|
||
--bg3: #333333;
|
||
--txt: #e8e6dc;
|
||
--txts: #9c9a92;
|
||
--txt3: #666660;
|
||
--border: rgba(255,255,255,0.1);
|
||
--border2: rgba(255,255,255,0.2);
|
||
--info-bg: #042c53;
|
||
--info-txt: #85b7eb;
|
||
--info-bdr: #185fa5;
|
||
--sk: rgba(255,255,255,0.08);
|
||
--top-gray: #2C2C2A; --left-gray: #444441; --right-gray: #1a1a18;
|
||
--top-blue: #0C447C; --left-blue: #185FA5; --right-blue: #042C53;
|
||
--top-teal: #085041; --left-teal: #0F6E56; --right-teal: #04342C;
|
||
--top-amber: #633806; --left-amber: #854F0B; --right-amber: #412402;
|
||
--top-purple:#3C3489; --left-purple:#534AB7; --right-purple:#26215C;
|
||
--top-coral: #712B13; --left-coral: #993C1D; --right-coral: #4A1B0C;
|
||
}
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-sans);
|
||
background: var(--bg);
|
||
color: var(--txt);
|
||
padding: 24px;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 13px;
|
||
color: var(--txts);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 320px 1fr;
|
||
gap: 20px;
|
||
align-items: start;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.panel {
|
||
background: var(--bg2);
|
||
border: 0.5px solid var(--border2);
|
||
border-radius: var(--radius);
|
||
padding: 16px;
|
||
}
|
||
|
||
.panel-label {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: var(--txt3);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
height: 320px;
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
padding: 10px;
|
||
resize: vertical;
|
||
background: var(--bg);
|
||
color: var(--txt);
|
||
border: 0.5px solid var(--border2);
|
||
border-radius: var(--radius);
|
||
outline: none;
|
||
}
|
||
|
||
textarea:focus { border-color: var(--info-bdr); }
|
||
|
||
.btn-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
button {
|
||
font-family: var(--font-sans);
|
||
font-size: 13px;
|
||
padding: 7px 16px;
|
||
border-radius: var(--radius);
|
||
border: 0.5px solid var(--border2);
|
||
background: transparent;
|
||
color: var(--txt);
|
||
cursor: pointer;
|
||
}
|
||
|
||
button:hover { background: var(--bg3); }
|
||
button:active { transform: scale(0.98); }
|
||
|
||
button.primary {
|
||
background: var(--info-bg);
|
||
color: var(--info-txt);
|
||
border-color: var(--info-bdr);
|
||
font-weight: 500;
|
||
}
|
||
|
||
#err {
|
||
font-size: 12px;
|
||
color: #c0392b;
|
||
margin-top: 6px;
|
||
min-height: 16px;
|
||
}
|
||
|
||
.legend {
|
||
margin-top: 14px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-size: 11px;
|
||
color: var(--txts);
|
||
}
|
||
|
||
.legend-swatch {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 2px;
|
||
border: 0.5px solid var(--border);
|
||
}
|
||
|
||
.hint-box {
|
||
margin-top: 12px;
|
||
padding: 8px 10px;
|
||
background: var(--info-bg);
|
||
border: 0.5px solid var(--info-bdr);
|
||
border-radius: var(--radius);
|
||
font-size: 11px;
|
||
color: var(--info-txt);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.canvas-panel {
|
||
background: var(--bg2);
|
||
border: 0.5px solid var(--border2);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.canvas-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 14px;
|
||
border-bottom: 0.5px solid var(--border);
|
||
}
|
||
|
||
#nav {
|
||
font-size: 12px;
|
||
color: var(--txts);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
#nav .crumb {
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
color: var(--info-txt);
|
||
}
|
||
|
||
#nav .crumb:hover { opacity: 0.75; }
|
||
#nav .sep { opacity: 0.35; }
|
||
|
||
#canvas-wrap {
|
||
padding: 8px;
|
||
background: var(--bg);
|
||
}
|
||
|
||
svg { display: block; }
|
||
|
||
.iso-lbl {
|
||
font-family: var(--font-sans);
|
||
font-size: 11px;
|
||
fill: var(--txt, #1a1a1a);
|
||
}
|
||
|
||
.iso-sub {
|
||
font-family: var(--font-sans);
|
||
font-size: 9px;
|
||
fill: var(--txts, #555);
|
||
}
|
||
|
||
.zoomable { cursor: zoom-in; }
|
||
.zoomable:hover polygon:first-child { opacity: 0.82; }
|
||
.conn { stroke-width: 1; fill: none; stroke-dasharray: 4 3; }
|
||
.conn-s { stroke-width: 1.5; fill: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<h1>PlantUML → Isometric 3D Renderer</h1>
|
||
<p class="subtitle">Paste a PlantUML component diagram and click Render to generate an interactive isometric view.</p>
|
||
|
||
<div class="layout">
|
||
<div class="panel">
|
||
<div class="panel-label">PlantUML source</div>
|
||
<textarea id="puml" spellcheck="false">@startuml
|
||
component "ECU A" as A
|
||
component "ECU C" as C
|
||
|
||
component "ECU B" as B {
|
||
component "Connector A" as ConA
|
||
component "Adapter A" as AdpA
|
||
component "Connector C" as ConC
|
||
component "Adapter C" as AdpC
|
||
component "Logic" as L {
|
||
component "a" as La
|
||
component "b" as Lb
|
||
component "c" as Lc
|
||
La --> Lb
|
||
Lb --> Lc
|
||
}
|
||
ConA --> AdpA
|
||
ConC --> AdpC
|
||
AdpA --> L
|
||
AdpC --> L
|
||
}
|
||
|
||
A --> ConA
|
||
C --> ConC
|
||
@enduml</textarea>
|
||
|
||
<div class="btn-row">
|
||
<button class="primary" onclick="render()">Render →</button>
|
||
<button onclick="resetSource()">Reset example</button>
|
||
</div>
|
||
<div id="err"></div>
|
||
|
||
<div class="panel-label" style="margin-top:16px">Bug counts (JSON — alias: count)</div>
|
||
<textarea id="bugJson" spellcheck="false" rows="3"
|
||
placeholder='{"ConA":3,"AdpA":1,"L":7}'></textarea>
|
||
<div class="btn-row">
|
||
<button class="primary" onclick="applyBugs()">Apply bugs</button>
|
||
<button onclick="clearBugs()">Clear</button>
|
||
</div>
|
||
|
||
<div class="legend">
|
||
<div class="legend-item">
|
||
<div class="legend-swatch" style="background:var(--top-gray)"></div> ECU / top-level
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-swatch" style="background:var(--top-teal)"></div> Connector
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-swatch" style="background:var(--top-amber)"></div> Adapter
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-swatch" style="background:var(--top-purple)"></div> Logic
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-swatch" style="background:var(--top-coral)"></div> Sequence step
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hint-box">
|
||
Color is inferred from the component label.<br>
|
||
Components with children are clickable — drill in to explore nested structure.
|
||
<br><br>
|
||
To connect your self-hosted PlantUML server, POST the source to
|
||
<code>/json</code> and feed the response AST into the renderer instead of the client-side parser.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="canvas-panel">
|
||
<div class="canvas-header">
|
||
<div id="nav"><span style="color:var(--txt3)">Overview</span></div>
|
||
</div>
|
||
<div id="canvas-wrap">
|
||
<svg id="iso" width="100%" viewBox="0 0 680 400"></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const EXAMPLE = document.getElementById('puml').value;
|
||
|
||
const PALETTE = {
|
||
ecu: { t:'--top-gray', l:'--left-gray', r:'--right-gray' },
|
||
conn: { t:'--top-teal', l:'--left-teal', r:'--right-teal' },
|
||
adp: { t:'--top-amber', l:'--left-amber', r:'--right-amber' },
|
||
logic: { t:'--top-purple', l:'--left-purple', r:'--right-purple' },
|
||
seq: { t:'--top-coral', l:'--left-coral', r:'--right-coral' },
|
||
};
|
||
|
||
function cv(v) {
|
||
return getComputedStyle(document.documentElement).getPropertyValue(v).trim() || v;
|
||
}
|
||
|
||
function parsePuml(src) {
|
||
const lines = src.split('\n')
|
||
.map(s => s.trim())
|
||
.filter(s => s && !s.startsWith("'") && s !== '@startuml' && s !== '@enduml');
|
||
|
||
const root = { id: '__root__', label: 'root', children: [], conns: [] };
|
||
const stack = [root];
|
||
const byId = {};
|
||
|
||
const aliasRe = /^component\s+"([^"]+)"\s+as\s+(\w+)\s*(\{)?\s*$/;
|
||
const aliasNoLblRe = /^component\s+(\w+)\s*(\{)?\s*$/;
|
||
const connRe = /^(\w+)\s*-->\s*(\w+)(?:\s*:\s*(.*))?$/;
|
||
const closeRe = /^\}$/;
|
||
|
||
function cur() { return stack[stack.length - 1]; }
|
||
|
||
for (const line of lines) {
|
||
let m;
|
||
if ((m = aliasRe.exec(line))) {
|
||
const node = { id: m[2], label: m[1], children: [], conns: [], parent: cur() };
|
||
byId[m[2]] = node;
|
||
cur().children.push(node);
|
||
if (m[3]) stack.push(node);
|
||
} else if ((m = aliasNoLblRe.exec(line))) {
|
||
const node = { id: m[1], label: m[1], children: [], conns: [], parent: cur() };
|
||
byId[m[1]] = node;
|
||
cur().children.push(node);
|
||
if (m[2]) stack.push(node);
|
||
} else if ((m = connRe.exec(line))) {
|
||
cur().conns.push({ from: m[1], to: m[2], label: m[3] || '' });
|
||
} else if (closeRe.test(line)) {
|
||
if (stack.length > 1) stack.pop();
|
||
}
|
||
}
|
||
return { root, byId };
|
||
}
|
||
|
||
function guessColor(node) {
|
||
const l = node.label.toLowerCase();
|
||
if (l.startsWith('connector')) return PALETTE.conn;
|
||
if (l.startsWith('adapter')) return PALETTE.adp;
|
||
if (l === 'logic' || l === 'logic component') return PALETTE.logic;
|
||
if (node.label.length <= 2) return PALETTE.seq;
|
||
return PALETTE.ecu;
|
||
}
|
||
|
||
function ns(tag) {
|
||
return document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||
}
|
||
|
||
function makePoly(pts, cssVar) {
|
||
const el = ns('polygon');
|
||
el.setAttribute('points', pts.map(p => p.join(',')).join(' '));
|
||
el.style.fill = `var(${cssVar})`;
|
||
el.style.stroke = 'var(--sk)';
|
||
el.setAttribute('stroke-width', '0.5');
|
||
return el;
|
||
}
|
||
|
||
function makeBlock(svg, x, y, W, H, pal, label, sublabel, clickable, bugCount = 0) {
|
||
const g = ns('g');
|
||
if (clickable) g.classList.add('zoomable');
|
||
|
||
const hw = W, hh = W * 0.47;
|
||
const top = makePoly([[x,y],[x+hw,y+hh],[x,y+hh*2],[x-hw,y+hh]], pal.t);
|
||
const left = makePoly([[x-hw,y+hh],[x,y+hh*2],[x,y+hh*2+H],[x-hw,y+hh+H]], pal.l);
|
||
const right = makePoly([[x,y+hh*2],[x+hw,y+hh],[x+hw,y+hh+H],[x,y+hh*2+H]], pal.r);
|
||
g.appendChild(top); g.appendChild(left); g.appendChild(right);
|
||
|
||
const tx = ns('text');
|
||
tx.setAttribute('x', x); tx.setAttribute('y', y + hh + 2);
|
||
tx.setAttribute('text-anchor', 'middle');
|
||
tx.setAttribute('dominant-baseline', 'central');
|
||
tx.setAttribute('class', 'iso-lbl');
|
||
tx.setAttribute('font-weight', '500');
|
||
tx.textContent = label;
|
||
g.appendChild(tx);
|
||
|
||
if (sublabel) {
|
||
const ts = ns('text');
|
||
ts.setAttribute('x', x); ts.setAttribute('y', y + hh + 15);
|
||
ts.setAttribute('text-anchor', 'middle');
|
||
ts.setAttribute('class', 'iso-sub');
|
||
ts.textContent = sublabel;
|
||
g.appendChild(ts);
|
||
}
|
||
|
||
if (bugCount > 0) {
|
||
const bx = x + hw - 8, by = y + 8;
|
||
const circle = ns('circle');
|
||
circle.setAttribute('cx', bx); circle.setAttribute('cy', by);
|
||
circle.setAttribute('r', '9');
|
||
circle.setAttribute('fill', '#d32f2f');
|
||
circle.setAttribute('stroke', '#fff'); circle.setAttribute('stroke-width', '1.5');
|
||
g.appendChild(circle);
|
||
const bt = ns('text');
|
||
bt.setAttribute('x', bx); bt.setAttribute('y', by);
|
||
bt.setAttribute('text-anchor', 'middle');
|
||
bt.setAttribute('dominant-baseline', 'central');
|
||
bt.setAttribute('fill', '#fff');
|
||
bt.setAttribute('font-size', '8');
|
||
bt.setAttribute('font-weight', 'bold');
|
||
bt.textContent = bugCount > 99 ? '99+' : String(bugCount);
|
||
g.appendChild(bt);
|
||
}
|
||
|
||
svg.appendChild(g);
|
||
return {
|
||
g,
|
||
cx: x,
|
||
cy: y + hh,
|
||
top_y: y,
|
||
bot_y: y + hh * 2 + H,
|
||
left_x: x - hw,
|
||
right_x: x + hw,
|
||
};
|
||
}
|
||
|
||
function makeArrow(svg) {
|
||
const defs = ns('defs');
|
||
const m = ns('marker');
|
||
m.setAttribute('id', 'arr');
|
||
m.setAttribute('viewBox', '0 0 10 10');
|
||
m.setAttribute('refX', '8'); m.setAttribute('refY', '5');
|
||
m.setAttribute('markerWidth', '6'); m.setAttribute('markerHeight', '6');
|
||
m.setAttribute('orient', 'auto-start-reverse');
|
||
const p = ns('path');
|
||
p.setAttribute('d', 'M2 1L8 5L2 9');
|
||
p.setAttribute('fill', 'none');
|
||
p.style.stroke = 'var(--left-gray)';
|
||
p.setAttribute('stroke-width', '1.5');
|
||
p.setAttribute('stroke-linecap', 'round');
|
||
m.appendChild(p); defs.appendChild(m); svg.insertBefore(defs, svg.firstChild);
|
||
}
|
||
|
||
function makeLine(svg, x1, y1, x2, y2) {
|
||
const p = ns('path');
|
||
const mx = (x1 + x2) / 2, my = Math.min(y1, y2) - 18;
|
||
p.setAttribute('d', `M${x1},${y1} Q${mx},${my} ${x2},${y2}`);
|
||
p.classList.add('conn-s');
|
||
p.style.stroke = 'var(--left-gray)';
|
||
p.setAttribute('marker-end', 'url(#arr)');
|
||
svg.appendChild(p);
|
||
}
|
||
|
||
let state = { parsed: null, viewStack: [], bugData: {}, currentNode: null };
|
||
|
||
function render() {
|
||
const src = document.getElementById('puml').value;
|
||
document.getElementById('err').textContent = '';
|
||
let parsed;
|
||
try { parsed = parsePuml(src); }
|
||
catch (e) { document.getElementById('err').textContent = 'Parse error: ' + e.message; return; }
|
||
state.parsed = parsed;
|
||
state.viewStack = [];
|
||
drawLevel(parsed.root);
|
||
}
|
||
|
||
function setBugData(data) {
|
||
state.bugData = data || {};
|
||
if (state.currentNode) drawLevel(state.currentNode);
|
||
}
|
||
window.setBugData = setBugData;
|
||
|
||
function applyBugs() {
|
||
const raw = document.getElementById('bugJson').value.trim();
|
||
document.getElementById('err').textContent = '';
|
||
if (!raw) { setBugData({}); return; }
|
||
try {
|
||
setBugData(JSON.parse(raw));
|
||
} catch (e) {
|
||
document.getElementById('err').textContent = 'Bug JSON error: ' + e.message;
|
||
}
|
||
}
|
||
|
||
function clearBugs() {
|
||
document.getElementById('bugJson').value = '';
|
||
setBugData({});
|
||
}
|
||
|
||
function drawLevel(node) {
|
||
state.currentNode = node;
|
||
const svg = document.getElementById('iso');
|
||
svg.innerHTML = '';
|
||
makeArrow(svg);
|
||
|
||
const children = node.children;
|
||
const conns = node.conns;
|
||
|
||
if (children.length === 0) {
|
||
svg.setAttribute('viewBox', '0 0 680 100');
|
||
const t = ns('text');
|
||
t.setAttribute('x', '340'); t.setAttribute('y', '50');
|
||
t.setAttribute('text-anchor', 'middle'); t.setAttribute('class', 'iso-sub');
|
||
t.textContent = 'No nested components.';
|
||
svg.appendChild(t);
|
||
updateNav(); return;
|
||
}
|
||
|
||
const W = 80, H = 28;
|
||
const cols = Math.min(children.length, 4);
|
||
const rows = Math.ceil(children.length / cols);
|
||
const vbH = rows * 160 + 80;
|
||
svg.setAttribute('viewBox', `0 0 680 ${vbH}`);
|
||
|
||
const startX = 340 - (cols - 1) * 90;
|
||
const positions = {};
|
||
|
||
children.forEach((child, i) => {
|
||
const col = i % cols, row = Math.floor(i / cols);
|
||
const x = startX + col * 180;
|
||
const y = 50 + row * 155;
|
||
const pal = guessColor(child);
|
||
const canZoom = child.children.length > 0;
|
||
const sub = canZoom ? 'click to expand →' : null;
|
||
const bugs = state.bugData[child.id] || 0;
|
||
const blk = makeBlock(svg, x, y, W, H, pal, child.label, sub, canZoom, bugs);
|
||
positions[child.id] = blk;
|
||
|
||
if (canZoom) {
|
||
blk.g.addEventListener('click', () => {
|
||
state.viewStack.push(node);
|
||
drawLevel(child);
|
||
});
|
||
}
|
||
});
|
||
|
||
conns.forEach(c => {
|
||
const fp = positions[c.from], tp = positions[c.to];
|
||
if (!fp || !tp) return;
|
||
makeLine(svg, fp.cx, fp.bot_y, tp.cx, tp.top_y);
|
||
});
|
||
|
||
updateNav();
|
||
}
|
||
|
||
function updateNav() {
|
||
const nav = document.getElementById('nav');
|
||
nav.innerHTML = '';
|
||
const stack = state.viewStack;
|
||
|
||
const sep = () => {
|
||
const s = document.createElement('span');
|
||
s.className = 'sep';
|
||
s.textContent = ' › ';
|
||
return s;
|
||
};
|
||
|
||
if (stack.length === 0) {
|
||
const s = document.createElement('span');
|
||
s.style.color = 'var(--txt3)';
|
||
s.textContent = 'Overview';
|
||
nav.appendChild(s);
|
||
return;
|
||
}
|
||
|
||
const overviewEl = document.createElement('span');
|
||
overviewEl.className = 'crumb';
|
||
overviewEl.textContent = 'Overview';
|
||
overviewEl.onclick = () => { state.viewStack = []; drawLevel(state.parsed.root); };
|
||
nav.appendChild(overviewEl);
|
||
|
||
stack.forEach((node, i) => {
|
||
nav.appendChild(sep());
|
||
if (i < stack.length - 1) {
|
||
const el = document.createElement('span');
|
||
el.className = 'crumb';
|
||
el.textContent = node.label || node.id;
|
||
el.onclick = () => { state.viewStack = stack.slice(0, i); drawLevel(node); };
|
||
nav.appendChild(el);
|
||
} else {
|
||
const el = document.createElement('span');
|
||
el.style.color = 'var(--txt3)';
|
||
el.textContent = node.label || node.id;
|
||
nav.appendChild(el);
|
||
}
|
||
});
|
||
|
||
nav.appendChild(sep());
|
||
const cur = stack[stack.length - 1];
|
||
const curChildren = cur.children;
|
||
if (curChildren.length > 0) {
|
||
const el = document.createElement('span');
|
||
el.style.color = 'var(--txt3)';
|
||
el.textContent = curChildren[0]?.parent?.label || cur.label;
|
||
nav.appendChild(el);
|
||
}
|
||
}
|
||
|
||
function resetSource() {
|
||
document.getElementById('puml').value = EXAMPLE;
|
||
render();
|
||
}
|
||
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|