diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/.gitignore
@@ -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?
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d554a0c
--- /dev/null
+++ b/index.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ PlantUML → Isometric 3D Renderer
+
+
+
+PlantUML → Isometric 3D Renderer
+Paste a PlantUML component diagram and click Render to generate an interactive isometric view.
+
+
+
+
PlantUML source
+
+
+
+ Render →
+ Reset example
+
+
+
+
Bug counts (JSON — alias: count)
+
+
+ Apply bugs
+ Clear
+
+
+
+
+
+ Color is inferred from the component label.
+ Components with children are clickable — drill in to explore nested structure.
+
+ To connect your self-hosted PlantUML server, POST the source to
+ /json and feed the response AST into the renderer instead of the client-side parser.
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5af1cce
--- /dev/null
+++ b/package-lock.json
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..16a5bbd
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/plantuml-3d-renderer.html b/plantuml-3d-renderer.html
deleted file mode 100644
index 9cae5ce..0000000
--- a/plantuml-3d-renderer.html
+++ /dev/null
@@ -1,645 +0,0 @@
-
-
-
-
-
-PlantUML → Isometric 3D Renderer
-
-
-
-
-PlantUML → Isometric 3D Renderer
-Paste a PlantUML component diagram and click Render to generate an interactive isometric view.
-
-
-
-
PlantUML source
-
-
-
- Render →
- Reset example
-
-
-
-
Bug counts (JSON — alias: count)
-
-
- Apply bugs
- Clear
-
-
-
-
-
- Color is inferred from the component label.
- Components with children are clickable — drill in to explore nested structure.
-
- To connect your self-hosted PlantUML server, POST the source to
- /json and feed the response AST into the renderer instead of the client-side parser.
-
-
-
-
-
-
-
-
-
diff --git a/src/colors.ts b/src/colors.ts
new file mode 100644
index 0000000..a7d4a76
--- /dev/null
+++ b/src/colors.ts
@@ -0,0 +1,18 @@
+import type { Node, PaletteEntry } from './types.ts';
+
+export const PALETTE: Record = {
+ 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']!;
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..a1277b9
--- /dev/null
+++ b/src/main.ts
@@ -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> = {};
+
+ 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): 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);
+ } 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)['setBugData'] = setBugData;
+
+render();
diff --git a/src/parser.ts b/src/parser.ts
new file mode 100644
index 0000000..a897dfd
--- /dev/null
+++ b/src/parser.ts
@@ -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 = {};
+
+ 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 };
+}
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..d7f1e62
--- /dev/null
+++ b/src/style.css
@@ -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; }
diff --git a/src/svg.ts b/src/svg.ts
new file mode 100644
index 0000000..70bf099
--- /dev/null
+++ b/src/svg.ts
@@ -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);
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..b62dc66
--- /dev/null
+++ b/src/types.ts
@@ -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;
+}
+
+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;
+ currentNode: Node | null;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..87cdec0
--- /dev/null
+++ b/tsconfig.json
@@ -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"]
+}