A demon compendium, deterministically derived from DIDs + PDS records. haunt.at
atmosphere atproto
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

author
Aly Raffauf
date (Jul 22, 2026, 10:23 PM -0400) commit 8efcc094
+2440
+32
.gitignore
··· 1 + # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 + 3 + # dependencies 4 + /node_modules 5 + /.pnp 6 + .pnp.js 7 + 8 + # testing 9 + /coverage 10 + 11 + # production 12 + /dist 13 + 14 + # misc 15 + .DS_Store 16 + *.pem 17 + 18 + # debug 19 + npm-debug.log* 20 + yarn-debug.log* 21 + yarn-error.log* 22 + .pnpm-debug.log* 23 + 24 + # local env files 25 + .env 26 + .env*.local 27 + 28 + # typescript 29 + *.tsbuildinfo 30 + 31 + # idea files 32 + .idea
+22
README.md
··· 1 + # Generic Website 2 + 3 + An atproto profile aggregator. Visit `/<handle>` or `/<did>` to see the records that a person has published across the Atmosphere. 4 + 5 + The app resolves identities through Slingshot, then fetches records directly from the person’s PDS in the browser. 6 + 7 + ## Development 8 + 9 + ```sh 10 + bun install 11 + bun run dev 12 + ``` 13 + 14 + Useful checks: 15 + 16 + ```sh 17 + bun run typecheck 18 + bun run format:check 19 + bun run build 20 + ``` 21 + 22 + The production site is written to `dist/`. Static hosts must serve `index.html` for unknown paths; `public/_redirects` configures this for Cloudflare Pages and Netlify.
+422
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "generic-website", 7 + "dependencies": { 8 + "@fontsource-variable/space-grotesk": "^5.3.0", 9 + "react": "^19.2.8", 10 + "react-dom": "^19.2.8", 11 + }, 12 + "devDependencies": { 13 + "@tailwindcss/postcss": "^4.3.3", 14 + "@types/node": "^20.19.43", 15 + "@types/react": "^19.2.17", 16 + "@types/react-dom": "^19.2.3", 17 + "@vitejs/plugin-react": "^6.0.4", 18 + "eslint": "^9.39.5", 19 + "postcss": "^8.5.22", 20 + "prettier": "^3.9.6", 21 + "prettier-plugin-tailwindcss": "^0.6.14", 22 + "tailwindcss": "^4.3.3", 23 + "typescript": "^5.9.3", 24 + "typescript-eslint": "^8.65.0", 25 + "vite": "^8.1.5", 26 + }, 27 + }, 28 + }, 29 + "packages": { 30 + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], 31 + 32 + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], 33 + 34 + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], 35 + 36 + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], 37 + 38 + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], 39 + 40 + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], 41 + 42 + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], 43 + 44 + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], 45 + 46 + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], 47 + 48 + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="], 49 + 50 + "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="], 51 + 52 + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], 53 + 54 + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], 55 + 56 + "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.3.0", "", {}, "sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw=="], 57 + 58 + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], 59 + 60 + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], 61 + 62 + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], 63 + 64 + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 65 + 66 + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], 67 + 68 + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], 69 + 70 + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], 71 + 72 + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 73 + 74 + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 75 + 76 + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 77 + 78 + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], 79 + 80 + "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], 81 + 82 + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], 83 + 84 + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], 85 + 86 + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], 87 + 88 + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], 89 + 90 + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], 91 + 92 + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], 93 + 94 + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], 95 + 96 + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], 97 + 98 + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], 99 + 100 + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], 101 + 102 + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], 103 + 104 + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], 105 + 106 + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], 107 + 108 + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], 109 + 110 + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], 111 + 112 + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], 113 + 114 + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], 115 + 116 + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], 117 + 118 + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], 119 + 120 + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], 121 + 122 + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], 123 + 124 + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], 125 + 126 + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], 127 + 128 + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], 129 + 130 + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], 131 + 132 + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], 133 + 134 + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], 135 + 136 + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], 137 + 138 + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], 139 + 140 + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], 141 + 142 + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], 143 + 144 + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], 145 + 146 + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], 147 + 148 + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 149 + 150 + "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], 151 + 152 + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], 153 + 154 + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 155 + 156 + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.65.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", "@typescript-eslint/utils": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA=="], 157 + 158 + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.65.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA=="], 159 + 160 + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.65.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.65.0", "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q=="], 161 + 162 + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0" } }, "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg=="], 163 + 164 + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.65.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg=="], 165 + 166 + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g=="], 167 + 168 + "@typescript-eslint/types": ["@typescript-eslint/types@8.65.0", "", {}, "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg=="], 169 + 170 + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.65.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.65.0", "@typescript-eslint/tsconfig-utils": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg=="], 171 + 172 + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.65.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA=="], 173 + 174 + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A=="], 175 + 176 + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg=="], 177 + 178 + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], 179 + 180 + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 181 + 182 + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], 183 + 184 + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 185 + 186 + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 187 + 188 + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 189 + 190 + "brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], 191 + 192 + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 193 + 194 + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 195 + 196 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 197 + 198 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 199 + 200 + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 201 + 202 + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 203 + 204 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 205 + 206 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 207 + 208 + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 209 + 210 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 211 + 212 + "enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="], 213 + 214 + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 215 + 216 + "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="], 217 + 218 + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 219 + 220 + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 221 + 222 + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], 223 + 224 + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], 225 + 226 + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], 227 + 228 + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 229 + 230 + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 231 + 232 + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 233 + 234 + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 235 + 236 + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 237 + 238 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 239 + 240 + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 241 + 242 + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], 243 + 244 + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 245 + 246 + "flatted": ["flatted@3.4.3", "", {}, "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ=="], 247 + 248 + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 249 + 250 + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 251 + 252 + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 253 + 254 + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 255 + 256 + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 257 + 258 + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 259 + 260 + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 261 + 262 + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 263 + 264 + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 265 + 266 + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 267 + 268 + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 269 + 270 + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], 271 + 272 + "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], 273 + 274 + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], 275 + 276 + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], 277 + 278 + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], 279 + 280 + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], 281 + 282 + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], 283 + 284 + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], 285 + 286 + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], 287 + 288 + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], 289 + 290 + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], 291 + 292 + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], 293 + 294 + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], 295 + 296 + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], 297 + 298 + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], 299 + 300 + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], 301 + 302 + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], 303 + 304 + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], 305 + 306 + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], 307 + 308 + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 309 + 310 + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 311 + 312 + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 313 + 314 + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], 315 + 316 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 317 + 318 + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], 319 + 320 + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 321 + 322 + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 323 + 324 + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], 325 + 326 + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 327 + 328 + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 329 + 330 + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 331 + 332 + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 333 + 334 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 335 + 336 + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], 337 + 338 + "postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="], 339 + 340 + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 341 + 342 + "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], 343 + 344 + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], 345 + 346 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 347 + 348 + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], 349 + 350 + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], 351 + 352 + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 353 + 354 + "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], 355 + 356 + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 357 + 358 + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], 359 + 360 + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 361 + 362 + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 363 + 364 + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 365 + 366 + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 367 + 368 + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 369 + 370 + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], 371 + 372 + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], 373 + 374 + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], 375 + 376 + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], 377 + 378 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 379 + 380 + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 381 + 382 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 383 + 384 + "typescript-eslint": ["typescript-eslint@8.65.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.65.0", "@typescript-eslint/parser": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA=="], 385 + 386 + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 387 + 388 + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 389 + 390 + "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], 391 + 392 + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 393 + 394 + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 395 + 396 + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 397 + 398 + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 399 + 400 + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], 401 + 402 + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], 403 + 404 + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], 405 + 406 + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], 407 + 408 + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], 409 + 410 + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 411 + 412 + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], 413 + 414 + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], 415 + 416 + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], 417 + 418 + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], 419 + 420 + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], 421 + } 422 + }
+42
eslint.config.js
··· 1 + import tseslint from "typescript-eslint"; 2 + 3 + export default tseslint.config( 4 + { 5 + ignores: ["dist"], 6 + }, 7 + { 8 + files: ["**/*.ts", "**/*.tsx"], 9 + extends: [ 10 + ...tseslint.configs.recommended, 11 + ...tseslint.configs.recommendedTypeChecked, 12 + ...tseslint.configs.stylisticTypeChecked, 13 + ], 14 + rules: { 15 + "@typescript-eslint/array-type": "off", 16 + "@typescript-eslint/consistent-type-definitions": "off", 17 + "@typescript-eslint/consistent-type-imports": [ 18 + "warn", 19 + { prefer: "type-imports", fixStyle: "inline-type-imports" }, 20 + ], 21 + "@typescript-eslint/no-unused-vars": [ 22 + "warn", 23 + { argsIgnorePattern: "^_" }, 24 + ], 25 + "@typescript-eslint/require-await": "off", 26 + "@typescript-eslint/no-misused-promises": [ 27 + "error", 28 + { checksVoidReturn: { attributes: false } }, 29 + ], 30 + }, 31 + }, 32 + { 33 + linterOptions: { 34 + reportUnusedDisableDirectives: true, 35 + }, 36 + languageOptions: { 37 + parserOptions: { 38 + projectService: true, 39 + }, 40 + }, 41 + }, 42 + );
+17
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <meta name="description" content="An atproto activity aggregator" /> 8 + <link rel="icon" href="/favicon.ico" /> 9 + <title>Atmosphere Pages</title> 10 + </head> 11 + 12 + <body> 13 + <div id="root"></div> 14 + <script type="module" src="/src/main.tsx"></script> 15 + </body> 16 + 17 + </html>
+37
package.json
··· 1 + { 2 + "name": "atmo-pages", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "build": "tsc --noEmit && vite build", 8 + "check": "eslint . && tsc --noEmit", 9 + "dev": "vite", 10 + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 11 + "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 12 + "lint": "eslint .", 13 + "lint:fix": "eslint . --fix", 14 + "preview": "vite preview", 15 + "typecheck": "tsc --noEmit" 16 + }, 17 + "dependencies": { 18 + "@fontsource-variable/space-grotesk": "^5.3.0", 19 + "react": "^19.2.8", 20 + "react-dom": "^19.2.8" 21 + }, 22 + "devDependencies": { 23 + "@tailwindcss/postcss": "^4.3.3", 24 + "@types/node": "^20.19.43", 25 + "@types/react": "^19.2.17", 26 + "@types/react-dom": "^19.2.3", 27 + "@vitejs/plugin-react": "^6.0.4", 28 + "eslint": "^9.39.5", 29 + "postcss": "^8.5.22", 30 + "prettier": "^3.9.6", 31 + "prettier-plugin-tailwindcss": "^0.6.14", 32 + "tailwindcss": "^4.3.3", 33 + "typescript": "^5.9.3", 34 + "typescript-eslint": "^8.65.0", 35 + "vite": "^8.1.5" 36 + } 37 + }
+5
postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + "@tailwindcss/postcss": {}, 4 + }, 5 + };
+4
prettier.config.js
··· 1 + /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 + export default { 3 + plugins: ["prettier-plugin-tailwindcss"], 4 + };
+1
public/_redirects
··· 1 + /* /index.html 200
public/favicon.ico

This is a binary file and will not be displayed.

+179
src/components/actor-app.tsx
··· 1 + import type { CSSProperties } from "react"; 2 + import { useEffect, useState } from "react"; 3 + 4 + import { BlueskyProfile } from "~/components/bluesky-profile"; 5 + import { IdentityCard } from "~/components/identity-card"; 6 + import { MasonryGrid } from "~/components/masonry-grid"; 7 + import { ProfileSection } from "~/components/profile-section"; 8 + import { ProfileSections } from "~/components/profile-sections"; 9 + import { resolveMiniDoc } from "~/lib/atproto/mini-doc"; 10 + import { getBlueskyProfile } from "~/lib/providers/bluesky"; 11 + import { getDidAtmosphere } from "~/lib/theme/did-atmosphere"; 12 + import { getDidCardRadius } from "~/lib/theme/did-card-radius"; 13 + import { getDidTheme } from "~/lib/theme/did-colors"; 14 + import type { BlueskyProfile as BlueskyProfileData } from "~/lib/providers/bluesky"; 15 + import type { AtprotoMiniIdentity } from "~/lib/atproto/mini-doc"; 16 + 17 + export function ActorApp() { 18 + const pathname = useBrowserPathname(); 19 + const identifier = getPathIdentifier(pathname); 20 + const [identity, setIdentity] = useState<AtprotoMiniIdentity | null>(null); 21 + const [profile, setProfile] = useState<BlueskyProfileData | null>(null); 22 + const [isLoading, setIsLoading] = useState(true); 23 + const [error, setError] = useState<string | null>(null); 24 + 25 + useEffect(() => { 26 + if (!identifier) { 27 + setIdentity(null); 28 + setProfile(null); 29 + setIsLoading(false); 30 + setError(null); 31 + return; 32 + } 33 + 34 + const actorIdentifier = identifier; 35 + 36 + const controller = new AbortController(); 37 + setIsLoading(true); 38 + setError(null); 39 + 40 + async function loadActor() { 41 + try { 42 + const resolvedIdentity = await resolveMiniDoc( 43 + actorIdentifier, 44 + controller.signal, 45 + ); 46 + const resolvedProfile = await getBlueskyProfile( 47 + resolvedIdentity.pds, 48 + resolvedIdentity.did, 49 + ); 50 + 51 + if (!controller.signal.aborted) { 52 + setIdentity(resolvedIdentity); 53 + setProfile(resolvedProfile); 54 + setIsLoading(false); 55 + } 56 + } catch (loadError) { 57 + if (!controller.signal.aborted) { 58 + setError( 59 + loadError instanceof Error 60 + ? loadError.message 61 + : "Could not resolve this profile.", 62 + ); 63 + setIsLoading(false); 64 + } 65 + } 66 + } 67 + 68 + void loadActor(); 69 + 70 + return () => controller.abort(); 71 + }, [identifier]); 72 + 73 + if (pathname === null) { 74 + return <LoadingPage />; 75 + } 76 + 77 + if (!identifier) { 78 + return <Home />; 79 + } 80 + 81 + if (isLoading) { 82 + return <LoadingPage />; 83 + } 84 + 85 + if (error || !identity) { 86 + return <ErrorPage message={error ?? "Could not resolve this profile."} />; 87 + } 88 + 89 + const theme = getDidTheme(identity.did).css; 90 + const atmosphere = getDidAtmosphere(identity.did); 91 + const cardRadius = getDidCardRadius(identity.did); 92 + 93 + return ( 94 + <main 95 + className="profile-page min-h-screen p-4 sm:p-8" 96 + style={ 97 + { 98 + "--profile-background": theme.background, 99 + "--profile-card": theme.card, 100 + "--profile-border": theme.border, 101 + "--profile-accent": theme.accent, 102 + "--profile-card-radius": cardRadius, 103 + "--profile-atmosphere": atmosphere.backgroundImage, 104 + } as CSSProperties 105 + } 106 + > 107 + <MasonryGrid> 108 + {profile ? ( 109 + <BlueskyProfile handle={identity.handle} profile={profile} /> 110 + ) : ( 111 + <IdentityCard identity={identity} /> 112 + )} 113 + 114 + <ProfileSections 115 + did={identity.did} 116 + handle={identity.handle} 117 + pds={identity.pds} 118 + /> 119 + </MasonryGrid> 120 + </main> 121 + ); 122 + } 123 + 124 + function useBrowserPathname(): string | null { 125 + const [pathname, setPathname] = useState<string | null>(null); 126 + 127 + useEffect(() => { 128 + setPathname(window.location.pathname); 129 + }, []); 130 + 131 + return pathname; 132 + } 133 + 134 + function getPathIdentifier(pathname: string | null): string | null { 135 + if (!pathname || pathname === "/") { 136 + return null; 137 + } 138 + 139 + const path = pathname.replace(/^\/+|\/+$/g, ""); 140 + return path ? decodeURIComponent(path) : null; 141 + } 142 + 143 + function Home() { 144 + return ( 145 + <main className="min-h-screen p-8"> 146 + <div className="mx-auto flex max-w-2xl flex-col gap-4"> 147 + <h1 className="text-3xl font-bold">Generic Website</h1> 148 + <p>Try visiting an atproto profile.</p> 149 + <a className="w-fit border px-3 py-2" href="/atproto.com"> 150 + View an example profile → 151 + </a> 152 + </div> 153 + </main> 154 + ); 155 + } 156 + 157 + function LoadingPage() { 158 + return ( 159 + <main className="min-h-screen p-4 sm:p-8"> 160 + <div className="mx-auto max-w-2xl"> 161 + <ProfileSection title="Resolving identity"> 162 + <p className="text-sm">Scanning the Atmosphere…</p> 163 + </ProfileSection> 164 + </div> 165 + </main> 166 + ); 167 + } 168 + 169 + function ErrorPage({ message }: { message: string }) { 170 + return ( 171 + <main className="min-h-screen p-4 sm:p-8"> 172 + <div className="mx-auto max-w-2xl"> 173 + <ProfileSection title="Could not resolve profile"> 174 + <p className="text-sm">{message}</p> 175 + </ProfileSection> 176 + </div> 177 + </main> 178 + ); 179 + }
+77
src/components/bluesky-profile.tsx
··· 1 + import { useState } from "react"; 2 + 3 + import type { BlueskyProfile } from "~/lib/providers/bluesky"; 4 + 5 + type BlueskyProfileProps = { 6 + handle: string | null; 7 + profile: BlueskyProfile; 8 + }; 9 + 10 + export function BlueskyProfile({ handle, profile }: BlueskyProfileProps) { 11 + const [avatarUrl, setAvatarUrl] = useState(profile.avatarUrl); 12 + 13 + return ( 14 + <section className="bluesky-profile profile-card border"> 15 + <div className="profile-identity-header"> 16 + {avatarUrl && ( 17 + <img 18 + src={avatarUrl} 19 + alt="" 20 + width={64} 21 + height={64} 22 + className="size-16 object-cover" 23 + onError={() => { 24 + if (profile.avatarFallbackUrl) { 25 + setAvatarUrl(profile.avatarFallbackUrl); 26 + } 27 + }} 28 + /> 29 + )} 30 + 31 + <div className="min-w-0"> 32 + <h1 className="profile-name"> 33 + {profile.displayName ?? handle ?? "Unnamed account"} 34 + </h1> 35 + 36 + {handle && <p className="profile-secondary mt-1">@{handle}</p>} 37 + 38 + {profile.pronouns && ( 39 + <p className="profile-metadata mt-1 tracking-wide"> 40 + {profile.pronouns} 41 + </p> 42 + )} 43 + </div> 44 + </div> 45 + 46 + <div className="mt-4"> 47 + {profile.description && ( 48 + <p className="profile-copy max-w-[42rem] whitespace-pre-wrap"> 49 + {profile.description} 50 + </p> 51 + )} 52 + 53 + {/* {profile.website && ( 54 + <p className="mt-5"> 55 + <a 56 + className="profile-link font-semibold" 57 + href={profile.website} 58 + target="_blank" 59 + rel="noreferrer" 60 + > 61 + {formatWebsiteLabel(profile.website)} 62 + </a> 63 + </p> 64 + )} */} 65 + </div> 66 + </section> 67 + ); 68 + } 69 + 70 + function formatWebsiteLabel(website: string): string { 71 + try { 72 + const url = new URL(website); 73 + return url.hostname.replace(/^www\./, ""); 74 + } catch { 75 + return website; 76 + } 77 + }
+43
src/components/grain-photos.tsx
··· 1 + import type { GrainPhoto } from "~/lib/providers/grain-photo"; 2 + import { ProfileSection } from "~/components/profile-section"; 3 + type GrainPhotosProps = { 4 + photos: GrainPhoto[]; 5 + }; 6 + 7 + export function GrainPhotos({ photos }: GrainPhotosProps) { 8 + return ( 9 + <ProfileSection title="Photos"> 10 + <div className="grid gap-3 md:grid-cols-3"> 11 + {photos.map((photo) => 12 + photo.postUrl ? ( 13 + <a 14 + key={`${photo.createdAt}-${photo.imageUrl}`} 15 + href={photo.postUrl} 16 + target="_blank" 17 + rel="noreferrer" 18 + > 19 + <PhotoImage photo={photo} /> 20 + </a> 21 + ) : ( 22 + <PhotoImage 23 + key={`${photo.createdAt}-${photo.imageUrl}`} 24 + photo={photo} 25 + /> 26 + ), 27 + )} 28 + </div> 29 + </ProfileSection> 30 + ); 31 + } 32 + 33 + function PhotoImage({ photo }: { photo: GrainPhoto }) { 34 + return ( 35 + <img 36 + src={photo.imageUrl} 37 + alt="" 38 + width={photo.aspectRatio.width} 39 + height={photo.aspectRatio.height} 40 + className="h-auto w-full object-cover" 41 + /> 42 + ); 43 + }
+18
src/components/identity-card.tsx
··· 1 + import type { AtprotoMiniIdentity } from "~/lib/atproto/mini-doc"; 2 + 3 + type IdentityCardProps = { 4 + identity: AtprotoMiniIdentity; 5 + }; 6 + 7 + export function IdentityCard({ identity }: IdentityCardProps) { 8 + return ( 9 + <section className="profile-card border"> 10 + <div> 11 + <h1 className="profile-name">{identity.handle ?? "Unknown handle"}</h1> 12 + <p className="profile-metadata mt-2 break-all">{identity.did}</p> 13 + </div> 14 + 15 + <p className="profile-metadata mt-4 break-all">PDS: {identity.pds}</p> 16 + </section> 17 + ); 18 + }
+115
src/components/masonry-grid.tsx
··· 1 + import type { ReactNode } from "react"; 2 + import { useLayoutEffect, useRef } from "react"; 3 + 4 + const MASONRY_MEDIA_QUERY = "(min-width: 40rem)"; 5 + 6 + type MasonryGridProps = { 7 + children: ReactNode; 8 + }; 9 + 10 + export function MasonryGrid({ children }: MasonryGridProps) { 11 + const gridRef = useRef<HTMLDivElement>(null); 12 + 13 + useLayoutEffect(() => { 14 + const masonryGrid = gridRef.current; 15 + if (!masonryGrid) { 16 + return; 17 + } 18 + 19 + const gridElement: HTMLDivElement = masonryGrid; 20 + 21 + const mediaQuery = window.matchMedia(MASONRY_MEDIA_QUERY); 22 + let animationFrame = 0; 23 + 24 + function scheduleLayout() { 25 + cancelAnimationFrame(animationFrame); 26 + animationFrame = requestAnimationFrame(updateLayout); 27 + } 28 + 29 + function updateLayout() { 30 + const cards = Array.from(gridElement.children) as HTMLElement[]; 31 + 32 + if (!mediaQuery.matches) { 33 + gridElement.classList.remove("profile-grid-masonry"); 34 + cards.forEach(clearCardPlacement); 35 + return; 36 + } 37 + 38 + gridElement.classList.add("profile-grid-masonry"); 39 + 40 + const styles = window.getComputedStyle(gridElement); 41 + const rowHeight = Number.parseFloat(styles.gridAutoRows); 42 + const rowGap = Number.parseFloat(styles.rowGap); 43 + const columnCount = getColumnCount(styles.gridTemplateColumns); 44 + 45 + if (!rowHeight || !columnCount) { 46 + return; 47 + } 48 + 49 + const columnHeights = Array<number>(columnCount).fill(0); 50 + 51 + cards.forEach((card) => { 52 + const columnIndex = getShortestColumn(columnHeights); 53 + const rowSpan = getRowSpan(card, rowHeight, rowGap); 54 + 55 + card.style.gridColumnStart = String(columnIndex + 1); 56 + card.style.gridRowStart = String(columnHeights[columnIndex]! + 1); 57 + card.style.gridRowEnd = `span ${rowSpan}`; 58 + columnHeights[columnIndex]! += rowSpan; 59 + }); 60 + } 61 + 62 + const resizeObserver = new ResizeObserver(scheduleLayout); 63 + const mutationObserver = new MutationObserver(scheduleLayout); 64 + 65 + resizeObserver.observe(gridElement); 66 + mutationObserver.observe(gridElement, { childList: true }); 67 + mediaQuery.addEventListener("change", scheduleLayout); 68 + scheduleLayout(); 69 + 70 + return () => { 71 + cancelAnimationFrame(animationFrame); 72 + resizeObserver.disconnect(); 73 + mutationObserver.disconnect(); 74 + mediaQuery.removeEventListener("change", scheduleLayout); 75 + }; 76 + }, []); 77 + 78 + return ( 79 + <div ref={gridRef} className="profile-grid mx-auto max-w-6xl"> 80 + {children} 81 + </div> 82 + ); 83 + } 84 + 85 + function clearCardPlacement(card: HTMLElement) { 86 + card.style.removeProperty("grid-column-start"); 87 + card.style.removeProperty("grid-row-start"); 88 + card.style.removeProperty("grid-row-end"); 89 + } 90 + 91 + function getColumnCount(gridTemplateColumns: string): number { 92 + if (gridTemplateColumns === "none") { 93 + return 0; 94 + } 95 + 96 + return gridTemplateColumns.split(" ").length; 97 + } 98 + 99 + function getShortestColumn(columnHeights: number[]): number { 100 + return columnHeights.reduce( 101 + (shortestIndex, height, index) => 102 + height < columnHeights[shortestIndex]! ? index : shortestIndex, 103 + 0, 104 + ); 105 + } 106 + 107 + function getRowSpan( 108 + card: HTMLElement, 109 + rowHeight: number, 110 + rowGap: number, 111 + ): number { 112 + return Math.ceil( 113 + (card.getBoundingClientRect().height + rowGap) / (rowHeight + rowGap), 114 + ); 115 + }
+15
src/components/profile-section.tsx
··· 1 + import type { ReactNode } from "react"; 2 + 3 + type ProfileSectionProps = { 4 + children: ReactNode; 5 + title: string; 6 + }; 7 + 8 + export function ProfileSection({ title, children }: ProfileSectionProps) { 9 + return ( 10 + <section className="profile-card border"> 11 + <h2 className="sr-only">{title}</h2> 12 + {children} 13 + </section> 14 + ); 15 + }
+85
src/components/profile-sections.tsx
··· 1 + import type { ReactNode } from "react"; 2 + import { useEffect, useState } from "react"; 3 + 4 + import { GrainPhotos } from "~/components/grain-photos"; 5 + import { ProfileSection } from "~/components/profile-section"; 6 + import { RockskyScrobbleList } from "~/components/rocksky-scrobble-list"; 7 + import { StandardDocuments } from "~/components/standard-documents"; 8 + import { TangledRepoList } from "~/components/tangled-repo-list"; 9 + import { getDidSectionOrder } from "~/lib/layout/did-section-order"; 10 + import { getRecentGrainPhotos } from "~/lib/providers/grain-photo"; 11 + import { getRecentRocksky } from "~/lib/providers/rocksky"; 12 + import { getMostStarredTangledRepos } from "~/lib/providers/tangled"; 13 + import { getRecentStandardDocuments } from "~/lib/providers/standard-site"; 14 + 15 + type ProfileSectionsProps = { 16 + did: string; 17 + handle: string | null; 18 + pds: string; 19 + }; 20 + 21 + type ProfileData = { 22 + photos: Awaited<ReturnType<typeof getRecentGrainPhotos>>; 23 + scrobbles: Awaited<ReturnType<typeof getRecentRocksky>>; 24 + documents: Awaited<ReturnType<typeof getRecentStandardDocuments>>; 25 + tangledRepos: Awaited<ReturnType<typeof getMostStarredTangledRepos>>; 26 + }; 27 + 28 + export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) { 29 + const [data, setData] = useState<ProfileData | null>(null); 30 + 31 + useEffect(() => { 32 + let isCurrent = true; 33 + 34 + async function loadProfileData() { 35 + const [photos, scrobbles, documents, tangledRepos] = await Promise.all([ 36 + getRecentGrainPhotos(pds, did), 37 + getRecentRocksky(pds, did), 38 + getRecentStandardDocuments(pds, did, handle), 39 + getMostStarredTangledRepos(pds, did, handle), 40 + ]); 41 + 42 + if (isCurrent) { 43 + setData({ photos, scrobbles, documents, tangledRepos }); 44 + } 45 + } 46 + 47 + void loadProfileData(); 48 + 49 + return () => { 50 + isCurrent = false; 51 + }; 52 + }, [did, handle, pds]); 53 + 54 + if (!data) { 55 + return ( 56 + <ProfileSection title="Gathering signals"> 57 + <p className="text-sm">Searching the Atmosphere…</p> 58 + </ProfileSection> 59 + ); 60 + } 61 + 62 + const sections: ReactNode[] = []; 63 + 64 + if (data.scrobbles.length > 0) { 65 + sections.push( 66 + <RockskyScrobbleList key="rocksky" scrobbles={data.scrobbles} />, 67 + ); 68 + } 69 + 70 + if (data.tangledRepos.length > 0) { 71 + sections.push(<TangledRepoList key="tangled" repos={data.tangledRepos} />); 72 + } 73 + 74 + if (data.documents.length > 0) { 75 + sections.push( 76 + <StandardDocuments key="standard" documents={data.documents} />, 77 + ); 78 + } 79 + 80 + if (data.photos.length > 0) { 81 + sections.push(<GrainPhotos key="grain" photos={data.photos} />); 82 + } 83 + 84 + return <>{getDidSectionOrder(did, sections)}</>; 85 + }
+86
src/components/rocksky-scrobble-list.tsx
··· 1 + import type { RockskyScrobbleRecord } from "~/lib/providers/rocksky"; 2 + import { ProfileSection } from "~/components/profile-section"; 3 + 4 + type RockskyScrobbleListProps = { 5 + scrobbles: RockskyScrobbleRecord[]; 6 + }; 7 + 8 + type RockskyScrobbleProps = Pick< 9 + RockskyScrobbleRecord, 10 + "album" | "albumArtUrl" | "artist" | "createdAt" | "spotifyLink" | "title" 11 + >; 12 + 13 + export function RockskyScrobbleList({ scrobbles }: RockskyScrobbleListProps) { 14 + return ( 15 + <ProfileSection title="Music"> 16 + <div className="max-w-3xl"> 17 + {scrobbles.map((scrobble) => ( 18 + <RockskyScrobble 19 + key={`${scrobble.createdAt}-${scrobble.mbid}`} 20 + album={scrobble.album} 21 + albumArtUrl={scrobble.albumArtUrl} 22 + artist={scrobble.artist} 23 + createdAt={scrobble.createdAt} 24 + spotifyLink={scrobble.spotifyLink} 25 + title={scrobble.title} 26 + /> 27 + ))} 28 + </div> 29 + </ProfileSection> 30 + ); 31 + } 32 + 33 + function formatRelativeTime(createdAt: string): string { 34 + const elapsedMinutes = Math.round( 35 + (Date.now() - new Date(createdAt).getTime()) / 60000, 36 + ); 37 + 38 + if (elapsedMinutes < 1) return "just now"; 39 + if (elapsedMinutes < 60) return `${elapsedMinutes} min ago`; 40 + 41 + const elapsedHours = Math.round(elapsedMinutes / 60); 42 + if (elapsedHours < 24) return `${elapsedHours} hr ago`; 43 + 44 + return `${Math.round(elapsedHours / 24)} days ago`; 45 + } 46 + 47 + function RockskyScrobble({ 48 + title, 49 + artist, 50 + album, 51 + createdAt, 52 + spotifyLink, 53 + albumArtUrl, 54 + }: RockskyScrobbleProps) { 55 + return ( 56 + <a 57 + href={spotifyLink} 58 + target="_blank" 59 + rel="noreferrer" 60 + className="profile-external-link profile-divider grid grid-cols-[3rem_1fr_auto] items-center gap-3 border-b py-2.5" 61 + > 62 + {albumArtUrl ? ( 63 + <img 64 + src={albumArtUrl} 65 + alt={`${album} album art`} 66 + width={40} 67 + height={40} 68 + className="size-10 object-cover" 69 + /> 70 + ) : ( 71 + <div className="size-10 border" /> 72 + )} 73 + 74 + <div className="min-w-0"> 75 + <h3 className="profile-item-title">{title}</h3> 76 + <p className="profile-secondary mt-0.5"> 77 + {artist} · {album} 78 + </p> 79 + </div> 80 + 81 + <span className="profile-metadata whitespace-nowrap"> 82 + {formatRelativeTime(createdAt)} 83 + </span> 84 + </a> 85 + ); 86 + }
+53
src/components/standard-documents.tsx
··· 1 + import type { StandardDocument } from "~/lib/providers/standard-site"; 2 + import { ProfileSection } from "~/components/profile-section"; 3 + type StandardDocumentsProps = { 4 + documents: StandardDocument[]; 5 + }; 6 + 7 + export function StandardDocuments({ documents }: StandardDocumentsProps) { 8 + return ( 9 + <ProfileSection title="Writing"> 10 + <div> 11 + {documents.map((document) => ( 12 + <article 13 + key={`${document.path}-${document.publishedAt}`} 14 + className="profile-divider border-b py-3 last:border-b-0" 15 + > 16 + <h3 className="profile-item-title"> 17 + {document.link ? ( 18 + <a 19 + className="profile-link" 20 + href={document.link} 21 + target="_blank" 22 + rel="noreferrer" 23 + > 24 + {document.title} 25 + </a> 26 + ) : ( 27 + document.title 28 + )} 29 + </h3> 30 + 31 + {document.description && ( 32 + <p className="profile-secondary mt-1">{document.description}</p> 33 + )} 34 + 35 + <div className="profile-metadata mt-2"> 36 + {document.publishedAt && ( 37 + <time dateTime={document.publishedAt}> 38 + {formatPublishedAt(document.publishedAt)} 39 + </time> 40 + )} 41 + </div> 42 + </article> 43 + ))} 44 + </div> 45 + </ProfileSection> 46 + ); 47 + } 48 + 49 + function formatPublishedAt(publishedAt: string): string { 50 + return new Intl.DateTimeFormat("en-US", { 51 + dateStyle: "medium", 52 + }).format(new Date(publishedAt)); 53 + }
+72
src/components/tangled-repo-list.tsx
··· 1 + import type { ReactNode } from "react"; 2 + 3 + import type { TangledRepo } from "~/lib/providers/tangled"; 4 + import { ProfileSection } from "~/components/profile-section"; 5 + 6 + type TangledRepoListProps = { 7 + repos: TangledRepo[]; 8 + }; 9 + 10 + export function TangledRepoList({ repos }: TangledRepoListProps) { 11 + return ( 12 + <ProfileSection title="Code"> 13 + <div> 14 + {repos.map((repo) => ( 15 + <article 16 + key={repo.repoDid} 17 + className="profile-divider border-b py-3 last:border-b-0" 18 + > 19 + <div className="flex items-center justify-between gap-3"> 20 + <h3 className="profile-item-title"> 21 + <a 22 + className="profile-link" 23 + href={repo.link} 24 + target="_blank" 25 + rel="noreferrer" 26 + > 27 + {repo.name} 28 + </a> 29 + </h3> 30 + <span className="profile-metadata shrink-0 opacity-100"> 31 + ⭐ {repo.stars} 32 + </span> 33 + </div> 34 + <p className="profile-secondary mt-1"> 35 + {renderMarkdownLinks(repo.description ?? "No description")} 36 + </p> 37 + </article> 38 + ))} 39 + </div> 40 + </ProfileSection> 41 + ); 42 + } 43 + 44 + function renderMarkdownLinks(text: string): ReactNode[] { 45 + const linkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g; 46 + const renderedParts: ReactNode[] = []; 47 + let currentIndex = 0; 48 + 49 + for (const match of text.matchAll(linkPattern)) { 50 + const matchIndex = match.index ?? currentIndex; 51 + const linkText = match[1]; 52 + const linkUrl = match[2]; 53 + 54 + renderedParts.push(text.slice(currentIndex, matchIndex)); 55 + renderedParts.push( 56 + <a 57 + className="profile-link" 58 + key={`${linkUrl}-${matchIndex}`} 59 + href={linkUrl} 60 + target="_blank" 61 + rel="noreferrer" 62 + > 63 + {linkText} 64 + </a>, 65 + ); 66 + currentIndex = matchIndex + match[0].length; 67 + } 68 + 69 + renderedParts.push(text.slice(currentIndex)); 70 + 71 + return renderedParts; 72 + }
+57
src/lib/atproto/did.ts
··· 1 + export type AtprotoDidDocument = { 2 + "@context": string[]; 3 + id: `did:${string}`; 4 + alsoKnownAs: string[]; 5 + verificationMethod: AtprotoVerificationMethod[]; 6 + service: AtprotoService[]; 7 + }; 8 + 9 + type AtprotoVerificationMethod = { 10 + id: string; 11 + type: "Multikey"; 12 + controller: `did:${string}`; 13 + publicKeyMultibase: string; 14 + }; 15 + 16 + type AtprotoService = { 17 + id: string; 18 + type: "AtprotoPersonalDataServer"; 19 + serviceEndpoint: string; 20 + }; 21 + 22 + export async function resolveDidDocument( 23 + did: string, 24 + ): Promise<AtprotoDidDocument> { 25 + const response = await fetch(getDidDocumentUrl(did)); 26 + 27 + if (!response.ok) { 28 + throw new Error(`Failed to resolve DID: ${did}`); 29 + } 30 + 31 + return (await response.json()) as AtprotoDidDocument; 32 + } 33 + 34 + function getDidDocumentUrl(did: string): string { 35 + if (did.startsWith("did:plc:")) { 36 + return `https://plc.directory/${encodeURIComponent(did)}`; 37 + } 38 + 39 + if (did.startsWith("did:web:")) { 40 + const [, , identifier, ...pathParts] = did.split(":"); 41 + 42 + if (!identifier) { 43 + throw new Error(`Invalid did:web identifier: ${did}`); 44 + } 45 + 46 + const hostname = decodeURIComponent(identifier); 47 + 48 + if (pathParts.length === 0) { 49 + return `https://${hostname}/.well-known/did.json`; 50 + } 51 + 52 + const path = pathParts.map(decodeURIComponent).join("/"); 53 + return `https://${hostname}/${path}/did.json`; 54 + } 55 + 56 + throw new Error(`Unsupported DID method: ${did}`); 57 + }
+36
src/lib/atproto/mini-doc.ts
··· 1 + const SLINGSHOT_URL = "https://slingshot.microcosm.blue"; 2 + 3 + export type AtprotoMiniIdentity = { 4 + did: string; 5 + handle: string | null; 6 + pds: string; 7 + }; 8 + 9 + type MiniDocResponse = { 10 + did: string; 11 + handle: string; 12 + pds: string; 13 + }; 14 + 15 + export async function resolveMiniDoc( 16 + identifier: string, 17 + signal?: AbortSignal, 18 + ): Promise<AtprotoMiniIdentity> { 19 + const params = new URLSearchParams({ identifier }); 20 + const response = await fetch( 21 + `${SLINGSHOT_URL}/xrpc/blue.microcosm.identity.resolveMiniDoc?${params}`, 22 + { signal }, 23 + ); 24 + 25 + if (!response.ok) { 26 + throw new Error(`Could not resolve identifier: ${identifier}`); 27 + } 28 + 29 + const data = (await response.json()) as MiniDocResponse; 30 + 31 + return { 32 + did: data.did, 33 + handle: data.handle === "handle.invalid" ? null : data.handle, 34 + pds: data.pds, 35 + }; 36 + }
+20
src/lib/did-random.ts
··· 1 + /** Creates a repeatable numeric seed from arbitrary DID-derived input. */ 2 + export function hashString(value: string): number { 3 + let hash = 0; 4 + 5 + for (const character of value) { 6 + hash = (hash * 31 + (character.codePointAt(0) ?? 0)) >>> 0; 7 + } 8 + 9 + return hash; 10 + } 11 + 12 + /** Returns deterministic pseudo-random values for visual generation. */ 13 + export function createSeededRandom(seed: number): () => number { 14 + let state = seed || 1; 15 + 16 + return () => { 17 + state = (Math.imul(1664525, state) + 1013904223) >>> 0; 18 + return state / 2 ** 32; 19 + }; 20 + }
+17
src/lib/layout/did-section-order.ts
··· 1 + import { createSeededRandom, hashString } from "~/lib/did-random"; 2 + 3 + /** Returns a deterministically shuffled copy of a list for DID-based layouts. */ 4 + export function getDidSectionOrder<T>(did: string, sections: T[]): T[] { 5 + const random = createSeededRandom(hashString(`${did}:sections`)); 6 + const orderedSections = [...sections]; 7 + 8 + for (let index = orderedSections.length - 1; index > 0; index -= 1) { 9 + const swapIndex = Math.floor(random() * (index + 1)); 10 + const currentSection = orderedSections[index]; 11 + 12 + orderedSections[index] = orderedSections[swapIndex]!; 13 + orderedSections[swapIndex] = currentSection!; 14 + } 15 + 16 + return orderedSections; 17 + }
+70
src/lib/providers/bluesky.ts
··· 1 + export type BlueskyProfile = { 2 + displayName?: string; 3 + description?: string; 4 + pronouns?: string; 5 + website?: string; 6 + avatarUrl?: string; 7 + avatarFallbackUrl?: string; 8 + createdAt?: string; 9 + }; 10 + 11 + type BlueskyProfileRecord = { 12 + displayName?: string; 13 + description?: string; 14 + pronouns?: string; 15 + website?: string; 16 + avatar?: BlueskyBlob; 17 + createdAt?: string; 18 + }; 19 + 20 + type BlueskyBlob = { 21 + ref?: { 22 + $link?: string; 23 + }; 24 + mimeType?: string; 25 + }; 26 + 27 + type GetRecordResponse = { 28 + value: BlueskyProfileRecord; 29 + }; 30 + 31 + export async function getBlueskyProfile( 32 + pds: string, 33 + did: string, 34 + ): Promise<BlueskyProfile | null> { 35 + try { 36 + const params = new URLSearchParams({ 37 + repo: did, 38 + collection: "app.bsky.actor.profile", 39 + rkey: "self", 40 + }); 41 + const response = await fetch( 42 + `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.getRecord?${params}`, 43 + { cache: "no-store" }, 44 + ); 45 + 46 + if (!response.ok) { 47 + return null; 48 + } 49 + 50 + const data = (await response.json()) as GetRecordResponse; 51 + const profile = data.value; 52 + const avatarCid = profile.avatar?.ref?.$link; 53 + 54 + return { 55 + displayName: profile.displayName, 56 + description: profile.description, 57 + pronouns: profile.pronouns, 58 + website: profile.website, 59 + createdAt: profile.createdAt, 60 + avatarUrl: avatarCid 61 + ? `https://cdn.bsky.app/img/avatar/plain/${did}/${avatarCid}@webp` 62 + : undefined, 63 + avatarFallbackUrl: avatarCid 64 + ? `${pds.replace(/\/$/, "")}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(avatarCid)}` 65 + : undefined, 66 + }; 67 + } catch { 68 + return null; 69 + } 70 + }
+140
src/lib/providers/grain-photo.ts
··· 1 + export type GrainPhoto = { 2 + createdAt: string; 3 + aspectRatio: { 4 + width: number; 5 + height: number; 6 + }; 7 + imageUrl: string; 8 + postUrl?: string; 9 + }; 10 + 11 + type GrainPhotoRecord = { 12 + createdAt: string; 13 + aspectRatio: { 14 + width: number; 15 + height: number; 16 + }; 17 + photo?: { 18 + ref?: { 19 + $link?: string; 20 + }; 21 + mimeType?: string; 22 + }; 23 + }; 24 + 25 + type GrainGalleryItemRecord = { 26 + gallery?: string; 27 + item?: string; 28 + createdAt?: string; 29 + }; 30 + 31 + type ListRecordsResponse<T> = { 32 + records: Array<{ 33 + uri: string; 34 + value: T; 35 + }>; 36 + }; 37 + 38 + const PHOTO_COLLECTION = "social.grain.photo"; 39 + const GALLERY_ITEM_COLLECTION = "social.grain.gallery.item"; 40 + const MAX_PHOTO_COUNT = 3; 41 + 42 + export async function getRecentGrainPhotos( 43 + pds: string, 44 + did: string, 45 + ): Promise<GrainPhoto[]> { 46 + const [photoRecords, galleryItems] = await Promise.all([ 47 + listRecords<GrainPhotoRecord>(pds, did, PHOTO_COLLECTION), 48 + listRecords<GrainGalleryItemRecord>(pds, did, GALLERY_ITEM_COLLECTION), 49 + ]); 50 + const galleryByPhotoUri = createGalleryLookup(galleryItems); 51 + 52 + return photoRecords 53 + .map(({ uri, value }) => 54 + createGrainPhoto(uri, value, pds, did, galleryByPhotoUri), 55 + ) 56 + .filter((photo): photo is GrainPhoto => photo !== null) 57 + .sort( 58 + (firstPhoto, secondPhoto) => 59 + Date.parse(secondPhoto.createdAt) - Date.parse(firstPhoto.createdAt), 60 + ) 61 + .slice(0, MAX_PHOTO_COUNT); 62 + } 63 + 64 + async function listRecords<T>( 65 + pds: string, 66 + did: string, 67 + collection: string, 68 + ): Promise<Array<{ uri: string; value: T }>> { 69 + try { 70 + const params = new URLSearchParams({ 71 + repo: did, 72 + collection, 73 + limit: "100", 74 + }); 75 + const response = await fetch( 76 + `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.listRecords?${params}`, 77 + { cache: "no-store" }, 78 + ); 79 + 80 + if (!response.ok) { 81 + return []; 82 + } 83 + 84 + const data = (await response.json()) as ListRecordsResponse<T>; 85 + 86 + return data.records; 87 + } catch { 88 + return []; 89 + } 90 + } 91 + 92 + function createGalleryLookup( 93 + galleryItems: Array<{ uri: string; value: GrainGalleryItemRecord }>, 94 + ): Map<string, string> { 95 + return new Map( 96 + galleryItems.flatMap(({ value }) => { 97 + if (!value.item || !value.gallery) { 98 + return []; 99 + } 100 + 101 + return [[value.item, value.gallery]] as [string, string][]; 102 + }), 103 + ); 104 + } 105 + 106 + function createGrainPhoto( 107 + uri: string, 108 + value: GrainPhotoRecord, 109 + pds: string, 110 + did: string, 111 + galleryByPhotoUri: Map<string, string>, 112 + ): GrainPhoto | null { 113 + const cid = value.photo?.ref?.$link; 114 + 115 + if (!cid) { 116 + return null; 117 + } 118 + 119 + const galleryUri = galleryByPhotoUri.get(uri); 120 + const galleryRkey = galleryUri?.split("/").pop(); 121 + 122 + return { 123 + createdAt: value.createdAt, 124 + aspectRatio: value.aspectRatio, 125 + imageUrl: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}${getImageSuffix(value.photo?.mimeType)}`, 126 + postUrl: galleryRkey 127 + ? `https://grain.social/profile/${did}/gallery/${galleryRkey}` 128 + : undefined, 129 + }; 130 + } 131 + 132 + function getImageSuffix(mimeType: string | undefined): string { 133 + if (!mimeType) { 134 + return ""; 135 + } 136 + 137 + const format = mimeType.split("/")[1]; 138 + 139 + return format ? `@${format}` : ""; 140 + }
+63
src/lib/providers/rocksky.ts
··· 1 + export type RockskyScrobbleRecord = { 2 + $type: "app.rocksky.scrobble"; 3 + album: string; 4 + albumArtUrl: string; 5 + albumArtist: string; 6 + artist: string; 7 + artists: RockskyArtist[]; 8 + createdAt: string; 9 + discNumber: number; 10 + duration: number; 11 + isrc: string; 12 + mbid: string; 13 + releaseDate: string; 14 + spotifyLink: string; 15 + tags: string[]; 16 + title: string; 17 + trackNumber: number; 18 + year: number; 19 + }; 20 + 21 + export type RockskyArtist = { 22 + mbid: string; 23 + name: string; 24 + }; 25 + 26 + type ListRecordsResponse = { 27 + records: Array<{ 28 + uri: string; 29 + cid: string; 30 + value: RockskyScrobbleRecord; 31 + }>; 32 + cursor?: string; 33 + }; 34 + 35 + export async function getRecentRocksky( 36 + pds: string, 37 + did: string, 38 + limit = 4, 39 + ): Promise<RockskyScrobbleRecord[]> { 40 + try { 41 + const params = new URLSearchParams({ 42 + repo: did, 43 + collection: "app.rocksky.scrobble", 44 + limit: String(limit), 45 + reverse: "false", 46 + }); 47 + 48 + const response = await fetch( 49 + `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.listRecords?${params}`, 50 + { cache: "no-store" }, 51 + ); 52 + 53 + if (!response.ok) { 54 + return []; 55 + } 56 + 57 + const data = (await response.json()) as ListRecordsResponse; 58 + 59 + return data.records.map(({ value }) => value); 60 + } catch { 61 + return []; 62 + } 63 + }
+183
src/lib/providers/standard-site.ts
··· 1 + import { resolveDidDocument } from "~/lib/atproto/did"; 2 + 3 + export type StandardDocument = { 4 + title: string; 5 + description?: string; 6 + path?: string; 7 + publishedAt?: string; 8 + tags?: string[]; 9 + link?: string; 10 + }; 11 + 12 + type StandardDocumentRecord = Omit<StandardDocument, "link"> & { 13 + site?: string; 14 + textContent?: string; 15 + }; 16 + 17 + type StandardPublicationRecord = { 18 + url?: string; 19 + }; 20 + 21 + type ListRecordsResponse = { 22 + records: Array<{ 23 + uri: string; 24 + value: StandardDocumentRecord; 25 + }>; 26 + }; 27 + 28 + const DOCUMENT_COLLECTION = "site.standard.document"; 29 + const PUBLICATION_COLLECTION = "site.standard.publication"; 30 + const MAX_DOCUMENT_COUNT = 3; 31 + 32 + export async function getRecentStandardDocuments( 33 + pds: string, 34 + did: string, 35 + handle: string | null, 36 + ): Promise<StandardDocument[]> { 37 + const documents = await listStandardDocuments(pds, did); 38 + 39 + const recentDocuments = documents 40 + .sort(compareByPublishedAt) 41 + .slice(0, MAX_DOCUMENT_COUNT); 42 + 43 + return Promise.all( 44 + recentDocuments.map(async (document) => { 45 + const publicationUrl = await getPublicationUrl(document.site); 46 + 47 + return { 48 + ...document, 49 + link: 50 + getDocumentLink(document.path, publicationUrl) ?? 51 + getDocumentLink(document.path, handle), 52 + }; 53 + }), 54 + ); 55 + } 56 + 57 + async function listStandardDocuments( 58 + pds: string, 59 + did: string, 60 + ): Promise<StandardDocumentRecord[]> { 61 + try { 62 + const params = new URLSearchParams({ 63 + repo: did, 64 + collection: DOCUMENT_COLLECTION, 65 + limit: "100", 66 + }); 67 + const response = await fetch( 68 + `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.listRecords?${params}`, 69 + { cache: "no-store" }, 70 + ); 71 + 72 + if (!response.ok) { 73 + return []; 74 + } 75 + 76 + const data = (await response.json()) as ListRecordsResponse; 77 + 78 + return data.records.map(({ value }) => value); 79 + } catch { 80 + return []; 81 + } 82 + } 83 + 84 + function compareByPublishedAt( 85 + firstDocument: StandardDocumentRecord, 86 + secondDocument: StandardDocumentRecord, 87 + ): number { 88 + return ( 89 + getTimestamp(secondDocument.publishedAt) - 90 + getTimestamp(firstDocument.publishedAt) 91 + ); 92 + } 93 + 94 + function getTimestamp(dateString: string | undefined): number { 95 + if (!dateString) { 96 + return 0; 97 + } 98 + 99 + const timestamp = Date.parse(dateString); 100 + 101 + return Number.isNaN(timestamp) ? 0 : timestamp; 102 + } 103 + 104 + function getDocumentLink( 105 + path: string | undefined, 106 + baseUrl: string | null | undefined, 107 + ): string | undefined { 108 + if (!path?.startsWith("/") || !baseUrl) { 109 + return undefined; 110 + } 111 + 112 + return `${baseUrl.replace(/\/$/, "")}${path}`; 113 + } 114 + 115 + async function getPublicationUrl( 116 + publicationUri: string | undefined, 117 + ): Promise<string | undefined> { 118 + if (!publicationUri) { 119 + return undefined; 120 + } 121 + 122 + if (publicationUri.startsWith("https://")) { 123 + return publicationUri.replace(/\/$/, ""); 124 + } 125 + 126 + const publicationReference = parsePublicationUri(publicationUri); 127 + if (!publicationReference) { 128 + return undefined; 129 + } 130 + 131 + try { 132 + const document = await resolveDidDocument(publicationReference.did); 133 + const pds = document.service.find( 134 + (service) => 135 + service.type === "AtprotoPersonalDataServer" && 136 + service.id.endsWith("#atproto_pds"), 137 + )?.serviceEndpoint; 138 + 139 + if (!pds) { 140 + return undefined; 141 + } 142 + 143 + const params = new URLSearchParams({ 144 + repo: publicationReference.did, 145 + collection: PUBLICATION_COLLECTION, 146 + rkey: publicationReference.rkey, 147 + }); 148 + const response = await fetch( 149 + `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.getRecord?${params}`, 150 + { cache: "no-store" }, 151 + ); 152 + 153 + if (!response.ok) { 154 + return undefined; 155 + } 156 + 157 + const data = (await response.json()) as { 158 + value?: StandardPublicationRecord; 159 + }; 160 + return data.value?.url?.replace(/\/$/, ""); 161 + } catch { 162 + return undefined; 163 + } 164 + } 165 + 166 + function parsePublicationUri( 167 + publicationUri: string, 168 + ): { did: string; rkey: string } | undefined { 169 + const match = /^at:\/\/([^/]+)\/site\.standard\.publication\/([^/]+)$/.exec( 170 + publicationUri, 171 + ); 172 + 173 + if (!match) { 174 + return undefined; 175 + } 176 + 177 + const [, did, rkey] = match; 178 + if (!did || !rkey) { 179 + return undefined; 180 + } 181 + 182 + return { did, rkey }; 183 + }
+120
src/lib/providers/tangled.ts
··· 1 + const CONSTELLATION_URL = "https://constellation.microcosm.blue"; 2 + 3 + export type TangledRepoRecord = { 4 + repoDid: string; 5 + name?: string; 6 + description?: string; 7 + website?: string; 8 + source?: string; 9 + topics?: string[]; 10 + }; 11 + 12 + type TangledRepoRecordWithRkey = TangledRepoRecord & { 13 + rkey: string; 14 + }; 15 + 16 + export type TangledRepo = TangledRepoRecordWithRkey & { 17 + name: string; 18 + stars: number; 19 + link: string; 20 + }; 21 + 22 + const STAR_SOURCE = "sh.tangled.feed.star:subject.did"; 23 + const TANGLED_REPO_COLLECTION = "sh.tangled.repo"; 24 + 25 + type ListRecordsResponse = { 26 + records: Array<{ 27 + uri: string; 28 + cid: string; 29 + value: TangledRepoRecord; 30 + }>; 31 + }; 32 + 33 + export async function getMostStarredTangledRepos( 34 + pds: string, 35 + did: string, 36 + handle: string | null, 37 + limit = 4, 38 + ): Promise<TangledRepo[]> { 39 + const records = await listTangledRepos(pds, did); 40 + 41 + const uniqueRecords = [ 42 + ...new Map(records.map((record) => [record.repoDid, record])).values(), 43 + ]; 44 + 45 + const repos = await Promise.all( 46 + uniqueRecords.map(async (record) => { 47 + const stars = await getTangledStars(record.repoDid); 48 + const owner = handle ?? did; 49 + const name = record.name ?? record.rkey; 50 + 51 + return { 52 + ...record, 53 + name, 54 + stars, 55 + link: 56 + record.website ?? 57 + record.source ?? 58 + `https://tangled.org/${owner}/${name}`, 59 + }; 60 + }), 61 + ); 62 + 63 + return repos 64 + .sort((firstRepo, secondRepo) => secondRepo.stars - firstRepo.stars) 65 + .slice(0, limit); 66 + } 67 + 68 + async function listTangledRepos( 69 + pds: string, 70 + did: string, 71 + ): Promise<TangledRepoRecordWithRkey[]> { 72 + try { 73 + const params = new URLSearchParams({ 74 + repo: did, 75 + collection: TANGLED_REPO_COLLECTION, 76 + limit: "20", 77 + }); 78 + const response = await fetch( 79 + `${pds.replace(/\/$/, "")}/xrpc/com.atproto.repo.listRecords?${params}`, 80 + { cache: "no-store" }, 81 + ); 82 + 83 + if (!response.ok) { 84 + return []; 85 + } 86 + 87 + const data = (await response.json()) as ListRecordsResponse; 88 + 89 + return data.records.map(({ uri, value }) => ({ 90 + ...value, 91 + rkey: uri.split("/").pop() ?? "unknown-repository", 92 + })); 93 + } catch { 94 + return []; 95 + } 96 + } 97 + 98 + async function getTangledStars(repoDid: string): Promise<number> { 99 + try { 100 + const params = new URLSearchParams({ 101 + subject: repoDid, 102 + source: STAR_SOURCE, 103 + limit: "1", 104 + }); 105 + const response = await fetch( 106 + `${CONSTELLATION_URL}/xrpc/blue.microcosm.links.getBacklinks?${params}`, 107 + { cache: "no-store" }, 108 + ); 109 + 110 + if (!response.ok) { 111 + return 0; 112 + } 113 + 114 + const data = (await response.json()) as { total?: number }; 115 + 116 + return data.total ?? 0; 117 + } catch { 118 + return 0; 119 + } 120 + }
+45
src/lib/theme/did-atmosphere.ts
··· 1 + import { createSeededRandom, hashString } from "~/lib/did-random"; 2 + 3 + export type DidAtmosphere = { 4 + backgroundImage: string; 5 + }; 6 + 7 + /** Builds a stable, CSS-only space backdrop from a DID. */ 8 + export function getDidAtmosphere(did: string): DidAtmosphere { 9 + const random = createSeededRandom(hashString(`${did}:atmosphere`)); 10 + const hue = random() * 360; 11 + const secondHue = (hue + 80 + random() * 100) % 360; 12 + const firstGlow = createGlowPosition(random); 13 + const secondGlow = createGlowPosition(random); 14 + const stars = createStars(random); 15 + 16 + return { 17 + backgroundImage: [ 18 + `radial-gradient(circle at ${firstGlow.x}% ${firstGlow.y}%, hsl(${hue} 80% 65% / 0.18), transparent ${firstGlow.size}%)`, 19 + `radial-gradient(circle at ${secondGlow.x}% ${secondGlow.y}%, hsl(${secondHue} 80% 65% / 0.14), transparent ${secondGlow.size}%)`, 20 + ...stars, 21 + ].join(",\n"), 22 + }; 23 + } 24 + 25 + function createGlowPosition(random: () => number): { 26 + x: number; 27 + y: number; 28 + size: number; 29 + } { 30 + return { 31 + x: Math.round(random() * 100), 32 + y: Math.round(random() * 100), 33 + size: Math.round(24 + random() * 20), 34 + }; 35 + } 36 + 37 + function createStars(random: () => number): string[] { 38 + return Array.from({ length: 14 }, () => { 39 + const x = Math.round(random() * 100); 40 + const y = Math.round(random() * 100); 41 + const opacity = (0.25 + random() * 0.5).toFixed(2); 42 + 43 + return `radial-gradient(circle at ${x}% ${y}%, hsl(0 0% 100% / ${opacity}) 0 1px, transparent 1.5px)`; 44 + }); 45 + }
+10
src/lib/theme/did-card-radius.ts
··· 1 + import { hashString } from "~/lib/did-random"; 2 + 3 + const MAX_CARD_RADIUS_PX = 24; 4 + 5 + /** Returns a stable, deliberately modest card radius for a DID. */ 6 + export function getDidCardRadius(did: string): string { 7 + const radius = hashString(`${did}:card-radius`) % (MAX_CARD_RADIUS_PX + 1); 8 + 9 + return `${radius}px`; 10 + }
+179
src/lib/theme/did-colors.ts
··· 1 + import { createSeededRandom, hashString } from "~/lib/did-random"; 2 + 3 + // The DTCG schema makes the token output consumable by design-token tooling. 4 + const DTCG_SCHEMA_URL = 5 + "https://www.designtokens.org/schemas/2025.10/format.json"; 6 + 7 + /** CSS-ready semantic colors for a dark profile theme. */ 8 + export type DidColorScheme = { 9 + background: string; 10 + card: string; 11 + border: string; 12 + accent: string; 13 + }; 14 + 15 + /** A minimal DTCG color-token document for platform-specific exporters. */ 16 + export type DidDesignTokens = { 17 + $schema: string; 18 + color: { 19 + canvas: DidColorToken; 20 + surface: DidColorToken; 21 + divider: DidColorToken; 22 + accent: DidColorToken; 23 + }; 24 + }; 25 + 26 + type DidColorToken = { 27 + $type: "color"; 28 + $value: { 29 + colorSpace: "srgb"; 30 + components: [number, number, number]; 31 + hex: string; 32 + }; 33 + }; 34 + 35 + type HslColor = { 36 + hue: number; 37 + saturation: number; 38 + lightness: number; 39 + }; 40 + 41 + type GeneratedColors = { 42 + canvas: HslColor; 43 + surface: HslColor; 44 + divider: HslColor; 45 + accent: HslColor; 46 + }; 47 + 48 + export type DidTheme = { 49 + css: DidColorScheme; 50 + tokens: DidDesignTokens; 51 + }; 52 + 53 + /** 54 + * Creates a stable dark-pastel theme from an AT Protocol DID. 55 + * 56 + * The DID seeds the generator, so the output is varied across identities but 57 + * remains stable across requests. Consumers can use `css` directly or pass 58 + * `tokens` to a DTCG-compatible exporter. 59 + */ 60 + export function getDidTheme(did: string): DidTheme { 61 + const colors = generateColors(did); 62 + const css = { 63 + background: formatHsl(colors.canvas), 64 + card: formatHsl(colors.surface), 65 + border: formatHsl(colors.divider), 66 + accent: formatHsl(colors.accent), 67 + }; 68 + 69 + return { 70 + css, 71 + tokens: { 72 + $schema: DTCG_SCHEMA_URL, 73 + color: { 74 + canvas: createColorToken(colors.canvas), 75 + surface: createColorToken(colors.surface), 76 + divider: createColorToken(colors.divider), 77 + accent: createColorToken(colors.accent), 78 + }, 79 + }, 80 + }; 81 + } 82 + 83 + /** Returns only the CSS colors for consumers that do not need token metadata. */ 84 + export function getDidColorScheme(did: string): DidColorScheme { 85 + return getDidTheme(did).css; 86 + } 87 + 88 + function generateColors(did: string): GeneratedColors { 89 + // Keep every random value inside a hand-tuned range so arbitrary hues stay 90 + // readable as dark backgrounds and pleasant as pastel accents. 91 + const random = createSeededRandom(hashString(did)); 92 + const hue = random() * 360; 93 + const backgroundSaturation = 20 + random() * 12; 94 + const backgroundLightness = 11 + random() * 6; 95 + const cardSaturation = backgroundSaturation + random() * 4; 96 + const cardLightness = backgroundLightness + 4 + random() * 3; 97 + 98 + return { 99 + canvas: { 100 + hue, 101 + saturation: backgroundSaturation, 102 + lightness: backgroundLightness, 103 + }, 104 + surface: { 105 + hue, 106 + saturation: cardSaturation, 107 + lightness: cardLightness, 108 + }, 109 + divider: { 110 + hue, 111 + saturation: 35 + random() * 20, 112 + lightness: 35 + random() * 12, 113 + }, 114 + accent: { 115 + hue, 116 + saturation: 65 + random() * 20, 117 + lightness: 76 + random() * 10, 118 + }, 119 + }; 120 + } 121 + 122 + function createColorToken(color: HslColor): DidColorToken { 123 + const components = hslToRgb(color); 124 + 125 + return { 126 + $type: "color", 127 + $value: { 128 + colorSpace: "srgb", 129 + components, 130 + hex: rgbToHex(components), 131 + }, 132 + }; 133 + } 134 + 135 + function formatHsl(color: HslColor): string { 136 + return `hsl(${Math.round(color.hue)} ${Math.round(color.saturation)}% ${Math.round(color.lightness)}%)`; 137 + } 138 + 139 + function hslToRgb(color: HslColor): [number, number, number] { 140 + // DTCG tokens use normalized sRGB components, while the browser theme uses 141 + // HSL because it makes the generated lightness and saturation easy to tune. 142 + const saturation = color.saturation / 100; 143 + const lightness = color.lightness / 100; 144 + const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation; 145 + const huePart = color.hue / 60; 146 + const secondComponent = chroma * (1 - Math.abs((huePart % 2) - 1)); 147 + const lightnessAdjustment = lightness - chroma / 2; 148 + const rgbByHue = getRgbByHue(huePart, chroma, secondComponent); 149 + 150 + return rgbByHue.map((component) => component + lightnessAdjustment) as [ 151 + number, 152 + number, 153 + number, 154 + ]; 155 + } 156 + 157 + function getRgbByHue( 158 + huePart: number, 159 + chroma: number, 160 + secondComponent: number, 161 + ): [number, number, number] { 162 + if (huePart < 1) return [chroma, secondComponent, 0]; 163 + if (huePart < 2) return [secondComponent, chroma, 0]; 164 + if (huePart < 3) return [0, chroma, secondComponent]; 165 + if (huePart < 4) return [0, secondComponent, chroma]; 166 + if (huePart < 5) return [secondComponent, 0, chroma]; 167 + 168 + return [chroma, 0, secondComponent]; 169 + } 170 + 171 + function rgbToHex(components: [number, number, number]): string { 172 + return `#${components 173 + .map((component) => 174 + Math.round(component * 255) 175 + .toString(16) 176 + .padStart(2, "0"), 177 + ) 178 + .join("")}`; 179 + }
+13
src/main.tsx
··· 1 + import { createRoot } from "react-dom/client"; 2 + import "@fontsource-variable/space-grotesk"; 3 + 4 + import { ActorApp } from "~/components/actor-app"; 5 + import "~/styles/globals.css"; 6 + 7 + const rootElement = document.getElementById("root"); 8 + 9 + if (!rootElement) { 10 + throw new Error("Could not find the application root element."); 11 + } 12 + 13 + createRoot(rootElement).render(<ActorApp />);
+121
src/styles/globals.css
··· 1 + @import "tailwindcss"; 2 + 3 + body { 4 + background-color: #111; 5 + color: #eee; 6 + font-family: "Space Grotesk Variable", "Space Grotesk", sans-serif; 7 + font-size: 0.9375rem; 8 + line-height: 1.5; 9 + text-rendering: optimizeLegibility; 10 + } 11 + 12 + .profile-page { 13 + background-color: var(--profile-background); 14 + background-image: var(--profile-atmosphere); 15 + background-attachment: fixed; 16 + background-size: 108% 108%; 17 + } 18 + 19 + .profile-grid { 20 + display: grid; 21 + grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); 22 + align-items: start; 23 + gap: 1.5rem; 24 + } 25 + 26 + .profile-grid-masonry { 27 + grid-auto-rows: 0.5rem; 28 + } 29 + 30 + .profile-card { 31 + background-color: var(--profile-card); 32 + border-color: color-mix(in srgb, var(--profile-border) 58%, transparent); 33 + border-radius: var(--profile-card-radius); 34 + padding: clamp(1rem, 2vw, 1.25rem); 35 + } 36 + 37 + .profile-divider { 38 + border-color: color-mix(in srgb, var(--profile-border) 44%, transparent); 39 + } 40 + 41 + .profile-link { 42 + color: var(--profile-accent); 43 + text-decoration-color: color-mix( 44 + in srgb, 45 + var(--profile-accent) 45%, 46 + transparent 47 + ); 48 + text-decoration-line: underline; 49 + text-decoration-thickness: 1px; 50 + text-underline-offset: 0.18em; 51 + } 52 + 53 + .profile-link:hover, 54 + .profile-link:focus-visible { 55 + text-decoration-color: currentColor; 56 + } 57 + 58 + .profile-link:focus-visible, 59 + .profile-external-link:focus-visible { 60 + outline: 2px solid var(--profile-accent); 61 + outline-offset: 3px; 62 + } 63 + 64 + .bluesky-profile { 65 + container-type: inline-size; 66 + } 67 + 68 + .profile-identity-header { 69 + display: grid; 70 + grid-template-columns: 4rem minmax(0, 1fr); 71 + align-items: center; 72 + gap: 0.875rem; 73 + } 74 + 75 + .profile-name { 76 + font-size: 1.625rem; 77 + font-weight: 700; 78 + letter-spacing: -0.035em; 79 + line-height: 1.05; 80 + } 81 + 82 + .profile-item-title { 83 + font-size: 0.9375rem; 84 + font-weight: 650; 85 + line-height: 1.3; 86 + } 87 + 88 + .profile-copy { 89 + font-size: 0.9375rem; 90 + line-height: 1.6; 91 + } 92 + 93 + .profile-secondary { 94 + font-size: 0.875rem; 95 + line-height: 1.5; 96 + opacity: 0.78; 97 + } 98 + 99 + .profile-metadata { 100 + font-size: 0.75rem; 101 + line-height: 1.4; 102 + opacity: 0.62; 103 + } 104 + 105 + @container (max-width: 21rem) { 106 + .profile-identity-header { 107 + grid-template-columns: 1fr; 108 + } 109 + } 110 + 111 + @keyframes atmosphere-drift { 112 + to { 113 + background-position: 100% 100%; 114 + } 115 + } 116 + 117 + @media (prefers-reduced-motion: no-preference) { 118 + .profile-page { 119 + animation: atmosphere-drift 28s ease-in-out infinite alternate; 120 + } 121 + }
+29
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + /* Base Options: */ 4 + "esModuleInterop": true, 5 + "skipLibCheck": true, 6 + "target": "es2022", 7 + "resolveJsonModule": true, 8 + "moduleDetection": "force", 9 + "isolatedModules": true, 10 + "verbatimModuleSyntax": true, 11 + 12 + /* Strictness */ 13 + "strict": true, 14 + "noUncheckedIndexedAccess": true, 15 + "lib": ["dom", "dom.iterable", "ES2022"], 16 + "noEmit": true, 17 + "module": "ESNext", 18 + "moduleResolution": "Bundler", 19 + "jsx": "react-jsx", 20 + "types": ["vite/client"], 21 + 22 + /* Path Aliases */ 23 + "paths": { 24 + "~/*": ["./src/*"] 25 + } 26 + }, 27 + "include": ["src", "vite.config.ts"], 28 + "exclude": ["node_modules", "dist"] 29 + }
+12
vite.config.ts
··· 1 + import react from "@vitejs/plugin-react"; 2 + import { defineConfig } from "vite"; 3 + 4 + export default defineConfig({ 5 + appType: "spa", 6 + plugins: [react()], 7 + resolve: { 8 + alias: { 9 + "~": new URL("./src", import.meta.url).pathname, 10 + }, 11 + }, 12 + });