refactor: break down single html file
Scaffold project structure with vite. src/ types.ts ← Node, Connection, ParseResult, PaletteEntry, BlockInfo, AppState parser.ts ← parsePuml() colors.ts ← PALETTE, guessColor() svg.ts ← makeBlock(), makeArrow(), makeLine() (pure SVG helpers) main.ts ← state, drawLevel(), updateNav(), render(), setBugData(), event wiring style.css ← all styles index.html ← shell + default PlantUML example in textarea package.json ← vite + typescript devDeps tsconfig.json ← strict mode, ESNext, bundler resolution
This commit is contained in:
parent
1ff6a55078
commit
f7dac370b4
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
95
index.html
Normal file
95
index.html
Normal file
@ -0,0 +1,95 @@
|
||||
<!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>
|
||||
</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 id="btn-render" class="primary">Render →</button>
|
||||
<button id="btn-reset">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 id="btn-apply-bugs" class="primary">Apply bugs</button>
|
||||
<button id="btn-clear-bugs">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 type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
895
package-lock.json
generated
Normal file
895
package-lock.json
generated
Normal file
@ -0,0 +1,895 @@
|
||||
{
|
||||
"name": "vite-scaffold",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vite-scaffold",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/runtime": {
|
||||
"version": "0.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
||||
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
|
||||
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.115.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.9"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.9",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.9",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
|
||||
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/runtime": "0.115.0",
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.9",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.0.0-alpha.31",
|
||||
"esbuild": "^0.27.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "vite-scaffold",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
@ -1,645 +0,0 @@
|
||||
<!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>
|
||||
18
src/colors.ts
Normal file
18
src/colors.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { Node, PaletteEntry } from './types.ts';
|
||||
|
||||
export const PALETTE: Record<string, PaletteEntry> = {
|
||||
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' },
|
||||
};
|
||||
|
||||
export function guessColor(node: Node): PaletteEntry {
|
||||
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']!;
|
||||
}
|
||||
180
src/main.ts
Normal file
180
src/main.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import './style.css';
|
||||
import type { AppState, Node } from './types.ts';
|
||||
import { parsePuml } from './parser.ts';
|
||||
import { guessColor } from './colors.ts';
|
||||
import { makeArrow, makeBlock, makeLine } from './svg.ts';
|
||||
|
||||
const EXAMPLE = (document.getElementById('puml') as HTMLTextAreaElement).value;
|
||||
|
||||
const state: AppState = {
|
||||
parsed: null,
|
||||
viewStack: [],
|
||||
bugData: {},
|
||||
currentNode: null,
|
||||
};
|
||||
|
||||
// ── Rendering ────────────────────────────────────────────────────────────────
|
||||
|
||||
function drawLevel(node: Node): void {
|
||||
state.currentNode = node;
|
||||
const svg = document.getElementById('iso') as unknown as SVGSVGElement;
|
||||
svg.innerHTML = '';
|
||||
makeArrow(svg);
|
||||
|
||||
const { children, conns } = node;
|
||||
|
||||
if (children.length === 0) {
|
||||
svg.setAttribute('viewBox', '0 0 680 100');
|
||||
const t = document.createElementNS('http://www.w3.org/2000/svg', '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);
|
||||
svg.setAttribute('viewBox', `0 0 680 ${rows * 160 + 80}`);
|
||||
|
||||
const startX = 340 - (cols - 1) * 90;
|
||||
const positions: Record<string, ReturnType<typeof makeBlock>> = {};
|
||||
|
||||
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(): void {
|
||||
const nav = document.getElementById('nav')!;
|
||||
nav.innerHTML = '';
|
||||
const { viewStack: stack } = state;
|
||||
|
||||
const sep = (): HTMLSpanElement => {
|
||||
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]!;
|
||||
if (cur.children.length > 0) {
|
||||
const el = document.createElement('span');
|
||||
el.style.color = 'var(--txt3)';
|
||||
el.textContent = cur.children[0]?.parent?.label ?? cur.label;
|
||||
nav.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function setBugData(data: Record<string, number>): void {
|
||||
state.bugData = data;
|
||||
if (state.currentNode) drawLevel(state.currentNode);
|
||||
}
|
||||
|
||||
// ── Event handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
function render(): void {
|
||||
const src = (document.getElementById('puml') as HTMLTextAreaElement).value;
|
||||
(document.getElementById('err') as HTMLElement).textContent = '';
|
||||
try {
|
||||
state.parsed = parsePuml(src);
|
||||
state.viewStack = [];
|
||||
drawLevel(state.parsed.root);
|
||||
} catch (e) {
|
||||
(document.getElementById('err') as HTMLElement).textContent =
|
||||
'Parse error: ' + (e as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
function resetSource(): void {
|
||||
(document.getElementById('puml') as HTMLTextAreaElement).value = EXAMPLE;
|
||||
render();
|
||||
}
|
||||
|
||||
function applyBugs(): void {
|
||||
const raw = (document.getElementById('bugJson') as HTMLTextAreaElement).value.trim();
|
||||
(document.getElementById('err') as HTMLElement).textContent = '';
|
||||
if (!raw) { setBugData({}); return; }
|
||||
try {
|
||||
setBugData(JSON.parse(raw) as Record<string, number>);
|
||||
} catch (e) {
|
||||
(document.getElementById('err') as HTMLElement).textContent =
|
||||
'Bug JSON error: ' + (e as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
function clearBugs(): void {
|
||||
(document.getElementById('bugJson') as HTMLTextAreaElement).value = '';
|
||||
setBugData({});
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-render')!.addEventListener('click', render);
|
||||
document.getElementById('btn-reset')!.addEventListener('click', resetSource);
|
||||
document.getElementById('btn-apply-bugs')!.addEventListener('click', applyBugs);
|
||||
document.getElementById('btn-clear-bugs')!.addEventListener('click', clearBugs);
|
||||
|
||||
// Expose setBugData for JIRA integration
|
||||
(window as unknown as Record<string, unknown>)['setBugData'] = setBugData;
|
||||
|
||||
render();
|
||||
42
src/parser.ts
Normal file
42
src/parser.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { Node, ParseResult } from './types.ts';
|
||||
|
||||
export function parsePuml(src: string): ParseResult {
|
||||
const lines = src
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s && !s.startsWith("'") && s !== '@startuml' && s !== '@enduml');
|
||||
|
||||
const root: Node = { id: '__root__', label: 'root', children: [], conns: [], parent: undefined };
|
||||
const stack: Node[] = [root];
|
||||
const byId: Record<string, Node> = {};
|
||||
|
||||
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(): Node {
|
||||
return stack[stack.length - 1]!;
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
let m: RegExpExecArray | null;
|
||||
if ((m = aliasRe.exec(line))) {
|
||||
const node: 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: 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 };
|
||||
}
|
||||
242
src/style.css
Normal file
242
src/style.css
Normal file
@ -0,0 +1,242 @@
|
||||
*, *::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;
|
||||
}
|
||||
|
||||
#bugJson {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
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; }
|
||||
111
src/svg.ts
Normal file
111
src/svg.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import type { BlockInfo, PaletteEntry } from './types.ts';
|
||||
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
function ns(tag: string): SVGElement {
|
||||
return document.createElementNS(SVG_NS, tag) as SVGElement;
|
||||
}
|
||||
|
||||
function makePoly(pts: [number, number][], cssVar: string): SVGPolygonElement {
|
||||
const el = ns('polygon') as SVGPolygonElement;
|
||||
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;
|
||||
}
|
||||
|
||||
export function makeBlock(
|
||||
svg: SVGSVGElement,
|
||||
x: number, y: number,
|
||||
W: number, H: number,
|
||||
pal: PaletteEntry,
|
||||
label: string,
|
||||
sublabel: string | null,
|
||||
clickable: boolean,
|
||||
bugCount = 0,
|
||||
): BlockInfo {
|
||||
const g = ns('g') as SVGGElement;
|
||||
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') as SVGTextElement;
|
||||
tx.setAttribute('x', String(x)); tx.setAttribute('y', String(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') as SVGTextElement;
|
||||
ts.setAttribute('x', String(x)); ts.setAttribute('y', String(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') as SVGCircleElement;
|
||||
circle.setAttribute('cx', String(bx)); circle.setAttribute('cy', String(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') as SVGTextElement;
|
||||
bt.setAttribute('x', String(bx)); bt.setAttribute('y', String(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,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeArrow(svg: SVGSVGElement): void {
|
||||
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') as SVGPathElement;
|
||||
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);
|
||||
}
|
||||
|
||||
export function makeLine(svg: SVGSVGElement, x1: number, y1: number, x2: number, y2: number): void {
|
||||
const p = ns('path') as SVGPathElement;
|
||||
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);
|
||||
}
|
||||
44
src/types.ts
Normal file
44
src/types.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export interface Connection {
|
||||
from: string;
|
||||
to: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
label: string;
|
||||
children: Node[];
|
||||
conns: Connection[];
|
||||
parent: Node | undefined;
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
root: Node;
|
||||
byId: Record<string, Node>;
|
||||
}
|
||||
|
||||
export interface PaletteEntry {
|
||||
/** CSS custom property name for the top face */
|
||||
t: string;
|
||||
/** CSS custom property name for the left face */
|
||||
l: string;
|
||||
/** CSS custom property name for the right face */
|
||||
r: string;
|
||||
}
|
||||
|
||||
export interface BlockInfo {
|
||||
g: SVGGElement;
|
||||
cx: number;
|
||||
cy: number;
|
||||
top_y: number;
|
||||
bot_y: number;
|
||||
left_x: number;
|
||||
right_x: number;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
parsed: ParseResult | null;
|
||||
viewStack: Node[];
|
||||
bugData: Record<string, number>;
|
||||
currentNode: Node | null;
|
||||
}
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user