diff --git a/README.md b/README.md index ca4ea90..f5a6465 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Shared Quartz engine, reusable GitHub Pages workflow, and the minimum amount of - Shared Quartz source, dependencies, and build tooling - `scripts/build-site.mjs` for staging a site repo against the shared engine +- `scripts/build-graph-spa.mjs` for building the shared fullscreen graph SPA into a consumer site - Reusable Pages workflow at `.github/workflows/quartz-pages.yml` - Generic defaults only @@ -39,6 +40,21 @@ Then from a consumer repo, use its launcher script, or call the shared builder d node ..\\GameCult-Quartz\\scripts\\build-site.mjs build --siteRoot E:\\Projects\\gamecult-site --contentDir GameCult --overlayDir site --outputDir quartz-site/public ``` +## Fullscreen Graph SPA + +Consumer sites can render the shared graph surface with `Component.GameCultGraphSpaShell(...)`. +The component only owns the mount point and asset tags; the consumer overlay decides where it appears. + +Build the static SPA assets into the consumer repo when its graph payload changes: + +```powershell +node ..\\GameCult-Quartz\\scripts\\build-graph-spa.mjs --siteRoot E:\\Projects\\Mimir --outputDir static/epiphany-graph +``` + +The SPA source lives in `quartz/graph-spa`. It currently imports the shared `EpiphanyGraphViewer` +from a sibling `EpiphanyGraph` checkout during local builds, so generated assets should be committed +by consumer sites until that viewer is packaged as a normal dependency. + ## Shared Components ### `AutoIndexFolder` diff --git a/package-lock.json b/package-lock.json index ff22d60..7f58862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@jackyzha0/quartz", + "name": "@gamecult/quartz", "version": "4.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@jackyzha0/quartz", + "name": "@gamecult/quartz", "version": "4.5.2", "license": "MIT", "dependencies": { @@ -14,11 +14,13 @@ "@myriaddreamin/rehype-typst": "^0.6.0", "@napi-rs/simple-git": "0.1.22", "@tweenjs/tween.js": "^25.0.0", + "@vitejs/plugin-react": "^5.2.0", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", "chokidar": "^5.0.0", "cli-spinner": "^0.2.10", "d3": "^7.9.0", + "elkjs": "^0.11.1", "esbuild-sass-plugin": "^3.6.0", "flexsearch": "^0.8.205", "github-slugger": "^2.0.0", @@ -40,6 +42,8 @@ "preact-render-to-string": "^6.6.5", "pretty-bytes": "^7.1.0", "pretty-time": "^1.1.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "reading-time": "^1.5.0", "rehype-autolink-headings": "^7.1.0", "rehype-citation": "^2.3.1", @@ -67,6 +71,7 @@ "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", + "vite": "^7.3.3", "workerpool": "^10.0.1", "ws": "^8.19.0", "yargs": "^18.0.0" @@ -93,6 +98,287 @@ "npm": ">=10.9.2" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bufbuild/protobuf": { "version": "2.10.2", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz", @@ -1134,6 +1420,51 @@ "node": "20 || >=22" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@myriaddreamin/rehype-typst": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@myriaddreamin/rehype-typst/-/rehype-typst-0.6.0.tgz", @@ -1964,6 +2295,376 @@ "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@shikijs/core": { "version": "1.26.2", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.26.2.tgz", @@ -2060,6 +2761,47 @@ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/css-font-loading-module": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", @@ -2334,9 +3076,10 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.3", @@ -2401,7 +3144,7 @@ "version": "25.0.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2458,6 +3201,26 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/@webgpu/types": { "version": "0.1.64", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz", @@ -2572,6 +3335,18 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2584,6 +3359,39 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2636,6 +3444,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2765,6 +3593,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -3256,6 +4090,18 @@ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", "license": "ISC" }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "license": "EPL-2.0" + }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -3334,9 +4180,10 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3560,7 +4407,6 @@ "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, "optional": true, "os": [ @@ -3579,6 +4425,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3603,7 +4458,7 @@ "version": "4.7.5", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", - "dev": true, + "devOptional": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -4293,6 +5148,12 @@ "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", "license": "MIT" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -4305,6 +5166,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/katex": { "version": "0.16.21", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", @@ -4614,6 +5499,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -5640,6 +6534,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/nlcst-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", @@ -5697,6 +6609,15 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/oniguruma-to-es": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-1.0.0.tgz", @@ -5843,6 +6764,34 @@ "url": "https://opencollective.com/pixijs" } }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -5941,6 +6890,36 @@ "node": ">= 0.6" } }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -6300,7 +7279,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "devOptional": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -6382,6 +7361,50 @@ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6853,6 +7876,12 @@ "node": ">=10.0.0" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -7203,6 +8232,51 @@ "node": ">=12" } }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7259,7 +8333,7 @@ "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -7293,7 +8367,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-trie": { @@ -7488,6 +8562,36 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", @@ -7556,6 +8660,109 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -7630,6 +8837,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", diff --git a/package.json b/package.json index 613a194..88803ac 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,13 @@ "@myriaddreamin/rehype-typst": "^0.6.0", "@napi-rs/simple-git": "0.1.22", "@tweenjs/tween.js": "^25.0.0", + "@vitejs/plugin-react": "^5.2.0", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", "chokidar": "^5.0.0", "cli-spinner": "^0.2.10", "d3": "^7.9.0", + "elkjs": "^0.11.1", "esbuild-sass-plugin": "^3.6.0", "flexsearch": "^0.8.205", "github-slugger": "^2.0.0", @@ -66,6 +68,8 @@ "preact-render-to-string": "^6.6.5", "pretty-bytes": "^7.1.0", "pretty-time": "^1.1.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "reading-time": "^1.5.0", "rehype-autolink-headings": "^7.1.0", "rehype-citation": "^2.3.1", @@ -93,6 +97,7 @@ "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", + "vite": "^7.3.3", "workerpool": "^10.0.1", "ws": "^8.19.0", "yargs": "^18.0.0" diff --git a/quartz/components/GameCultGraphSpaShell.tsx b/quartz/components/GameCultGraphSpaShell.tsx new file mode 100644 index 0000000..a54cb41 --- /dev/null +++ b/quartz/components/GameCultGraphSpaShell.tsx @@ -0,0 +1,26 @@ +import { QuartzComponent, QuartzComponentConstructor } from "./types" + +export type GameCultGraphSpaShellOptions = { + stylesheetHref: string + moduleSrc: string + rootClassName?: string +} + +export default ((options?: Partial) => { + const shellOptions: GameCultGraphSpaShellOptions = { + stylesheetHref: "/static/epiphany-graph/assets/viewer.css", + moduleSrc: "/static/epiphany-graph/assets/viewer.js", + rootClassName: "gamecult-epiphany-graph-root", + ...options, + } + + const GameCultGraphSpaShell: QuartzComponent = () => ( + <> + +
+ + + ) + + return GameCultGraphSpaShell +}) satisfies QuartzComponentConstructor | undefined> diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 0b23565..42698cc 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -24,6 +24,7 @@ import Comments from "./Comments" import Flex from "./Flex" import ConditionalRender from "./ConditionalRender" import AutoIndexFolder from "./AutoIndexFolder" +import GameCultGraphSpaShell from "./GameCultGraphSpaShell" export { ArticleTitle, @@ -52,4 +53,7 @@ export { Comments, Flex, ConditionalRender, + GameCultGraphSpaShell, } + +export type { GameCultGraphSpaShellOptions } from "./GameCultGraphSpaShell" diff --git a/quartz/graph-spa/package.json b/quartz/graph-spa/package.json new file mode 100644 index 0000000..e78fad9 --- /dev/null +++ b/quartz/graph-spa/package.json @@ -0,0 +1,18 @@ +{ + "name": "@gamecult/quartz-graph-spa", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build --base ./" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.1.1", + "elkjs": "^0.11.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "typescript": "^5.9.3", + "vite": "^7.2.4" + }, + "devDependencies": {} +} diff --git a/quartz/graph-spa/src/main.tsx b/quartz/graph-spa/src/main.tsx new file mode 100644 index 0000000..5affb30 --- /dev/null +++ b/quartz/graph-spa/src/main.tsx @@ -0,0 +1,826 @@ +import React from "react" +import { flushSync } from "react-dom" +import ReactDOM from "react-dom/client" +import { EpiphanyGraphViewer } from "../../../../EpiphanyGraph/web/epiphany-graph-viewer/src/lib/EpiphanyGraphViewer" +import type { + EpiphanyGraphEdge, + EpiphanyGraphNode, + EpiphanyGraphsState, + GraphKey, + PositionedNode, + ViewerSelection, +} from "../../../../EpiphanyGraph/web/epiphany-graph-viewer/src/lib/types" +import "./styles.css" + +type QuartzContentEntry = { + slug: string + filePath?: string + title?: string + links?: string[] + tags?: string[] + content?: string +} + +type QuartzContentIndex = Record + +type ArticleState = + | { + status: "idle" + html: string | null + } + | { + status: "loading" + html: string | null + } + | { + status: "ready" + html: string + } + | { + status: "error" + html: string | null + message: string + } + +declare global { + interface Window { + fetchData?: Promise + } +} + +type GraphErrorBoundaryState = { + error: Error | null +} + +class GraphErrorBoundary extends React.Component { + state: GraphErrorBoundaryState = { error: null } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + render() { + if (this.state.error) { + return ( +
+          {this.state.error.message}
+          {"\n"}
+          {this.state.error.stack}
+        
+ ) + } + + return this.props.children + } +} + +const SECTION_ORDER = [ + "World", + "Ecology", + "Species", + "Civilizations", + "Technologies", + "Conflicts", + "Themes", + "Root", +] + +function entryTitle(slug: string, entry?: QuartzContentEntry) { + if (entry?.title) { + return entry.title + } + + return slug + .split("/") + .at(-1)! + .replace(/-/g, " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase()) +} + +function normalizeSlug(slug: string) { + if (slug.endsWith("/")) { + return `${slug.slice(0, -1)}/index` + } + + return slug +} + +function sectionForSlug(slug: string) { + return slug.includes("/") ? slug.split("/")[0] : "Root" +} + +function slugToArticleUrl(slug: string) { + if (slug === "index") { + return "/index.html" + } + + return `/${slug.split("/").map(encodeURIComponent).join("/")}.html` +} + +function slugFromArticleUrl(url: string, slugs: Set) { + const parsed = new URL(url, window.location.href) + + if (parsed.origin !== window.location.origin) { + return null + } + + const decodedPath = decodeURIComponent(parsed.pathname) + .replace(/^\//, "") + .replace(/\.html$/, "") + .replace(/\/$/, "/index") + const slug = normalizeSlug(decodedPath || "index") + + return slugs.has(slug) ? slug : null +} + +function parseHashSelection(slugs: Set): ViewerSelection | null { + const match = window.location.hash.match(/^#note=(.+)$/) + + if (!match) { + return null + } + + const slug = normalizeSlug(decodeURIComponent(match[1])) + + if (!slugs.has(slug)) { + return null + } + + return { + kind: "node", + graphKey: "architecture", + nodeId: slug, + } +} + +function currentRouteSelection(slugs: Set): ViewerSelection | null { + const bodySlug = document.body.dataset.slug + const pathSlug = decodeURIComponent(window.location.pathname) + .replace(/^\//, "") + .replace(/\.html$/, "") + .replace(/\/$/, "/index") + const slug = normalizeSlug(bodySlug || pathSlug || "index") + + if (!slugs.has(slug)) { + return null + } + + return { + kind: "node", + graphKey: "architecture", + nodeId: slug, + } +} + +function compareSections(left: string, right: string) { + const leftIndex = SECTION_ORDER.indexOf(left) + const rightIndex = SECTION_ORDER.indexOf(right) + + if (leftIndex !== -1 || rightIndex !== -1) { + const orderedLeft = leftIndex === -1 ? SECTION_ORDER.length : leftIndex + const orderedRight = rightIndex === -1 ? SECTION_ORDER.length : rightIndex + + return orderedLeft - orderedRight + } + + return left.localeCompare(right) +} + +function summarizeContent(entry: QuartzContentEntry) { + const text = (entry.content ?? "") + .replace(/\s+/g, " ") + .replace(/^Table of contents\s*/i, "") + .trim() + + if (!text) { + return `Vault note at ${entry.filePath ?? entry.slug}.` + } + + const sentenceEnd = text.search(/[.!?](\s|$)/) + const sentence = sentenceEnd >= 0 ? text.slice(0, sentenceEnd + 1) : text + + if (sentence.length <= 220) { + return sentence + } + + return `${sentence.slice(0, 217).trim()}...` +} + +function validLinkedSlugs(entry: QuartzContentEntry, slugs: Set) { + return (entry.links ?? []) + .map(normalizeSlug) + .filter( + (targetSlug, index, links) => slugs.has(targetSlug) && links.indexOf(targetSlug) === index, + ) +} + +function buildIncomingLinks(entries: Array<[string, QuartzContentEntry]>, slugs: Set) { + const incoming = new Map() + + for (const [slug] of entries) { + incoming.set(slug, []) + } + + for (const [sourceSlug, entry] of entries) { + for (const targetSlug of validLinkedSlugs(entry, slugs)) { + incoming.get(targetSlug)?.push(sourceSlug) + } + } + + return incoming +} + +function buildSectionEdges(entries: Array<[string, QuartzContentEntry]>, slugs: Set) { + const edgeCounts = new Map() + + for (const [sourceSlug, entry] of entries) { + const sourceSection = sectionForSlug(sourceSlug) + + for (const targetSlug of validLinkedSlugs(entry, slugs)) { + const targetSection = sectionForSlug(targetSlug) + const edgeId = `${sourceSection}->${targetSection}` + edgeCounts.set(edgeId, (edgeCounts.get(edgeId) ?? 0) + 1) + } + } + + return Array.from(edgeCounts.entries()).map(([edgeId, count]) => { + const [sourceSection, targetSection] = edgeId.split("->") + + return { + id: `section:${edgeId}`, + source_id: `section:${sourceSection}`, + target_id: `section:${targetSection}`, + kind: sourceSection === targetSection ? "internal_links" : "cross_links", + label: `${count} ${count === 1 ? "link" : "links"}`, + mechanism: + sourceSection === targetSection + ? `${count} wiki ${count === 1 ? "link stays" : "links stay"} inside ${sourceSection}.` + : `${count} wiki ${count === 1 ? "link runs" : "links run"} from ${sourceSection} into ${targetSection}.`, + } satisfies EpiphanyGraphEdge + }) +} + +function buildGraphState(contentIndex: QuartzContentIndex): EpiphanyGraphsState { + const entries = Object.entries(contentIndex) + .map(([slug, entry]) => { + const normalizedSlug = normalizeSlug(entry.slug || slug) + + return [normalizedSlug, { ...entry, slug: normalizedSlug }] as [string, QuartzContentEntry] + }) + .filter(([slug]) => !slug.startsWith("Inspirations/")) + .sort(([left], [right]) => left.localeCompare(right)) + + const entryBySlug = new Map(entries) + const slugs = new Set(entries.map(([slug]) => slug)) + const incoming = buildIncomingLinks(entries, slugs) + + const architectureNodes: EpiphanyGraphNode[] = entries.map(([slug, entry]) => { + const outgoingCount = validLinkedSlugs(entry, slugs).length + const backlinkCount = incoming.get(slug)?.length ?? 0 + + return { + id: slug, + title: entryTitle(slug, entry), + status: backlinkCount > 0 ? "linked" : "source", + purpose: summarizeContent(entry), + mechanism: `${outgoingCount} outgoing wiki ${outgoingCount === 1 ? "link" : "links"}; ${backlinkCount} incoming ${backlinkCount === 1 ? "backlink" : "backlinks"}. Folder: ${sectionForSlug(slug)}.`, + metaphor: entry.filePath ?? slug, + } + }) + + const architectureEdges: EpiphanyGraphEdge[] = entries.flatMap(([sourceSlug, entry]) => + validLinkedSlugs(entry, slugs).map((targetSlug, index) => ({ + id: `${sourceSlug}->${targetSlug}:${index}`, + source_id: sourceSlug, + target_id: targetSlug, + kind: "wiki_link", + label: "links to", + mechanism: `${entryTitle(sourceSlug, entry)} links to ${entryTitle(targetSlug, entryBySlug.get(targetSlug))}.`, + })), + ) + + const sections = Array.from(new Set(entries.map(([slug]) => sectionForSlug(slug)))).sort( + compareSections, + ) + const dataflowNodes: EpiphanyGraphNode[] = sections.map((section) => { + const sectionEntries = entries.filter(([slug]) => sectionForSlug(slug) === section) + const sectionSlugs = new Set(sectionEntries.map(([slug]) => slug)) + const outgoingCrossSection = sectionEntries.reduce((count, [, entry]) => { + return ( + count + + validLinkedSlugs(entry, slugs).filter((targetSlug) => !sectionSlugs.has(targetSlug)).length + ) + }, 0) + const incomingCrossSection = entries.reduce((count, [sourceSlug, entry]) => { + if (sectionForSlug(sourceSlug) === section) { + return count + } + + return ( + count + + validLinkedSlugs(entry, slugs).filter((targetSlug) => sectionSlugs.has(targetSlug)).length + ) + }, 0) + + return { + id: `section:${section}`, + title: section, + status: "active", + purpose: `${sectionEntries.length} ${sectionEntries.length === 1 ? "note" : "notes"} in this vault section.`, + mechanism: `${outgoingCrossSection} outgoing cross-section ${outgoingCrossSection === 1 ? "link" : "links"}; ${incomingCrossSection} incoming cross-section ${incomingCrossSection === 1 ? "backlink" : "backlinks"}.`, + metaphor: "Quartz backlink cluster", + } + }) + + return { + architecture: { + nodes: architectureNodes, + edges: architectureEdges, + }, + dataflow: { + nodes: dataflowNodes, + edges: buildSectionEdges(entries, slugs), + }, + links: entries.map(([slug]) => ({ + architecture_node_id: slug, + dataflow_node_id: `section:${sectionForSlug(slug)}`, + relationship: "section membership", + })), + } +} + +async function loadGraphState() { + const contentIndex = window.fetchData + ? await window.fetchData + : await fetch("/static/contentIndex.json").then( + (response) => response.json() as Promise, + ) + + return { + contentIndex, + graphState: buildGraphState(contentIndex), + } +} + +async function loadArticleHtml(slug: string) { + const response = await fetch(slugToArticleUrl(slug)) + + if (!response.ok) { + throw new Error(`Could not load ${slug}: ${response.status}`) + } + + const html = await response.text() + const documentHtml = new DOMParser().parseFromString(html, "text/html") + const article = documentHtml.querySelector("article") + + if (!article) { + throw new Error(`No article body found for ${slug}`) + } + + const articleUrl = new URL(slugToArticleUrl(slug), window.location.href) + + article.querySelectorAll("a[href]").forEach((anchor) => { + anchor.href = new URL(anchor.getAttribute("href") ?? "", articleUrl).toString() + }) + + article.querySelectorAll("img[src]").forEach((image) => { + image.src = new URL(image.getAttribute("src") ?? "", articleUrl).toString() + }) + + return article.innerHTML +} + +function selectedNoteSlug(selection: ViewerSelection | null) { + return selection?.kind === "node" && selection.graphKey === "architecture" + ? selection.nodeId + : null +} + +function App() { + const [graphState, setGraphState] = React.useState(null) + const [contentIndex, setContentIndex] = React.useState(null) + const [selection, setSelection] = React.useState(null) + const [viewportTarget, setViewportTarget] = React.useState(null) + const [articleCache, setArticleCache] = React.useState>({}) + const [error, setError] = React.useState(null) + const articleCacheRef = React.useRef>({}) + const loadingArticlesRef = React.useRef>(new Set()) + const slugs = React.useMemo( + () => new Set(Object.keys(contentIndex ?? {}).map(normalizeSlug)), + [contentIndex], + ) + const noteSlug = selectedNoteSlug(selection) + + React.useEffect(() => { + articleCacheRef.current = articleCache + }, [articleCache]) + + React.useEffect(() => { + document.body.classList.add("gamecult-graph-spa-active") + + return () => { + document.body.classList.remove("gamecult-graph-spa-active") + } + }, []) + + React.useEffect(() => { + let cancelled = false + + loadGraphState() + .then(({ contentIndex: nextContentIndex, graphState: nextGraphState }) => { + if (!cancelled) { + const nextSlugs = new Set(Object.keys(nextContentIndex).map(normalizeSlug)) + const initialViewportTarget = parseHashSelection(nextSlugs) ?? + currentRouteSelection(nextSlugs) ?? { + kind: "node" as const, + graphKey: "architecture" as const, + nodeId: nextSlugs.has("index") + ? "index" + : (nextGraphState.architecture.nodes[0]?.id ?? ""), + } + setContentIndex(nextContentIndex) + setGraphState(nextGraphState) + setViewportTarget(initialViewportTarget) + } + }) + .catch((caught) => { + if (!cancelled) { + setError(caught instanceof Error ? caught : new Error(String(caught))) + } + }) + + return () => { + cancelled = true + } + }, []) + + const ensureArticle = React.useCallback((slug: string) => { + const current = articleCacheRef.current[slug] + + if (current?.status === "ready" || loadingArticlesRef.current.has(slug)) { + return + } + + loadingArticlesRef.current.add(slug) + setArticleCache((currentCache) => ({ + ...currentCache, + [slug]: { status: "loading", html: currentCache[slug]?.html ?? null }, + })) + + loadArticleHtml(slug) + .then((html) => { + loadingArticlesRef.current.delete(slug) + setArticleCache((currentCache) => ({ + ...currentCache, + [slug]: { status: "ready", html }, + })) + }) + .catch((caught) => { + loadingArticlesRef.current.delete(slug) + setArticleCache((currentCache) => ({ + ...currentCache, + [slug]: { + status: "error", + html: null, + message: caught instanceof Error ? caught.message : String(caught), + }, + })) + }) + }, []) + + React.useEffect(() => { + if (noteSlug) { + ensureArticle(noteSlug) + } + }, [ensureArticle, noteSlug]) + + React.useEffect(() => { + if (!noteSlug) { + return + } + + window.history.replaceState(null, "", `#note=${encodeURIComponent(noteSlug)}`) + }, [noteSlug]) + + React.useEffect(() => { + const onHashChange = () => { + const hashSelection = parseHashSelection(slugs) + + if (hashSelection) { + setViewportTarget(hashSelection) + } + } + + window.addEventListener("hashchange", onHashChange) + return () => window.removeEventListener("hashchange", onHashChange) + }, [slugs]) + + const selectNote = React.useCallback( + (slug: string) => + setViewportTarget({ + kind: "node", + graphKey: "architecture", + nodeId: slug, + }), + [], + ) + + const onViewportTargetComplete = React.useCallback(() => { + setViewportTarget(null) + }, []) + + const onArticleLinkClick = React.useCallback( + (event: React.MouseEvent) => { + const anchor = (event.target as HTMLElement).closest("a[href]") + + if (!(anchor instanceof HTMLAnchorElement)) { + return + } + + const slug = slugFromArticleUrl(anchor.href, slugs) + + if (!slug) { + return + } + + event.preventDefault() + event.stopPropagation() + selectNote(slug) + }, + [selectNote, slugs], + ) + + if (error) { + return ( +
+        {error.message}
+        {"\n"}
+        {error.stack}
+      
+ ) + } + + if (!graphState) { + return ( +
+
Loading Quartz backlink graph...
+
+ ) + } + + return ( +
+ + `${node.title} article node`, + content: (node) => ( + + ), + }} + graphLabels={{ + architecture: "Notes", + dataflow: "Sections", + }} + graphDescriptions={{ + architecture: + "Notes are individual vault pages from the gamecult knowledge base. Their edges are Quartz wiki links, with incoming backlinks counted into each node.", + dataflow: + "Sections are folder-level clusters. Their edges summarize how links and backlinks move between regions of the vault.", + }} + layoutMode={{ + architecture: "combined-force", + dataflow: "combined-force", + }} + /> + +
+ ) +} + +function ViewportNodeArticle({ + node, + articleCache, + contentIndex, + ensureArticle, + onSelectNote, + onArticleLinkClick, +}: { + node: PositionedNode + articleCache: Record + contentIndex: QuartzContentIndex | null + ensureArticle: (slug: string) => void + onSelectNote: (slug: string) => void + onArticleLinkClick: (event: React.MouseEvent) => void +}) { + const noteSlug = node.graphKey === "architecture" ? node.id : null + const section = node.graphKey === "dataflow" ? node.id.replace(/^section:/, "") : null + + React.useEffect(() => { + if (noteSlug) { + ensureArticle(noteSlug) + } + }, [ensureArticle, noteSlug]) + + return ( + + ) +} + +function ArticlePanel({ + articleState, + contentIndex, + noteSlug, + section, + onSelectNote, + onArticleLinkClick, +}: { + articleState: ArticleState + contentIndex: QuartzContentIndex | null + noteSlug: string | null + section: string | null + onSelectNote: (slug: string) => void + onArticleLinkClick: (event: React.MouseEvent) => void +}) { + const note = noteSlug && contentIndex ? contentIndex[noteSlug] : null + const sectionNotes = React.useMemo(() => { + if (!section || !contentIndex) { + return [] + } + + return Object.entries(contentIndex) + .map( + ([slug, entry]) => + [normalizeSlug(entry.slug || slug), entry] as [string, QuartzContentEntry], + ) + .filter(([slug]) => sectionForSlug(slug) === section) + .sort(([left, leftEntry], [right, rightEntry]) => + entryTitle(left, leftEntry).localeCompare(entryTitle(right, rightEntry)), + ) + }, [contentIndex, section]) + + if (section) { + return ( +
+
+ + +

Section Cluster

+

{section}

+
+
+

+ {sectionNotes.length} notes live in this section. Pick one and the graph becomes the table + of contents it was clearly auditioning to be. +

+
+ {sectionNotes.map(([slug, entry]) => ( + + ))} +
+
+ ) + } + + if (!noteSlug || !note) { + return ( +
+
+ + +

Graph Atlas

+

gamecult

+
+
+

+ Select a note to read it here. The graph is the site map now, which is probably what it + was trying to confess. +

+
+ ) + } + + return ( +
+
+ + +

{sectionForSlug(noteSlug)}

+

{entryTitle(noteSlug, note)}

+
+
+ {articleState.status === "loading" && ( +
Loading article...
+ )} + {articleState.status === "error" && ( +
{articleState.message}
+ )} + {articleState.status === "ready" && ( +
+ )} + {articleState.status !== "ready" && articleState.html && ( +
+ )} +
+ ) +} + +function initialsForTitle(title: string) { + const words = title + .replace(/['’]/g, "") + .split(/[^A-Za-z0-9]+/) + .filter(Boolean) + + if (words.length === 0) { + return "GC" + } + + return words + .slice(0, 2) + .map((word) => word[0]?.toUpperCase() ?? "") + .join("") +} + +function mountGameCultGraph() { + document.querySelectorAll(".gamecult-epiphany-graph-root").forEach((element) => { + if (!(element instanceof HTMLElement)) { + return + } + + if (element.dataset.ready === "true" && element.childElementCount > 0) { + return + } + + const root = ReactDOM.createRoot(element) + flushSync(() => { + root.render( + + + , + ) + }) + element.dataset.ready = "true" + }) +} + +function scheduleGameCultGraphMount() { + mountGameCultGraph() + window.setTimeout(mountGameCultGraph, 50) + window.setTimeout(mountGameCultGraph, 300) + window.setTimeout(mountGameCultGraph, 1000) + window.setTimeout(mountGameCultGraph, 2000) +} + +document.addEventListener("nav", scheduleGameCultGraphMount) +document.addEventListener("DOMContentLoaded", scheduleGameCultGraphMount) +scheduleGameCultGraphMount() diff --git a/quartz/graph-spa/src/styles.css b/quartz/graph-spa/src/styles.css new file mode 100644 index 0000000..f926d79 --- /dev/null +++ b/quartz/graph-spa/src/styles.css @@ -0,0 +1,277 @@ +html, +body, +#root { + width: 100%; + min-height: 100%; + margin: 0; +} + +body { + background: + radial-gradient(circle at 72% 12%, rgba(255, 174, 88, 0.16), transparent 24%), + radial-gradient(circle at 10% 18%, rgba(142, 223, 176, 0.16), transparent 28%), + linear-gradient(180deg, #03070d 0%, #07110f 52%, #091712 100%); + color: #effcf8; + font-family: "Ubuntu", "Segoe UI", sans-serif; +} + +.gamecult-epiphany-graph-app { + box-sizing: border-box; + width: 100vw; + min-height: 100vh; + padding: 0; +} + +.gamecult-epiphany-graph-app * { + box-sizing: border-box; +} + +.gamecult-epiphany-graph-root { + position: fixed; + inset: 0; + z-index: 1000; + width: 100vw; + height: 100vh; + min-height: 100vh; + margin: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background: #03070d; +} + +.gamecult-graph-error { + white-space: pre-wrap; + color: #fecaca; + background: rgba(69, 10, 10, 0.6); + border: 1px solid rgba(248, 113, 113, 0.45); + border-radius: 16px; + padding: 16px; +} + +.gamecult-graph-loading { + display: grid; + min-height: 320px; + place-items: center; + color: #cfeee4; + font-size: 0.95rem; +} + +body.gamecult-graph-spa-active { + overflow: hidden; +} + +.gamecult-graph-shell { + width: 100vw; + height: 100vh; +} + +.gamecult-graph-shell .epiphany-graph-overlay-panel { + opacity: 0.25; + transition: + opacity 160ms ease, + transform 160ms ease, + box-shadow 160ms ease; +} + +.gamecult-graph-shell .epiphany-graph-overlay-panel:hover, +.gamecult-graph-shell .epiphany-graph-overlay-panel:focus-within { + opacity: 1; + transform: translateY(-1px); +} + +.gamecult-graph-shell aside.epiphany-graph-overlay-panel > section { + display: none !important; +} + +.gamecult-graph-shell aside.epiphany-graph-overlay-panel > section:last-child { + display: grid !important; + grid-row: 1 / -1; +} + +.gamecult-expanded-article-node { + box-sizing: border-box; + padding: var(--node-pad-y, 0.62rem) var(--node-pad-x, 0.82rem); + scrollbar-color: rgba(142, 223, 176, 0.58) rgba(3, 7, 13, 0.32); + transition: + border-radius 120ms ease, + box-shadow 120ms ease, + padding 120ms ease, + opacity 120ms ease; +} + +.gamecult-expanded-article-node .gamecult-spa-panel { + max-height: none; + overflow: visible; +} + +.gamecult-spa-panel { + min-height: 0; + max-height: 100%; + overflow: auto; + color: #effcf8; +} + +.gamecult-spa-panel h1 { + max-height: var(--node-title-max-height, 1.12em); + margin: 0; + overflow: hidden; + color: #f7fffb; + font-size: var(--node-title-size, 1.08rem); + line-height: 1.04; + letter-spacing: 0; +} + +.gamecult-spa-kicker { + margin: 0 0 var(--node-kicker-margin, 0.12rem); + color: #8edfb0; + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + opacity: var(--node-preview, 0); + transition: opacity 120ms ease; +} + +.gamecult-spa-summary, +.gamecult-spa-status { + color: rgba(239, 252, 248, 0.76); + line-height: 1.55; + max-height: var(--node-preview-height, 0rem); + margin-block: var(--node-status-margin, 0rem); + overflow: hidden; + opacity: var(--node-preview, 0); +} + +.gamecult-spa-status { + margin: 1rem 0; + padding: 0.75rem 0.9rem; + border: 1px solid rgba(142, 223, 176, 0.16); + border-radius: 16px; + background: rgba(3, 7, 13, 0.45); +} + +.gamecult-spa-status-error { + color: #fecaca; + border-color: rgba(248, 113, 113, 0.35); +} + +.gamecult-spa-article-header { + display: grid; + grid-template-columns: var(--node-badge-size, 3rem) minmax(0, 1fr); + align-items: center; + gap: var(--node-panel-gap, 0.72rem); + min-height: var(--node-badge-size, 3rem); + margin-bottom: var(--node-header-margin, 0rem); +} + +.gamecult-spa-node-badge { + display: grid; + width: var(--node-badge-size, 3rem); + height: var(--node-badge-size, 3rem); + place-items: center; + border-radius: 999px; + background: linear-gradient(160deg, #69e2ef, #c2f5f0); + color: #08313a; + font-size: var(--node-title-size, 1.08rem); + font-weight: 900; + line-height: 1; + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.28), + 0 0 var(--node-badge-glow, 1rem) rgba(94, 234, 212, 0.3); +} + +.gamecult-spa-title-cluster { + min-width: 0; +} + +.gamecult-spa-actions { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.gamecult-spa-actions a, +.gamecult-spa-note-list button { + border: 1px solid rgba(142, 223, 176, 0.18); + border-radius: 16px; + background: rgba(142, 223, 176, 0.08); + color: #effcf8; +} + +.gamecult-spa-actions a { + padding: 0.5rem 0.7rem; + text-decoration: none; +} + +.gamecult-spa-article { + color: rgba(239, 252, 248, 0.86); + line-height: 1.65; + max-height: var(--node-article-height, 0rem); + overflow: hidden; + opacity: var(--node-article, 0); + transform: translateY(var(--node-article-offset, 0.65rem)); + transition: + opacity 120ms ease, + transform 120ms ease; +} + +.gamecult-spa-article h1 { + display: none; +} + +.gamecult-spa-article h2, +.gamecult-spa-article h3 { + color: #f7fffb; + line-height: 1.12; +} + +.gamecult-spa-article a { + color: #ffe0a8; +} + +.gamecult-spa-article img { + max-width: 100%; + border-radius: 18px; +} + +.gamecult-spa-note-list { + display: grid; + gap: 0.7rem; + max-height: var(--node-note-list-height, 0rem); + overflow: hidden; + opacity: var(--node-article, 0); +} + +.gamecult-spa-note-list button { + display: grid; + gap: 0.35rem; + padding: 0.85rem; + text-align: left; + cursor: pointer; +} + +.gamecult-spa-note-list button:hover { + border-color: rgba(255, 174, 88, 0.38); + background: rgba(255, 174, 88, 0.11); +} + +.gamecult-spa-note-list strong { + color: #f7fffb; +} + +.gamecult-spa-note-list span { + color: rgba(239, 252, 248, 0.68); + font-size: 0.92rem; + line-height: 1.45; +} + +@media (max-width: 900px) { + .gamecult-graph-shell aside.epiphany-graph-overlay-panel { + left: 12px; + right: 12px; + top: auto !important; + bottom: 12px !important; + width: auto !important; + max-height: 48vh; + } +} diff --git a/quartz/graph-spa/viewer.html b/quartz/graph-spa/viewer.html new file mode 100644 index 0000000..a03cbed --- /dev/null +++ b/quartz/graph-spa/viewer.html @@ -0,0 +1,12 @@ + + + + + + gamecult Fractal Eusocial Web + + +
+ + + diff --git a/quartz/graph-spa/vite.config.ts b/quartz/graph-spa/vite.config.ts new file mode 100644 index 0000000..76eacee --- /dev/null +++ b/quartz/graph-spa/vite.config.ts @@ -0,0 +1,43 @@ +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" +import { fileURLToPath } from "node:url" +import path from "node:path" + +const viewerEntry = fileURLToPath(new URL("./viewer.html", import.meta.url)) +const reactPath = fileURLToPath(new URL("./node_modules/react", import.meta.url)) +const reactDomPath = fileURLToPath(new URL("./node_modules/react-dom", import.meta.url)) +const outDir = process.env.GAMECULT_GRAPH_SPA_OUT_DIR + ? path.resolve(process.env.GAMECULT_GRAPH_SPA_OUT_DIR) + : fileURLToPath(new URL("./dist", import.meta.url)) + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + react: reactPath, + "react-dom": reactDomPath, + }, + }, + server: { + host: "127.0.0.1", + port: 4182, + }, + build: { + outDir, + emptyOutDir: true, + rollupOptions: { + input: { + viewer: viewerEntry, + }, + output: { + manualChunks: { + react: ["react", "react-dom"], + elk: ["elkjs/lib/elk.bundled.js"], + }, + entryFileNames: "assets/[name].js", + chunkFileNames: "assets/[name].js", + assetFileNames: "assets/[name][extname]", + }, + }, + }, +}) diff --git a/scripts/build-graph-spa.mjs b/scripts/build-graph-spa.mjs new file mode 100644 index 0000000..a7a91e0 --- /dev/null +++ b/scripts/build-graph-spa.mjs @@ -0,0 +1,94 @@ +import fs from "node:fs/promises" +import path from "node:path" +import { spawn } from "node:child_process" +import { fileURLToPath } from "node:url" + +const engineRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") + +function parseArgs(argv) { + const options = { + siteRoot: process.cwd(), + outputDir: "static/epiphany-graph", + } + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (!arg.startsWith("--")) continue + + const key = arg.slice(2) + const next = argv[i + 1] + if (next && !next.startsWith("--")) { + options[key] = next + i++ + } else { + options[key] = "true" + } + } + + return options +} + +async function exists(targetPath) { + try { + await fs.lstat(targetPath) + return true + } catch { + return false + } +} + +async function linkNodeModules(graphRoot) { + const source = path.join(engineRoot, "node_modules") + const dest = path.join(graphRoot, "node_modules") + + if (!(await exists(source))) { + throw new Error( + "GameCult-Quartz dependencies are missing. Run `npm ci` in the GameCult-Quartz repo first.", + ) + } + + await fs.rm(dest, { recursive: true, force: true }) + await fs.symlink(source, dest, "junction") +} + +function runBuild(graphRoot, outputRoot) { + return new Promise((resolve, reject) => { + const child = spawn( + process.execPath, + ["./node_modules/vite/bin/vite.js", "build", "--base", "./"], + { + cwd: graphRoot, + stdio: "inherit", + env: { + ...process.env, + GAMECULT_GRAPH_SPA_OUT_DIR: outputRoot, + }, + }, + ) + + child.on("exit", (code) => { + if (code === 0) { + resolve() + return + } + + reject(new Error(`Graph SPA build exited with code ${code}`)) + }) + child.on("error", reject) + }) +} + +async function main() { + const options = parseArgs(process.argv.slice(2)) + const siteRoot = path.resolve(options.siteRoot) + const outputRoot = path.resolve(siteRoot, options.outputDir) + const graphRoot = path.join(engineRoot, "quartz", "graph-spa") + + await linkNodeModules(graphRoot) + await runBuild(graphRoot, outputRoot) +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error) + process.exit(1) +}) diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs index 80f4b37..5dcb019 100644 --- a/scripts/build-site.mjs +++ b/scripts/build-site.mjs @@ -70,7 +70,13 @@ async function copyEngineScaffold(runtimeRoot) { const dest = path.join(runtimeRoot, entry.name) if (entry.isDirectory()) { - await fs.cp(source, dest, { recursive: true }) + await fs.cp(source, dest, { + recursive: true, + filter: (candidate) => { + const relative = path.relative(engineRoot, candidate).split(path.sep).join("/") + return relative !== "quartz/graph-spa/node_modules" + }, + }) } else { await fs.copyFile(source, dest) } diff --git a/tsconfig.json b/tsconfig.json index 637d096..381fe47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,5 @@ "jsxImportSource": "preact" }, "include": ["**/*.ts", "**/*.tsx", "./package.json"], - "exclude": ["build/**/*.d.ts"] + "exclude": ["build/**/*.d.ts", "quartz/graph-spa/**"] }