an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

Compare changes

Choose any two refs to compare.

+22 -4
README.md
··· 1 # Red Dwarf 2 Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS. 3 4 - ![screenshot of red dwarf](/public/screenshot.png) 5 6 huge thanks to [Microcosm](https://microcosm.blue/) for making this possible 7 8 ## running dev and build 9 in the `vite.config.ts` file you should change these values 10 ```ts 11 - const PROD_URL = "https://reddwarf.whey.party" 12 const DEV_URL = "https://local3768forumtest.whey.party" 13 ``` 14 the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work ··· 52 and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed 53 54 ## Tanstack Router 55 - it does the job, nothing very specific was used here 56 57 - im planning to use the loader system on select pages to prevent loss of scroll positon and state though its really complex so i havent done it yet but the migration to tanstack query is a huge first step towards this goal
··· 1 # Red Dwarf 2 Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS. 3 4 + ![screenshot of red dwarf](/public/screenshot.jpg) 5 6 huge thanks to [Microcosm](https://microcosm.blue/) for making this possible 7 8 ## running dev and build 9 in the `vite.config.ts` file you should change these values 10 ```ts 11 + const PROD_URL = "https://reddwarf.app" 12 const DEV_URL = "https://local3768forumtest.whey.party" 13 ``` 14 the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work ··· 52 and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed 53 54 ## Tanstack Router 55 + something specific was used here 56 + 57 + so tanstack router is used as the base, but the home route is using tanstack-router-keepalive to preserve the route for better responsiveness, and it also saves scroll position of feeds into jotai (persistent) 58 + 59 + i previously used a tanstack router loader to ensure the tanstack query cache is ready to prevent scroll jumps but it is way too slow so i replaced it with tanstack-router-keepalive 60 + 61 + ## Icons 62 + this project uses Material icons. do not the light variant. sometimes i use `Mdi` if the icon needed doesnt exist in `MaterialSymbols` 63 64 + the project uses unplugin icon auto import, so you can just use the component and itll just work! 65 + 66 + the format is: 67 + ```tsx 68 + <IconMaterialSymbols{icon name here} /> 69 + // or 70 + <IconMdi{icon name here} /> 71 + ``` 72 + 73 + you can get the full list of icon names from iconify ([Material Symbols](https://icon-sets.iconify.design/material-symbols/) or [MDI](https://icon-sets.iconify.design/mdi/)) 74 + 75 + while it is nice to keep everything consistent by using material icons, if the icon you need is not provided by either material symbols nor mdi, you are allowed to just grab any icon from any pack (please do prioritize icons that fit in)
+4980 -25
package-lock.json
··· 8 "dependencies": { 9 "@atproto/api": "^0.16.6", 10 "@atproto/oauth-client-browser": "^0.3.33", 11 "@tailwindcss/vite": "^4.0.6", 12 "@tanstack/query-sync-storage-persister": "^5.85.6", 13 "@tanstack/react-devtools": "^0.2.2", ··· 16 "@tanstack/react-router": "^1.130.2", 17 "@tanstack/react-router-devtools": "^1.131.5", 18 "@tanstack/router-plugin": "^1.121.2", 19 "idb-keyval": "^6.2.2", 20 "jotai": "^2.13.1", 21 "react": "^19.0.0", 22 "react-dom": "^19.0.0", 23 "react-player": "^3.3.2", 24 - "tailwindcss": "^4.0.6" 25 }, 26 "devDependencies": { 27 "@eslint-react/eslint-plugin": "^2.2.1", 28 "@testing-library/dom": "^10.4.0", 29 "@testing-library/react": "^16.2.0", 30 "@types/node": "^24.3.0", ··· 42 "prettier": "^3.6.2", 43 "typescript": "^5.7.2", 44 "typescript-eslint": "^8.46.1", 45 "vite": "^6.3.5", 46 "vitest": "^3.0.5", 47 "web-vitals": "^4.2.4" ··· 58 }, 59 "engines": { 60 "node": ">=6.0.0" 61 } 62 }, 63 "node_modules/@asamuzakjp/css-color": { ··· 1549 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1550 } 1551 }, 1552 "node_modules/@humanfs/core": { 1553 "version": "0.19.1", 1554 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 1605 "url": "https://github.com/sponsors/nzakas" 1606 } 1607 }, 1608 "node_modules/@isaacs/fs-minipass": { 1609 "version": "4.0.1", 1610 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", ··· 1768 "node": ">= 8" 1769 } 1770 }, 1771 "node_modules/@rolldown/pluginutils": { 1772 "version": "1.0.0-beta.27", 1773 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", ··· 2082 "solid-js": "^1.6.12" 2083 } 2084 }, 2085 "node_modules/@svta/common-media-library": { 2086 "version": "0.12.4", 2087 "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz", ··· 2909 "@types/react": "^19.0.0" 2910 } 2911 }, 2912 "node_modules/@typescript-eslint/eslint-plugin": { 2913 "version": "8.46.1", 2914 "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", ··· 3433 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 3434 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 3435 "dev": true, 3436 - "license": "Python-2.0", 3437 - "peer": true 3438 }, 3439 "node_modules/aria-query": { 3440 "version": "5.3.0", ··· 3846 "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 3847 "dev": true, 3848 "license": "MIT", 3849 - "peer": true, 3850 "engines": { 3851 "node": ">=6" 3852 } 3853 }, 3854 "node_modules/caniuse-lite": { 3855 "version": "1.0.30001737", 3856 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", ··· 4041 "dev": true, 4042 "license": "MIT" 4043 }, 4044 "node_modules/convert-source-map": { 4045 "version": "2.0.0", 4046 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", ··· 4064 "url": "https://opencollective.com/core-js" 4065 } 4066 }, 4067 "node_modules/cross-spawn": { 4068 "version": "7.0.6", 4069 "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", ··· 4203 } 4204 }, 4205 "node_modules/debug": { 4206 - "version": "4.4.1", 4207 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 4208 - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 4209 "license": "MIT", 4210 "dependencies": { 4211 "ms": "^2.1.3" ··· 4299 "node": ">=8" 4300 } 4301 }, 4302 "node_modules/diff": { 4303 "version": "8.0.2", 4304 "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", ··· 4328 "dev": true, 4329 "license": "MIT" 4330 }, 4331 "node_modules/dunder-proto": { 4332 "version": "1.0.1", 4333 "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", ··· 4373 }, 4374 "funding": { 4375 "url": "https://github.com/fb55/entities?sponsor=1" 4376 } 4377 }, 4378 "node_modules/es-abstract": { ··· 5036 "node": ">=0.10.0" 5037 } 5038 }, 5039 "node_modules/expect-type": { 5040 "version": "1.2.2", 5041 "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", ··· 5045 "engines": { 5046 "node": ">=12.0.0" 5047 } 5048 }, 5049 "node_modules/fast-deep-equal": { 5050 "version": "3.1.3", ··· 5275 }, 5276 "funding": { 5277 "url": "https://github.com/sponsors/ljharb" 5278 } 5279 }, 5280 "node_modules/get-proto": { ··· 5584 "node": ">= 14" 5585 } 5586 }, 5587 "node_modules/iconv-lite": { 5588 "version": "0.6.3", 5589 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", ··· 5626 "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 5627 "dev": true, 5628 "license": "MIT", 5629 - "peer": true, 5630 "dependencies": { 5631 "parent-module": "^1.0.0", 5632 "resolve-from": "^4.0.0" ··· 5714 "funding": { 5715 "url": "https://github.com/sponsors/ljharb" 5716 } 5717 }, 5718 "node_modules/is-async-function": { 5719 "version": "2.1.1", ··· 6238 "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 6239 "dev": true, 6240 "license": "MIT", 6241 - "peer": true, 6242 "dependencies": { 6243 "argparse": "^2.0.1" 6244 }, ··· 6306 "license": "MIT", 6307 "peer": true 6308 }, 6309 "node_modules/json-schema-traverse": { 6310 "version": "0.4.1", 6311 "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", ··· 6361 "json-buffer": "3.0.1" 6362 } 6363 }, 6364 "node_modules/levn": { 6365 "version": "0.4.1", 6366 "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", ··· 6613 "url": "https://opencollective.com/parcel" 6614 } 6615 }, 6616 "node_modules/localforage": { 6617 "version": "1.10.0", 6618 "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", ··· 6639 "url": "https://github.com/sponsors/sindresorhus" 6640 } 6641 }, 6642 "node_modules/lodash.merge": { 6643 "version": "4.6.2", 6644 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", ··· 6666 "dev": true, 6667 "license": "MIT" 6668 }, 6669 "node_modules/lru-cache": { 6670 "version": "5.1.1", 6671 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", ··· 6686 } 6687 }, 6688 "node_modules/magic-string": { 6689 - "version": "0.30.18", 6690 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", 6691 - "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", 6692 "license": "MIT", 6693 "dependencies": { 6694 "@jridgewell/sourcemap-codec": "^1.5.5" ··· 6793 "url": "https://github.com/sponsors/isaacs" 6794 } 6795 }, 6796 "node_modules/ms": { 6797 "version": "2.1.3", 6798 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 6842 "dev": true, 6843 "license": "MIT" 6844 }, 6845 "node_modules/node-releases": { 6846 "version": "2.0.19", 6847 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", ··· 6857 "node": ">=0.10.0" 6858 } 6859 }, 6860 "node_modules/nwsapi": { 6861 "version": "2.2.21", 6862 "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", ··· 7042 "url": "https://github.com/sponsors/sindresorhus" 7043 } 7044 }, 7045 "node_modules/parent-module": { 7046 "version": "1.0.1", 7047 "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 7048 "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 7049 "dev": true, 7050 "license": "MIT", 7051 - "peer": true, 7052 "dependencies": { 7053 "callsites": "^3.0.0" 7054 }, ··· 7056 "node": ">=6" 7057 } 7058 }, 7059 "node_modules/parse5": { 7060 "version": "7.3.0", 7061 "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", ··· 7104 "dev": true, 7105 "license": "MIT" 7106 }, 7107 "node_modules/pathe": { 7108 "version": "2.0.3", 7109 "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", ··· 7137 }, 7138 "funding": { 7139 "url": "https://github.com/sponsors/jonschlinkert" 7140 } 7141 }, 7142 "node_modules/player.style": { ··· 7261 "node": ">=6" 7262 } 7263 }, 7264 "node_modules/queue-microtask": { 7265 "version": "1.2.3", 7266 "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", ··· 7282 ], 7283 "license": "MIT" 7284 }, 7285 "node_modules/react": { 7286 "version": "19.1.1", 7287 "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", ··· 7343 "node": ">=0.10.0" 7344 } 7345 }, 7346 "node_modules/readdirp": { 7347 "version": "3.6.0", 7348 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", ··· 7448 "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 7449 "dev": true, 7450 "license": "MIT", 7451 - "peer": true, 7452 "engines": { 7453 "node": ">=4" 7454 } ··· 7630 "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", 7631 "license": "MIT" 7632 }, 7633 "node_modules/semver": { 7634 "version": "6.3.1", 7635 "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", ··· 7817 "dev": true, 7818 "license": "ISC" 7819 }, 7820 "node_modules/solid-js": { 7821 "version": "1.9.9", 7822 "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz", ··· 8000 } 8001 }, 8002 "node_modules/strip-literal": { 8003 - "version": "3.0.0", 8004 - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", 8005 - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", 8006 "dev": true, 8007 "license": "MIT", 8008 "dependencies": { ··· 8052 "url": "https://github.com/sponsors/ljharb" 8053 } 8054 }, 8055 "node_modules/symbol-tree": { 8056 "version": "3.2.4", 8057 "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", ··· 8064 "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", 8065 "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", 8066 "license": "MIT" 8067 }, 8068 "node_modules/tapable": { 8069 "version": "2.2.3", ··· 8137 "license": "MIT" 8138 }, 8139 "node_modules/tinyglobby": { 8140 - "version": "0.2.14", 8141 - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", 8142 - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", 8143 "license": "MIT", 8144 "dependencies": { 8145 - "fdir": "^6.4.4", 8146 - "picomatch": "^4.0.2" 8147 }, 8148 "engines": { 8149 "node": ">=12.0.0" ··· 8521 "node": "*" 8522 } 8523 }, 8524 "node_modules/uint8arrays": { 8525 "version": "3.0.0", 8526 "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", ··· 8556 "devOptional": true, 8557 "license": "MIT" 8558 }, 8559 "node_modules/unplugin": { 8560 - "version": "2.3.9", 8561 - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.9.tgz", 8562 - "integrity": "sha512-2dcbZq6aprwXTkzptq3k5qm5B8cvpjG9ynPd5fyM2wDJuuF7PeUK64Sxf0d+X1ZyDOeGydbNzMqBSIVlH8GIfA==", 8563 "license": "MIT", 8564 "dependencies": { 8565 "@jridgewell/remapping": "^2.3.5", ··· 8571 "node": ">=18.12.0" 8572 } 8573 }, 8574 "node_modules/unplugin/node_modules/picomatch": { 8575 "version": "4.0.3", 8576 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", ··· 8622 "peer": true, 8623 "dependencies": { 8624 "punycode": "^2.1.0" 8625 } 8626 }, 8627 "node_modules/use-sync-external-store": {
··· 8 "dependencies": { 9 "@atproto/api": "^0.16.6", 10 "@atproto/oauth-client-browser": "^0.3.33", 11 + "@radix-ui/react-dialog": "^1.1.15", 12 + "@radix-ui/react-dropdown-menu": "^2.1.16", 13 + "@radix-ui/react-hover-card": "^1.1.15", 14 + "@radix-ui/react-slider": "^1.3.6", 15 "@tailwindcss/vite": "^4.0.6", 16 "@tanstack/query-sync-storage-persister": "^5.85.6", 17 "@tanstack/react-devtools": "^0.2.2", ··· 20 "@tanstack/react-router": "^1.130.2", 21 "@tanstack/react-router-devtools": "^1.131.5", 22 "@tanstack/router-plugin": "^1.121.2", 23 + "dompurify": "^3.3.0", 24 + "i": "^0.3.7", 25 "idb-keyval": "^6.2.2", 26 "jotai": "^2.13.1", 27 + "npm": "^11.6.2", 28 + "radix-ui": "^1.4.3", 29 "react": "^19.0.0", 30 "react-dom": "^19.0.0", 31 "react-player": "^3.3.2", 32 + "tailwindcss": "^4.0.6", 33 + "tanstack-router-keepalive": "^1.0.0" 34 }, 35 "devDependencies": { 36 "@eslint-react/eslint-plugin": "^2.2.1", 37 + "@iconify-icon/react": "^3.0.1", 38 + "@iconify-json/material-symbols": "^1.2.42", 39 + "@iconify-json/mdi": "^1.2.3", 40 + "@iconify/json": "^2.2.396", 41 + "@svgr/core": "^8.1.0", 42 + "@svgr/plugin-jsx": "^8.1.0", 43 "@testing-library/dom": "^10.4.0", 44 "@testing-library/react": "^16.2.0", 45 "@types/node": "^24.3.0", ··· 57 "prettier": "^3.6.2", 58 "typescript": "^5.7.2", 59 "typescript-eslint": "^8.46.1", 60 + "unplugin-auto-import": "^20.2.0", 61 + "unplugin-icons": "^22.4.2", 62 "vite": "^6.3.5", 63 "vitest": "^3.0.5", 64 "web-vitals": "^4.2.4" ··· 75 }, 76 "engines": { 77 "node": ">=6.0.0" 78 + } 79 + }, 80 + "node_modules/@antfu/install-pkg": { 81 + "version": "1.1.0", 82 + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", 83 + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", 84 + "dev": true, 85 + "license": "MIT", 86 + "dependencies": { 87 + "package-manager-detector": "^1.3.0", 88 + "tinyexec": "^1.0.1" 89 + }, 90 + "funding": { 91 + "url": "https://github.com/sponsors/antfu" 92 + } 93 + }, 94 + "node_modules/@antfu/install-pkg/node_modules/tinyexec": { 95 + "version": "1.0.1", 96 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", 97 + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", 98 + "dev": true, 99 + "license": "MIT" 100 + }, 101 + "node_modules/@antfu/utils": { 102 + "version": "9.3.0", 103 + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz", 104 + "integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==", 105 + "dev": true, 106 + "license": "MIT", 107 + "funding": { 108 + "url": "https://github.com/sponsors/antfu" 109 } 110 }, 111 "node_modules/@asamuzakjp/css-color": { ··· 1597 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1598 } 1599 }, 1600 + "node_modules/@floating-ui/core": { 1601 + "version": "1.7.3", 1602 + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", 1603 + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", 1604 + "dependencies": { 1605 + "@floating-ui/utils": "^0.2.10" 1606 + } 1607 + }, 1608 + "node_modules/@floating-ui/dom": { 1609 + "version": "1.7.4", 1610 + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", 1611 + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", 1612 + "dependencies": { 1613 + "@floating-ui/core": "^1.7.3", 1614 + "@floating-ui/utils": "^0.2.10" 1615 + } 1616 + }, 1617 + "node_modules/@floating-ui/react-dom": { 1618 + "version": "2.1.6", 1619 + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", 1620 + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", 1621 + "dependencies": { 1622 + "@floating-ui/dom": "^1.7.4" 1623 + }, 1624 + "peerDependencies": { 1625 + "react": ">=16.8.0", 1626 + "react-dom": ">=16.8.0" 1627 + } 1628 + }, 1629 + "node_modules/@floating-ui/utils": { 1630 + "version": "0.2.10", 1631 + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", 1632 + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" 1633 + }, 1634 "node_modules/@humanfs/core": { 1635 "version": "0.19.1", 1636 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 1687 "url": "https://github.com/sponsors/nzakas" 1688 } 1689 }, 1690 + "node_modules/@iconify-icon/react": { 1691 + "version": "3.0.1", 1692 + "resolved": "https://registry.npmjs.org/@iconify-icon/react/-/react-3.0.1.tgz", 1693 + "integrity": "sha512-/4CAVpk8HDyKS78r1G0rZhML7hI6jLxb8kAmjEXsCtuVUDwdGqicGCRg0T14mqeHNImrQPR49MhbuSSS++JlUA==", 1694 + "dev": true, 1695 + "license": "MIT", 1696 + "dependencies": { 1697 + "iconify-icon": "^3.0.1" 1698 + }, 1699 + "funding": { 1700 + "url": "https://github.com/sponsors/cyberalien" 1701 + }, 1702 + "peerDependencies": { 1703 + "react": ">=16" 1704 + } 1705 + }, 1706 + "node_modules/@iconify-json/material-symbols": { 1707 + "version": "1.2.42", 1708 + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.42.tgz", 1709 + "integrity": "sha512-FDRfnQqy8iXaq/swVPFWaHftqP9tk3qDCRhC30s3UZL2j4mvGZk5gVECRXCkZv5jnsAiTpZxGQM8HrMiwE7GtA==", 1710 + "dev": true, 1711 + "license": "Apache-2.0", 1712 + "dependencies": { 1713 + "@iconify/types": "*" 1714 + } 1715 + }, 1716 + "node_modules/@iconify-json/mdi": { 1717 + "version": "1.2.3", 1718 + "resolved": "https://registry.npmjs.org/@iconify-json/mdi/-/mdi-1.2.3.tgz", 1719 + "integrity": "sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==", 1720 + "dev": true, 1721 + "license": "Apache-2.0", 1722 + "dependencies": { 1723 + "@iconify/types": "*" 1724 + } 1725 + }, 1726 + "node_modules/@iconify/json": { 1727 + "version": "2.2.396", 1728 + "resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.396.tgz", 1729 + "integrity": "sha512-tijg77JFuYIt32S9N8p7La8C0zp9zKZsX6UP8ip5GVB1F6Mp3pZA5Vc5eAquTY50NoDJX58U6z4Qn3d6Wyossg==", 1730 + "dev": true, 1731 + "license": "MIT", 1732 + "dependencies": { 1733 + "@iconify/types": "*", 1734 + "pathe": "^2.0.0" 1735 + } 1736 + }, 1737 + "node_modules/@iconify/types": { 1738 + "version": "2.0.0", 1739 + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", 1740 + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", 1741 + "dev": true, 1742 + "license": "MIT" 1743 + }, 1744 + "node_modules/@iconify/utils": { 1745 + "version": "3.0.2", 1746 + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz", 1747 + "integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==", 1748 + "dev": true, 1749 + "license": "MIT", 1750 + "dependencies": { 1751 + "@antfu/install-pkg": "^1.1.0", 1752 + "@antfu/utils": "^9.2.0", 1753 + "@iconify/types": "^2.0.0", 1754 + "debug": "^4.4.1", 1755 + "globals": "^15.15.0", 1756 + "kolorist": "^1.8.0", 1757 + "local-pkg": "^1.1.1", 1758 + "mlly": "^1.7.4" 1759 + } 1760 + }, 1761 + "node_modules/@iconify/utils/node_modules/globals": { 1762 + "version": "15.15.0", 1763 + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", 1764 + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", 1765 + "dev": true, 1766 + "license": "MIT", 1767 + "engines": { 1768 + "node": ">=18" 1769 + }, 1770 + "funding": { 1771 + "url": "https://github.com/sponsors/sindresorhus" 1772 + } 1773 + }, 1774 "node_modules/@isaacs/fs-minipass": { 1775 "version": "4.0.1", 1776 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", ··· 1934 "node": ">= 8" 1935 } 1936 }, 1937 + "node_modules/@radix-ui/number": { 1938 + "version": "1.1.1", 1939 + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", 1940 + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" 1941 + }, 1942 + "node_modules/@radix-ui/primitive": { 1943 + "version": "1.1.3", 1944 + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", 1945 + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" 1946 + }, 1947 + "node_modules/@radix-ui/react-accessible-icon": { 1948 + "version": "1.1.7", 1949 + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", 1950 + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", 1951 + "dependencies": { 1952 + "@radix-ui/react-visually-hidden": "1.2.3" 1953 + }, 1954 + "peerDependencies": { 1955 + "@types/react": "*", 1956 + "@types/react-dom": "*", 1957 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 1958 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 1959 + }, 1960 + "peerDependenciesMeta": { 1961 + "@types/react": { 1962 + "optional": true 1963 + }, 1964 + "@types/react-dom": { 1965 + "optional": true 1966 + } 1967 + } 1968 + }, 1969 + "node_modules/@radix-ui/react-accordion": { 1970 + "version": "1.2.12", 1971 + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", 1972 + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", 1973 + "dependencies": { 1974 + "@radix-ui/primitive": "1.1.3", 1975 + "@radix-ui/react-collapsible": "1.1.12", 1976 + "@radix-ui/react-collection": "1.1.7", 1977 + "@radix-ui/react-compose-refs": "1.1.2", 1978 + "@radix-ui/react-context": "1.1.2", 1979 + "@radix-ui/react-direction": "1.1.1", 1980 + "@radix-ui/react-id": "1.1.1", 1981 + "@radix-ui/react-primitive": "2.1.3", 1982 + "@radix-ui/react-use-controllable-state": "1.2.2" 1983 + }, 1984 + "peerDependencies": { 1985 + "@types/react": "*", 1986 + "@types/react-dom": "*", 1987 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 1988 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 1989 + }, 1990 + "peerDependenciesMeta": { 1991 + "@types/react": { 1992 + "optional": true 1993 + }, 1994 + "@types/react-dom": { 1995 + "optional": true 1996 + } 1997 + } 1998 + }, 1999 + "node_modules/@radix-ui/react-alert-dialog": { 2000 + "version": "1.1.15", 2001 + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", 2002 + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", 2003 + "dependencies": { 2004 + "@radix-ui/primitive": "1.1.3", 2005 + "@radix-ui/react-compose-refs": "1.1.2", 2006 + "@radix-ui/react-context": "1.1.2", 2007 + "@radix-ui/react-dialog": "1.1.15", 2008 + "@radix-ui/react-primitive": "2.1.3", 2009 + "@radix-ui/react-slot": "1.2.3" 2010 + }, 2011 + "peerDependencies": { 2012 + "@types/react": "*", 2013 + "@types/react-dom": "*", 2014 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2015 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2016 + }, 2017 + "peerDependenciesMeta": { 2018 + "@types/react": { 2019 + "optional": true 2020 + }, 2021 + "@types/react-dom": { 2022 + "optional": true 2023 + } 2024 + } 2025 + }, 2026 + "node_modules/@radix-ui/react-arrow": { 2027 + "version": "1.1.7", 2028 + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", 2029 + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", 2030 + "dependencies": { 2031 + "@radix-ui/react-primitive": "2.1.3" 2032 + }, 2033 + "peerDependencies": { 2034 + "@types/react": "*", 2035 + "@types/react-dom": "*", 2036 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2037 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2038 + }, 2039 + "peerDependenciesMeta": { 2040 + "@types/react": { 2041 + "optional": true 2042 + }, 2043 + "@types/react-dom": { 2044 + "optional": true 2045 + } 2046 + } 2047 + }, 2048 + "node_modules/@radix-ui/react-aspect-ratio": { 2049 + "version": "1.1.7", 2050 + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", 2051 + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", 2052 + "dependencies": { 2053 + "@radix-ui/react-primitive": "2.1.3" 2054 + }, 2055 + "peerDependencies": { 2056 + "@types/react": "*", 2057 + "@types/react-dom": "*", 2058 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2059 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2060 + }, 2061 + "peerDependenciesMeta": { 2062 + "@types/react": { 2063 + "optional": true 2064 + }, 2065 + "@types/react-dom": { 2066 + "optional": true 2067 + } 2068 + } 2069 + }, 2070 + "node_modules/@radix-ui/react-avatar": { 2071 + "version": "1.1.10", 2072 + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", 2073 + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", 2074 + "dependencies": { 2075 + "@radix-ui/react-context": "1.1.2", 2076 + "@radix-ui/react-primitive": "2.1.3", 2077 + "@radix-ui/react-use-callback-ref": "1.1.1", 2078 + "@radix-ui/react-use-is-hydrated": "0.1.0", 2079 + "@radix-ui/react-use-layout-effect": "1.1.1" 2080 + }, 2081 + "peerDependencies": { 2082 + "@types/react": "*", 2083 + "@types/react-dom": "*", 2084 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2085 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2086 + }, 2087 + "peerDependenciesMeta": { 2088 + "@types/react": { 2089 + "optional": true 2090 + }, 2091 + "@types/react-dom": { 2092 + "optional": true 2093 + } 2094 + } 2095 + }, 2096 + "node_modules/@radix-ui/react-checkbox": { 2097 + "version": "1.3.3", 2098 + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", 2099 + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", 2100 + "dependencies": { 2101 + "@radix-ui/primitive": "1.1.3", 2102 + "@radix-ui/react-compose-refs": "1.1.2", 2103 + "@radix-ui/react-context": "1.1.2", 2104 + "@radix-ui/react-presence": "1.1.5", 2105 + "@radix-ui/react-primitive": "2.1.3", 2106 + "@radix-ui/react-use-controllable-state": "1.2.2", 2107 + "@radix-ui/react-use-previous": "1.1.1", 2108 + "@radix-ui/react-use-size": "1.1.1" 2109 + }, 2110 + "peerDependencies": { 2111 + "@types/react": "*", 2112 + "@types/react-dom": "*", 2113 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2114 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2115 + }, 2116 + "peerDependenciesMeta": { 2117 + "@types/react": { 2118 + "optional": true 2119 + }, 2120 + "@types/react-dom": { 2121 + "optional": true 2122 + } 2123 + } 2124 + }, 2125 + "node_modules/@radix-ui/react-collapsible": { 2126 + "version": "1.1.12", 2127 + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", 2128 + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", 2129 + "dependencies": { 2130 + "@radix-ui/primitive": "1.1.3", 2131 + "@radix-ui/react-compose-refs": "1.1.2", 2132 + "@radix-ui/react-context": "1.1.2", 2133 + "@radix-ui/react-id": "1.1.1", 2134 + "@radix-ui/react-presence": "1.1.5", 2135 + "@radix-ui/react-primitive": "2.1.3", 2136 + "@radix-ui/react-use-controllable-state": "1.2.2", 2137 + "@radix-ui/react-use-layout-effect": "1.1.1" 2138 + }, 2139 + "peerDependencies": { 2140 + "@types/react": "*", 2141 + "@types/react-dom": "*", 2142 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2143 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2144 + }, 2145 + "peerDependenciesMeta": { 2146 + "@types/react": { 2147 + "optional": true 2148 + }, 2149 + "@types/react-dom": { 2150 + "optional": true 2151 + } 2152 + } 2153 + }, 2154 + "node_modules/@radix-ui/react-collection": { 2155 + "version": "1.1.7", 2156 + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", 2157 + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", 2158 + "dependencies": { 2159 + "@radix-ui/react-compose-refs": "1.1.2", 2160 + "@radix-ui/react-context": "1.1.2", 2161 + "@radix-ui/react-primitive": "2.1.3", 2162 + "@radix-ui/react-slot": "1.2.3" 2163 + }, 2164 + "peerDependencies": { 2165 + "@types/react": "*", 2166 + "@types/react-dom": "*", 2167 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2168 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2169 + }, 2170 + "peerDependenciesMeta": { 2171 + "@types/react": { 2172 + "optional": true 2173 + }, 2174 + "@types/react-dom": { 2175 + "optional": true 2176 + } 2177 + } 2178 + }, 2179 + "node_modules/@radix-ui/react-compose-refs": { 2180 + "version": "1.1.2", 2181 + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", 2182 + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", 2183 + "peerDependencies": { 2184 + "@types/react": "*", 2185 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2186 + }, 2187 + "peerDependenciesMeta": { 2188 + "@types/react": { 2189 + "optional": true 2190 + } 2191 + } 2192 + }, 2193 + "node_modules/@radix-ui/react-context": { 2194 + "version": "1.1.2", 2195 + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", 2196 + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", 2197 + "peerDependencies": { 2198 + "@types/react": "*", 2199 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2200 + }, 2201 + "peerDependenciesMeta": { 2202 + "@types/react": { 2203 + "optional": true 2204 + } 2205 + } 2206 + }, 2207 + "node_modules/@radix-ui/react-context-menu": { 2208 + "version": "2.2.16", 2209 + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", 2210 + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", 2211 + "dependencies": { 2212 + "@radix-ui/primitive": "1.1.3", 2213 + "@radix-ui/react-context": "1.1.2", 2214 + "@radix-ui/react-menu": "2.1.16", 2215 + "@radix-ui/react-primitive": "2.1.3", 2216 + "@radix-ui/react-use-callback-ref": "1.1.1", 2217 + "@radix-ui/react-use-controllable-state": "1.2.2" 2218 + }, 2219 + "peerDependencies": { 2220 + "@types/react": "*", 2221 + "@types/react-dom": "*", 2222 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2223 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2224 + }, 2225 + "peerDependenciesMeta": { 2226 + "@types/react": { 2227 + "optional": true 2228 + }, 2229 + "@types/react-dom": { 2230 + "optional": true 2231 + } 2232 + } 2233 + }, 2234 + "node_modules/@radix-ui/react-dialog": { 2235 + "version": "1.1.15", 2236 + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", 2237 + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", 2238 + "dependencies": { 2239 + "@radix-ui/primitive": "1.1.3", 2240 + "@radix-ui/react-compose-refs": "1.1.2", 2241 + "@radix-ui/react-context": "1.1.2", 2242 + "@radix-ui/react-dismissable-layer": "1.1.11", 2243 + "@radix-ui/react-focus-guards": "1.1.3", 2244 + "@radix-ui/react-focus-scope": "1.1.7", 2245 + "@radix-ui/react-id": "1.1.1", 2246 + "@radix-ui/react-portal": "1.1.9", 2247 + "@radix-ui/react-presence": "1.1.5", 2248 + "@radix-ui/react-primitive": "2.1.3", 2249 + "@radix-ui/react-slot": "1.2.3", 2250 + "@radix-ui/react-use-controllable-state": "1.2.2", 2251 + "aria-hidden": "^1.2.4", 2252 + "react-remove-scroll": "^2.6.3" 2253 + }, 2254 + "peerDependencies": { 2255 + "@types/react": "*", 2256 + "@types/react-dom": "*", 2257 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2258 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2259 + }, 2260 + "peerDependenciesMeta": { 2261 + "@types/react": { 2262 + "optional": true 2263 + }, 2264 + "@types/react-dom": { 2265 + "optional": true 2266 + } 2267 + } 2268 + }, 2269 + "node_modules/@radix-ui/react-direction": { 2270 + "version": "1.1.1", 2271 + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", 2272 + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", 2273 + "peerDependencies": { 2274 + "@types/react": "*", 2275 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2276 + }, 2277 + "peerDependenciesMeta": { 2278 + "@types/react": { 2279 + "optional": true 2280 + } 2281 + } 2282 + }, 2283 + "node_modules/@radix-ui/react-dismissable-layer": { 2284 + "version": "1.1.11", 2285 + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", 2286 + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", 2287 + "dependencies": { 2288 + "@radix-ui/primitive": "1.1.3", 2289 + "@radix-ui/react-compose-refs": "1.1.2", 2290 + "@radix-ui/react-primitive": "2.1.3", 2291 + "@radix-ui/react-use-callback-ref": "1.1.1", 2292 + "@radix-ui/react-use-escape-keydown": "1.1.1" 2293 + }, 2294 + "peerDependencies": { 2295 + "@types/react": "*", 2296 + "@types/react-dom": "*", 2297 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2298 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2299 + }, 2300 + "peerDependenciesMeta": { 2301 + "@types/react": { 2302 + "optional": true 2303 + }, 2304 + "@types/react-dom": { 2305 + "optional": true 2306 + } 2307 + } 2308 + }, 2309 + "node_modules/@radix-ui/react-dropdown-menu": { 2310 + "version": "2.1.16", 2311 + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", 2312 + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", 2313 + "dependencies": { 2314 + "@radix-ui/primitive": "1.1.3", 2315 + "@radix-ui/react-compose-refs": "1.1.2", 2316 + "@radix-ui/react-context": "1.1.2", 2317 + "@radix-ui/react-id": "1.1.1", 2318 + "@radix-ui/react-menu": "2.1.16", 2319 + "@radix-ui/react-primitive": "2.1.3", 2320 + "@radix-ui/react-use-controllable-state": "1.2.2" 2321 + }, 2322 + "peerDependencies": { 2323 + "@types/react": "*", 2324 + "@types/react-dom": "*", 2325 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2326 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2327 + }, 2328 + "peerDependenciesMeta": { 2329 + "@types/react": { 2330 + "optional": true 2331 + }, 2332 + "@types/react-dom": { 2333 + "optional": true 2334 + } 2335 + } 2336 + }, 2337 + "node_modules/@radix-ui/react-focus-guards": { 2338 + "version": "1.1.3", 2339 + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", 2340 + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", 2341 + "peerDependencies": { 2342 + "@types/react": "*", 2343 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2344 + }, 2345 + "peerDependenciesMeta": { 2346 + "@types/react": { 2347 + "optional": true 2348 + } 2349 + } 2350 + }, 2351 + "node_modules/@radix-ui/react-focus-scope": { 2352 + "version": "1.1.7", 2353 + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", 2354 + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", 2355 + "dependencies": { 2356 + "@radix-ui/react-compose-refs": "1.1.2", 2357 + "@radix-ui/react-primitive": "2.1.3", 2358 + "@radix-ui/react-use-callback-ref": "1.1.1" 2359 + }, 2360 + "peerDependencies": { 2361 + "@types/react": "*", 2362 + "@types/react-dom": "*", 2363 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2364 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2365 + }, 2366 + "peerDependenciesMeta": { 2367 + "@types/react": { 2368 + "optional": true 2369 + }, 2370 + "@types/react-dom": { 2371 + "optional": true 2372 + } 2373 + } 2374 + }, 2375 + "node_modules/@radix-ui/react-form": { 2376 + "version": "0.1.8", 2377 + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", 2378 + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", 2379 + "dependencies": { 2380 + "@radix-ui/primitive": "1.1.3", 2381 + "@radix-ui/react-compose-refs": "1.1.2", 2382 + "@radix-ui/react-context": "1.1.2", 2383 + "@radix-ui/react-id": "1.1.1", 2384 + "@radix-ui/react-label": "2.1.7", 2385 + "@radix-ui/react-primitive": "2.1.3" 2386 + }, 2387 + "peerDependencies": { 2388 + "@types/react": "*", 2389 + "@types/react-dom": "*", 2390 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2391 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2392 + }, 2393 + "peerDependenciesMeta": { 2394 + "@types/react": { 2395 + "optional": true 2396 + }, 2397 + "@types/react-dom": { 2398 + "optional": true 2399 + } 2400 + } 2401 + }, 2402 + "node_modules/@radix-ui/react-hover-card": { 2403 + "version": "1.1.15", 2404 + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", 2405 + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", 2406 + "license": "MIT", 2407 + "dependencies": { 2408 + "@radix-ui/primitive": "1.1.3", 2409 + "@radix-ui/react-compose-refs": "1.1.2", 2410 + "@radix-ui/react-context": "1.1.2", 2411 + "@radix-ui/react-dismissable-layer": "1.1.11", 2412 + "@radix-ui/react-popper": "1.2.8", 2413 + "@radix-ui/react-portal": "1.1.9", 2414 + "@radix-ui/react-presence": "1.1.5", 2415 + "@radix-ui/react-primitive": "2.1.3", 2416 + "@radix-ui/react-use-controllable-state": "1.2.2" 2417 + }, 2418 + "peerDependencies": { 2419 + "@types/react": "*", 2420 + "@types/react-dom": "*", 2421 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2422 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2423 + }, 2424 + "peerDependenciesMeta": { 2425 + "@types/react": { 2426 + "optional": true 2427 + }, 2428 + "@types/react-dom": { 2429 + "optional": true 2430 + } 2431 + } 2432 + }, 2433 + "node_modules/@radix-ui/react-id": { 2434 + "version": "1.1.1", 2435 + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", 2436 + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", 2437 + "dependencies": { 2438 + "@radix-ui/react-use-layout-effect": "1.1.1" 2439 + }, 2440 + "peerDependencies": { 2441 + "@types/react": "*", 2442 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2443 + }, 2444 + "peerDependenciesMeta": { 2445 + "@types/react": { 2446 + "optional": true 2447 + } 2448 + } 2449 + }, 2450 + "node_modules/@radix-ui/react-label": { 2451 + "version": "2.1.7", 2452 + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", 2453 + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", 2454 + "dependencies": { 2455 + "@radix-ui/react-primitive": "2.1.3" 2456 + }, 2457 + "peerDependencies": { 2458 + "@types/react": "*", 2459 + "@types/react-dom": "*", 2460 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2461 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2462 + }, 2463 + "peerDependenciesMeta": { 2464 + "@types/react": { 2465 + "optional": true 2466 + }, 2467 + "@types/react-dom": { 2468 + "optional": true 2469 + } 2470 + } 2471 + }, 2472 + "node_modules/@radix-ui/react-menu": { 2473 + "version": "2.1.16", 2474 + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", 2475 + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", 2476 + "dependencies": { 2477 + "@radix-ui/primitive": "1.1.3", 2478 + "@radix-ui/react-collection": "1.1.7", 2479 + "@radix-ui/react-compose-refs": "1.1.2", 2480 + "@radix-ui/react-context": "1.1.2", 2481 + "@radix-ui/react-direction": "1.1.1", 2482 + "@radix-ui/react-dismissable-layer": "1.1.11", 2483 + "@radix-ui/react-focus-guards": "1.1.3", 2484 + "@radix-ui/react-focus-scope": "1.1.7", 2485 + "@radix-ui/react-id": "1.1.1", 2486 + "@radix-ui/react-popper": "1.2.8", 2487 + "@radix-ui/react-portal": "1.1.9", 2488 + "@radix-ui/react-presence": "1.1.5", 2489 + "@radix-ui/react-primitive": "2.1.3", 2490 + "@radix-ui/react-roving-focus": "1.1.11", 2491 + "@radix-ui/react-slot": "1.2.3", 2492 + "@radix-ui/react-use-callback-ref": "1.1.1", 2493 + "aria-hidden": "^1.2.4", 2494 + "react-remove-scroll": "^2.6.3" 2495 + }, 2496 + "peerDependencies": { 2497 + "@types/react": "*", 2498 + "@types/react-dom": "*", 2499 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2500 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2501 + }, 2502 + "peerDependenciesMeta": { 2503 + "@types/react": { 2504 + "optional": true 2505 + }, 2506 + "@types/react-dom": { 2507 + "optional": true 2508 + } 2509 + } 2510 + }, 2511 + "node_modules/@radix-ui/react-menubar": { 2512 + "version": "1.1.16", 2513 + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", 2514 + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", 2515 + "dependencies": { 2516 + "@radix-ui/primitive": "1.1.3", 2517 + "@radix-ui/react-collection": "1.1.7", 2518 + "@radix-ui/react-compose-refs": "1.1.2", 2519 + "@radix-ui/react-context": "1.1.2", 2520 + "@radix-ui/react-direction": "1.1.1", 2521 + "@radix-ui/react-id": "1.1.1", 2522 + "@radix-ui/react-menu": "2.1.16", 2523 + "@radix-ui/react-primitive": "2.1.3", 2524 + "@radix-ui/react-roving-focus": "1.1.11", 2525 + "@radix-ui/react-use-controllable-state": "1.2.2" 2526 + }, 2527 + "peerDependencies": { 2528 + "@types/react": "*", 2529 + "@types/react-dom": "*", 2530 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2531 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2532 + }, 2533 + "peerDependenciesMeta": { 2534 + "@types/react": { 2535 + "optional": true 2536 + }, 2537 + "@types/react-dom": { 2538 + "optional": true 2539 + } 2540 + } 2541 + }, 2542 + "node_modules/@radix-ui/react-navigation-menu": { 2543 + "version": "1.2.14", 2544 + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", 2545 + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", 2546 + "dependencies": { 2547 + "@radix-ui/primitive": "1.1.3", 2548 + "@radix-ui/react-collection": "1.1.7", 2549 + "@radix-ui/react-compose-refs": "1.1.2", 2550 + "@radix-ui/react-context": "1.1.2", 2551 + "@radix-ui/react-direction": "1.1.1", 2552 + "@radix-ui/react-dismissable-layer": "1.1.11", 2553 + "@radix-ui/react-id": "1.1.1", 2554 + "@radix-ui/react-presence": "1.1.5", 2555 + "@radix-ui/react-primitive": "2.1.3", 2556 + "@radix-ui/react-use-callback-ref": "1.1.1", 2557 + "@radix-ui/react-use-controllable-state": "1.2.2", 2558 + "@radix-ui/react-use-layout-effect": "1.1.1", 2559 + "@radix-ui/react-use-previous": "1.1.1", 2560 + "@radix-ui/react-visually-hidden": "1.2.3" 2561 + }, 2562 + "peerDependencies": { 2563 + "@types/react": "*", 2564 + "@types/react-dom": "*", 2565 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2566 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2567 + }, 2568 + "peerDependenciesMeta": { 2569 + "@types/react": { 2570 + "optional": true 2571 + }, 2572 + "@types/react-dom": { 2573 + "optional": true 2574 + } 2575 + } 2576 + }, 2577 + "node_modules/@radix-ui/react-one-time-password-field": { 2578 + "version": "0.1.8", 2579 + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", 2580 + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", 2581 + "dependencies": { 2582 + "@radix-ui/number": "1.1.1", 2583 + "@radix-ui/primitive": "1.1.3", 2584 + "@radix-ui/react-collection": "1.1.7", 2585 + "@radix-ui/react-compose-refs": "1.1.2", 2586 + "@radix-ui/react-context": "1.1.2", 2587 + "@radix-ui/react-direction": "1.1.1", 2588 + "@radix-ui/react-primitive": "2.1.3", 2589 + "@radix-ui/react-roving-focus": "1.1.11", 2590 + "@radix-ui/react-use-controllable-state": "1.2.2", 2591 + "@radix-ui/react-use-effect-event": "0.0.2", 2592 + "@radix-ui/react-use-is-hydrated": "0.1.0", 2593 + "@radix-ui/react-use-layout-effect": "1.1.1" 2594 + }, 2595 + "peerDependencies": { 2596 + "@types/react": "*", 2597 + "@types/react-dom": "*", 2598 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2599 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2600 + }, 2601 + "peerDependenciesMeta": { 2602 + "@types/react": { 2603 + "optional": true 2604 + }, 2605 + "@types/react-dom": { 2606 + "optional": true 2607 + } 2608 + } 2609 + }, 2610 + "node_modules/@radix-ui/react-password-toggle-field": { 2611 + "version": "0.1.3", 2612 + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", 2613 + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", 2614 + "dependencies": { 2615 + "@radix-ui/primitive": "1.1.3", 2616 + "@radix-ui/react-compose-refs": "1.1.2", 2617 + "@radix-ui/react-context": "1.1.2", 2618 + "@radix-ui/react-id": "1.1.1", 2619 + "@radix-ui/react-primitive": "2.1.3", 2620 + "@radix-ui/react-use-controllable-state": "1.2.2", 2621 + "@radix-ui/react-use-effect-event": "0.0.2", 2622 + "@radix-ui/react-use-is-hydrated": "0.1.0" 2623 + }, 2624 + "peerDependencies": { 2625 + "@types/react": "*", 2626 + "@types/react-dom": "*", 2627 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2628 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2629 + }, 2630 + "peerDependenciesMeta": { 2631 + "@types/react": { 2632 + "optional": true 2633 + }, 2634 + "@types/react-dom": { 2635 + "optional": true 2636 + } 2637 + } 2638 + }, 2639 + "node_modules/@radix-ui/react-popover": { 2640 + "version": "1.1.15", 2641 + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", 2642 + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", 2643 + "dependencies": { 2644 + "@radix-ui/primitive": "1.1.3", 2645 + "@radix-ui/react-compose-refs": "1.1.2", 2646 + "@radix-ui/react-context": "1.1.2", 2647 + "@radix-ui/react-dismissable-layer": "1.1.11", 2648 + "@radix-ui/react-focus-guards": "1.1.3", 2649 + "@radix-ui/react-focus-scope": "1.1.7", 2650 + "@radix-ui/react-id": "1.1.1", 2651 + "@radix-ui/react-popper": "1.2.8", 2652 + "@radix-ui/react-portal": "1.1.9", 2653 + "@radix-ui/react-presence": "1.1.5", 2654 + "@radix-ui/react-primitive": "2.1.3", 2655 + "@radix-ui/react-slot": "1.2.3", 2656 + "@radix-ui/react-use-controllable-state": "1.2.2", 2657 + "aria-hidden": "^1.2.4", 2658 + "react-remove-scroll": "^2.6.3" 2659 + }, 2660 + "peerDependencies": { 2661 + "@types/react": "*", 2662 + "@types/react-dom": "*", 2663 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2664 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2665 + }, 2666 + "peerDependenciesMeta": { 2667 + "@types/react": { 2668 + "optional": true 2669 + }, 2670 + "@types/react-dom": { 2671 + "optional": true 2672 + } 2673 + } 2674 + }, 2675 + "node_modules/@radix-ui/react-popper": { 2676 + "version": "1.2.8", 2677 + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", 2678 + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", 2679 + "dependencies": { 2680 + "@floating-ui/react-dom": "^2.0.0", 2681 + "@radix-ui/react-arrow": "1.1.7", 2682 + "@radix-ui/react-compose-refs": "1.1.2", 2683 + "@radix-ui/react-context": "1.1.2", 2684 + "@radix-ui/react-primitive": "2.1.3", 2685 + "@radix-ui/react-use-callback-ref": "1.1.1", 2686 + "@radix-ui/react-use-layout-effect": "1.1.1", 2687 + "@radix-ui/react-use-rect": "1.1.1", 2688 + "@radix-ui/react-use-size": "1.1.1", 2689 + "@radix-ui/rect": "1.1.1" 2690 + }, 2691 + "peerDependencies": { 2692 + "@types/react": "*", 2693 + "@types/react-dom": "*", 2694 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2695 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2696 + }, 2697 + "peerDependenciesMeta": { 2698 + "@types/react": { 2699 + "optional": true 2700 + }, 2701 + "@types/react-dom": { 2702 + "optional": true 2703 + } 2704 + } 2705 + }, 2706 + "node_modules/@radix-ui/react-portal": { 2707 + "version": "1.1.9", 2708 + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", 2709 + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", 2710 + "dependencies": { 2711 + "@radix-ui/react-primitive": "2.1.3", 2712 + "@radix-ui/react-use-layout-effect": "1.1.1" 2713 + }, 2714 + "peerDependencies": { 2715 + "@types/react": "*", 2716 + "@types/react-dom": "*", 2717 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2718 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2719 + }, 2720 + "peerDependenciesMeta": { 2721 + "@types/react": { 2722 + "optional": true 2723 + }, 2724 + "@types/react-dom": { 2725 + "optional": true 2726 + } 2727 + } 2728 + }, 2729 + "node_modules/@radix-ui/react-presence": { 2730 + "version": "1.1.5", 2731 + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", 2732 + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", 2733 + "dependencies": { 2734 + "@radix-ui/react-compose-refs": "1.1.2", 2735 + "@radix-ui/react-use-layout-effect": "1.1.1" 2736 + }, 2737 + "peerDependencies": { 2738 + "@types/react": "*", 2739 + "@types/react-dom": "*", 2740 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2741 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2742 + }, 2743 + "peerDependenciesMeta": { 2744 + "@types/react": { 2745 + "optional": true 2746 + }, 2747 + "@types/react-dom": { 2748 + "optional": true 2749 + } 2750 + } 2751 + }, 2752 + "node_modules/@radix-ui/react-primitive": { 2753 + "version": "2.1.3", 2754 + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", 2755 + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", 2756 + "dependencies": { 2757 + "@radix-ui/react-slot": "1.2.3" 2758 + }, 2759 + "peerDependencies": { 2760 + "@types/react": "*", 2761 + "@types/react-dom": "*", 2762 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2763 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2764 + }, 2765 + "peerDependenciesMeta": { 2766 + "@types/react": { 2767 + "optional": true 2768 + }, 2769 + "@types/react-dom": { 2770 + "optional": true 2771 + } 2772 + } 2773 + }, 2774 + "node_modules/@radix-ui/react-progress": { 2775 + "version": "1.1.7", 2776 + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", 2777 + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", 2778 + "dependencies": { 2779 + "@radix-ui/react-context": "1.1.2", 2780 + "@radix-ui/react-primitive": "2.1.3" 2781 + }, 2782 + "peerDependencies": { 2783 + "@types/react": "*", 2784 + "@types/react-dom": "*", 2785 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2786 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2787 + }, 2788 + "peerDependenciesMeta": { 2789 + "@types/react": { 2790 + "optional": true 2791 + }, 2792 + "@types/react-dom": { 2793 + "optional": true 2794 + } 2795 + } 2796 + }, 2797 + "node_modules/@radix-ui/react-radio-group": { 2798 + "version": "1.3.8", 2799 + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", 2800 + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", 2801 + "dependencies": { 2802 + "@radix-ui/primitive": "1.1.3", 2803 + "@radix-ui/react-compose-refs": "1.1.2", 2804 + "@radix-ui/react-context": "1.1.2", 2805 + "@radix-ui/react-direction": "1.1.1", 2806 + "@radix-ui/react-presence": "1.1.5", 2807 + "@radix-ui/react-primitive": "2.1.3", 2808 + "@radix-ui/react-roving-focus": "1.1.11", 2809 + "@radix-ui/react-use-controllable-state": "1.2.2", 2810 + "@radix-ui/react-use-previous": "1.1.1", 2811 + "@radix-ui/react-use-size": "1.1.1" 2812 + }, 2813 + "peerDependencies": { 2814 + "@types/react": "*", 2815 + "@types/react-dom": "*", 2816 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2817 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2818 + }, 2819 + "peerDependenciesMeta": { 2820 + "@types/react": { 2821 + "optional": true 2822 + }, 2823 + "@types/react-dom": { 2824 + "optional": true 2825 + } 2826 + } 2827 + }, 2828 + "node_modules/@radix-ui/react-roving-focus": { 2829 + "version": "1.1.11", 2830 + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", 2831 + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", 2832 + "dependencies": { 2833 + "@radix-ui/primitive": "1.1.3", 2834 + "@radix-ui/react-collection": "1.1.7", 2835 + "@radix-ui/react-compose-refs": "1.1.2", 2836 + "@radix-ui/react-context": "1.1.2", 2837 + "@radix-ui/react-direction": "1.1.1", 2838 + "@radix-ui/react-id": "1.1.1", 2839 + "@radix-ui/react-primitive": "2.1.3", 2840 + "@radix-ui/react-use-callback-ref": "1.1.1", 2841 + "@radix-ui/react-use-controllable-state": "1.2.2" 2842 + }, 2843 + "peerDependencies": { 2844 + "@types/react": "*", 2845 + "@types/react-dom": "*", 2846 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2847 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2848 + }, 2849 + "peerDependenciesMeta": { 2850 + "@types/react": { 2851 + "optional": true 2852 + }, 2853 + "@types/react-dom": { 2854 + "optional": true 2855 + } 2856 + } 2857 + }, 2858 + "node_modules/@radix-ui/react-scroll-area": { 2859 + "version": "1.2.10", 2860 + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", 2861 + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", 2862 + "dependencies": { 2863 + "@radix-ui/number": "1.1.1", 2864 + "@radix-ui/primitive": "1.1.3", 2865 + "@radix-ui/react-compose-refs": "1.1.2", 2866 + "@radix-ui/react-context": "1.1.2", 2867 + "@radix-ui/react-direction": "1.1.1", 2868 + "@radix-ui/react-presence": "1.1.5", 2869 + "@radix-ui/react-primitive": "2.1.3", 2870 + "@radix-ui/react-use-callback-ref": "1.1.1", 2871 + "@radix-ui/react-use-layout-effect": "1.1.1" 2872 + }, 2873 + "peerDependencies": { 2874 + "@types/react": "*", 2875 + "@types/react-dom": "*", 2876 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2877 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2878 + }, 2879 + "peerDependenciesMeta": { 2880 + "@types/react": { 2881 + "optional": true 2882 + }, 2883 + "@types/react-dom": { 2884 + "optional": true 2885 + } 2886 + } 2887 + }, 2888 + "node_modules/@radix-ui/react-select": { 2889 + "version": "2.2.6", 2890 + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", 2891 + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", 2892 + "dependencies": { 2893 + "@radix-ui/number": "1.1.1", 2894 + "@radix-ui/primitive": "1.1.3", 2895 + "@radix-ui/react-collection": "1.1.7", 2896 + "@radix-ui/react-compose-refs": "1.1.2", 2897 + "@radix-ui/react-context": "1.1.2", 2898 + "@radix-ui/react-direction": "1.1.1", 2899 + "@radix-ui/react-dismissable-layer": "1.1.11", 2900 + "@radix-ui/react-focus-guards": "1.1.3", 2901 + "@radix-ui/react-focus-scope": "1.1.7", 2902 + "@radix-ui/react-id": "1.1.1", 2903 + "@radix-ui/react-popper": "1.2.8", 2904 + "@radix-ui/react-portal": "1.1.9", 2905 + "@radix-ui/react-primitive": "2.1.3", 2906 + "@radix-ui/react-slot": "1.2.3", 2907 + "@radix-ui/react-use-callback-ref": "1.1.1", 2908 + "@radix-ui/react-use-controllable-state": "1.2.2", 2909 + "@radix-ui/react-use-layout-effect": "1.1.1", 2910 + "@radix-ui/react-use-previous": "1.1.1", 2911 + "@radix-ui/react-visually-hidden": "1.2.3", 2912 + "aria-hidden": "^1.2.4", 2913 + "react-remove-scroll": "^2.6.3" 2914 + }, 2915 + "peerDependencies": { 2916 + "@types/react": "*", 2917 + "@types/react-dom": "*", 2918 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2919 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2920 + }, 2921 + "peerDependenciesMeta": { 2922 + "@types/react": { 2923 + "optional": true 2924 + }, 2925 + "@types/react-dom": { 2926 + "optional": true 2927 + } 2928 + } 2929 + }, 2930 + "node_modules/@radix-ui/react-separator": { 2931 + "version": "1.1.7", 2932 + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", 2933 + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", 2934 + "dependencies": { 2935 + "@radix-ui/react-primitive": "2.1.3" 2936 + }, 2937 + "peerDependencies": { 2938 + "@types/react": "*", 2939 + "@types/react-dom": "*", 2940 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2941 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2942 + }, 2943 + "peerDependenciesMeta": { 2944 + "@types/react": { 2945 + "optional": true 2946 + }, 2947 + "@types/react-dom": { 2948 + "optional": true 2949 + } 2950 + } 2951 + }, 2952 + "node_modules/@radix-ui/react-slider": { 2953 + "version": "1.3.6", 2954 + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", 2955 + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", 2956 + "dependencies": { 2957 + "@radix-ui/number": "1.1.1", 2958 + "@radix-ui/primitive": "1.1.3", 2959 + "@radix-ui/react-collection": "1.1.7", 2960 + "@radix-ui/react-compose-refs": "1.1.2", 2961 + "@radix-ui/react-context": "1.1.2", 2962 + "@radix-ui/react-direction": "1.1.1", 2963 + "@radix-ui/react-primitive": "2.1.3", 2964 + "@radix-ui/react-use-controllable-state": "1.2.2", 2965 + "@radix-ui/react-use-layout-effect": "1.1.1", 2966 + "@radix-ui/react-use-previous": "1.1.1", 2967 + "@radix-ui/react-use-size": "1.1.1" 2968 + }, 2969 + "peerDependencies": { 2970 + "@types/react": "*", 2971 + "@types/react-dom": "*", 2972 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2973 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2974 + }, 2975 + "peerDependenciesMeta": { 2976 + "@types/react": { 2977 + "optional": true 2978 + }, 2979 + "@types/react-dom": { 2980 + "optional": true 2981 + } 2982 + } 2983 + }, 2984 + "node_modules/@radix-ui/react-slot": { 2985 + "version": "1.2.3", 2986 + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", 2987 + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", 2988 + "dependencies": { 2989 + "@radix-ui/react-compose-refs": "1.1.2" 2990 + }, 2991 + "peerDependencies": { 2992 + "@types/react": "*", 2993 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2994 + }, 2995 + "peerDependenciesMeta": { 2996 + "@types/react": { 2997 + "optional": true 2998 + } 2999 + } 3000 + }, 3001 + "node_modules/@radix-ui/react-switch": { 3002 + "version": "1.2.6", 3003 + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", 3004 + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", 3005 + "dependencies": { 3006 + "@radix-ui/primitive": "1.1.3", 3007 + "@radix-ui/react-compose-refs": "1.1.2", 3008 + "@radix-ui/react-context": "1.1.2", 3009 + "@radix-ui/react-primitive": "2.1.3", 3010 + "@radix-ui/react-use-controllable-state": "1.2.2", 3011 + "@radix-ui/react-use-previous": "1.1.1", 3012 + "@radix-ui/react-use-size": "1.1.1" 3013 + }, 3014 + "peerDependencies": { 3015 + "@types/react": "*", 3016 + "@types/react-dom": "*", 3017 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3018 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3019 + }, 3020 + "peerDependenciesMeta": { 3021 + "@types/react": { 3022 + "optional": true 3023 + }, 3024 + "@types/react-dom": { 3025 + "optional": true 3026 + } 3027 + } 3028 + }, 3029 + "node_modules/@radix-ui/react-tabs": { 3030 + "version": "1.1.13", 3031 + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", 3032 + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", 3033 + "dependencies": { 3034 + "@radix-ui/primitive": "1.1.3", 3035 + "@radix-ui/react-context": "1.1.2", 3036 + "@radix-ui/react-direction": "1.1.1", 3037 + "@radix-ui/react-id": "1.1.1", 3038 + "@radix-ui/react-presence": "1.1.5", 3039 + "@radix-ui/react-primitive": "2.1.3", 3040 + "@radix-ui/react-roving-focus": "1.1.11", 3041 + "@radix-ui/react-use-controllable-state": "1.2.2" 3042 + }, 3043 + "peerDependencies": { 3044 + "@types/react": "*", 3045 + "@types/react-dom": "*", 3046 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3047 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3048 + }, 3049 + "peerDependenciesMeta": { 3050 + "@types/react": { 3051 + "optional": true 3052 + }, 3053 + "@types/react-dom": { 3054 + "optional": true 3055 + } 3056 + } 3057 + }, 3058 + "node_modules/@radix-ui/react-toast": { 3059 + "version": "1.2.15", 3060 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", 3061 + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", 3062 + "dependencies": { 3063 + "@radix-ui/primitive": "1.1.3", 3064 + "@radix-ui/react-collection": "1.1.7", 3065 + "@radix-ui/react-compose-refs": "1.1.2", 3066 + "@radix-ui/react-context": "1.1.2", 3067 + "@radix-ui/react-dismissable-layer": "1.1.11", 3068 + "@radix-ui/react-portal": "1.1.9", 3069 + "@radix-ui/react-presence": "1.1.5", 3070 + "@radix-ui/react-primitive": "2.1.3", 3071 + "@radix-ui/react-use-callback-ref": "1.1.1", 3072 + "@radix-ui/react-use-controllable-state": "1.2.2", 3073 + "@radix-ui/react-use-layout-effect": "1.1.1", 3074 + "@radix-ui/react-visually-hidden": "1.2.3" 3075 + }, 3076 + "peerDependencies": { 3077 + "@types/react": "*", 3078 + "@types/react-dom": "*", 3079 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3080 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3081 + }, 3082 + "peerDependenciesMeta": { 3083 + "@types/react": { 3084 + "optional": true 3085 + }, 3086 + "@types/react-dom": { 3087 + "optional": true 3088 + } 3089 + } 3090 + }, 3091 + "node_modules/@radix-ui/react-toggle": { 3092 + "version": "1.1.10", 3093 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", 3094 + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", 3095 + "dependencies": { 3096 + "@radix-ui/primitive": "1.1.3", 3097 + "@radix-ui/react-primitive": "2.1.3", 3098 + "@radix-ui/react-use-controllable-state": "1.2.2" 3099 + }, 3100 + "peerDependencies": { 3101 + "@types/react": "*", 3102 + "@types/react-dom": "*", 3103 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3104 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3105 + }, 3106 + "peerDependenciesMeta": { 3107 + "@types/react": { 3108 + "optional": true 3109 + }, 3110 + "@types/react-dom": { 3111 + "optional": true 3112 + } 3113 + } 3114 + }, 3115 + "node_modules/@radix-ui/react-toggle-group": { 3116 + "version": "1.1.11", 3117 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", 3118 + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", 3119 + "dependencies": { 3120 + "@radix-ui/primitive": "1.1.3", 3121 + "@radix-ui/react-context": "1.1.2", 3122 + "@radix-ui/react-direction": "1.1.1", 3123 + "@radix-ui/react-primitive": "2.1.3", 3124 + "@radix-ui/react-roving-focus": "1.1.11", 3125 + "@radix-ui/react-toggle": "1.1.10", 3126 + "@radix-ui/react-use-controllable-state": "1.2.2" 3127 + }, 3128 + "peerDependencies": { 3129 + "@types/react": "*", 3130 + "@types/react-dom": "*", 3131 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3132 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3133 + }, 3134 + "peerDependenciesMeta": { 3135 + "@types/react": { 3136 + "optional": true 3137 + }, 3138 + "@types/react-dom": { 3139 + "optional": true 3140 + } 3141 + } 3142 + }, 3143 + "node_modules/@radix-ui/react-toolbar": { 3144 + "version": "1.1.11", 3145 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", 3146 + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", 3147 + "dependencies": { 3148 + "@radix-ui/primitive": "1.1.3", 3149 + "@radix-ui/react-context": "1.1.2", 3150 + "@radix-ui/react-direction": "1.1.1", 3151 + "@radix-ui/react-primitive": "2.1.3", 3152 + "@radix-ui/react-roving-focus": "1.1.11", 3153 + "@radix-ui/react-separator": "1.1.7", 3154 + "@radix-ui/react-toggle-group": "1.1.11" 3155 + }, 3156 + "peerDependencies": { 3157 + "@types/react": "*", 3158 + "@types/react-dom": "*", 3159 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3160 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3161 + }, 3162 + "peerDependenciesMeta": { 3163 + "@types/react": { 3164 + "optional": true 3165 + }, 3166 + "@types/react-dom": { 3167 + "optional": true 3168 + } 3169 + } 3170 + }, 3171 + "node_modules/@radix-ui/react-tooltip": { 3172 + "version": "1.2.8", 3173 + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", 3174 + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", 3175 + "dependencies": { 3176 + "@radix-ui/primitive": "1.1.3", 3177 + "@radix-ui/react-compose-refs": "1.1.2", 3178 + "@radix-ui/react-context": "1.1.2", 3179 + "@radix-ui/react-dismissable-layer": "1.1.11", 3180 + "@radix-ui/react-id": "1.1.1", 3181 + "@radix-ui/react-popper": "1.2.8", 3182 + "@radix-ui/react-portal": "1.1.9", 3183 + "@radix-ui/react-presence": "1.1.5", 3184 + "@radix-ui/react-primitive": "2.1.3", 3185 + "@radix-ui/react-slot": "1.2.3", 3186 + "@radix-ui/react-use-controllable-state": "1.2.2", 3187 + "@radix-ui/react-visually-hidden": "1.2.3" 3188 + }, 3189 + "peerDependencies": { 3190 + "@types/react": "*", 3191 + "@types/react-dom": "*", 3192 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3193 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3194 + }, 3195 + "peerDependenciesMeta": { 3196 + "@types/react": { 3197 + "optional": true 3198 + }, 3199 + "@types/react-dom": { 3200 + "optional": true 3201 + } 3202 + } 3203 + }, 3204 + "node_modules/@radix-ui/react-use-callback-ref": { 3205 + "version": "1.1.1", 3206 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", 3207 + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", 3208 + "peerDependencies": { 3209 + "@types/react": "*", 3210 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3211 + }, 3212 + "peerDependenciesMeta": { 3213 + "@types/react": { 3214 + "optional": true 3215 + } 3216 + } 3217 + }, 3218 + "node_modules/@radix-ui/react-use-controllable-state": { 3219 + "version": "1.2.2", 3220 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", 3221 + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", 3222 + "dependencies": { 3223 + "@radix-ui/react-use-effect-event": "0.0.2", 3224 + "@radix-ui/react-use-layout-effect": "1.1.1" 3225 + }, 3226 + "peerDependencies": { 3227 + "@types/react": "*", 3228 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3229 + }, 3230 + "peerDependenciesMeta": { 3231 + "@types/react": { 3232 + "optional": true 3233 + } 3234 + } 3235 + }, 3236 + "node_modules/@radix-ui/react-use-effect-event": { 3237 + "version": "0.0.2", 3238 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", 3239 + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", 3240 + "dependencies": { 3241 + "@radix-ui/react-use-layout-effect": "1.1.1" 3242 + }, 3243 + "peerDependencies": { 3244 + "@types/react": "*", 3245 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3246 + }, 3247 + "peerDependenciesMeta": { 3248 + "@types/react": { 3249 + "optional": true 3250 + } 3251 + } 3252 + }, 3253 + "node_modules/@radix-ui/react-use-escape-keydown": { 3254 + "version": "1.1.1", 3255 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", 3256 + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", 3257 + "dependencies": { 3258 + "@radix-ui/react-use-callback-ref": "1.1.1" 3259 + }, 3260 + "peerDependencies": { 3261 + "@types/react": "*", 3262 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3263 + }, 3264 + "peerDependenciesMeta": { 3265 + "@types/react": { 3266 + "optional": true 3267 + } 3268 + } 3269 + }, 3270 + "node_modules/@radix-ui/react-use-is-hydrated": { 3271 + "version": "0.1.0", 3272 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", 3273 + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", 3274 + "dependencies": { 3275 + "use-sync-external-store": "^1.5.0" 3276 + }, 3277 + "peerDependencies": { 3278 + "@types/react": "*", 3279 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3280 + }, 3281 + "peerDependenciesMeta": { 3282 + "@types/react": { 3283 + "optional": true 3284 + } 3285 + } 3286 + }, 3287 + "node_modules/@radix-ui/react-use-layout-effect": { 3288 + "version": "1.1.1", 3289 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", 3290 + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", 3291 + "peerDependencies": { 3292 + "@types/react": "*", 3293 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3294 + }, 3295 + "peerDependenciesMeta": { 3296 + "@types/react": { 3297 + "optional": true 3298 + } 3299 + } 3300 + }, 3301 + "node_modules/@radix-ui/react-use-previous": { 3302 + "version": "1.1.1", 3303 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", 3304 + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", 3305 + "peerDependencies": { 3306 + "@types/react": "*", 3307 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3308 + }, 3309 + "peerDependenciesMeta": { 3310 + "@types/react": { 3311 + "optional": true 3312 + } 3313 + } 3314 + }, 3315 + "node_modules/@radix-ui/react-use-rect": { 3316 + "version": "1.1.1", 3317 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", 3318 + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", 3319 + "dependencies": { 3320 + "@radix-ui/rect": "1.1.1" 3321 + }, 3322 + "peerDependencies": { 3323 + "@types/react": "*", 3324 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3325 + }, 3326 + "peerDependenciesMeta": { 3327 + "@types/react": { 3328 + "optional": true 3329 + } 3330 + } 3331 + }, 3332 + "node_modules/@radix-ui/react-use-size": { 3333 + "version": "1.1.1", 3334 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", 3335 + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", 3336 + "dependencies": { 3337 + "@radix-ui/react-use-layout-effect": "1.1.1" 3338 + }, 3339 + "peerDependencies": { 3340 + "@types/react": "*", 3341 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3342 + }, 3343 + "peerDependenciesMeta": { 3344 + "@types/react": { 3345 + "optional": true 3346 + } 3347 + } 3348 + }, 3349 + "node_modules/@radix-ui/react-visually-hidden": { 3350 + "version": "1.2.3", 3351 + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", 3352 + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", 3353 + "dependencies": { 3354 + "@radix-ui/react-primitive": "2.1.3" 3355 + }, 3356 + "peerDependencies": { 3357 + "@types/react": "*", 3358 + "@types/react-dom": "*", 3359 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3360 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3361 + }, 3362 + "peerDependenciesMeta": { 3363 + "@types/react": { 3364 + "optional": true 3365 + }, 3366 + "@types/react-dom": { 3367 + "optional": true 3368 + } 3369 + } 3370 + }, 3371 + "node_modules/@radix-ui/rect": { 3372 + "version": "1.1.1", 3373 + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", 3374 + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" 3375 + }, 3376 "node_modules/@rolldown/pluginutils": { 3377 "version": "1.0.0-beta.27", 3378 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", ··· 3687 "solid-js": "^1.6.12" 3688 } 3689 }, 3690 + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { 3691 + "version": "8.0.0", 3692 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", 3693 + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", 3694 + "dev": true, 3695 + "license": "MIT", 3696 + "engines": { 3697 + "node": ">=14" 3698 + }, 3699 + "funding": { 3700 + "type": "github", 3701 + "url": "https://github.com/sponsors/gregberge" 3702 + }, 3703 + "peerDependencies": { 3704 + "@babel/core": "^7.0.0-0" 3705 + } 3706 + }, 3707 + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { 3708 + "version": "8.0.0", 3709 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", 3710 + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", 3711 + "dev": true, 3712 + "license": "MIT", 3713 + "engines": { 3714 + "node": ">=14" 3715 + }, 3716 + "funding": { 3717 + "type": "github", 3718 + "url": "https://github.com/sponsors/gregberge" 3719 + }, 3720 + "peerDependencies": { 3721 + "@babel/core": "^7.0.0-0" 3722 + } 3723 + }, 3724 + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { 3725 + "version": "8.0.0", 3726 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", 3727 + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", 3728 + "dev": true, 3729 + "license": "MIT", 3730 + "engines": { 3731 + "node": ">=14" 3732 + }, 3733 + "funding": { 3734 + "type": "github", 3735 + "url": "https://github.com/sponsors/gregberge" 3736 + }, 3737 + "peerDependencies": { 3738 + "@babel/core": "^7.0.0-0" 3739 + } 3740 + }, 3741 + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { 3742 + "version": "8.0.0", 3743 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", 3744 + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", 3745 + "dev": true, 3746 + "license": "MIT", 3747 + "engines": { 3748 + "node": ">=14" 3749 + }, 3750 + "funding": { 3751 + "type": "github", 3752 + "url": "https://github.com/sponsors/gregberge" 3753 + }, 3754 + "peerDependencies": { 3755 + "@babel/core": "^7.0.0-0" 3756 + } 3757 + }, 3758 + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { 3759 + "version": "8.0.0", 3760 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", 3761 + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", 3762 + "dev": true, 3763 + "license": "MIT", 3764 + "engines": { 3765 + "node": ">=14" 3766 + }, 3767 + "funding": { 3768 + "type": "github", 3769 + "url": "https://github.com/sponsors/gregberge" 3770 + }, 3771 + "peerDependencies": { 3772 + "@babel/core": "^7.0.0-0" 3773 + } 3774 + }, 3775 + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { 3776 + "version": "8.0.0", 3777 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", 3778 + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", 3779 + "dev": true, 3780 + "license": "MIT", 3781 + "engines": { 3782 + "node": ">=14" 3783 + }, 3784 + "funding": { 3785 + "type": "github", 3786 + "url": "https://github.com/sponsors/gregberge" 3787 + }, 3788 + "peerDependencies": { 3789 + "@babel/core": "^7.0.0-0" 3790 + } 3791 + }, 3792 + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { 3793 + "version": "8.1.0", 3794 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", 3795 + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", 3796 + "dev": true, 3797 + "license": "MIT", 3798 + "engines": { 3799 + "node": ">=14" 3800 + }, 3801 + "funding": { 3802 + "type": "github", 3803 + "url": "https://github.com/sponsors/gregberge" 3804 + }, 3805 + "peerDependencies": { 3806 + "@babel/core": "^7.0.0-0" 3807 + } 3808 + }, 3809 + "node_modules/@svgr/babel-plugin-transform-svg-component": { 3810 + "version": "8.0.0", 3811 + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", 3812 + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", 3813 + "dev": true, 3814 + "license": "MIT", 3815 + "engines": { 3816 + "node": ">=12" 3817 + }, 3818 + "funding": { 3819 + "type": "github", 3820 + "url": "https://github.com/sponsors/gregberge" 3821 + }, 3822 + "peerDependencies": { 3823 + "@babel/core": "^7.0.0-0" 3824 + } 3825 + }, 3826 + "node_modules/@svgr/babel-preset": { 3827 + "version": "8.1.0", 3828 + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", 3829 + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", 3830 + "dev": true, 3831 + "license": "MIT", 3832 + "dependencies": { 3833 + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", 3834 + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", 3835 + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", 3836 + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", 3837 + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", 3838 + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", 3839 + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", 3840 + "@svgr/babel-plugin-transform-svg-component": "8.0.0" 3841 + }, 3842 + "engines": { 3843 + "node": ">=14" 3844 + }, 3845 + "funding": { 3846 + "type": "github", 3847 + "url": "https://github.com/sponsors/gregberge" 3848 + }, 3849 + "peerDependencies": { 3850 + "@babel/core": "^7.0.0-0" 3851 + } 3852 + }, 3853 + "node_modules/@svgr/core": { 3854 + "version": "8.1.0", 3855 + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", 3856 + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", 3857 + "dev": true, 3858 + "license": "MIT", 3859 + "dependencies": { 3860 + "@babel/core": "^7.21.3", 3861 + "@svgr/babel-preset": "8.1.0", 3862 + "camelcase": "^6.2.0", 3863 + "cosmiconfig": "^8.1.3", 3864 + "snake-case": "^3.0.4" 3865 + }, 3866 + "engines": { 3867 + "node": ">=14" 3868 + }, 3869 + "funding": { 3870 + "type": "github", 3871 + "url": "https://github.com/sponsors/gregberge" 3872 + } 3873 + }, 3874 + "node_modules/@svgr/hast-util-to-babel-ast": { 3875 + "version": "8.0.0", 3876 + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", 3877 + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", 3878 + "dev": true, 3879 + "license": "MIT", 3880 + "dependencies": { 3881 + "@babel/types": "^7.21.3", 3882 + "entities": "^4.4.0" 3883 + }, 3884 + "engines": { 3885 + "node": ">=14" 3886 + }, 3887 + "funding": { 3888 + "type": "github", 3889 + "url": "https://github.com/sponsors/gregberge" 3890 + } 3891 + }, 3892 + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { 3893 + "version": "4.5.0", 3894 + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 3895 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 3896 + "dev": true, 3897 + "license": "BSD-2-Clause", 3898 + "engines": { 3899 + "node": ">=0.12" 3900 + }, 3901 + "funding": { 3902 + "url": "https://github.com/fb55/entities?sponsor=1" 3903 + } 3904 + }, 3905 + "node_modules/@svgr/plugin-jsx": { 3906 + "version": "8.1.0", 3907 + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", 3908 + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", 3909 + "dev": true, 3910 + "license": "MIT", 3911 + "dependencies": { 3912 + "@babel/core": "^7.21.3", 3913 + "@svgr/babel-preset": "8.1.0", 3914 + "@svgr/hast-util-to-babel-ast": "8.0.0", 3915 + "svg-parser": "^2.0.4" 3916 + }, 3917 + "engines": { 3918 + "node": ">=14" 3919 + }, 3920 + "funding": { 3921 + "type": "github", 3922 + "url": "https://github.com/sponsors/gregberge" 3923 + }, 3924 + "peerDependencies": { 3925 + "@svgr/core": "*" 3926 + } 3927 + }, 3928 "node_modules/@svta/common-media-library": { 3929 "version": "0.12.4", 3930 "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz", ··· 4752 "@types/react": "^19.0.0" 4753 } 4754 }, 4755 + "node_modules/@types/trusted-types": { 4756 + "version": "2.0.7", 4757 + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 4758 + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", 4759 + "license": "MIT", 4760 + "optional": true 4761 + }, 4762 "node_modules/@typescript-eslint/eslint-plugin": { 4763 "version": "8.46.1", 4764 "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", ··· 5283 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 5284 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 5285 "dev": true, 5286 + "license": "Python-2.0" 5287 + }, 5288 + "node_modules/aria-hidden": { 5289 + "version": "1.2.6", 5290 + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", 5291 + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", 5292 + "dependencies": { 5293 + "tslib": "^2.0.0" 5294 + }, 5295 + "engines": { 5296 + "node": ">=10" 5297 + } 5298 }, 5299 "node_modules/aria-query": { 5300 "version": "5.3.0", ··· 5706 "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 5707 "dev": true, 5708 "license": "MIT", 5709 "engines": { 5710 "node": ">=6" 5711 } 5712 }, 5713 + "node_modules/camelcase": { 5714 + "version": "6.3.0", 5715 + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 5716 + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 5717 + "dev": true, 5718 + "license": "MIT", 5719 + "engines": { 5720 + "node": ">=10" 5721 + }, 5722 + "funding": { 5723 + "url": "https://github.com/sponsors/sindresorhus" 5724 + } 5725 + }, 5726 "node_modules/caniuse-lite": { 5727 "version": "1.0.30001737", 5728 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", ··· 5913 "dev": true, 5914 "license": "MIT" 5915 }, 5916 + "node_modules/confbox": { 5917 + "version": "0.2.2", 5918 + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", 5919 + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", 5920 + "dev": true, 5921 + "license": "MIT" 5922 + }, 5923 "node_modules/convert-source-map": { 5924 "version": "2.0.0", 5925 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", ··· 5943 "url": "https://opencollective.com/core-js" 5944 } 5945 }, 5946 + "node_modules/cosmiconfig": { 5947 + "version": "8.3.6", 5948 + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", 5949 + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", 5950 + "dev": true, 5951 + "license": "MIT", 5952 + "dependencies": { 5953 + "import-fresh": "^3.3.0", 5954 + "js-yaml": "^4.1.0", 5955 + "parse-json": "^5.2.0", 5956 + "path-type": "^4.0.0" 5957 + }, 5958 + "engines": { 5959 + "node": ">=14" 5960 + }, 5961 + "funding": { 5962 + "url": "https://github.com/sponsors/d-fischer" 5963 + }, 5964 + "peerDependencies": { 5965 + "typescript": ">=4.9.5" 5966 + }, 5967 + "peerDependenciesMeta": { 5968 + "typescript": { 5969 + "optional": true 5970 + } 5971 + } 5972 + }, 5973 "node_modules/cross-spawn": { 5974 "version": "7.0.6", 5975 "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", ··· 6109 } 6110 }, 6111 "node_modules/debug": { 6112 + "version": "4.4.3", 6113 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 6114 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 6115 "license": "MIT", 6116 "dependencies": { 6117 "ms": "^2.1.3" ··· 6205 "node": ">=8" 6206 } 6207 }, 6208 + "node_modules/detect-node-es": { 6209 + "version": "1.1.0", 6210 + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", 6211 + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" 6212 + }, 6213 "node_modules/diff": { 6214 "version": "8.0.2", 6215 "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", ··· 6239 "dev": true, 6240 "license": "MIT" 6241 }, 6242 + "node_modules/dompurify": { 6243 + "version": "3.3.0", 6244 + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", 6245 + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", 6246 + "license": "(MPL-2.0 OR Apache-2.0)", 6247 + "optionalDependencies": { 6248 + "@types/trusted-types": "^2.0.7" 6249 + } 6250 + }, 6251 + "node_modules/dot-case": { 6252 + "version": "3.0.4", 6253 + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", 6254 + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", 6255 + "dev": true, 6256 + "license": "MIT", 6257 + "dependencies": { 6258 + "no-case": "^3.0.4", 6259 + "tslib": "^2.0.3" 6260 + } 6261 + }, 6262 "node_modules/dunder-proto": { 6263 "version": "1.0.1", 6264 "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", ··· 6304 }, 6305 "funding": { 6306 "url": "https://github.com/fb55/entities?sponsor=1" 6307 + } 6308 + }, 6309 + "node_modules/error-ex": { 6310 + "version": "1.3.4", 6311 + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", 6312 + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", 6313 + "dev": true, 6314 + "license": "MIT", 6315 + "dependencies": { 6316 + "is-arrayish": "^0.2.1" 6317 } 6318 }, 6319 "node_modules/es-abstract": { ··· 6977 "node": ">=0.10.0" 6978 } 6979 }, 6980 + "node_modules/eventemitter3": { 6981 + "version": "5.0.1", 6982 + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", 6983 + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", 6984 + "license": "MIT" 6985 + }, 6986 "node_modules/expect-type": { 6987 "version": "1.2.2", 6988 "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", ··· 6992 "engines": { 6993 "node": ">=12.0.0" 6994 } 6995 + }, 6996 + "node_modules/exsolve": { 6997 + "version": "1.0.7", 6998 + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", 6999 + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", 7000 + "dev": true, 7001 + "license": "MIT" 7002 }, 7003 "node_modules/fast-deep-equal": { 7004 "version": "3.1.3", ··· 7229 }, 7230 "funding": { 7231 "url": "https://github.com/sponsors/ljharb" 7232 + } 7233 + }, 7234 + "node_modules/get-nonce": { 7235 + "version": "1.0.1", 7236 + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", 7237 + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", 7238 + "engines": { 7239 + "node": ">=6" 7240 } 7241 }, 7242 "node_modules/get-proto": { ··· 7546 "node": ">= 14" 7547 } 7548 }, 7549 + "node_modules/i": { 7550 + "version": "0.3.7", 7551 + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", 7552 + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", 7553 + "engines": { 7554 + "node": ">=0.4" 7555 + } 7556 + }, 7557 + "node_modules/iconify-icon": { 7558 + "version": "3.0.1", 7559 + "resolved": "https://registry.npmjs.org/iconify-icon/-/iconify-icon-3.0.1.tgz", 7560 + "integrity": "sha512-M3/kH3C+e/ufhmQuOSYSb1Ri1ImJ+ZEQYcVRMKnlSc8Nrdoy+iY9YvFnplX8t/3aCRuo5wN4RVPtCSHGnbt8dg==", 7561 + "dev": true, 7562 + "license": "MIT", 7563 + "dependencies": { 7564 + "@iconify/types": "^2.0.0" 7565 + }, 7566 + "funding": { 7567 + "url": "https://github.com/sponsors/cyberalien" 7568 + } 7569 + }, 7570 "node_modules/iconv-lite": { 7571 "version": "0.6.3", 7572 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", ··· 7609 "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 7610 "dev": true, 7611 "license": "MIT", 7612 "dependencies": { 7613 "parent-module": "^1.0.0", 7614 "resolve-from": "^4.0.0" ··· 7696 "funding": { 7697 "url": "https://github.com/sponsors/ljharb" 7698 } 7699 + }, 7700 + "node_modules/is-arrayish": { 7701 + "version": "0.2.1", 7702 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 7703 + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", 7704 + "dev": true, 7705 + "license": "MIT" 7706 }, 7707 "node_modules/is-async-function": { 7708 "version": "2.1.1", ··· 8227 "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 8228 "dev": true, 8229 "license": "MIT", 8230 "dependencies": { 8231 "argparse": "^2.0.1" 8232 }, ··· 8294 "license": "MIT", 8295 "peer": true 8296 }, 8297 + "node_modules/json-parse-even-better-errors": { 8298 + "version": "2.3.1", 8299 + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", 8300 + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", 8301 + "dev": true, 8302 + "license": "MIT" 8303 + }, 8304 "node_modules/json-schema-traverse": { 8305 "version": "0.4.1", 8306 "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", ··· 8356 "json-buffer": "3.0.1" 8357 } 8358 }, 8359 + "node_modules/kolorist": { 8360 + "version": "1.8.0", 8361 + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", 8362 + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", 8363 + "dev": true, 8364 + "license": "MIT" 8365 + }, 8366 "node_modules/levn": { 8367 "version": "0.4.1", 8368 "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", ··· 8615 "url": "https://opencollective.com/parcel" 8616 } 8617 }, 8618 + "node_modules/lines-and-columns": { 8619 + "version": "1.2.4", 8620 + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 8621 + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 8622 + "dev": true, 8623 + "license": "MIT" 8624 + }, 8625 + "node_modules/local-pkg": { 8626 + "version": "1.1.2", 8627 + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", 8628 + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", 8629 + "dev": true, 8630 + "license": "MIT", 8631 + "dependencies": { 8632 + "mlly": "^1.7.4", 8633 + "pkg-types": "^2.3.0", 8634 + "quansync": "^0.2.11" 8635 + }, 8636 + "engines": { 8637 + "node": ">=14" 8638 + }, 8639 + "funding": { 8640 + "url": "https://github.com/sponsors/antfu" 8641 + } 8642 + }, 8643 "node_modules/localforage": { 8644 "version": "1.10.0", 8645 "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", ··· 8666 "url": "https://github.com/sponsors/sindresorhus" 8667 } 8668 }, 8669 + "node_modules/lodash.clonedeep": { 8670 + "version": "4.5.0", 8671 + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", 8672 + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", 8673 + "license": "MIT" 8674 + }, 8675 "node_modules/lodash.merge": { 8676 "version": "4.6.2", 8677 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", ··· 8699 "dev": true, 8700 "license": "MIT" 8701 }, 8702 + "node_modules/lower-case": { 8703 + "version": "2.0.2", 8704 + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", 8705 + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", 8706 + "dev": true, 8707 + "license": "MIT", 8708 + "dependencies": { 8709 + "tslib": "^2.0.3" 8710 + } 8711 + }, 8712 "node_modules/lru-cache": { 8713 "version": "5.1.1", 8714 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", ··· 8729 } 8730 }, 8731 "node_modules/magic-string": { 8732 + "version": "0.30.19", 8733 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", 8734 + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", 8735 "license": "MIT", 8736 "dependencies": { 8737 "@jridgewell/sourcemap-codec": "^1.5.5" ··· 8836 "url": "https://github.com/sponsors/isaacs" 8837 } 8838 }, 8839 + "node_modules/mlly": { 8840 + "version": "1.8.0", 8841 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", 8842 + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", 8843 + "dev": true, 8844 + "license": "MIT", 8845 + "dependencies": { 8846 + "acorn": "^8.15.0", 8847 + "pathe": "^2.0.3", 8848 + "pkg-types": "^1.3.1", 8849 + "ufo": "^1.6.1" 8850 + } 8851 + }, 8852 + "node_modules/mlly/node_modules/confbox": { 8853 + "version": "0.1.8", 8854 + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", 8855 + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", 8856 + "dev": true, 8857 + "license": "MIT" 8858 + }, 8859 + "node_modules/mlly/node_modules/pkg-types": { 8860 + "version": "1.3.1", 8861 + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", 8862 + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", 8863 + "dev": true, 8864 + "license": "MIT", 8865 + "dependencies": { 8866 + "confbox": "^0.1.8", 8867 + "mlly": "^1.7.4", 8868 + "pathe": "^2.0.1" 8869 + } 8870 + }, 8871 "node_modules/ms": { 8872 "version": "2.1.3", 8873 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 8917 "dev": true, 8918 "license": "MIT" 8919 }, 8920 + "node_modules/no-case": { 8921 + "version": "3.0.4", 8922 + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", 8923 + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", 8924 + "dev": true, 8925 + "license": "MIT", 8926 + "dependencies": { 8927 + "lower-case": "^2.0.2", 8928 + "tslib": "^2.0.3" 8929 + } 8930 + }, 8931 "node_modules/node-releases": { 8932 "version": "2.0.19", 8933 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", ··· 8943 "node": ">=0.10.0" 8944 } 8945 }, 8946 + "node_modules/npm": { 8947 + "version": "11.6.2", 8948 + "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.2.tgz", 8949 + "integrity": "sha512-7iKzNfy8lWYs3zq4oFPa8EXZz5xt9gQNKJZau3B1ErLBb6bF7sBJ00x09485DOvRT2l5Gerbl3VlZNT57MxJVA==", 8950 + "bundleDependencies": [ 8951 + "@isaacs/string-locale-compare", 8952 + "@npmcli/arborist", 8953 + "@npmcli/config", 8954 + "@npmcli/fs", 8955 + "@npmcli/map-workspaces", 8956 + "@npmcli/package-json", 8957 + "@npmcli/promise-spawn", 8958 + "@npmcli/redact", 8959 + "@npmcli/run-script", 8960 + "@sigstore/tuf", 8961 + "abbrev", 8962 + "archy", 8963 + "cacache", 8964 + "chalk", 8965 + "ci-info", 8966 + "cli-columns", 8967 + "fastest-levenshtein", 8968 + "fs-minipass", 8969 + "glob", 8970 + "graceful-fs", 8971 + "hosted-git-info", 8972 + "ini", 8973 + "init-package-json", 8974 + "is-cidr", 8975 + "json-parse-even-better-errors", 8976 + "libnpmaccess", 8977 + "libnpmdiff", 8978 + "libnpmexec", 8979 + "libnpmfund", 8980 + "libnpmorg", 8981 + "libnpmpack", 8982 + "libnpmpublish", 8983 + "libnpmsearch", 8984 + "libnpmteam", 8985 + "libnpmversion", 8986 + "make-fetch-happen", 8987 + "minimatch", 8988 + "minipass", 8989 + "minipass-pipeline", 8990 + "ms", 8991 + "node-gyp", 8992 + "nopt", 8993 + "npm-audit-report", 8994 + "npm-install-checks", 8995 + "npm-package-arg", 8996 + "npm-pick-manifest", 8997 + "npm-profile", 8998 + "npm-registry-fetch", 8999 + "npm-user-validate", 9000 + "p-map", 9001 + "pacote", 9002 + "parse-conflict-json", 9003 + "proc-log", 9004 + "qrcode-terminal", 9005 + "read", 9006 + "semver", 9007 + "spdx-expression-parse", 9008 + "ssri", 9009 + "supports-color", 9010 + "tar", 9011 + "text-table", 9012 + "tiny-relative-date", 9013 + "treeverse", 9014 + "validate-npm-package-name", 9015 + "which" 9016 + ], 9017 + "license": "Artistic-2.0", 9018 + "workspaces": [ 9019 + "docs", 9020 + "smoke-tests", 9021 + "mock-globals", 9022 + "mock-registry", 9023 + "workspaces/*" 9024 + ], 9025 + "dependencies": { 9026 + "@isaacs/string-locale-compare": "^1.1.0", 9027 + "@npmcli/arborist": "^9.1.6", 9028 + "@npmcli/config": "^10.4.2", 9029 + "@npmcli/fs": "^4.0.0", 9030 + "@npmcli/map-workspaces": "^5.0.0", 9031 + "@npmcli/package-json": "^7.0.1", 9032 + "@npmcli/promise-spawn": "^8.0.3", 9033 + "@npmcli/redact": "^3.2.2", 9034 + "@npmcli/run-script": "^10.0.0", 9035 + "@sigstore/tuf": "^4.0.0", 9036 + "abbrev": "^3.0.1", 9037 + "archy": "~1.0.0", 9038 + "cacache": "^20.0.1", 9039 + "chalk": "^5.6.2", 9040 + "ci-info": "^4.3.1", 9041 + "cli-columns": "^4.0.0", 9042 + "fastest-levenshtein": "^1.0.16", 9043 + "fs-minipass": "^3.0.3", 9044 + "glob": "^11.0.3", 9045 + "graceful-fs": "^4.2.11", 9046 + "hosted-git-info": "^9.0.2", 9047 + "ini": "^5.0.0", 9048 + "init-package-json": "^8.2.2", 9049 + "is-cidr": "^6.0.1", 9050 + "json-parse-even-better-errors": "^4.0.0", 9051 + "libnpmaccess": "^10.0.3", 9052 + "libnpmdiff": "^8.0.9", 9053 + "libnpmexec": "^10.1.8", 9054 + "libnpmfund": "^7.0.9", 9055 + "libnpmorg": "^8.0.1", 9056 + "libnpmpack": "^9.0.9", 9057 + "libnpmpublish": "^11.1.2", 9058 + "libnpmsearch": "^9.0.1", 9059 + "libnpmteam": "^8.0.2", 9060 + "libnpmversion": "^8.0.2", 9061 + "make-fetch-happen": "^15.0.2", 9062 + "minimatch": "^10.0.3", 9063 + "minipass": "^7.1.1", 9064 + "minipass-pipeline": "^1.2.4", 9065 + "ms": "^2.1.2", 9066 + "node-gyp": "^11.4.2", 9067 + "nopt": "^8.1.0", 9068 + "npm-audit-report": "^6.0.0", 9069 + "npm-install-checks": "^7.1.2", 9070 + "npm-package-arg": "^13.0.1", 9071 + "npm-pick-manifest": "^11.0.1", 9072 + "npm-profile": "^12.0.0", 9073 + "npm-registry-fetch": "^19.0.0", 9074 + "npm-user-validate": "^3.0.0", 9075 + "p-map": "^7.0.3", 9076 + "pacote": "^21.0.3", 9077 + "parse-conflict-json": "^4.0.0", 9078 + "proc-log": "^5.0.0", 9079 + "qrcode-terminal": "^0.12.0", 9080 + "read": "^4.1.0", 9081 + "semver": "^7.7.3", 9082 + "spdx-expression-parse": "^4.0.0", 9083 + "ssri": "^12.0.0", 9084 + "supports-color": "^10.2.2", 9085 + "tar": "^7.5.1", 9086 + "text-table": "~0.2.0", 9087 + "tiny-relative-date": "^2.0.2", 9088 + "treeverse": "^3.0.0", 9089 + "validate-npm-package-name": "^6.0.2", 9090 + "which": "^5.0.0" 9091 + }, 9092 + "bin": { 9093 + "npm": "bin/npm-cli.js", 9094 + "npx": "bin/npx-cli.js" 9095 + }, 9096 + "engines": { 9097 + "node": "^20.17.0 || >=22.9.0" 9098 + } 9099 + }, 9100 + "node_modules/npm/node_modules/@isaacs/balanced-match": { 9101 + "version": "4.0.1", 9102 + "inBundle": true, 9103 + "license": "MIT", 9104 + "engines": { 9105 + "node": "20 || >=22" 9106 + } 9107 + }, 9108 + "node_modules/npm/node_modules/@isaacs/brace-expansion": { 9109 + "version": "5.0.0", 9110 + "inBundle": true, 9111 + "license": "MIT", 9112 + "dependencies": { 9113 + "@isaacs/balanced-match": "^4.0.1" 9114 + }, 9115 + "engines": { 9116 + "node": "20 || >=22" 9117 + } 9118 + }, 9119 + "node_modules/npm/node_modules/@isaacs/cliui": { 9120 + "version": "8.0.2", 9121 + "inBundle": true, 9122 + "license": "ISC", 9123 + "dependencies": { 9124 + "string-width": "^5.1.2", 9125 + "string-width-cjs": "npm:string-width@^4.2.0", 9126 + "strip-ansi": "^7.0.1", 9127 + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 9128 + "wrap-ansi": "^8.1.0", 9129 + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 9130 + }, 9131 + "engines": { 9132 + "node": ">=12" 9133 + } 9134 + }, 9135 + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { 9136 + "version": "6.2.2", 9137 + "inBundle": true, 9138 + "license": "MIT", 9139 + "engines": { 9140 + "node": ">=12" 9141 + }, 9142 + "funding": { 9143 + "url": "https://github.com/chalk/ansi-regex?sponsor=1" 9144 + } 9145 + }, 9146 + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { 9147 + "version": "9.2.2", 9148 + "inBundle": true, 9149 + "license": "MIT" 9150 + }, 9151 + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { 9152 + "version": "5.1.2", 9153 + "inBundle": true, 9154 + "license": "MIT", 9155 + "dependencies": { 9156 + "eastasianwidth": "^0.2.0", 9157 + "emoji-regex": "^9.2.2", 9158 + "strip-ansi": "^7.0.1" 9159 + }, 9160 + "engines": { 9161 + "node": ">=12" 9162 + }, 9163 + "funding": { 9164 + "url": "https://github.com/sponsors/sindresorhus" 9165 + } 9166 + }, 9167 + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { 9168 + "version": "7.1.2", 9169 + "inBundle": true, 9170 + "license": "MIT", 9171 + "dependencies": { 9172 + "ansi-regex": "^6.0.1" 9173 + }, 9174 + "engines": { 9175 + "node": ">=12" 9176 + }, 9177 + "funding": { 9178 + "url": "https://github.com/chalk/strip-ansi?sponsor=1" 9179 + } 9180 + }, 9181 + "node_modules/npm/node_modules/@isaacs/fs-minipass": { 9182 + "version": "4.0.1", 9183 + "inBundle": true, 9184 + "license": "ISC", 9185 + "dependencies": { 9186 + "minipass": "^7.0.4" 9187 + }, 9188 + "engines": { 9189 + "node": ">=18.0.0" 9190 + } 9191 + }, 9192 + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { 9193 + "version": "1.1.0", 9194 + "inBundle": true, 9195 + "license": "ISC" 9196 + }, 9197 + "node_modules/npm/node_modules/@npmcli/agent": { 9198 + "version": "4.0.0", 9199 + "inBundle": true, 9200 + "license": "ISC", 9201 + "dependencies": { 9202 + "agent-base": "^7.1.0", 9203 + "http-proxy-agent": "^7.0.0", 9204 + "https-proxy-agent": "^7.0.1", 9205 + "lru-cache": "^11.2.1", 9206 + "socks-proxy-agent": "^8.0.3" 9207 + }, 9208 + "engines": { 9209 + "node": "^20.17.0 || >=22.9.0" 9210 + } 9211 + }, 9212 + "node_modules/npm/node_modules/@npmcli/arborist": { 9213 + "version": "9.1.6", 9214 + "inBundle": true, 9215 + "license": "ISC", 9216 + "dependencies": { 9217 + "@isaacs/string-locale-compare": "^1.1.0", 9218 + "@npmcli/fs": "^4.0.0", 9219 + "@npmcli/installed-package-contents": "^3.0.0", 9220 + "@npmcli/map-workspaces": "^5.0.0", 9221 + "@npmcli/metavuln-calculator": "^9.0.2", 9222 + "@npmcli/name-from-folder": "^3.0.0", 9223 + "@npmcli/node-gyp": "^4.0.0", 9224 + "@npmcli/package-json": "^7.0.0", 9225 + "@npmcli/query": "^4.0.0", 9226 + "@npmcli/redact": "^3.0.0", 9227 + "@npmcli/run-script": "^10.0.0", 9228 + "bin-links": "^5.0.0", 9229 + "cacache": "^20.0.1", 9230 + "common-ancestor-path": "^1.0.1", 9231 + "hosted-git-info": "^9.0.0", 9232 + "json-stringify-nice": "^1.1.4", 9233 + "lru-cache": "^11.2.1", 9234 + "minimatch": "^10.0.3", 9235 + "nopt": "^8.0.0", 9236 + "npm-install-checks": "^7.1.0", 9237 + "npm-package-arg": "^13.0.0", 9238 + "npm-pick-manifest": "^11.0.1", 9239 + "npm-registry-fetch": "^19.0.0", 9240 + "pacote": "^21.0.2", 9241 + "parse-conflict-json": "^4.0.0", 9242 + "proc-log": "^5.0.0", 9243 + "proggy": "^3.0.0", 9244 + "promise-all-reject-late": "^1.0.0", 9245 + "promise-call-limit": "^3.0.1", 9246 + "semver": "^7.3.7", 9247 + "ssri": "^12.0.0", 9248 + "treeverse": "^3.0.0", 9249 + "walk-up-path": "^4.0.0" 9250 + }, 9251 + "bin": { 9252 + "arborist": "bin/index.js" 9253 + }, 9254 + "engines": { 9255 + "node": "^20.17.0 || >=22.9.0" 9256 + } 9257 + }, 9258 + "node_modules/npm/node_modules/@npmcli/config": { 9259 + "version": "10.4.2", 9260 + "inBundle": true, 9261 + "license": "ISC", 9262 + "dependencies": { 9263 + "@npmcli/map-workspaces": "^5.0.0", 9264 + "@npmcli/package-json": "^7.0.0", 9265 + "ci-info": "^4.0.0", 9266 + "ini": "^5.0.0", 9267 + "nopt": "^8.1.0", 9268 + "proc-log": "^5.0.0", 9269 + "semver": "^7.3.5", 9270 + "walk-up-path": "^4.0.0" 9271 + }, 9272 + "engines": { 9273 + "node": "^20.17.0 || >=22.9.0" 9274 + } 9275 + }, 9276 + "node_modules/npm/node_modules/@npmcli/fs": { 9277 + "version": "4.0.0", 9278 + "inBundle": true, 9279 + "license": "ISC", 9280 + "dependencies": { 9281 + "semver": "^7.3.5" 9282 + }, 9283 + "engines": { 9284 + "node": "^18.17.0 || >=20.5.0" 9285 + } 9286 + }, 9287 + "node_modules/npm/node_modules/@npmcli/git": { 9288 + "version": "7.0.0", 9289 + "inBundle": true, 9290 + "license": "ISC", 9291 + "dependencies": { 9292 + "@npmcli/promise-spawn": "^8.0.0", 9293 + "ini": "^5.0.0", 9294 + "lru-cache": "^11.2.1", 9295 + "npm-pick-manifest": "^11.0.1", 9296 + "proc-log": "^5.0.0", 9297 + "promise-retry": "^2.0.1", 9298 + "semver": "^7.3.5", 9299 + "which": "^5.0.0" 9300 + }, 9301 + "engines": { 9302 + "node": "^20.17.0 || >=22.9.0" 9303 + } 9304 + }, 9305 + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { 9306 + "version": "3.0.0", 9307 + "inBundle": true, 9308 + "license": "ISC", 9309 + "dependencies": { 9310 + "npm-bundled": "^4.0.0", 9311 + "npm-normalize-package-bin": "^4.0.0" 9312 + }, 9313 + "bin": { 9314 + "installed-package-contents": "bin/index.js" 9315 + }, 9316 + "engines": { 9317 + "node": "^18.17.0 || >=20.5.0" 9318 + } 9319 + }, 9320 + "node_modules/npm/node_modules/@npmcli/map-workspaces": { 9321 + "version": "5.0.0", 9322 + "inBundle": true, 9323 + "license": "ISC", 9324 + "dependencies": { 9325 + "@npmcli/name-from-folder": "^3.0.0", 9326 + "@npmcli/package-json": "^7.0.0", 9327 + "glob": "^11.0.3", 9328 + "minimatch": "^10.0.3" 9329 + }, 9330 + "engines": { 9331 + "node": "^20.17.0 || >=22.9.0" 9332 + } 9333 + }, 9334 + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { 9335 + "version": "9.0.2", 9336 + "inBundle": true, 9337 + "license": "ISC", 9338 + "dependencies": { 9339 + "cacache": "^20.0.0", 9340 + "json-parse-even-better-errors": "^4.0.0", 9341 + "pacote": "^21.0.0", 9342 + "proc-log": "^5.0.0", 9343 + "semver": "^7.3.5" 9344 + }, 9345 + "engines": { 9346 + "node": "^20.17.0 || >=22.9.0" 9347 + } 9348 + }, 9349 + "node_modules/npm/node_modules/@npmcli/name-from-folder": { 9350 + "version": "3.0.0", 9351 + "inBundle": true, 9352 + "license": "ISC", 9353 + "engines": { 9354 + "node": "^18.17.0 || >=20.5.0" 9355 + } 9356 + }, 9357 + "node_modules/npm/node_modules/@npmcli/node-gyp": { 9358 + "version": "4.0.0", 9359 + "inBundle": true, 9360 + "license": "ISC", 9361 + "engines": { 9362 + "node": "^18.17.0 || >=20.5.0" 9363 + } 9364 + }, 9365 + "node_modules/npm/node_modules/@npmcli/package-json": { 9366 + "version": "7.0.1", 9367 + "inBundle": true, 9368 + "license": "ISC", 9369 + "dependencies": { 9370 + "@npmcli/git": "^7.0.0", 9371 + "glob": "^11.0.3", 9372 + "hosted-git-info": "^9.0.0", 9373 + "json-parse-even-better-errors": "^4.0.0", 9374 + "proc-log": "^5.0.0", 9375 + "semver": "^7.5.3", 9376 + "validate-npm-package-license": "^3.0.4" 9377 + }, 9378 + "engines": { 9379 + "node": "^20.17.0 || >=22.9.0" 9380 + } 9381 + }, 9382 + "node_modules/npm/node_modules/@npmcli/promise-spawn": { 9383 + "version": "8.0.3", 9384 + "inBundle": true, 9385 + "license": "ISC", 9386 + "dependencies": { 9387 + "which": "^5.0.0" 9388 + }, 9389 + "engines": { 9390 + "node": "^18.17.0 || >=20.5.0" 9391 + } 9392 + }, 9393 + "node_modules/npm/node_modules/@npmcli/query": { 9394 + "version": "4.0.1", 9395 + "inBundle": true, 9396 + "license": "ISC", 9397 + "dependencies": { 9398 + "postcss-selector-parser": "^7.0.0" 9399 + }, 9400 + "engines": { 9401 + "node": "^18.17.0 || >=20.5.0" 9402 + } 9403 + }, 9404 + "node_modules/npm/node_modules/@npmcli/redact": { 9405 + "version": "3.2.2", 9406 + "inBundle": true, 9407 + "license": "ISC", 9408 + "engines": { 9409 + "node": "^18.17.0 || >=20.5.0" 9410 + } 9411 + }, 9412 + "node_modules/npm/node_modules/@npmcli/run-script": { 9413 + "version": "10.0.0", 9414 + "inBundle": true, 9415 + "license": "ISC", 9416 + "dependencies": { 9417 + "@npmcli/node-gyp": "^4.0.0", 9418 + "@npmcli/package-json": "^7.0.0", 9419 + "@npmcli/promise-spawn": "^8.0.0", 9420 + "node-gyp": "^11.0.0", 9421 + "proc-log": "^5.0.0", 9422 + "which": "^5.0.0" 9423 + }, 9424 + "engines": { 9425 + "node": "^20.17.0 || >=22.9.0" 9426 + } 9427 + }, 9428 + "node_modules/npm/node_modules/@pkgjs/parseargs": { 9429 + "version": "0.11.0", 9430 + "inBundle": true, 9431 + "license": "MIT", 9432 + "optional": true, 9433 + "engines": { 9434 + "node": ">=14" 9435 + } 9436 + }, 9437 + "node_modules/npm/node_modules/@sigstore/bundle": { 9438 + "version": "4.0.0", 9439 + "inBundle": true, 9440 + "license": "Apache-2.0", 9441 + "dependencies": { 9442 + "@sigstore/protobuf-specs": "^0.5.0" 9443 + }, 9444 + "engines": { 9445 + "node": "^20.17.0 || >=22.9.0" 9446 + } 9447 + }, 9448 + "node_modules/npm/node_modules/@sigstore/core": { 9449 + "version": "3.0.0", 9450 + "inBundle": true, 9451 + "license": "Apache-2.0", 9452 + "engines": { 9453 + "node": "^20.17.0 || >=22.9.0" 9454 + } 9455 + }, 9456 + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { 9457 + "version": "0.5.0", 9458 + "inBundle": true, 9459 + "license": "Apache-2.0", 9460 + "engines": { 9461 + "node": "^18.17.0 || >=20.5.0" 9462 + } 9463 + }, 9464 + "node_modules/npm/node_modules/@sigstore/sign": { 9465 + "version": "4.0.1", 9466 + "inBundle": true, 9467 + "license": "Apache-2.0", 9468 + "dependencies": { 9469 + "@sigstore/bundle": "^4.0.0", 9470 + "@sigstore/core": "^3.0.0", 9471 + "@sigstore/protobuf-specs": "^0.5.0", 9472 + "make-fetch-happen": "^15.0.2", 9473 + "proc-log": "^5.0.0", 9474 + "promise-retry": "^2.0.1" 9475 + }, 9476 + "engines": { 9477 + "node": "^20.17.0 || >=22.9.0" 9478 + } 9479 + }, 9480 + "node_modules/npm/node_modules/@sigstore/tuf": { 9481 + "version": "4.0.0", 9482 + "inBundle": true, 9483 + "license": "Apache-2.0", 9484 + "dependencies": { 9485 + "@sigstore/protobuf-specs": "^0.5.0", 9486 + "tuf-js": "^4.0.0" 9487 + }, 9488 + "engines": { 9489 + "node": "^20.17.0 || >=22.9.0" 9490 + } 9491 + }, 9492 + "node_modules/npm/node_modules/@sigstore/verify": { 9493 + "version": "3.0.0", 9494 + "inBundle": true, 9495 + "license": "Apache-2.0", 9496 + "dependencies": { 9497 + "@sigstore/bundle": "^4.0.0", 9498 + "@sigstore/core": "^3.0.0", 9499 + "@sigstore/protobuf-specs": "^0.5.0" 9500 + }, 9501 + "engines": { 9502 + "node": "^20.17.0 || >=22.9.0" 9503 + } 9504 + }, 9505 + "node_modules/npm/node_modules/@tufjs/canonical-json": { 9506 + "version": "2.0.0", 9507 + "inBundle": true, 9508 + "license": "MIT", 9509 + "engines": { 9510 + "node": "^16.14.0 || >=18.0.0" 9511 + } 9512 + }, 9513 + "node_modules/npm/node_modules/@tufjs/models": { 9514 + "version": "4.0.0", 9515 + "inBundle": true, 9516 + "license": "MIT", 9517 + "dependencies": { 9518 + "@tufjs/canonical-json": "2.0.0", 9519 + "minimatch": "^9.0.5" 9520 + }, 9521 + "engines": { 9522 + "node": "^20.17.0 || >=22.9.0" 9523 + } 9524 + }, 9525 + "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { 9526 + "version": "9.0.5", 9527 + "inBundle": true, 9528 + "license": "ISC", 9529 + "dependencies": { 9530 + "brace-expansion": "^2.0.1" 9531 + }, 9532 + "engines": { 9533 + "node": ">=16 || 14 >=14.17" 9534 + }, 9535 + "funding": { 9536 + "url": "https://github.com/sponsors/isaacs" 9537 + } 9538 + }, 9539 + "node_modules/npm/node_modules/abbrev": { 9540 + "version": "3.0.1", 9541 + "inBundle": true, 9542 + "license": "ISC", 9543 + "engines": { 9544 + "node": "^18.17.0 || >=20.5.0" 9545 + } 9546 + }, 9547 + "node_modules/npm/node_modules/agent-base": { 9548 + "version": "7.1.4", 9549 + "inBundle": true, 9550 + "license": "MIT", 9551 + "engines": { 9552 + "node": ">= 14" 9553 + } 9554 + }, 9555 + "node_modules/npm/node_modules/ansi-regex": { 9556 + "version": "5.0.1", 9557 + "inBundle": true, 9558 + "license": "MIT", 9559 + "engines": { 9560 + "node": ">=8" 9561 + } 9562 + }, 9563 + "node_modules/npm/node_modules/ansi-styles": { 9564 + "version": "6.2.3", 9565 + "inBundle": true, 9566 + "license": "MIT", 9567 + "engines": { 9568 + "node": ">=12" 9569 + }, 9570 + "funding": { 9571 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 9572 + } 9573 + }, 9574 + "node_modules/npm/node_modules/aproba": { 9575 + "version": "2.1.0", 9576 + "inBundle": true, 9577 + "license": "ISC" 9578 + }, 9579 + "node_modules/npm/node_modules/archy": { 9580 + "version": "1.0.0", 9581 + "inBundle": true, 9582 + "license": "MIT" 9583 + }, 9584 + "node_modules/npm/node_modules/balanced-match": { 9585 + "version": "1.0.2", 9586 + "inBundle": true, 9587 + "license": "MIT" 9588 + }, 9589 + "node_modules/npm/node_modules/bin-links": { 9590 + "version": "5.0.0", 9591 + "inBundle": true, 9592 + "license": "ISC", 9593 + "dependencies": { 9594 + "cmd-shim": "^7.0.0", 9595 + "npm-normalize-package-bin": "^4.0.0", 9596 + "proc-log": "^5.0.0", 9597 + "read-cmd-shim": "^5.0.0", 9598 + "write-file-atomic": "^6.0.0" 9599 + }, 9600 + "engines": { 9601 + "node": "^18.17.0 || >=20.5.0" 9602 + } 9603 + }, 9604 + "node_modules/npm/node_modules/binary-extensions": { 9605 + "version": "3.1.0", 9606 + "inBundle": true, 9607 + "license": "MIT", 9608 + "engines": { 9609 + "node": ">=18.20" 9610 + }, 9611 + "funding": { 9612 + "url": "https://github.com/sponsors/sindresorhus" 9613 + } 9614 + }, 9615 + "node_modules/npm/node_modules/brace-expansion": { 9616 + "version": "2.0.2", 9617 + "inBundle": true, 9618 + "license": "MIT", 9619 + "dependencies": { 9620 + "balanced-match": "^1.0.0" 9621 + } 9622 + }, 9623 + "node_modules/npm/node_modules/cacache": { 9624 + "version": "20.0.1", 9625 + "inBundle": true, 9626 + "license": "ISC", 9627 + "dependencies": { 9628 + "@npmcli/fs": "^4.0.0", 9629 + "fs-minipass": "^3.0.0", 9630 + "glob": "^11.0.3", 9631 + "lru-cache": "^11.1.0", 9632 + "minipass": "^7.0.3", 9633 + "minipass-collect": "^2.0.1", 9634 + "minipass-flush": "^1.0.5", 9635 + "minipass-pipeline": "^1.2.4", 9636 + "p-map": "^7.0.2", 9637 + "ssri": "^12.0.0", 9638 + "unique-filename": "^4.0.0" 9639 + }, 9640 + "engines": { 9641 + "node": "^20.17.0 || >=22.9.0" 9642 + } 9643 + }, 9644 + "node_modules/npm/node_modules/chalk": { 9645 + "version": "5.6.2", 9646 + "inBundle": true, 9647 + "license": "MIT", 9648 + "engines": { 9649 + "node": "^12.17.0 || ^14.13 || >=16.0.0" 9650 + }, 9651 + "funding": { 9652 + "url": "https://github.com/chalk/chalk?sponsor=1" 9653 + } 9654 + }, 9655 + "node_modules/npm/node_modules/chownr": { 9656 + "version": "3.0.0", 9657 + "inBundle": true, 9658 + "license": "BlueOak-1.0.0", 9659 + "engines": { 9660 + "node": ">=18" 9661 + } 9662 + }, 9663 + "node_modules/npm/node_modules/ci-info": { 9664 + "version": "4.3.1", 9665 + "funding": [ 9666 + { 9667 + "type": "github", 9668 + "url": "https://github.com/sponsors/sibiraj-s" 9669 + } 9670 + ], 9671 + "inBundle": true, 9672 + "license": "MIT", 9673 + "engines": { 9674 + "node": ">=8" 9675 + } 9676 + }, 9677 + "node_modules/npm/node_modules/cidr-regex": { 9678 + "version": "5.0.1", 9679 + "inBundle": true, 9680 + "license": "BSD-2-Clause", 9681 + "dependencies": { 9682 + "ip-regex": "5.0.0" 9683 + }, 9684 + "engines": { 9685 + "node": ">=20" 9686 + } 9687 + }, 9688 + "node_modules/npm/node_modules/cli-columns": { 9689 + "version": "4.0.0", 9690 + "inBundle": true, 9691 + "license": "MIT", 9692 + "dependencies": { 9693 + "string-width": "^4.2.3", 9694 + "strip-ansi": "^6.0.1" 9695 + }, 9696 + "engines": { 9697 + "node": ">= 10" 9698 + } 9699 + }, 9700 + "node_modules/npm/node_modules/cmd-shim": { 9701 + "version": "7.0.0", 9702 + "inBundle": true, 9703 + "license": "ISC", 9704 + "engines": { 9705 + "node": "^18.17.0 || >=20.5.0" 9706 + } 9707 + }, 9708 + "node_modules/npm/node_modules/color-convert": { 9709 + "version": "2.0.1", 9710 + "inBundle": true, 9711 + "license": "MIT", 9712 + "dependencies": { 9713 + "color-name": "~1.1.4" 9714 + }, 9715 + "engines": { 9716 + "node": ">=7.0.0" 9717 + } 9718 + }, 9719 + "node_modules/npm/node_modules/color-name": { 9720 + "version": "1.1.4", 9721 + "inBundle": true, 9722 + "license": "MIT" 9723 + }, 9724 + "node_modules/npm/node_modules/common-ancestor-path": { 9725 + "version": "1.0.1", 9726 + "inBundle": true, 9727 + "license": "ISC" 9728 + }, 9729 + "node_modules/npm/node_modules/cross-spawn": { 9730 + "version": "7.0.6", 9731 + "inBundle": true, 9732 + "license": "MIT", 9733 + "dependencies": { 9734 + "path-key": "^3.1.0", 9735 + "shebang-command": "^2.0.0", 9736 + "which": "^2.0.1" 9737 + }, 9738 + "engines": { 9739 + "node": ">= 8" 9740 + } 9741 + }, 9742 + "node_modules/npm/node_modules/cross-spawn/node_modules/isexe": { 9743 + "version": "2.0.0", 9744 + "inBundle": true, 9745 + "license": "ISC" 9746 + }, 9747 + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { 9748 + "version": "2.0.2", 9749 + "inBundle": true, 9750 + "license": "ISC", 9751 + "dependencies": { 9752 + "isexe": "^2.0.0" 9753 + }, 9754 + "bin": { 9755 + "node-which": "bin/node-which" 9756 + }, 9757 + "engines": { 9758 + "node": ">= 8" 9759 + } 9760 + }, 9761 + "node_modules/npm/node_modules/cssesc": { 9762 + "version": "3.0.0", 9763 + "inBundle": true, 9764 + "license": "MIT", 9765 + "bin": { 9766 + "cssesc": "bin/cssesc" 9767 + }, 9768 + "engines": { 9769 + "node": ">=4" 9770 + } 9771 + }, 9772 + "node_modules/npm/node_modules/debug": { 9773 + "version": "4.4.3", 9774 + "inBundle": true, 9775 + "license": "MIT", 9776 + "dependencies": { 9777 + "ms": "^2.1.3" 9778 + }, 9779 + "engines": { 9780 + "node": ">=6.0" 9781 + }, 9782 + "peerDependenciesMeta": { 9783 + "supports-color": { 9784 + "optional": true 9785 + } 9786 + } 9787 + }, 9788 + "node_modules/npm/node_modules/diff": { 9789 + "version": "8.0.2", 9790 + "inBundle": true, 9791 + "license": "BSD-3-Clause", 9792 + "engines": { 9793 + "node": ">=0.3.1" 9794 + } 9795 + }, 9796 + "node_modules/npm/node_modules/eastasianwidth": { 9797 + "version": "0.2.0", 9798 + "inBundle": true, 9799 + "license": "MIT" 9800 + }, 9801 + "node_modules/npm/node_modules/emoji-regex": { 9802 + "version": "8.0.0", 9803 + "inBundle": true, 9804 + "license": "MIT" 9805 + }, 9806 + "node_modules/npm/node_modules/encoding": { 9807 + "version": "0.1.13", 9808 + "inBundle": true, 9809 + "license": "MIT", 9810 + "optional": true, 9811 + "dependencies": { 9812 + "iconv-lite": "^0.6.2" 9813 + } 9814 + }, 9815 + "node_modules/npm/node_modules/env-paths": { 9816 + "version": "2.2.1", 9817 + "inBundle": true, 9818 + "license": "MIT", 9819 + "engines": { 9820 + "node": ">=6" 9821 + } 9822 + }, 9823 + "node_modules/npm/node_modules/err-code": { 9824 + "version": "2.0.3", 9825 + "inBundle": true, 9826 + "license": "MIT" 9827 + }, 9828 + "node_modules/npm/node_modules/exponential-backoff": { 9829 + "version": "3.1.2", 9830 + "inBundle": true, 9831 + "license": "Apache-2.0" 9832 + }, 9833 + "node_modules/npm/node_modules/fastest-levenshtein": { 9834 + "version": "1.0.16", 9835 + "inBundle": true, 9836 + "license": "MIT", 9837 + "engines": { 9838 + "node": ">= 4.9.1" 9839 + } 9840 + }, 9841 + "node_modules/npm/node_modules/foreground-child": { 9842 + "version": "3.3.1", 9843 + "inBundle": true, 9844 + "license": "ISC", 9845 + "dependencies": { 9846 + "cross-spawn": "^7.0.6", 9847 + "signal-exit": "^4.0.1" 9848 + }, 9849 + "engines": { 9850 + "node": ">=14" 9851 + }, 9852 + "funding": { 9853 + "url": "https://github.com/sponsors/isaacs" 9854 + } 9855 + }, 9856 + "node_modules/npm/node_modules/fs-minipass": { 9857 + "version": "3.0.3", 9858 + "inBundle": true, 9859 + "license": "ISC", 9860 + "dependencies": { 9861 + "minipass": "^7.0.3" 9862 + }, 9863 + "engines": { 9864 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" 9865 + } 9866 + }, 9867 + "node_modules/npm/node_modules/glob": { 9868 + "version": "11.0.3", 9869 + "inBundle": true, 9870 + "license": "ISC", 9871 + "dependencies": { 9872 + "foreground-child": "^3.3.1", 9873 + "jackspeak": "^4.1.1", 9874 + "minimatch": "^10.0.3", 9875 + "minipass": "^7.1.2", 9876 + "package-json-from-dist": "^1.0.0", 9877 + "path-scurry": "^2.0.0" 9878 + }, 9879 + "bin": { 9880 + "glob": "dist/esm/bin.mjs" 9881 + }, 9882 + "engines": { 9883 + "node": "20 || >=22" 9884 + }, 9885 + "funding": { 9886 + "url": "https://github.com/sponsors/isaacs" 9887 + } 9888 + }, 9889 + "node_modules/npm/node_modules/graceful-fs": { 9890 + "version": "4.2.11", 9891 + "inBundle": true, 9892 + "license": "ISC" 9893 + }, 9894 + "node_modules/npm/node_modules/hosted-git-info": { 9895 + "version": "9.0.2", 9896 + "inBundle": true, 9897 + "license": "ISC", 9898 + "dependencies": { 9899 + "lru-cache": "^11.1.0" 9900 + }, 9901 + "engines": { 9902 + "node": "^20.17.0 || >=22.9.0" 9903 + } 9904 + }, 9905 + "node_modules/npm/node_modules/http-cache-semantics": { 9906 + "version": "4.2.0", 9907 + "inBundle": true, 9908 + "license": "BSD-2-Clause" 9909 + }, 9910 + "node_modules/npm/node_modules/http-proxy-agent": { 9911 + "version": "7.0.2", 9912 + "inBundle": true, 9913 + "license": "MIT", 9914 + "dependencies": { 9915 + "agent-base": "^7.1.0", 9916 + "debug": "^4.3.4" 9917 + }, 9918 + "engines": { 9919 + "node": ">= 14" 9920 + } 9921 + }, 9922 + "node_modules/npm/node_modules/https-proxy-agent": { 9923 + "version": "7.0.6", 9924 + "inBundle": true, 9925 + "license": "MIT", 9926 + "dependencies": { 9927 + "agent-base": "^7.1.2", 9928 + "debug": "4" 9929 + }, 9930 + "engines": { 9931 + "node": ">= 14" 9932 + } 9933 + }, 9934 + "node_modules/npm/node_modules/iconv-lite": { 9935 + "version": "0.6.3", 9936 + "inBundle": true, 9937 + "license": "MIT", 9938 + "optional": true, 9939 + "dependencies": { 9940 + "safer-buffer": ">= 2.1.2 < 3.0.0" 9941 + }, 9942 + "engines": { 9943 + "node": ">=0.10.0" 9944 + } 9945 + }, 9946 + "node_modules/npm/node_modules/ignore-walk": { 9947 + "version": "8.0.0", 9948 + "inBundle": true, 9949 + "license": "ISC", 9950 + "dependencies": { 9951 + "minimatch": "^10.0.3" 9952 + }, 9953 + "engines": { 9954 + "node": "^20.17.0 || >=22.9.0" 9955 + } 9956 + }, 9957 + "node_modules/npm/node_modules/imurmurhash": { 9958 + "version": "0.1.4", 9959 + "inBundle": true, 9960 + "license": "MIT", 9961 + "engines": { 9962 + "node": ">=0.8.19" 9963 + } 9964 + }, 9965 + "node_modules/npm/node_modules/ini": { 9966 + "version": "5.0.0", 9967 + "inBundle": true, 9968 + "license": "ISC", 9969 + "engines": { 9970 + "node": "^18.17.0 || >=20.5.0" 9971 + } 9972 + }, 9973 + "node_modules/npm/node_modules/init-package-json": { 9974 + "version": "8.2.2", 9975 + "inBundle": true, 9976 + "license": "ISC", 9977 + "dependencies": { 9978 + "@npmcli/package-json": "^7.0.0", 9979 + "npm-package-arg": "^13.0.0", 9980 + "promzard": "^2.0.0", 9981 + "read": "^4.0.0", 9982 + "semver": "^7.7.2", 9983 + "validate-npm-package-license": "^3.0.4", 9984 + "validate-npm-package-name": "^6.0.2" 9985 + }, 9986 + "engines": { 9987 + "node": "^20.17.0 || >=22.9.0" 9988 + } 9989 + }, 9990 + "node_modules/npm/node_modules/ip-address": { 9991 + "version": "10.0.1", 9992 + "inBundle": true, 9993 + "license": "MIT", 9994 + "engines": { 9995 + "node": ">= 12" 9996 + } 9997 + }, 9998 + "node_modules/npm/node_modules/ip-regex": { 9999 + "version": "5.0.0", 10000 + "inBundle": true, 10001 + "license": "MIT", 10002 + "engines": { 10003 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 10004 + }, 10005 + "funding": { 10006 + "url": "https://github.com/sponsors/sindresorhus" 10007 + } 10008 + }, 10009 + "node_modules/npm/node_modules/is-cidr": { 10010 + "version": "6.0.1", 10011 + "inBundle": true, 10012 + "license": "BSD-2-Clause", 10013 + "dependencies": { 10014 + "cidr-regex": "5.0.1" 10015 + }, 10016 + "engines": { 10017 + "node": ">=20" 10018 + } 10019 + }, 10020 + "node_modules/npm/node_modules/is-fullwidth-code-point": { 10021 + "version": "3.0.0", 10022 + "inBundle": true, 10023 + "license": "MIT", 10024 + "engines": { 10025 + "node": ">=8" 10026 + } 10027 + }, 10028 + "node_modules/npm/node_modules/isexe": { 10029 + "version": "3.1.1", 10030 + "inBundle": true, 10031 + "license": "ISC", 10032 + "engines": { 10033 + "node": ">=16" 10034 + } 10035 + }, 10036 + "node_modules/npm/node_modules/jackspeak": { 10037 + "version": "4.1.1", 10038 + "inBundle": true, 10039 + "license": "BlueOak-1.0.0", 10040 + "dependencies": { 10041 + "@isaacs/cliui": "^8.0.2" 10042 + }, 10043 + "engines": { 10044 + "node": "20 || >=22" 10045 + }, 10046 + "funding": { 10047 + "url": "https://github.com/sponsors/isaacs" 10048 + } 10049 + }, 10050 + "node_modules/npm/node_modules/json-parse-even-better-errors": { 10051 + "version": "4.0.0", 10052 + "inBundle": true, 10053 + "license": "MIT", 10054 + "engines": { 10055 + "node": "^18.17.0 || >=20.5.0" 10056 + } 10057 + }, 10058 + "node_modules/npm/node_modules/json-stringify-nice": { 10059 + "version": "1.1.4", 10060 + "inBundle": true, 10061 + "license": "ISC", 10062 + "funding": { 10063 + "url": "https://github.com/sponsors/isaacs" 10064 + } 10065 + }, 10066 + "node_modules/npm/node_modules/jsonparse": { 10067 + "version": "1.3.1", 10068 + "engines": [ 10069 + "node >= 0.2.0" 10070 + ], 10071 + "inBundle": true, 10072 + "license": "MIT" 10073 + }, 10074 + "node_modules/npm/node_modules/just-diff": { 10075 + "version": "6.0.2", 10076 + "inBundle": true, 10077 + "license": "MIT" 10078 + }, 10079 + "node_modules/npm/node_modules/just-diff-apply": { 10080 + "version": "5.5.0", 10081 + "inBundle": true, 10082 + "license": "MIT" 10083 + }, 10084 + "node_modules/npm/node_modules/libnpmaccess": { 10085 + "version": "10.0.3", 10086 + "inBundle": true, 10087 + "license": "ISC", 10088 + "dependencies": { 10089 + "npm-package-arg": "^13.0.0", 10090 + "npm-registry-fetch": "^19.0.0" 10091 + }, 10092 + "engines": { 10093 + "node": "^20.17.0 || >=22.9.0" 10094 + } 10095 + }, 10096 + "node_modules/npm/node_modules/libnpmdiff": { 10097 + "version": "8.0.9", 10098 + "inBundle": true, 10099 + "license": "ISC", 10100 + "dependencies": { 10101 + "@npmcli/arborist": "^9.1.6", 10102 + "@npmcli/installed-package-contents": "^3.0.0", 10103 + "binary-extensions": "^3.0.0", 10104 + "diff": "^8.0.2", 10105 + "minimatch": "^10.0.3", 10106 + "npm-package-arg": "^13.0.0", 10107 + "pacote": "^21.0.2", 10108 + "tar": "^7.5.1" 10109 + }, 10110 + "engines": { 10111 + "node": "^20.17.0 || >=22.9.0" 10112 + } 10113 + }, 10114 + "node_modules/npm/node_modules/libnpmexec": { 10115 + "version": "10.1.8", 10116 + "inBundle": true, 10117 + "license": "ISC", 10118 + "dependencies": { 10119 + "@npmcli/arborist": "^9.1.6", 10120 + "@npmcli/package-json": "^7.0.0", 10121 + "@npmcli/run-script": "^10.0.0", 10122 + "ci-info": "^4.0.0", 10123 + "npm-package-arg": "^13.0.0", 10124 + "pacote": "^21.0.2", 10125 + "proc-log": "^5.0.0", 10126 + "promise-retry": "^2.0.1", 10127 + "read": "^4.0.0", 10128 + "semver": "^7.3.7", 10129 + "signal-exit": "^4.1.0", 10130 + "walk-up-path": "^4.0.0" 10131 + }, 10132 + "engines": { 10133 + "node": "^20.17.0 || >=22.9.0" 10134 + } 10135 + }, 10136 + "node_modules/npm/node_modules/libnpmfund": { 10137 + "version": "7.0.9", 10138 + "inBundle": true, 10139 + "license": "ISC", 10140 + "dependencies": { 10141 + "@npmcli/arborist": "^9.1.6" 10142 + }, 10143 + "engines": { 10144 + "node": "^20.17.0 || >=22.9.0" 10145 + } 10146 + }, 10147 + "node_modules/npm/node_modules/libnpmorg": { 10148 + "version": "8.0.1", 10149 + "inBundle": true, 10150 + "license": "ISC", 10151 + "dependencies": { 10152 + "aproba": "^2.0.0", 10153 + "npm-registry-fetch": "^19.0.0" 10154 + }, 10155 + "engines": { 10156 + "node": "^20.17.0 || >=22.9.0" 10157 + } 10158 + }, 10159 + "node_modules/npm/node_modules/libnpmpack": { 10160 + "version": "9.0.9", 10161 + "inBundle": true, 10162 + "license": "ISC", 10163 + "dependencies": { 10164 + "@npmcli/arborist": "^9.1.6", 10165 + "@npmcli/run-script": "^10.0.0", 10166 + "npm-package-arg": "^13.0.0", 10167 + "pacote": "^21.0.2" 10168 + }, 10169 + "engines": { 10170 + "node": "^20.17.0 || >=22.9.0" 10171 + } 10172 + }, 10173 + "node_modules/npm/node_modules/libnpmpublish": { 10174 + "version": "11.1.2", 10175 + "inBundle": true, 10176 + "license": "ISC", 10177 + "dependencies": { 10178 + "@npmcli/package-json": "^7.0.0", 10179 + "ci-info": "^4.0.0", 10180 + "npm-package-arg": "^13.0.0", 10181 + "npm-registry-fetch": "^19.0.0", 10182 + "proc-log": "^5.0.0", 10183 + "semver": "^7.3.7", 10184 + "sigstore": "^4.0.0", 10185 + "ssri": "^12.0.0" 10186 + }, 10187 + "engines": { 10188 + "node": "^20.17.0 || >=22.9.0" 10189 + } 10190 + }, 10191 + "node_modules/npm/node_modules/libnpmsearch": { 10192 + "version": "9.0.1", 10193 + "inBundle": true, 10194 + "license": "ISC", 10195 + "dependencies": { 10196 + "npm-registry-fetch": "^19.0.0" 10197 + }, 10198 + "engines": { 10199 + "node": "^20.17.0 || >=22.9.0" 10200 + } 10201 + }, 10202 + "node_modules/npm/node_modules/libnpmteam": { 10203 + "version": "8.0.2", 10204 + "inBundle": true, 10205 + "license": "ISC", 10206 + "dependencies": { 10207 + "aproba": "^2.0.0", 10208 + "npm-registry-fetch": "^19.0.0" 10209 + }, 10210 + "engines": { 10211 + "node": "^20.17.0 || >=22.9.0" 10212 + } 10213 + }, 10214 + "node_modules/npm/node_modules/libnpmversion": { 10215 + "version": "8.0.2", 10216 + "inBundle": true, 10217 + "license": "ISC", 10218 + "dependencies": { 10219 + "@npmcli/git": "^7.0.0", 10220 + "@npmcli/run-script": "^10.0.0", 10221 + "json-parse-even-better-errors": "^4.0.0", 10222 + "proc-log": "^5.0.0", 10223 + "semver": "^7.3.7" 10224 + }, 10225 + "engines": { 10226 + "node": "^20.17.0 || >=22.9.0" 10227 + } 10228 + }, 10229 + "node_modules/npm/node_modules/lru-cache": { 10230 + "version": "11.2.2", 10231 + "inBundle": true, 10232 + "license": "ISC", 10233 + "engines": { 10234 + "node": "20 || >=22" 10235 + } 10236 + }, 10237 + "node_modules/npm/node_modules/make-fetch-happen": { 10238 + "version": "15.0.2", 10239 + "inBundle": true, 10240 + "license": "ISC", 10241 + "dependencies": { 10242 + "@npmcli/agent": "^4.0.0", 10243 + "cacache": "^20.0.1", 10244 + "http-cache-semantics": "^4.1.1", 10245 + "minipass": "^7.0.2", 10246 + "minipass-fetch": "^4.0.0", 10247 + "minipass-flush": "^1.0.5", 10248 + "minipass-pipeline": "^1.2.4", 10249 + "negotiator": "^1.0.0", 10250 + "proc-log": "^5.0.0", 10251 + "promise-retry": "^2.0.1", 10252 + "ssri": "^12.0.0" 10253 + }, 10254 + "engines": { 10255 + "node": "^20.17.0 || >=22.9.0" 10256 + } 10257 + }, 10258 + "node_modules/npm/node_modules/minimatch": { 10259 + "version": "10.0.3", 10260 + "inBundle": true, 10261 + "license": "ISC", 10262 + "dependencies": { 10263 + "@isaacs/brace-expansion": "^5.0.0" 10264 + }, 10265 + "engines": { 10266 + "node": "20 || >=22" 10267 + }, 10268 + "funding": { 10269 + "url": "https://github.com/sponsors/isaacs" 10270 + } 10271 + }, 10272 + "node_modules/npm/node_modules/minipass": { 10273 + "version": "7.1.2", 10274 + "inBundle": true, 10275 + "license": "ISC", 10276 + "engines": { 10277 + "node": ">=16 || 14 >=14.17" 10278 + } 10279 + }, 10280 + "node_modules/npm/node_modules/minipass-collect": { 10281 + "version": "2.0.1", 10282 + "inBundle": true, 10283 + "license": "ISC", 10284 + "dependencies": { 10285 + "minipass": "^7.0.3" 10286 + }, 10287 + "engines": { 10288 + "node": ">=16 || 14 >=14.17" 10289 + } 10290 + }, 10291 + "node_modules/npm/node_modules/minipass-fetch": { 10292 + "version": "4.0.1", 10293 + "inBundle": true, 10294 + "license": "MIT", 10295 + "dependencies": { 10296 + "minipass": "^7.0.3", 10297 + "minipass-sized": "^1.0.3", 10298 + "minizlib": "^3.0.1" 10299 + }, 10300 + "engines": { 10301 + "node": "^18.17.0 || >=20.5.0" 10302 + }, 10303 + "optionalDependencies": { 10304 + "encoding": "^0.1.13" 10305 + } 10306 + }, 10307 + "node_modules/npm/node_modules/minipass-flush": { 10308 + "version": "1.0.5", 10309 + "inBundle": true, 10310 + "license": "ISC", 10311 + "dependencies": { 10312 + "minipass": "^3.0.0" 10313 + }, 10314 + "engines": { 10315 + "node": ">= 8" 10316 + } 10317 + }, 10318 + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { 10319 + "version": "3.3.6", 10320 + "inBundle": true, 10321 + "license": "ISC", 10322 + "dependencies": { 10323 + "yallist": "^4.0.0" 10324 + }, 10325 + "engines": { 10326 + "node": ">=8" 10327 + } 10328 + }, 10329 + "node_modules/npm/node_modules/minipass-pipeline": { 10330 + "version": "1.2.4", 10331 + "inBundle": true, 10332 + "license": "ISC", 10333 + "dependencies": { 10334 + "minipass": "^3.0.0" 10335 + }, 10336 + "engines": { 10337 + "node": ">=8" 10338 + } 10339 + }, 10340 + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { 10341 + "version": "3.3.6", 10342 + "inBundle": true, 10343 + "license": "ISC", 10344 + "dependencies": { 10345 + "yallist": "^4.0.0" 10346 + }, 10347 + "engines": { 10348 + "node": ">=8" 10349 + } 10350 + }, 10351 + "node_modules/npm/node_modules/minipass-sized": { 10352 + "version": "1.0.3", 10353 + "inBundle": true, 10354 + "license": "ISC", 10355 + "dependencies": { 10356 + "minipass": "^3.0.0" 10357 + }, 10358 + "engines": { 10359 + "node": ">=8" 10360 + } 10361 + }, 10362 + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { 10363 + "version": "3.3.6", 10364 + "inBundle": true, 10365 + "license": "ISC", 10366 + "dependencies": { 10367 + "yallist": "^4.0.0" 10368 + }, 10369 + "engines": { 10370 + "node": ">=8" 10371 + } 10372 + }, 10373 + "node_modules/npm/node_modules/minizlib": { 10374 + "version": "3.1.0", 10375 + "inBundle": true, 10376 + "license": "MIT", 10377 + "dependencies": { 10378 + "minipass": "^7.1.2" 10379 + }, 10380 + "engines": { 10381 + "node": ">= 18" 10382 + } 10383 + }, 10384 + "node_modules/npm/node_modules/ms": { 10385 + "version": "2.1.3", 10386 + "inBundle": true, 10387 + "license": "MIT" 10388 + }, 10389 + "node_modules/npm/node_modules/mute-stream": { 10390 + "version": "2.0.0", 10391 + "inBundle": true, 10392 + "license": "ISC", 10393 + "engines": { 10394 + "node": "^18.17.0 || >=20.5.0" 10395 + } 10396 + }, 10397 + "node_modules/npm/node_modules/negotiator": { 10398 + "version": "1.0.0", 10399 + "inBundle": true, 10400 + "license": "MIT", 10401 + "engines": { 10402 + "node": ">= 0.6" 10403 + } 10404 + }, 10405 + "node_modules/npm/node_modules/node-gyp": { 10406 + "version": "11.4.2", 10407 + "inBundle": true, 10408 + "license": "MIT", 10409 + "dependencies": { 10410 + "env-paths": "^2.2.0", 10411 + "exponential-backoff": "^3.1.1", 10412 + "graceful-fs": "^4.2.6", 10413 + "make-fetch-happen": "^14.0.3", 10414 + "nopt": "^8.0.0", 10415 + "proc-log": "^5.0.0", 10416 + "semver": "^7.3.5", 10417 + "tar": "^7.4.3", 10418 + "tinyglobby": "^0.2.12", 10419 + "which": "^5.0.0" 10420 + }, 10421 + "bin": { 10422 + "node-gyp": "bin/node-gyp.js" 10423 + }, 10424 + "engines": { 10425 + "node": "^18.17.0 || >=20.5.0" 10426 + } 10427 + }, 10428 + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { 10429 + "version": "3.0.0", 10430 + "inBundle": true, 10431 + "license": "ISC", 10432 + "dependencies": { 10433 + "agent-base": "^7.1.0", 10434 + "http-proxy-agent": "^7.0.0", 10435 + "https-proxy-agent": "^7.0.1", 10436 + "lru-cache": "^10.0.1", 10437 + "socks-proxy-agent": "^8.0.3" 10438 + }, 10439 + "engines": { 10440 + "node": "^18.17.0 || >=20.5.0" 10441 + } 10442 + }, 10443 + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { 10444 + "version": "19.0.1", 10445 + "inBundle": true, 10446 + "license": "ISC", 10447 + "dependencies": { 10448 + "@npmcli/fs": "^4.0.0", 10449 + "fs-minipass": "^3.0.0", 10450 + "glob": "^10.2.2", 10451 + "lru-cache": "^10.0.1", 10452 + "minipass": "^7.0.3", 10453 + "minipass-collect": "^2.0.1", 10454 + "minipass-flush": "^1.0.5", 10455 + "minipass-pipeline": "^1.2.4", 10456 + "p-map": "^7.0.2", 10457 + "ssri": "^12.0.0", 10458 + "tar": "^7.4.3", 10459 + "unique-filename": "^4.0.0" 10460 + }, 10461 + "engines": { 10462 + "node": "^18.17.0 || >=20.5.0" 10463 + } 10464 + }, 10465 + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { 10466 + "version": "10.4.5", 10467 + "inBundle": true, 10468 + "license": "ISC", 10469 + "dependencies": { 10470 + "foreground-child": "^3.1.0", 10471 + "jackspeak": "^3.1.2", 10472 + "minimatch": "^9.0.4", 10473 + "minipass": "^7.1.2", 10474 + "package-json-from-dist": "^1.0.0", 10475 + "path-scurry": "^1.11.1" 10476 + }, 10477 + "bin": { 10478 + "glob": "dist/esm/bin.mjs" 10479 + }, 10480 + "funding": { 10481 + "url": "https://github.com/sponsors/isaacs" 10482 + } 10483 + }, 10484 + "node_modules/npm/node_modules/node-gyp/node_modules/jackspeak": { 10485 + "version": "3.4.3", 10486 + "inBundle": true, 10487 + "license": "BlueOak-1.0.0", 10488 + "dependencies": { 10489 + "@isaacs/cliui": "^8.0.2" 10490 + }, 10491 + "funding": { 10492 + "url": "https://github.com/sponsors/isaacs" 10493 + }, 10494 + "optionalDependencies": { 10495 + "@pkgjs/parseargs": "^0.11.0" 10496 + } 10497 + }, 10498 + "node_modules/npm/node_modules/node-gyp/node_modules/lru-cache": { 10499 + "version": "10.4.3", 10500 + "inBundle": true, 10501 + "license": "ISC" 10502 + }, 10503 + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { 10504 + "version": "14.0.3", 10505 + "inBundle": true, 10506 + "license": "ISC", 10507 + "dependencies": { 10508 + "@npmcli/agent": "^3.0.0", 10509 + "cacache": "^19.0.1", 10510 + "http-cache-semantics": "^4.1.1", 10511 + "minipass": "^7.0.2", 10512 + "minipass-fetch": "^4.0.0", 10513 + "minipass-flush": "^1.0.5", 10514 + "minipass-pipeline": "^1.2.4", 10515 + "negotiator": "^1.0.0", 10516 + "proc-log": "^5.0.0", 10517 + "promise-retry": "^2.0.1", 10518 + "ssri": "^12.0.0" 10519 + }, 10520 + "engines": { 10521 + "node": "^18.17.0 || >=20.5.0" 10522 + } 10523 + }, 10524 + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { 10525 + "version": "9.0.5", 10526 + "inBundle": true, 10527 + "license": "ISC", 10528 + "dependencies": { 10529 + "brace-expansion": "^2.0.1" 10530 + }, 10531 + "engines": { 10532 + "node": ">=16 || 14 >=14.17" 10533 + }, 10534 + "funding": { 10535 + "url": "https://github.com/sponsors/isaacs" 10536 + } 10537 + }, 10538 + "node_modules/npm/node_modules/node-gyp/node_modules/path-scurry": { 10539 + "version": "1.11.1", 10540 + "inBundle": true, 10541 + "license": "BlueOak-1.0.0", 10542 + "dependencies": { 10543 + "lru-cache": "^10.2.0", 10544 + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 10545 + }, 10546 + "engines": { 10547 + "node": ">=16 || 14 >=14.18" 10548 + }, 10549 + "funding": { 10550 + "url": "https://github.com/sponsors/isaacs" 10551 + } 10552 + }, 10553 + "node_modules/npm/node_modules/nopt": { 10554 + "version": "8.1.0", 10555 + "inBundle": true, 10556 + "license": "ISC", 10557 + "dependencies": { 10558 + "abbrev": "^3.0.0" 10559 + }, 10560 + "bin": { 10561 + "nopt": "bin/nopt.js" 10562 + }, 10563 + "engines": { 10564 + "node": "^18.17.0 || >=20.5.0" 10565 + } 10566 + }, 10567 + "node_modules/npm/node_modules/npm-audit-report": { 10568 + "version": "6.0.0", 10569 + "inBundle": true, 10570 + "license": "ISC", 10571 + "engines": { 10572 + "node": "^18.17.0 || >=20.5.0" 10573 + } 10574 + }, 10575 + "node_modules/npm/node_modules/npm-bundled": { 10576 + "version": "4.0.0", 10577 + "inBundle": true, 10578 + "license": "ISC", 10579 + "dependencies": { 10580 + "npm-normalize-package-bin": "^4.0.0" 10581 + }, 10582 + "engines": { 10583 + "node": "^18.17.0 || >=20.5.0" 10584 + } 10585 + }, 10586 + "node_modules/npm/node_modules/npm-install-checks": { 10587 + "version": "7.1.2", 10588 + "inBundle": true, 10589 + "license": "BSD-2-Clause", 10590 + "dependencies": { 10591 + "semver": "^7.1.1" 10592 + }, 10593 + "engines": { 10594 + "node": "^18.17.0 || >=20.5.0" 10595 + } 10596 + }, 10597 + "node_modules/npm/node_modules/npm-normalize-package-bin": { 10598 + "version": "4.0.0", 10599 + "inBundle": true, 10600 + "license": "ISC", 10601 + "engines": { 10602 + "node": "^18.17.0 || >=20.5.0" 10603 + } 10604 + }, 10605 + "node_modules/npm/node_modules/npm-package-arg": { 10606 + "version": "13.0.1", 10607 + "inBundle": true, 10608 + "license": "ISC", 10609 + "dependencies": { 10610 + "hosted-git-info": "^9.0.0", 10611 + "proc-log": "^5.0.0", 10612 + "semver": "^7.3.5", 10613 + "validate-npm-package-name": "^6.0.0" 10614 + }, 10615 + "engines": { 10616 + "node": "^20.17.0 || >=22.9.0" 10617 + } 10618 + }, 10619 + "node_modules/npm/node_modules/npm-packlist": { 10620 + "version": "10.0.2", 10621 + "inBundle": true, 10622 + "license": "ISC", 10623 + "dependencies": { 10624 + "ignore-walk": "^8.0.0", 10625 + "proc-log": "^5.0.0" 10626 + }, 10627 + "engines": { 10628 + "node": "^20.17.0 || >=22.9.0" 10629 + } 10630 + }, 10631 + "node_modules/npm/node_modules/npm-pick-manifest": { 10632 + "version": "11.0.1", 10633 + "inBundle": true, 10634 + "license": "ISC", 10635 + "dependencies": { 10636 + "npm-install-checks": "^7.1.0", 10637 + "npm-normalize-package-bin": "^4.0.0", 10638 + "npm-package-arg": "^13.0.0", 10639 + "semver": "^7.3.5" 10640 + }, 10641 + "engines": { 10642 + "node": "^20.17.0 || >=22.9.0" 10643 + } 10644 + }, 10645 + "node_modules/npm/node_modules/npm-profile": { 10646 + "version": "12.0.0", 10647 + "inBundle": true, 10648 + "license": "ISC", 10649 + "dependencies": { 10650 + "npm-registry-fetch": "^19.0.0", 10651 + "proc-log": "^5.0.0" 10652 + }, 10653 + "engines": { 10654 + "node": "^20.17.0 || >=22.9.0" 10655 + } 10656 + }, 10657 + "node_modules/npm/node_modules/npm-registry-fetch": { 10658 + "version": "19.0.0", 10659 + "inBundle": true, 10660 + "license": "ISC", 10661 + "dependencies": { 10662 + "@npmcli/redact": "^3.0.0", 10663 + "jsonparse": "^1.3.1", 10664 + "make-fetch-happen": "^15.0.0", 10665 + "minipass": "^7.0.2", 10666 + "minipass-fetch": "^4.0.0", 10667 + "minizlib": "^3.0.1", 10668 + "npm-package-arg": "^13.0.0", 10669 + "proc-log": "^5.0.0" 10670 + }, 10671 + "engines": { 10672 + "node": "^20.17.0 || >=22.9.0" 10673 + } 10674 + }, 10675 + "node_modules/npm/node_modules/npm-user-validate": { 10676 + "version": "3.0.0", 10677 + "inBundle": true, 10678 + "license": "BSD-2-Clause", 10679 + "engines": { 10680 + "node": "^18.17.0 || >=20.5.0" 10681 + } 10682 + }, 10683 + "node_modules/npm/node_modules/p-map": { 10684 + "version": "7.0.3", 10685 + "inBundle": true, 10686 + "license": "MIT", 10687 + "engines": { 10688 + "node": ">=18" 10689 + }, 10690 + "funding": { 10691 + "url": "https://github.com/sponsors/sindresorhus" 10692 + } 10693 + }, 10694 + "node_modules/npm/node_modules/package-json-from-dist": { 10695 + "version": "1.0.1", 10696 + "inBundle": true, 10697 + "license": "BlueOak-1.0.0" 10698 + }, 10699 + "node_modules/npm/node_modules/pacote": { 10700 + "version": "21.0.3", 10701 + "inBundle": true, 10702 + "license": "ISC", 10703 + "dependencies": { 10704 + "@npmcli/git": "^7.0.0", 10705 + "@npmcli/installed-package-contents": "^3.0.0", 10706 + "@npmcli/package-json": "^7.0.0", 10707 + "@npmcli/promise-spawn": "^8.0.0", 10708 + "@npmcli/run-script": "^10.0.0", 10709 + "cacache": "^20.0.0", 10710 + "fs-minipass": "^3.0.0", 10711 + "minipass": "^7.0.2", 10712 + "npm-package-arg": "^13.0.0", 10713 + "npm-packlist": "^10.0.1", 10714 + "npm-pick-manifest": "^11.0.1", 10715 + "npm-registry-fetch": "^19.0.0", 10716 + "proc-log": "^5.0.0", 10717 + "promise-retry": "^2.0.1", 10718 + "sigstore": "^4.0.0", 10719 + "ssri": "^12.0.0", 10720 + "tar": "^7.4.3" 10721 + }, 10722 + "bin": { 10723 + "pacote": "bin/index.js" 10724 + }, 10725 + "engines": { 10726 + "node": "^20.17.0 || >=22.9.0" 10727 + } 10728 + }, 10729 + "node_modules/npm/node_modules/parse-conflict-json": { 10730 + "version": "4.0.0", 10731 + "inBundle": true, 10732 + "license": "ISC", 10733 + "dependencies": { 10734 + "json-parse-even-better-errors": "^4.0.0", 10735 + "just-diff": "^6.0.0", 10736 + "just-diff-apply": "^5.2.0" 10737 + }, 10738 + "engines": { 10739 + "node": "^18.17.0 || >=20.5.0" 10740 + } 10741 + }, 10742 + "node_modules/npm/node_modules/path-key": { 10743 + "version": "3.1.1", 10744 + "inBundle": true, 10745 + "license": "MIT", 10746 + "engines": { 10747 + "node": ">=8" 10748 + } 10749 + }, 10750 + "node_modules/npm/node_modules/path-scurry": { 10751 + "version": "2.0.0", 10752 + "inBundle": true, 10753 + "license": "BlueOak-1.0.0", 10754 + "dependencies": { 10755 + "lru-cache": "^11.0.0", 10756 + "minipass": "^7.1.2" 10757 + }, 10758 + "engines": { 10759 + "node": "20 || >=22" 10760 + }, 10761 + "funding": { 10762 + "url": "https://github.com/sponsors/isaacs" 10763 + } 10764 + }, 10765 + "node_modules/npm/node_modules/postcss-selector-parser": { 10766 + "version": "7.1.0", 10767 + "inBundle": true, 10768 + "license": "MIT", 10769 + "dependencies": { 10770 + "cssesc": "^3.0.0", 10771 + "util-deprecate": "^1.0.2" 10772 + }, 10773 + "engines": { 10774 + "node": ">=4" 10775 + } 10776 + }, 10777 + "node_modules/npm/node_modules/proc-log": { 10778 + "version": "5.0.0", 10779 + "inBundle": true, 10780 + "license": "ISC", 10781 + "engines": { 10782 + "node": "^18.17.0 || >=20.5.0" 10783 + } 10784 + }, 10785 + "node_modules/npm/node_modules/proggy": { 10786 + "version": "3.0.0", 10787 + "inBundle": true, 10788 + "license": "ISC", 10789 + "engines": { 10790 + "node": "^18.17.0 || >=20.5.0" 10791 + } 10792 + }, 10793 + "node_modules/npm/node_modules/promise-all-reject-late": { 10794 + "version": "1.0.1", 10795 + "inBundle": true, 10796 + "license": "ISC", 10797 + "funding": { 10798 + "url": "https://github.com/sponsors/isaacs" 10799 + } 10800 + }, 10801 + "node_modules/npm/node_modules/promise-call-limit": { 10802 + "version": "3.0.2", 10803 + "inBundle": true, 10804 + "license": "ISC", 10805 + "funding": { 10806 + "url": "https://github.com/sponsors/isaacs" 10807 + } 10808 + }, 10809 + "node_modules/npm/node_modules/promise-retry": { 10810 + "version": "2.0.1", 10811 + "inBundle": true, 10812 + "license": "MIT", 10813 + "dependencies": { 10814 + "err-code": "^2.0.2", 10815 + "retry": "^0.12.0" 10816 + }, 10817 + "engines": { 10818 + "node": ">=10" 10819 + } 10820 + }, 10821 + "node_modules/npm/node_modules/promzard": { 10822 + "version": "2.0.0", 10823 + "inBundle": true, 10824 + "license": "ISC", 10825 + "dependencies": { 10826 + "read": "^4.0.0" 10827 + }, 10828 + "engines": { 10829 + "node": "^18.17.0 || >=20.5.0" 10830 + } 10831 + }, 10832 + "node_modules/npm/node_modules/qrcode-terminal": { 10833 + "version": "0.12.0", 10834 + "inBundle": true, 10835 + "bin": { 10836 + "qrcode-terminal": "bin/qrcode-terminal.js" 10837 + } 10838 + }, 10839 + "node_modules/npm/node_modules/read": { 10840 + "version": "4.1.0", 10841 + "inBundle": true, 10842 + "license": "ISC", 10843 + "dependencies": { 10844 + "mute-stream": "^2.0.0" 10845 + }, 10846 + "engines": { 10847 + "node": "^18.17.0 || >=20.5.0" 10848 + } 10849 + }, 10850 + "node_modules/npm/node_modules/read-cmd-shim": { 10851 + "version": "5.0.0", 10852 + "inBundle": true, 10853 + "license": "ISC", 10854 + "engines": { 10855 + "node": "^18.17.0 || >=20.5.0" 10856 + } 10857 + }, 10858 + "node_modules/npm/node_modules/retry": { 10859 + "version": "0.12.0", 10860 + "inBundle": true, 10861 + "license": "MIT", 10862 + "engines": { 10863 + "node": ">= 4" 10864 + } 10865 + }, 10866 + "node_modules/npm/node_modules/safer-buffer": { 10867 + "version": "2.1.2", 10868 + "inBundle": true, 10869 + "license": "MIT", 10870 + "optional": true 10871 + }, 10872 + "node_modules/npm/node_modules/semver": { 10873 + "version": "7.7.3", 10874 + "inBundle": true, 10875 + "license": "ISC", 10876 + "bin": { 10877 + "semver": "bin/semver.js" 10878 + }, 10879 + "engines": { 10880 + "node": ">=10" 10881 + } 10882 + }, 10883 + "node_modules/npm/node_modules/shebang-command": { 10884 + "version": "2.0.0", 10885 + "inBundle": true, 10886 + "license": "MIT", 10887 + "dependencies": { 10888 + "shebang-regex": "^3.0.0" 10889 + }, 10890 + "engines": { 10891 + "node": ">=8" 10892 + } 10893 + }, 10894 + "node_modules/npm/node_modules/shebang-regex": { 10895 + "version": "3.0.0", 10896 + "inBundle": true, 10897 + "license": "MIT", 10898 + "engines": { 10899 + "node": ">=8" 10900 + } 10901 + }, 10902 + "node_modules/npm/node_modules/signal-exit": { 10903 + "version": "4.1.0", 10904 + "inBundle": true, 10905 + "license": "ISC", 10906 + "engines": { 10907 + "node": ">=14" 10908 + }, 10909 + "funding": { 10910 + "url": "https://github.com/sponsors/isaacs" 10911 + } 10912 + }, 10913 + "node_modules/npm/node_modules/sigstore": { 10914 + "version": "4.0.0", 10915 + "inBundle": true, 10916 + "license": "Apache-2.0", 10917 + "dependencies": { 10918 + "@sigstore/bundle": "^4.0.0", 10919 + "@sigstore/core": "^3.0.0", 10920 + "@sigstore/protobuf-specs": "^0.5.0", 10921 + "@sigstore/sign": "^4.0.0", 10922 + "@sigstore/tuf": "^4.0.0", 10923 + "@sigstore/verify": "^3.0.0" 10924 + }, 10925 + "engines": { 10926 + "node": "^20.17.0 || >=22.9.0" 10927 + } 10928 + }, 10929 + "node_modules/npm/node_modules/smart-buffer": { 10930 + "version": "4.2.0", 10931 + "inBundle": true, 10932 + "license": "MIT", 10933 + "engines": { 10934 + "node": ">= 6.0.0", 10935 + "npm": ">= 3.0.0" 10936 + } 10937 + }, 10938 + "node_modules/npm/node_modules/socks": { 10939 + "version": "2.8.7", 10940 + "inBundle": true, 10941 + "license": "MIT", 10942 + "dependencies": { 10943 + "ip-address": "^10.0.1", 10944 + "smart-buffer": "^4.2.0" 10945 + }, 10946 + "engines": { 10947 + "node": ">= 10.0.0", 10948 + "npm": ">= 3.0.0" 10949 + } 10950 + }, 10951 + "node_modules/npm/node_modules/socks-proxy-agent": { 10952 + "version": "8.0.5", 10953 + "inBundle": true, 10954 + "license": "MIT", 10955 + "dependencies": { 10956 + "agent-base": "^7.1.2", 10957 + "debug": "^4.3.4", 10958 + "socks": "^2.8.3" 10959 + }, 10960 + "engines": { 10961 + "node": ">= 14" 10962 + } 10963 + }, 10964 + "node_modules/npm/node_modules/spdx-correct": { 10965 + "version": "3.2.0", 10966 + "inBundle": true, 10967 + "license": "Apache-2.0", 10968 + "dependencies": { 10969 + "spdx-expression-parse": "^3.0.0", 10970 + "spdx-license-ids": "^3.0.0" 10971 + } 10972 + }, 10973 + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { 10974 + "version": "3.0.1", 10975 + "inBundle": true, 10976 + "license": "MIT", 10977 + "dependencies": { 10978 + "spdx-exceptions": "^2.1.0", 10979 + "spdx-license-ids": "^3.0.0" 10980 + } 10981 + }, 10982 + "node_modules/npm/node_modules/spdx-exceptions": { 10983 + "version": "2.5.0", 10984 + "inBundle": true, 10985 + "license": "CC-BY-3.0" 10986 + }, 10987 + "node_modules/npm/node_modules/spdx-expression-parse": { 10988 + "version": "4.0.0", 10989 + "inBundle": true, 10990 + "license": "MIT", 10991 + "dependencies": { 10992 + "spdx-exceptions": "^2.1.0", 10993 + "spdx-license-ids": "^3.0.0" 10994 + } 10995 + }, 10996 + "node_modules/npm/node_modules/spdx-license-ids": { 10997 + "version": "3.0.22", 10998 + "inBundle": true, 10999 + "license": "CC0-1.0" 11000 + }, 11001 + "node_modules/npm/node_modules/ssri": { 11002 + "version": "12.0.0", 11003 + "inBundle": true, 11004 + "license": "ISC", 11005 + "dependencies": { 11006 + "minipass": "^7.0.3" 11007 + }, 11008 + "engines": { 11009 + "node": "^18.17.0 || >=20.5.0" 11010 + } 11011 + }, 11012 + "node_modules/npm/node_modules/string-width": { 11013 + "version": "4.2.3", 11014 + "inBundle": true, 11015 + "license": "MIT", 11016 + "dependencies": { 11017 + "emoji-regex": "^8.0.0", 11018 + "is-fullwidth-code-point": "^3.0.0", 11019 + "strip-ansi": "^6.0.1" 11020 + }, 11021 + "engines": { 11022 + "node": ">=8" 11023 + } 11024 + }, 11025 + "node_modules/npm/node_modules/string-width-cjs": { 11026 + "name": "string-width", 11027 + "version": "4.2.3", 11028 + "inBundle": true, 11029 + "license": "MIT", 11030 + "dependencies": { 11031 + "emoji-regex": "^8.0.0", 11032 + "is-fullwidth-code-point": "^3.0.0", 11033 + "strip-ansi": "^6.0.1" 11034 + }, 11035 + "engines": { 11036 + "node": ">=8" 11037 + } 11038 + }, 11039 + "node_modules/npm/node_modules/strip-ansi": { 11040 + "version": "6.0.1", 11041 + "inBundle": true, 11042 + "license": "MIT", 11043 + "dependencies": { 11044 + "ansi-regex": "^5.0.1" 11045 + }, 11046 + "engines": { 11047 + "node": ">=8" 11048 + } 11049 + }, 11050 + "node_modules/npm/node_modules/strip-ansi-cjs": { 11051 + "name": "strip-ansi", 11052 + "version": "6.0.1", 11053 + "inBundle": true, 11054 + "license": "MIT", 11055 + "dependencies": { 11056 + "ansi-regex": "^5.0.1" 11057 + }, 11058 + "engines": { 11059 + "node": ">=8" 11060 + } 11061 + }, 11062 + "node_modules/npm/node_modules/supports-color": { 11063 + "version": "10.2.2", 11064 + "inBundle": true, 11065 + "license": "MIT", 11066 + "engines": { 11067 + "node": ">=18" 11068 + }, 11069 + "funding": { 11070 + "url": "https://github.com/chalk/supports-color?sponsor=1" 11071 + } 11072 + }, 11073 + "node_modules/npm/node_modules/tar": { 11074 + "version": "7.5.1", 11075 + "inBundle": true, 11076 + "license": "ISC", 11077 + "dependencies": { 11078 + "@isaacs/fs-minipass": "^4.0.0", 11079 + "chownr": "^3.0.0", 11080 + "minipass": "^7.1.2", 11081 + "minizlib": "^3.1.0", 11082 + "yallist": "^5.0.0" 11083 + }, 11084 + "engines": { 11085 + "node": ">=18" 11086 + } 11087 + }, 11088 + "node_modules/npm/node_modules/tar/node_modules/yallist": { 11089 + "version": "5.0.0", 11090 + "inBundle": true, 11091 + "license": "BlueOak-1.0.0", 11092 + "engines": { 11093 + "node": ">=18" 11094 + } 11095 + }, 11096 + "node_modules/npm/node_modules/text-table": { 11097 + "version": "0.2.0", 11098 + "inBundle": true, 11099 + "license": "MIT" 11100 + }, 11101 + "node_modules/npm/node_modules/tiny-relative-date": { 11102 + "version": "2.0.2", 11103 + "inBundle": true, 11104 + "license": "MIT" 11105 + }, 11106 + "node_modules/npm/node_modules/tinyglobby": { 11107 + "version": "0.2.15", 11108 + "inBundle": true, 11109 + "license": "MIT", 11110 + "dependencies": { 11111 + "fdir": "^6.5.0", 11112 + "picomatch": "^4.0.3" 11113 + }, 11114 + "engines": { 11115 + "node": ">=12.0.0" 11116 + }, 11117 + "funding": { 11118 + "url": "https://github.com/sponsors/SuperchupuDev" 11119 + } 11120 + }, 11121 + "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { 11122 + "version": "6.5.0", 11123 + "inBundle": true, 11124 + "license": "MIT", 11125 + "engines": { 11126 + "node": ">=12.0.0" 11127 + }, 11128 + "peerDependencies": { 11129 + "picomatch": "^3 || ^4" 11130 + }, 11131 + "peerDependenciesMeta": { 11132 + "picomatch": { 11133 + "optional": true 11134 + } 11135 + } 11136 + }, 11137 + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { 11138 + "version": "4.0.3", 11139 + "inBundle": true, 11140 + "license": "MIT", 11141 + "engines": { 11142 + "node": ">=12" 11143 + }, 11144 + "funding": { 11145 + "url": "https://github.com/sponsors/jonschlinkert" 11146 + } 11147 + }, 11148 + "node_modules/npm/node_modules/treeverse": { 11149 + "version": "3.0.0", 11150 + "inBundle": true, 11151 + "license": "ISC", 11152 + "engines": { 11153 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" 11154 + } 11155 + }, 11156 + "node_modules/npm/node_modules/tuf-js": { 11157 + "version": "4.0.0", 11158 + "inBundle": true, 11159 + "license": "MIT", 11160 + "dependencies": { 11161 + "@tufjs/models": "4.0.0", 11162 + "debug": "^4.4.1", 11163 + "make-fetch-happen": "^15.0.0" 11164 + }, 11165 + "engines": { 11166 + "node": "^20.17.0 || >=22.9.0" 11167 + } 11168 + }, 11169 + "node_modules/npm/node_modules/unique-filename": { 11170 + "version": "4.0.0", 11171 + "inBundle": true, 11172 + "license": "ISC", 11173 + "dependencies": { 11174 + "unique-slug": "^5.0.0" 11175 + }, 11176 + "engines": { 11177 + "node": "^18.17.0 || >=20.5.0" 11178 + } 11179 + }, 11180 + "node_modules/npm/node_modules/unique-slug": { 11181 + "version": "5.0.0", 11182 + "inBundle": true, 11183 + "license": "ISC", 11184 + "dependencies": { 11185 + "imurmurhash": "^0.1.4" 11186 + }, 11187 + "engines": { 11188 + "node": "^18.17.0 || >=20.5.0" 11189 + } 11190 + }, 11191 + "node_modules/npm/node_modules/util-deprecate": { 11192 + "version": "1.0.2", 11193 + "inBundle": true, 11194 + "license": "MIT" 11195 + }, 11196 + "node_modules/npm/node_modules/validate-npm-package-license": { 11197 + "version": "3.0.4", 11198 + "inBundle": true, 11199 + "license": "Apache-2.0", 11200 + "dependencies": { 11201 + "spdx-correct": "^3.0.0", 11202 + "spdx-expression-parse": "^3.0.0" 11203 + } 11204 + }, 11205 + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { 11206 + "version": "3.0.1", 11207 + "inBundle": true, 11208 + "license": "MIT", 11209 + "dependencies": { 11210 + "spdx-exceptions": "^2.1.0", 11211 + "spdx-license-ids": "^3.0.0" 11212 + } 11213 + }, 11214 + "node_modules/npm/node_modules/validate-npm-package-name": { 11215 + "version": "6.0.2", 11216 + "inBundle": true, 11217 + "license": "ISC", 11218 + "engines": { 11219 + "node": "^18.17.0 || >=20.5.0" 11220 + } 11221 + }, 11222 + "node_modules/npm/node_modules/walk-up-path": { 11223 + "version": "4.0.0", 11224 + "inBundle": true, 11225 + "license": "ISC", 11226 + "engines": { 11227 + "node": "20 || >=22" 11228 + } 11229 + }, 11230 + "node_modules/npm/node_modules/which": { 11231 + "version": "5.0.0", 11232 + "inBundle": true, 11233 + "license": "ISC", 11234 + "dependencies": { 11235 + "isexe": "^3.1.1" 11236 + }, 11237 + "bin": { 11238 + "node-which": "bin/which.js" 11239 + }, 11240 + "engines": { 11241 + "node": "^18.17.0 || >=20.5.0" 11242 + } 11243 + }, 11244 + "node_modules/npm/node_modules/wrap-ansi": { 11245 + "version": "8.1.0", 11246 + "inBundle": true, 11247 + "license": "MIT", 11248 + "dependencies": { 11249 + "ansi-styles": "^6.1.0", 11250 + "string-width": "^5.0.1", 11251 + "strip-ansi": "^7.0.1" 11252 + }, 11253 + "engines": { 11254 + "node": ">=12" 11255 + }, 11256 + "funding": { 11257 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 11258 + } 11259 + }, 11260 + "node_modules/npm/node_modules/wrap-ansi-cjs": { 11261 + "name": "wrap-ansi", 11262 + "version": "7.0.0", 11263 + "inBundle": true, 11264 + "license": "MIT", 11265 + "dependencies": { 11266 + "ansi-styles": "^4.0.0", 11267 + "string-width": "^4.1.0", 11268 + "strip-ansi": "^6.0.0" 11269 + }, 11270 + "engines": { 11271 + "node": ">=10" 11272 + }, 11273 + "funding": { 11274 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 11275 + } 11276 + }, 11277 + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 11278 + "version": "4.3.0", 11279 + "inBundle": true, 11280 + "license": "MIT", 11281 + "dependencies": { 11282 + "color-convert": "^2.0.1" 11283 + }, 11284 + "engines": { 11285 + "node": ">=8" 11286 + }, 11287 + "funding": { 11288 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 11289 + } 11290 + }, 11291 + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { 11292 + "version": "6.2.2", 11293 + "inBundle": true, 11294 + "license": "MIT", 11295 + "engines": { 11296 + "node": ">=12" 11297 + }, 11298 + "funding": { 11299 + "url": "https://github.com/chalk/ansi-regex?sponsor=1" 11300 + } 11301 + }, 11302 + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { 11303 + "version": "9.2.2", 11304 + "inBundle": true, 11305 + "license": "MIT" 11306 + }, 11307 + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { 11308 + "version": "5.1.2", 11309 + "inBundle": true, 11310 + "license": "MIT", 11311 + "dependencies": { 11312 + "eastasianwidth": "^0.2.0", 11313 + "emoji-regex": "^9.2.2", 11314 + "strip-ansi": "^7.0.1" 11315 + }, 11316 + "engines": { 11317 + "node": ">=12" 11318 + }, 11319 + "funding": { 11320 + "url": "https://github.com/sponsors/sindresorhus" 11321 + } 11322 + }, 11323 + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { 11324 + "version": "7.1.2", 11325 + "inBundle": true, 11326 + "license": "MIT", 11327 + "dependencies": { 11328 + "ansi-regex": "^6.0.1" 11329 + }, 11330 + "engines": { 11331 + "node": ">=12" 11332 + }, 11333 + "funding": { 11334 + "url": "https://github.com/chalk/strip-ansi?sponsor=1" 11335 + } 11336 + }, 11337 + "node_modules/npm/node_modules/write-file-atomic": { 11338 + "version": "6.0.0", 11339 + "inBundle": true, 11340 + "license": "ISC", 11341 + "dependencies": { 11342 + "imurmurhash": "^0.1.4", 11343 + "signal-exit": "^4.0.1" 11344 + }, 11345 + "engines": { 11346 + "node": "^18.17.0 || >=20.5.0" 11347 + } 11348 + }, 11349 + "node_modules/npm/node_modules/yallist": { 11350 + "version": "4.0.0", 11351 + "inBundle": true, 11352 + "license": "ISC" 11353 + }, 11354 "node_modules/nwsapi": { 11355 "version": "2.2.21", 11356 "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", ··· 11536 "url": "https://github.com/sponsors/sindresorhus" 11537 } 11538 }, 11539 + "node_modules/package-manager-detector": { 11540 + "version": "1.4.1", 11541 + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.1.tgz", 11542 + "integrity": "sha512-dSMiVLBEA4XaNJ0PRb4N5cV/SEP4BWrWZKBmfF+OUm2pQTiZ6DDkKeWaltwu3JRhLoy59ayIkJ00cx9K9CaYTg==", 11543 + "dev": true, 11544 + "license": "MIT" 11545 + }, 11546 "node_modules/parent-module": { 11547 "version": "1.0.1", 11548 "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 11549 "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 11550 "dev": true, 11551 "license": "MIT", 11552 "dependencies": { 11553 "callsites": "^3.0.0" 11554 }, ··· 11556 "node": ">=6" 11557 } 11558 }, 11559 + "node_modules/parse-json": { 11560 + "version": "5.2.0", 11561 + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", 11562 + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", 11563 + "dev": true, 11564 + "license": "MIT", 11565 + "dependencies": { 11566 + "@babel/code-frame": "^7.0.0", 11567 + "error-ex": "^1.3.1", 11568 + "json-parse-even-better-errors": "^2.3.0", 11569 + "lines-and-columns": "^1.1.6" 11570 + }, 11571 + "engines": { 11572 + "node": ">=8" 11573 + }, 11574 + "funding": { 11575 + "url": "https://github.com/sponsors/sindresorhus" 11576 + } 11577 + }, 11578 "node_modules/parse5": { 11579 "version": "7.3.0", 11580 "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", ··· 11623 "dev": true, 11624 "license": "MIT" 11625 }, 11626 + "node_modules/path-type": { 11627 + "version": "4.0.0", 11628 + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 11629 + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 11630 + "dev": true, 11631 + "license": "MIT", 11632 + "engines": { 11633 + "node": ">=8" 11634 + } 11635 + }, 11636 "node_modules/pathe": { 11637 "version": "2.0.3", 11638 "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", ··· 11666 }, 11667 "funding": { 11668 "url": "https://github.com/sponsors/jonschlinkert" 11669 + } 11670 + }, 11671 + "node_modules/pkg-types": { 11672 + "version": "2.3.0", 11673 + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", 11674 + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", 11675 + "dev": true, 11676 + "license": "MIT", 11677 + "dependencies": { 11678 + "confbox": "^0.2.2", 11679 + "exsolve": "^1.0.7", 11680 + "pathe": "^2.0.3" 11681 } 11682 }, 11683 "node_modules/player.style": { ··· 11802 "node": ">=6" 11803 } 11804 }, 11805 + "node_modules/quansync": { 11806 + "version": "0.2.11", 11807 + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", 11808 + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", 11809 + "dev": true, 11810 + "funding": [ 11811 + { 11812 + "type": "individual", 11813 + "url": "https://github.com/sponsors/antfu" 11814 + }, 11815 + { 11816 + "type": "individual", 11817 + "url": "https://github.com/sponsors/sxzz" 11818 + } 11819 + ], 11820 + "license": "MIT" 11821 + }, 11822 "node_modules/queue-microtask": { 11823 "version": "1.2.3", 11824 "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", ··· 11840 ], 11841 "license": "MIT" 11842 }, 11843 + "node_modules/radix-ui": { 11844 + "version": "1.4.3", 11845 + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", 11846 + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", 11847 + "dependencies": { 11848 + "@radix-ui/primitive": "1.1.3", 11849 + "@radix-ui/react-accessible-icon": "1.1.7", 11850 + "@radix-ui/react-accordion": "1.2.12", 11851 + "@radix-ui/react-alert-dialog": "1.1.15", 11852 + "@radix-ui/react-arrow": "1.1.7", 11853 + "@radix-ui/react-aspect-ratio": "1.1.7", 11854 + "@radix-ui/react-avatar": "1.1.10", 11855 + "@radix-ui/react-checkbox": "1.3.3", 11856 + "@radix-ui/react-collapsible": "1.1.12", 11857 + "@radix-ui/react-collection": "1.1.7", 11858 + "@radix-ui/react-compose-refs": "1.1.2", 11859 + "@radix-ui/react-context": "1.1.2", 11860 + "@radix-ui/react-context-menu": "2.2.16", 11861 + "@radix-ui/react-dialog": "1.1.15", 11862 + "@radix-ui/react-direction": "1.1.1", 11863 + "@radix-ui/react-dismissable-layer": "1.1.11", 11864 + "@radix-ui/react-dropdown-menu": "2.1.16", 11865 + "@radix-ui/react-focus-guards": "1.1.3", 11866 + "@radix-ui/react-focus-scope": "1.1.7", 11867 + "@radix-ui/react-form": "0.1.8", 11868 + "@radix-ui/react-hover-card": "1.1.15", 11869 + "@radix-ui/react-label": "2.1.7", 11870 + "@radix-ui/react-menu": "2.1.16", 11871 + "@radix-ui/react-menubar": "1.1.16", 11872 + "@radix-ui/react-navigation-menu": "1.2.14", 11873 + "@radix-ui/react-one-time-password-field": "0.1.8", 11874 + "@radix-ui/react-password-toggle-field": "0.1.3", 11875 + "@radix-ui/react-popover": "1.1.15", 11876 + "@radix-ui/react-popper": "1.2.8", 11877 + "@radix-ui/react-portal": "1.1.9", 11878 + "@radix-ui/react-presence": "1.1.5", 11879 + "@radix-ui/react-primitive": "2.1.3", 11880 + "@radix-ui/react-progress": "1.1.7", 11881 + "@radix-ui/react-radio-group": "1.3.8", 11882 + "@radix-ui/react-roving-focus": "1.1.11", 11883 + "@radix-ui/react-scroll-area": "1.2.10", 11884 + "@radix-ui/react-select": "2.2.6", 11885 + "@radix-ui/react-separator": "1.1.7", 11886 + "@radix-ui/react-slider": "1.3.6", 11887 + "@radix-ui/react-slot": "1.2.3", 11888 + "@radix-ui/react-switch": "1.2.6", 11889 + "@radix-ui/react-tabs": "1.1.13", 11890 + "@radix-ui/react-toast": "1.2.15", 11891 + "@radix-ui/react-toggle": "1.1.10", 11892 + "@radix-ui/react-toggle-group": "1.1.11", 11893 + "@radix-ui/react-toolbar": "1.1.11", 11894 + "@radix-ui/react-tooltip": "1.2.8", 11895 + "@radix-ui/react-use-callback-ref": "1.1.1", 11896 + "@radix-ui/react-use-controllable-state": "1.2.2", 11897 + "@radix-ui/react-use-effect-event": "0.0.2", 11898 + "@radix-ui/react-use-escape-keydown": "1.1.1", 11899 + "@radix-ui/react-use-is-hydrated": "0.1.0", 11900 + "@radix-ui/react-use-layout-effect": "1.1.1", 11901 + "@radix-ui/react-use-size": "1.1.1", 11902 + "@radix-ui/react-visually-hidden": "1.2.3" 11903 + }, 11904 + "peerDependencies": { 11905 + "@types/react": "*", 11906 + "@types/react-dom": "*", 11907 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 11908 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 11909 + }, 11910 + "peerDependenciesMeta": { 11911 + "@types/react": { 11912 + "optional": true 11913 + }, 11914 + "@types/react-dom": { 11915 + "optional": true 11916 + } 11917 + } 11918 + }, 11919 "node_modules/react": { 11920 "version": "19.1.1", 11921 "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", ··· 11977 "node": ">=0.10.0" 11978 } 11979 }, 11980 + "node_modules/react-remove-scroll": { 11981 + "version": "2.7.1", 11982 + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", 11983 + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", 11984 + "dependencies": { 11985 + "react-remove-scroll-bar": "^2.3.7", 11986 + "react-style-singleton": "^2.2.3", 11987 + "tslib": "^2.1.0", 11988 + "use-callback-ref": "^1.3.3", 11989 + "use-sidecar": "^1.1.3" 11990 + }, 11991 + "engines": { 11992 + "node": ">=10" 11993 + }, 11994 + "peerDependencies": { 11995 + "@types/react": "*", 11996 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 11997 + }, 11998 + "peerDependenciesMeta": { 11999 + "@types/react": { 12000 + "optional": true 12001 + } 12002 + } 12003 + }, 12004 + "node_modules/react-remove-scroll-bar": { 12005 + "version": "2.3.8", 12006 + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", 12007 + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", 12008 + "dependencies": { 12009 + "react-style-singleton": "^2.2.2", 12010 + "tslib": "^2.0.0" 12011 + }, 12012 + "engines": { 12013 + "node": ">=10" 12014 + }, 12015 + "peerDependencies": { 12016 + "@types/react": "*", 12017 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 12018 + }, 12019 + "peerDependenciesMeta": { 12020 + "@types/react": { 12021 + "optional": true 12022 + } 12023 + } 12024 + }, 12025 + "node_modules/react-style-singleton": { 12026 + "version": "2.2.3", 12027 + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", 12028 + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", 12029 + "dependencies": { 12030 + "get-nonce": "^1.0.0", 12031 + "tslib": "^2.0.0" 12032 + }, 12033 + "engines": { 12034 + "node": ">=10" 12035 + }, 12036 + "peerDependencies": { 12037 + "@types/react": "*", 12038 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 12039 + }, 12040 + "peerDependenciesMeta": { 12041 + "@types/react": { 12042 + "optional": true 12043 + } 12044 + } 12045 + }, 12046 "node_modules/readdirp": { 12047 "version": "3.6.0", 12048 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", ··· 12148 "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 12149 "dev": true, 12150 "license": "MIT", 12151 "engines": { 12152 "node": ">=4" 12153 } ··· 12329 "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", 12330 "license": "MIT" 12331 }, 12332 + "node_modules/scule": { 12333 + "version": "1.3.0", 12334 + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", 12335 + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", 12336 + "dev": true, 12337 + "license": "MIT" 12338 + }, 12339 "node_modules/semver": { 12340 "version": "6.3.1", 12341 "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", ··· 12523 "dev": true, 12524 "license": "ISC" 12525 }, 12526 + "node_modules/snake-case": { 12527 + "version": "3.0.4", 12528 + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", 12529 + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", 12530 + "dev": true, 12531 + "license": "MIT", 12532 + "dependencies": { 12533 + "dot-case": "^3.0.4", 12534 + "tslib": "^2.0.3" 12535 + } 12536 + }, 12537 "node_modules/solid-js": { 12538 "version": "1.9.9", 12539 "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz", ··· 12717 } 12718 }, 12719 "node_modules/strip-literal": { 12720 + "version": "3.1.0", 12721 + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", 12722 + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", 12723 "dev": true, 12724 "license": "MIT", 12725 "dependencies": { ··· 12769 "url": "https://github.com/sponsors/ljharb" 12770 } 12771 }, 12772 + "node_modules/svg-parser": { 12773 + "version": "2.0.4", 12774 + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", 12775 + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", 12776 + "dev": true, 12777 + "license": "MIT" 12778 + }, 12779 "node_modules/symbol-tree": { 12780 "version": "3.2.4", 12781 "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", ··· 12788 "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", 12789 "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", 12790 "license": "MIT" 12791 + }, 12792 + "node_modules/tanstack-router-keepalive": { 12793 + "version": "1.0.0", 12794 + "resolved": "https://registry.npmjs.org/tanstack-router-keepalive/-/tanstack-router-keepalive-1.0.0.tgz", 12795 + "integrity": "sha512-SxMl9sgIZGjB4OZvGXufTz14ygmZi+eAbrhz3sjmXYZzhSQOekx5LYi9TvKcMXVu8fQR6W/itF5hdglqD6WB/w==", 12796 + "license": "MIT", 12797 + "dependencies": { 12798 + "eventemitter3": "^5.0.1", 12799 + "lodash.clonedeep": "^4.5.0" 12800 + } 12801 }, 12802 "node_modules/tapable": { 12803 "version": "2.2.3", ··· 12871 "license": "MIT" 12872 }, 12873 "node_modules/tinyglobby": { 12874 + "version": "0.2.15", 12875 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 12876 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 12877 "license": "MIT", 12878 "dependencies": { 12879 + "fdir": "^6.5.0", 12880 + "picomatch": "^4.0.3" 12881 }, 12882 "engines": { 12883 "node": ">=12.0.0" ··· 13255 "node": "*" 13256 } 13257 }, 13258 + "node_modules/ufo": { 13259 + "version": "1.6.1", 13260 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", 13261 + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", 13262 + "dev": true, 13263 + "license": "MIT" 13264 + }, 13265 "node_modules/uint8arrays": { 13266 "version": "3.0.0", 13267 "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", ··· 13297 "devOptional": true, 13298 "license": "MIT" 13299 }, 13300 + "node_modules/unimport": { 13301 + "version": "5.5.0", 13302 + "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.5.0.tgz", 13303 + "integrity": "sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg==", 13304 + "dev": true, 13305 + "license": "MIT", 13306 + "dependencies": { 13307 + "acorn": "^8.15.0", 13308 + "escape-string-regexp": "^5.0.0", 13309 + "estree-walker": "^3.0.3", 13310 + "local-pkg": "^1.1.2", 13311 + "magic-string": "^0.30.19", 13312 + "mlly": "^1.8.0", 13313 + "pathe": "^2.0.3", 13314 + "picomatch": "^4.0.3", 13315 + "pkg-types": "^2.3.0", 13316 + "scule": "^1.3.0", 13317 + "strip-literal": "^3.1.0", 13318 + "tinyglobby": "^0.2.15", 13319 + "unplugin": "^2.3.10", 13320 + "unplugin-utils": "^0.3.0" 13321 + }, 13322 + "engines": { 13323 + "node": ">=18.12.0" 13324 + } 13325 + }, 13326 + "node_modules/unimport/node_modules/escape-string-regexp": { 13327 + "version": "5.0.0", 13328 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", 13329 + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", 13330 + "dev": true, 13331 + "license": "MIT", 13332 + "engines": { 13333 + "node": ">=12" 13334 + }, 13335 + "funding": { 13336 + "url": "https://github.com/sponsors/sindresorhus" 13337 + } 13338 + }, 13339 + "node_modules/unimport/node_modules/picomatch": { 13340 + "version": "4.0.3", 13341 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 13342 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 13343 + "dev": true, 13344 + "license": "MIT", 13345 + "engines": { 13346 + "node": ">=12" 13347 + }, 13348 + "funding": { 13349 + "url": "https://github.com/sponsors/jonschlinkert" 13350 + } 13351 + }, 13352 "node_modules/unplugin": { 13353 + "version": "2.3.10", 13354 + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", 13355 + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", 13356 "license": "MIT", 13357 "dependencies": { 13358 "@jridgewell/remapping": "^2.3.5", ··· 13364 "node": ">=18.12.0" 13365 } 13366 }, 13367 + "node_modules/unplugin-auto-import": { 13368 + "version": "20.2.0", 13369 + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-20.2.0.tgz", 13370 + "integrity": "sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==", 13371 + "dev": true, 13372 + "license": "MIT", 13373 + "dependencies": { 13374 + "local-pkg": "^1.1.2", 13375 + "magic-string": "^0.30.19", 13376 + "picomatch": "^4.0.3", 13377 + "unimport": "^5.4.0", 13378 + "unplugin": "^2.3.10", 13379 + "unplugin-utils": "^0.3.0" 13380 + }, 13381 + "engines": { 13382 + "node": ">=14" 13383 + }, 13384 + "funding": { 13385 + "url": "https://github.com/sponsors/antfu" 13386 + }, 13387 + "peerDependencies": { 13388 + "@nuxt/kit": "^4.0.0", 13389 + "@vueuse/core": "*" 13390 + }, 13391 + "peerDependenciesMeta": { 13392 + "@nuxt/kit": { 13393 + "optional": true 13394 + }, 13395 + "@vueuse/core": { 13396 + "optional": true 13397 + } 13398 + } 13399 + }, 13400 + "node_modules/unplugin-auto-import/node_modules/picomatch": { 13401 + "version": "4.0.3", 13402 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 13403 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 13404 + "dev": true, 13405 + "license": "MIT", 13406 + "engines": { 13407 + "node": ">=12" 13408 + }, 13409 + "funding": { 13410 + "url": "https://github.com/sponsors/jonschlinkert" 13411 + } 13412 + }, 13413 + "node_modules/unplugin-icons": { 13414 + "version": "22.4.2", 13415 + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-22.4.2.tgz", 13416 + "integrity": "sha512-Yv15405unO67Chme0Slk0JRA/H2AiAZLK5t7ebt8/ZpTDlBfM4d4En2qD3MX2rzOSkIteQ0syIm3q8MSofeoBA==", 13417 + "dev": true, 13418 + "license": "MIT", 13419 + "dependencies": { 13420 + "@antfu/install-pkg": "^1.1.0", 13421 + "@iconify/utils": "^3.0.2", 13422 + "debug": "^4.4.3", 13423 + "local-pkg": "^1.1.2", 13424 + "unplugin": "^2.3.10" 13425 + }, 13426 + "funding": { 13427 + "url": "https://github.com/sponsors/antfu" 13428 + }, 13429 + "peerDependencies": { 13430 + "@svgr/core": ">=7.0.0", 13431 + "@svgx/core": "^1.0.1", 13432 + "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", 13433 + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", 13434 + "vue-template-compiler": "^2.6.12", 13435 + "vue-template-es2015-compiler": "^1.9.0" 13436 + }, 13437 + "peerDependenciesMeta": { 13438 + "@svgr/core": { 13439 + "optional": true 13440 + }, 13441 + "@svgx/core": { 13442 + "optional": true 13443 + }, 13444 + "@vue/compiler-sfc": { 13445 + "optional": true 13446 + }, 13447 + "svelte": { 13448 + "optional": true 13449 + }, 13450 + "vue-template-compiler": { 13451 + "optional": true 13452 + }, 13453 + "vue-template-es2015-compiler": { 13454 + "optional": true 13455 + } 13456 + } 13457 + }, 13458 + "node_modules/unplugin-utils": { 13459 + "version": "0.3.1", 13460 + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", 13461 + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", 13462 + "dev": true, 13463 + "license": "MIT", 13464 + "dependencies": { 13465 + "pathe": "^2.0.3", 13466 + "picomatch": "^4.0.3" 13467 + }, 13468 + "engines": { 13469 + "node": ">=20.19.0" 13470 + }, 13471 + "funding": { 13472 + "url": "https://github.com/sponsors/sxzz" 13473 + } 13474 + }, 13475 + "node_modules/unplugin-utils/node_modules/picomatch": { 13476 + "version": "4.0.3", 13477 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 13478 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 13479 + "dev": true, 13480 + "license": "MIT", 13481 + "engines": { 13482 + "node": ">=12" 13483 + }, 13484 + "funding": { 13485 + "url": "https://github.com/sponsors/jonschlinkert" 13486 + } 13487 + }, 13488 "node_modules/unplugin/node_modules/picomatch": { 13489 "version": "4.0.3", 13490 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", ··· 13536 "peer": true, 13537 "dependencies": { 13538 "punycode": "^2.1.0" 13539 + } 13540 + }, 13541 + "node_modules/use-callback-ref": { 13542 + "version": "1.3.3", 13543 + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", 13544 + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", 13545 + "dependencies": { 13546 + "tslib": "^2.0.0" 13547 + }, 13548 + "engines": { 13549 + "node": ">=10" 13550 + }, 13551 + "peerDependencies": { 13552 + "@types/react": "*", 13553 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 13554 + }, 13555 + "peerDependenciesMeta": { 13556 + "@types/react": { 13557 + "optional": true 13558 + } 13559 + } 13560 + }, 13561 + "node_modules/use-sidecar": { 13562 + "version": "1.1.3", 13563 + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", 13564 + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", 13565 + "dependencies": { 13566 + "detect-node-es": "^1.1.0", 13567 + "tslib": "^2.0.0" 13568 + }, 13569 + "engines": { 13570 + "node": ">=10" 13571 + }, 13572 + "peerDependencies": { 13573 + "@types/react": "*", 13574 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 13575 + }, 13576 + "peerDependenciesMeta": { 13577 + "@types/react": { 13578 + "optional": true 13579 + } 13580 } 13581 }, 13582 "node_modules/use-sync-external-store": {
+18 -1
package.json
··· 12 "dependencies": { 13 "@atproto/api": "^0.16.6", 14 "@atproto/oauth-client-browser": "^0.3.33", 15 "@tailwindcss/vite": "^4.0.6", 16 "@tanstack/query-sync-storage-persister": "^5.85.6", 17 "@tanstack/react-devtools": "^0.2.2", ··· 20 "@tanstack/react-router": "^1.130.2", 21 "@tanstack/react-router-devtools": "^1.131.5", 22 "@tanstack/router-plugin": "^1.121.2", 23 "idb-keyval": "^6.2.2", 24 "jotai": "^2.13.1", 25 "react": "^19.0.0", 26 "react-dom": "^19.0.0", 27 "react-player": "^3.3.2", 28 - "tailwindcss": "^4.0.6" 29 }, 30 "devDependencies": { 31 "@eslint-react/eslint-plugin": "^2.2.1", 32 "@testing-library/dom": "^10.4.0", 33 "@testing-library/react": "^16.2.0", 34 "@types/node": "^24.3.0", ··· 46 "prettier": "^3.6.2", 47 "typescript": "^5.7.2", 48 "typescript-eslint": "^8.46.1", 49 "vite": "^6.3.5", 50 "vitest": "^3.0.5", 51 "web-vitals": "^4.2.4"
··· 12 "dependencies": { 13 "@atproto/api": "^0.16.6", 14 "@atproto/oauth-client-browser": "^0.3.33", 15 + "@radix-ui/react-dialog": "^1.1.15", 16 + "@radix-ui/react-dropdown-menu": "^2.1.16", 17 + "@radix-ui/react-hover-card": "^1.1.15", 18 + "@radix-ui/react-slider": "^1.3.6", 19 "@tailwindcss/vite": "^4.0.6", 20 "@tanstack/query-sync-storage-persister": "^5.85.6", 21 "@tanstack/react-devtools": "^0.2.2", ··· 24 "@tanstack/react-router": "^1.130.2", 25 "@tanstack/react-router-devtools": "^1.131.5", 26 "@tanstack/router-plugin": "^1.121.2", 27 + "dompurify": "^3.3.0", 28 + "i": "^0.3.7", 29 "idb-keyval": "^6.2.2", 30 "jotai": "^2.13.1", 31 + "npm": "^11.6.2", 32 + "radix-ui": "^1.4.3", 33 "react": "^19.0.0", 34 "react-dom": "^19.0.0", 35 "react-player": "^3.3.2", 36 + "tailwindcss": "^4.0.6", 37 + "tanstack-router-keepalive": "^1.0.0" 38 }, 39 "devDependencies": { 40 "@eslint-react/eslint-plugin": "^2.2.1", 41 + "@iconify-icon/react": "^3.0.1", 42 + "@iconify-json/material-symbols": "^1.2.42", 43 + "@iconify-json/mdi": "^1.2.3", 44 + "@iconify/json": "^2.2.396", 45 + "@svgr/core": "^8.1.0", 46 + "@svgr/plugin-jsx": "^8.1.0", 47 "@testing-library/dom": "^10.4.0", 48 "@testing-library/react": "^16.2.0", 49 "@types/node": "^24.3.0", ··· 61 "prettier": "^3.6.2", 62 "typescript": "^5.7.2", 63 "typescript-eslint": "^8.46.1", 64 + "unplugin-auto-import": "^20.2.0", 65 + "unplugin-icons": "^22.4.2", 66 "vite": "^6.3.5", 67 "vitest": "^3.0.5", 68 "web-vitals": "^4.2.4"
public/screenshot.jpg

This is a binary file and will not be displayed.

public/screenshot.png

This is a binary file and will not be displayed.

+22
src/auto-imports.d.ts
···
··· 1 + /* eslint-disable */ 2 + /* prettier-ignore */ 3 + // @ts-nocheck 4 + // noinspection JSUnusedGlobalSymbols 5 + // Generated by unplugin-auto-import 6 + // biome-ignore lint: disable 7 + export {} 8 + declare global { 9 + const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default 10 + const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default 11 + const IconMaterialSymbolsArrowBack: typeof import('~icons/material-symbols/arrow-back.jsx').default 12 + const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default 13 + const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default 14 + const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default 15 + const IconMaterialSymbolsNotificationsOutline: typeof import('~icons/material-symbols/notifications-outline.jsx').default 16 + const IconMaterialSymbolsSearch: typeof import('~icons/material-symbols/search.jsx').default 17 + const IconMaterialSymbolsSettings: typeof import('~icons/material-symbols/settings.jsx').default 18 + const IconMaterialSymbolsSettingsOutline: typeof import('~icons/material-symbols/settings-outline.jsx').default 19 + const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default 20 + const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default 21 + const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default 22 + }
+292
src/components/Composer.tsx
···
··· 1 + import { AppBskyRichtextFacet, RichText } from "@atproto/api"; 2 + import { useAtom } from "jotai"; 3 + import { Dialog } from "radix-ui"; 4 + import { useEffect, useRef, useState } from "react"; 5 + 6 + import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 + import { composerAtom } from "~/utils/atoms"; 8 + import { useQueryPost } from "~/utils/useQuery"; 9 + 10 + import { ProfileThing } from "./Login"; 11 + import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer"; 12 + 13 + const MAX_POST_LENGTH = 300; 14 + 15 + export function Composer() { 16 + const [composerState, setComposerState] = useAtom(composerAtom); 17 + const { agent } = useAuth(); 18 + 19 + const [postText, setPostText] = useState(""); 20 + const [posting, setPosting] = useState(false); 21 + const [postSuccess, setPostSuccess] = useState(false); 22 + const [postError, setPostError] = useState<string | null>(null); 23 + 24 + useEffect(() => { 25 + setPostText(""); 26 + setPosting(false); 27 + setPostSuccess(false); 28 + setPostError(null); 29 + }, [composerState.kind]); 30 + 31 + const parentUri = 32 + composerState.kind === "reply" 33 + ? composerState.parent 34 + : composerState.kind === "quote" 35 + ? composerState.subject 36 + : undefined; 37 + 38 + const { data: parentPost, isLoading: isParentLoading } = 39 + useQueryPost(parentUri); 40 + 41 + async function handlePost() { 42 + if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return; 43 + 44 + setPosting(true); 45 + setPostError(null); 46 + 47 + try { 48 + const rt = new RichText({ text: postText }); 49 + await rt.detectFacets(agent); 50 + 51 + if (rt.facets?.length) { 52 + rt.facets = rt.facets.filter((item) => { 53 + if (item.$type !== "app.bsky.richtext.facet") return true; 54 + if (!item.features?.length) return true; 55 + 56 + item.features = item.features.filter((feature) => { 57 + if (feature.$type !== "app.bsky.richtext.facet#mention") return true; 58 + const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined; 59 + return typeof did === "string" && did.startsWith("did:"); 60 + }); 61 + 62 + return item.features.length > 0; 63 + }); 64 + } 65 + 66 + const record: Record<string, unknown> = { 67 + $type: "app.bsky.feed.post", 68 + text: rt.text, 69 + facets: rt.facets, 70 + createdAt: new Date().toISOString(), 71 + }; 72 + 73 + if (composerState.kind === "reply" && parentPost) { 74 + record.reply = { 75 + root: parentPost.value?.reply?.root ?? { 76 + uri: parentPost.uri, 77 + cid: parentPost.cid, 78 + }, 79 + parent: { 80 + uri: parentPost.uri, 81 + cid: parentPost.cid, 82 + }, 83 + }; 84 + } 85 + 86 + if (composerState.kind === "quote" && parentPost) { 87 + record.embed = { 88 + $type: "app.bsky.embed.record", 89 + record: { 90 + uri: parentPost.uri, 91 + cid: parentPost.cid, 92 + }, 93 + }; 94 + } 95 + 96 + await agent.com.atproto.repo.createRecord({ 97 + collection: "app.bsky.feed.post", 98 + repo: agent.assertDid, 99 + record, 100 + }); 101 + 102 + setPostSuccess(true); 103 + setPostText(""); 104 + 105 + setTimeout(() => { 106 + setPostSuccess(false); 107 + setComposerState({ kind: "closed" }); 108 + }, 1500); 109 + } catch (e: any) { 110 + setPostError(e?.message || "Failed to post"); 111 + } finally { 112 + setPosting(false); 113 + } 114 + } 115 + // if (composerState.kind === "closed") { 116 + // return null; 117 + // } 118 + 119 + const getPlaceholder = () => { 120 + switch (composerState.kind) { 121 + case "reply": 122 + return "Post your reply"; 123 + case "quote": 124 + return "Add a comment..."; 125 + case "root": 126 + default: 127 + return "What's happening?!"; 128 + } 129 + }; 130 + 131 + const charsLeft = MAX_POST_LENGTH - postText.length; 132 + const isPostButtonDisabled = 133 + posting || !postText.trim() || isParentLoading || charsLeft < 0; 134 + 135 + return ( 136 + <Dialog.Root 137 + open={composerState.kind !== "closed"} 138 + onOpenChange={(open) => { 139 + if (!open) setComposerState({ kind: "closed" }); 140 + }} 141 + > 142 + <Dialog.Portal> 143 + <Dialog.Overlay className="fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 144 + 145 + <Dialog.Content className="fixed overflow-y-scroll inset-0 z-50 flex items-start justify-center py-10 sm:py-20"> 146 + <div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4"> 147 + <div className="flex flex-row justify-between p-2"> 148 + <Dialog.Close asChild> 149 + <button 150 + className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800" 151 + disabled={posting} 152 + aria-label="Close" 153 + > 154 + <svg 155 + xmlns="http://www.w3.org/2000/svg" 156 + width="20" 157 + height="20" 158 + viewBox="0 0 24 24" 159 + fill="none" 160 + stroke="currentColor" 161 + strokeWidth="2.5" 162 + strokeLinecap="round" 163 + strokeLinejoin="round" 164 + > 165 + <line x1="18" y1="6" x2="6" y2="18"></line> 166 + <line x1="6" y1="6" x2="18" y2="18"></line> 167 + </svg> 168 + </button> 169 + </Dialog.Close> 170 + 171 + <div className="flex-1" /> 172 + <div className="flex items-center gap-4"> 173 + <span 174 + className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`} 175 + > 176 + {charsLeft} 177 + </span> 178 + <button 179 + className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors" 180 + onClick={handlePost} 181 + disabled={isPostButtonDisabled} 182 + > 183 + {posting ? "Posting..." : "Post"} 184 + </button> 185 + </div> 186 + </div> 187 + 188 + {postSuccess ? ( 189 + <div className="flex flex-col items-center justify-center py-16"> 190 + <span className="text-gray-500 text-6xl mb-4">โœ“</span> 191 + <span className="text-xl font-bold text-black dark:text-white"> 192 + Posted! 193 + </span> 194 + </div> 195 + ) : ( 196 + <div className="px-4"> 197 + {composerState.kind === "reply" && ( 198 + <div className="mb-1 -mx-4"> 199 + {isParentLoading ? ( 200 + <div className="text-sm text-gray-500 animate-pulse"> 201 + Loading parent post... 202 + </div> 203 + ) : parentUri ? ( 204 + <UniversalPostRendererATURILoader 205 + atUri={parentUri} 206 + bottomReplyLine 207 + bottomBorder={false} 208 + /> 209 + ) : ( 210 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 211 + Could not load parent post. 212 + </div> 213 + )} 214 + </div> 215 + )} 216 + 217 + <div className="flex w-full gap-1 flex-col"> 218 + <ProfileThing agent={agent} large /> 219 + <div className="flex pl-[50px]"> 220 + <AutoGrowTextarea 221 + className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2" 222 + rows={5} 223 + placeholder={getPlaceholder()} 224 + value={postText} 225 + onChange={(e) => setPostText(e.target.value)} 226 + disabled={posting} 227 + autoFocus 228 + /> 229 + </div> 230 + </div> 231 + 232 + {composerState.kind === "quote" && ( 233 + <div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> 234 + {isParentLoading ? ( 235 + <div className="text-sm text-gray-500 animate-pulse"> 236 + Loading parent post... 237 + </div> 238 + ) : parentUri ? ( 239 + <UniversalPostRendererATURILoader 240 + atUri={parentUri} 241 + isQuote 242 + /> 243 + ) : ( 244 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 245 + Could not load parent post. 246 + </div> 247 + )} 248 + </div> 249 + )} 250 + 251 + {postError && ( 252 + <div className="text-red-500 text-sm my-2 text-center"> 253 + {postError} 254 + </div> 255 + )} 256 + </div> 257 + )} 258 + </div> 259 + </Dialog.Content> 260 + </Dialog.Portal> 261 + </Dialog.Root> 262 + ); 263 + } 264 + 265 + function AutoGrowTextarea({ 266 + value, 267 + className, 268 + onChange, 269 + ...props 270 + }: React.DetailedHTMLProps< 271 + React.TextareaHTMLAttributes<HTMLTextAreaElement>, 272 + HTMLTextAreaElement 273 + >) { 274 + const ref = useRef<HTMLTextAreaElement>(null); 275 + 276 + useEffect(() => { 277 + const el = ref.current; 278 + if (!el) return; 279 + el.style.height = "auto"; 280 + el.style.height = el.scrollHeight + "px"; 281 + }, [value]); 282 + 283 + return ( 284 + <textarea 285 + ref={ref} 286 + className={className} 287 + value={value} 288 + onChange={onChange} 289 + {...props} 290 + /> 291 + ); 292 + }
+33
src/components/Header.tsx
···
··· 1 + import { Link, useRouter } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + 4 + import { isAtTopAtom } from "~/utils/atoms"; 5 + 6 + export function Header({ 7 + backButtonCallback, 8 + title 9 + }: { 10 + backButtonCallback?: () => void; 11 + title?: string; 12 + }) { 13 + const router = useRouter(); 14 + const [isAtTop] = useAtom(isAtTopAtom); 15 + //const what = router.history. 16 + return ( 17 + <div className={`flex items-center gap-3 px-3 py-3 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`}> 18 + {backButtonCallback ? (<Link 19 + to=".." 20 + //className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 21 + className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 22 + onClick={(e) => { 23 + e.preventDefault(); 24 + backButtonCallback(); 25 + }} 26 + aria-label="Go back" 27 + > 28 + <IconMaterialSymbolsArrowBack className="w-6 h-6" /> 29 + </Link>) : (<div className="w-[0px]" />)} 30 + <span className="text-[21px] sm:text-[19px] sm:font-semibold font-roboto">{title}</span> 31 + </div> 32 + ); 33 + }
+150
src/components/Import.tsx
···
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { useNavigate, type UseNavigateResult } from "@tanstack/react-router"; 3 + import { useState } from "react"; 4 + 5 + /** 6 + * Basically the best equivalent to Search that i can do 7 + */ 8 + export function Import() { 9 + const [textInput, setTextInput] = useState<string | undefined>(); 10 + const navigate = useNavigate(); 11 + 12 + const handleEnter = () => { 13 + if (!textInput) return; 14 + handleImport({ 15 + text: textInput, 16 + navigate, 17 + }); 18 + }; 19 + 20 + return ( 21 + <div className="w-full relative"> 22 + <IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" /> 23 + 24 + <input 25 + type="text" 26 + placeholder="Import..." 27 + value={textInput} 28 + onChange={(e) => setTextInput(e.target.value)} 29 + onKeyDown={(e) => { 30 + if (e.key === "Enter") handleEnter(); 31 + }} 32 + className="w-full h-12 pl-12 pr-4 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-500 box-border transition" 33 + /> 34 + </div> 35 + ); 36 + } 37 + 38 + function handleImport({ 39 + text, 40 + navigate, 41 + }: { 42 + text: string; 43 + navigate: UseNavigateResult<string>; 44 + }) { 45 + const trimmed = text.trim(); 46 + // parse text 47 + /** 48 + * text might be 49 + * 1. bsky dot app url (reddwarf link segments might be uri encoded,) 50 + * 2. aturi 51 + * 3. plain handle 52 + * 4. plain did 53 + */ 54 + 55 + // 1. Check if itโ€™s a URL 56 + try { 57 + const url = new URL(text); 58 + const knownHosts = [ 59 + "bsky.app", 60 + "social.daniela.lol", 61 + "deer.social", 62 + "reddwarf.whey.party", 63 + "reddwarf.app", 64 + "main.bsky.dev", 65 + "catsky.social", 66 + "blacksky.community", 67 + "red-dwarf-social-app.whey.party", 68 + "zeppelin.social", 69 + ]; 70 + if (knownHosts.includes(url.hostname)) { 71 + // parse path to get URI or handle 72 + const path = decodeURIComponent(url.pathname.slice(1)); // remove leading / 73 + console.log("BSky URL path:", path); 74 + navigate({ 75 + to: `/${path}`, 76 + }); 77 + return; 78 + } 79 + } catch { 80 + // not a URL, continue 81 + } 82 + 83 + // 2. Check if text looks like an at-uri 84 + try { 85 + if (text.startsWith("at://")) { 86 + console.log("AT URI detected:", text); 87 + const aturi = new AtUri(text); 88 + switch (aturi.collection) { 89 + case "app.bsky.feed.post": { 90 + navigate({ 91 + to: "/profile/$did/post/$rkey", 92 + params: { 93 + did: aturi.host, 94 + rkey: aturi.rkey, 95 + }, 96 + }); 97 + return; 98 + } 99 + case "app.bsky.actor.profile": { 100 + navigate({ 101 + to: "/profile/$did", 102 + params: { 103 + did: aturi.host, 104 + }, 105 + }); 106 + return; 107 + } 108 + // todo add more handlers as more routes are added. like feeds, lists, etc etc thanks! 109 + default: { 110 + // continue 111 + } 112 + } 113 + } 114 + } catch { 115 + // continue 116 + } 117 + 118 + // 3. Plain handle (starts with @) 119 + try { 120 + if (text.startsWith("@")) { 121 + const handle = text.slice(1); 122 + console.log("Handle detected:", handle); 123 + navigate({ to: "/profile/$did", params: { did: handle } }); 124 + return; 125 + } 126 + } catch { 127 + // continue 128 + } 129 + 130 + // 4. Plain DID (starts with did:) 131 + try { 132 + if (text.startsWith("did:")) { 133 + console.log("did detected:", text); 134 + navigate({ to: "/profile/$did", params: { did: text } }); 135 + return; 136 + } 137 + } catch { 138 + // continue 139 + } 140 + 141 + // if all else fails 142 + 143 + // try { 144 + // // probably a user? 145 + // navigate({ to: "/profile/$did", params: { did: text } }); 146 + // return; 147 + // } catch { 148 + // // continue 149 + // } 150 + }
+36 -9
src/components/InfiniteCustomFeed.tsx
··· 1 import * as React from "react"; 2 //import { useInView } from "react-intersection-observer"; 3 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 4 import { useAuth } from "~/providers/UnifiedAuthProvider"; 5 import { 6 - useQueryArbitrary, 7 - useQueryIdentity, 8 useInfiniteQueryFeedSkeleton, 9 } from "~/utils/useQuery"; 10 11 interface InfiniteCustomFeedProps { ··· 36 isFetchingNextPage, 37 refetch, 38 isRefetching, 39 } = useInfiniteQueryFeedSkeleton({ 40 feedUri: feedUri, 41 agent: agent ?? undefined, ··· 43 pdsUrl: pdsUrl, 44 feedServiceDid: feedServiceDid, 45 }); 46 47 const handleRefresh = () => { 48 refetch(); 49 }; 50 51 //const { ref, inView } = useInView(); 52 53 // React.useEffect(() => { ··· 66 ); 67 } 68 69 - const allPosts = 70 - data?.pages.flatMap((page) => { 71 - if (page) return page.feed; 72 - }) ?? []; 73 74 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 75 return ( ··· 112 <button 113 onClick={handleRefresh} 114 disabled={isRefetching} 115 - className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed" 116 aria-label="Refresh feed" 117 > 118 - {isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />} 119 </button> 120 </> 121 ); ··· 138 d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" 139 ></path> 140 </svg> 141 - );
··· 1 + import { useQueryClient } from "@tanstack/react-query"; 2 import * as React from "react"; 3 + 4 //import { useInView } from "react-intersection-observer"; 5 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 import { 8 useInfiniteQueryFeedSkeleton, 9 + // useQueryArbitrary, 10 + // useQueryIdentity, 11 } from "~/utils/useQuery"; 12 13 interface InfiniteCustomFeedProps { ··· 38 isFetchingNextPage, 39 refetch, 40 isRefetching, 41 + queryKey, 42 } = useInfiniteQueryFeedSkeleton({ 43 feedUri: feedUri, 44 agent: agent ?? undefined, ··· 46 pdsUrl: pdsUrl, 47 feedServiceDid: feedServiceDid, 48 }); 49 + const queryClient = useQueryClient(); 50 + 51 52 const handleRefresh = () => { 53 + queryClient.removeQueries({queryKey: queryKey}); 54 + //queryClient.invalidateQueries(["infinite-feed", feedUri] as const); 55 refetch(); 56 }; 57 58 + const allPosts = React.useMemo(() => { 59 + const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? []; 60 + 61 + const seenUris = new Set<string>(); 62 + 63 + return flattenedPosts.filter((item) => { 64 + if (!item?.post) return false; 65 + 66 + if (seenUris.has(item.post)) { 67 + return false; 68 + } 69 + 70 + seenUris.add(item.post); 71 + 72 + return true; 73 + }); 74 + }, [data]); 75 + 76 //const { ref, inView } = useInView(); 77 78 // React.useEffect(() => { ··· 91 ); 92 } 93 94 + // const allPosts = 95 + // data?.pages.flatMap((page) => { 96 + // if (page) return page.feed; 97 + // }) ?? []; 98 99 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 100 return ( ··· 137 <button 138 onClick={handleRefresh} 139 disabled={isRefetching} 140 + className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed" 141 aria-label="Refresh feed" 142 > 143 + <RefreshIcon 144 + className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} 145 + /> 146 </button> 147 </> 148 ); ··· 165 d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" 166 ></path> 167 </svg> 168 + );
+188 -58
src/components/Login.tsx
··· 1 // src/components/Login.tsx 2 - import React, { useEffect, useState, useRef } from "react"; 3 import { useAuth } from "~/providers/UnifiedAuthProvider"; 4 - import { Agent } from "@atproto/api"; 5 6 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 7 - export default function Login({ compact = false }: { compact?: boolean }) { 8 const { status, agent, logout } = useAuth(); 9 10 // Loading state can be styled differently based on the prop ··· 14 className={ 15 compact 16 ? "flex items-center justify-center p-1" 17 - : "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4 flex justify-center items-center h-[280px]" 18 } 19 > 20 <span ··· 33 // Large view 34 if (!compact) { 35 return ( 36 - <div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4"> 37 <div className="flex flex-col items-center justify-center text-center"> 38 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 39 You are logged in! ··· 41 <ProfileThing agent={agent} large /> 42 <button 43 onClick={logout} 44 - className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors" 45 > 46 Log out 47 </button> ··· 67 if (!compact) { 68 // Large view renders the form directly in the card 69 return ( 70 - <div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4"> 71 <UnifiedLoginForm /> 72 </div> 73 ); 74 } 75 76 // Compact view renders a button that toggles the form in a dropdown 77 - return <CompactLoginButton />; 78 } 79 80 // --- 2. The Reusable, Self-Contained Login Form Component --- ··· 83 84 return ( 85 <div> 86 - <div className="flex border-b border-gray-200 dark:border-gray-700 mb-4"> 87 <TabButton 88 label="OAuth" 89 active={mode === "oauth"} ··· 103 // --- 3. Helper components for layouts, forms, and UI --- 104 105 // A new component to contain the logic for the compact dropdown 106 - const CompactLoginButton = () => { 107 const [showForm, setShowForm] = useState(false); 108 const formRef = useRef<HTMLDivElement>(null); 109 ··· 125 <div className="relative" ref={formRef}> 126 <button 127 onClick={() => setShowForm(!showForm)} 128 - className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors" 129 > 130 Log in 131 </button> 132 {showForm && ( 133 - <div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50"> 134 <UnifiedLoginForm /> 135 </div> 136 )} ··· 138 ); 139 }; 140 141 - const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => ( 142 <button 143 onClick={onClick} 144 - className={`px-4 py-2 text-sm font-medium transition-colors ${ 145 active 146 - ? "text-gray-600 dark:text-gray-200 border-b-2 border-gray-500" 147 - : "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" 148 }`} 149 > 150 {label} ··· 169 }; 170 return ( 171 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 172 - <p className="text-xs text-gray-500 dark:text-gray-400">Sign in with AT. Your password is never shared.</p> 173 - <input type="text" placeholder="handle.bsky.social" value={handle} onChange={(e) => setHandle(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> 174 - <button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button> 175 </form> 176 ); 177 }; ··· 201 202 return ( 203 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 204 - <p className="text-xs text-red-500 dark:text-red-400">Warning: Less secure. Use an App Password.</p> 205 - <input type="text" placeholder="handle.bsky.social" value={user} onChange={(e) => setUser(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="username" /> 206 - <input type="password" placeholder="App Password" value={password} onChange={(e) => setPassword(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="current-password" /> 207 - <input type="text" placeholder="PDS (e.g., bsky.social)" value={serviceURL} onChange={(e) => setServiceURL(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> 208 {error && <p className="text-xs text-red-500">{error}</p>} 209 - <button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button> 210 </form> 211 ); 212 }; 213 214 // --- Profile Component (now supports a `large` prop for styling) --- 215 - export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => { 216 - const [profile, setProfile] = useState<any>(null); 217 218 - useEffect(() => { 219 - const fetchUser = async () => { 220 - const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid; 221 - if (!did) return; 222 - try { 223 - const res = await agent!.getProfile({ actor: did }); 224 - setProfile(res.data); 225 - } catch (e) { console.error("Failed to fetch profile", e); } 226 - }; 227 - if (agent) fetchUser(); 228 - }, [agent]); 229 230 - if (!profile) { 231 - return ( // Skeleton loader 232 - <div className={`flex items-center gap-2.5 animate-pulse ${large ? 'mb-1' : ''}`}> 233 - <div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} /> 234 - <div className="flex flex-col gap-2"> 235 - <div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-28' : 'h-3 w-20'}`} /> 236 - <div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-20' : 'h-3 w-16'}`} /> 237 - </div> 238 - </div> 239 - ); 240 - } 241 - 242 - return ( 243 - <div className={`flex flex-row items-center gap-2.5 ${large ? 'mb-1' : ''}`}> 244 - <img src={profile?.avatar} alt="avatar" className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} /> 245 - <div className="flex flex-col items-start text-left"> 246 - <div className={`font-medium ${large ? 'text-gray-800 dark:text-gray-100 text-md' : 'text-gray-800 dark:text-gray-100 text-sm'}`}>{profile?.displayName}</div> 247 - <div className={` ${large ? 'text-gray-500 dark:text-gray-400 text-sm' : 'text-gray-500 dark:text-gray-400 text-xs'}`}>@{profile?.handle}</div> 248 - </div> 249 </div> 250 - ); 251 - };
··· 1 // src/components/Login.tsx 2 + import AtpAgent, { Agent } from "@atproto/api"; 3 + import { useAtom } from "jotai"; 4 + import React, { useEffect, useRef, useState } from "react"; 5 + 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 + import { imgCDNAtom } from "~/utils/atoms"; 8 + import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; 9 10 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 11 + export default function Login({ 12 + compact = false, 13 + popup = false, 14 + }: { 15 + compact?: boolean; 16 + popup?: boolean; 17 + }) { 18 const { status, agent, logout } = useAuth(); 19 20 // Loading state can be styled differently based on the prop ··· 24 className={ 25 compact 26 ? "flex items-center justify-center p-1" 27 + : "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-4 mx-4 flex justify-center items-center h-[280px]" 28 } 29 > 30 <span ··· 43 // Large view 44 if (!compact) { 45 return ( 46 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 47 <div className="flex flex-col items-center justify-center text-center"> 48 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 49 You are logged in! ··· 51 <ProfileThing agent={agent} large /> 52 <button 53 onClick={logout} 54 + className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors" 55 > 56 Log out 57 </button> ··· 77 if (!compact) { 78 // Large view renders the form directly in the card 79 return ( 80 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 81 <UnifiedLoginForm /> 82 </div> 83 ); 84 } 85 86 // Compact view renders a button that toggles the form in a dropdown 87 + return <CompactLoginButton popup={popup} />; 88 } 89 90 // --- 2. The Reusable, Self-Contained Login Form Component --- ··· 93 94 return ( 95 <div> 96 + <div className="flex bg-gray-300 rounded-full dark:bg-gray-700 mb-4"> 97 <TabButton 98 label="OAuth" 99 active={mode === "oauth"} ··· 113 // --- 3. Helper components for layouts, forms, and UI --- 114 115 // A new component to contain the logic for the compact dropdown 116 + const CompactLoginButton = ({ popup }: { popup?: boolean }) => { 117 const [showForm, setShowForm] = useState(false); 118 const formRef = useRef<HTMLDivElement>(null); 119 ··· 135 <div className="relative" ref={formRef}> 136 <button 137 onClick={() => setShowForm(!showForm)} 138 + className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors" 139 > 140 Log in 141 </button> 142 {showForm && ( 143 + <div 144 + className={`absolute ${popup ? `bottom-[calc(100%)]` : `top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`} 145 + > 146 <UnifiedLoginForm /> 147 </div> 148 )} ··· 150 ); 151 }; 152 153 + const TabButton = ({ 154 + label, 155 + active, 156 + onClick, 157 + }: { 158 + label: string; 159 + active: boolean; 160 + onClick: () => void; 161 + }) => ( 162 <button 163 onClick={onClick} 164 + className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 165 active 166 + ? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 167 + : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 168 }`} 169 > 170 {label} ··· 189 }; 190 return ( 191 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 192 + <p className="text-xs text-gray-500 dark:text-gray-400"> 193 + Sign in with AT. Your password is never shared. 194 + </p> 195 + {/* <input 196 + type="text" 197 + placeholder="handle.bsky.social" 198 + value={handle} 199 + onChange={(e) => setHandle(e.target.value)} 200 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 201 + /> */} 202 + <div className="flex flex-col gap-3"> 203 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 204 + <input 205 + type="text" 206 + placeholder=" " 207 + value={handle} 208 + onChange={(e) => setHandle(e.target.value)} 209 + /> 210 + <label>AT Handle</label> 211 + </div> 212 + <button 213 + type="submit" 214 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 215 + > 216 + Log in 217 + </button> 218 + </div> 219 </form> 220 ); 221 }; ··· 245 246 return ( 247 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 248 + <p className="text-xs text-red-500 dark:text-red-400"> 249 + Warning: Less secure. Use an App Password. 250 + </p> 251 + {/* <input 252 + type="text" 253 + placeholder="handle.bsky.social" 254 + value={user} 255 + onChange={(e) => setUser(e.target.value)} 256 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 257 + autoComplete="username" 258 + /> 259 + <input 260 + type="password" 261 + placeholder="App Password" 262 + value={password} 263 + onChange={(e) => setPassword(e.target.value)} 264 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 265 + autoComplete="current-password" 266 + /> 267 + <input 268 + type="text" 269 + placeholder="PDS (e.g., bsky.social)" 270 + value={serviceURL} 271 + onChange={(e) => setServiceURL(e.target.value)} 272 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 273 + /> */} 274 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 275 + <input 276 + type="text" 277 + placeholder=" " 278 + value={user} 279 + onChange={(e) => setUser(e.target.value)} 280 + /> 281 + <label>AT Handle</label> 282 + </div> 283 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 284 + <input 285 + type="text" 286 + placeholder=" " 287 + value={password} 288 + onChange={(e) => setPassword(e.target.value)} 289 + /> 290 + <label>App Password</label> 291 + </div> 292 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 293 + <input 294 + type="text" 295 + placeholder=" " 296 + value={serviceURL} 297 + onChange={(e) => setServiceURL(e.target.value)} 298 + /> 299 + <label>PDS</label> 300 + </div> 301 {error && <p className="text-xs text-red-500">{error}</p>} 302 + <button 303 + type="submit" 304 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 305 + > 306 + Log in 307 + </button> 308 </form> 309 ); 310 }; 311 312 // --- Profile Component (now supports a `large` prop for styling) --- 313 + export const ProfileThing = ({ 314 + agent, 315 + large = false, 316 + }: { 317 + agent: Agent | null; 318 + large?: boolean; 319 + }) => { 320 + const did = ((agent as AtpAgent)?.session?.did ?? 321 + (agent as AtpAgent)?.assertDid ?? 322 + agent?.did) as string | undefined; 323 + const { data: identity } = useQueryIdentity(did); 324 + const { data: profiledata } = useQueryProfile( 325 + `at://${did}/app.bsky.actor.profile/self` 326 + ); 327 + const profile = profiledata?.value; 328 329 + const [imgcdn] = useAtom(imgCDNAtom) 330 + 331 + function getAvatarUrl(p: typeof profile) { 332 + const link = p?.avatar?.ref?.["$link"]; 333 + if (!link || !did) return null; 334 + return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; 335 + } 336 + 337 + if (!profiledata) { 338 + return ( 339 + // Skeleton loader 340 + <div 341 + className={`flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`} 342 + > 343 + <div 344 + className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 345 + /> 346 + <div className="flex flex-col gap-2"> 347 + <div 348 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`} 349 + /> 350 + <div 351 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`} 352 + /> 353 + </div> 354 + </div> 355 + ); 356 + } 357 358 + return ( 359 + <div 360 + className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 361 + > 362 + <img 363 + src={getAvatarUrl(profile) ?? undefined} 364 + alt="avatar" 365 + className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 366 + /> 367 + <div className="flex flex-col items-start text-left"> 368 + <div 369 + className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`} 370 + > 371 + {profile?.displayName} 372 + </div> 373 + <div 374 + className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 375 + > 376 + @{identity?.handle} 377 </div> 378 + </div> 379 + </div> 380 + ); 381 + };
+6
src/components/Star.tsx
···
··· 1 + import type { SVGProps } from 'react'; 2 + import React from 'react'; 3 + 4 + export function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) { 5 + return (<svg xmlns="http://www.w3.org/2000/svg" width={32} height={32} viewBox="0 0 32 32" {...props}><g fill="currentColor"><path d="m28.979 17.003l-3.108.214c-.834.06-1.178 1.079-.542 1.608l2.388 1.955c.521.428 1.314.204 1.523-.428l.709-2.127c.219-.632-.292-1.273-.97-1.222M21.75 2.691l-.72 2.9c-.2.78.66 1.41 1.34.98l2.54-1.58c.55-.34.58-1.14.05-1.52l-1.78-1.29a.912.912 0 0 0-1.43.51M6.43 4.995l2.53 1.58c.68.43 1.54-.19 1.35-.98l-.72-2.9a.92.92 0 0 0-1.43-.52l-1.78 1.29c-.53.4-.5 1.19.05 1.53M4.185 20.713l2.29-1.92c.62-.52.29-1.53-.51-1.58l-2.98-.21a.92.92 0 0 0-.94 1.2l.68 2.09c.2.62.97.84 1.46.42m13.61 7.292l-1.12-2.77c-.3-.75-1.36-.75-1.66 0l-1.12 2.77c-.24.6.2 1.26.85 1.26h2.2a.92.92 0 0 0 .85-1.26"></path><path d="m17.565 3.324l1.726 3.72c.326.694.967 1.18 1.717 1.29l4.056.624c1.835.278 2.575 2.53 1.293 3.859L23.268 16a2.28 2.28 0 0 0-.612 1.964l.71 4.374c.307 1.885-1.687 3.293-3.354 2.37l-3.405-1.894a2.25 2.25 0 0 0-2.21 0l-3.404 1.895c-1.668.922-3.661-.486-3.355-2.37l.71-4.375A2.28 2.28 0 0 0 7.736 16l-3.088-3.184c-1.293-1.34-.543-3.581 1.293-3.859l4.055-.625a2.3 2.3 0 0 0 1.717-1.29l1.727-3.719c.819-1.765 3.306-1.765 4.124 0"></path></g></svg>); 6 + }
+859 -534
src/components/UniversalPostRenderer.tsx
··· 1 import { useNavigate } from "@tanstack/react-router"; 2 - import { useAtom } from 'jotai'; 3 import * as React from "react"; 4 import { type SVGProps } from "react"; 5 6 - import { likedPostsAtom } from "~/utils/atoms"; 7 import { useHydratedEmbed } from "~/utils/useHydrated"; 8 import { 9 useQueryConstellation, 10 useQueryIdentity, 11 useQueryPost, 12 useQueryProfile, 13 } from "~/utils/useQuery"; 14 15 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { ··· 28 bottomBorder?: boolean; 29 feedviewpost?: boolean; 30 repostedby?: string; 31 } 32 33 // export async function cachedGetRecord({ ··· 132 bottomBorder = true, 133 feedviewpost = false, 134 repostedby, 135 }: UniversalPostRendererATURILoaderProps) { 136 // /*mass comment*/ console.log("atUri", atUri); 137 //const { get, set } = usePersistentStore(); 138 //const [record, setRecord] = React.useState<any>(null); ··· 146 // >(null); 147 //const router = useRouter(); 148 149 - const parsed = React.useMemo(() => parseAtUri(atUri), [atUri]); 150 - const did = parsed?.did; 151 const rkey = parsed?.rkey; 152 // /*mass comment*/ console.log("did", did); 153 // /*mass comment*/ console.log("rkey", rkey); ··· 376 ); 377 }, [links]); 378 379 // const navigateToProfile = (e: React.MouseEvent) => { 380 // e.stopPropagation(); 381 // if (resolved?.did) { ··· 387 // }; 388 if (!postQuery?.value) { 389 // deleted post more often than a non-resolvable post 390 - return (<></>) 391 } 392 393 return ( 394 - <UniversalPostRendererRawRecordShim 395 - detailed={detailed} 396 - postRecord={postQuery} 397 - profileRecord={opProfile} 398 - aturi={atUri} 399 - resolved={resolved} 400 - likesCount={likes} 401 - repostsCount={reposts} 402 - repliesCount={replies} 403 - bottomReplyLine={bottomReplyLine} 404 - topReplyLine={topReplyLine} 405 - bottomBorder={bottomBorder} 406 - feedviewpost={feedviewpost} 407 - repostedby={repostedby} 408 - /> 409 ); 410 } 411 412 export function UniversalPostRendererRawRecordShim({ ··· 423 bottomBorder = true, 424 feedviewpost = false, 425 repostedby, 426 }: { 427 postRecord: any; 428 profileRecord: any; ··· 437 bottomBorder?: boolean; 438 feedviewpost?: boolean; 439 repostedby?: string; 440 }) { 441 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 442 const navigate = useNavigate(); 443 444 //const { get, set } = usePersistentStore(); 445 - function getAvatarUrl(opProfile: any) { 446 - const link = opProfile?.value?.avatar?.ref?.["$link"]; 447 - if (!link) return null; 448 - return `https://cdn.bsky.app/img/avatar/plain/${resolved?.did}/${link}@jpeg`; 449 - } 450 - 451 // const [hydratedEmbed, setHydratedEmbed] = useState<any>(undefined); 452 453 // useEffect(() => { ··· 519 error: embedError, 520 } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); 521 522 - const parsedaturi = parseAtUri(aturi); 523 524 const fakepost = React.useMemo<AppBskyFeedDefs.PostView>( 525 () => ({ 526 $type: "app.bsky.feed.defs#postView", 527 uri: aturi, 528 cid: postRecord?.cid || "", 529 - author: { 530 - did: resolved?.did || "", 531 - handle: resolved?.handle || "", 532 - displayName: profileRecord?.value?.displayName || "", 533 - avatar: getAvatarUrl(profileRecord) || "", 534 - viewer: undefined, 535 - labels: profileRecord?.labels || undefined, 536 - verification: undefined, 537 - }, 538 record: postRecord?.value || {}, 539 embed: hydratedEmbed ?? undefined, 540 replyCount: repliesCount ?? 0, ··· 548 }), 549 [ 550 aturi, 551 - postRecord, 552 - profileRecord, 553 hydratedEmbed, 554 repliesCount, 555 repostsCount, 556 likesCount, 557 - resolved, 558 ] 559 ); 560 ··· 595 ); 596 const feedviewpostreplyhandle = replyhookvalue?.data?.handle; 597 598 - 599 - const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined 600 const repostedbyhookvalue = useQueryIdentity( 601 repostedby ? aturirepostbydid : undefined 602 ); ··· 612 parsedaturi && 613 navigate({ 614 to: "/profile/$did/post/$rkey", 615 - params: { did: parsedaturi.did, rkey: parsedaturi.rkey }, 616 }) 617 } 618 // onProfileClick={() => parsedaturi && navigate({to: "/profile/$did", ··· 623 if (parsedaturi) { 624 navigate({ 625 to: "/profile/$did", 626 - params: { did: parsedaturi.did }, 627 }); 628 } 629 }} 630 post={fakepost} 631 salt={aturi} 632 bottomReplyLine={bottomReplyLine} 633 topReplyLine={topReplyLine} ··· 635 //extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}} 636 feedviewpostreplyhandle={feedviewpostreplyhandle} 637 repostedby={feedviewpostrepostedbyhandle} 638 /> 639 </> 640 ); 641 } 642 643 - export function parseAtUri( 644 - atUri: string 645 - ): { did: string; collection: string; rkey: string } | null { 646 - const PREFIX = "at://"; 647 - if (!atUri.startsWith(PREFIX)) { 648 - return null; 649 - } 650 651 - const parts = atUri.slice(PREFIX.length).split("/"); 652 653 - if (parts.length !== 3) { 654 - return null; 655 - } 656 657 - const [did, collection, rkey] = parts; 658 659 - if (!did || !collection || !rkey) { 660 - return null; 661 - } 662 663 - return { did, collection, rkey }; 664 - } 665 666 export function MdiCommentOutline(props: SVGProps<SVGSVGElement>) { 667 return ( ··· 673 {...props} 674 > 675 <path 676 - fill="oklch(0.704 0.05 28)" 677 d="M9 22a1 1 0 0 1-1-1v-3H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6.1l-3.7 3.71c-.2.19-.45.29-.7.29zm1-6v3.08L13.08 16H20V4H4v12z" 678 ></path> 679 </svg> ··· 690 {...props} 691 > 692 <path 693 - fill="oklch(0.704 0.05 28)" 694 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 695 ></path> 696 </svg> ··· 741 {...props} 742 > 743 <path 744 - fill="oklch(0.704 0.05 28)" 745 d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3" 746 ></path> 747 </svg> ··· 758 {...props} 759 > 760 <path 761 - fill="oklch(0.704 0.05 28)" 762 d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3a3 3 0 0 0-3-3a3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08" 763 ></path> 764 </svg> ··· 775 {...props} 776 > 777 <path 778 - fill="oklch(0.704 0.05 28)" 779 d="M16 12a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2" 780 ></path> 781 </svg> ··· 792 {...props} 793 > 794 <path 795 - fill="oklch(0.704 0.05 28)" 796 d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2" 797 ></path> 798 </svg> ··· 826 {...props} 827 > 828 <path 829 - fill="oklch(0.704 0.05 28)" 830 d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11" 831 ></path> 832 </svg> ··· 880 {...props} 881 > 882 <path 883 - fill="oklch(0.704 0.05 28)" 884 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 885 ></path> 886 </svg> ··· 897 {...props} 898 > 899 <path 900 - fill="oklch(0.704 0.05 28)" 901 d="M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z" 902 ></path> 903 </svg> ··· 925 //import Masonry from "@mui/lab/Masonry"; 926 import { 927 type $Typed, 928 AppBskyEmbedDefs, 929 AppBskyEmbedExternal, 930 AppBskyEmbedImages, ··· 948 PostView, 949 //ThreadViewPost, 950 } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 951 import { useEffect, useRef, useState } from "react"; 952 import ReactPlayer from "react-player"; 953 954 import defaultpfp from "~/../public/favicon.png"; 955 import { useAuth } from "~/providers/UnifiedAuthProvider"; 956 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 957 // import type { 958 // ViewRecord, ··· 1060 1061 function UniversalPostRenderer({ 1062 post, 1063 //setMainItem, 1064 //isMainItem, 1065 onPostClick, ··· 1076 feedviewpostreplyhandle, 1077 depth = 0, 1078 repostedby, 1079 }: { 1080 post: PostView; 1081 // optional for now because i havent ported every use to this yet 1082 // setMainItem?: React.Dispatch< 1083 // React.SetStateAction<AppBskyFeedDefs.FeedViewPost> ··· 1095 feedviewpostreplyhandle?: string; 1096 depth?: number; 1097 repostedby?: string; 1098 }) { 1099 const navigate = useNavigate(); 1100 const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom); 1101 const [hasRetweeted, setHasRetweeted] = useState<boolean>( 1102 post.viewer?.repost ? true : false 1103 ); 1104 const [hasLiked, setHasLiked] = useState<boolean>( 1105 - (post.uri in likedPosts) || post.viewer?.like ? true : false 1106 ); 1107 const { agent } = useAuth(); 1108 const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like); 1109 const [retweetUri, setRetweetUri] = useState<string | undefined>( ··· 1132 setHasLiked(true); 1133 newLikedPosts[post.uri] = uri; 1134 } 1135 - setLikedPosts(newLikedPosts) 1136 }; 1137 1138 const repostOrUnrepostPost = async () => { ··· 1152 } 1153 }; 1154 1155 - const isRepost = repostedby ? repostedby : extraOptionalItemInfo 1156 - ? AppBskyFeedDefs.isReasonRepost(extraOptionalItemInfo.reason) 1157 - ? extraOptionalItemInfo.reason?.by.displayName 1158 - : undefined 1159 - : undefined; 1160 const isReply = extraOptionalItemInfo 1161 ? extraOptionalItemInfo.reply 1162 : undefined; 1163 1164 const emergencySalt = randomString(); 1165 1166 /* fuck you */ 1167 const isMainItem = false; 1168 const setMainItem = (any: any) => {}; 1169 return ( 1170 - <div 1171 - key={salt + "-" + (post.uri || emergencySalt)} 1172 - onClick={ 1173 - isMainItem 1174 - ? onPostClick 1175 - : setMainItem 1176 - ? onPostClick 1177 - ? (e) => { 1178 - setMainItem({ post: post }); 1179 - onPostClick(e); 1180 - } 1181 - : () => { 1182 - setMainItem({ post: post }); 1183 - } 1184 - : undefined 1185 - } 1186 - style={{ 1187 - //border: "1px solid #e1e8ed", 1188 - //borderRadius: 12, 1189 - opacity: "1 !important", 1190 - background: "transparent", 1191 - paddingLeft: isQuote ? 12 : 16, 1192 - paddingRight: isQuote ? 12 : 16, 1193 - //paddingTop: 16, 1194 - paddingTop: isRepost ? 10 : isQuote ? 12 : 16, 1195 - //paddingBottom: bottomReplyLine ? 0 : 16, 1196 - paddingBottom: 0, 1197 - fontFamily: "system-ui, sans-serif", 1198 - //boxShadow: "0 2px 8px rgba(0,0,0,0.04)", 1199 - position: "relative", 1200 - // dont cursor: "pointer", 1201 - borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0, 1202 - }} 1203 - className="border-gray-300 dark:border-gray-600" 1204 - > 1205 - {isRepost && ( 1206 - <div 1207 - style={{ 1208 - marginLeft: 36, 1209 - display: "flex", 1210 - borderRadius: 12, 1211 - paddingBottom: "calc(22px - 1rem)", 1212 - fontSize: 14, 1213 - maxHeight: "1rem", 1214 - justifyContent: "flex-start", 1215 - //color: theme.textSecondary, 1216 - gap: 4, 1217 - alignItems: "center", 1218 - }} 1219 - className="text-gray-500 dark:text-gray-400" 1220 - > 1221 - <MdiRepost /> Reposted by @{isRepost}{" "} 1222 - </div> 1223 - )} 1224 - {!isQuote && ( 1225 - <div 1226 - style={{ 1227 - opacity: topReplyLine || (isReply /*&& (true || expanded)*/) ? 0.5 : 0, 1228 - position: "absolute", 1229 - top: 0, 1230 - left: 36, // why 36 ??? 1231 - //left: 16 + (42 / 2), 1232 - width: 2, 1233 - //height: "100%", 1234 - height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6, 1235 - // background: theme.textSecondary, 1236 - //opacity: 0.5, 1237 - // no flex here 1238 - }} 1239 - className="bg-gray-500 dark:bg-gray-400" 1240 - /> 1241 - )} 1242 <div 1243 style={{ 1244 - position: "absolute", 1245 - //top: isRepost ? "calc(16px + 1rem)" : 16, 1246 - //left: 16, 1247 - zIndex: 1, 1248 - top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16, 1249 - left: isQuote ? 12 : 16, 1250 }} 1251 - onClick={onProfileClick} 1252 > 1253 - <img 1254 - src={post.author.avatar || defaultpfp} 1255 - alt="avatar" 1256 - // transition={{ 1257 - // type: "spring", 1258 - // stiffness: 260, 1259 - // damping: 20, 1260 - // }} 1261 - style={{ 1262 - borderRadius: "50%", 1263 - marginRight: 12, 1264 - objectFit: "cover", 1265 - //background: theme.border, 1266 - //border: `1px solid ${theme.border}`, 1267 - width: isQuote ? 16 : 42, 1268 - height: isQuote ? 16 : 42, 1269 - }} 1270 - className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600" 1271 - /> 1272 - </div> 1273 - <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1274 - <div 1275 - style={{ 1276 - display: "flex", 1277 - flexDirection: "column", 1278 - alignSelf: "stretch", 1279 - alignItems: "center", 1280 - overflow: "hidden", 1281 - width: expanded || isQuote ? 0 : "auto", 1282 - marginRight: expanded || isQuote ? 0 : 12, 1283 - }} 1284 - > 1285 - {/* dummy for later use */} 1286 - <div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} /> 1287 - {/* reply line !!!! bottomReplyLine */} 1288 - {bottomReplyLine && ( 1289 <div 1290 style={{ 1291 - width: 2, 1292 - height: "100%", 1293 - //background: theme.textSecondary, 1294 - opacity: 0.5, 1295 - // no flex here 1296 - //color: "Red", 1297 - //zIndex: 99 1298 }} 1299 - className="bg-gray-500 dark:bg-gray-400" 1300 - /> 1301 - )} 1302 - {/* <div 1303 layout 1304 transition={{ duration: 0.2 }} 1305 animate={{ height: expanded ? 0 : '100%' }} ··· 1309 // no flex here 1310 }} 1311 /> */} 1312 - </div> 1313 - <div style={{ flex: 1, maxWidth: "100%" }}> 1314 - <div 1315 - style={{ 1316 - display: "flex", 1317 - flexDirection: "row", 1318 - alignItems: "center", 1319 - flexWrap: "nowrap", 1320 - maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1321 - width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1322 - marginLeft: !expanded ? (isQuote ? 26 : 0) : 54, 1323 - marginBottom: !expanded ? 4 : 6, 1324 - }} 1325 - > 1326 <div 1327 style={{ 1328 display: "flex", 1329 - //overflow: "hidden", // hey why is overflow hidden unapplied 1330 - overflow: "hidden", 1331 - textOverflow: "ellipsis", 1332 - flexShrink: 1, 1333 - flexGrow: 1, 1334 - flexBasis: 0, 1335 - width: 0, 1336 - gap: expanded ? 0 : 6, 1337 - alignItems: expanded ? "flex-start" : "center", 1338 - flexDirection: expanded ? "column" : "row", 1339 - height: expanded ? 42 : "1rem", 1340 }} 1341 > 1342 - <span 1343 style={{ 1344 display: "flex", 1345 - fontWeight: 700, 1346 - fontSize: 16, 1347 overflow: "hidden", 1348 textOverflow: "ellipsis", 1349 - whiteSpace: "nowrap", 1350 flexShrink: 1, 1351 - minWidth: 0, 1352 - gap: 4, 1353 - alignItems: "center", 1354 - //color: theme.text, 1355 }} 1356 - className="text-gray-900 dark:text-gray-100" 1357 > 1358 - {/* verified checkmark */} 1359 - {post.author.displayName || post.author.handle}{" "} 1360 - {post.author.verification?.verifiedStatus == "valid" && ( 1361 - <MdiVerified /> 1362 - )} 1363 - </span> 1364 1365 - <span 1366 style={{ 1367 - //color: theme.textSecondary, 1368 - fontSize: 16, 1369 - overflowX: "hidden", 1370 - textOverflow: "ellipsis", 1371 - whiteSpace: "nowrap", 1372 - flexShrink: 1, 1373 - flexGrow: 0, 1374 - minWidth: 0, 1375 }} 1376 - className="text-gray-500 dark:text-gray-400" 1377 > 1378 - @{post.author.handle} 1379 - </span> 1380 </div> 1381 - <div 1382 - style={{ 1383 - display: "flex", 1384 - alignItems: "center", 1385 - height: "1rem", 1386 - }} 1387 - > 1388 - <span 1389 style={{ 1390 //color: theme.textSecondary, 1391 - fontSize: 16, 1392 - marginLeft: 8, 1393 - whiteSpace: "nowrap", 1394 - flexShrink: 0, 1395 - maxWidth: "100%", 1396 }} 1397 className="text-gray-500 dark:text-gray-400" 1398 > 1399 - ยท {/* time placeholder */} 1400 - {shortTimeAgo(post.indexedAt)} 1401 - </span> 1402 - </div> 1403 - </div> 1404 - {/* reply indicator */} 1405 - {!!feedviewpostreplyhandle && ( 1406 <div 1407 style={{ 1408 - display: "flex", 1409 - borderRadius: 12, 1410 - paddingBottom: 2, 1411 - fontSize: 14, 1412 - justifyContent: "flex-start", 1413 - //color: theme.textSecondary, 1414 - gap: 4, 1415 - alignItems: "center", 1416 - //marginLeft: 36, 1417 - height: 1418 - !(expanded || isQuote) && !!feedviewpostreplyhandle 1419 - ? "1rem" 1420 - : 0, 1421 - opacity: 1422 - !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0, 1423 }} 1424 - className="text-gray-500 dark:text-gray-400" 1425 > 1426 - <MdiReply /> Reply to @{feedviewpostreplyhandle} 1427 </div> 1428 - )} 1429 - <div 1430 - style={{ 1431 - fontSize: 16, 1432 - marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8, 1433 - whiteSpace: "pre-wrap", 1434 - textAlign: "left", 1435 - overflowWrap: "anywhere", 1436 - wordBreak: "break-word", 1437 - //color: theme.text, 1438 - }} 1439 - className="text-gray-900 dark:text-gray-100" 1440 - > 1441 - {renderTextWithFacets({ 1442 - text: (post.record as { text?: string }).text ?? "", 1443 - facets: (post.record.facets as Facet[]) ?? [], 1444 - navigate: navigate 1445 - })} 1446 - {} 1447 - </div> 1448 - {post.embed && depth < 1 ? ( 1449 - <PostEmbeds 1450 - embed={post.embed} 1451 - //moderation={moderation} 1452 - viewContext={PostEmbedViewContext.Feed} 1453 - salt={salt} 1454 - navigate={navigate} 1455 - /> 1456 - ) : null} 1457 - {post.embed && depth > 0 && ( 1458 - <> 1459 - <div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]"> 1460 - (there is an embed here thats too deep to render) 1461 - </div> 1462 - </> 1463 - )} 1464 - <div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}> 1465 - <> 1466 - {expanded && ( 1467 <div 1468 style={{ 1469 - overflow: "hidden", 1470 - //color: theme.textSecondary, 1471 - fontSize: 14, 1472 display: "flex", 1473 - borderBottomStyle: "solid", 1474 - //borderBottomColor: theme.border, 1475 - //background: "#f00", 1476 - // height: "1rem", 1477 - paddingTop: 4, 1478 - paddingBottom: 8, 1479 - borderBottomWidth: 1, 1480 - marginBottom: 8, 1481 - }} // important for height animation 1482 - className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700" 1483 - > 1484 - {fullDateTimeFormat(post.indexedAt)} 1485 - </div> 1486 - )} 1487 - </> 1488 - {!isQuote && ( 1489 - <div 1490 - style={{ 1491 - display: "flex", 1492 - gap: 32, 1493 - paddingTop: 8, 1494 - //color: theme.textSecondary, 1495 - fontSize: 15, 1496 - justifyContent: "space-between", 1497 - //background: "#0f0", 1498 - }} 1499 - className="text-gray-500 dark:text-gray-400" 1500 - > 1501 - <span style={btnstyle}> 1502 - <MdiCommentOutline /> 1503 - {post.replyCount} 1504 - </span> 1505 - <HitSlopButton 1506 - onClick={() => { 1507 - repostOrUnrepostPost(); 1508 - }} 1509 - style={{ 1510 - ...btnstyle, 1511 - ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1512 }} 1513 > 1514 - {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1515 - {(post.repostCount || 0) + (hasRetweeted ? 1 : 0)} 1516 - </HitSlopButton> 1517 - <HitSlopButton 1518 - onClick={() => { 1519 - likeOrUnlikePost(); 1520 - }} 1521 - style={{ 1522 - ...btnstyle, 1523 - ...(hasLiked ? { color: "#EC4899" } : {}), 1524 - }} 1525 - > 1526 - {hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />} 1527 - {(post.likeCount || 0) + (hasLiked ? 1 : 0)} 1528 - </HitSlopButton> 1529 - <div style={{ display: "flex", gap: 8 }}> 1530 <HitSlopButton 1531 - onClick={async (e) => { 1532 - e.stopPropagation(); 1533 - try { 1534 - await navigator.clipboard.writeText( 1535 - "https://bsky.app" + 1536 - "/profile/" + 1537 - post.author.handle + 1538 - "/post/" + 1539 - post.uri.split("/").pop() 1540 - ); 1541 - } catch (_e) { 1542 - // idk 1543 - } 1544 }} 1545 style={{ 1546 ...btnstyle, 1547 }} 1548 > 1549 - <MdiShareVariant /> 1550 </HitSlopButton> 1551 - <span style={btnstyle}> 1552 - <MdiMoreHoriz /> 1553 - </span> 1554 </div> 1555 - </div> 1556 - )} 1557 </div> 1558 - <div 1559 - style={{ 1560 - //height: bottomReplyLine ? 16 : 0 1561 - height: isQuote ? 12 : 16, 1562 - }} 1563 - /> 1564 </div> 1565 </div> 1566 </div> ··· 1652 viewContext, 1653 salt, 1654 navigate, 1655 }: { 1656 embed?: Embed; 1657 moderation?: ModerationDecision; ··· 1660 viewContext?: PostEmbedViewContext; 1661 salt: string; 1662 navigate: (_: any) => void; 1663 }) { 1664 - const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1665 if ( 1666 AppBskyEmbedRecordWithMedia.isView(embed) && 1667 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 1695 viewContext={viewContext} 1696 salt={salt} 1697 navigate={navigate} 1698 /> 1699 {/* padding empty div of 8px height */} 1700 <div style={{ height: 12 }} /> ··· 1708 //boxShadow: theme.cardShadow, 1709 overflow: "hidden", 1710 }} 1711 - className="shadow border border-gray-200 dark:border-gray-700" 1712 > 1713 <UniversalPostRenderer 1714 post={post} ··· 1716 salt={salt} 1717 onPostClick={(e) => { 1718 e.stopPropagation(); 1719 - const parsed = parseAtUri(post.uri); 1720 if (parsed) { 1721 navigate({ 1722 to: "/profile/$did/post/$rkey", 1723 - params: { did: parsed.did, rkey: parsed.rkey }, 1724 }); 1725 } 1726 }} ··· 1825 //boxShadow: theme.cardShadow, 1826 overflow: "hidden", 1827 }} 1828 - className="shadow border border-gray-200 dark:border-gray-700" 1829 > 1830 <UniversalPostRenderer 1831 post={post} ··· 1833 salt={salt} 1834 onPostClick={(e) => { 1835 e.stopPropagation(); 1836 - const parsed = parseAtUri(post.uri); 1837 if (parsed) { 1838 navigate({ 1839 to: "/profile/$did/post/$rkey", 1840 - params: { did: parsed.did, rkey: parsed.rkey }, 1841 }); 1842 } 1843 }} ··· 1869 src: img.fullsize, 1870 alt: img.alt, 1871 })); 1872 1873 if (images.length > 0) { 1874 // const items = embed.images.map(img => ({ ··· 1898 //border: `1px solid ${theme.border}`, 1899 overflow: "hidden", 1900 }} 1901 - className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900" 1902 > 1903 - {lightboxIndex !== null && ( 1904 <Lightbox 1905 images={lightboxImages} 1906 index={lightboxIndex} 1907 onClose={() => setLightboxIndex(null)} 1908 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 1909 /> 1910 - )} 1911 <img 1912 src={image.fullsize} 1913 alt={image.alt} ··· 1938 overflow: "hidden", 1939 //border: `1px solid ${theme.border}`, 1940 }} 1941 - className="border border-gray-200 dark:border-gray-700" 1942 > 1943 - {lightboxIndex !== null && ( 1944 <Lightbox 1945 images={lightboxImages} 1946 index={lightboxIndex} 1947 onClose={() => setLightboxIndex(null)} 1948 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 1949 /> 1950 - )} 1951 {images.map((img, i) => ( 1952 <div 1953 key={i} ··· 1987 //border: `1px solid ${theme.border}`, 1988 // height: 240, // fixed height for cropping 1989 }} 1990 - className="border border-gray-200 dark:border-gray-700" 1991 > 1992 - {lightboxIndex !== null && ( 1993 <Lightbox 1994 images={lightboxImages} 1995 index={lightboxIndex} 1996 onClose={() => setLightboxIndex(null)} 1997 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 1998 /> 1999 - )} 2000 {/* Left: 1:1 */} 2001 <div 2002 style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} ··· 2071 //border: `1px solid ${theme.border}`, 2072 //aspectRatio: "3 / 2", // overall grid aspect 2073 }} 2074 - className="border border-gray-200 dark:border-gray-700" 2075 > 2076 - {lightboxIndex !== null && ( 2077 <Lightbox 2078 images={lightboxImages} 2079 index={lightboxIndex} 2080 onClose={() => setLightboxIndex(null)} 2081 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2082 /> 2083 - )} 2084 {images.map((img, i) => ( 2085 <div 2086 key={i} ··· 2171 return <div />; 2172 } 2173 2174 - import { createPortal } from "react-dom"; 2175 - type LightboxProps = { 2176 - images: { src: string; alt?: string }[]; 2177 - index: number; 2178 - onClose: () => void; 2179 - onNavigate?: (newIndex: number) => void; 2180 - }; 2181 - export function Lightbox({ 2182 - images, 2183 - index, 2184 - onClose, 2185 - onNavigate, 2186 - }: LightboxProps) { 2187 - const image = images[index]; 2188 - 2189 - useEffect(() => { 2190 - function handleKey(e: KeyboardEvent) { 2191 - if (e.key === "Escape") onClose(); 2192 - if (e.key === "ArrowRight" && onNavigate) 2193 - onNavigate((index + 1) % images.length); 2194 - if (e.key === "ArrowLeft" && onNavigate) 2195 - onNavigate((index - 1 + images.length) % images.length); 2196 - } 2197 - window.addEventListener("keydown", handleKey); 2198 - return () => window.removeEventListener("keydown", handleKey); 2199 - }, [index, images.length, onClose, onNavigate]); 2200 - 2201 - return createPortal( 2202 - <div 2203 - className="fixed inset-0 z-50 flex items-center justify-center bg-black/80" 2204 - onClick={(e) => { 2205 - e.stopPropagation(); 2206 - onClose(); 2207 - }} 2208 - > 2209 - <img 2210 - src={image.src} 2211 - alt={image.alt} 2212 - className="max-h-[90vh] max-w-[90vw] object-contain rounded-lg shadow-lg" 2213 - onClick={(e) => e.stopPropagation()} 2214 - /> 2215 - 2216 - {images.length > 1 && ( 2217 - <> 2218 - <button 2219 - onClick={(e) => { 2220 - e.stopPropagation(); 2221 - onNavigate?.((index - 1 + images.length) % images.length); 2222 - }} 2223 - className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 2224 - > 2225 - <svg 2226 - xmlns="http://www.w3.org/2000/svg" 2227 - width={28} 2228 - height={28} 2229 - viewBox="0 0 24 24" 2230 - > 2231 - <g fill="none" fillRule="evenodd"> 2232 - <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 2233 - <path 2234 - fill="currentColor" 2235 - d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z" 2236 - ></path> 2237 - </g> 2238 - </svg> 2239 - </button> 2240 - <button 2241 - onClick={(e) => { 2242 - e.stopPropagation(); 2243 - onNavigate?.((index + 1) % images.length); 2244 - }} 2245 - className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 2246 - > 2247 - <svg 2248 - xmlns="http://www.w3.org/2000/svg" 2249 - width={28} 2250 - height={28} 2251 - viewBox="0 0 24 24" 2252 - > 2253 - <g fill="none" fillRule="evenodd"> 2254 - <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 2255 - <path 2256 - fill="currentColor" 2257 - d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z" 2258 - ></path> 2259 - </g> 2260 - </svg> 2261 - </button> 2262 - </> 2263 - )} 2264 - </div>, 2265 - document.body 2266 - ); 2267 - } 2268 - 2269 function getDomain(url: string) { 2270 try { 2271 const { hostname } = new URL(url); ··· 2296 for (let i = 0; i < bytes.length; i++) { 2297 map[byteIndex++] = charIndex; 2298 } 2299 - charIndex+=char.length; 2300 } 2301 2302 return map; ··· 2330 return { start, end, feature: f.features[0] }; 2331 }); 2332 } 2333 - function renderTextWithFacets({ 2334 text, 2335 facets, 2336 navigate, ··· 2389 navigate({ 2390 to: "/profile/$did", 2391 // @ts-expect-error i didnt bother with the correct types here sorry. bsky api types are cursed 2392 - params: { did: feature.did}, 2393 }); 2394 }} 2395 > ··· 2493 > 2494 <div 2495 style={containerStyle as React.CSSProperties} 2496 - className="border border-gray-200 dark:border-gray-700" 2497 > 2498 {thumb && ( 2499 <div ··· 2507 marginBottom: 8, 2508 //borderBottom: `1px solid ${theme.border}`, 2509 }} 2510 - className="border-b border-gray-200 dark:border-gray-700" 2511 > 2512 <img 2513 src={thumb} ··· 2633 borderRadius: 12, 2634 //border: `1px solid ${theme.border}`, 2635 }} 2636 - className="border border-gray-200 dark:border-gray-700" 2637 onClick={async (e) => { 2638 e.stopPropagation(); 2639 setPlaying(true); ··· 2674 100 / (aspect ? aspect.width / aspect.height : 16 / 9) 2675 }%`, // 16:9 = 56.25%, 4:3 = 75% 2676 }} 2677 - className="border border-gray-200 dark:border-gray-700" 2678 > 2679 <ReactPlayer 2680 src={url}
··· 1 import { useNavigate } from "@tanstack/react-router"; 2 + import DOMPurify from "dompurify"; 3 + import { useAtom } from "jotai"; 4 + import { DropdownMenu } from "radix-ui"; 5 + import { HoverCard } from "radix-ui"; 6 import * as React from "react"; 7 import { type SVGProps } from "react"; 8 9 + import { 10 + composerAtom, 11 + constellationURLAtom, 12 + imgCDNAtom, 13 + likedPostsAtom, 14 + } from "~/utils/atoms"; 15 import { useHydratedEmbed } from "~/utils/useHydrated"; 16 import { 17 useQueryConstellation, 18 useQueryIdentity, 19 useQueryPost, 20 useQueryProfile, 21 + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 22 } from "~/utils/useQuery"; 23 24 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { ··· 37 bottomBorder?: boolean; 38 feedviewpost?: boolean; 39 repostedby?: string; 40 + style?: React.CSSProperties; 41 + ref?: React.Ref<HTMLDivElement>; 42 + dataIndexPropPass?: number; 43 + nopics?: boolean; 44 + lightboxCallback?: (d: LightboxProps) => void; 45 + maxReplies?: number; 46 + isQuote?: boolean; 47 } 48 49 // export async function cachedGetRecord({ ··· 148 bottomBorder = true, 149 feedviewpost = false, 150 repostedby, 151 + style, 152 + ref, 153 + dataIndexPropPass, 154 + nopics, 155 + lightboxCallback, 156 + maxReplies, 157 + isQuote, 158 }: UniversalPostRendererATURILoaderProps) { 159 + // todo remove this once tree rendering is implemented, use a prop like isTree 160 + const TEMPLINEAR = true; 161 // /*mass comment*/ console.log("atUri", atUri); 162 //const { get, set } = usePersistentStore(); 163 //const [record, setRecord] = React.useState<any>(null); ··· 171 // >(null); 172 //const router = useRouter(); 173 174 + //const parsed = React.useMemo(() => parseAtUri(atUri), [atUri]); 175 + const parsed = new AtUri(atUri); 176 + const did = parsed?.host; 177 const rkey = parsed?.rkey; 178 // /*mass comment*/ console.log("did", did); 179 // /*mass comment*/ console.log("rkey", rkey); ··· 402 ); 403 }, [links]); 404 405 + // const { data: repliesData } = useQueryConstellation({ 406 + // method: "/links", 407 + // target: atUri, 408 + // collection: "app.bsky.feed.post", 409 + // path: ".reply.parent.uri", 410 + // }); 411 + 412 + const [constellationurl] = useAtom(constellationURLAtom); 413 + 414 + const infinitequeryresults = useInfiniteQuery({ 415 + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 416 + { 417 + constellation: constellationurl, 418 + method: "/links", 419 + target: atUri, 420 + collection: "app.bsky.feed.post", 421 + path: ".reply.parent.uri", 422 + } 423 + ), 424 + enabled: !!atUri && !!maxReplies && !isQuote, 425 + }); 426 + 427 + const { 428 + data: repliesData, 429 + // fetchNextPage, 430 + // hasNextPage, 431 + // isFetchingNextPage, 432 + } = infinitequeryresults; 433 + 434 + // auto-fetch all pages 435 + useEffect(() => { 436 + if (!maxReplies || isQuote || TEMPLINEAR) return; 437 + if ( 438 + infinitequeryresults.hasNextPage && 439 + !infinitequeryresults.isFetchingNextPage 440 + ) { 441 + console.log("Fetching the next page..."); 442 + infinitequeryresults.fetchNextPage(); 443 + } 444 + }, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]); 445 + 446 + const replyAturis = repliesData 447 + ? repliesData.pages.flatMap((page) => 448 + page 449 + ? page.linking_records.map((record) => { 450 + const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 451 + return aturi; 452 + }) 453 + : [] 454 + ) 455 + : []; 456 + 457 + //const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined); 458 + 459 + const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => { 460 + if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies) 461 + return { 462 + oldestOpsReply: undefined, 463 + oldestOpsReplyElseNewestNonOpsReply: undefined, 464 + }; 465 + 466 + const opdid = new AtUri( 467 + //postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri 468 + atUri 469 + ).host; 470 + 471 + const opReplies = replyAturis.filter( 472 + (aturi) => new AtUri(aturi).host === opdid 473 + ); 474 + 475 + if (opReplies.length > 0) { 476 + const opreply = opReplies[opReplies.length - 1]; 477 + //setOldestOpsReply(opreply); 478 + return { 479 + oldestOpsReply: opreply, 480 + oldestOpsReplyElseNewestNonOpsReply: opreply, 481 + }; 482 + } else { 483 + return { 484 + oldestOpsReply: undefined, 485 + oldestOpsReplyElseNewestNonOpsReply: replyAturis[0], 486 + }; 487 + } 488 + })(); 489 + 490 // const navigateToProfile = (e: React.MouseEvent) => { 491 // e.stopPropagation(); 492 // if (resolved?.did) { ··· 498 // }; 499 if (!postQuery?.value) { 500 // deleted post more often than a non-resolvable post 501 + return <></>; 502 } 503 504 return ( 505 + <> 506 + {/* <span>uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"}</span> */} 507 + <UniversalPostRendererRawRecordShim 508 + detailed={detailed} 509 + postRecord={postQuery} 510 + profileRecord={opProfile} 511 + aturi={atUri} 512 + resolved={resolved} 513 + likesCount={likes} 514 + repostsCount={reposts} 515 + repliesCount={replies} 516 + bottomReplyLine={ 517 + maxReplies && oldestOpsReplyElseNewestNonOpsReply 518 + ? true 519 + : maxReplies && !oldestOpsReplyElseNewestNonOpsReply 520 + ? false 521 + : (maxReplies === 0 && (!replies || (!!replies && replies === 0))) ? false : bottomReplyLine 522 + } 523 + topReplyLine={topReplyLine} 524 + //bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder} 525 + bottomBorder={ 526 + maxReplies && oldestOpsReplyElseNewestNonOpsReply 527 + ? false 528 + : maxReplies === 0 529 + ? false 530 + : bottomBorder 531 + } 532 + feedviewpost={feedviewpost} 533 + repostedby={repostedby} 534 + //style={{...style, background: oldestOpsReply === atUri ? "Red" : undefined}} 535 + style={style} 536 + ref={ref} 537 + dataIndexPropPass={dataIndexPropPass} 538 + nopics={nopics} 539 + lightboxCallback={lightboxCallback} 540 + maxReplies={maxReplies} 541 + isQuote={isQuote} 542 + /> 543 + <> 544 + {(maxReplies && maxReplies === 0 && replies && replies > 0) ? ( 545 + <> 546 + {/* <div>hello</div> */} 547 + <MoreReplies atUri={atUri} /> 548 + </> 549 + ) : (<></>)} 550 + </> 551 + {!isQuote && oldestOpsReplyElseNewestNonOpsReply && ( 552 + <> 553 + {/* <span>hello {maxReplies}</span> */} 554 + <UniversalPostRendererATURILoader 555 + //detailed={detailed} 556 + atUri={oldestOpsReplyElseNewestNonOpsReply} 557 + bottomReplyLine={(maxReplies ?? 0) > 0} 558 + topReplyLine={ 559 + (!!(maxReplies && maxReplies - 1 === 0) && 560 + !!(replies && replies > 0)) || 561 + !!((maxReplies ?? 0) > 1) 562 + } 563 + bottomBorder={bottomBorder} 564 + feedviewpost={feedviewpost} 565 + repostedby={repostedby} 566 + style={style} 567 + ref={ref} 568 + dataIndexPropPass={dataIndexPropPass} 569 + nopics={nopics} 570 + lightboxCallback={lightboxCallback} 571 + maxReplies={ 572 + maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined 573 + } 574 + /> 575 + </> 576 + )} 577 + </> 578 + ); 579 + } 580 + 581 + function MoreReplies({ atUri }: { atUri: string }) { 582 + const navigate = useNavigate(); 583 + const aturio = new AtUri(atUri); 584 + return ( 585 + <div 586 + onClick={() => 587 + navigate({ 588 + to: "/profile/$did/post/$rkey", 589 + params: { did: aturio.host, rkey: aturio.rkey }, 590 + }) 591 + } 592 + className="border-b border-gray-300 dark:border-gray-800 flex flex-row px-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors" 593 + > 594 + <div className="w-[42px] h-12 flex flex-col items-center justify-center"> 595 + <div 596 + style={{ 597 + width: 2, 598 + height: "100%", 599 + backgroundImage: 600 + "repeating-linear-gradient(to bottom, var(--color-gray-500) 0, var(--color-gray-500) 4px, transparent 4px, transparent 8px)", 601 + opacity: 0.5, 602 + }} 603 + className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]" 604 + //className="border-gray-400 dark:border-gray-500" 605 + /> 606 + </div> 607 + 608 + <div className="flex items-center pl-3 text-sm text-gray-500 dark:text-gray-400 select-none"> 609 + More Replies 610 + </div> 611 + </div> 612 ); 613 + } 614 + 615 + function getAvatarUrl(opProfile: any, did: string, cdn: string) { 616 + const link = opProfile?.value?.avatar?.ref?.["$link"]; 617 + if (!link) return null; 618 + return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`; 619 } 620 621 export function UniversalPostRendererRawRecordShim({ ··· 632 bottomBorder = true, 633 feedviewpost = false, 634 repostedby, 635 + style, 636 + ref, 637 + dataIndexPropPass, 638 + nopics, 639 + lightboxCallback, 640 + maxReplies, 641 + isQuote, 642 }: { 643 postRecord: any; 644 profileRecord: any; ··· 653 bottomBorder?: boolean; 654 feedviewpost?: boolean; 655 repostedby?: string; 656 + style?: React.CSSProperties; 657 + ref?: React.Ref<HTMLDivElement>; 658 + dataIndexPropPass?: number; 659 + nopics?: boolean; 660 + lightboxCallback?: (d: LightboxProps) => void; 661 + maxReplies?: number; 662 + isQuote?: boolean; 663 }) { 664 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 665 const navigate = useNavigate(); 666 667 //const { get, set } = usePersistentStore(); 668 // const [hydratedEmbed, setHydratedEmbed] = useState<any>(undefined); 669 670 // useEffect(() => { ··· 736 error: embedError, 737 } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); 738 739 + const [imgcdn] = useAtom(imgCDNAtom); 740 + 741 + const parsedaturi = new AtUri(aturi); //parseAtUri(aturi); 742 + 743 + const fakeprofileviewbasic = React.useMemo<AppBskyActorDefs.ProfileViewBasic>( 744 + () => ({ 745 + did: resolved?.did || "", 746 + handle: resolved?.handle || "", 747 + displayName: profileRecord?.value?.displayName || "", 748 + avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "", 749 + viewer: undefined, 750 + labels: profileRecord?.labels || undefined, 751 + verification: undefined, 752 + }), 753 + [imgcdn, profileRecord, resolved?.did, resolved?.handle] 754 + ); 755 + 756 + const fakeprofileviewdetailed = 757 + React.useMemo<AppBskyActorDefs.ProfileViewDetailed>( 758 + () => ({ 759 + ...fakeprofileviewbasic, 760 + $type: "app.bsky.actor.defs#profileViewDetailed", 761 + description: profileRecord?.value?.description || undefined, 762 + }), 763 + [fakeprofileviewbasic, profileRecord?.value?.description] 764 + ); 765 766 const fakepost = React.useMemo<AppBskyFeedDefs.PostView>( 767 () => ({ 768 $type: "app.bsky.feed.defs#postView", 769 uri: aturi, 770 cid: postRecord?.cid || "", 771 + author: fakeprofileviewbasic, 772 record: postRecord?.value || {}, 773 embed: hydratedEmbed ?? undefined, 774 replyCount: repliesCount ?? 0, ··· 782 }), 783 [ 784 aturi, 785 + postRecord?.cid, 786 + postRecord?.value, 787 + postRecord?.labels, 788 + fakeprofileviewbasic, 789 hydratedEmbed, 790 repliesCount, 791 repostsCount, 792 likesCount, 793 ] 794 ); 795 ··· 830 ); 831 const feedviewpostreplyhandle = replyhookvalue?.data?.handle; 832 833 + const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined; 834 const repostedbyhookvalue = useQueryIdentity( 835 repostedby ? aturirepostbydid : undefined 836 ); ··· 846 parsedaturi && 847 navigate({ 848 to: "/profile/$did/post/$rkey", 849 + params: { did: parsedaturi.host, rkey: parsedaturi.rkey }, 850 }) 851 } 852 // onProfileClick={() => parsedaturi && navigate({to: "/profile/$did", ··· 857 if (parsedaturi) { 858 navigate({ 859 to: "/profile/$did", 860 + params: { did: parsedaturi.host }, 861 }); 862 } 863 }} 864 post={fakepost} 865 + uprrrsauthor={fakeprofileviewdetailed} 866 salt={aturi} 867 bottomReplyLine={bottomReplyLine} 868 topReplyLine={topReplyLine} ··· 870 //extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}} 871 feedviewpostreplyhandle={feedviewpostreplyhandle} 872 repostedby={feedviewpostrepostedbyhandle} 873 + style={style} 874 + ref={ref} 875 + dataIndexPropPass={dataIndexPropPass} 876 + nopics={nopics} 877 + lightboxCallback={lightboxCallback} 878 + maxReplies={maxReplies} 879 + isQuote={isQuote} 880 /> 881 </> 882 ); 883 } 884 885 + // export function parseAtUri( 886 + // atUri: string 887 + // ): { did: string; collection: string; rkey: string } | null { 888 + // const PREFIX = "at://"; 889 + // if (!atUri.startsWith(PREFIX)) { 890 + // return null; 891 + // } 892 893 + // const parts = atUri.slice(PREFIX.length).split("/"); 894 895 + // if (parts.length !== 3) { 896 + // return null; 897 + // } 898 899 + // const [did, collection, rkey] = parts; 900 901 + // if (!did || !collection || !rkey) { 902 + // return null; 903 + // } 904 905 + // return { did, collection, rkey }; 906 + // } 907 908 export function MdiCommentOutline(props: SVGProps<SVGSVGElement>) { 909 return ( ··· 915 {...props} 916 > 917 <path 918 + fill="var(--color-gray-400)" 919 d="M9 22a1 1 0 0 1-1-1v-3H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6.1l-3.7 3.71c-.2.19-.45.29-.7.29zm1-6v3.08L13.08 16H20V4H4v12z" 920 ></path> 921 </svg> ··· 932 {...props} 933 > 934 <path 935 + fill="var(--color-gray-400)" 936 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 937 ></path> 938 </svg> ··· 983 {...props} 984 > 985 <path 986 + fill="var(--color-gray-400)" 987 d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3" 988 ></path> 989 </svg> ··· 1000 {...props} 1001 > 1002 <path 1003 + fill="var(--color-gray-400)" 1004 d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3a3 3 0 0 0-3-3a3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08" 1005 ></path> 1006 </svg> ··· 1017 {...props} 1018 > 1019 <path 1020 + fill="var(--color-gray-400)" 1021 d="M16 12a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2" 1022 ></path> 1023 </svg> ··· 1034 {...props} 1035 > 1036 <path 1037 + fill="var(--color-gray-400)" 1038 d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2" 1039 ></path> 1040 </svg> ··· 1068 {...props} 1069 > 1070 <path 1071 + fill="var(--color-gray-400)" 1072 d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11" 1073 ></path> 1074 </svg> ··· 1122 {...props} 1123 > 1124 <path 1125 + fill="var(--color-gray-400)" 1126 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 1127 ></path> 1128 </svg> ··· 1139 {...props} 1140 > 1141 <path 1142 + fill="var(--color-gray-400)" 1143 d="M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z" 1144 ></path> 1145 </svg> ··· 1167 //import Masonry from "@mui/lab/Masonry"; 1168 import { 1169 type $Typed, 1170 + AppBskyActorDefs, 1171 AppBskyEmbedDefs, 1172 AppBskyEmbedExternal, 1173 AppBskyEmbedImages, ··· 1191 PostView, 1192 //ThreadViewPost, 1193 } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 1194 + import { useInfiniteQuery } from "@tanstack/react-query"; 1195 import { useEffect, useRef, useState } from "react"; 1196 import ReactPlayer from "react-player"; 1197 1198 import defaultpfp from "~/../public/favicon.png"; 1199 import { useAuth } from "~/providers/UnifiedAuthProvider"; 1200 + import { FollowButton, Mutual } from "~/routes/profile.$did"; 1201 + import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 1202 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 1203 // import type { 1204 // ViewRecord, ··· 1306 1307 function UniversalPostRenderer({ 1308 post, 1309 + uprrrsauthor, 1310 //setMainItem, 1311 //isMainItem, 1312 onPostClick, ··· 1323 feedviewpostreplyhandle, 1324 depth = 0, 1325 repostedby, 1326 + style, 1327 + ref, 1328 + dataIndexPropPass, 1329 + nopics, 1330 + lightboxCallback, 1331 + maxReplies, 1332 }: { 1333 post: PostView; 1334 + uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; 1335 // optional for now because i havent ported every use to this yet 1336 // setMainItem?: React.Dispatch< 1337 // React.SetStateAction<AppBskyFeedDefs.FeedViewPost> ··· 1349 feedviewpostreplyhandle?: string; 1350 depth?: number; 1351 repostedby?: string; 1352 + style?: React.CSSProperties; 1353 + ref?: React.Ref<HTMLDivElement>; 1354 + dataIndexPropPass?: number; 1355 + nopics?: boolean; 1356 + lightboxCallback?: (d: LightboxProps) => void; 1357 + maxReplies?: number; 1358 }) { 1359 + const parsed = new AtUri(post.uri); 1360 const navigate = useNavigate(); 1361 const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom); 1362 const [hasRetweeted, setHasRetweeted] = useState<boolean>( 1363 post.viewer?.repost ? true : false 1364 ); 1365 const [hasLiked, setHasLiked] = useState<boolean>( 1366 + post.uri in likedPosts || post.viewer?.like ? true : false 1367 ); 1368 + const [, setComposerPost] = useAtom(composerAtom); 1369 const { agent } = useAuth(); 1370 const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like); 1371 const [retweetUri, setRetweetUri] = useState<string | undefined>( ··· 1394 setHasLiked(true); 1395 newLikedPosts[post.uri] = uri; 1396 } 1397 + setLikedPosts(newLikedPosts); 1398 }; 1399 1400 const repostOrUnrepostPost = async () => { ··· 1414 } 1415 }; 1416 1417 + const isRepost = repostedby 1418 + ? repostedby 1419 + : extraOptionalItemInfo 1420 + ? AppBskyFeedDefs.isReasonRepost(extraOptionalItemInfo.reason) 1421 + ? extraOptionalItemInfo.reason?.by.displayName 1422 + : undefined 1423 + : undefined; 1424 const isReply = extraOptionalItemInfo 1425 ? extraOptionalItemInfo.reply 1426 : undefined; 1427 1428 const emergencySalt = randomString(); 1429 + const fedi = (post.record as { bridgyOriginalText?: string }) 1430 + .bridgyOriginalText; 1431 1432 /* fuck you */ 1433 const isMainItem = false; 1434 const setMainItem = (any: any) => {}; 1435 + // eslint-disable-next-line react-hooks/refs 1436 + console.log("Received ref in UniversalPostRenderer:", ref); 1437 return ( 1438 + <div ref={ref} style={style} data-index={dataIndexPropPass}> 1439 <div 1440 + //ref={ref} 1441 + key={salt + "-" + (post.uri || emergencySalt)} 1442 + onClick={ 1443 + isMainItem 1444 + ? onPostClick 1445 + : setMainItem 1446 + ? onPostClick 1447 + ? (e) => { 1448 + setMainItem({ post: post }); 1449 + onPostClick(e); 1450 + } 1451 + : () => { 1452 + setMainItem({ post: post }); 1453 + } 1454 + : undefined 1455 + } 1456 style={{ 1457 + //...style, 1458 + //border: "1px solid #e1e8ed", 1459 + //borderRadius: 12, 1460 + opacity: "1 !important", 1461 + background: "transparent", 1462 + paddingLeft: isQuote ? 12 : 16, 1463 + paddingRight: isQuote ? 12 : 16, 1464 + //paddingTop: 16, 1465 + paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16, 1466 + //paddingBottom: bottomReplyLine ? 0 : 16, 1467 + paddingBottom: 0, 1468 + fontFamily: "system-ui, sans-serif", 1469 + //boxShadow: "0 2px 8px rgba(0,0,0,0.04)", 1470 + position: "relative", 1471 + // dont cursor: "pointer", 1472 + borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0, 1473 }} 1474 + className="border-gray-300 dark:border-gray-800" 1475 > 1476 + {isRepost && ( 1477 + <div 1478 + style={{ 1479 + marginLeft: 36, 1480 + display: "flex", 1481 + borderRadius: 12, 1482 + paddingBottom: "calc(22px - 1rem)", 1483 + fontSize: 14, 1484 + maxHeight: "1rem", 1485 + justifyContent: "flex-start", 1486 + //color: theme.textSecondary, 1487 + gap: 4, 1488 + alignItems: "center", 1489 + }} 1490 + className="text-gray-500 dark:text-gray-400" 1491 + > 1492 + <MdiRepost /> Reposted by @{isRepost}{" "} 1493 + </div> 1494 + )} 1495 + {!isQuote && ( 1496 + <div 1497 + style={{ 1498 + opacity: 1499 + topReplyLine || isReply /*&& (true || expanded)*/ ? 0.5 : 0, 1500 + position: "absolute", 1501 + top: 0, 1502 + left: 36, // why 36 ??? 1503 + //left: 16 + (42 / 2), 1504 + width: 2, 1505 + //height: "100%", 1506 + height: isRepost 1507 + ? "calc(16px + 1rem - 6px)" 1508 + : topReplyLine 1509 + ? 8 - 6 1510 + : 16 - 6, 1511 + // background: theme.textSecondary, 1512 + //opacity: 0.5, 1513 + // no flex here 1514 + }} 1515 + className="bg-gray-500 dark:bg-gray-400" 1516 + /> 1517 + )} 1518 + <HoverCard.Root> 1519 + <HoverCard.Trigger asChild> 1520 <div 1521 + className={`absolute`} 1522 style={{ 1523 + top: isRepost 1524 + ? "calc(16px + 1rem)" 1525 + : isQuote 1526 + ? 12 1527 + : topReplyLine 1528 + ? 8 1529 + : 16, 1530 + left: isQuote ? 12 : 16, 1531 }} 1532 + onClick={onProfileClick} 1533 + > 1534 + <img 1535 + src={post.author.avatar || defaultpfp} 1536 + alt="avatar" 1537 + className={`rounded-full object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600`} 1538 + style={{ 1539 + width: isQuote ? 16 : 42, 1540 + height: isQuote ? 16 : 42, 1541 + }} 1542 + /> 1543 + </div> 1544 + </HoverCard.Trigger> 1545 + <HoverCard.Portal> 1546 + <HoverCard.Content 1547 + className="rounded-md p-4 w-72 bg-gray-50 dark:bg-gray-900 shadow-lg border border-gray-300 dark:border-gray-800 animate-slide-fade z-50" 1548 + side={"bottom"} 1549 + sideOffset={5} 1550 + onClick={onProfileClick} 1551 + > 1552 + <div className="flex flex-col gap-2"> 1553 + <div className="flex flex-row"> 1554 + <img 1555 + src={post.author.avatar || defaultpfp} 1556 + alt="avatar" 1557 + className="rounded-full w-[58px] h-[58px] object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1558 + /> 1559 + <div className=" flex-1 flex flex-row align-middle justify-end"> 1560 + <FollowButton targetdidorhandle={post.author.did} /> 1561 + </div> 1562 + </div> 1563 + <div className="flex flex-col gap-3"> 1564 + <div> 1565 + <div className="text-gray-900 dark:text-gray-100 font-medium text-md"> 1566 + {post.author.displayName || post.author.handle}{" "} 1567 + </div> 1568 + <div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1"> 1569 + <Mutual targetdidorhandle={post.author.did} />@{post.author.handle}{" "} 1570 + </div> 1571 + </div> 1572 + {uprrrsauthor?.description && ( 1573 + <div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3"> 1574 + {uprrrsauthor.description} 1575 + </div> 1576 + )} 1577 + {/* <div className="flex gap-4"> 1578 + <div className="flex gap-1"> 1579 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1580 + 0 1581 + </div> 1582 + <div className="text-gray-500 dark:text-gray-400"> 1583 + Following 1584 + </div> 1585 + </div> 1586 + <div className="flex gap-1"> 1587 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1588 + 2,900 1589 + </div> 1590 + <div className="text-gray-500 dark:text-gray-400"> 1591 + Followers 1592 + </div> 1593 + </div> 1594 + </div> */} 1595 + </div> 1596 + </div> 1597 + 1598 + {/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */} 1599 + </HoverCard.Content> 1600 + </HoverCard.Portal> 1601 + </HoverCard.Root> 1602 + 1603 + <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1604 + <div 1605 + style={{ 1606 + display: "flex", 1607 + flexDirection: "column", 1608 + alignSelf: "stretch", 1609 + alignItems: "center", 1610 + overflow: "hidden", 1611 + width: expanded || isQuote ? 0 : "auto", 1612 + marginRight: expanded || isQuote ? 0 : 12, 1613 + }} 1614 + > 1615 + {/* dummy for later use */} 1616 + <div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} /> 1617 + {/* reply line !!!! bottomReplyLine */} 1618 + {bottomReplyLine && ( 1619 + <div 1620 + style={{ 1621 + width: 2, 1622 + height: "100%", 1623 + //background: theme.textSecondary, 1624 + opacity: 0.5, 1625 + // no flex here 1626 + //color: "Red", 1627 + //zIndex: 99 1628 + }} 1629 + className="bg-gray-500 dark:bg-gray-400" 1630 + /> 1631 + )} 1632 + {/* <div 1633 layout 1634 transition={{ duration: 0.2 }} 1635 animate={{ height: expanded ? 0 : '100%' }} ··· 1639 // no flex here 1640 }} 1641 /> */} 1642 + </div> 1643 + <div style={{ flex: 1, maxWidth: "100%" }}> 1644 <div 1645 style={{ 1646 display: "flex", 1647 + flexDirection: "row", 1648 + alignItems: "center", 1649 + flexWrap: "nowrap", 1650 + maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1651 + width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1652 + marginLeft: !expanded ? (isQuote ? 26 : 0) : 54, 1653 + marginBottom: !expanded ? 4 : 6, 1654 }} 1655 > 1656 + <div 1657 style={{ 1658 display: "flex", 1659 + //overflow: "hidden", // hey why is overflow hidden unapplied 1660 overflow: "hidden", 1661 textOverflow: "ellipsis", 1662 flexShrink: 1, 1663 + flexGrow: 1, 1664 + flexBasis: 0, 1665 + width: 0, 1666 + gap: expanded ? 0 : 6, 1667 + alignItems: expanded ? "flex-start" : "center", 1668 + flexDirection: expanded ? "column" : "row", 1669 + height: expanded ? 42 : "1rem", 1670 }} 1671 > 1672 + <span 1673 + style={{ 1674 + display: "flex", 1675 + fontWeight: 700, 1676 + fontSize: 16, 1677 + overflow: "hidden", 1678 + textOverflow: "ellipsis", 1679 + whiteSpace: "nowrap", 1680 + flexShrink: 1, 1681 + minWidth: 0, 1682 + gap: 4, 1683 + alignItems: "center", 1684 + //color: theme.text, 1685 + }} 1686 + className="text-gray-900 dark:text-gray-100" 1687 + > 1688 + {/* verified checkmark */} 1689 + {post.author.displayName || post.author.handle}{" "} 1690 + {post.author.verification?.verifiedStatus == "valid" && ( 1691 + <MdiVerified /> 1692 + )} 1693 + </span> 1694 1695 + <span 1696 + style={{ 1697 + //color: theme.textSecondary, 1698 + fontSize: 16, 1699 + overflowX: "hidden", 1700 + textOverflow: "ellipsis", 1701 + whiteSpace: "nowrap", 1702 + flexShrink: 1, 1703 + flexGrow: 0, 1704 + minWidth: 0, 1705 + }} 1706 + className="text-gray-500 dark:text-gray-400" 1707 + > 1708 + @{post.author.handle} 1709 + </span> 1710 + </div> 1711 + <div 1712 style={{ 1713 + display: "flex", 1714 + alignItems: "center", 1715 + height: "1rem", 1716 }} 1717 > 1718 + <span 1719 + style={{ 1720 + //color: theme.textSecondary, 1721 + fontSize: 16, 1722 + marginLeft: 8, 1723 + whiteSpace: "nowrap", 1724 + flexShrink: 0, 1725 + maxWidth: "100%", 1726 + }} 1727 + className="text-gray-500 dark:text-gray-400" 1728 + > 1729 + ยท {/* time placeholder */} 1730 + {shortTimeAgo(post.indexedAt)} 1731 + </span> 1732 + </div> 1733 </div> 1734 + {/* reply indicator */} 1735 + {!!feedviewpostreplyhandle && ( 1736 + <div 1737 style={{ 1738 + display: "flex", 1739 + borderRadius: 12, 1740 + paddingBottom: 2, 1741 + fontSize: 14, 1742 + justifyContent: "flex-start", 1743 //color: theme.textSecondary, 1744 + gap: 4, 1745 + alignItems: "center", 1746 + //marginLeft: 36, 1747 + height: 1748 + !(expanded || isQuote) && !!feedviewpostreplyhandle 1749 + ? "1rem" 1750 + : 0, 1751 + opacity: 1752 + !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0, 1753 }} 1754 className="text-gray-500 dark:text-gray-400" 1755 > 1756 + <MdiReply /> Reply to @{feedviewpostreplyhandle} 1757 + </div> 1758 + )} 1759 <div 1760 style={{ 1761 + fontSize: 16, 1762 + marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8, 1763 + whiteSpace: "pre-wrap", 1764 + textAlign: "left", 1765 + overflowWrap: "anywhere", 1766 + wordBreak: "break-word", 1767 + //color: theme.text, 1768 }} 1769 + className="text-gray-900 dark:text-gray-100" 1770 > 1771 + {fedi ? ( 1772 + <> 1773 + <span 1774 + className="dangerousFediContent" 1775 + dangerouslySetInnerHTML={{ 1776 + __html: DOMPurify.sanitize(fedi), 1777 + }} 1778 + /> 1779 + </> 1780 + ) : ( 1781 + <> 1782 + {renderTextWithFacets({ 1783 + text: (post.record as { text?: string }).text ?? "", 1784 + facets: (post.record.facets as Facet[]) ?? [], 1785 + navigate: navigate, 1786 + })} 1787 + </> 1788 + )} 1789 </div> 1790 + {post.embed && depth < 1 ? ( 1791 + <PostEmbeds 1792 + embed={post.embed} 1793 + //moderation={moderation} 1794 + viewContext={PostEmbedViewContext.Feed} 1795 + salt={salt} 1796 + navigate={navigate} 1797 + postid={{ did: post.author.did, rkey: parsed.rkey }} 1798 + nopics={nopics} 1799 + lightboxCallback={lightboxCallback} 1800 + /> 1801 + ) : null} 1802 + {post.embed && depth > 0 && ( 1803 + /* pretty bad hack imo. its trying to sync up with how the embed shim doesnt 1804 + hydrate embeds this deep but the connection here is implicit 1805 + todo: idk make this a real part of the embed shim so its not implicit */ 1806 + <> 1807 + <div className="border-gray-300 dark:border-gray-800 p-3 rounded-xl border italic text-gray-400 text-[14px]"> 1808 + (there is an embed here thats too deep to render) 1809 + </div> 1810 + </> 1811 + )} 1812 + <div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}> 1813 + <> 1814 + {expanded && ( 1815 + <div 1816 + style={{ 1817 + overflow: "hidden", 1818 + //color: theme.textSecondary, 1819 + fontSize: 14, 1820 + display: "flex", 1821 + borderBottomStyle: "solid", 1822 + //borderBottomColor: theme.border, 1823 + //background: "#f00", 1824 + // height: "1rem", 1825 + paddingTop: 4, 1826 + paddingBottom: 8, 1827 + borderBottomWidth: 1, 1828 + marginBottom: 8, 1829 + }} // important for height animation 1830 + className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7" 1831 + > 1832 + {fullDateTimeFormat(post.indexedAt)} 1833 + </div> 1834 + )} 1835 + </> 1836 + {!isQuote && ( 1837 <div 1838 style={{ 1839 display: "flex", 1840 + gap: 32, 1841 + paddingTop: 8, 1842 + //color: theme.textSecondary, 1843 + fontSize: 15, 1844 + justifyContent: "space-between", 1845 + //background: "#0f0", 1846 }} 1847 + className="text-gray-500 dark:text-gray-400" 1848 > 1849 <HitSlopButton 1850 + onClick={() => { 1851 + setComposerPost({ kind: "reply", parent: post.uri }); 1852 }} 1853 style={{ 1854 ...btnstyle, 1855 }} 1856 > 1857 + <MdiCommentOutline /> 1858 + {post.replyCount} 1859 </HitSlopButton> 1860 + <DropdownMenu.Root modal={false}> 1861 + <DropdownMenu.Trigger asChild> 1862 + <div 1863 + style={{ 1864 + ...btnstyle, 1865 + ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1866 + }} 1867 + aria-label="Repost or quote post" 1868 + > 1869 + {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1870 + {post.repostCount ?? 0} 1871 + </div> 1872 + </DropdownMenu.Trigger> 1873 + 1874 + <DropdownMenu.Portal> 1875 + <DropdownMenu.Content 1876 + align="start" 1877 + sideOffset={5} 1878 + className="bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-32 z-50 overflow-hidden" 1879 + > 1880 + <DropdownMenu.Item 1881 + onSelect={repostOrUnrepostPost} 1882 + className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700" 1883 + > 1884 + <MdiRepeat 1885 + className={hasRetweeted ? "text-green-400" : ""} 1886 + /> 1887 + <span>{hasRetweeted ? "Undo Repost" : "Repost"}</span> 1888 + </DropdownMenu.Item> 1889 + 1890 + <DropdownMenu.Item 1891 + onSelect={() => { 1892 + setComposerPost({ 1893 + kind: "quote", 1894 + subject: post.uri, 1895 + }); 1896 + }} 1897 + className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700" 1898 + > 1899 + {/* You might want a specific quote icon here */} 1900 + <MdiCommentOutline /> 1901 + <span>Quote</span> 1902 + </DropdownMenu.Item> 1903 + </DropdownMenu.Content> 1904 + </DropdownMenu.Portal> 1905 + </DropdownMenu.Root> 1906 + <HitSlopButton 1907 + onClick={() => { 1908 + likeOrUnlikePost(); 1909 + }} 1910 + style={{ 1911 + ...btnstyle, 1912 + ...(hasLiked ? { color: "#EC4899" } : {}), 1913 + }} 1914 + > 1915 + {hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />} 1916 + {(post.likeCount || 0) + (hasLiked ? 1 : 0)} 1917 + </HitSlopButton> 1918 + <div style={{ display: "flex", gap: 8 }}> 1919 + <HitSlopButton 1920 + onClick={async (e) => { 1921 + e.stopPropagation(); 1922 + try { 1923 + await navigator.clipboard.writeText( 1924 + "https://bsky.app" + 1925 + "/profile/" + 1926 + post.author.handle + 1927 + "/post/" + 1928 + post.uri.split("/").pop() 1929 + ); 1930 + } catch (_e) { 1931 + // idk 1932 + } 1933 + }} 1934 + style={{ 1935 + ...btnstyle, 1936 + }} 1937 + > 1938 + <MdiShareVariant /> 1939 + </HitSlopButton> 1940 + <span style={btnstyle}> 1941 + <MdiMoreHoriz /> 1942 + </span> 1943 + </div> 1944 </div> 1945 + )} 1946 + </div> 1947 + <div 1948 + style={{ 1949 + //height: bottomReplyLine ? 16 : 0 1950 + height: isQuote ? 12 : 16, 1951 + }} 1952 + /> 1953 </div> 1954 </div> 1955 </div> 1956 </div> ··· 2042 viewContext, 2043 salt, 2044 navigate, 2045 + postid, 2046 + nopics, 2047 + lightboxCallback, 2048 }: { 2049 embed?: Embed; 2050 moderation?: ModerationDecision; ··· 2053 viewContext?: PostEmbedViewContext; 2054 salt: string; 2055 navigate: (_: any) => void; 2056 + postid?: { did: string; rkey: string }; 2057 + nopics?: boolean; 2058 + lightboxCallback?: (d: LightboxProps) => void; 2059 }) { 2060 + //const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 2061 + function setLightboxIndex(number: number) { 2062 + navigate({ 2063 + to: "/profile/$did/post/$rkey/image/$i", 2064 + params: { 2065 + did: postid?.did, 2066 + rkey: postid?.rkey, 2067 + i: number.toString(), 2068 + }, 2069 + }); 2070 + } 2071 if ( 2072 AppBskyEmbedRecordWithMedia.isView(embed) && 2073 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 2101 viewContext={viewContext} 2102 salt={salt} 2103 navigate={navigate} 2104 + postid={postid} 2105 + nopics={nopics} 2106 + lightboxCallback={lightboxCallback} 2107 /> 2108 {/* padding empty div of 8px height */} 2109 <div style={{ height: 12 }} /> ··· 2117 //boxShadow: theme.cardShadow, 2118 overflow: "hidden", 2119 }} 2120 + className="shadow border border-gray-200 dark:border-gray-800 was7" 2121 > 2122 <UniversalPostRenderer 2123 post={post} ··· 2125 salt={salt} 2126 onPostClick={(e) => { 2127 e.stopPropagation(); 2128 + const parsed = new AtUri(post.uri); //parseAtUri(post.uri); 2129 if (parsed) { 2130 navigate({ 2131 to: "/profile/$did/post/$rkey", 2132 + params: { did: parsed.host, rkey: parsed.rkey }, 2133 }); 2134 } 2135 }} ··· 2234 //boxShadow: theme.cardShadow, 2235 overflow: "hidden", 2236 }} 2237 + className="shadow border border-gray-200 dark:border-gray-800 was7" 2238 > 2239 <UniversalPostRenderer 2240 post={post} ··· 2242 salt={salt} 2243 onPostClick={(e) => { 2244 e.stopPropagation(); 2245 + const parsed = new AtUri(post.uri); //parseAtUri(post.uri); 2246 if (parsed) { 2247 navigate({ 2248 to: "/profile/$did/post/$rkey", 2249 + params: { did: parsed.host, rkey: parsed.rkey }, 2250 }); 2251 } 2252 }} ··· 2278 src: img.fullsize, 2279 alt: img.alt, 2280 })); 2281 + console.log("rendering images"); 2282 + if (lightboxCallback) { 2283 + lightboxCallback({ images: lightboxImages }); 2284 + console.log("rendering images"); 2285 + } 2286 + 2287 + if (nopics) return; 2288 2289 if (images.length > 0) { 2290 // const items = embed.images.map(img => ({ ··· 2314 //border: `1px solid ${theme.border}`, 2315 overflow: "hidden", 2316 }} 2317 + className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900" 2318 > 2319 + {/* {lightboxIndex !== null && ( 2320 <Lightbox 2321 images={lightboxImages} 2322 index={lightboxIndex} 2323 onClose={() => setLightboxIndex(null)} 2324 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2325 + post={postid} 2326 /> 2327 + )} */} 2328 <img 2329 src={image.fullsize} 2330 alt={image.alt} ··· 2355 overflow: "hidden", 2356 //border: `1px solid ${theme.border}`, 2357 }} 2358 + className="border border-gray-200 dark:border-gray-800 was7" 2359 > 2360 + {/* {lightboxIndex !== null && ( 2361 <Lightbox 2362 images={lightboxImages} 2363 index={lightboxIndex} 2364 onClose={() => setLightboxIndex(null)} 2365 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2366 + post={postid} 2367 /> 2368 + )} */} 2369 {images.map((img, i) => ( 2370 <div 2371 key={i} ··· 2405 //border: `1px solid ${theme.border}`, 2406 // height: 240, // fixed height for cropping 2407 }} 2408 + className="border border-gray-200 dark:border-gray-800 was7" 2409 > 2410 + {/* {lightboxIndex !== null && ( 2411 <Lightbox 2412 images={lightboxImages} 2413 index={lightboxIndex} 2414 onClose={() => setLightboxIndex(null)} 2415 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2416 + post={postid} 2417 /> 2418 + )} */} 2419 {/* Left: 1:1 */} 2420 <div 2421 style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} ··· 2490 //border: `1px solid ${theme.border}`, 2491 //aspectRatio: "3 / 2", // overall grid aspect 2492 }} 2493 + className="border border-gray-200 dark:border-gray-800 was7" 2494 > 2495 + {/* {lightboxIndex !== null && ( 2496 <Lightbox 2497 images={lightboxImages} 2498 index={lightboxIndex} 2499 onClose={() => setLightboxIndex(null)} 2500 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2501 + post={postid} 2502 /> 2503 + )} */} 2504 {images.map((img, i) => ( 2505 <div 2506 key={i} ··· 2591 return <div />; 2592 } 2593 2594 function getDomain(url: string) { 2595 try { 2596 const { hostname } = new URL(url); ··· 2621 for (let i = 0; i < bytes.length; i++) { 2622 map[byteIndex++] = charIndex; 2623 } 2624 + charIndex += char.length; 2625 } 2626 2627 return map; ··· 2655 return { start, end, feature: f.features[0] }; 2656 }); 2657 } 2658 + export function renderTextWithFacets({ 2659 text, 2660 facets, 2661 navigate, ··· 2714 navigate({ 2715 to: "/profile/$did", 2716 // @ts-expect-error i didnt bother with the correct types here sorry. bsky api types are cursed 2717 + params: { did: feature.did }, 2718 }); 2719 }} 2720 > ··· 2818 > 2819 <div 2820 style={containerStyle as React.CSSProperties} 2821 + className="border border-gray-200 dark:border-gray-800 was7" 2822 > 2823 {thumb && ( 2824 <div ··· 2832 marginBottom: 8, 2833 //borderBottom: `1px solid ${theme.border}`, 2834 }} 2835 + className="border-b border-gray-200 dark:border-gray-800 was7" 2836 > 2837 <img 2838 src={thumb} ··· 2958 borderRadius: 12, 2959 //border: `1px solid ${theme.border}`, 2960 }} 2961 + className="border border-gray-200 dark:border-gray-800 was7" 2962 onClick={async (e) => { 2963 e.stopPropagation(); 2964 setPlaying(true); ··· 2999 100 / (aspect ? aspect.width / aspect.height : 16 / 9) 3000 }%`, // 16:9 = 56.25%, 4:3 = 75% 3001 }} 3002 + className="border border-gray-200 dark:border-gray-800 was7" 3003 > 3004 <ReactPlayer 3005 src={url}
+59 -14
src/main.tsx
··· 1 - import { StrictMode } from "react"; 2 import ReactDOM from "react-dom/client"; 3 - import { RouterProvider, createRouter } from "@tanstack/react-router"; 4 5 // Import the generated route tree 6 import { routeTree } from "./routeTree.gen"; 7 8 - import "~/styles/app.css"; 9 - import reportWebVitals from "./reportWebVitals.ts"; 10 - import { QueryClient, QueryClientProvider, } from "@tanstack/react-query"; 11 - import { 12 - persistQueryClient, 13 - } from "@tanstack/react-query-persist-client"; 14 - import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 15 - 16 17 const queryClient = new QueryClient({ 18 defaultOptions: { ··· 28 persistQueryClient({ 29 queryClient, 30 persister: localStoragePersister, 31 - }) 32 33 // Create a new router instance 34 const router = createRouter({ ··· 54 root.render( 55 // double queries annoys me 56 // <StrictMode> 57 - <QueryClientProvider client={queryClient}> 58 - <RouterProvider router={router} /> 59 - </QueryClientProvider> 60 // </StrictMode> 61 ); 62 } ··· 65 // to log results (for example: reportWebVitals(// /*mass comment*/ console.log)) 66 // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 67 reportWebVitals();
··· 1 + import "~/styles/app.css"; 2 + 3 + import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 4 + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 + import { persistQueryClient } from "@tanstack/react-query-persist-client"; 6 + import { createRouter, RouterProvider } from "@tanstack/react-router"; 7 + import { useSetAtom } from "jotai"; 8 + import { useEffect } from "react"; 9 + //import { StrictMode } from "react"; 10 import ReactDOM from "react-dom/client"; 11 12 + import reportWebVitals from "./reportWebVitals.ts"; 13 // Import the generated route tree 14 import { routeTree } from "./routeTree.gen"; 15 + import { isAtTopAtom } from "./utils/atoms.ts"; 16 17 + //initAtomToCssVar(hueAtom, "--tw-gray-hue") 18 19 const queryClient = new QueryClient({ 20 defaultOptions: { ··· 30 persistQueryClient({ 31 queryClient, 32 persister: localStoragePersister, 33 + }); 34 35 // Create a new router instance 36 const router = createRouter({ ··· 56 root.render( 57 // double queries annoys me 58 // <StrictMode> 59 + <QueryClientProvider client={queryClient}> 60 + <ScrollTopWatcher /> 61 + <RouterProvider router={router} /> 62 + </QueryClientProvider> 63 // </StrictMode> 64 ); 65 } ··· 68 // to log results (for example: reportWebVitals(// /*mass comment*/ console.log)) 69 // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 70 reportWebVitals(); 71 + 72 + export default function ScrollTopWatcher() { 73 + const setIsAtTop = useSetAtom(isAtTopAtom); 74 + useEffect(() => { 75 + const meta = document.querySelector('meta[name="theme-color"]'); 76 + let lastAtTop = window.scrollY === 0; 77 + let timeoutId: number | undefined; 78 + 79 + const setVars = (atTop: boolean) => { 80 + const root = document.documentElement; 81 + root.style.setProperty("--is-top", atTop ? "1" : "0"); 82 + 83 + const bg = getComputedStyle(root).getPropertyValue("--header-bg").trim(); 84 + if (meta && bg) meta.setAttribute("content", bg); 85 + setIsAtTop(atTop); 86 + }; 87 + 88 + const check = () => { 89 + const atTop = window.scrollY === 0; 90 + if (atTop !== lastAtTop) { 91 + lastAtTop = atTop; 92 + setVars(atTop); 93 + } 94 + }; 95 + 96 + const handleScroll = () => { 97 + if (timeoutId) clearTimeout(timeoutId); 98 + timeoutId = window.setTimeout(check, 2); 99 + }; 100 + 101 + // initialize 102 + setVars(lastAtTop); 103 + window.addEventListener("scroll", handleScroll, { passive: true }); 104 + 105 + return () => { 106 + window.removeEventListener("scroll", handleScroll); 107 + if (timeoutId) clearTimeout(timeoutId); 108 + }; 109 + }, []); 110 + 111 + return null; 112 + }
-149
src/providers/PassAuthProvider.tsx
··· 1 - import React, { createContext, useState, useEffect, useContext } from "react"; 2 - import { AtpAgent, type AtpSessionData } from "@atproto/api"; 3 - 4 - interface AuthContextValue { 5 - agent: AtpAgent | null; 6 - loginStatus: boolean; 7 - login: (user: string, password: string, service?: string) => Promise<void>; 8 - logout: () => Promise<void>; 9 - loading: boolean; 10 - authed: boolean | undefined; 11 - } 12 - 13 - const AuthContext = createContext<AuthContextValue>({} as AuthContextValue); 14 - 15 - export const AuthProvider = ({ children }: { children: React.ReactNode }) => { 16 - const [agent, setAgent] = useState<AtpAgent | null>(null); 17 - const [loginStatus, setLoginStatus] = useState(false); 18 - const [loading, setLoading] = useState(true); 19 - const [increment, setIncrement] = useState(0); 20 - const [authed, setAuthed] = useState<boolean | undefined>(undefined); 21 - 22 - useEffect(() => { 23 - const initialize = async () => { 24 - try { 25 - const service = localStorage.getItem("service"); 26 - // const user = await AsyncStorage.getItem('user'); 27 - // const password = await AsyncStorage.getItem('password'); 28 - const session = localStorage.getItem("sess"); 29 - 30 - if (service && session) { 31 - // /*mass comment*/ console.log("Auto-login service is:", service); 32 - const apiAgent = new AtpAgent({ service }); 33 - try { 34 - if (!apiAgent) { 35 - // /*mass comment*/ console.log("Agent is null or undefined"); 36 - return; 37 - } 38 - let sess: AtpSessionData = JSON.parse(session); 39 - // /*mass comment*/ console.log("resuming session is:", sess); 40 - const { data } = await apiAgent.resumeSession(sess); 41 - // /*mass comment*/ console.log("!!!8!!! agent resume session"); 42 - setAgent(apiAgent); 43 - setLoginStatus(true); 44 - setLoading(false); 45 - setAuthed(true); 46 - } catch (e) { 47 - // /*mass comment*/ console.log("Failed to resume session" + e); 48 - setLoginStatus(true); 49 - localStorage.removeItem("sess"); 50 - localStorage.removeItem("service"); 51 - const apiAgent = new AtpAgent({ service: "https://api.bsky.app" }); 52 - setAgent(apiAgent); 53 - setLoginStatus(true); 54 - setLoading(false); 55 - setAuthed(false); 56 - return; 57 - } 58 - } else { 59 - const apiAgent = new AtpAgent({ service: "https://api.bsky.app" }); 60 - setAgent(apiAgent); 61 - setLoginStatus(true); 62 - setLoading(false); 63 - setAuthed(false); 64 - } 65 - } catch (e) { 66 - // /*mass comment*/ console.log("Failed to auto-login:", e); 67 - } finally { 68 - setLoading(false); 69 - } 70 - }; 71 - 72 - initialize(); 73 - }, [increment]); 74 - 75 - const login = async ( 76 - user: string, 77 - password: string, 78 - service: string = "https://bsky.social", 79 - ) => { 80 - try { 81 - let sessionthing; 82 - const apiAgent = new AtpAgent({ 83 - service: service, 84 - persistSession: (evt, sess) => { 85 - sessionthing = sess; 86 - }, 87 - }); 88 - await apiAgent.login({ identifier: user, password }); 89 - // /*mass comment*/ console.log("!!!8!!! agent logged on"); 90 - 91 - localStorage.setItem("service", service); 92 - // await AsyncStorage.setItem('user', user); 93 - // await AsyncStorage.setItem('password', password); 94 - if (sessionthing) { 95 - localStorage.setItem("sess", JSON.stringify(sessionthing)); 96 - } else { 97 - localStorage.setItem("sess", "{}"); 98 - } 99 - 100 - setAgent(apiAgent); 101 - setLoginStatus(true); 102 - setAuthed(true); 103 - } catch (e) { 104 - console.error("Login failed:", e); 105 - } 106 - }; 107 - 108 - const logout = async () => { 109 - if (!agent) { 110 - console.error("Agent is null or undefined"); 111 - return; 112 - } 113 - setLoading(true); 114 - try { 115 - // check if its even in async storage before removing 116 - if (localStorage.getItem("service") && localStorage.getItem("sess")) { 117 - localStorage.removeItem("service"); 118 - localStorage.removeItem("sess"); 119 - } 120 - await agent.logout(); 121 - // /*mass comment*/ console.log("!!!8!!! agent logout"); 122 - setLoginStatus(false); 123 - setAuthed(undefined); 124 - await agent.com.atproto.server.deleteSession(); 125 - // /*mass comment*/ console.log("!!!8!!! agent deltesession"); 126 - //setAgent(null); 127 - setIncrement(increment + 1); 128 - } catch (e) { 129 - console.error("Logout failed:", e); 130 - } finally { 131 - setLoading(false); 132 - } 133 - }; 134 - 135 - // why the hell are we doing this 136 - /*if (loading) { 137 - return <div><span>Laoding...ae</span></div>; 138 - }*/ 139 - 140 - return ( 141 - <AuthContext.Provider 142 - value={{ agent, loginStatus, login, logout, loading, authed }} 143 - > 144 - {children} 145 - </AuthContext.Provider> 146 - ); 147 - }; 148 - 149 - export const useAuth = () => useContext(AuthContext);
···
-61
src/providers/PersistentStoreProvider.tsx
··· 1 - import React, { createContext, useContext, useCallback } from "react"; 2 - import { get as idbGet, set as idbSet, del as idbDel } from "idb-keyval"; 3 - 4 - type PersistentValue = { 5 - value: string; 6 - time: number; 7 - }; 8 - 9 - type PersistentStoreContextType = { 10 - get: (key: string) => Promise<PersistentValue | null>; 11 - set: (key: string, value: string) => Promise<void>; 12 - remove: (key: string) => Promise<void>; 13 - }; 14 - 15 - const PersistentStoreContext = createContext<PersistentStoreContextType | null>( 16 - null, 17 - ); 18 - 19 - export const PersistentStoreProvider: React.FC<{ 20 - children: React.ReactNode; 21 - }> = ({ children }) => { 22 - const get = useCallback( 23 - async (key: string): Promise<PersistentValue | null> => { 24 - if (typeof window === "undefined") return null; 25 - const raw = await idbGet(key); 26 - if (!raw) return null; 27 - try { 28 - return JSON.parse(raw) as PersistentValue; 29 - } catch { 30 - return null; 31 - } 32 - }, 33 - [], 34 - ); 35 - 36 - const set = useCallback(async (key: string, value: string) => { 37 - if (typeof window === "undefined") return; 38 - const entry: PersistentValue = { value, time: Date.now() }; 39 - await idbSet(key, JSON.stringify(entry)); 40 - }, []); 41 - 42 - const remove = useCallback(async (key: string) => { 43 - if (typeof window === "undefined") return; 44 - await idbDel(key); 45 - }, []); 46 - 47 - return ( 48 - <PersistentStoreContext.Provider value={{ get, set, remove }}> 49 - {children} 50 - </PersistentStoreContext.Provider> 51 - ); 52 - }; 53 - 54 - export const usePersistentStore = (): PersistentStoreContextType => { 55 - const context = useContext(PersistentStoreContext); 56 - if (!context) 57 - throw new Error( 58 - "usePersistentStore must be used within a PersistentStoreProvider", 59 - ); 60 - return context; 61 - };
···
+36 -5
src/routeTree.gen.ts
··· 21 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' 24 25 const SettingsRoute = SettingsRouteImport.update({ 26 id: '/settings', ··· 83 path: '/profile/$did/post/$rkey', 84 getParentRoute: () => rootRouteImport, 85 } as any) 86 87 export interface FileRoutesByFullPath { 88 '/': typeof IndexRoute ··· 94 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 95 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 96 '/profile/$did': typeof ProfileDidIndexRoute 97 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 98 } 99 export interface FileRoutesByTo { 100 '/': typeof IndexRoute ··· 106 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 107 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 108 '/profile/$did': typeof ProfileDidIndexRoute 109 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 110 } 111 export interface FileRoutesById { 112 __root__: typeof rootRouteImport ··· 121 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 122 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 123 '/profile/$did/': typeof ProfileDidIndexRoute 124 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 125 } 126 export interface FileRouteTypes { 127 fileRoutesByFullPath: FileRoutesByFullPath ··· 136 | '/route-b' 137 | '/profile/$did' 138 | '/profile/$did/post/$rkey' 139 fileRoutesByTo: FileRoutesByTo 140 to: 141 | '/' ··· 148 | '/route-b' 149 | '/profile/$did' 150 | '/profile/$did/post/$rkey' 151 id: 152 | '__root__' 153 | '/' ··· 162 | '/_pathlessLayout/_nested-layout/route-b' 163 | '/profile/$did/' 164 | '/profile/$did/post/$rkey' 165 fileRoutesById: FileRoutesById 166 } 167 export interface RootRouteChildren { ··· 173 SettingsRoute: typeof SettingsRoute 174 CallbackIndexRoute: typeof CallbackIndexRoute 175 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 176 - ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute 177 } 178 179 declare module '@tanstack/react-router' { ··· 262 preLoaderRoute: typeof ProfileDidPostRkeyRouteImport 263 parentRoute: typeof rootRouteImport 264 } 265 } 266 } 267 ··· 295 PathlessLayoutRouteChildren, 296 ) 297 298 const rootRouteChildren: RootRouteChildren = { 299 IndexRoute: IndexRoute, 300 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, ··· 304 SettingsRoute: SettingsRoute, 305 CallbackIndexRoute: CallbackIndexRoute, 306 ProfileDidIndexRoute: ProfileDidIndexRoute, 307 - ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute, 308 } 309 export const routeTree = rootRouteImport 310 ._addFileChildren(rootRouteChildren)
··· 21 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' 24 + import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i' 25 26 const SettingsRoute = SettingsRouteImport.update({ 27 id: '/settings', ··· 84 path: '/profile/$did/post/$rkey', 85 getParentRoute: () => rootRouteImport, 86 } as any) 87 + const ProfileDidPostRkeyImageIRoute = 88 + ProfileDidPostRkeyImageIRouteImport.update({ 89 + id: '/image/$i', 90 + path: '/image/$i', 91 + getParentRoute: () => ProfileDidPostRkeyRoute, 92 + } as any) 93 94 export interface FileRoutesByFullPath { 95 '/': typeof IndexRoute ··· 101 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 102 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 103 '/profile/$did': typeof ProfileDidIndexRoute 104 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 105 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 106 } 107 export interface FileRoutesByTo { 108 '/': typeof IndexRoute ··· 114 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 115 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 116 '/profile/$did': typeof ProfileDidIndexRoute 117 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 118 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 119 } 120 export interface FileRoutesById { 121 __root__: typeof rootRouteImport ··· 130 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 131 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 132 '/profile/$did/': typeof ProfileDidIndexRoute 133 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 134 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 135 } 136 export interface FileRouteTypes { 137 fileRoutesByFullPath: FileRoutesByFullPath ··· 146 | '/route-b' 147 | '/profile/$did' 148 | '/profile/$did/post/$rkey' 149 + | '/profile/$did/post/$rkey/image/$i' 150 fileRoutesByTo: FileRoutesByTo 151 to: 152 | '/' ··· 159 | '/route-b' 160 | '/profile/$did' 161 | '/profile/$did/post/$rkey' 162 + | '/profile/$did/post/$rkey/image/$i' 163 id: 164 | '__root__' 165 | '/' ··· 174 | '/_pathlessLayout/_nested-layout/route-b' 175 | '/profile/$did/' 176 | '/profile/$did/post/$rkey' 177 + | '/profile/$did/post/$rkey/image/$i' 178 fileRoutesById: FileRoutesById 179 } 180 export interface RootRouteChildren { ··· 186 SettingsRoute: typeof SettingsRoute 187 CallbackIndexRoute: typeof CallbackIndexRoute 188 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 189 + ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren 190 } 191 192 declare module '@tanstack/react-router' { ··· 275 preLoaderRoute: typeof ProfileDidPostRkeyRouteImport 276 parentRoute: typeof rootRouteImport 277 } 278 + '/profile/$did/post/$rkey/image/$i': { 279 + id: '/profile/$did/post/$rkey/image/$i' 280 + path: '/image/$i' 281 + fullPath: '/profile/$did/post/$rkey/image/$i' 282 + preLoaderRoute: typeof ProfileDidPostRkeyImageIRouteImport 283 + parentRoute: typeof ProfileDidPostRkeyRoute 284 + } 285 } 286 } 287 ··· 315 PathlessLayoutRouteChildren, 316 ) 317 318 + interface ProfileDidPostRkeyRouteChildren { 319 + ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute 320 + } 321 + 322 + const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = { 323 + ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute, 324 + } 325 + 326 + const ProfileDidPostRkeyRouteWithChildren = 327 + ProfileDidPostRkeyRoute._addFileChildren(ProfileDidPostRkeyRouteChildren) 328 + 329 const rootRouteChildren: RootRouteChildren = { 330 IndexRoute: IndexRoute, 331 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, ··· 335 SettingsRoute: SettingsRoute, 336 CallbackIndexRoute: CallbackIndexRoute, 337 ProfileDidIndexRoute: ProfileDidIndexRoute, 338 + ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren, 339 } 340 export const routeTree = rootRouteImport 341 ._addFileChildren(rootRouteChildren)
+564 -465
src/routes/__root.tsx
··· 2 3 // dont forget to run this 4 // npx @tanstack/router-cli generate 5 - 6 - import { useState, type SVGProps } from "react"; 7 import { 8 - HeadContent, 9 - Link, 10 - Outlet, 11 - Scripts, 12 - createRootRoute, 13 createRootRouteWithContext, 14 useLocation, 15 useNavigate, 16 } from "@tanstack/react-router"; 17 import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 18 import * as React from "react"; 19 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 20 import Login from "~/components/Login"; 21 import { NotFound } from "~/components/NotFound"; 22 - import appCss from "~/styles/app.css?url"; 23 - import { seo } from "~/utils/seo"; 24 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 25 - import { PersistentStoreProvider } from "~/providers/PersistentStoreProvider"; 26 - import type Agent from "@atproto/api"; 27 - import type { QueryClient } from "@tanstack/react-query"; 28 29 export const Route = createRootRouteWithContext<{ 30 queryClient: QueryClient; ··· 79 function RootComponent() { 80 return ( 81 <UnifiedAuthProvider> 82 - <PersistentStoreProvider> 83 - <RootDocument> 84 - <Outlet /> 85 - </RootDocument> 86 - </PersistentStoreProvider> 87 </UnifiedAuthProvider> 88 ); 89 } 90 91 function RootDocument({ children }: { children: React.ReactNode }) { 92 const location = useLocation(); 93 const navigate = useNavigate(); 94 const { agent } = useAuth(); 95 const authed = !!agent?.did; 96 const isHome = location.pathname === "/"; 97 const isNotifications = location.pathname.startsWith("/notifications"); 98 - const isProfile = agent && ((location.pathname === (`/profile/${agent?.did}`)) || (location.pathname === (`/profile/${encodeURIComponent(agent?.did??"")}`))); 99 100 - const [postOpen, setPostOpen] = useState(false); 101 - const [postText, setPostText] = useState(""); 102 - const [posting, setPosting] = useState(false); 103 - const [postSuccess, setPostSuccess] = useState(false); 104 - const [postError, setPostError] = useState<string | null>(null); 105 106 - async function handlePost() { 107 - if (!agent) return; 108 - setPosting(true); 109 - setPostError(null); 110 - try { 111 - await agent.com.atproto.repo.createRecord({ 112 - collection: "app.bsky.feed.post", 113 - repo: agent.assertDid, 114 - record: { 115 - $type: "app.bsky.feed.post", 116 - text: postText, 117 - createdAt: new Date().toISOString(), 118 - }, 119 - }); 120 - setPostSuccess(true); 121 - setPostText(""); 122 - setTimeout(() => { 123 - setPostSuccess(false); 124 - setPostOpen(false); 125 - }, 1500); 126 - } catch (e: any) { 127 - setPostError(e?.message || "Failed to post"); 128 - } finally { 129 - setPosting(false); 130 - } 131 - } 132 133 return ( 134 <> 135 - {postOpen && ( 136 - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> 137 - <div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md relative"> 138 - <button 139 - className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" 140 - onClick={() => !posting && setPostOpen(false)} 141 - disabled={posting} 142 - aria-label="Close" 143 - > 144 - ร— 145 - </button> 146 - <h2 className="text-lg font-bold mb-2">Create Post</h2> 147 - {postSuccess ? ( 148 - <div className="flex flex-col items-center justify-center py-8"> 149 - <span className="text-green-500 text-4xl mb-2">โœ“</span> 150 - <span className="text-green-600">Posted!</span> 151 - </div> 152 - ) : ( 153 - <> 154 - <textarea 155 - className="w-full border rounded p-2 mb-2 dark:bg-gray-800 dark:border-gray-700" 156 - rows={4} 157 - placeholder="What's on your mind?" 158 - value={postText} 159 - onChange={(e) => setPostText(e.target.value)} 160 - disabled={posting} 161 - autoFocus 162 - /> 163 - {postError && ( 164 - <div className="text-red-500 text-sm mb-2">{postError}</div> 165 - )} 166 - <button 167 - className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50" 168 - onClick={handlePost} 169 - disabled={posting || !postText.trim()} 170 - > 171 - {posting ? "Posting..." : "Post"} 172 - </button> 173 - </> 174 - )} 175 - </div> 176 - </div> 177 - )} 178 179 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 180 - <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start"> 181 <div className="flex items-center gap-3 mb-4"> 182 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 183 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 184 Red Dwarf{" "} 185 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 187 </span> */} 188 </span> 189 </div> 190 - <Link 191 to="/" 192 className={ 193 `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` + 194 (isHome ? "font-bold" : "") 195 } 196 > 197 - {isHome ? ( 198 - <TablerHomeFilled width={28} height={28} /> 199 ) : ( 200 - <TablerHome width={28} height={28} /> 201 )} 202 <span>Home</span> 203 </Link> ··· 208 (isNotifications ? "font-bold" : "") 209 } 210 > 211 - {isNotifications ? ( 212 - <TablerBellFilled width={28} height={28} /> 213 ) : ( 214 - <TablerBell width={28} height={28} /> 215 )} 216 <span>Notifications</span> 217 </Link> ··· 222 }`} 223 > 224 {location.pathname.startsWith("/feeds") ? ( 225 - <TablerHashtagFilled width={28} height={28} /> 226 ) : ( 227 - <TablerHashtag width={28} height={28} /> 228 )} 229 <span>Feeds</span> 230 </Link> ··· 236 }`} 237 > 238 {location.pathname.startsWith("/search") ? ( 239 - <TablerSearchFilled width={28} height={28} /> 240 ) : ( 241 - <TablerSearch width={28} height={28} /> 242 )} 243 <span>Search</span> 244 </Link> ··· 252 navigate({ 253 to: "/profile/$did", 254 params: { did: agent.assertDid }, 255 - }) 256 } 257 }} 258 type="button" 259 > 260 - <TablerUserCircle width={28} height={28} /> 261 <span>Profile</span> 262 </button> 263 <Link ··· 266 location.pathname.startsWith("/settings") ? "font-bold" : "" 267 }`} 268 > 269 - {location.pathname.startsWith("/settings") ? ( 270 - <IonSettingsSharp width={28} height={28} /> 271 ) : ( 272 - <IonSettings width={28} height={28} /> 273 )} 274 <span>Settings</span> 275 - </Link> 276 - <button 277 className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow" 278 onClick={() => setPostOpen(true)} 279 type="button" 280 > 281 - <TablerEdit 282 width={24} 283 height={24} 284 className="text-gray-600 dark:text-gray-400" 285 /> 286 <span>Post</span> 287 - </button> 288 <div className="flex-1"></div> 289 <a 290 href="https://tangled.sh/@whey.party/red-dwarf" ··· 315 </div> 316 </nav> 317 318 - <button 319 - className="lg:hidden fixed bottom-20 right-6 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-blue-600 dark:text-blue-400 rounded-full shadow-lg w-16 h-16 flex items-center justify-center border-4 border-white dark:border-gray-950 transition-all" 320 - style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 321 - onClick={() => setPostOpen(true)} 322 - type="button" 323 - aria-label="Create Post" 324 - > 325 - <TablerEdit 326 - width={24} 327 - height={24} 328 - className="text-gray-600 dark:text-gray-400" 329 /> 330 - </button> 331 332 - <main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0"> 333 - <div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950"> 334 - <div className="flex items-center gap-2"> 335 - <img 336 - src="/redstar.png" 337 - alt="Red Dwarf Logo" 338 - className="w-6 h-6" 339 - /> 340 - <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 341 - Red Dwarf{" "} 342 - {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 343 - lite 344 - </span> */} 345 - </span> 346 - </div> 347 - <div className="flex items-center gap-2"> 348 - <Login compact={true} /> 349 - </div> 350 </div> 351 352 {children} 353 </main> 354 355 <aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col"> 356 <Login /> 357 358 <div className="flex-1"></div> 359 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 360 - Red Dwarf is a bluesky client that uses Constellation and direct PDS 361 - queries. Skylite would be a self-hosted bluesky "instance". Stay 362 - tuned for the release of Skylite. 363 </p> 364 </aside> 365 </div> 366 367 - <nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40"> 368 - <div className="flex justify-around items-center py-2"> 369 - <Link 370 - to="/" 371 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 372 - isHome 373 - ? "text-gray-900 dark:text-gray-100" 374 - : "text-gray-600 dark:text-gray-400" 375 - }`} 376 - > 377 - {isHome ? ( 378 - <TablerHomeFilled width={24} height={24} /> 379 - ) : ( 380 - <TablerHome width={24} height={24} /> 381 - )} 382 - <span className="text-xs mt-1">Home</span> 383 - </Link> 384 - <Link 385 - to="/search" 386 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 387 - location.pathname.startsWith("/search") 388 - ? "text-gray-900 dark:text-gray-100" 389 - : "text-gray-600 dark:text-gray-400" 390 - }`} 391 - > 392 - {location.pathname.startsWith("/search") ? ( 393 - <TablerSearchFilled width={24} height={24} /> 394 - ) : ( 395 - <TablerSearch width={24} height={24} /> 396 - )} 397 - <span className="text-xs mt-1">Search</span> 398 - </Link> 399 - <Link 400 - to="/notifications" 401 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 402 - isNotifications 403 - ? "text-gray-900 dark:text-gray-100" 404 - : "text-gray-600 dark:text-gray-400" 405 - }`} 406 - > 407 - {isNotifications ? ( 408 - <TablerBellFilled width={24} height={24} /> 409 - ) : ( 410 - <TablerBell width={24} height={24} /> 411 - )} 412 - <span className="text-xs mt-1">Notifications</span> 413 - </Link> 414 - <button 415 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 416 - isProfile 417 - ? "text-gray-900 dark:text-gray-100" 418 - : "text-gray-600 dark:text-gray-400" 419 - }`} 420 - onClick={() => { 421 - if (authed && agent && agent.assertDid) { 422 - //window.location.href = `/profile/${agent.assertDid}`; 423 navigate({ 424 - to: "/profile/$did", 425 - params: { did: agent.assertDid }, 426 }) 427 } 428 - }} 429 - type="button" 430 - > 431 - <TablerUserCircle width={24} height={24} /> 432 - <span className="text-xs mt-1">Profile</span> 433 - </button> 434 - <Link 435 - to="/settings" 436 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 437 - location.pathname.startsWith("/settings") 438 - ? "text-gray-900 dark:text-gray-100" 439 - : "text-gray-600 dark:text-gray-400" 440 - }`} 441 - > 442 - {location.pathname.startsWith("/settings") ? ( 443 - <IonSettingsSharp width={24} height={24} /> 444 - ) : ( 445 - <IonSettings width={24} height={24} /> 446 - )} 447 - <span className="text-xs mt-1">Settings</span> 448 - </Link> 449 </div> 450 - </nav> 451 452 - <TanStackRouterDevtools position="bottom-right" /> 453 <Scripts /> 454 </> 455 ); 456 } 457 - export function TablerHashtag(props: SVGProps<SVGSVGElement>) { 458 - return ( 459 - <svg 460 - xmlns="http://www.w3.org/2000/svg" 461 - width={24} 462 - height={24} 463 - viewBox="0 0 24 24" 464 - {...props} 465 - > 466 - <path 467 - fill="none" 468 - stroke="currentColor" 469 - strokeLinecap="round" 470 - strokeLinejoin="round" 471 - strokeWidth={2} 472 - d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" 473 - ></path> 474 - </svg> 475 - ); 476 - } 477 478 - export function TablerHashtagFilled(props: SVGProps<SVGSVGElement>) { 479 - return ( 480 - <svg 481 - xmlns="http://www.w3.org/2000/svg" 482 - width={24} 483 - height={24} 484 - viewBox="0 0 24 24" 485 - {...props} 486 - > 487 - <path 488 - fill="none" 489 - stroke="currentColor" 490 - strokeLinecap="round" 491 - strokeLinejoin="round" 492 - strokeWidth={3} 493 - d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" 494 - ></path> 495 - </svg> 496 - ); 497 - } 498 - export function TablerEdit(props: SVGProps<SVGSVGElement>) { 499 - return ( 500 - <svg 501 - xmlns="http://www.w3.org/2000/svg" 502 - width={24} 503 - height={24} 504 - viewBox="0 0 24 24" 505 - className="text-white" 506 - {...props} 507 - > 508 - <g 509 - fill="none" 510 - stroke="currentColor" 511 - strokeLinecap="round" 512 - strokeLinejoin="round" 513 - strokeWidth={2} 514 - > 515 - <path d="M16.475 5.408a2.36 2.36 0 1 1 3.34 3.34L7.5 21H3v-4.5z"></path> 516 - </g> 517 - </svg> 518 - ); 519 - } 520 - export function TablerHome(props: SVGProps<SVGSVGElement>) { 521 - return ( 522 - <svg 523 - xmlns="http://www.w3.org/2000/svg" 524 - width={24} 525 - height={24} 526 - viewBox="0 0 24 24" 527 - className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" 528 - {...props} 529 - > 530 - <g 531 - stroke="currentColor" 532 - strokeLinecap="round" 533 - strokeLinejoin="round" 534 - strokeWidth={2} 535 - fill="none" 536 > 537 - <path d="M5 12H3l9-9l9 9h-2M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7"></path> 538 - <path d="M9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6"></path> 539 - </g> 540 - </svg> 541 - ); 542 - } 543 - export function TablerHomeFilled(props: SVGProps<SVGSVGElement>) { 544 - return ( 545 - <svg 546 - xmlns="http://www.w3.org/2000/svg" 547 - width={24} 548 - height={24} 549 - viewBox="0 0 24 24" 550 - className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" 551 - {...props} 552 - > 553 - <path 554 - fill="currentColor" 555 - d="m12.707 2.293l9 9c.63.63.184 1.707-.707 1.707h-1v6a3 3 0 0 1-3 3h-1v-7a3 3 0 0 0-2.824-2.995L13 12h-2a3 3 0 0 0-3 3v7H7a3 3 0 0 1-3-3v-6H3c-.89 0-1.337-1.077-.707-1.707l9-9a1 1 0 0 1 1.414 0M13 14a1 1 0 0 1 1 1v7h-4v-7a1 1 0 0 1 .883-.993L11 14z" 556 - ></path> 557 - </svg> 558 - ); 559 - } 560 561 - export function TablerBell(props: SVGProps<SVGSVGElement>) { 562 return ( 563 - <svg 564 - xmlns="http://www.w3.org/2000/svg" 565 - width={24} 566 - height={24} 567 - viewBox="0 0 24 24" 568 - {...props} 569 - > 570 - <path 571 - className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" 572 - stroke="currentColor" 573 - strokeLinecap="round" 574 - strokeLinejoin="round" 575 - strokeWidth={2} 576 - d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1" 577 - ></path> 578 - </svg> 579 - ); 580 - } 581 - export function TablerBellFilled(props: SVGProps<SVGSVGElement>) { 582 - return ( 583 - <svg 584 - xmlns="http://www.w3.org/2000/svg" 585 - width={24} 586 - height={24} 587 - viewBox="0 0 24 24" 588 - className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" 589 - {...props} 590 - > 591 - <path 592 - fill="currentColor" 593 - stroke="currentColor" 594 - d="M14.235 19c.865 0 1.322 1.024.745 1.668A4 4 0 0 1 12 22a4 4 0 0 1-2.98-1.332c-.552-.616-.158-1.579.634-1.661l.11-.006zM12 2c1.358 0 2.506.903 2.875 2.141l.046.171l.008.043a8.01 8.01 0 0 1 4.024 6.069l.028.287L19 11v2.931l.021.136a3 3 0 0 0 1.143 1.847l.167.117l.162.099c.86.487.56 1.766-.377 1.864L20 18H4c-1.028 0-1.387-1.364-.493-1.87a3 3 0 0 0 1.472-2.063L5 13.924l.001-2.97A8 8 0 0 1 8.822 4.5l.248-.146l.01-.043a3 3 0 0 1 2.562-2.29l.182-.017z" 595 - ></path> 596 - </svg> 597 - ); 598 - } 599 - 600 - export function TablerUserCircle(props: SVGProps<SVGSVGElement>) { 601 - return ( 602 - <svg 603 - xmlns="http://www.w3.org/2000/svg" 604 - width={24} 605 - height={24} 606 - viewBox="0 0 24 24" 607 - className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" 608 - {...props} 609 > 610 - <g 611 - fill="none" 612 - stroke="currentColor" 613 - strokeLinecap="round" 614 - strokeLinejoin="round" 615 - strokeWidth={2} 616 > 617 - <path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0"></path> 618 - <path d="M9 10a3 3 0 1 0 6 0a3 3 0 1 0-6 0m-2.832 8.849A4 4 0 0 1 10 16h4a4 4 0 0 1 3.834 2.855"></path> 619 - </g> 620 - </svg> 621 - ); 622 - } 623 - 624 - export function TablerSearch(props: SVGProps<SVGSVGElement>) { 625 - return ( 626 - <svg 627 - xmlns="http://www.w3.org/2000/svg" 628 - width={24} 629 - height={24} 630 - viewBox="0 0 24 24" 631 - //className="text-gray-400 dark:text-gray-500" 632 - {...props} 633 - > 634 - <g 635 - fill="none" 636 - stroke="currentColor" 637 - strokeLinecap="round" 638 - strokeLinejoin="round" 639 - strokeWidth={2} 640 - > 641 - <path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0"></path> 642 - <path d="m21 21l-6-6"></path> 643 - </g> 644 - </svg> 645 - ); 646 - } 647 - export function TablerSearchFilled(props: SVGProps<SVGSVGElement>) { 648 - return ( 649 - <svg 650 - xmlns="http://www.w3.org/2000/svg" 651 - width={24} 652 - height={24} 653 - viewBox="0 0 24 24" 654 - //className="text-gray-400 dark:text-gray-500" 655 - {...props} 656 - > 657 - <g 658 - fill="none" 659 - stroke="currentColor" 660 - strokeLinecap="round" 661 - strokeLinejoin="round" 662 - strokeWidth={3} 663 - > 664 - <path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0"></path> 665 - <path d="m21 21l-6-6"></path> 666 - </g> 667 - </svg> 668 ); 669 } 670 671 - export function IonSettings(props: SVGProps<SVGSVGElement>) { 672 - return ( 673 - <svg 674 - xmlns="http://www.w3.org/2000/svg" 675 - width={24} 676 - height={24} 677 - viewBox="0 0 512 512" 678 - {...props} 679 - > 680 - <path 681 - fill="none" 682 - stroke="currentColor" 683 - strokeLinecap="round" 684 - strokeLinejoin="round" 685 - strokeWidth={32} 686 - d="M262.29 192.31a64 64 0 1 0 57.4 57.4a64.13 64.13 0 0 0-57.4-57.4M416.39 256a154 154 0 0 1-1.53 20.79l45.21 35.46a10.81 10.81 0 0 1 2.45 13.75l-42.77 74a10.81 10.81 0 0 1-13.14 4.59l-44.9-18.08a16.11 16.11 0 0 0-15.17 1.75A164.5 164.5 0 0 1 325 400.8a15.94 15.94 0 0 0-8.82 12.14l-6.73 47.89a11.08 11.08 0 0 1-10.68 9.17h-85.54a11.11 11.11 0 0 1-10.69-8.87l-6.72-47.82a16.07 16.07 0 0 0-9-12.22a155 155 0 0 1-21.46-12.57a16 16 0 0 0-15.11-1.71l-44.89 18.07a10.81 10.81 0 0 1-13.14-4.58l-42.77-74a10.8 10.8 0 0 1 2.45-13.75l38.21-30a16.05 16.05 0 0 0 6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 0 0-6.07-13.94l-38.19-30A10.81 10.81 0 0 1 49.48 186l42.77-74a10.81 10.81 0 0 1 13.14-4.59l44.9 18.08a16.11 16.11 0 0 0 15.17-1.75A164.5 164.5 0 0 1 187 111.2a15.94 15.94 0 0 0 8.82-12.14l6.73-47.89A11.08 11.08 0 0 1 213.23 42h85.54a11.11 11.11 0 0 1 10.69 8.87l6.72 47.82a16.07 16.07 0 0 0 9 12.22a155 155 0 0 1 21.46 12.57a16 16 0 0 0 15.11 1.71l44.89-18.07a10.81 10.81 0 0 1 13.14 4.58l42.77 74a10.8 10.8 0 0 1-2.45 13.75l-38.21 30a16.05 16.05 0 0 0-6.05 14.08c.33 4.14.55 8.3.55 12.47" 687 - ></path> 688 - </svg> 689 - ); 690 - } 691 - export function IonSettingsSharp(props: SVGProps<SVGSVGElement>) { 692 return ( 693 - <svg 694 - xmlns="http://www.w3.org/2000/svg" 695 - width={24} 696 - height={24} 697 - viewBox="0 0 512 512" 698 - {...props} 699 > 700 - <path 701 - fill="currentColor" 702 - d="M256 176a80 80 0 1 0 80 80a80.24 80.24 0 0 0-80-80m172.72 80a165.5 165.5 0 0 1-1.64 22.34l48.69 38.12a11.59 11.59 0 0 1 2.63 14.78l-46.06 79.52a11.64 11.64 0 0 1-14.14 4.93l-57.25-23a176.6 176.6 0 0 1-38.82 22.67l-8.56 60.78a11.93 11.93 0 0 1-11.51 9.86h-92.12a12 12 0 0 1-11.51-9.53l-8.56-60.78A169.3 169.3 0 0 1 151.05 393L93.8 416a11.64 11.64 0 0 1-14.14-4.92L33.6 331.57a11.59 11.59 0 0 1 2.63-14.78l48.69-38.12A175 175 0 0 1 83.28 256a165.5 165.5 0 0 1 1.64-22.34l-48.69-38.12a11.59 11.59 0 0 1-2.63-14.78l46.06-79.52a11.64 11.64 0 0 1 14.14-4.93l57.25 23a176.6 176.6 0 0 1 38.82-22.67l8.56-60.78A11.93 11.93 0 0 1 209.94 26h92.12a12 12 0 0 1 11.51 9.53l8.56 60.78A169.3 169.3 0 0 1 361 119l57.2-23a11.64 11.64 0 0 1 14.14 4.92l46.06 79.52a11.59 11.59 0 0 1-2.63 14.78l-48.69 38.12a175 175 0 0 1 1.64 22.66" 703 - ></path> 704 - </svg> 705 ); 706 }
··· 2 3 // dont forget to run this 4 // npx @tanstack/router-cli generate 5 + import type { QueryClient } from "@tanstack/react-query"; 6 import { 7 createRootRouteWithContext, 8 + // Link, 9 + // Outlet, 10 + Scripts, 11 useLocation, 12 useNavigate, 13 } from "@tanstack/react-router"; 14 import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 15 + import { useAtom } from "jotai"; 16 import * as React from "react"; 17 + import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 18 + 19 + import { Composer } from "~/components/Composer"; 20 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 21 + import { Import } from "~/components/Import"; 22 import Login from "~/components/Login"; 23 import { NotFound } from "~/components/NotFound"; 24 + import { FluentEmojiHighContrastGlowingStar } from "~/components/Star"; 25 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 26 + import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms"; 27 + import { seo } from "~/utils/seo"; 28 29 export const Route = createRootRouteWithContext<{ 30 queryClient: QueryClient; ··· 79 function RootComponent() { 80 return ( 81 <UnifiedAuthProvider> 82 + <RootDocument> 83 + <KeepAliveProvider> 84 + <KeepAliveOutlet /> 85 + </KeepAliveProvider> 86 + </RootDocument> 87 </UnifiedAuthProvider> 88 ); 89 } 90 91 function RootDocument({ children }: { children: React.ReactNode }) { 92 + useAtomCssVar(hueAtom, "--tw-gray-hue"); 93 const location = useLocation(); 94 const navigate = useNavigate(); 95 const { agent } = useAuth(); 96 const authed = !!agent?.did; 97 const isHome = location.pathname === "/"; 98 const isNotifications = location.pathname.startsWith("/notifications"); 99 + const isProfile = 100 + agent && 101 + (location.pathname === `/profile/${agent?.did}` || 102 + location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`); 103 + const isSettings = location.pathname.startsWith("/settings"); 104 + const isSearch = location.pathname.startsWith("/search"); 105 + const isFeeds = location.pathname.startsWith("/feeds"); 106 107 + const locationEnum: 108 + | "feeds" 109 + | "search" 110 + | "settings" 111 + | "notifications" 112 + | "profile" 113 + | "home" = isFeeds 114 + ? "feeds" 115 + : isSearch 116 + ? "search" 117 + : isSettings 118 + ? "settings" 119 + : isNotifications 120 + ? "notifications" 121 + : isProfile 122 + ? "profile" 123 + : "home"; 124 125 + const [, setComposerPost] = useAtom(composerAtom); 126 127 return ( 128 <> 129 + <Composer /> 130 131 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 132 + <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 133 <div className="flex items-center gap-3 mb-4"> 134 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 135 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 136 Red Dwarf{" "} 137 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 139 </span> */} 140 </span> 141 </div> 142 + <MaterialNavItem 143 + InactiveIcon={ 144 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 145 + } 146 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 147 + active={locationEnum === "home"} 148 + onClickCallbback={() => 149 + navigate({ 150 + to: "/", 151 + //params: { did: agent.assertDid }, 152 + }) 153 + } 154 + text="Home" 155 + /> 156 + 157 + <MaterialNavItem 158 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 159 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 160 + active={locationEnum === "search"} 161 + onClickCallbback={() => 162 + navigate({ 163 + to: "/search", 164 + //params: { did: agent.assertDid }, 165 + }) 166 + } 167 + text="Explore" 168 + /> 169 + <MaterialNavItem 170 + InactiveIcon={ 171 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 172 + } 173 + ActiveIcon={ 174 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 175 + } 176 + active={locationEnum === "notifications"} 177 + onClickCallbback={() => 178 + navigate({ 179 + to: "/notifications", 180 + //params: { did: agent.assertDid }, 181 + }) 182 + } 183 + text="Notifications" 184 + /> 185 + <MaterialNavItem 186 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 187 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 188 + active={locationEnum === "feeds"} 189 + onClickCallbback={() => 190 + navigate({ 191 + to: "/feeds", 192 + //params: { did: agent.assertDid }, 193 + }) 194 + } 195 + text="Feeds" 196 + /> 197 + <MaterialNavItem 198 + InactiveIcon={ 199 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 200 + } 201 + ActiveIcon={ 202 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 203 + } 204 + active={locationEnum === "profile"} 205 + onClickCallbback={() => { 206 + if (authed && agent && agent.assertDid) { 207 + //window.location.href = `/profile/${agent.assertDid}`; 208 + navigate({ 209 + to: "/profile/$did", 210 + params: { did: agent.assertDid }, 211 + }); 212 + } 213 + }} 214 + text="Profile" 215 + /> 216 + <MaterialNavItem 217 + InactiveIcon={ 218 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 219 + } 220 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 221 + active={locationEnum === "settings"} 222 + onClickCallbback={() => 223 + navigate({ 224 + to: "/settings", 225 + //params: { did: agent.assertDid }, 226 + }) 227 + } 228 + text="Settings" 229 + /> 230 + <div className="flex flex-row items-center justify-center mt-3"> 231 + <MaterialPillButton 232 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 233 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 234 + //active={true} 235 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 236 + text="Post" 237 + /> 238 + </div> 239 + {/* <Link 240 to="/" 241 className={ 242 `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` + 243 (isHome ? "font-bold" : "") 244 } 245 > 246 + {!isHome ? ( 247 + <IconMaterialSymbolsHomeOutline width={28} height={28} /> 248 ) : ( 249 + <IconMaterialSymbolsHome width={28} height={28} /> 250 )} 251 <span>Home</span> 252 </Link> ··· 257 (isNotifications ? "font-bold" : "") 258 } 259 > 260 + {!isNotifications ? ( 261 + <IconMaterialSymbolsNotificationsOutline width={28} height={28} /> 262 ) : ( 263 + <IconMaterialSymbolsNotifications width={28} height={28} /> 264 )} 265 <span>Notifications</span> 266 </Link> ··· 271 }`} 272 > 273 {location.pathname.startsWith("/feeds") ? ( 274 + <IconMaterialSymbolsTag width={28} height={28} /> 275 ) : ( 276 + <IconMaterialSymbolsTag width={28} height={28} /> 277 )} 278 <span>Feeds</span> 279 </Link> ··· 285 }`} 286 > 287 {location.pathname.startsWith("/search") ? ( 288 + <IconMaterialSymbolsSearch width={28} height={28} /> 289 ) : ( 290 + <IconMaterialSymbolsSearch width={28} height={28} /> 291 )} 292 <span>Search</span> 293 </Link> ··· 301 navigate({ 302 to: "/profile/$did", 303 params: { did: agent.assertDid }, 304 + }); 305 } 306 }} 307 type="button" 308 > 309 + {!isProfile ? ( 310 + <IconMaterialSymbolsAccountCircleOutline width={28} height={28} /> 311 + ) : ( 312 + <IconMaterialSymbolsAccountCircle width={28} height={28} /> 313 + )} 314 <span>Profile</span> 315 </button> 316 <Link ··· 319 location.pathname.startsWith("/settings") ? "font-bold" : "" 320 }`} 321 > 322 + {!location.pathname.startsWith("/settings") ? ( 323 + <IconMaterialSymbolsSettingsOutline width={28} height={28} /> 324 ) : ( 325 + <IconMaterialSymbolsSettings width={28} height={28} /> 326 )} 327 <span>Settings</span> 328 + </Link> */} 329 + {/* <button 330 className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow" 331 onClick={() => setPostOpen(true)} 332 type="button" 333 > 334 + <IconMdiPencilOutline 335 width={24} 336 height={24} 337 className="text-gray-600 dark:text-gray-400" 338 /> 339 <span>Post</span> 340 + </button> */} 341 <div className="flex-1"></div> 342 <a 343 href="https://tangled.sh/@whey.party/red-dwarf" ··· 368 </div> 369 </nav> 370 371 + <nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start"> 372 + <div className="flex items-center gap-3 mb-4"> 373 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 374 + </div> 375 + <MaterialNavItem 376 + small 377 + InactiveIcon={ 378 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 379 + } 380 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 381 + active={locationEnum === "home"} 382 + onClickCallbback={() => 383 + navigate({ 384 + to: "/", 385 + //params: { did: agent.assertDid }, 386 + }) 387 + } 388 + text="Home" 389 /> 390 391 + <MaterialNavItem 392 + small 393 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 394 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 395 + active={locationEnum === "search"} 396 + onClickCallbback={() => 397 + navigate({ 398 + to: "/search", 399 + //params: { did: agent.assertDid }, 400 + }) 401 + } 402 + text="Explore" 403 + /> 404 + <MaterialNavItem 405 + small 406 + InactiveIcon={ 407 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 408 + } 409 + ActiveIcon={ 410 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 411 + } 412 + active={locationEnum === "notifications"} 413 + onClickCallbback={() => 414 + navigate({ 415 + to: "/notifications", 416 + //params: { did: agent.assertDid }, 417 + }) 418 + } 419 + text="Notifications" 420 + /> 421 + <MaterialNavItem 422 + small 423 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 424 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 425 + active={locationEnum === "feeds"} 426 + onClickCallbback={() => 427 + navigate({ 428 + to: "/feeds", 429 + //params: { did: agent.assertDid }, 430 + }) 431 + } 432 + text="Feeds" 433 + /> 434 + <MaterialNavItem 435 + small 436 + InactiveIcon={ 437 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 438 + } 439 + ActiveIcon={ 440 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 441 + } 442 + active={locationEnum === "profile"} 443 + onClickCallbback={() => { 444 + if (authed && agent && agent.assertDid) { 445 + //window.location.href = `/profile/${agent.assertDid}`; 446 + navigate({ 447 + to: "/profile/$did", 448 + params: { did: agent.assertDid }, 449 + }); 450 + } 451 + }} 452 + text="Profile" 453 + /> 454 + <MaterialNavItem 455 + small 456 + InactiveIcon={ 457 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 458 + } 459 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 460 + active={locationEnum === "settings"} 461 + onClickCallbback={() => 462 + navigate({ 463 + to: "/settings", 464 + //params: { did: agent.assertDid }, 465 + }) 466 + } 467 + text="Settings" 468 + /> 469 + <div className="flex flex-row items-center justify-center mt-3"> 470 + <MaterialPillButton 471 + small 472 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 473 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 474 + //active={true} 475 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 476 + text="Post" 477 + /> 478 </div> 479 + </nav> 480 481 + {agent?.did && ( 482 + <button 483 + className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all" 484 + style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 485 + onClick={() => setComposerPost({ kind: 'root' })} 486 + type="button" 487 + aria-label="Create Post" 488 + > 489 + <IconMdiPencilOutline 490 + width={24} 491 + height={24} 492 + className="text-gray-600 dark:text-gray-400" 493 + /> 494 + </button> 495 + )} 496 + 497 + <main className="w-full max-w-[600px] sm:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip"> 498 {children} 499 </main> 500 501 <aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col"> 502 + <div className="px-4 pt-4"><Import /></div> 503 <Login /> 504 505 <div className="flex-1"></div> 506 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 507 + Red Dwarf is a Bluesky client that does not rely on any Bluesky API App Servers. Instead, it uses Microcosm to fetch records directly from each users' PDS (via Slingshot) and connect them using backlinks (via Constellation) 508 </p> 509 </aside> 510 </div> 511 512 + {agent?.did ? ( 513 + <nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40"> 514 + <div className="flex justify-around items-center p-2"> 515 + <MaterialNavItem 516 + small 517 + InactiveIcon={ 518 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 519 + } 520 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 521 + active={locationEnum === "home"} 522 + onClickCallbback={() => 523 + navigate({ 524 + to: "/", 525 + //params: { did: agent.assertDid }, 526 + }) 527 + } 528 + text="Home" 529 + /> 530 + {/* <Link 531 + to="/" 532 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 533 + isHome 534 + ? "text-gray-900 dark:text-gray-100" 535 + : "text-gray-600 dark:text-gray-400" 536 + }`} 537 + > 538 + {!isHome ? ( 539 + <IconMaterialSymbolsHomeOutline width={24} height={24} /> 540 + ) : ( 541 + <IconMaterialSymbolsHome width={24} height={24} /> 542 + )} 543 + <span className="text-xs mt-1">Home</span> 544 + </Link> */} 545 + <MaterialNavItem 546 + small 547 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 548 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 549 + active={locationEnum === "search"} 550 + onClickCallbback={() => 551 + navigate({ 552 + to: "/search", 553 + //params: { did: agent.assertDid }, 554 + }) 555 + } 556 + text="Explore" 557 + /> 558 + {/* <Link 559 + to="/search" 560 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 561 + location.pathname.startsWith("/search") 562 + ? "text-gray-900 dark:text-gray-100" 563 + : "text-gray-600 dark:text-gray-400" 564 + }`} 565 + > 566 + {!location.pathname.startsWith("/search") ? ( 567 + <IconMaterialSymbolsSearch width={24} height={24} /> 568 + ) : ( 569 + <IconMaterialSymbolsSearch width={24} height={24} /> 570 + )} 571 + <span className="text-xs mt-1">Search</span> 572 + </Link> */} 573 + <MaterialNavItem 574 + small 575 + InactiveIcon={ 576 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 577 + } 578 + ActiveIcon={ 579 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 580 + } 581 + active={locationEnum === "notifications"} 582 + onClickCallbback={() => 583 + navigate({ 584 + to: "/notifications", 585 + //params: { did: agent.assertDid }, 586 + }) 587 + } 588 + text="Notifications" 589 + /> 590 + {/* <Link 591 + to="/notifications" 592 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 593 + isNotifications 594 + ? "text-gray-900 dark:text-gray-100" 595 + : "text-gray-600 dark:text-gray-400" 596 + }`} 597 + > 598 + {!isNotifications ? ( 599 + <IconMaterialSymbolsNotificationsOutline 600 + width={24} 601 + height={24} 602 + /> 603 + ) : ( 604 + <IconMaterialSymbolsNotifications width={24} height={24} /> 605 + )} 606 + <span className="text-xs mt-1">Notifications</span> 607 + </Link> */} 608 + <MaterialNavItem 609 + small 610 + InactiveIcon={ 611 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 612 + } 613 + ActiveIcon={ 614 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 615 + } 616 + active={locationEnum === "profile"} 617 + onClickCallbback={() => { 618 + if (authed && agent && agent.assertDid) { 619 + //window.location.href = `/profile/${agent.assertDid}`; 620 + navigate({ 621 + to: "/profile/$did", 622 + params: { did: agent.assertDid }, 623 + }); 624 + } 625 + }} 626 + text="Profile" 627 + /> 628 + {/* <button 629 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 630 + isProfile 631 + ? "text-gray-900 dark:text-gray-100" 632 + : "text-gray-600 dark:text-gray-400" 633 + }`} 634 + onClick={() => { 635 + if (authed && agent && agent.assertDid) { 636 + //window.location.href = `/profile/${agent.assertDid}`; 637 + navigate({ 638 + to: "/profile/$did", 639 + params: { did: agent.assertDid }, 640 + }); 641 + } 642 + }} 643 + type="button" 644 + > 645 + <IconMaterialSymbolsAccountCircleOutline width={24} height={24} /> 646 + <span className="text-xs mt-1">Profile</span> 647 + </button> */} 648 + <MaterialNavItem 649 + small 650 + InactiveIcon={ 651 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 652 + } 653 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 654 + active={locationEnum === "settings"} 655 + onClickCallbback={() => 656 navigate({ 657 + to: "/settings", 658 + //params: { did: agent.assertDid }, 659 }) 660 } 661 + text="Settings" 662 + /> 663 + {/* <Link 664 + to="/settings" 665 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 666 + location.pathname.startsWith("/settings") 667 + ? "text-gray-900 dark:text-gray-100" 668 + : "text-gray-600 dark:text-gray-400" 669 + }`} 670 + > 671 + {!location.pathname.startsWith("/settings") ? ( 672 + <IconMaterialSymbolsSettingsOutline width={24} height={24} /> 673 + ) : ( 674 + <IconMaterialSymbolsSettings width={24} height={24} /> 675 + )} 676 + <span className="text-xs mt-1">Settings</span> 677 + </Link> */} 678 + </div> 679 + </nav> 680 + ) : ( 681 + <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10"> 682 + <div className="flex items-center gap-2"> 683 + <FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 684 + <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 685 + Red Dwarf{" "} 686 + {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 687 + lite 688 + </span> */} 689 + </span> 690 + </div> 691 + <div className="flex items-center gap-2"> 692 + <Login compact={true} popup={true} /> 693 + </div> 694 </div> 695 + )} 696 697 + <TanStackRouterDevtools position="bottom-left" /> 698 <Scripts /> 699 </> 700 ); 701 } 702 703 + function MaterialNavItem({ 704 + InactiveIcon, 705 + ActiveIcon, 706 + text, 707 + active, 708 + onClickCallbback, 709 + small, 710 + }: { 711 + InactiveIcon: React.ReactElement; 712 + ActiveIcon: React.ReactElement; 713 + text: string; 714 + active: boolean; 715 + onClickCallbback: () => void; 716 + small?: boolean | string; 717 + }) { 718 + if (small) 719 + return ( 720 + <button 721 + className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${ 722 + active 723 + ? "text-gray-900 dark:text-gray-100" 724 + : "text-gray-600 dark:text-gray-400" 725 + }`} 726 + onClick={() => { 727 + onClickCallbback(); 728 + }} 729 > 730 + <div 731 + className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`} 732 + > 733 + {active ? ActiveIcon : InactiveIcon} 734 + </div> 735 + <span 736 + className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`} 737 + > 738 + {text} 739 + </span> 740 + </button> 741 + ); 742 743 return ( 744 + <button 745 + className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${ 746 + active 747 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700" 748 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900" 749 + }`} 750 + onClick={() => { 751 + onClickCallbback(); 752 + }} 753 > 754 + <div className={`mr-4 ${active ? " " : " "}`}> 755 + {active ? ActiveIcon : InactiveIcon} 756 + </div> 757 + <span 758 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 759 > 760 + {text} 761 + </span> 762 + </button> 763 ); 764 } 765 766 + function MaterialPillButton({ 767 + InactiveIcon, 768 + ActiveIcon, 769 + text, 770 + //active, 771 + onClickCallbback, 772 + small, 773 + }: { 774 + InactiveIcon: React.ReactElement; 775 + ActiveIcon: React.ReactElement; 776 + text: string; 777 + //active: boolean; 778 + onClickCallbback: () => void; 779 + small?: boolean | string; 780 + }) { 781 + const active = false; 782 return ( 783 + <button 784 + className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${ 785 + active 786 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 787 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 788 + }`} 789 + onClick={() => { 790 + onClickCallbback(); 791 + }} 792 > 793 + <div className={`${!small && "mr-2"} ${active ? " " : " "}`}> 794 + {active ? ActiveIcon : InactiveIcon} 795 + </div> 796 + {!small && ( 797 + <span 798 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 799 + > 800 + {text} 801 + </span> 802 + )} 803 + </button> 804 ); 805 }
+174 -173
src/routes/index.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 - import { 3 - CACHE_TIMEOUT, 4 - //cachedGetRecord, 5 - //cachedResolveIdentity, 6 - UniversalPostRendererATURILoader, 7 - } from "~/components/UniversalPostRenderer"; 8 import * as React from "react"; 9 import { useAuth } from "~/providers/UnifiedAuthProvider"; 10 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 11 import { 12 useQueryIdentity, 13 - useQueryPost, 14 - useQueryFeedSkeleton, 15 useQueryPreferences, 16 - useQueryArbitrary, 17 - constructInfiniteFeedSkeletonQuery, 18 - constructArbitraryQuery, 19 - constructIdentityQuery, 20 - constructPostQuery, 21 } from "~/utils/useQuery"; 22 - import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 23 - import { useAtom, useSetAtom } from "jotai"; 24 - import { 25 - selectedFeedUriAtom, 26 - store, 27 - agentAtom, 28 - authedAtom, 29 - feedScrollPositionsAtom, 30 - } from "~/utils/atoms"; 31 - import { useEffect, useLayoutEffect } from "react"; 32 33 export const Route = createFileRoute("/")({ 34 - loader: async ({ context }) => { 35 - const { queryClient } = context; 36 - const atomauth = store.get(authedAtom); 37 - const atomagent = store.get(agentAtom); 38 39 - let identitypds: string | undefined; 40 - const initialselectedfeed = store.get(selectedFeedUriAtom); 41 - if (atomagent && atomauth && atomagent?.did) { 42 - const identityopts = constructIdentityQuery(atomagent.did); 43 - const identityresultmaybe = 44 - await queryClient.ensureQueryData(identityopts); 45 - identitypds = identityresultmaybe?.pds; 46 - } 47 48 - const arbitraryopts = constructArbitraryQuery( 49 - initialselectedfeed ?? 50 - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" 51 - ); 52 - const feedGengetrecordquery = 53 - await queryClient.ensureQueryData(arbitraryopts); 54 - const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; 55 - //queryClient.ensureInfiniteQueryData() 56 57 - const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ 58 - feedUri: 59 - initialselectedfeed ?? 60 - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 61 - agent: atomagent ?? undefined, 62 - isAuthed: atomauth ?? false, 63 - pdsUrl: identitypds, 64 - feedServiceDid: feedServiceDid, 65 - }); 66 67 - const res = await queryClient.ensureInfiniteQueryData({ 68 - queryKey, 69 - queryFn, 70 - initialPageParam: undefined as never, 71 - getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, 72 - staleTime: Infinity, 73 - //refetchOnWindowFocus: false, 74 - //enabled: true, 75 - }); 76 - await Promise.all( 77 - res.pages.map(async (page) => { 78 - await Promise.all( 79 - page.feed.map(async (feedviewpost) => { 80 - if (!feedviewpost.post) return; 81 - // /*mass comment*/ console.log("preloading: ", feedviewpost.post); 82 - const opts = constructPostQuery(feedviewpost.post); 83 - try { 84 - await queryClient.ensureQueryData(opts); 85 - } catch (e) { 86 - // /*mass comment*/ console.log(" failed:", e); 87 - } 88 - }) 89 - ); 90 - }) 91 - ); 92 - }, 93 component: Home, 94 - pendingComponent: PendingHome, 95 }); 96 function PendingHome() { 97 return <div>loading... (prefetching your timeline)</div>; 98 } 99 - function Home() { 100 const { 101 agent, 102 status, ··· 116 }, [status, agent, authed]); 117 useEffect(() => { 118 if (agent) { 119 - // is it just me or is the type really weird here it should be Agent not AtpAgent 120 store.set(agentAtom, agent); 121 } else { 122 store.set(agentAtom, null); ··· 298 feedScrollPositionsAtom 299 ); 300 301 - const scrollRef = React.useRef<Record<string, number>>({}); 302 303 - useEffect(() => { 304 - const onScroll = () => { 305 - //if (!selectedFeed) return; 306 - scrollRef.current[selectedFeed ?? "null"] = window.scrollY; 307 - }; 308 - window.addEventListener("scroll", onScroll, { passive: true }); 309 - return () => window.removeEventListener("scroll", onScroll); 310 - }, [selectedFeed]); 311 - const [donerestored, setdonerestored] = React.useState(false); 312 313 - useEffect(() => { 314 - return () => { 315 - if (!donerestored) return; 316 - // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current); 317 - //if (!selectedFeed) return; 318 - setScrollPositions((prev) => ({ 319 - ...prev, 320 - [selectedFeed ?? "null"]: 321 - scrollRef.current[selectedFeed ?? "null"] ?? 0, 322 - })); 323 - }; 324 - }, [selectedFeed, setScrollPositions, donerestored]); 325 326 - const [restoringScrollPosition, setRestoringScrollPosition] = 327 - React.useState(false); 328 329 useLayoutEffect(() => { 330 - setRestoringScrollPosition(true); 331 - const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 332 333 - let raf = requestAnimationFrame(() => { 334 - // setRestoringScrollPosition(true); 335 - // raf = requestAnimationFrame(() => { 336 - // window.scrollTo({ top: savedPosition, behavior: "instant" }); 337 - // setRestoringScrollPosition(false); 338 - // setdonerestored(true); 339 - // }); 340 - window.scrollTo({ top: savedPosition, behavior: "instant" }); 341 - setRestoringScrollPosition(false); 342 - setdonerestored(true); 343 - }); 344 345 - return () => cancelAnimationFrame(raf); 346 - }, [selectedFeed, scrollPositions]); 347 348 const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 349 const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; ··· 366 authed && agent && identity?.pds && feedServiceDid; 367 const isReadyForUnauthedFeed = !authed && selectedFeed; 368 369 return ( 370 - <div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800"> 371 - <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 372 - {savedFeeds.length > 0 ? ( 373 - savedFeeds.map((item: any, idx: number) => { 374 const label = item.value.split("/").pop() || item.value; 375 const isActive = selectedFeed === item.value; 376 return ( ··· 378 key={item.value || idx} 379 className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${ 380 isActive 381 - ? "bg-gray-500 text-white" 382 - : item.pinned 383 - ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 384 - : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 385 }`} 386 onClick={() => setSelectedFeed(item.value)} 387 title={item.value} 388 > 389 {label} 390 {item.pinned && ( 391 - <span className="ml-1 text-xs text-gray-700 dark:text-gray-200"> 392 โ˜… 393 </span> 394 )} 395 </button> 396 ); 397 - }) 398 - ) : ( 399 - <span className="text-xl font-bold ml-2">Home</span> 400 - )} 401 - </div> 402 {/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>} 403 {feedError && <div className="p-4 text-red-500">{feedError.message}</div>} 404 {!isFeedLoading && !feedError && feed.length === 0 && ( ··· 419 420 {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 421 <InfiniteCustomFeed 422 feedUri={selectedFeed!} 423 pdsUrl={identity?.pds} 424 feedServiceDid={feedServiceDid} ··· 428 Select a feed to get started. 429 </div> 430 )} 431 - {false && restoringScrollPosition && ( 432 <div className="fixed top-1/2 left-1/2 right-1/2"> 433 restoringScrollPosition 434 </div> 435 - )} 436 </div> 437 ); 438 } 439 440 - export async function cachedResolveDIDWEBDOC({ 441 - didweb, 442 - cacheTimeout = CACHE_TIMEOUT, 443 - get, 444 - set, 445 - }: { 446 - didweb: string; 447 - cacheTimeout?: number; 448 - get: (key: string) => any; 449 - set: (key: string, value: string) => void; 450 - }): Promise<any> { 451 - const isDidInput = didweb.startsWith("did:web:"); 452 - const cacheKey = `didwebdoc:${didweb}`; 453 - const now = Date.now(); 454 - const cached = get(cacheKey); 455 - if ( 456 - cached && 457 - cached.value && 458 - cached.time && 459 - now - cached.time < cacheTimeout 460 - ) { 461 - try { 462 - return JSON.parse(cached.value); 463 - } catch {} 464 - } 465 - const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 466 - didweb 467 - )}`; 468 - const res = await fetch(url); 469 - if (!res.ok) throw new Error("Failed to resolve didwebdoc"); 470 - const data = await res.json(); 471 - set(cacheKey, JSON.stringify(data)); 472 - if (!isDidInput && data.did) { 473 - set(`didwebdoc:${data.did}`, JSON.stringify(data)); 474 - } 475 - return data; 476 - } 477 478 // export async function cachedGetPrefs({ 479 // did,
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 import * as React from "react"; 4 + import { useEffect, useLayoutEffect } from "react"; 5 + 6 + import { Header } from "~/components/Header"; 7 + import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 + import { 10 + agentAtom, 11 + authedAtom, 12 + feedScrollPositionsAtom, 13 + isAtTopAtom, 14 + selectedFeedUriAtom, 15 + store, 16 + } from "~/utils/atoms"; 17 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 18 import { 19 + //constructArbitraryQuery, 20 + //constructIdentityQuery, 21 + //constructInfiniteFeedSkeletonQuery, 22 + //constructPostQuery, 23 + useQueryArbitrary, 24 useQueryIdentity, 25 useQueryPreferences, 26 } from "~/utils/useQuery"; 27 28 export const Route = createFileRoute("/")({ 29 + // loader: async ({ context }) => { 30 + // const { queryClient } = context; 31 + // const atomauth = store.get(authedAtom); 32 + // const atomagent = store.get(agentAtom); 33 34 + // let identitypds: string | undefined; 35 + // const initialselectedfeed = store.get(selectedFeedUriAtom); 36 + // if (atomagent && atomauth && atomagent?.did) { 37 + // const identityopts = constructIdentityQuery(atomagent.did); 38 + // const identityresultmaybe = 39 + // await queryClient.ensureQueryData(identityopts); 40 + // identitypds = identityresultmaybe?.pds; 41 + // } 42 43 + // const arbitraryopts = constructArbitraryQuery( 44 + // initialselectedfeed ?? 45 + // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" 46 + // ); 47 + // const feedGengetrecordquery = 48 + // await queryClient.ensureQueryData(arbitraryopts); 49 + // const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; 50 + // //queryClient.ensureInfiniteQueryData() 51 52 + // const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ 53 + // feedUri: 54 + // initialselectedfeed ?? 55 + // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 56 + // agent: atomagent ?? undefined, 57 + // isAuthed: atomauth ?? false, 58 + // pdsUrl: identitypds, 59 + // feedServiceDid: feedServiceDid, 60 + // }); 61 62 + // const res = await queryClient.ensureInfiniteQueryData({ 63 + // queryKey, 64 + // queryFn, 65 + // initialPageParam: undefined as never, 66 + // getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, 67 + // staleTime: Infinity, 68 + // //refetchOnWindowFocus: false, 69 + // //enabled: true, 70 + // }); 71 + // await Promise.all( 72 + // res.pages.map(async (page) => { 73 + // await Promise.all( 74 + // page.feed.map(async (feedviewpost) => { 75 + // if (!feedviewpost.post) return; 76 + // // /*mass comment*/ console.log("preloading: ", feedviewpost.post); 77 + // const opts = constructPostQuery(feedviewpost.post); 78 + // try { 79 + // await queryClient.ensureQueryData(opts); 80 + // } catch (e) { 81 + // // /*mass comment*/ console.log(" failed:", e); 82 + // } 83 + // }) 84 + // ); 85 + // }) 86 + // ); 87 + // }, 88 component: Home, 89 + pendingComponent: PendingHome, // PendingHome, 90 + staticData: { keepAlive: true }, 91 }); 92 function PendingHome() { 93 return <div>loading... (prefetching your timeline)</div>; 94 } 95 + 96 + //function Homer() { 97 + // return <div></div> 98 + //} 99 + export function Home({ hidden = false }: { hidden?: boolean }) { 100 const { 101 agent, 102 status, ··· 116 }, [status, agent, authed]); 117 useEffect(() => { 118 if (agent) { 119 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment 120 + // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 121 store.set(agentAtom, agent); 122 } else { 123 store.set(agentAtom, null); ··· 299 feedScrollPositionsAtom 300 ); 301 302 + const scrollPositionsRef = React.useRef(scrollPositions); 303 304 + React.useEffect(() => { 305 + scrollPositionsRef.current = scrollPositions; 306 + }, [scrollPositions]); 307 308 + useLayoutEffect(() => { 309 + const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 311 + window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 + // eslint-disable-next-line react-hooks/exhaustive-deps 313 + }, [selectedFeed]); 314 315 useLayoutEffect(() => { 316 + if (!selectedFeed) return; 317 318 + const handleScroll = () => { 319 + scrollPositionsRef.current = { 320 + ...scrollPositionsRef.current, 321 + [selectedFeed]: window.scrollY, 322 + }; 323 + }; 324 + 325 + window.addEventListener("scroll", handleScroll, { passive: true }); 326 + return () => { 327 + window.removeEventListener("scroll", handleScroll); 328 329 + setScrollPositions(scrollPositionsRef.current); 330 + }; 331 + }, [selectedFeed, setScrollPositions]); 332 333 const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 334 const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; ··· 351 authed && agent && identity?.pds && feedServiceDid; 352 const isReadyForUnauthedFeed = !authed && selectedFeed; 353 354 + 355 + const [isAtTop] = useAtom(isAtTopAtom); 356 + 357 return ( 358 + <div 359 + className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 360 + > 361 + {savedFeeds.length > 0 ? ( 362 + <div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}> 363 + {savedFeeds.map((item: any, idx: number) => { 364 const label = item.value.split("/").pop() || item.value; 365 const isActive = selectedFeed === item.value; 366 return ( ··· 368 key={item.value || idx} 369 className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${ 370 isActive 371 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 372 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 373 + // ? "bg-gray-500 text-white" 374 + // : item.pinned 375 + // ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 376 + // : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 377 }`} 378 onClick={() => setSelectedFeed(item.value)} 379 title={item.value} 380 > 381 {label} 382 {item.pinned && ( 383 + <span 384 + className={`ml-1 text-xs ${ 385 + isActive 386 + ? "text-gray-900 dark:text-gray-100" 387 + : "text-gray-600 dark:text-gray-400" 388 + }`} 389 + > 390 โ˜… 391 </span> 392 )} 393 </button> 394 ); 395 + })} 396 + </div> 397 + ) : ( 398 + // <span className="text-xl font-bold ml-2">Home</span> 399 + <Header title="Home" /> 400 + )} 401 {/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>} 402 {feedError && <div className="p-4 text-red-500">{feedError.message}</div>} 403 {!isFeedLoading && !feedError && feed.length === 0 && ( ··· 418 419 {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 420 <InfiniteCustomFeed 421 + key={selectedFeed!} 422 feedUri={selectedFeed!} 423 pdsUrl={identity?.pds} 424 feedServiceDid={feedServiceDid} ··· 428 Select a feed to get started. 429 </div> 430 )} 431 + {/* {false && restoringScrollPosition && ( 432 <div className="fixed top-1/2 left-1/2 right-1/2"> 433 restoringScrollPosition 434 </div> 435 + )} */} 436 </div> 437 ); 438 } 439 + // not even used lmaooo 440 441 + // export async function cachedResolveDIDWEBDOC({ 442 + // didweb, 443 + // cacheTimeout = CACHE_TIMEOUT, 444 + // get, 445 + // set, 446 + // }: { 447 + // didweb: string; 448 + // cacheTimeout?: number; 449 + // get: (key: string) => any; 450 + // set: (key: string, value: string) => void; 451 + // }): Promise<any> { 452 + // const isDidInput = didweb.startsWith("did:web:"); 453 + // const cacheKey = `didwebdoc:${didweb}`; 454 + // const now = Date.now(); 455 + // const cached = get(cacheKey); 456 + // if ( 457 + // cached && 458 + // cached.value && 459 + // cached.time && 460 + // now - cached.time < cacheTimeout 461 + // ) { 462 + // try { 463 + // return JSON.parse(cached.value); 464 + // } catch (_e) {/* whatever*/ } 465 + // } 466 + // const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 467 + // didweb 468 + // )}`; 469 + // const res = await fetch(url); 470 + // if (!res.ok) throw new Error("Failed to resolve didwebdoc"); 471 + // const data = await res.json(); 472 + // set(cacheKey, JSON.stringify(data)); 473 + // if (!isDidInput && data.did) { 474 + // set(`didwebdoc:${data.did}`, JSON.stringify(data)); 475 + // } 476 + // return data; 477 + // } 478 479 // export async function cachedGetPrefs({ 480 // did,
+29 -24
src/routes/notifications.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 - import React, { useEffect, useState, useRef } from "react"; 3 - import { useAuth } from "~/providers/PassAuthProvider"; 4 - import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 5 6 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 7 ··· 11 12 function NotificationsComponent() { 13 // /*mass comment*/ console.log("NotificationsComponent render"); 14 - const { agent, authed, loading: authLoading } = useAuth(); 15 - const { get, set } = usePersistentStore(); 16 const [did, setDid] = useState<string | null>(null); 17 const [resolving, setResolving] = useState(false); 18 const [error, setError] = useState<string | null>(null); ··· 41 setResolving(true); 42 const cacheKey = `handleDid:${value}`; 43 const now = Date.now(); 44 - const cached = await get(cacheKey); 45 - if ( 46 - cached && 47 - cached.value && 48 - cached.time && 49 - now - cached.time < HANDLE_DID_CACHE_TIMEOUT 50 - ) { 51 - try { 52 - const data = JSON.parse(cached.value); 53 - setDid(data.did); 54 - setResolving(false); 55 - return; 56 - } catch {} 57 - } 58 try { 59 const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(value)}`; 60 const res = await fetch(url); 61 if (!res.ok) throw new Error("Failed to resolve handle"); 62 const data = await res.json(); 63 - set(cacheKey, JSON.stringify(data)); 64 setDid(data.did); 65 } catch (e: any) { 66 setError("Failed to resolve handle: " + (e?.message || e)); ··· 69 } 70 } 71 72 useEffect(() => { 73 if (!did) return; 74 setLoading(true); 75 setError(null); 76 const urls = [ 77 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`, 78 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`, 79 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`, 80 ]; 81 let ignore = false; 82 Promise.all( ··· 94 } catch (e: any) { 95 return { error: e?.message || String(e) }; 96 } 97 - }), 98 ) 99 .then((results) => { 100 if (!ignore) setResponses(results);
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + import React, { useEffect, useRef,useState } from "react"; 4 + 5 + import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 + import { constellationURLAtom } from "~/utils/atoms"; 7 8 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 9 ··· 13 14 function NotificationsComponent() { 15 // /*mass comment*/ console.log("NotificationsComponent render"); 16 + const { agent, status } = useAuth(); 17 + const authed = !!agent?.did; 18 + const authLoading = status === "loading"; 19 const [did, setDid] = useState<string | null>(null); 20 const [resolving, setResolving] = useState(false); 21 const [error, setError] = useState<string | null>(null); ··· 44 setResolving(true); 45 const cacheKey = `handleDid:${value}`; 46 const now = Date.now(); 47 + const cached = undefined // await get(cacheKey); 48 + // if ( 49 + // cached && 50 + // cached.value && 51 + // cached.time && 52 + // now - cached.time < HANDLE_DID_CACHE_TIMEOUT 53 + // ) { 54 + // try { 55 + // const data = JSON.parse(cached.value); 56 + // setDid(data.did); 57 + // setResolving(false); 58 + // return; 59 + // } catch {} 60 + // } 61 try { 62 const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(value)}`; 63 const res = await fetch(url); 64 if (!res.ok) throw new Error("Failed to resolve handle"); 65 const data = await res.json(); 66 + //set(cacheKey, JSON.stringify(data)); 67 setDid(data.did); 68 } catch (e: any) { 69 setError("Failed to resolve handle: " + (e?.message || e)); ··· 72 } 73 } 74 75 + const [constellationURL] = useAtom(constellationURLAtom) 76 + 77 useEffect(() => { 78 if (!did) return; 79 setLoading(true); 80 setError(null); 81 const urls = [ 82 + `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`, 83 + `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`, 84 + `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`, 85 ]; 86 let ignore = false; 87 Promise.all( ··· 99 } catch (e: any) { 100 return { error: e?.message || String(e) }; 101 } 102 + }) 103 ) 104 .then((results) => { 105 if (!ignore) setResponses(results);
+207 -58
src/routes/profile.$did/index.tsx
··· 1 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute, Link } from "@tanstack/react-router"; 3 - import React from "react"; 4 5 - import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 - import { toggleFollow, useGetFollowState } from "~/utils/followState"; 8 import { 9 - useInfiniteQueryAuthorFeed, 10 useQueryIdentity, 11 useQueryProfile, 12 } from "~/utils/useQuery"; ··· 18 function ProfileComponent() { 19 // booo bad this is not always the did it might be a handle, use identity.did instead 20 const { did } = Route.useParams(); 21 const queryClient = useQueryClient(); 22 - const { agent } = useAuth(); 23 const { 24 data: identity, 25 isLoading: isIdentityLoading, 26 error: identityError, 27 } = useQueryIdentity(did); 28 29 - const followRecords = useGetFollowState({ 30 - target: identity?.did || did, 31 - user: agent?.did, 32 - }); 33 - 34 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 35 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; 36 - const pdsUrl = identity?.pds; 37 38 const profileUri = resolvedDid 39 ? `at://${resolvedDid}/app.bsky.actor.profile/self` ··· 41 const { data: profileRecord } = useQueryProfile(profileUri); 42 const profile = profileRecord?.value; 43 44 const { 45 data: postsData, 46 fetchNextPage, 47 hasNextPage, 48 isFetchingNextPage, 49 isLoading: arePostsLoading, 50 - } = useInfiniteQueryAuthorFeed(resolvedDid, pdsUrl); 51 52 React.useEffect(() => { 53 if (postsData) { 54 postsData.pages.forEach((page) => { 55 - page.records.forEach((record) => { 56 if (!queryClient.getQueryData(["post", record.uri])) { 57 queryClient.setQueryData(["post", record.uri], record); 58 } ··· 62 }, [postsData, queryClient]); 63 64 const posts = React.useMemo( 65 - () => postsData?.pages.flatMap((page) => page.records) ?? [], 66 [postsData] 67 ); 68 69 function getAvatarUrl(p: typeof profile) { 70 const link = p?.avatar?.ref?.["$link"]; 71 if (!link || !resolvedDid) return null; 72 - return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 73 } 74 function getBannerUrl(p: typeof profile) { 75 const link = p?.banner?.ref?.["$link"]; 76 if (!link || !resolvedDid) return null; 77 - return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`; 78 } 79 80 const displayName = ··· 104 105 return ( 106 <> 107 - <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 108 <Link 109 to=".." 110 className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" ··· 121 โ† 122 </Link> 123 <span className="text-xl font-bold ml-2">Profile</span> 124 - </div> 125 126 {/* Profile Header */} 127 <div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900"> ··· 151 also delay the backfill to be on demand because it would be pretty intense 152 also save it persistently 153 */} 154 - {identity?.did !== agent?.did ? ( 155 - <> 156 - {!(followRecords?.length && followRecords?.length > 0) ? ( 157 - <button 158 - onClick={() => 159 - toggleFollow({ 160 - agent: agent || undefined, 161 - targetDid: identity?.did, 162 - followRecords: followRecords, 163 - queryClient: queryClient, 164 - }) 165 - } 166 - className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]" 167 - > 168 - Follow 169 - </button> 170 - ) : ( 171 - <button 172 - onClick={() => 173 - toggleFollow({ 174 - agent: agent || undefined, 175 - targetDid: identity?.did, 176 - followRecords: followRecords, 177 - queryClient: queryClient, 178 - }) 179 - } 180 - className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]" 181 - > 182 - Unfollow 183 - </button> 184 - )} 185 - </> 186 - ) : ( 187 - <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 188 - Edit Profile 189 - </button> 190 - )} 191 <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 192 ... {/* todo: icon */} 193 </button> ··· 196 {/* Info Card */} 197 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 198 <div className="font-bold text-2xl">{displayName}</div> 199 - <div className="text-gray-500 dark:text-gray-400 text-base mb-3"> 200 {handle} 201 </div> 202 {description && ( 203 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 204 - {description} 205 </div> 206 )} 207 </div> ··· 244 </> 245 ); 246 }
··· 1 + import { RichText } from "@atproto/api"; 2 import { useQueryClient } from "@tanstack/react-query"; 3 + import { createFileRoute, useNavigate } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 + import React, { type ReactNode, useEffect, useState } from "react"; 6 7 + import { Header } from "~/components/Header"; 8 + import { 9 + renderTextWithFacets, 10 + UniversalPostRendererATURILoader, 11 + } from "~/components/UniversalPostRenderer"; 12 import { useAuth } from "~/providers/UnifiedAuthProvider"; 13 + import { aturiListServiceAtom, imgCDNAtom } from "~/utils/atoms"; 14 import { 15 + toggleFollow, 16 + useGetFollowState, 17 + useGetOneToOneState, 18 + } from "~/utils/followState"; 19 + import { 20 + useInfiniteQueryAturiList, 21 useQueryIdentity, 22 useQueryProfile, 23 } from "~/utils/useQuery"; ··· 29 function ProfileComponent() { 30 // booo bad this is not always the did it might be a handle, use identity.did instead 31 const { did } = Route.useParams(); 32 + //const navigate = useNavigate(); 33 const queryClient = useQueryClient(); 34 const { 35 data: identity, 36 isLoading: isIdentityLoading, 37 error: identityError, 38 } = useQueryIdentity(did); 39 40 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 41 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; 42 + //const pdsUrl = identity?.pds; 43 44 const profileUri = resolvedDid 45 ? `at://${resolvedDid}/app.bsky.actor.profile/self` ··· 47 const { data: profileRecord } = useQueryProfile(profileUri); 48 const profile = profileRecord?.value; 49 50 + const [aturilistservice] = useAtom(aturiListServiceAtom); 51 + 52 const { 53 data: postsData, 54 fetchNextPage, 55 hasNextPage, 56 isFetchingNextPage, 57 isLoading: arePostsLoading, 58 + } = useInfiniteQueryAturiList({ 59 + aturilistservice: aturilistservice, 60 + did: resolvedDid, 61 + collection: "app.bsky.feed.post", 62 + reverse: true 63 + }); 64 65 React.useEffect(() => { 66 if (postsData) { 67 postsData.pages.forEach((page) => { 68 + page.forEach((record) => { 69 if (!queryClient.getQueryData(["post", record.uri])) { 70 queryClient.setQueryData(["post", record.uri], record); 71 } ··· 75 }, [postsData, queryClient]); 76 77 const posts = React.useMemo( 78 + () => postsData?.pages.flatMap((page) => page) ?? [], 79 [postsData] 80 ); 81 82 + const [imgcdn] = useAtom(imgCDNAtom); 83 + 84 function getAvatarUrl(p: typeof profile) { 85 const link = p?.avatar?.ref?.["$link"]; 86 if (!link || !resolvedDid) return null; 87 + return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 88 } 89 function getBannerUrl(p: typeof profile) { 90 const link = p?.banner?.ref?.["$link"]; 91 if (!link || !resolvedDid) return null; 92 + return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`; 93 } 94 95 const displayName = ··· 119 120 return ( 121 <> 122 + <Header 123 + title={`Profile`} 124 + backButtonCallback={() => { 125 + if (window.history.length > 1) { 126 + window.history.back(); 127 + } else { 128 + window.location.assign("/"); 129 + } 130 + }} 131 + /> 132 + {/* <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 133 <Link 134 to=".." 135 className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" ··· 146 โ† 147 </Link> 148 <span className="text-xl font-bold ml-2">Profile</span> 149 + </div> */} 150 151 {/* Profile Header */} 152 <div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900"> ··· 176 also delay the backfill to be on demand because it would be pretty intense 177 also save it persistently 178 */} 179 + <FollowButton targetdidorhandle={did} /> 180 <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 181 ... {/* todo: icon */} 182 </button> ··· 185 {/* Info Card */} 186 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 187 <div className="font-bold text-2xl">{displayName}</div> 188 + <div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1"> 189 + <Mutual targetdidorhandle={did} /> 190 {handle} 191 </div> 192 {description && ( 193 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 194 + {/* {description} */} 195 + <RichTextRenderer key={did} description={description} /> 196 </div> 197 )} 198 </div> ··· 235 </> 236 ); 237 } 238 + 239 + export function FollowButton({ 240 + targetdidorhandle, 241 + }: { 242 + targetdidorhandle: string; 243 + }) { 244 + const { agent } = useAuth(); 245 + const { data: identity } = useQueryIdentity(targetdidorhandle); 246 + const queryClient = useQueryClient(); 247 + 248 + const followRecords = useGetFollowState({ 249 + target: identity?.did ?? targetdidorhandle, 250 + user: agent?.did, 251 + }); 252 + 253 + return ( 254 + <> 255 + {identity?.did !== agent?.did ? ( 256 + <> 257 + {!(followRecords?.length && followRecords?.length > 0) ? ( 258 + <button 259 + onClick={(e) => { 260 + e.stopPropagation(); 261 + toggleFollow({ 262 + agent: agent || undefined, 263 + targetDid: identity?.did, 264 + followRecords: followRecords, 265 + queryClient: queryClient, 266 + }); 267 + }} 268 + className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 269 + > 270 + Follow 271 + </button> 272 + ) : ( 273 + <button 274 + onClick={(e) => { 275 + e.stopPropagation(); 276 + toggleFollow({ 277 + agent: agent || undefined, 278 + targetDid: identity?.did, 279 + followRecords: followRecords, 280 + queryClient: queryClient, 281 + }); 282 + }} 283 + className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 284 + > 285 + Unfollow 286 + </button> 287 + )} 288 + </> 289 + ) : ( 290 + <button className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"> 291 + Edit Profile 292 + </button> 293 + )} 294 + </> 295 + ); 296 + } 297 + 298 + export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { 299 + const { agent } = useAuth(); 300 + const { data: identity } = useQueryIdentity(targetdidorhandle); 301 + 302 + const theyFollowYouRes = useGetOneToOneState( 303 + agent?.did 304 + ? { 305 + target: agent?.did, 306 + user: identity?.did ?? targetdidorhandle, 307 + collection: "app.bsky.graph.follow", 308 + path: ".subject", 309 + } 310 + : undefined 311 + ); 312 + 313 + const youFollowThemRes = useGetFollowState({ 314 + target: identity?.did ?? targetdidorhandle, 315 + user: agent?.did, 316 + }); 317 + 318 + const theyFollowYou: boolean = 319 + !!theyFollowYouRes?.length && theyFollowYouRes.length > 0; 320 + const youFollowThem: boolean = 321 + !!youFollowThemRes?.length && youFollowThemRes.length > 0; 322 + 323 + return ( 324 + <> 325 + {/* if not self */} 326 + {identity?.did !== agent?.did ? ( 327 + <> 328 + {theyFollowYou ? ( 329 + <> 330 + {youFollowThem ? ( 331 + <div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center"> 332 + mutuals 333 + </div> 334 + ) : ( 335 + <div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center"> 336 + follows you 337 + </div> 338 + )} 339 + </> 340 + ) : ( 341 + <></> 342 + )} 343 + </> 344 + ) : ( 345 + // lmao can someone be mutuals with themselves ?? 346 + <></> 347 + )} 348 + </> 349 + ); 350 + } 351 + 352 + export function RichTextRenderer({ description }: { description: string }) { 353 + const [richDescription, setRichDescription] = useState<string | ReactNode[]>( 354 + description 355 + ); 356 + const { agent } = useAuth(); 357 + const navigate = useNavigate(); 358 + 359 + useEffect(() => { 360 + let mounted = true; 361 + 362 + // setRichDescription(description); 363 + 364 + async function processRichText() { 365 + try { 366 + if (!agent?.did) return; 367 + const rt = new RichText({ text: description }); 368 + await rt.detectFacets(agent); 369 + 370 + if (!mounted) return; 371 + 372 + if (rt.facets) { 373 + setRichDescription( 374 + renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate }) 375 + ); 376 + } else { 377 + setRichDescription(rt.text); 378 + } 379 + } catch (error) { 380 + console.error("Failed to detect facets:", error); 381 + if (mounted) { 382 + setRichDescription(description); 383 + } 384 + } 385 + } 386 + 387 + processRichText(); 388 + 389 + return () => { 390 + mounted = false; 391 + }; 392 + }, [description, agent, navigate]); 393 + 394 + return <>{richDescription}</>; 395 + }
+165
src/routes/profile.$did/post.$rkey.image.$i.tsx
···
··· 1 + import { 2 + createFileRoute, 3 + useNavigate, 4 + type UseNavigateResult, 5 + } from "@tanstack/react-router"; 6 + import { useEffect, useState } from "react"; 7 + import { createPortal } from "react-dom"; 8 + 9 + import { ProfilePostComponent } from "./post.$rkey"; 10 + 11 + export const Route = createFileRoute("/profile/$did/post/$rkey/image/$i")({ 12 + component: Lightbox, 13 + }); 14 + 15 + export type LightboxProps = { 16 + images: { src: string; alt?: string }[]; 17 + }; 18 + 19 + function nextprev({ 20 + index, 21 + images, 22 + navigate, 23 + did, 24 + rkey, 25 + prev, 26 + }: { 27 + index?: number; 28 + images?: LightboxProps["images"]; 29 + navigate: UseNavigateResult<string>; 30 + did: string; 31 + rkey: string; 32 + prev?: boolean; 33 + }) { 34 + const len = images?.length ?? 0; 35 + if (len === 0) return; 36 + 37 + const nextIndex = ((index ?? 0) + (prev ? -1 : 1) + len) % len; 38 + 39 + navigate({ 40 + to: "/profile/$did/post/$rkey/image/$i", 41 + params: { 42 + did, 43 + rkey, 44 + i: nextIndex.toString(), 45 + }, 46 + replace: true, 47 + }); 48 + } 49 + 50 + export function Lightbox() { 51 + console.log("hey the $i route is loaded w!!!"); 52 + const { did, rkey, i } = Route.useParams(); 53 + const [images, setImages] = useState<LightboxProps["images"] | undefined>( 54 + undefined 55 + ); 56 + const index = Number(i); 57 + const navigate = useNavigate(); 58 + const post = true; 59 + const image = images?.[index] ?? undefined; 60 + 61 + function lightboxCallback(d: LightboxProps) { 62 + console.log("callback actually called!"); 63 + setImages(d.images); 64 + } 65 + 66 + useEffect(() => { 67 + function handleKey(e: KeyboardEvent) { 68 + if (e.key === "Escape") window.history.back(); 69 + if (e.key === "ArrowRight") 70 + nextprev({ index, images, navigate, did, rkey }); 71 + //onNavigate((index + 1) % images.length); 72 + if (e.key === "ArrowLeft") 73 + nextprev({ index, images, navigate, did, rkey, prev: true }); 74 + //onNavigate((index - 1 + images.length) % images.length); 75 + } 76 + window.addEventListener("keydown", handleKey); 77 + return () => window.removeEventListener("keydown", handleKey); 78 + }, [index, navigate, did, rkey, images]); 79 + 80 + return createPortal( 81 + <> 82 + {post && ( 83 + <div 84 + onClick={(e) => { 85 + e.stopPropagation(); 86 + e.nativeEvent.stopImmediatePropagation(); 87 + }} 88 + className="lightbox-sidebar hidden lg:flex overscroll-none disablegutter border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white" 89 + > 90 + <ProfilePostComponent 91 + key={`/profile/${did}/post/${rkey}`} 92 + did={did} 93 + rkey={rkey} 94 + nopics 95 + lightboxCallback={lightboxCallback} 96 + /> 97 + </div> 98 + )} 99 + <div 100 + className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px)] lg:max-w-[calc(100vw-350px)]" 101 + onClick={(e) => { 102 + e.stopPropagation(); 103 + window.history.back(); 104 + }} 105 + > 106 + <img 107 + src={image?.src} 108 + alt={image?.alt} 109 + className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg" 110 + onClick={(e) => e.stopPropagation()} 111 + /> 112 + 113 + {(images?.length ?? 0) > 1 && ( 114 + <> 115 + <button 116 + onClick={(e) => { 117 + e.stopPropagation(); 118 + nextprev({ index, images, navigate, did, rkey, prev: true }); 119 + }} 120 + className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 121 + > 122 + <svg 123 + xmlns="http://www.w3.org/2000/svg" 124 + width={28} 125 + height={28} 126 + viewBox="0 0 24 24" 127 + > 128 + <g fill="none" fillRule="evenodd"> 129 + <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 130 + <path 131 + fill="currentColor" 132 + d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z" 133 + ></path> 134 + </g> 135 + </svg> 136 + </button> 137 + <button 138 + onClick={(e) => { 139 + e.stopPropagation(); 140 + nextprev({ index, images, navigate, did, rkey }); 141 + }} 142 + className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 143 + > 144 + <svg 145 + xmlns="http://www.w3.org/2000/svg" 146 + width={28} 147 + height={28} 148 + viewBox="0 0 24 24" 149 + > 150 + <g fill="none" fillRule="evenodd"> 151 + <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 152 + <path 153 + fill="currentColor" 154 + d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z" 155 + ></path> 156 + </g> 157 + </svg> 158 + </button> 159 + </> 160 + )} 161 + </div> 162 + </>, 163 + document.body 164 + ); 165 + }
+238 -68
src/routes/profile.$did/post.$rkey.tsx
··· 1 - import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute, Link } from "@tanstack/react-router"; 3 import React, { useLayoutEffect } from "react"; 4 5 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 6 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 7 import { 8 constructPostQuery, 9 useQueryConstellation, 10 useQueryIdentity, 11 useQueryPost, 12 } from "~/utils/useQuery"; 13 14 //const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 15 ··· 32 ); 33 } 34 35 - function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { 36 //const { get, set } = usePersistentStore(); 37 const queryClient = useQueryClient(); 38 // const [resolvedDid, setResolvedDid] = React.useState<string | null>(null); ··· 179 () => 180 resolvedDid 181 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 182 - : "", 183 [resolvedDid, rkey] 184 ); 185 186 const { data: mainPost } = useQueryPost(atUri); 187 188 - const { data: repliesData } = useQueryConstellation({ 189 - method: "/links", 190 target: atUri, 191 - collection: "app.bsky.feed.post", 192 - path: ".reply.parent.uri", 193 }); 194 - const replies = repliesData?.linking_records.slice(0, 50) ?? []; 195 196 const [parents, setParents] = React.useState<any[]>([]); 197 const [parentsLoading, setParentsLoading] = React.useState(false); 198 199 const mainPostRef = React.useRef<HTMLDivElement>(null); 200 - const userHasScrolled = React.useRef(false); 201 202 - const scrollAnchor = React.useRef<{ top: number } | null>(null); 203 204 205 - React.useEffect(() => { 206 - const onScroll = () => { 207 208 - if (window.scrollY > 50) { 209 - userHasScrolled.current = true; 210 211 - window.removeEventListener("scroll", onScroll); 212 } 213 - }; 214 - 215 - if (!userHasScrolled.current) { 216 - window.addEventListener("scroll", onScroll, { passive: true }); 217 } 218 - return () => window.removeEventListener("scroll", onScroll); 219 - }, []); 220 221 - useLayoutEffect(() => { 222 - if (parentsLoading && mainPostRef.current && !userHasScrolled.current) { 223 - scrollAnchor.current = { 224 - top: mainPostRef.current.getBoundingClientRect().top, 225 - }; 226 } 227 - }, [parentsLoading]); 228 229 - useLayoutEffect(() => { 230 - if ( 231 - scrollAnchor.current && 232 - mainPostRef.current && 233 - !userHasScrolled.current 234 - ) { 235 - const newTop = mainPostRef.current.getBoundingClientRect().top; 236 - const topDiff = newTop - scrollAnchor.current.top; 237 - if (topDiff > 0) { 238 - window.scrollBy(0, topDiff); 239 - } 240 - scrollAnchor.current = null; 241 } 242 - }, [parents]); 243 244 React.useEffect(() => { 245 if (!mainPost?.value?.reply?.parent?.uri) { ··· 258 while (currentParentUri && safetyCounter < MAX_PARENTS) { 259 try { 260 const parentPost = await queryClient.fetchQuery( 261 - constructPostQuery(currentParentUri) 262 ); 263 if (!parentPost) break; 264 parentChain.push(parentPost); ··· 290 291 return ( 292 <> 293 - <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 294 - <Link 295 - to=".." 296 - className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 297 - onClick={(e) => { 298 - e.preventDefault(); 299 - if (window.history.length > 1) { 300 - window.history.back(); 301 - } else { 302 - window.location.assign("/"); 303 - } 304 - }} 305 - aria-label="Go back" 306 - > 307 - โ† 308 - </Link> 309 - <span className="text-xl font-bold ml-2">Post</span> 310 - </div> 311 312 {parentsLoading && ( 313 <div className="text-center text-gray-500 dark:text-gray-400 flex flex-row"> ··· 322 )} 323 324 {/* we should use the reply lines here thats provided by UPR*/} 325 - <div style={{ maxWidth: 600, margin: "0px auto 0", padding: 0 }}> 326 {parents.map((parent, index) => ( 327 <UniversalPostRendererATURILoader 328 key={parent.uri} ··· 338 atUri={atUri} 339 detailed={true} 340 topReplyLine={parentsLoading || parents.length > 0} 341 /> 342 </div> 343 <div 344 style={{ 345 maxWidth: 600, 346 - margin: "0px auto 0", 347 padding: 0, 348 - minHeight: "100dvh", 349 }} 350 > 351 <div ··· 359 Replies 360 </div> 361 <div style={{ display: "flex", flexDirection: "column", gap: 0 }}> 362 - {replies.length > 0 && 363 - replies.map((reply) => { 364 - const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; 365 return ( 366 <UniversalPostRendererATURILoader 367 - key={replyAtUri} 368 - atUri={replyAtUri} 369 /> 370 ); 371 })} 372 </div> 373 </div> 374 </>
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 3 + import { createFileRoute, Outlet } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 import React, { useLayoutEffect } from "react"; 6 7 + import { Header } from "~/components/Header"; 8 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 9 + import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms"; 10 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 11 import { 12 constructPostQuery, 13 + type linksAllResponse, 14 + type linksRecordsResponse, 15 useQueryConstellation, 16 useQueryIdentity, 17 useQueryPost, 18 + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 19 } from "~/utils/useQuery"; 20 + 21 + import type { LightboxProps } from "./post.$rkey.image.$i"; 22 23 //const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 24 ··· 41 ); 42 } 43 44 + export function ProfilePostComponent({ 45 + did, 46 + rkey, 47 + nopics, 48 + lightboxCallback, 49 + }: { 50 + did: string; 51 + rkey: string; 52 + nopics?: boolean; 53 + lightboxCallback?: (d: LightboxProps) => void; 54 + }) { 55 //const { get, set } = usePersistentStore(); 56 const queryClient = useQueryClient(); 57 // const [resolvedDid, setResolvedDid] = React.useState<string | null>(null); ··· 198 () => 199 resolvedDid 200 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 201 + : undefined, 202 [resolvedDid, rkey] 203 ); 204 205 const { data: mainPost } = useQueryPost(atUri); 206 207 + console.log("atUri",atUri) 208 + 209 + const opdid = React.useMemo( 210 + () => 211 + atUri 212 + ? new AtUri(atUri).host 213 + : undefined, 214 + [atUri] 215 + ); 216 + 217 + // @ts-expect-error i hate overloads 218 + const { data: links } = useQueryConstellation(atUri?{ 219 + method: "/links/all", 220 target: atUri, 221 + } : { 222 + method: "undefined", 223 + target: "" 224 + })as { data: linksAllResponse | undefined }; 225 + 226 + //const [likes, setLikes] = React.useState<number | null>(null); 227 + //const [reposts, setReposts] = React.useState<number | null>(null); 228 + const [replyCount, setReplyCount] = React.useState<number | null>(null); 229 + 230 + React.useEffect(() => { 231 + // /*mass comment*/ console.log(JSON.stringify(links, null, 2)); 232 + // setLikes( 233 + // links 234 + // ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0 235 + // : null 236 + // ); 237 + // setReposts( 238 + // links 239 + // ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0 240 + // : null 241 + // ); 242 + setReplyCount( 243 + links 244 + ? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"] 245 + ?.records || 0 246 + : null 247 + ); 248 + }, [links]); 249 + 250 + const { data: opreplies } = useQueryConstellation( 251 + !!opdid && replyCount && replyCount >= 25 252 + ? { 253 + method: "/links", 254 + target: atUri, 255 + // @ts-expect-error overloading sucks so much 256 + collection: "app.bsky.feed.post", 257 + path: ".reply.parent.uri", 258 + //cursor?: string; 259 + dids: [opdid], 260 + } 261 + : { 262 + method: "undefined", 263 + target: "", 264 + } 265 + ) as { data: linksRecordsResponse | undefined }; 266 + 267 + const opReplyAturis = 268 + opreplies?.linking_records.map( 269 + (r) => `at://${r.did}/${r.collection}/${r.rkey}`, 270 + ) ?? []; 271 + 272 + 273 + // const { data: repliesData } = useQueryConstellation({ 274 + // method: "/links", 275 + // target: atUri, 276 + // collection: "app.bsky.feed.post", 277 + // path: ".reply.parent.uri", 278 + // }); 279 + // const replies = repliesData?.linking_records.slice(0, 50) ?? []; 280 + const [constellationurl] = useAtom(constellationURLAtom) 281 + 282 + const infinitequeryresults = useInfiniteQuery({ 283 + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 284 + { 285 + constellation: constellationurl, 286 + method: "/links", 287 + target: atUri, 288 + collection: "app.bsky.feed.post", 289 + path: ".reply.parent.uri", 290 + } 291 + ), 292 + enabled: !!atUri, 293 }); 294 + 295 + const { 296 + data: infiniteRepliesData, 297 + fetchNextPage, 298 + hasNextPage, 299 + isFetchingNextPage, 300 + } = infinitequeryresults; 301 + 302 + // // auto-fetch all pages 303 + // useEffect(() => { 304 + // if ( 305 + // infinitequeryresults.hasNextPage && 306 + // !infinitequeryresults.isFetchingNextPage 307 + // ) { 308 + // console.log("Fetching the next page..."); 309 + // infinitequeryresults.fetchNextPage(); 310 + // } 311 + // }, [infinitequeryresults]); 312 + 313 + // const replyAturis = repliesData 314 + // ? repliesData.pages.flatMap((page) => 315 + // page 316 + // ? page.linking_records.map((record) => { 317 + // const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 318 + // return aturi; 319 + // }) 320 + // : [] 321 + // ) 322 + // : []; 323 + 324 + const replyAturis = React.useMemo(() => { 325 + // Get all replies from the standard infinite query 326 + const allReplies = 327 + infiniteRepliesData?.pages.flatMap( 328 + (page) => 329 + page?.linking_records.map( 330 + (r) => `at://${r.did}/${r.collection}/${r.rkey}`, 331 + ) ?? [], 332 + ) ?? []; 333 + 334 + if (replyCount && (replyCount < 25)) { 335 + // If count is low, just use the standard list and find the oldest OP reply to move to the top 336 + const opdidFromUri = atUri ? new AtUri(atUri).host : undefined; 337 + const oldestOpsIndex = allReplies.findIndex( 338 + (aturi) => new AtUri(aturi).host === opdidFromUri, 339 + ); 340 + if (oldestOpsIndex > 0) { 341 + const [oldestOpsReply] = allReplies.splice(oldestOpsIndex, 1); 342 + allReplies.unshift(oldestOpsReply); 343 + } 344 + return allReplies; 345 + } else { 346 + // If count is high, prioritize OP replies from the special query 347 + // and filter them out from the main list to avoid duplication. 348 + const opReplySet = new Set(opReplyAturis); 349 + const otherReplies = allReplies.filter((uri) => !opReplySet.has(uri)); 350 + return [...opReplyAturis, ...otherReplies]; 351 + } 352 + }, [infiniteRepliesData, opReplyAturis, replyCount, atUri]); 353 + 354 + // Find oldest OP reply 355 + const oldestOpsIndex = replyAturis.findIndex( 356 + (aturi) => new AtUri(aturi).host === opdid 357 + ); 358 + 359 + // Reorder: move oldest OP reply to the front 360 + if (oldestOpsIndex > 0) { 361 + const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1); 362 + replyAturis.unshift(oldestOpsReply); 363 + } 364 365 const [parents, setParents] = React.useState<any[]>([]); 366 const [parentsLoading, setParentsLoading] = React.useState(false); 367 368 const mainPostRef = React.useRef<HTMLDivElement>(null); 369 + const hasPerformedInitialLayout = React.useRef(false); 370 371 + const [layoutReady, setLayoutReady] = React.useState(false); 372 + 373 + useLayoutEffect(() => { 374 + if (parents.length > 0 && !layoutReady && mainPostRef.current) { 375 + const mainPostElement = mainPostRef.current; 376 377 + if (window.scrollY === 0 && !hasPerformedInitialLayout.current) { 378 + const elementTop = mainPostElement.getBoundingClientRect().top; 379 + const headerOffset = 70; 380 381 + const targetScrollY = elementTop - headerOffset; 382 383 + window.scrollBy(0, targetScrollY); 384 385 + hasPerformedInitialLayout.current = true; 386 } 387 + 388 + // todo idk what to do with this 389 + // eslint-disable-next-line react-hooks/set-state-in-effect 390 + setLayoutReady(true); 391 } 392 + }, [parents, layoutReady]); 393 394 + 395 + const [slingshoturl] = useAtom(slingshotURLAtom) 396 + 397 + React.useEffect(() => { 398 + if (parentsLoading) { 399 + setLayoutReady(false); 400 } 401 402 + if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) { 403 + setLayoutReady(true); 404 + hasPerformedInitialLayout.current = true; 405 } 406 + }, [parentsLoading, mainPost]); 407 408 React.useEffect(() => { 409 if (!mainPost?.value?.reply?.parent?.uri) { ··· 422 while (currentParentUri && safetyCounter < MAX_PARENTS) { 423 try { 424 const parentPost = await queryClient.fetchQuery( 425 + constructPostQuery(currentParentUri, slingshoturl) 426 ); 427 if (!parentPost) break; 428 parentChain.push(parentPost); ··· 454 455 return ( 456 <> 457 + <Outlet /> 458 + <Header 459 + title={`Post`} 460 + backButtonCallback={() => { 461 + if (window.history.length > 1) { 462 + window.history.back(); 463 + } else { 464 + window.location.assign("/"); 465 + } 466 + }} 467 + /> 468 469 {parentsLoading && ( 470 <div className="text-center text-gray-500 dark:text-gray-400 flex flex-row"> ··· 479 )} 480 481 {/* we should use the reply lines here thats provided by UPR*/} 482 + <div style={{ maxWidth: 600, padding: 0 }}> 483 {parents.map((parent, index) => ( 484 <UniversalPostRendererATURILoader 485 key={parent.uri} ··· 495 atUri={atUri} 496 detailed={true} 497 topReplyLine={parentsLoading || parents.length > 0} 498 + nopics={!!nopics} 499 + lightboxCallback={lightboxCallback} 500 /> 501 </div> 502 <div 503 style={{ 504 maxWidth: 600, 505 + //margin: "0px auto 0", 506 padding: 0, 507 + minHeight: "80dvh", 508 + paddingBottom: "20dvh", 509 }} 510 > 511 <div ··· 519 Replies 520 </div> 521 <div style={{ display: "flex", flexDirection: "column", gap: 0 }}> 522 + {replyAturis.length > 0 && 523 + replyAturis.map((reply) => { 524 + //const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; 525 return ( 526 <UniversalPostRendererATURILoader 527 + key={reply} 528 + atUri={reply} 529 + maxReplies={4} 530 /> 531 ); 532 })} 533 + {hasNextPage && ( 534 + <button 535 + onClick={() => fetchNextPage()} 536 + disabled={isFetchingNextPage} 537 + className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50" 538 + > 539 + {isFetchingNextPage ? "Loading..." : "Load More"} 540 + </button> 541 + )} 542 </div> 543 </div> 544 </>
+50 -1
src/routes/search.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 3 export const Route = createFileRoute("/search")({ 4 component: Search, 5 }); 6 7 export function Search() { 8 - return <div className="p-6">Search page (coming soon)</div>; 9 }
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 3 + import { Header } from "~/components/Header"; 4 + import { Import } from "~/components/Import"; 5 + 6 export const Route = createFileRoute("/search")({ 7 component: Search, 8 }); 9 10 export function Search() { 11 + return ( 12 + <> 13 + <Header 14 + title="Explore" 15 + backButtonCallback={() => { 16 + if (window.history.length > 1) { 17 + window.history.back(); 18 + } else { 19 + window.location.assign("/"); 20 + } 21 + }} 22 + /> 23 + <div className=" flex flex-col items-center mt-4 mx-4 gap-4"> 24 + <Import /> 25 + <div className="flex flex-col"> 26 + <p className="text-gray-600 dark:text-gray-400"> 27 + Sorry we dont have search. But instead, you can load some of these 28 + types of content into Red Dwarf: 29 + </p> 30 + <ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400"> 31 + <li> 32 + Bluesky URLs from supported clients (like{" "} 33 + <code className="text-sm">bsky.app</code> or{" "} 34 + <code className="text-sm">deer.social</code>). 35 + </li> 36 + <li> 37 + AT-URIs (e.g.,{" "} 38 + <code className="text-sm">at://did:example/collection/item</code> 39 + ). 40 + </li> 41 + <li> 42 + Plain handles (like{" "} 43 + <code className="text-sm">@username.bsky.social</code>). 44 + </li> 45 + <li> 46 + Direct DIDs (Decentralized Identifiers, starting with{" "} 47 + <code className="text-sm">did:</code>). 48 + </li> 49 + </ul> 50 + <p className="mt-2 text-gray-600 dark:text-gray-400"> 51 + Simply paste one of these into the import field above and press 52 + Enter to load the content. 53 + </p> 54 + </div> 55 + </div> 56 + </> 57 + ); 58 }
+189 -1
src/routes/settings.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 3 export const Route = createFileRoute("/settings")({ 4 component: Settings, 5 }); 6 7 export function Settings() { 8 - return <div className="p-6">Settings page (coming soon)</div>; 9 }
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + import { Slider } from "radix-ui"; 4 + 5 + import { Header } from "~/components/Header"; 6 + import Login from "~/components/Login"; 7 + import { 8 + aturiListServiceAtom, 9 + constellationURLAtom, 10 + defaultaturilistservice, 11 + defaultconstellationURL, 12 + defaulthue, 13 + defaultImgCDN, 14 + defaultslingshotURL, 15 + defaultVideoCDN, 16 + hueAtom, 17 + imgCDNAtom, 18 + slingshotURLAtom, 19 + videoCDNAtom, 20 + } from "~/utils/atoms"; 21 22 export const Route = createFileRoute("/settings")({ 23 component: Settings, 24 }); 25 26 export function Settings() { 27 + return ( 28 + <> 29 + <Header 30 + title="Settings" 31 + backButtonCallback={() => { 32 + if (window.history.length > 1) { 33 + window.history.back(); 34 + } else { 35 + window.location.assign("/"); 36 + } 37 + }} 38 + /> 39 + <div className="lg:hidden"> 40 + <Login /> 41 + </div> 42 + <div className="h-4" /> 43 + <TextInputSetting 44 + atom={constellationURLAtom} 45 + title={"Constellation"} 46 + description={ 47 + "Customize the Constellation instance to be used by Red Dwarf" 48 + } 49 + init={defaultconstellationURL} 50 + /> 51 + <TextInputSetting 52 + atom={slingshotURLAtom} 53 + title={"Slingshot"} 54 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 55 + init={defaultslingshotURL} 56 + /> 57 + <TextInputSetting 58 + atom={aturiListServiceAtom} 59 + title={"AtUriListService"} 60 + description={"Customize the AtUriListService instance to be used by Red Dwarf"} 61 + init={defaultaturilistservice} 62 + /> 63 + <TextInputSetting 64 + atom={imgCDNAtom} 65 + title={"Image CDN"} 66 + description={ 67 + "Customize the Constellation instance to be used by Red Dwarf" 68 + } 69 + init={defaultImgCDN} 70 + /> 71 + <TextInputSetting 72 + atom={videoCDNAtom} 73 + title={"Video CDN"} 74 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 75 + init={defaultVideoCDN} 76 + /> 77 + 78 + <Hue /> 79 + <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm"> 80 + please restart/refresh the app if changes arent applying correctly 81 + </p> 82 + </> 83 + ); 84 } 85 + function Hue() { 86 + const [hue, setHue] = useAtom(hueAtom); 87 + return ( 88 + <div className="flex flex-col px-4 mt-4 "> 89 + <span className="z-10">Hue</span> 90 + <div className="flex flex-row items-center gap-4"> 91 + <SliderComponent 92 + atom={hueAtom} 93 + max={360} 94 + /> 95 + <button 96 + onClick={() => setHue(defaulthue ?? 28)} 97 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 98 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 99 + > 100 + Reset 101 + </button> 102 + </div> 103 + </div> 104 + ); 105 + } 106 + 107 + export function TextInputSetting({ 108 + atom, 109 + title, 110 + description, 111 + init, 112 + }: { 113 + atom: typeof constellationURLAtom; 114 + title?: string; 115 + description?: string; 116 + init?: string; 117 + }) { 118 + const [value, setValue] = useAtom(atom); 119 + return ( 120 + <div className="flex flex-col gap-2 px-4 py-2"> 121 + {/* <div> 122 + {title && ( 123 + <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> 124 + {title} 125 + </h3> 126 + )} 127 + {description && ( 128 + <p className="text-sm text-gray-500 dark:text-gray-400"> 129 + {description} 130 + </p> 131 + )} 132 + </div> */} 133 + 134 + <div className="flex flex-row gap-2 items-center"> 135 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 136 + <input 137 + type="text" 138 + placeholder=" " 139 + value={value} 140 + onChange={(e) => setValue(e.target.value)} 141 + /> 142 + <label>{title}</label> 143 + </div> 144 + {/* <input 145 + type="text" 146 + value={value} 147 + onChange={(e) => setValue(e.target.value)} 148 + className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 149 + text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 150 + focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600" 151 + placeholder="Enter value..." 152 + /> */} 153 + <button 154 + onClick={() => setValue(init ?? "")} 155 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 156 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 157 + > 158 + Reset 159 + </button> 160 + </div> 161 + </div> 162 + ); 163 + } 164 + 165 + 166 + interface SliderProps { 167 + atom: typeof hueAtom; 168 + min?: number; 169 + max?: number; 170 + step?: number; 171 + } 172 + 173 + export const SliderComponent: React.FC<SliderProps> = ({ 174 + atom, 175 + min = 0, 176 + max = 100, 177 + step = 1, 178 + }) => { 179 + 180 + const [value, setValue] = useAtom(atom) 181 + 182 + return ( 183 + <Slider.Root 184 + className="relative flex items-center w-full h-4" 185 + value={[value]} 186 + min={min} 187 + max={max} 188 + step={step} 189 + onValueChange={(v: number[]) => setValue(v[0])} 190 + > 191 + <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 192 + <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 193 + </Slider.Track> 194 + <Slider.Thumb className="shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" /> 195 + </Slider.Root> 196 + ); 197 + };
+180 -13
src/styles/app.css
··· 1 @import "tailwindcss"; 2 3 /* @theme { ··· 14 --color-gray-950: oklch(0.129 0.050 222.000); 15 } */ 16 17 @theme { 18 - --color-gray-50: oklch(0.984 0.012 28); 19 - --color-gray-100: oklch(0.968 0.017 28); 20 - --color-gray-200: oklch(0.929 0.025 28); 21 - --color-gray-300: oklch(0.869 0.035 28); 22 - --color-gray-400: oklch(0.704 0.05 28); 23 - --color-gray-500: oklch(0.554 0.06 28); 24 - --color-gray-600: oklch(0.446 0.058 28); 25 - --color-gray-700: oklch(0.372 0.058 28); 26 - --color-gray-800: oklch(0.279 0.055 28); 27 - --color-gray-900: oklch(0.208 0.055 28); 28 - --color-gray-950: oklch(0.129 0.055 28); 29 } 30 31 @layer base { ··· 48 } 49 50 @media (width >= 64rem /* 1024px */) { 51 - html, 52 - body { 53 scrollbar-gutter: stable both-edges !important; 54 } 55 } 56 .scroll-thin { 57 scrollbar-width: thin; 58 /*scrollbar-gutter: stable both-edges !important;*/ ··· 61 .scroll-none { 62 scrollbar-width: none; 63 }
··· 1 + @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Spectral+SC:wght@500&display=swap'); 2 @import "tailwindcss"; 3 4 /* @theme { ··· 15 --color-gray-950: oklch(0.129 0.050 222.000); 16 } */ 17 18 + :root { 19 + --safe-hue: var(--tw-gray-hue, 28) 20 + } 21 + 22 @theme { 23 + --color-gray-50: oklch(0.984 0.012 var(--safe-hue)); 24 + --color-gray-100: oklch(0.968 0.017 var(--safe-hue)); 25 + --color-gray-200: oklch(0.929 0.025 var(--safe-hue)); 26 + --color-gray-300: oklch(0.869 0.035 var(--safe-hue)); 27 + --color-gray-400: oklch(0.704 0.05 var(--safe-hue)); 28 + --color-gray-500: oklch(0.554 0.06 var(--safe-hue)); 29 + --color-gray-600: oklch(0.446 0.058 var(--safe-hue)); 30 + --color-gray-700: oklch(0.372 0.058 var(--safe-hue)); 31 + --color-gray-800: oklch(0.279 0.055 var(--safe-hue)); 32 + --color-gray-900: oklch(0.208 0.055 var(--safe-hue)); 33 + --color-gray-950: oklch(0.129 0.055 var(--safe-hue)); 34 } 35 36 @layer base { ··· 53 } 54 55 @media (width >= 64rem /* 1024px */) { 56 + html:not(:has(.disablegutter)), 57 + body:not(:has(.disablegutter)) { 58 scrollbar-gutter: stable both-edges !important; 59 } 60 + html:has(.disablegutter), 61 + body:has(.disablegutter) { 62 + scrollbar-width: none; 63 + overflow-y: hidden; 64 + } 65 } 66 + 67 + .lightbox:has(+.lightbox-sidebar){ 68 + opacity: 0; 69 + } 70 + 71 .scroll-thin { 72 scrollbar-width: thin; 73 /*scrollbar-gutter: stable both-edges !important;*/ ··· 76 .scroll-none { 77 scrollbar-width: none; 78 } 79 + 80 + .dangerousFediContent { 81 + & a[href]{ 82 + text-decoration: none; 83 + color: rgb(29, 122, 242); 84 + word-break: break-all; 85 + } 86 + } 87 + 88 + .font-inter { 89 + font-family: "Inter", sans-serif; 90 + } 91 + .font-roboto { 92 + font-family: "Roboto", sans-serif; 93 + } 94 + 95 + :root { 96 + --header-bg-light: color-mix(in srgb, var(--color-white) calc(var(--is-top) * 100%), var(--color-gray-50)); 97 + --header-bg-dark: color-mix(in srgb, var(--color-gray-950) calc(var(--is-top) * 100%), var(--color-gray-900)); 98 + } 99 + 100 + :root { 101 + --header-bg: var(--header-bg-light); 102 + } 103 + @media (prefers-color-scheme: dark) { 104 + :root { 105 + --header-bg: var(--header-bg-dark); 106 + } 107 + } 108 + 109 + :root { 110 + --shadow-opacity: calc(1 - var(--is-top)); 111 + --tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15)); 112 + } 113 + 114 + 115 + /* m3 input */ 116 + :root { 117 + --m3input-radius: 6px; 118 + --m3input-border-width: .0625rem; 119 + --m3input-font-size: 16px; 120 + --m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1); 121 + /* light theme */ 122 + --m3input-bg: var(--color-gray-50); 123 + --m3input-border-color: var(--color-gray-400); 124 + --m3input-label-color: var(--color-gray-500); 125 + --m3input-text-color: var(--color-gray-900); 126 + --m3input-focus-color: var(--color-gray-600); 127 + } 128 + 129 + @media (prefers-color-scheme: dark) { 130 + :root { 131 + --m3input-bg: var(--color-gray-950); 132 + --m3input-border-color: var(--color-gray-700); 133 + --m3input-label-color: var(--color-gray-400); 134 + --m3input-text-color: var(--color-gray-50); 135 + --m3input-focus-color: var(--color-gray-400); 136 + } 137 + } 138 + 139 + /* reset page *//* 140 + html, 141 + body { 142 + background: var(--m3input-bg); 143 + margin: 0; 144 + padding: 1rem; 145 + color: var(--m3input-text-color); 146 + font-family: system-ui, sans-serif; 147 + font-size: var(--m3input-font-size); 148 + }*/ 149 + 150 + /* base wrapper */ 151 + .m3input-field.m3input-label.m3input-border { 152 + position: relative; 153 + display: inline-block; 154 + width: 100%; 155 + /*max-width: 400px;*/ 156 + } 157 + 158 + /* size variants */ 159 + .m3input-field.size-sm { 160 + --m3input-h: 40px; 161 + } 162 + 163 + .m3input-field.size-md { 164 + --m3input-h: 48px; 165 + } 166 + 167 + .m3input-field.size-lg { 168 + --m3input-h: 56px; 169 + } 170 + 171 + .m3input-field.size-xl { 172 + --m3input-h: 64px; 173 + } 174 + 175 + .m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) { 176 + --m3input-h: 48px; 177 + } 178 + 179 + /* outlined input */ 180 + .m3input-field.m3input-label.m3input-border input { 181 + width: 100%; 182 + height: var(--m3input-h); 183 + border: var(--m3input-border-width) solid var(--m3input-border-color); 184 + border-radius: var(--m3input-radius); 185 + background: var(--m3input-bg); 186 + color: var(--m3input-text-color); 187 + font-size: var(--m3input-font-size); 188 + padding: 0 12px; 189 + box-sizing: border-box; 190 + outline: none; 191 + transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition); 192 + } 193 + 194 + /* focus ring */ 195 + .m3input-field.m3input-label.m3input-border input:focus { 196 + border-color: var(--m3input-focus-color); 197 + /*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/ 198 + } 199 + 200 + /* label */ 201 + .m3input-field.m3input-label.m3input-border label { 202 + position: absolute; 203 + left: 12px; 204 + top: 50%; 205 + transform: translateY(-50%); 206 + background: var(--m3input-bg); 207 + padding: 0 .25em; 208 + color: var(--m3input-label-color); 209 + pointer-events: none; 210 + transition: all var(--m3input-transition); 211 + } 212 + 213 + /* float on focus or when filled */ 214 + .m3input-field.m3input-label.m3input-border input:focus+label, 215 + .m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label { 216 + top: 0; 217 + transform: translateY(-50%) scale(.78); 218 + left: 0; 219 + color: var(--m3input-focus-color); 220 + } 221 + 222 + /* placeholder trick */ 223 + .m3input-field.m3input-label.m3input-border input::placeholder { 224 + color: transparent; 225 + } 226 + 227 + /* radix i love you but like cmon man */ 228 + body[data-scroll-locked]{ 229 + margin-left: var(--removed-body-scroll-bar-size) !important; 230 + }
+64 -6
src/utils/atoms.ts
··· 1 import type Agent from "@atproto/api"; 2 - import { atom, createStore } from "jotai"; 3 - import { atomWithStorage } from 'jotai/utils'; 4 5 export const store = createStore(); 6 7 export const selectedFeedUriAtom = atomWithStorage<string | null>( 8 - 'selectedFeedUri', 9 null 10 ); 11 12 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 14 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 - 'feedscrollpositions', 16 {} 17 ); 18 19 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 20 - 'likedPosts', 21 {} 22 ); 23 24 - export const agentAtom = atom<Agent|null>(null); 25 export const authedAtom = atom<boolean>(false);
··· 1 import type Agent from "@atproto/api"; 2 + import { atom, createStore, useAtomValue } from "jotai"; 3 + import { atomWithStorage } from "jotai/utils"; 4 + import { useEffect } from "react"; 5 6 export const store = createStore(); 7 8 export const selectedFeedUriAtom = atomWithStorage<string | null>( 9 + "selectedFeedUri", 10 null 11 ); 12 13 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 14 15 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 16 + "feedscrollpositions", 17 {} 18 ); 19 20 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 21 + "likedPosts", 22 {} 23 ); 24 25 + export const defaultconstellationURL = "constellation.microcosm.blue"; 26 + export const constellationURLAtom = atomWithStorage<string>( 27 + "constellationURL", 28 + defaultconstellationURL 29 + ); 30 + export const defaultslingshotURL = "slingshot.microcosm.blue"; 31 + export const slingshotURLAtom = atomWithStorage<string>( 32 + "slingshotURL", 33 + defaultslingshotURL 34 + ); 35 + export const defaultaturilistservice = "aturilistservice.reddwarf.app"; 36 + export const aturiListServiceAtom = atomWithStorage<string>( 37 + "aturilistservice", 38 + defaultaturilistservice 39 + ); 40 + export const defaultImgCDN = "cdn.bsky.app"; 41 + export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN); 42 + export const defaultVideoCDN = "video.bsky.app"; 43 + export const videoCDNAtom = atomWithStorage<string>( 44 + "videocdnurl", 45 + defaultVideoCDN 46 + ); 47 + 48 + export const defaulthue = 28; 49 + export const hueAtom = atomWithStorage<number>("hue", defaulthue); 50 + 51 + export const isAtTopAtom = atom<boolean>(true); 52 + 53 + type ComposerState = 54 + | { kind: "closed" } 55 + | { kind: "root" } 56 + | { kind: "reply"; parent: string } 57 + | { kind: "quote"; subject: string }; 58 + export const composerAtom = atom<ComposerState>({ kind: "closed" }); 59 + 60 + export const agentAtom = atom<Agent | null>(null); 61 export const authedAtom = atom<boolean>(false); 62 + 63 + export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 64 + const value = useAtomValue(atom); 65 + 66 + useEffect(() => { 67 + document.documentElement.style.setProperty(cssVar, value.toString()); 68 + }, [value, cssVar]); 69 + 70 + useEffect(() => { 71 + document.documentElement.style.setProperty(cssVar, value.toString()); 72 + }, []); 73 + } 74 + 75 + hueAtom.onMount = (setAtom) => { 76 + const stored = localStorage.getItem("hue"); 77 + if (stored != null) setAtom(Number(stored)); 78 + }; 79 + // export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) { 80 + // const initial = store.get(atom); 81 + // console.log("atom get ", initial); 82 + // document.documentElement.style.setProperty(cssVar, initial.toString()); 83 + // }
+37 -3
src/utils/followState.ts
··· 1 - import { AtUri, type Agent } from "@atproto/api"; 2 - import { useQueryConstellation, type linksRecordsResponse } from "./useQuery"; 3 import type { QueryClient } from "@tanstack/react-query"; 4 - import { TID } from "@atproto/common-web"; 5 6 export function useGetFollowState({ 7 target, ··· 127 }; 128 }); 129 }
··· 1 + import { type Agent,AtUri } from "@atproto/api"; 2 + import { TID } from "@atproto/common-web"; 3 import type { QueryClient } from "@tanstack/react-query"; 4 + 5 + import { type linksRecordsResponse,useQueryConstellation } from "./useQuery"; 6 7 export function useGetFollowState({ 8 target, ··· 128 }; 129 }); 130 } 131 + 132 + 133 + 134 + export function useGetOneToOneState(params?: { 135 + target: string; 136 + user: string; 137 + collection: string; 138 + path: string; 139 + }): string[] | undefined { 140 + const { data: arbitrarydata } = useQueryConstellation( 141 + params && params.user 142 + ? { 143 + method: "/links", 144 + target: params.target, 145 + // @ts-expect-error overloading sucks so much 146 + collection: params.collection, 147 + path: params.path, 148 + dids: [params.user], 149 + } 150 + : { method: "undefined", target: "whatever" } 151 + // overloading sucks so much 152 + ) as { data: linksRecordsResponse | undefined }; 153 + if (!params || !params.user) return undefined; 154 + const data = arbitrarydata?.linking_records.slice(0, 50) ?? []; 155 + 156 + if (data.length > 0) { 157 + return data.map((linksRecord) => { 158 + return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`; 159 + }); 160 + } 161 + 162 + return undefined; 163 + }
+3 -5
src/utils/oauthClient.ts
··· 1 - // src/helpers/oauthClient.ts 2 import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser'; 3 4 - // This is your app's PDS for resolving handles if not provided. 5 - // You might need to host your own or use a public one. 6 const handleResolverPDS = 'https://bsky.social'; 7 8 - // This assumes your client-metadata.json is in the /public folder 9 - // and will be served at the root of your domain. 10 import clientMetadata from '../../public/client-metadata.json' with { type: 'json' }; 11 12 export const oauthClient = new BrowserOAuthClient({
··· 1 import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser'; 2 3 + // i tried making this https://pds-nd.whey.party but cors is annoying as fuck 4 const handleResolverPDS = 'https://bsky.social'; 5 6 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 + // @ts-ignore this should be fine ? the vite plugin should generate this before errors 8 import clientMetadata from '../../public/client-metadata.json' with { type: 'json' }; 9 10 export const oauthClient = new BrowserOAuthClient({
+68 -68
src/utils/useHydrated.ts
··· 9 AppBskyFeedPost, 10 AtUri, 11 } from "@atproto/api"; 12 - import { useEffect, useMemo,useState } from "react"; 13 14 - import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery"; 15 16 - type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends 17 - | { data: infer D } 18 - | undefined 19 - ? D 20 - : never; 21 22 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 23 return obj as $Typed<T>; ··· 26 export function hydrateEmbedImages( 27 embed: AppBskyEmbedImages.Main, 28 did: string, 29 ): $Typed<AppBskyEmbedImages.View> { 30 return asTyped({ 31 $type: "app.bsky.embed.images#view" as const, ··· 34 const link = img.image.ref?.["$link"]; 35 if (!link) return null; 36 return { 37 - thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 - fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 alt: img.alt || "", 40 aspectRatio: img.aspectRatio, 41 }; ··· 47 export function hydrateEmbedExternal( 48 embed: AppBskyEmbedExternal.Main, 49 did: string, 50 ): $Typed<AppBskyEmbedExternal.View> { 51 return asTyped({ 52 $type: "app.bsky.embed.external#view" as const, ··· 55 title: embed.external.title, 56 description: embed.external.description, 57 thumb: embed.external.thumb?.ref?.$link 58 - ? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 : undefined, 60 }, 61 }); ··· 64 export function hydrateEmbedVideo( 65 embed: AppBskyEmbedVideo.Main, 66 did: string, 67 ): $Typed<AppBskyEmbedVideo.View> { 68 const videoLink = embed.video.ref.$link; 69 return asTyped({ 70 $type: "app.bsky.embed.video#view" as const, 71 - playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`, 72 - thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`, 73 aspectRatio: embed.aspectRatio, 74 cid: videoLink, 75 }); ··· 80 quotedPost: QueryResultData<typeof useQueryPost>, 81 quotedProfile: QueryResultData<typeof useQueryProfile>, 82 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 83 ): $Typed<AppBskyEmbedRecord.View> | undefined { 84 if (!quotedPost || !quotedProfile || !quotedIdentity) { 85 return undefined; ··· 91 handle: quotedIdentity.handle, 92 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 93 avatar: quotedProfile.value.avatar?.ref?.$link 94 - ? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 95 : undefined, 96 viewer: {}, 97 labels: [], ··· 122 quotedPost: QueryResultData<typeof useQueryPost>, 123 quotedProfile: QueryResultData<typeof useQueryProfile>, 124 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 125 ): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 126 const hydratedRecord = hydrateEmbedRecord( 127 embed.record, 128 quotedPost, 129 quotedProfile, 130 quotedIdentity, 131 ); 132 133 if (!hydratedRecord) return undefined; ··· 148 149 export function useHydratedEmbed( 150 embed: AppBskyFeedPost.Record["embed"], 151 - postAuthorDid: string | undefined, 152 ) { 153 const recordInfo = useMemo(() => { 154 - if ( 155 - AppBskyEmbedRecordWithMedia.isMain(embed) 156 - ) { 157 const recordUri = embed.record.record.uri; 158 const quotedAuthorDid = new AtUri(recordUri).hostname; 159 return { recordUri, quotedAuthorDid, isRecordType: true }; 160 - } else 161 - if ( 162 - AppBskyEmbedRecord.isMain(embed) 163 - ) { 164 const recordUri = embed.record.uri; 165 const quotedAuthorDid = new AtUri(recordUri).hostname; 166 return { recordUri, quotedAuthorDid, isRecordType: true }; ··· 171 isRecordType: false, 172 }; 173 }, [embed]); 174 const { isRecordType, recordUri, quotedAuthorDid } = recordInfo; 175 176 - 177 const usequerypostresults = useQueryPost(recordUri); 178 - // const { 179 - // data: quotedPost, 180 - // isLoading: isLoadingPost, 181 - // error: postError, 182 - // } = usequerypostresults 183 184 - const profileUri = quotedAuthorDid ? `at://${quotedAuthorDid}/app.bsky.actor.profile/self` : undefined; 185 186 const { 187 data: quotedProfile, ··· 189 error: profileError, 190 } = useQueryProfile(profileUri); 191 192 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 193 - // const { 194 - // data: quotedIdentity, 195 - // isLoading: isLoadingIdentity, 196 - // error: identityError, 197 - // } = queryidentityresult 198 199 - const [hydratedEmbed, setHydratedEmbed] = useState< 200 - HydratedEmbedView | undefined 201 - >(undefined); 202 - 203 - useEffect(() => { 204 - if (!embed || !postAuthorDid) { 205 - setHydratedEmbed(undefined); 206 - return; 207 - } 208 209 - if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) { 210 - setHydratedEmbed(undefined); 211 - return; 212 } 213 214 try { 215 - let result: HydratedEmbedView | undefined; 216 - 217 if (AppBskyEmbedImages.isMain(embed)) { 218 - result = hydrateEmbedImages(embed, postAuthorDid); 219 } else if (AppBskyEmbedExternal.isMain(embed)) { 220 - result = hydrateEmbedExternal(embed, postAuthorDid); 221 } else if (AppBskyEmbedVideo.isMain(embed)) { 222 - result = hydrateEmbedVideo(embed, postAuthorDid); 223 } else if (AppBskyEmbedRecord.isMain(embed)) { 224 - result = hydrateEmbedRecord( 225 embed, 226 usequerypostresults?.data, 227 quotedProfile, 228 queryidentityresult?.data, 229 ); 230 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 231 let hydratedMedia: ··· 235 | undefined; 236 237 if (AppBskyEmbedImages.isMain(embed.media)) { 238 - hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid); 239 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 240 - hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid); 241 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 242 - hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid); 243 } 244 245 if (hydratedMedia) { 246 - result = hydrateEmbedRecordWithMedia( 247 embed, 248 hydratedMedia, 249 usequerypostresults?.data, 250 quotedProfile, 251 queryidentityresult?.data, 252 ); 253 } 254 } 255 - setHydratedEmbed(result); 256 } catch (e) { 257 console.error("Error hydrating embed", e); 258 - setHydratedEmbed(undefined); 259 } 260 - }, [ 261 - embed, 262 - postAuthorDid, 263 - isRecordType, 264 - usequerypostresults?.data, 265 - quotedProfile, 266 - queryidentityresult?.data, 267 - ]); 268 269 const isLoading = isRecordType 270 - ? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading 271 : false; 272 - const error = usequerypostresults?.error || profileError || queryidentityresult?.error; 273 274 return { data: hydratedEmbed, isLoading, error }; 275 - }
··· 9 AppBskyFeedPost, 10 AtUri, 11 } from "@atproto/api"; 12 + import { useAtom } from "jotai"; 13 + import { useMemo } from "react"; 14 15 + import { imgCDNAtom, videoCDNAtom } from "./atoms"; 16 + import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery"; 17 18 + type QueryResultData<T extends (...args: any) => any> = 19 + ReturnType<T> extends { data: infer D } | undefined ? D : never; 20 21 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 22 return obj as $Typed<T>; ··· 25 export function hydrateEmbedImages( 26 embed: AppBskyEmbedImages.Main, 27 did: string, 28 + cdn: string 29 ): $Typed<AppBskyEmbedImages.View> { 30 return asTyped({ 31 $type: "app.bsky.embed.images#view" as const, ··· 34 const link = img.image.ref?.["$link"]; 35 if (!link) return null; 36 return { 37 + thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 + fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 alt: img.alt || "", 40 aspectRatio: img.aspectRatio, 41 }; ··· 47 export function hydrateEmbedExternal( 48 embed: AppBskyEmbedExternal.Main, 49 did: string, 50 + cdn: string 51 ): $Typed<AppBskyEmbedExternal.View> { 52 return asTyped({ 53 $type: "app.bsky.embed.external#view" as const, ··· 56 title: embed.external.title, 57 description: embed.external.description, 58 thumb: embed.external.thumb?.ref?.$link 59 + ? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 60 : undefined, 61 }, 62 }); ··· 65 export function hydrateEmbedVideo( 66 embed: AppBskyEmbedVideo.Main, 67 did: string, 68 + videocdn: string 69 ): $Typed<AppBskyEmbedVideo.View> { 70 const videoLink = embed.video.ref.$link; 71 return asTyped({ 72 $type: "app.bsky.embed.video#view" as const, 73 + playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`, 74 + thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`, 75 aspectRatio: embed.aspectRatio, 76 cid: videoLink, 77 }); ··· 82 quotedPost: QueryResultData<typeof useQueryPost>, 83 quotedProfile: QueryResultData<typeof useQueryProfile>, 84 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 85 + cdn: string 86 ): $Typed<AppBskyEmbedRecord.View> | undefined { 87 if (!quotedPost || !quotedProfile || !quotedIdentity) { 88 return undefined; ··· 94 handle: quotedIdentity.handle, 95 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 96 avatar: quotedProfile.value.avatar?.ref?.$link 97 + ? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 98 : undefined, 99 viewer: {}, 100 labels: [], ··· 125 quotedPost: QueryResultData<typeof useQueryPost>, 126 quotedProfile: QueryResultData<typeof useQueryProfile>, 127 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 128 + cdn: string 129 ): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 130 const hydratedRecord = hydrateEmbedRecord( 131 embed.record, 132 quotedPost, 133 quotedProfile, 134 quotedIdentity, 135 + cdn 136 ); 137 138 if (!hydratedRecord) return undefined; ··· 153 154 export function useHydratedEmbed( 155 embed: AppBskyFeedPost.Record["embed"], 156 + postAuthorDid: string | undefined 157 ) { 158 const recordInfo = useMemo(() => { 159 + if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 160 const recordUri = embed.record.record.uri; 161 const quotedAuthorDid = new AtUri(recordUri).hostname; 162 return { recordUri, quotedAuthorDid, isRecordType: true }; 163 + } else if (AppBskyEmbedRecord.isMain(embed)) { 164 const recordUri = embed.record.uri; 165 const quotedAuthorDid = new AtUri(recordUri).hostname; 166 return { recordUri, quotedAuthorDid, isRecordType: true }; ··· 171 isRecordType: false, 172 }; 173 }, [embed]); 174 + 175 const { isRecordType, recordUri, quotedAuthorDid } = recordInfo; 176 177 const usequerypostresults = useQueryPost(recordUri); 178 179 + const profileUri = quotedAuthorDid 180 + ? `at://${quotedAuthorDid}/app.bsky.actor.profile/self` 181 + : undefined; 182 183 const { 184 data: quotedProfile, ··· 186 error: profileError, 187 } = useQueryProfile(profileUri); 188 189 + const [imgcdn] = useAtom(imgCDNAtom); 190 + const [videocdn] = useAtom(videoCDNAtom); 191 + 192 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 193 194 + const hydratedEmbed: HydratedEmbedView | undefined = (() => { 195 + if (!embed || !postAuthorDid) return undefined; 196 197 + if ( 198 + isRecordType && 199 + (!usequerypostresults?.data || 200 + !quotedProfile || 201 + !queryidentityresult?.data) 202 + ) { 203 + return undefined; 204 } 205 206 try { 207 if (AppBskyEmbedImages.isMain(embed)) { 208 + return hydrateEmbedImages(embed, postAuthorDid, imgcdn); 209 } else if (AppBskyEmbedExternal.isMain(embed)) { 210 + return hydrateEmbedExternal(embed, postAuthorDid, imgcdn); 211 } else if (AppBskyEmbedVideo.isMain(embed)) { 212 + return hydrateEmbedVideo(embed, postAuthorDid, videocdn); 213 } else if (AppBskyEmbedRecord.isMain(embed)) { 214 + return hydrateEmbedRecord( 215 embed, 216 usequerypostresults?.data, 217 quotedProfile, 218 queryidentityresult?.data, 219 + imgcdn 220 ); 221 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 222 let hydratedMedia: ··· 226 | undefined; 227 228 if (AppBskyEmbedImages.isMain(embed.media)) { 229 + hydratedMedia = hydrateEmbedImages( 230 + embed.media, 231 + postAuthorDid, 232 + imgcdn 233 + ); 234 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 235 + hydratedMedia = hydrateEmbedExternal( 236 + embed.media, 237 + postAuthorDid, 238 + imgcdn 239 + ); 240 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 241 + hydratedMedia = hydrateEmbedVideo( 242 + embed.media, 243 + postAuthorDid, 244 + videocdn 245 + ); 246 } 247 248 if (hydratedMedia) { 249 + return hydrateEmbedRecordWithMedia( 250 embed, 251 hydratedMedia, 252 usequerypostresults?.data, 253 quotedProfile, 254 queryidentityresult?.data, 255 + imgcdn 256 ); 257 } 258 } 259 } catch (e) { 260 console.error("Error hydrating embed", e); 261 + return undefined; 262 } 263 + })(); 264 265 const isLoading = isRecordType 266 + ? usequerypostresults?.isLoading || 267 + isLoadingProfile || 268 + queryidentityresult?.isLoading 269 : false; 270 + 271 + const error = 272 + usequerypostresults?.error || profileError || queryidentityresult?.error; 273 274 return { data: hydratedEmbed, isLoading, error }; 275 + }
+161 -17
src/utils/useQuery.ts
··· 1 import * as ATPAPI from "@atproto/api"; 2 import { 3 type QueryFunctionContext, 4 queryOptions, 5 useInfiniteQuery, 6 useQuery, 7 type UseQueryResult} from "@tanstack/react-query"; 8 9 - export function constructIdentityQuery(didorhandle?: string) { 10 return queryOptions({ 11 queryKey: ["identity", didorhandle], 12 queryFn: async () => { 13 if (!didorhandle) return undefined as undefined 14 const res = await fetch( 15 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 16 ); 17 if (!res.ok) throw new Error("Failed to fetch post"); 18 try { ··· 54 Error 55 > 56 export function useQueryIdentity(didorhandle?: string) { 57 - return useQuery(constructIdentityQuery(didorhandle)); 58 } 59 60 - export function constructPostQuery(uri?: string) { 61 return queryOptions({ 62 queryKey: ["post", uri], 63 queryFn: async () => { 64 if (!uri) return undefined as undefined 65 const res = await fetch( 66 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 67 ); 68 let data: any; 69 try { ··· 117 Error 118 > 119 export function useQueryPost(uri?: string) { 120 - return useQuery(constructPostQuery(uri)); 121 } 122 123 - export function constructProfileQuery(uri?: string) { 124 return queryOptions({ 125 queryKey: ["profile", uri], 126 queryFn: async () => { 127 if (!uri) return undefined as undefined 128 const res = await fetch( 129 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 130 ); 131 let data: any; 132 try { ··· 180 Error 181 > 182 export function useQueryProfile(uri?: string) { 183 - return useQuery(constructProfileQuery(uri)); 184 } 185 186 // export function constructConstellationQuery( ··· 216 // target: string 217 // ): QueryOptions<linksAllResponse, Error>; 218 export function constructConstellationQuery(query?:{ 219 method: 220 | "/links" 221 | "/links/distinct-dids" ··· 249 const cursor = query.cursor 250 const dids = query?.dids 251 const res = await fetch( 252 - `https://constellation.microcosm.blue${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}` 253 ); 254 if (!res.ok) throw new Error("Failed to fetch post"); 255 try { ··· 338 > 339 | undefined { 340 //if (!query) return; 341 return useQuery( 342 - constructConstellationQuery(query) 343 ); 344 } 345 ··· 361 type linksCountResponse = { 362 total: string; 363 }; 364 - type linksAllResponse = { 365 links: Record< 366 string, 367 Record< ··· 444 445 446 447 - export function constructArbitraryQuery(uri?: string) { 448 return queryOptions({ 449 queryKey: ["arbitrary", uri], 450 queryFn: async () => { 451 if (!uri) return undefined as undefined 452 const res = await fetch( 453 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 454 ); 455 let data: any; 456 try { ··· 503 Error 504 >; 505 export function useQueryArbitrary(uri?: string) { 506 - return useQuery(constructArbitraryQuery(uri)); 507 } 508 509 export function constructFallbackNothingQuery(){ ··· 555 }); 556 } 557 558 type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 559 560 export function constructInfiniteFeedSkeletonQuery(options: { ··· 605 }) { 606 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 607 608 - return useInfiniteQuery({ 609 queryKey, 610 queryFn, 611 initialPageParam: undefined as never, ··· 613 staleTime: Infinity, 614 refetchOnWindowFocus: false, 615 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 616 - }); 617 }
··· 1 import * as ATPAPI from "@atproto/api"; 2 import { 3 + infiniteQueryOptions, 4 type QueryFunctionContext, 5 queryOptions, 6 useInfiniteQuery, 7 useQuery, 8 type UseQueryResult} from "@tanstack/react-query"; 9 + import { useAtom } from "jotai"; 10 11 + import { constellationURLAtom, slingshotURLAtom } from "./atoms"; 12 + 13 + export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) { 14 return queryOptions({ 15 queryKey: ["identity", didorhandle], 16 queryFn: async () => { 17 if (!didorhandle) return undefined as undefined 18 const res = await fetch( 19 + `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 20 ); 21 if (!res.ok) throw new Error("Failed to fetch post"); 22 try { ··· 58 Error 59 > 60 export function useQueryIdentity(didorhandle?: string) { 61 + const [slingshoturl] = useAtom(slingshotURLAtom) 62 + return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 63 } 64 65 + export function constructPostQuery(uri?: string, slingshoturl?: string) { 66 return queryOptions({ 67 queryKey: ["post", uri], 68 queryFn: async () => { 69 if (!uri) return undefined as undefined 70 const res = await fetch( 71 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 72 ); 73 let data: any; 74 try { ··· 122 Error 123 > 124 export function useQueryPost(uri?: string) { 125 + const [slingshoturl] = useAtom(slingshotURLAtom) 126 + return useQuery(constructPostQuery(uri, slingshoturl)); 127 } 128 129 + export function constructProfileQuery(uri?: string, slingshoturl?: string) { 130 return queryOptions({ 131 queryKey: ["profile", uri], 132 queryFn: async () => { 133 if (!uri) return undefined as undefined 134 const res = await fetch( 135 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 136 ); 137 let data: any; 138 try { ··· 186 Error 187 > 188 export function useQueryProfile(uri?: string) { 189 + const [slingshoturl] = useAtom(slingshotURLAtom) 190 + return useQuery(constructProfileQuery(uri, slingshoturl)); 191 } 192 193 // export function constructConstellationQuery( ··· 223 // target: string 224 // ): QueryOptions<linksAllResponse, Error>; 225 export function constructConstellationQuery(query?:{ 226 + constellation: string, 227 method: 228 | "/links" 229 | "/links/distinct-dids" ··· 257 const cursor = query.cursor 258 const dids = query?.dids 259 const res = await fetch( 260 + `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}` 261 ); 262 if (!res.ok) throw new Error("Failed to fetch post"); 263 try { ··· 346 > 347 | undefined { 348 //if (!query) return; 349 + const [constellationurl] = useAtom(constellationURLAtom) 350 return useQuery( 351 + constructConstellationQuery(query && {constellation: constellationurl, ...query}) 352 ); 353 } 354 ··· 370 type linksCountResponse = { 371 total: string; 372 }; 373 + export type linksAllResponse = { 374 links: Record< 375 string, 376 Record< ··· 453 454 455 456 + export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 457 return queryOptions({ 458 queryKey: ["arbitrary", uri], 459 queryFn: async () => { 460 if (!uri) return undefined as undefined 461 const res = await fetch( 462 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 463 ); 464 let data: any; 465 try { ··· 512 Error 513 >; 514 export function useQueryArbitrary(uri?: string) { 515 + const [slingshoturl] = useAtom(slingshotURLAtom) 516 + return useQuery(constructArbitraryQuery(uri, slingshoturl)); 517 } 518 519 export function constructFallbackNothingQuery(){ ··· 565 }); 566 } 567 568 + export const ATURI_PAGE_LIMIT = 100; 569 + 570 + export interface AturiDirectoryAturisItem { 571 + uri: string; 572 + cid: string; 573 + rkey: string; 574 + } 575 + 576 + export type AturiDirectoryAturis = AturiDirectoryAturisItem[]; 577 + 578 + export function constructAturiListQuery(aturilistservice: string, did: string, collection: string, reverse?: boolean) { 579 + return queryOptions({ 580 + // A unique key for this query, including all parameters that affect the data. 581 + queryKey: ["aturiList", did, collection, { reverse }], 582 + 583 + // The function that fetches the data. 584 + queryFn: async ({ pageParam }: QueryFunctionContext) => { 585 + const cursor = pageParam as string | undefined; 586 + 587 + // Use URLSearchParams for safe and clean URL construction. 588 + const params = new URLSearchParams({ 589 + did, 590 + collection, 591 + }); 592 + 593 + if (cursor) { 594 + params.set("cursor", cursor); 595 + } 596 + 597 + // Add the reverse parameter if it's true 598 + if (reverse) { 599 + params.set("reverse", "true"); 600 + } 601 + 602 + const url = `https://${aturilistservice}/aturis?${params.toString()}`; 603 + 604 + const res = await fetch(url); 605 + if (!res.ok) { 606 + // You can add more specific error handling here 607 + throw new Error(`Failed to fetch AT-URI list for ${did}`); 608 + } 609 + 610 + return res.json() as Promise<AturiDirectoryAturis>; 611 + }, 612 + }); 613 + } 614 + 615 + export function useInfiniteQueryAturiList({aturilistservice, did, collection, reverse}:{aturilistservice: string, did: string | undefined, collection: string | undefined, reverse?: boolean}) { 616 + // We only enable the query if both `did` and `collection` are provided. 617 + const isEnabled = !!did && !!collection; 618 + 619 + const { queryKey, queryFn } = constructAturiListQuery(aturilistservice, did!, collection!, reverse); 620 + 621 + return useInfiniteQuery({ 622 + queryKey, 623 + queryFn, 624 + initialPageParam: undefined as never, // ???? what is this shit 625 + 626 + // @ts-expect-error i wouldve used as null | undefined, anyways 627 + getNextPageParam: (lastPage: AturiDirectoryAturis) => { 628 + // If the last page returned no records, we're at the end. 629 + if (!lastPage || lastPage.length === 0) { 630 + return undefined; 631 + } 632 + 633 + // If the number of records is less than our page limit, it must be the last page. 634 + if (lastPage.length < ATURI_PAGE_LIMIT) { 635 + return undefined; 636 + } 637 + 638 + // The cursor for the next page is the `rkey` of the last item we received. 639 + const lastItem = lastPage[lastPage.length - 1]; 640 + return lastItem.rkey; 641 + }, 642 + 643 + enabled: isEnabled, 644 + }); 645 + } 646 + 647 + 648 type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 649 650 export function constructInfiniteFeedSkeletonQuery(options: { ··· 695 }) { 696 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 697 698 + return {...useInfiniteQuery({ 699 queryKey, 700 queryFn, 701 initialPageParam: undefined as never, ··· 703 staleTime: Infinity, 704 refetchOnWindowFocus: false, 705 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 706 + }), queryKey: queryKey}; 707 + } 708 + 709 + 710 + export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 711 + constellation: string, 712 + method: '/links' 713 + target?: string 714 + collection: string 715 + path: string 716 + }) { 717 + console.log( 718 + 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 719 + query, 720 + ) 721 + 722 + return infiniteQueryOptions({ 723 + enabled: !!query?.target, 724 + queryKey: [ 725 + 'reddwarf_constellation', 726 + query?.method, 727 + query?.target, 728 + query?.collection, 729 + query?.path, 730 + ] as const, 731 + 732 + queryFn: async ({pageParam}: {pageParam?: string}) => { 733 + if (!query || !query?.target) return undefined 734 + 735 + const method = query.method 736 + const target = query.target 737 + const collection = query.collection 738 + const path = query.path 739 + const cursor = pageParam 740 + 741 + const res = await fetch( 742 + `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 743 + collection ? `&collection=${encodeURIComponent(collection)}` : '' 744 + }${path ? `&path=${encodeURIComponent(path)}` : ''}${ 745 + cursor ? `&cursor=${encodeURIComponent(cursor)}` : '' 746 + }`, 747 + ) 748 + 749 + if (!res.ok) throw new Error('Failed to fetch') 750 + 751 + return (await res.json()) as linksRecordsResponse 752 + }, 753 + 754 + getNextPageParam: lastPage => { 755 + return (lastPage as any)?.cursor ?? undefined 756 + }, 757 + initialPageParam: undefined, 758 + staleTime: 5 * 60 * 1000, 759 + gcTime: 5 * 60 * 1000, 760 + }) 761 }
+1 -1
tsconfig.json
··· 5 "jsx": "react-jsx", 6 "module": "ESNext", 7 "lib": ["ES2022", "DOM", "DOM.Iterable"], 8 - "types": ["vite/client"], 9 10 /* Bundler mode */ 11 "moduleResolution": "bundler",
··· 5 "jsx": "react-jsx", 6 "module": "ESNext", 7 "lib": ["ES2022", "DOM", "DOM.Iterable"], 8 + "types": ["vite/client", "unplugin-icons/types/react"], 9 10 /* Bundler mode */ 11 "moduleResolution": "bundler",
+22 -1
vite.config.ts
··· 3 import tailwindcss from "@tailwindcss/vite"; 4 import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; 5 import viteReact from "@vitejs/plugin-react"; 6 import { defineConfig } from "vite"; 7 8 import { generateMetadataPlugin } from "./oauthdev.mts"; 9 10 - const PROD_URL = "https://reddwarf.whey.party" 11 const DEV_URL = "https://local3768forumtest.whey.party" 12 13 function shp(url: string): string { ··· 28 }, 29 }), 30 tailwindcss(), 31 ], 32 // test: { 33 // globals: true,
··· 3 import tailwindcss from "@tailwindcss/vite"; 4 import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; 5 import viteReact from "@vitejs/plugin-react"; 6 + import AutoImport from 'unplugin-auto-import/vite' 7 + import IconsResolver from 'unplugin-icons/resolver' 8 + import Icons from 'unplugin-icons/vite' 9 import { defineConfig } from "vite"; 10 11 import { generateMetadataPlugin } from "./oauthdev.mts"; 12 13 + const PROD_URL = "https://reddwarf.app" 14 const DEV_URL = "https://local3768forumtest.whey.party" 15 16 function shp(url: string): string { ··· 31 }, 32 }), 33 tailwindcss(), 34 + AutoImport({ 35 + include: [ 36 + /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx 37 + ], 38 + resolvers: [ 39 + IconsResolver({ 40 + prefix: 'Icon', 41 + extension: 'jsx', 42 + enabledCollections: ['mdi','material-symbols'], 43 + }), 44 + ], 45 + dts: 'src/auto-imports.d.ts', 46 + }), 47 + Icons({ 48 + //autoInstall: true, 49 + compiler: 'jsx', 50 + jsx: 'react' 51 + }), 52 ], 53 // test: { 54 // globals: true,