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
+ + +
+ + +
+
+ +
Bug counts (JSON — alias: count)
+ +
+ + +
+ +
+
+
ECU / top-level +
+
+
Connector +
+
+
Adapter +
+
+
Logic +
+
+
Sequence step +
+
+ +
+ 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
- - -
- - -
-
- -
Bug counts (JSON — alias: count)
- -
- - -
- -
-
-
ECU / top-level -
-
-
Connector -
-
-
Adapter -
-
-
Logic -
-
-
Sequence step -
-
- -
- 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"] +}