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.

+1 -1
README.md
··· 8 8 ## running dev and build 9 9 in the `vite.config.ts` file you should change these values 10 10 ```ts 11 - const PROD_URL = "https://reddwarf.whey.party" 11 + const PROD_URL = "https://reddwarf.app" 12 12 const DEV_URL = "https://local3768forumtest.whey.party" 13 13 ``` 14 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
+1685
package-lock.json
··· 8 8 "dependencies": { 9 9 "@atproto/api": "^0.16.6", 10 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", 11 15 "@tailwindcss/vite": "^4.0.6", 12 16 "@tanstack/query-sync-storage-persister": "^5.85.6", 13 17 "@tanstack/react-devtools": "^0.2.2", ··· 21 25 "idb-keyval": "^6.2.2", 22 26 "jotai": "^2.13.1", 23 27 "npm": "^11.6.2", 28 + "radix-ui": "^1.4.3", 24 29 "react": "^19.0.0", 25 30 "react-dom": "^19.0.0", 26 31 "react-player": "^3.3.2", ··· 1592 1597 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1593 1598 } 1594 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 + }, 1595 1634 "node_modules/@humanfs/core": { 1596 1635 "version": "0.19.1", 1597 1636 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 1895 1934 "node": ">= 8" 1896 1935 } 1897 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 + }, 1898 3376 "node_modules/@rolldown/pluginutils": { 1899 3377 "version": "1.0.0-beta.27", 1900 3378 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", ··· 3806 5284 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 3807 5285 "dev": true, 3808 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 + } 3809 5298 }, 3810 5299 "node_modules/aria-query": { 3811 5300 "version": "5.3.0", ··· 4716 6205 "node": ">=8" 4717 6206 } 4718 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 + }, 4719 6213 "node_modules/diff": { 4720 6214 "version": "8.0.2", 4721 6215 "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", ··· 5735 7229 }, 5736 7230 "funding": { 5737 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" 5738 7240 } 5739 7241 }, 5740 7242 "node_modules/get-proto": { ··· 10338 11840 ], 10339 11841 "license": "MIT" 10340 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 + }, 10341 11919 "node_modules/react": { 10342 11920 "version": "19.1.1", 10343 11921 "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", ··· 10399 11977 "node": ">=0.10.0" 10400 11978 } 10401 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 + }, 10402 12046 "node_modules/readdirp": { 10403 12047 "version": "3.6.0", 10404 12048 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", ··· 11892 13536 "peer": true, 11893 13537 "dependencies": { 11894 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 + } 11895 13580 } 11896 13581 }, 11897 13582 "node_modules/use-sync-external-store": {
+5
package.json
··· 12 12 "dependencies": { 13 13 "@atproto/api": "^0.16.6", 14 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", 15 19 "@tailwindcss/vite": "^4.0.6", 16 20 "@tanstack/query-sync-storage-persister": "^5.85.6", 17 21 "@tanstack/react-devtools": "^0.2.2", ··· 25 29 "idb-keyval": "^6.2.2", 26 30 "jotai": "^2.13.1", 27 31 "npm": "^11.6.2", 32 + "radix-ui": "^1.4.3", 28 33 "react": "^19.0.0", 29 34 "react-dom": "^19.0.0", 30 35 "react-player": "^3.3.2",
+339
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 { Button } from "./radix-m3-rd/Button"; 12 + import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer"; 13 + 14 + const MAX_POST_LENGTH = 300; 15 + 16 + export function Composer() { 17 + const [composerState, setComposerState] = useAtom(composerAtom); 18 + const [closeConfirmState, setCloseConfirmState] = useState<boolean>(false); 19 + const { agent } = useAuth(); 20 + 21 + const [postText, setPostText] = useState(""); 22 + const [posting, setPosting] = useState(false); 23 + const [postSuccess, setPostSuccess] = useState(false); 24 + const [postError, setPostError] = useState<string | null>(null); 25 + 26 + useEffect(() => { 27 + setPostText(""); 28 + setPosting(false); 29 + setPostSuccess(false); 30 + setPostError(null); 31 + }, [composerState.kind]); 32 + 33 + const parentUri = 34 + composerState.kind === "reply" 35 + ? composerState.parent 36 + : composerState.kind === "quote" 37 + ? composerState.subject 38 + : undefined; 39 + 40 + const { data: parentPost, isLoading: isParentLoading } = 41 + useQueryPost(parentUri); 42 + 43 + async function handlePost() { 44 + if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return; 45 + 46 + setPosting(true); 47 + setPostError(null); 48 + 49 + try { 50 + const rt = new RichText({ text: postText }); 51 + await rt.detectFacets(agent); 52 + 53 + if (rt.facets?.length) { 54 + rt.facets = rt.facets.filter((item) => { 55 + if (item.$type !== "app.bsky.richtext.facet") return true; 56 + if (!item.features?.length) return true; 57 + 58 + item.features = item.features.filter((feature) => { 59 + if (feature.$type !== "app.bsky.richtext.facet#mention") return true; 60 + const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined; 61 + return typeof did === "string" && did.startsWith("did:"); 62 + }); 63 + 64 + return item.features.length > 0; 65 + }); 66 + } 67 + 68 + const record: Record<string, unknown> = { 69 + $type: "app.bsky.feed.post", 70 + text: rt.text, 71 + facets: rt.facets, 72 + createdAt: new Date().toISOString(), 73 + }; 74 + 75 + if (composerState.kind === "reply" && parentPost) { 76 + record.reply = { 77 + root: parentPost.value?.reply?.root ?? { 78 + uri: parentPost.uri, 79 + cid: parentPost.cid, 80 + }, 81 + parent: { 82 + uri: parentPost.uri, 83 + cid: parentPost.cid, 84 + }, 85 + }; 86 + } 87 + 88 + if (composerState.kind === "quote" && parentPost) { 89 + record.embed = { 90 + $type: "app.bsky.embed.record", 91 + record: { 92 + uri: parentPost.uri, 93 + cid: parentPost.cid, 94 + }, 95 + }; 96 + } 97 + 98 + await agent.com.atproto.repo.createRecord({ 99 + collection: "app.bsky.feed.post", 100 + repo: agent.assertDid, 101 + record, 102 + }); 103 + 104 + setPostSuccess(true); 105 + setPostText(""); 106 + 107 + setTimeout(() => { 108 + setPostSuccess(false); 109 + setComposerState({ kind: "closed" }); 110 + }, 1500); 111 + } catch (e: any) { 112 + setPostError(e?.message || "Failed to post"); 113 + } finally { 114 + setPosting(false); 115 + } 116 + } 117 + 118 + const getPlaceholder = () => { 119 + switch (composerState.kind) { 120 + case "reply": 121 + return "Post your reply"; 122 + case "quote": 123 + return "Add a comment..."; 124 + case "root": 125 + default: 126 + return "What's happening?!"; 127 + } 128 + }; 129 + 130 + const charsLeft = MAX_POST_LENGTH - postText.length; 131 + const isPostButtonDisabled = 132 + posting || !postText.trim() || isParentLoading || charsLeft < 0; 133 + 134 + function handleAttemptClose() { 135 + if (postText.trim() && !posting) { 136 + setCloseConfirmState(true); 137 + } else { 138 + setComposerState({ kind: "closed" }); 139 + } 140 + } 141 + 142 + function handleConfirmClose() { 143 + setComposerState({ kind: "closed" }); 144 + setCloseConfirmState(false); 145 + setPostText(""); 146 + } 147 + 148 + return ( 149 + <> 150 + <Dialog.Root 151 + open={composerState.kind !== "closed"} 152 + onOpenChange={(open) => { 153 + if (!open) handleAttemptClose(); 154 + }} 155 + > 156 + <Dialog.Portal> 157 + <Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 158 + 159 + <Dialog.Content className="fixed overflow-y-auto gutter inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 pb-[50dvh] sm:pb-[50dvh]"> 160 + <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"> 161 + <div className="flex flex-row justify-between p-2"> 162 + <Dialog.Close asChild> 163 + <button 164 + 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" 165 + disabled={posting} 166 + aria-label="Close" 167 + onClick={handleAttemptClose} 168 + > 169 + <svg 170 + xmlns="http://www.w3.org/2000/svg" 171 + width="20" 172 + height="20" 173 + viewBox="0 0 24 24" 174 + fill="none" 175 + stroke="currentColor" 176 + strokeWidth="2.5" 177 + strokeLinecap="round" 178 + strokeLinejoin="round" 179 + > 180 + <line x1="18" y1="6" x2="6" y2="18"></line> 181 + <line x1="6" y1="6" x2="18" y2="18"></line> 182 + </svg> 183 + </button> 184 + </Dialog.Close> 185 + 186 + <div className="flex-1" /> 187 + <div className="flex items-center gap-4"> 188 + <span 189 + className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`} 190 + > 191 + {charsLeft} 192 + </span> 193 + <Button 194 + onClick={handlePost} 195 + disabled={isPostButtonDisabled} 196 + > 197 + {posting ? "Posting..." : "Post"} 198 + </Button> 199 + </div> 200 + </div> 201 + 202 + {postSuccess ? ( 203 + <div className="flex flex-col items-center justify-center py-16"> 204 + <span className="text-gray-500 text-6xl mb-4">โœ“</span> 205 + <span className="text-xl font-bold text-black dark:text-white"> 206 + Posted! 207 + </span> 208 + </div> 209 + ) : ( 210 + <div className="px-4"> 211 + {composerState.kind === "reply" && ( 212 + <div className="mb-1 -mx-4"> 213 + {isParentLoading ? ( 214 + <div className="text-sm text-gray-500 animate-pulse"> 215 + Loading parent post... 216 + </div> 217 + ) : parentUri ? ( 218 + <UniversalPostRendererATURILoader 219 + atUri={parentUri} 220 + bottomReplyLine 221 + bottomBorder={false} 222 + /> 223 + ) : ( 224 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 225 + Could not load parent post. 226 + </div> 227 + )} 228 + </div> 229 + )} 230 + 231 + <div className="flex w-full gap-1 flex-col"> 232 + <ProfileThing agent={agent} large /> 233 + <div className="flex pl-[50px]"> 234 + <AutoGrowTextarea 235 + className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2" 236 + rows={5} 237 + placeholder={getPlaceholder()} 238 + value={postText} 239 + onChange={(e) => setPostText(e.target.value)} 240 + disabled={posting} 241 + autoFocus 242 + /> 243 + </div> 244 + </div> 245 + 246 + {composerState.kind === "quote" && ( 247 + <div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> 248 + {isParentLoading ? ( 249 + <div className="text-sm text-gray-500 animate-pulse"> 250 + Loading parent post... 251 + </div> 252 + ) : parentUri ? ( 253 + <UniversalPostRendererATURILoader 254 + atUri={parentUri} 255 + isQuote 256 + /> 257 + ) : ( 258 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 259 + Could not load parent post. 260 + </div> 261 + )} 262 + </div> 263 + )} 264 + 265 + {postError && ( 266 + <div className="text-red-500 text-sm my-2 text-center"> 267 + {postError} 268 + </div> 269 + )} 270 + </div> 271 + )} 272 + </div> 273 + </Dialog.Content> 274 + </Dialog.Portal> 275 + </Dialog.Root> 276 + 277 + {/* Close confirmation dialog */} 278 + <Dialog.Root open={closeConfirmState} onOpenChange={setCloseConfirmState}> 279 + <Dialog.Portal> 280 + 281 + <Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 282 + 283 + <Dialog.Content className="fixed gutter inset-0 z-50 flex items-start justify-center pt-30 sm:pt-40"> 284 + <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-md relative mx-4 py-6"> 285 + <div className="text-xl mb-4 text-center"> 286 + Discard your post? 287 + </div> 288 + <div className="text-md mb-4 text-center"> 289 + You will lose your draft 290 + </div> 291 + <div className="flex justify-end gap-2 px-6"> 292 + <Button 293 + onClick={handleConfirmClose} 294 + > 295 + Discard 296 + </Button> 297 + <Button 298 + variant={"outlined"} 299 + onClick={() => setCloseConfirmState(false)} 300 + > 301 + Cancel 302 + </Button> 303 + </div> 304 + </div> 305 + </Dialog.Content> 306 + </Dialog.Portal> 307 + </Dialog.Root> 308 + </> 309 + ); 310 + } 311 + 312 + function AutoGrowTextarea({ 313 + value, 314 + className, 315 + onChange, 316 + ...props 317 + }: React.DetailedHTMLProps< 318 + React.TextareaHTMLAttributes<HTMLTextAreaElement>, 319 + HTMLTextAreaElement 320 + >) { 321 + const ref = useRef<HTMLTextAreaElement>(null); 322 + 323 + useEffect(() => { 324 + const el = ref.current; 325 + if (!el) return; 326 + el.style.height = "auto"; 327 + el.style.height = el.scrollHeight + "px"; 328 + }, [value]); 329 + 330 + return ( 331 + <textarea 332 + ref={ref} 333 + className={className} 334 + value={value} 335 + onChange={onChange} 336 + {...props} 337 + /> 338 + ); 339 + }
+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 + }
+32 -6
src/components/InfiniteCustomFeed.tsx
··· 1 + import { useQueryClient } from "@tanstack/react-query"; 1 2 import * as React from "react"; 2 3 3 4 //import { useInView } from "react-intersection-observer"; ··· 37 38 isFetchingNextPage, 38 39 refetch, 39 40 isRefetching, 41 + queryKey, 40 42 } = useInfiniteQueryFeedSkeleton({ 41 43 feedUri: feedUri, 42 44 agent: agent ?? undefined, ··· 44 46 pdsUrl: pdsUrl, 45 47 feedServiceDid: feedServiceDid, 46 48 }); 49 + const queryClient = useQueryClient(); 50 + 47 51 48 52 const handleRefresh = () => { 53 + queryClient.removeQueries({queryKey: queryKey}); 54 + //queryClient.invalidateQueries(["infinite-feed", feedUri] as const); 49 55 refetch(); 50 56 }; 51 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 + 52 76 //const { ref, inView } = useInView(); 53 77 54 78 // React.useEffect(() => { ··· 67 91 ); 68 92 } 69 93 70 - const allPosts = 71 - data?.pages.flatMap((page) => { 72 - if (page) return page.feed; 73 - }) ?? []; 94 + // const allPosts = 95 + // data?.pages.flatMap((page) => { 96 + // if (page) return page.feed; 97 + // }) ?? []; 74 98 75 99 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 76 100 return ( ··· 116 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" 117 141 aria-label="Refresh feed" 118 142 > 119 - <RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} /> 143 + <RefreshIcon 144 + className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} 145 + /> 120 146 </button> 121 147 </> 122 148 ); ··· 139 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" 140 166 ></path> 141 167 </svg> 142 - ); 168 + );
+83 -37
src/components/Login.tsx
··· 1 1 // src/components/Login.tsx 2 - import { Agent } from "@atproto/api"; 2 + import AtpAgent, { Agent } from "@atproto/api"; 3 + import { useAtom } from "jotai"; 3 4 import React, { useEffect, useRef, useState } from "react"; 4 5 5 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 + import { imgCDNAtom } from "~/utils/atoms"; 8 + import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; 9 + 10 + import { Button } from "./radix-m3-rd/Button"; 6 11 7 12 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 8 13 export default function Login({ ··· 21 26 className={ 22 27 compact 23 28 ? "flex items-center justify-center p-1" 24 - : "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]" 29 + : "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]" 25 30 } 26 31 > 27 32 <span ··· 40 45 // Large view 41 46 if (!compact) { 42 47 return ( 43 - <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4"> 48 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 44 49 <div className="flex flex-col items-center justify-center text-center"> 45 50 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 46 51 You are logged in! 47 52 </p> 48 53 <ProfileThing agent={agent} large /> 49 - <button 54 + <Button 50 55 onClick={logout} 51 - className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors" 56 + className="mt-4" 52 57 > 53 58 Log out 54 - </button> 59 + </Button> 55 60 </div> 56 61 </div> 57 62 ); ··· 74 79 if (!compact) { 75 80 // Large view renders the form directly in the card 76 81 return ( 77 - <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4"> 82 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 78 83 <UnifiedLoginForm /> 79 84 </div> 80 85 ); ··· 110 115 // --- 3. Helper components for layouts, forms, and UI --- 111 116 112 117 // A new component to contain the logic for the compact dropdown 113 - const CompactLoginButton = ({popup}:{popup?: boolean}) => { 118 + const CompactLoginButton = ({ popup }: { popup?: boolean }) => { 114 119 const [showForm, setShowForm] = useState(false); 115 120 const formRef = useRef<HTMLDivElement>(null); 116 121 ··· 137 142 Log in 138 143 </button> 139 144 {showForm && ( 140 - <div 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 + <div 146 + 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`} 147 + > 141 148 <UnifiedLoginForm /> 142 149 </div> 143 150 )} ··· 158 165 onClick={onClick} 159 166 className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 160 167 active 161 - ? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 168 + ? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 162 169 : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 163 170 }`} 164 171 > ··· 187 194 <p className="text-xs text-gray-500 dark:text-gray-400"> 188 195 Sign in with AT. Your password is never shared. 189 196 </p> 190 - <input 197 + {/* <input 191 198 type="text" 192 199 placeholder="handle.bsky.social" 193 200 value={handle} 194 201 onChange={(e) => setHandle(e.target.value)} 195 202 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" 196 - /> 197 - <button 198 - type="submit" 199 - className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 200 - > 201 - Log in 202 - </button> 203 + /> */} 204 + <div className="flex flex-col gap-3"> 205 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 206 + <input 207 + type="text" 208 + placeholder=" " 209 + value={handle} 210 + onChange={(e) => setHandle(e.target.value)} 211 + /> 212 + <label>AT Handle</label> 213 + </div> 214 + <button 215 + type="submit" 216 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 217 + > 218 + Log in 219 + </button> 220 + </div> 203 221 </form> 204 222 ); 205 223 }; ··· 232 250 <p className="text-xs text-red-500 dark:text-red-400"> 233 251 Warning: Less secure. Use an App Password. 234 252 </p> 235 - <input 253 + {/* <input 236 254 type="text" 237 255 placeholder="handle.bsky.social" 238 256 value={user} ··· 254 272 value={serviceURL} 255 273 onChange={(e) => setServiceURL(e.target.value)} 256 274 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 - /> 275 + /> */} 276 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 277 + <input 278 + type="text" 279 + placeholder=" " 280 + value={user} 281 + onChange={(e) => setUser(e.target.value)} 282 + /> 283 + <label>AT Handle</label> 284 + </div> 285 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 286 + <input 287 + type="text" 288 + placeholder=" " 289 + value={password} 290 + onChange={(e) => setPassword(e.target.value)} 291 + /> 292 + <label>App Password</label> 293 + </div> 294 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 295 + <input 296 + type="text" 297 + placeholder=" " 298 + value={serviceURL} 299 + onChange={(e) => setServiceURL(e.target.value)} 300 + /> 301 + <label>PDS</label> 302 + </div> 258 303 {error && <p className="text-xs text-red-500">{error}</p>} 259 304 <button 260 305 type="submit" ··· 274 319 agent: Agent | null; 275 320 large?: boolean; 276 321 }) => { 277 - const [profile, setProfile] = useState<any>(null); 322 + const did = ((agent as AtpAgent)?.session?.did ?? 323 + (agent as AtpAgent)?.assertDid ?? 324 + agent?.did) as string | undefined; 325 + const { data: identity } = useQueryIdentity(did); 326 + const { data: profiledata } = useQueryProfile( 327 + `at://${did}/app.bsky.actor.profile/self` 328 + ); 329 + const profile = profiledata?.value; 278 330 279 - useEffect(() => { 280 - const fetchUser = async () => { 281 - const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid; 282 - if (!did) return; 283 - try { 284 - const res = await agent!.getProfile({ actor: did }); 285 - setProfile(res.data); 286 - } catch (e) { 287 - console.error("Failed to fetch profile", e); 288 - } 289 - }; 290 - if (agent) fetchUser(); 291 - }, [agent]); 331 + const [imgcdn] = useAtom(imgCDNAtom) 332 + 333 + function getAvatarUrl(p: typeof profile) { 334 + const link = p?.avatar?.ref?.["$link"]; 335 + if (!link || !did) return null; 336 + return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; 337 + } 292 338 293 - if (!profile) { 339 + if (!profiledata) { 294 340 return ( 295 341 // Skeleton loader 296 342 <div ··· 316 362 className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 317 363 > 318 364 <img 319 - src={profile?.avatar} 365 + src={getAvatarUrl(profile) ?? undefined} 320 366 alt="avatar" 321 367 className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 322 368 /> ··· 329 375 <div 330 376 className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 331 377 > 332 - @{profile?.handle} 378 + @{identity?.handle} 333 379 </div> 334 380 </div> 335 381 </div>
+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 + }
+226 -77
src/components/UniversalPostRenderer.tsx
··· 1 1 import { useNavigate } from "@tanstack/react-router"; 2 2 import DOMPurify from "dompurify"; 3 3 import { useAtom } from "jotai"; 4 + import { DropdownMenu } from "radix-ui"; 5 + import { HoverCard } from "radix-ui"; 4 6 import * as React from "react"; 5 7 import { type SVGProps } from "react"; 6 8 7 - import { likedPostsAtom } from "~/utils/atoms"; 9 + import { 10 + composerAtom, 11 + constellationURLAtom, 12 + imgCDNAtom, 13 + likedPostsAtom, 14 + } from "~/utils/atoms"; 8 15 import { useHydratedEmbed } from "~/utils/useHydrated"; 9 16 import { 10 17 useQueryConstellation, ··· 36 43 nopics?: boolean; 37 44 lightboxCallback?: (d: LightboxProps) => void; 38 45 maxReplies?: number; 46 + isQuote?: boolean; 39 47 } 40 48 41 49 // export async function cachedGetRecord({ ··· 146 154 nopics, 147 155 lightboxCallback, 148 156 maxReplies, 157 + isQuote, 149 158 }: UniversalPostRendererATURILoaderProps) { 159 + // todo remove this once tree rendering is implemented, use a prop like isTree 160 + const TEMPLINEAR = true; 150 161 // /*mass comment*/ console.log("atUri", atUri); 151 162 //const { get, set } = usePersistentStore(); 152 163 //const [record, setRecord] = React.useState<any>(null); ··· 398 409 // path: ".reply.parent.uri", 399 410 // }); 400 411 412 + const [constellationurl] = useAtom(constellationURLAtom); 413 + 401 414 const infinitequeryresults = useInfiniteQuery({ 402 415 ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 403 416 { 417 + constellation: constellationurl, 404 418 method: "/links", 405 419 target: atUri, 406 420 collection: "app.bsky.feed.post", 407 421 path: ".reply.parent.uri", 408 422 } 409 423 ), 410 - enabled: !!atUri && !!maxReplies, 424 + enabled: !!atUri && !!maxReplies && !isQuote, 411 425 }); 412 426 413 427 const { ··· 419 433 420 434 // auto-fetch all pages 421 435 useEffect(() => { 422 - if (!maxReplies) return; 436 + if (!maxReplies || isQuote || TEMPLINEAR) return; 423 437 if ( 424 438 infinitequeryresults.hasNextPage && 425 439 !infinitequeryresults.isFetchingNextPage ··· 427 441 console.log("Fetching the next page..."); 428 442 infinitequeryresults.fetchNextPage(); 429 443 } 430 - }, [infinitequeryresults]); 444 + }, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]); 431 445 432 446 const replyAturis = repliesData 433 447 ? repliesData.pages.flatMap((page) => ··· 443 457 //const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined); 444 458 445 459 const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => { 446 - if (!replyAturis || replyAturis.length === 0 || !maxReplies) 460 + if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies) 447 461 return { 448 462 oldestOpsReply: undefined, 449 463 oldestOpsReplyElseNewestNonOpsReply: undefined, ··· 504 518 ? true 505 519 : maxReplies && !oldestOpsReplyElseNewestNonOpsReply 506 520 ? false 507 - : bottomReplyLine 521 + : (maxReplies === 0 && (!replies || (!!replies && replies === 0))) ? false : bottomReplyLine 508 522 } 509 523 topReplyLine={topReplyLine} 510 524 //bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder} ··· 524 538 nopics={nopics} 525 539 lightboxCallback={lightboxCallback} 526 540 maxReplies={maxReplies} 541 + isQuote={isQuote} 527 542 /> 528 - {oldestOpsReplyElseNewestNonOpsReply && ( 543 + <> 544 + {(maxReplies && maxReplies === 0 && replies && replies > 0) ? ( 545 + <> 546 + {/* <div>hello</div> */} 547 + <MoreReplies atUri={atUri} /> 548 + </> 549 + ) : (<></>)} 550 + </> 551 + {!isQuote && oldestOpsReplyElseNewestNonOpsReply && ( 529 552 <> 530 553 {/* <span>hello {maxReplies}</span> */} 531 554 <UniversalPostRendererATURILoader 532 555 //detailed={detailed} 533 556 atUri={oldestOpsReplyElseNewestNonOpsReply} 534 557 bottomReplyLine={(maxReplies ?? 0) > 0} 535 - topReplyLine={(maxReplies ?? 0) > 1} 558 + topReplyLine={ 559 + (!!(maxReplies && maxReplies - 1 === 0) && 560 + !!(replies && replies > 0)) || 561 + !!((maxReplies ?? 0) > 1) 562 + } 536 563 bottomBorder={bottomBorder} 537 564 feedviewpost={feedviewpost} 538 565 repostedby={repostedby} ··· 545 572 maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined 546 573 } 547 574 /> 548 - {maxReplies && maxReplies - 1 === 0 && ( 549 - <MoreReplies atUri={oldestOpsReplyElseNewestNonOpsReply} /> 550 - )} 551 575 </> 552 576 )} 553 577 </> ··· 588 612 ); 589 613 } 590 614 591 - function getAvatarUrl(opProfile: any, did: string) { 615 + function getAvatarUrl(opProfile: any, did: string, cdn: string) { 592 616 const link = opProfile?.value?.avatar?.ref?.["$link"]; 593 617 if (!link) return null; 594 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`; 618 + return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`; 595 619 } 596 620 597 621 export function UniversalPostRendererRawRecordShim({ ··· 614 638 nopics, 615 639 lightboxCallback, 616 640 maxReplies, 641 + isQuote, 617 642 }: { 618 643 postRecord: any; 619 644 profileRecord: any; ··· 634 659 nopics?: boolean; 635 660 lightboxCallback?: (d: LightboxProps) => void; 636 661 maxReplies?: number; 662 + isQuote?: boolean; 637 663 }) { 638 664 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 639 665 const navigate = useNavigate(); ··· 710 736 error: embedError, 711 737 } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); 712 738 739 + const [imgcdn] = useAtom(imgCDNAtom); 740 + 713 741 const parsedaturi = new AtUri(aturi); //parseAtUri(aturi); 714 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 + 715 766 const fakepost = React.useMemo<AppBskyFeedDefs.PostView>( 716 767 () => ({ 717 768 $type: "app.bsky.feed.defs#postView", 718 769 uri: aturi, 719 770 cid: postRecord?.cid || "", 720 - author: { 721 - did: resolved?.did || "", 722 - handle: resolved?.handle || "", 723 - displayName: profileRecord?.value?.displayName || "", 724 - avatar: getAvatarUrl(profileRecord, resolved?.did) || "", 725 - viewer: undefined, 726 - labels: profileRecord?.labels || undefined, 727 - verification: undefined, 728 - }, 771 + author: fakeprofileviewbasic, 729 772 record: postRecord?.value || {}, 730 773 embed: hydratedEmbed ?? undefined, 731 774 replyCount: repliesCount ?? 0, ··· 742 785 postRecord?.cid, 743 786 postRecord?.value, 744 787 postRecord?.labels, 745 - resolved?.did, 746 - resolved?.handle, 747 - profileRecord, 788 + fakeprofileviewbasic, 748 789 hydratedEmbed, 749 790 repliesCount, 750 791 repostsCount, ··· 821 862 } 822 863 }} 823 864 post={fakepost} 865 + uprrrsauthor={fakeprofileviewdetailed} 824 866 salt={aturi} 825 867 bottomReplyLine={bottomReplyLine} 826 868 topReplyLine={topReplyLine} ··· 834 876 nopics={nopics} 835 877 lightboxCallback={lightboxCallback} 836 878 maxReplies={maxReplies} 879 + isQuote={isQuote} 837 880 /> 838 881 </> 839 882 ); ··· 872 915 {...props} 873 916 > 874 917 <path 875 - fill="oklch(0.704 0.05 28)" 918 + fill="var(--color-gray-400)" 876 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" 877 920 ></path> 878 921 </svg> ··· 889 932 {...props} 890 933 > 891 934 <path 892 - fill="oklch(0.704 0.05 28)" 935 + fill="var(--color-gray-400)" 893 936 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 894 937 ></path> 895 938 </svg> ··· 940 983 {...props} 941 984 > 942 985 <path 943 - fill="oklch(0.704 0.05 28)" 986 + fill="var(--color-gray-400)" 944 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" 945 988 ></path> 946 989 </svg> ··· 957 1000 {...props} 958 1001 > 959 1002 <path 960 - fill="oklch(0.704 0.05 28)" 1003 + fill="var(--color-gray-400)" 961 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" 962 1005 ></path> 963 1006 </svg> ··· 974 1017 {...props} 975 1018 > 976 1019 <path 977 - fill="oklch(0.704 0.05 28)" 1020 + fill="var(--color-gray-400)" 978 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" 979 1022 ></path> 980 1023 </svg> ··· 991 1034 {...props} 992 1035 > 993 1036 <path 994 - fill="oklch(0.704 0.05 28)" 1037 + fill="var(--color-gray-400)" 995 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" 996 1039 ></path> 997 1040 </svg> ··· 1025 1068 {...props} 1026 1069 > 1027 1070 <path 1028 - fill="oklch(0.704 0.05 28)" 1071 + fill="var(--color-gray-400)" 1029 1072 d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11" 1030 1073 ></path> 1031 1074 </svg> ··· 1079 1122 {...props} 1080 1123 > 1081 1124 <path 1082 - fill="oklch(0.704 0.05 28)" 1125 + fill="var(--color-gray-400)" 1083 1126 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 1084 1127 ></path> 1085 1128 </svg> ··· 1096 1139 {...props} 1097 1140 > 1098 1141 <path 1099 - fill="oklch(0.704 0.05 28)" 1142 + fill="var(--color-gray-400)" 1100 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" 1101 1144 ></path> 1102 1145 </svg> ··· 1124 1167 //import Masonry from "@mui/lab/Masonry"; 1125 1168 import { 1126 1169 type $Typed, 1170 + AppBskyActorDefs, 1127 1171 AppBskyEmbedDefs, 1128 1172 AppBskyEmbedExternal, 1129 1173 AppBskyEmbedImages, ··· 1153 1197 1154 1198 import defaultpfp from "~/../public/favicon.png"; 1155 1199 import { useAuth } from "~/providers/UnifiedAuthProvider"; 1200 + import { FollowButton, Mutual } from "~/routes/profile.$did"; 1156 1201 import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 1157 1202 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 1158 1203 // import type { ··· 1261 1306 1262 1307 function UniversalPostRenderer({ 1263 1308 post, 1309 + uprrrsauthor, 1264 1310 //setMainItem, 1265 1311 //isMainItem, 1266 1312 onPostClick, ··· 1285 1331 maxReplies, 1286 1332 }: { 1287 1333 post: PostView; 1334 + uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; 1288 1335 // optional for now because i havent ported every use to this yet 1289 1336 // setMainItem?: React.Dispatch< 1290 1337 // React.SetStateAction<AppBskyFeedDefs.FeedViewPost> ··· 1318 1365 const [hasLiked, setHasLiked] = useState<boolean>( 1319 1366 post.uri in likedPosts || post.viewer?.like ? true : false 1320 1367 ); 1368 + const [, setComposerPost] = useAtom(composerAtom); 1321 1369 const { agent } = useAuth(); 1322 1370 const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like); 1323 1371 const [retweetUri, setRetweetUri] = useState<string | undefined>( ··· 1414 1462 paddingLeft: isQuote ? 12 : 16, 1415 1463 paddingRight: isQuote ? 12 : 16, 1416 1464 //paddingTop: 16, 1417 - paddingTop: isRepost ? 10 : isQuote ? 12 : 16, 1465 + paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16, 1418 1466 //paddingBottom: bottomReplyLine ? 0 : 16, 1419 1467 paddingBottom: 0, 1420 1468 fontFamily: "system-ui, sans-serif", ··· 1455 1503 //left: 16 + (42 / 2), 1456 1504 width: 2, 1457 1505 //height: "100%", 1458 - height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6, 1506 + height: isRepost 1507 + ? "calc(16px + 1rem - 6px)" 1508 + : topReplyLine 1509 + ? 8 - 6 1510 + : 16 - 6, 1459 1511 // background: theme.textSecondary, 1460 1512 //opacity: 0.5, 1461 1513 // no flex here ··· 1463 1515 className="bg-gray-500 dark:bg-gray-400" 1464 1516 /> 1465 1517 )} 1466 - <div 1467 - style={{ 1468 - position: "absolute", 1469 - //top: isRepost ? "calc(16px + 1rem)" : 16, 1470 - //left: 16, 1471 - zIndex: 1, 1472 - top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16, 1473 - left: isQuote ? 12 : 16, 1474 - }} 1475 - onClick={onProfileClick} 1476 - > 1477 - <img 1478 - src={post.author.avatar || defaultpfp} 1479 - alt="avatar" 1480 - // transition={{ 1481 - // type: "spring", 1482 - // stiffness: 260, 1483 - // damping: 20, 1484 - // }} 1485 - style={{ 1486 - borderRadius: "50%", 1487 - marginRight: 12, 1488 - objectFit: "cover", 1489 - //background: theme.border, 1490 - //border: `1px solid ${theme.border}`, 1491 - width: isQuote ? 16 : 42, 1492 - height: isQuote ? 16 : 42, 1493 - }} 1494 - className="border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1495 - /> 1496 - </div> 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 + <div className=" flex flex-col justify-start"> 1561 + <FollowButton targetdidorhandle={post.author.did} /> 1562 + </div> 1563 + </div> 1564 + </div> 1565 + <div className="flex flex-col gap-3"> 1566 + <div> 1567 + <div className="text-gray-900 dark:text-gray-100 font-medium text-md"> 1568 + {post.author.displayName || post.author.handle}{" "} 1569 + </div> 1570 + <div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1"> 1571 + <Mutual targetdidorhandle={post.author.did} />@{post.author.handle}{" "} 1572 + </div> 1573 + </div> 1574 + {uprrrsauthor?.description && ( 1575 + <div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3"> 1576 + {uprrrsauthor.description} 1577 + </div> 1578 + )} 1579 + {/* <div className="flex gap-4"> 1580 + <div className="flex gap-1"> 1581 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1582 + 0 1583 + </div> 1584 + <div className="text-gray-500 dark:text-gray-400"> 1585 + Following 1586 + </div> 1587 + </div> 1588 + <div className="flex gap-1"> 1589 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1590 + 2,900 1591 + </div> 1592 + <div className="text-gray-500 dark:text-gray-400"> 1593 + Followers 1594 + </div> 1595 + </div> 1596 + </div> */} 1597 + </div> 1598 + </div> 1599 + 1600 + {/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */} 1601 + </HoverCard.Content> 1602 + </HoverCard.Portal> 1603 + </HoverCard.Root> 1604 + 1497 1605 <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1498 1606 <div 1499 1607 style={{ ··· 1507 1615 }} 1508 1616 > 1509 1617 {/* dummy for later use */} 1510 - <div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} /> 1618 + <div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} /> 1511 1619 {/* reply line !!!! bottomReplyLine */} 1512 1620 {bottomReplyLine && ( 1513 1621 <div ··· 1740 1848 }} 1741 1849 className="text-gray-500 dark:text-gray-400" 1742 1850 > 1743 - <span style={btnstyle}> 1744 - <MdiCommentOutline /> 1745 - {post.replyCount} 1746 - </span> 1747 1851 <HitSlopButton 1748 1852 onClick={() => { 1749 - repostOrUnrepostPost(); 1853 + setComposerPost({ kind: "reply", parent: post.uri }); 1750 1854 }} 1751 1855 style={{ 1752 1856 ...btnstyle, 1753 - ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1754 1857 }} 1755 1858 > 1756 - {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1757 - {(post.repostCount || 0) + (hasRetweeted ? 1 : 0)} 1859 + <MdiCommentOutline /> 1860 + {post.replyCount} 1758 1861 </HitSlopButton> 1862 + <DropdownMenu.Root modal={false}> 1863 + <DropdownMenu.Trigger asChild> 1864 + <div 1865 + style={{ 1866 + ...btnstyle, 1867 + ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1868 + }} 1869 + aria-label="Repost or quote post" 1870 + > 1871 + {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1872 + {post.repostCount ?? 0} 1873 + </div> 1874 + </DropdownMenu.Trigger> 1875 + 1876 + <DropdownMenu.Portal> 1877 + <DropdownMenu.Content 1878 + align="start" 1879 + sideOffset={5} 1880 + 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" 1881 + > 1882 + <DropdownMenu.Item 1883 + onSelect={repostOrUnrepostPost} 1884 + 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" 1885 + > 1886 + <MdiRepeat 1887 + className={hasRetweeted ? "text-green-400" : ""} 1888 + /> 1889 + <span>{hasRetweeted ? "Undo Repost" : "Repost"}</span> 1890 + </DropdownMenu.Item> 1891 + 1892 + <DropdownMenu.Item 1893 + onSelect={() => { 1894 + setComposerPost({ 1895 + kind: "quote", 1896 + subject: post.uri, 1897 + }); 1898 + }} 1899 + 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" 1900 + > 1901 + {/* You might want a specific quote icon here */} 1902 + <MdiCommentOutline /> 1903 + <span>Quote</span> 1904 + </DropdownMenu.Item> 1905 + </DropdownMenu.Content> 1906 + </DropdownMenu.Portal> 1907 + </DropdownMenu.Root> 1759 1908 <HitSlopButton 1760 1909 onClick={() => { 1761 1910 likeOrUnlikePost(); ··· 2508 2657 return { start, end, feature: f.features[0] }; 2509 2658 }); 2510 2659 } 2511 - function renderTextWithFacets({ 2660 + export function renderTextWithFacets({ 2512 2661 text, 2513 2662 facets, 2514 2663 navigate,
+59
src/components/radix-m3-rd/Button.tsx
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import clsx from "clsx"; 3 + import * as React from "react"; 4 + 5 + export type ButtonVariant = "filled" | "outlined" | "text" | "secondary"; 6 + export type ButtonSize = "sm" | "md" | "lg"; 7 + 8 + const variantClasses: Record<ButtonVariant, string> = { 9 + filled: 10 + "bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500", 11 + secondary: 12 + "bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500", 13 + outlined: 14 + "border border-gray-800 text-gray-800 hover:bg-gray-100 dark:border-gray-200 dark:text-gray-200 dark:hover:bg-gray-800/10", 15 + text: "bg-transparent text-gray-800 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-800/10", 16 + }; 17 + 18 + const sizeClasses: Record<ButtonSize, string> = { 19 + sm: "px-3 py-1.5 text-sm", 20 + md: "px-4 py-2 text-base", 21 + lg: "px-6 py-3 text-lg", 22 + }; 23 + 24 + export function Button({ 25 + variant = "filled", 26 + size = "md", 27 + asChild = false, 28 + ref, 29 + className, 30 + children, 31 + ...props 32 + }: { 33 + variant?: ButtonVariant; 34 + size?: ButtonSize; 35 + asChild?: boolean; 36 + className?: string; 37 + children?: React.ReactNode; 38 + ref?: React.Ref<HTMLButtonElement>; 39 + } & React.ComponentPropsWithoutRef<"button">) { 40 + const Comp = asChild ? Slot : "button"; 41 + 42 + return ( 43 + <Comp 44 + ref={ref} 45 + className={clsx( 46 + //focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-500 dark:focus:ring-gray-300 47 + "inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed", 48 + variantClasses[variant], 49 + sizeClasses[size], 50 + className 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + </Comp> 56 + ); 57 + } 58 + 59 + Button.displayName = "Button";
+2
src/main.tsx
··· 14 14 import { routeTree } from "./routeTree.gen"; 15 15 import { isAtTopAtom } from "./utils/atoms.ts"; 16 16 17 + //initAtomToCssVar(hueAtom, "--tw-gray-hue") 18 + 17 19 const queryClient = new QueryClient({ 18 20 defaultOptions: { 19 21 queries: {
+26 -23
src/providers/UnifiedAuthProvider.tsx
··· 1 - // src/providers/UnifiedAuthProvider.tsx 2 - // Import both Agent and the (soon to be deprecated) AtpAgent 3 1 import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 4 2 import { 5 3 type OAuthSession, ··· 7 5 TokenRefreshError, 8 6 TokenRevokedError, 9 7 } from "@atproto/oauth-client-browser"; 8 + import { useAtom } from "jotai"; 10 9 import React, { 11 10 createContext, 12 11 use, ··· 15 14 useState, 16 15 } from "react"; 17 16 18 - import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed 17 + import { quickAuthAtom } from "~/utils/atoms"; 18 + 19 + import { oauthClient } from "../utils/oauthClient"; 19 20 20 - // Define the unified status and authentication method 21 21 type AuthStatus = "loading" | "signedIn" | "signedOut"; 22 22 type AuthMethod = "password" | "oauth" | null; 23 23 24 24 interface AuthContextValue { 25 - agent: Agent | null; // The agent is typed as the base class `Agent` 25 + agent: Agent | null; 26 26 status: AuthStatus; 27 27 authMethod: AuthMethod; 28 28 loginWithPassword: ( ··· 41 41 }: { 42 42 children: React.ReactNode; 43 43 }) => { 44 - // The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances. 45 44 const [agent, setAgent] = useState<Agent | null>(null); 46 45 const [status, setStatus] = useState<AuthStatus>("loading"); 47 46 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 48 47 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 48 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 49 49 50 - // Unified Initialization Logic 51 50 const initialize = useCallback(async () => { 52 - // --- 1. Try OAuth initialization first --- 53 51 try { 54 52 const oauthResult = await oauthClient.init(); 55 53 if (oauthResult) { 56 54 // /*mass comment*/ console.log("OAuth session restored."); 57 - const apiAgent = new Agent(oauthResult.session); // Standard Agent 55 + const apiAgent = new Agent(oauthResult.session); 58 56 setAgent(apiAgent); 59 57 setOauthSession(oauthResult.session); 60 58 setAuthMethod("oauth"); 61 59 setStatus("signedIn"); 62 - return; // Success 60 + setQuickAuth(apiAgent?.did || null); 61 + return; 63 62 } 64 63 } catch (e) { 65 64 console.error("OAuth init failed, checking password session.", e); 65 + if (!quickAuth) { 66 + // quickAuth restoration. if last used method is oauth we immediately call for oauth redo 67 + // (and set a persistent atom somewhere to not retry again if it failed) 68 + } 66 69 } 67 70 68 - // --- 2. If no OAuth, try password-based session using AtpAgent --- 69 71 try { 70 72 const service = localStorage.getItem("service"); 71 73 const sessionString = localStorage.getItem("sess"); 72 74 73 75 if (service && sessionString) { 74 76 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 75 - // Use the original, working AtpAgent logic 76 77 const apiAgent = new AtpAgent({ service }); 77 78 const session: AtpSessionData = JSON.parse(sessionString); 78 79 await apiAgent.resumeSession(session); 79 80 80 81 // /*mass comment*/ console.log("Password-based session resumed successfully."); 81 - setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent 82 + setAgent(apiAgent); 82 83 setAuthMethod("password"); 83 84 setStatus("signedIn"); 84 - return; // Success 85 + setQuickAuth(apiAgent?.did || null); 86 + return; 85 87 } 86 88 } catch (e) { 87 89 console.error("Failed to resume password-based session.", e); ··· 89 91 localStorage.removeItem("service"); 90 92 } 91 93 92 - // --- 3. If neither worked, user is signed out --- 93 94 // /*mass comment*/ console.log("No active session found."); 94 95 setStatus("signedOut"); 95 96 setAgent(null); 96 97 setAuthMethod(null); 97 - }, []); 98 + // do we want to null it here? 99 + setQuickAuth(null); 100 + }, [quickAuth, setQuickAuth]); 98 101 99 102 useEffect(() => { 100 103 const handleOAuthSessionDeleted = ( ··· 105 108 setOauthSession(null); 106 109 setAuthMethod(null); 107 110 setStatus("signedOut"); 111 + setQuickAuth(null); 108 112 }; 109 113 110 114 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); ··· 113 117 return () => { 114 118 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 115 119 }; 116 - }, [initialize]); 120 + }, [initialize, setQuickAuth]); 117 121 118 - // --- Login Methods --- 119 122 const loginWithPassword = async ( 120 123 user: string, 121 124 password: string, ··· 125 128 setStatus("loading"); 126 129 try { 127 130 let sessionData: AtpSessionData | undefined; 128 - // Use the AtpAgent for its simple login and session persistence 129 131 const apiAgent = new AtpAgent({ 130 132 service, 131 133 persistSession: (_evt, sess) => { ··· 137 139 if (sessionData) { 138 140 localStorage.setItem("service", service); 139 141 localStorage.setItem("sess", JSON.stringify(sessionData)); 140 - setAgent(apiAgent); // Store the AtpAgent instance in our state 142 + setAgent(apiAgent); 141 143 setAuthMethod("password"); 142 144 setStatus("signedIn"); 145 + setQuickAuth(apiAgent?.did || null); 143 146 // /*mass comment*/ console.log("Successfully logged in with password."); 144 147 } else { 145 148 throw new Error("Session data not persisted after login."); ··· 147 150 } catch (e) { 148 151 console.error("Password login failed:", e); 149 152 setStatus("signedOut"); 153 + setQuickAuth(null); 150 154 throw e; 151 155 } 152 156 }; ··· 161 165 } 162 166 }, [status]); 163 167 164 - // --- Unified Logout --- 165 168 const logout = useCallback(async () => { 166 169 if (status !== "signedIn" || !agent) return; 167 170 setStatus("loading"); ··· 173 176 } else if (authMethod === "password") { 174 177 localStorage.removeItem("service"); 175 178 localStorage.removeItem("sess"); 176 - // AtpAgent has its own logout methods 177 179 await (agent as AtpAgent).com.atproto.server.deleteSession(); 178 180 // /*mass comment*/ console.log("Password-based session deleted."); 179 181 } ··· 184 186 setAuthMethod(null); 185 187 setOauthSession(null); 186 188 setStatus("signedOut"); 189 + setQuickAuth(null); 187 190 } 188 - }, [status, authMethod, agent, oauthSession]); 191 + }, [status, agent, authMethod, oauthSession, setQuickAuth]); 189 192 190 193 return ( 191 194 <AuthContext
+42 -111
src/routes/__root.tsx
··· 12 12 useNavigate, 13 13 } from "@tanstack/react-router"; 14 14 import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 15 - import { useState } from "react"; 15 + import { useAtom } from "jotai"; 16 16 import * as React from "react"; 17 17 import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 18 18 19 + import { Composer } from "~/components/Composer"; 19 20 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 21 + import { Import } from "~/components/Import"; 20 22 import Login from "~/components/Login"; 21 23 import { NotFound } from "~/components/NotFound"; 24 + import { FluentEmojiHighContrastGlowingStar } from "~/components/Star"; 22 25 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 26 + import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms"; 23 27 import { seo } from "~/utils/seo"; 24 28 25 29 export const Route = createRootRouteWithContext<{ ··· 85 89 } 86 90 87 91 function RootDocument({ children }: { children: React.ReactNode }) { 92 + useAtomCssVar(hueAtom, "--tw-gray-hue"); 88 93 const location = useLocation(); 89 94 const navigate = useNavigate(); 90 95 const { agent } = useAuth(); ··· 117 122 ? "profile" 118 123 : "home"; 119 124 120 - const [postOpen, setPostOpen] = useState(false); 121 - const [postText, setPostText] = useState(""); 122 - const [posting, setPosting] = useState(false); 123 - const [postSuccess, setPostSuccess] = useState(false); 124 - const [postError, setPostError] = useState<string | null>(null); 125 - 126 - async function handlePost() { 127 - if (!agent) return; 128 - setPosting(true); 129 - setPostError(null); 130 - try { 131 - await agent.com.atproto.repo.createRecord({ 132 - collection: "app.bsky.feed.post", 133 - repo: agent.assertDid, 134 - record: { 135 - $type: "app.bsky.feed.post", 136 - text: postText, 137 - createdAt: new Date().toISOString(), 138 - }, 139 - }); 140 - setPostSuccess(true); 141 - setPostText(""); 142 - setTimeout(() => { 143 - setPostSuccess(false); 144 - setPostOpen(false); 145 - }, 1500); 146 - } catch (e: any) { 147 - setPostError(e?.message || "Failed to post"); 148 - } finally { 149 - setPosting(false); 150 - } 151 - } 125 + const [, setComposerPost] = useAtom(composerAtom); 152 126 153 127 return ( 154 128 <> 155 - {postOpen && ( 156 - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> 157 - <div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md relative"> 158 - <button 159 - className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" 160 - onClick={() => !posting && setPostOpen(false)} 161 - disabled={posting} 162 - aria-label="Close" 163 - > 164 - ร— 165 - </button> 166 - <h2 className="text-lg font-bold mb-2">Create Post</h2> 167 - {postSuccess ? ( 168 - <div className="flex flex-col items-center justify-center py-8"> 169 - <span className="text-green-500 text-4xl mb-2">โœ“</span> 170 - <span className="text-green-600">Posted!</span> 171 - </div> 172 - ) : ( 173 - <> 174 - <textarea 175 - className="w-full border rounded p-2 mb-2 dark:bg-gray-800 dark:border-gray-700" 176 - rows={4} 177 - placeholder="What's on your mind?" 178 - value={postText} 179 - onChange={(e) => setPostText(e.target.value)} 180 - disabled={posting} 181 - autoFocus 182 - /> 183 - {postError && ( 184 - <div className="text-red-500 text-sm mb-2">{postError}</div> 185 - )} 186 - <button 187 - className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50" 188 - onClick={handlePost} 189 - disabled={posting || !postText.trim()} 190 - > 191 - {posting ? "Posting..." : "Post"} 192 - </button> 193 - </> 194 - )} 195 - </div> 196 - </div> 197 - )} 129 + <Composer /> 198 130 199 131 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 200 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"> 201 133 <div className="flex items-center gap-3 mb-4"> 202 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 134 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 203 135 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 204 136 Red Dwarf{" "} 205 137 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 223 155 /> 224 156 225 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 226 170 InactiveIcon={ 227 171 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 228 172 } ··· 249 193 }) 250 194 } 251 195 text="Feeds" 252 - /> 253 - <MaterialNavItem 254 - InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 255 - ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 256 - active={locationEnum === "search"} 257 - onClickCallbback={() => 258 - navigate({ 259 - to: "/search", 260 - //params: { did: agent.assertDid }, 261 - }) 262 - } 263 - text="Search" 264 196 /> 265 197 <MaterialNavItem 266 198 InactiveIcon={ ··· 300 232 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 301 233 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 302 234 //active={true} 303 - onClickCallbback={() => setPostOpen(true)} 235 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 304 236 text="Post" 305 237 /> 306 238 </div> ··· 438 370 439 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"> 440 372 <div className="flex items-center gap-3 mb-4"> 441 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 373 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 442 374 </div> 443 375 <MaterialNavItem 444 376 small ··· 458 390 459 391 <MaterialNavItem 460 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 461 406 InactiveIcon={ 462 407 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 463 408 } ··· 488 433 /> 489 434 <MaterialNavItem 490 435 small 491 - InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 492 - ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 493 - active={locationEnum === "search"} 494 - onClickCallbback={() => 495 - navigate({ 496 - to: "/search", 497 - //params: { did: agent.assertDid }, 498 - }) 499 - } 500 - text="Search" 501 - /> 502 - <MaterialNavItem 503 - small 504 436 InactiveIcon={ 505 437 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 506 438 } ··· 540 472 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 541 473 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 542 474 //active={true} 543 - onClickCallbback={() => setPostOpen(true)} 475 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 544 476 text="Post" 545 477 /> 546 478 </div> ··· 550 482 <button 551 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" 552 484 style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 553 - onClick={() => setPostOpen(true)} 485 + onClick={() => setComposerPost({ kind: 'root' })} 554 486 type="button" 555 487 aria-label="Create Post" 556 488 > ··· 567 499 </main> 568 500 569 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> 570 503 <Login /> 571 504 572 505 <div className="flex-1"></div> 573 506 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 574 - Red Dwarf is a bluesky client that uses Constellation and direct PDS 575 - queries. Skylite would be a self-hosted bluesky "instance". Stay 576 - tuned for the release of Skylite. 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) 577 508 </p> 578 509 </aside> 579 510 </div> ··· 622 553 //params: { did: agent.assertDid }, 623 554 }) 624 555 } 625 - text="Search" 556 + text="Explore" 626 557 /> 627 558 {/* <Link 628 559 to="/search" ··· 749 680 ) : ( 750 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"> 751 682 <div className="flex items-center gap-2"> 752 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" /> 683 + <FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 753 684 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 754 685 Red Dwarf{" "} 755 686 {/* <span className="text-gray-500 dark:text-gray-400 text-sm">
+40 -40
src/routes/index.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useAtom } from "jotai"; 3 3 import * as React from "react"; 4 - import { useEffect, useLayoutEffect } from "react"; 4 + import { useLayoutEffect, useState } from "react"; 5 5 6 6 import { Header } from "~/components/Header"; 7 7 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 9 import { 10 - agentAtom, 11 - authedAtom, 12 10 feedScrollPositionsAtom, 13 11 isAtTopAtom, 12 + quickAuthAtom, 14 13 selectedFeedUriAtom, 15 - store, 16 14 } from "~/utils/atoms"; 17 15 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 18 16 import { ··· 107 105 } = useAuth(); 108 106 const authed = !!agent?.did; 109 107 110 - useEffect(() => { 111 - if (agent?.did) { 112 - store.set(authedAtom, true); 113 - } else { 114 - store.set(authedAtom, false); 115 - } 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); 124 - } 125 - }, [status, agent, authed]); 108 + // i dont remember why this is even here 109 + // useEffect(() => { 110 + // if (agent?.did) { 111 + // store.set(authedAtom, true); 112 + // } else { 113 + // store.set(authedAtom, false); 114 + // } 115 + // }, [status, agent, authed]); 116 + // useEffect(() => { 117 + // if (agent) { 118 + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 119 + // // @ts-ignore 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); 123 + // } 124 + // }, [status, agent, authed]); 126 125 127 126 //const { get, set } = usePersistentStore(); 128 127 // const [feed, setFeed] = React.useState<any[]>([]); ··· 162 161 163 162 // const savedFeeds = savedFeedsPref?.items || []; 164 163 165 - const identityresultmaybe = useQueryIdentity(agent?.did); 164 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 165 + const isAuthRestoring = quickAuth ? status === "loading" : false; 166 + 167 + const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined); 166 168 const identity = identityresultmaybe?.data; 167 169 168 170 const prefsresultmaybe = useQueryPreferences({ 169 - agent: agent ?? undefined, 170 - pdsUrl: identity?.pds, 171 + agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 172 + pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined, 171 173 }); 172 174 const prefs = prefsresultmaybe?.data; 173 175 ··· 178 180 return savedFeedsPref?.items || []; 179 181 }, [prefs]); 180 182 181 - const [persistentSelectedFeed, setPersistentSelectedFeed] = 182 - useAtom(selectedFeedUriAtom); // React.useState<string | null>(null); 183 - const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState( 184 - persistentSelectedFeed 185 - ); // React.useState<string | null>(null); 183 + const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 184 + const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed); 186 185 const selectedFeed = agent?.did 187 186 ? persistentSelectedFeed 188 187 : unauthedSelectedFeed; ··· 306 305 }, [scrollPositions]); 307 306 308 307 useLayoutEffect(() => { 308 + if (isAuthRestoring) return; 309 309 const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 310 311 311 window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 312 // eslint-disable-next-line react-hooks/exhaustive-deps 313 - }, [selectedFeed]); 313 + }, [selectedFeed, isAuthRestoring]); 314 314 315 315 useLayoutEffect(() => { 316 - if (!selectedFeed) return; 316 + if (!selectedFeed || isAuthRestoring) return; 317 317 318 318 const handleScroll = () => { 319 319 scrollPositionsRef.current = { ··· 328 328 329 329 setScrollPositions(scrollPositionsRef.current); 330 330 }; 331 - }, [selectedFeed, setScrollPositions]); 331 + }, [isAuthRestoring, selectedFeed, setScrollPositions]); 332 332 333 - const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 334 - const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; 333 + const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined); 334 + const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined; 335 335 336 336 // const { 337 337 // data: feedData, ··· 347 347 348 348 // const feed = feedData?.feed || []; 349 349 350 - const isReadyForAuthedFeed = 351 - authed && agent && identity?.pds && feedServiceDid; 352 - const isReadyForUnauthedFeed = !authed && selectedFeed; 350 + const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid; 351 + const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed; 353 352 354 353 355 354 const [isAtTop] = useAtom(isAtTopAtom); ··· 358 357 <div 359 358 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 360 359 > 361 - {savedFeeds.length > 0 ? ( 360 + {!isAuthRestoring && savedFeeds.length > 0 ? ( 362 361 <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 362 {savedFeeds.map((item: any, idx: number) => { 364 363 const label = item.value.split("/").pop() || item.value; ··· 410 409 /> 411 410 ))} */} 412 411 413 - {authed && (!identity?.pds || !feedServiceDid) && ( 412 + {isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && ( 414 413 <div className="p-4 text-center text-gray-500"> 415 414 Preparing your feed... 416 415 </div> 417 416 )} 418 417 419 - {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 418 + {!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? ( 420 419 <InfiniteCustomFeed 420 + key={selectedFeed!} 421 421 feedUri={selectedFeed!} 422 422 pdsUrl={identity?.pds} 423 423 feedServiceDid={feedServiceDid} 424 424 /> 425 425 ) : ( 426 426 <div className="p-4 text-center text-gray-500"> 427 - Select a feed to get started. 427 + Loading....... 428 428 </div> 429 429 )} 430 430 {/* {false && restoringScrollPosition && (
+7 -3
src/routes/notifications.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 2 3 import React, { useEffect, useRef,useState } from "react"; 3 4 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 + import { constellationURLAtom } from "~/utils/atoms"; 5 7 6 8 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 7 9 ··· 70 72 } 71 73 } 72 74 75 + const [constellationURL] = useAtom(constellationURLAtom) 76 + 73 77 useEffect(() => { 74 78 if (!did) return; 75 79 setLoading(true); 76 80 setError(null); 77 81 const urls = [ 78 - `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`, 79 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`, 80 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`, 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`, 81 85 ]; 82 86 let ignore = false; 83 87 Promise.all(
+181 -53
src/routes/profile.$did/index.tsx
··· 1 + import { RichText } from "@atproto/api"; 1 2 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute } from "@tanstack/react-router"; 3 - import React from "react"; 3 + import { createFileRoute, useNavigate } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 + import React, { type ReactNode, useEffect, useState } from "react"; 4 6 5 7 import { Header } from "~/components/Header"; 6 - import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 8 + import { Button } from "~/components/radix-m3-rd/Button"; 9 + import { 10 + renderTextWithFacets, 11 + UniversalPostRendererATURILoader, 12 + } from "~/components/UniversalPostRenderer"; 7 13 import { useAuth } from "~/providers/UnifiedAuthProvider"; 8 - import { toggleFollow, useGetFollowState } from "~/utils/followState"; 14 + import { imgCDNAtom } from "~/utils/atoms"; 15 + import { 16 + toggleFollow, 17 + useGetFollowState, 18 + useGetOneToOneState, 19 + } from "~/utils/followState"; 9 20 import { 10 21 useInfiniteQueryAuthorFeed, 11 22 useQueryIdentity, ··· 19 30 function ProfileComponent() { 20 31 // booo bad this is not always the did it might be a handle, use identity.did instead 21 32 const { did } = Route.useParams(); 33 + const navigate = useNavigate(); 22 34 const queryClient = useQueryClient(); 23 - const { agent } = useAuth(); 24 35 const { 25 36 data: identity, 26 37 isLoading: isIdentityLoading, 27 38 error: identityError, 28 39 } = useQueryIdentity(did); 29 - 30 - const followRecords = useGetFollowState({ 31 - target: identity?.did || did, 32 - user: agent?.did, 33 - }); 34 40 35 41 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 36 42 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; ··· 67 73 [postsData] 68 74 ); 69 75 76 + const [imgcdn] = useAtom(imgCDNAtom); 77 + 70 78 function getAvatarUrl(p: typeof profile) { 71 79 const link = p?.avatar?.ref?.["$link"]; 72 80 if (!link || !resolvedDid) return null; 73 - return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 81 + return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 74 82 } 75 83 function getBannerUrl(p: typeof profile) { 76 84 const link = p?.banner?.ref?.["$link"]; 77 85 if (!link || !resolvedDid) return null; 78 - return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`; 86 + return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`; 79 87 } 80 88 81 89 const displayName = ··· 162 170 also delay the backfill to be on demand because it would be pretty intense 163 171 also save it persistently 164 172 */} 165 - {identity?.did !== agent?.did ? ( 166 - <> 167 - {!(followRecords?.length && followRecords?.length > 0) ? ( 168 - <button 169 - onClick={() => 170 - toggleFollow({ 171 - agent: agent || undefined, 172 - targetDid: identity?.did, 173 - followRecords: followRecords, 174 - queryClient: queryClient, 175 - }) 176 - } 177 - className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]" 178 - > 179 - Follow 180 - </button> 181 - ) : ( 182 - <button 183 - onClick={() => 184 - toggleFollow({ 185 - agent: agent || undefined, 186 - targetDid: identity?.did, 187 - followRecords: followRecords, 188 - queryClient: queryClient, 189 - }) 190 - } 191 - className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]" 192 - > 193 - Unfollow 194 - </button> 195 - )} 196 - </> 197 - ) : ( 198 - <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 199 - Edit Profile 200 - </button> 201 - )} 202 - <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 173 + <FollowButton targetdidorhandle={did} /> 174 + <Button className="rounded-full" variant={"secondary"}> 203 175 ... {/* todo: icon */} 204 - </button> 176 + </Button> 205 177 </div> 206 178 207 179 {/* Info Card */} 208 180 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 209 181 <div className="font-bold text-2xl">{displayName}</div> 210 - <div className="text-gray-500 dark:text-gray-400 text-base mb-3"> 182 + <div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1"> 183 + <Mutual targetdidorhandle={did} /> 211 184 {handle} 212 185 </div> 213 186 {description && ( 214 187 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 215 - {description} 188 + {/* {description} */} 189 + <RichTextRenderer key={did} description={description} /> 216 190 </div> 217 191 )} 218 192 </div> ··· 255 229 </> 256 230 ); 257 231 } 232 + 233 + export function FollowButton({ 234 + targetdidorhandle, 235 + }: { 236 + targetdidorhandle: string; 237 + }) { 238 + const { agent } = useAuth(); 239 + const { data: identity } = useQueryIdentity(targetdidorhandle); 240 + const queryClient = useQueryClient(); 241 + 242 + const followRecords = useGetFollowState({ 243 + target: identity?.did ?? targetdidorhandle, 244 + user: agent?.did, 245 + }); 246 + 247 + return ( 248 + <> 249 + {identity?.did !== agent?.did ? ( 250 + <> 251 + {!(followRecords?.length && followRecords?.length > 0) ? ( 252 + <Button 253 + onClick={(e) => { 254 + e.stopPropagation(); 255 + toggleFollow({ 256 + agent: agent || undefined, 257 + targetDid: identity?.did, 258 + followRecords: followRecords, 259 + queryClient: queryClient, 260 + }); 261 + }} 262 + > 263 + Follow 264 + </Button> 265 + ) : ( 266 + <Button 267 + onClick={(e) => { 268 + e.stopPropagation(); 269 + toggleFollow({ 270 + agent: agent || undefined, 271 + targetDid: identity?.did, 272 + followRecords: followRecords, 273 + queryClient: queryClient, 274 + }); 275 + }} 276 + > 277 + Unfollow 278 + </Button> 279 + )} 280 + </> 281 + ) : ( 282 + <Button variant={"secondary"}>Edit Profile</Button> 283 + )} 284 + </> 285 + ); 286 + } 287 + 288 + export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { 289 + const { agent } = useAuth(); 290 + const { data: identity } = useQueryIdentity(targetdidorhandle); 291 + 292 + const theyFollowYouRes = useGetOneToOneState( 293 + agent?.did 294 + ? { 295 + target: agent?.did, 296 + user: identity?.did ?? targetdidorhandle, 297 + collection: "app.bsky.graph.follow", 298 + path: ".subject", 299 + } 300 + : undefined 301 + ); 302 + 303 + const youFollowThemRes = useGetFollowState({ 304 + target: identity?.did ?? targetdidorhandle, 305 + user: agent?.did, 306 + }); 307 + 308 + const theyFollowYou: boolean = 309 + !!theyFollowYouRes?.length && theyFollowYouRes.length > 0; 310 + const youFollowThem: boolean = 311 + !!youFollowThemRes?.length && youFollowThemRes.length > 0; 312 + 313 + return ( 314 + <> 315 + {/* if not self */} 316 + {identity?.did !== agent?.did ? ( 317 + <> 318 + {theyFollowYou ? ( 319 + <> 320 + {youFollowThem ? ( 321 + <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"> 322 + mutuals 323 + </div> 324 + ) : ( 325 + <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"> 326 + follows you 327 + </div> 328 + )} 329 + </> 330 + ) : ( 331 + <></> 332 + )} 333 + </> 334 + ) : ( 335 + // lmao can someone be mutuals with themselves ?? 336 + <></> 337 + )} 338 + </> 339 + ); 340 + } 341 + 342 + export function RichTextRenderer({ description }: { description: string }) { 343 + const [richDescription, setRichDescription] = useState<string | ReactNode[]>( 344 + description 345 + ); 346 + const { agent } = useAuth(); 347 + const navigate = useNavigate(); 348 + 349 + useEffect(() => { 350 + let mounted = true; 351 + 352 + // setRichDescription(description); 353 + 354 + async function processRichText() { 355 + try { 356 + if (!agent?.did) return; 357 + const rt = new RichText({ text: description }); 358 + await rt.detectFacets(agent); 359 + 360 + if (!mounted) return; 361 + 362 + if (rt.facets) { 363 + setRichDescription( 364 + renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate }) 365 + ); 366 + } else { 367 + setRichDescription(rt.text); 368 + } 369 + } catch (error) { 370 + console.error("Failed to detect facets:", error); 371 + if (mounted) { 372 + setRichDescription(description); 373 + } 374 + } 375 + } 376 + 377 + processRichText(); 378 + 379 + return () => { 380 + mounted = false; 381 + }; 382 + }, [description, agent, navigate]); 383 + 384 + return <>{richDescription}</>; 385 + }
+1 -1
src/routes/profile.$did/post.$rkey.image.$i.tsx
··· 85 85 e.stopPropagation(); 86 86 e.nativeEvent.stopImmediatePropagation(); 87 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" 88 + className="lightbox-sidebar hidden lg:flex overscroll-none disablegutter disablescroll 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 89 > 90 90 <ProfilePostComponent 91 91 key={`/profile/${did}/post/${rkey}`}
+171 -64
src/routes/profile.$did/post.$rkey.tsx
··· 1 1 import { AtUri } from "@atproto/api"; 2 2 import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 3 3 import { createFileRoute, Outlet } from "@tanstack/react-router"; 4 - import React, { useEffect, useLayoutEffect } from "react"; 4 + import { useAtom } from "jotai"; 5 + import React, { useLayoutEffect } from "react"; 5 6 6 7 import { Header } from "~/components/Header"; 7 8 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 9 + import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms"; 8 10 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 9 11 import { 10 12 constructPostQuery, 13 + type linksAllResponse, 14 + type linksRecordsResponse, 15 + useQueryConstellation, 11 16 useQueryIdentity, 12 17 useQueryPost, 13 18 yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, ··· 40 45 did, 41 46 rkey, 42 47 nopics, 43 - lightboxCallback 48 + lightboxCallback, 44 49 }: { 45 50 did: string; 46 51 rkey: string; 47 52 nopics?: boolean; 48 - lightboxCallback?: (d:LightboxProps) => void; 53 + lightboxCallback?: (d: LightboxProps) => void; 49 54 }) { 50 55 //const { get, set } = usePersistentStore(); 51 56 const queryClient = useQueryClient(); ··· 193 198 () => 194 199 resolvedDid 195 200 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 196 - : "", 201 + : undefined, 197 202 [resolvedDid, rkey] 198 203 ); 199 204 200 205 const { data: mainPost } = useQueryPost(atUri); 201 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 + 202 273 // const { data: repliesData } = useQueryConstellation({ 203 274 // method: "/links", 204 275 // target: atUri, ··· 206 277 // path: ".reply.parent.uri", 207 278 // }); 208 279 // const replies = repliesData?.linking_records.slice(0, 50) ?? []; 280 + const [constellationurl] = useAtom(constellationURLAtom) 281 + 209 282 const infinitequeryresults = useInfiniteQuery({ 210 283 ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 211 284 { 285 + constellation: constellationurl, 212 286 method: "/links", 213 287 target: atUri, 214 288 collection: "app.bsky.feed.post", ··· 219 293 }); 220 294 221 295 const { 222 - data: repliesData, 223 - // fetchNextPage, 224 - // hasNextPage, 225 - // isFetchingNextPage, 296 + data: infiniteRepliesData, 297 + fetchNextPage, 298 + hasNextPage, 299 + isFetchingNextPage, 226 300 } = infinitequeryresults; 227 301 228 - // auto-fetch all pages 229 - useEffect(() => { 230 - if ( 231 - infinitequeryresults.hasNextPage && 232 - !infinitequeryresults.isFetchingNextPage 233 - ) { 234 - console.log("Fetching the next page..."); 235 - infinitequeryresults.fetchNextPage(); 236 - } 237 - }, [infinitequeryresults]); 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 + // : []; 238 323 239 - const replyAturis = repliesData 240 - ? repliesData.pages.flatMap((page) => 241 - page 242 - ? page.linking_records.map((record) => { 243 - const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 244 - return aturi; 245 - }) 246 - : [] 247 - ) 248 - : []; 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 + ) ?? []; 249 333 250 - const opdid = new AtUri(atUri).host; 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]); 251 353 252 354 // Find oldest OP reply 253 355 const oldestOpsIndex = replyAturis.findIndex( ··· 260 362 replyAturis.unshift(oldestOpsReply); 261 363 } 262 364 263 - 264 365 const [parents, setParents] = React.useState<any[]>([]); 265 366 const [parentsLoading, setParentsLoading] = React.useState(false); 266 367 267 368 const mainPostRef = React.useRef<HTMLDivElement>(null); 268 - const userHasScrolled = React.useRef(false); 369 + const hasPerformedInitialLayout = React.useRef(false); 370 + 371 + const [layoutReady, setLayoutReady] = React.useState(false); 269 372 270 - const scrollAnchor = React.useRef<{ top: number } | null>(null); 373 + useLayoutEffect(() => { 374 + if (parents.length > 0 && !layoutReady && mainPostRef.current) { 375 + const mainPostElement = mainPostRef.current; 271 376 272 - React.useEffect(() => { 273 - const onScroll = () => { 274 - if (window.scrollY > 50) { 275 - userHasScrolled.current = true; 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); 276 384 277 - window.removeEventListener("scroll", onScroll); 385 + hasPerformedInitialLayout.current = true; 278 386 } 279 - }; 280 - 281 - if (!userHasScrolled.current) { 282 - window.addEventListener("scroll", onScroll, { passive: true }); 387 + 388 + // todo idk what to do with this 389 + // eslint-disable-next-line react-hooks/set-state-in-effect 390 + setLayoutReady(true); 283 391 } 284 - return () => window.removeEventListener("scroll", onScroll); 285 - }, []); 392 + }, [parents, layoutReady]); 286 393 287 - useLayoutEffect(() => { 288 - if (parentsLoading && mainPostRef.current && !userHasScrolled.current) { 289 - scrollAnchor.current = { 290 - top: mainPostRef.current.getBoundingClientRect().top, 291 - }; 394 + 395 + const [slingshoturl] = useAtom(slingshotURLAtom) 396 + 397 + React.useEffect(() => { 398 + if (parentsLoading) { 399 + setLayoutReady(false); 292 400 } 293 - }, [parentsLoading]); 294 401 295 - useLayoutEffect(() => { 296 - if ( 297 - scrollAnchor.current && 298 - mainPostRef.current && 299 - !userHasScrolled.current 300 - ) { 301 - const newTop = mainPostRef.current.getBoundingClientRect().top; 302 - const topDiff = newTop - scrollAnchor.current.top; 303 - if (topDiff > 0) { 304 - window.scrollBy(0, topDiff); 305 - } 306 - scrollAnchor.current = null; 402 + if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) { 403 + setLayoutReady(true); 404 + hasPerformedInitialLayout.current = true; 307 405 } 308 - }, [parents]); 406 + }, [parentsLoading, mainPost]); 309 407 310 408 React.useEffect(() => { 311 409 if (!mainPost?.value?.reply?.parent?.uri) { ··· 324 422 while (currentParentUri && safetyCounter < MAX_PARENTS) { 325 423 try { 326 424 const parentPost = await queryClient.fetchQuery( 327 - constructPostQuery(currentParentUri) 425 + constructPostQuery(currentParentUri, slingshoturl) 328 426 ); 329 427 if (!parentPost) break; 330 428 parentChain.push(parentPost); ··· 407 505 //margin: "0px auto 0", 408 506 padding: 0, 409 507 minHeight: "80dvh", 410 - paddingBottom: "20dvh" 508 + paddingBottom: "20dvh", 411 509 }} 412 510 > 413 511 <div ··· 432 530 /> 433 531 ); 434 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 + )} 435 542 </div> 436 543 </div> 437 544 </>
+50 -1
src/routes/search.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 3 + import { Header } from "~/components/Header"; 4 + import { Import } from "~/components/Import"; 5 + 3 6 export const Route = createFileRoute("/search")({ 4 7 component: Search, 5 8 }); 6 9 7 10 export function Search() { 8 - return <div className="p-6">Search page (coming soon)</div>; 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 + ); 9 58 }
+164 -1
src/routes/settings.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + import { Slider } from "radix-ui"; 2 4 3 5 import { Header } from "~/components/Header"; 4 6 import Login from "~/components/Login"; 7 + import { 8 + constellationURLAtom, 9 + defaultconstellationURL, 10 + defaulthue, 11 + defaultImgCDN, 12 + defaultslingshotURL, 13 + defaultVideoCDN, 14 + hueAtom, 15 + imgCDNAtom, 16 + slingshotURLAtom, 17 + videoCDNAtom, 18 + } from "~/utils/atoms"; 5 19 6 20 export const Route = createFileRoute("/settings")({ 7 21 component: Settings, ··· 20 34 } 21 35 }} 22 36 /> 23 - <Login /> 37 + <div className="lg:hidden"> 38 + <Login /> 39 + </div> 40 + <div className="h-4" /> 41 + <TextInputSetting 42 + atom={constellationURLAtom} 43 + title={"Constellation"} 44 + description={ 45 + "Customize the Constellation instance to be used by Red Dwarf" 46 + } 47 + init={defaultconstellationURL} 48 + /> 49 + <TextInputSetting 50 + atom={slingshotURLAtom} 51 + title={"Slingshot"} 52 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 53 + init={defaultslingshotURL} 54 + /> 55 + <TextInputSetting 56 + atom={imgCDNAtom} 57 + title={"Image CDN"} 58 + description={ 59 + "Customize the Constellation instance to be used by Red Dwarf" 60 + } 61 + init={defaultImgCDN} 62 + /> 63 + <TextInputSetting 64 + atom={videoCDNAtom} 65 + title={"Video CDN"} 66 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 67 + init={defaultVideoCDN} 68 + /> 69 + 70 + <Hue /> 71 + <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm"> 72 + please restart/refresh the app if changes arent applying correctly 73 + </p> 24 74 </> 25 75 ); 26 76 } 77 + function Hue() { 78 + const [hue, setHue] = useAtom(hueAtom); 79 + return ( 80 + <div className="flex flex-col px-4 mt-4 "> 81 + <span className="z-10">Hue</span> 82 + <div className="flex flex-row items-center gap-4"> 83 + <SliderComponent 84 + atom={hueAtom} 85 + max={360} 86 + /> 87 + <button 88 + onClick={() => setHue(defaulthue ?? 28)} 89 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 90 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 91 + > 92 + Reset 93 + </button> 94 + </div> 95 + </div> 96 + ); 97 + } 98 + 99 + export function TextInputSetting({ 100 + atom, 101 + title, 102 + description, 103 + init, 104 + }: { 105 + atom: typeof constellationURLAtom; 106 + title?: string; 107 + description?: string; 108 + init?: string; 109 + }) { 110 + const [value, setValue] = useAtom(atom); 111 + return ( 112 + <div className="flex flex-col gap-2 px-4 py-2"> 113 + {/* <div> 114 + {title && ( 115 + <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> 116 + {title} 117 + </h3> 118 + )} 119 + {description && ( 120 + <p className="text-sm text-gray-500 dark:text-gray-400"> 121 + {description} 122 + </p> 123 + )} 124 + </div> */} 125 + 126 + <div className="flex flex-row gap-2 items-center"> 127 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 128 + <input 129 + type="text" 130 + placeholder=" " 131 + value={value} 132 + onChange={(e) => setValue(e.target.value)} 133 + /> 134 + <label>{title}</label> 135 + </div> 136 + {/* <input 137 + type="text" 138 + value={value} 139 + onChange={(e) => setValue(e.target.value)} 140 + className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 141 + text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 142 + focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600" 143 + placeholder="Enter value..." 144 + /> */} 145 + <button 146 + onClick={() => setValue(init ?? "")} 147 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 148 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 149 + > 150 + Reset 151 + </button> 152 + </div> 153 + </div> 154 + ); 155 + } 156 + 157 + 158 + interface SliderProps { 159 + atom: typeof hueAtom; 160 + min?: number; 161 + max?: number; 162 + step?: number; 163 + } 164 + 165 + export const SliderComponent: React.FC<SliderProps> = ({ 166 + atom, 167 + min = 0, 168 + max = 100, 169 + step = 1, 170 + }) => { 171 + 172 + const [value, setValue] = useAtom(atom) 173 + 174 + return ( 175 + <Slider.Root 176 + className="relative flex items-center w-full h-4" 177 + value={[value]} 178 + min={min} 179 + max={max} 180 + step={step} 181 + onValueChange={(v: number[]) => setValue(v[0])} 182 + > 183 + <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 184 + <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 185 + </Slider.Track> 186 + <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" /> 187 + </Slider.Root> 188 + ); 189 + };
+139 -13
src/styles/app.css
··· 15 15 --color-gray-950: oklch(0.129 0.050 222.000); 16 16 } */ 17 17 18 + :root { 19 + --safe-hue: var(--tw-gray-hue, 28) 20 + } 21 + 18 22 @theme { 19 - --color-gray-50: oklch(0.984 0.012 28); 20 - --color-gray-100: oklch(0.968 0.017 28); 21 - --color-gray-200: oklch(0.929 0.025 28); 22 - --color-gray-300: oklch(0.869 0.035 28); 23 - --color-gray-400: oklch(0.704 0.05 28); 24 - --color-gray-500: oklch(0.554 0.06 28); 25 - --color-gray-600: oklch(0.446 0.058 28); 26 - --color-gray-700: oklch(0.372 0.058 28); 27 - --color-gray-800: oklch(0.279 0.055 28); 28 - --color-gray-900: oklch(0.208 0.055 28); 29 - --color-gray-950: oklch(0.129 0.055 28); 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)); 30 34 } 31 35 32 36 @layer base { ··· 48 52 } 49 53 } 50 54 55 + .gutter{ 56 + scrollbar-gutter: stable both-edges; 57 + } 58 + 51 59 @media (width >= 64rem /* 1024px */) { 52 60 html:not(:has(.disablegutter)), 53 61 body:not(:has(.disablegutter)) { 54 62 scrollbar-gutter: stable both-edges !important; 55 63 } 56 - html:has(.disablegutter), 57 - body:has(.disablegutter) { 64 + html:has(.disablescroll), 65 + body:has(.disablescroll) { 58 66 scrollbar-width: none; 59 67 overflow-y: hidden; 60 68 } ··· 105 113 :root { 106 114 --shadow-opacity: calc(1 - var(--is-top)); 107 115 --tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15)); 116 + } 117 + 118 + 119 + /* m3 input */ 120 + :root { 121 + --m3input-radius: 6px; 122 + --m3input-border-width: .0625rem; 123 + --m3input-font-size: 16px; 124 + --m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1); 125 + /* light theme */ 126 + --m3input-bg: var(--color-gray-50); 127 + --m3input-border-color: var(--color-gray-400); 128 + --m3input-label-color: var(--color-gray-500); 129 + --m3input-text-color: var(--color-gray-900); 130 + --m3input-focus-color: var(--color-gray-600); 131 + } 132 + 133 + @media (prefers-color-scheme: dark) { 134 + :root { 135 + --m3input-bg: var(--color-gray-950); 136 + --m3input-border-color: var(--color-gray-700); 137 + --m3input-label-color: var(--color-gray-400); 138 + --m3input-text-color: var(--color-gray-50); 139 + --m3input-focus-color: var(--color-gray-400); 140 + } 141 + } 142 + 143 + /* reset page *//* 144 + html, 145 + body { 146 + background: var(--m3input-bg); 147 + margin: 0; 148 + padding: 1rem; 149 + color: var(--m3input-text-color); 150 + font-family: system-ui, sans-serif; 151 + font-size: var(--m3input-font-size); 152 + }*/ 153 + 154 + /* base wrapper */ 155 + .m3input-field.m3input-label.m3input-border { 156 + position: relative; 157 + display: inline-block; 158 + width: 100%; 159 + /*max-width: 400px;*/ 160 + } 161 + 162 + /* size variants */ 163 + .m3input-field.size-sm { 164 + --m3input-h: 40px; 165 + } 166 + 167 + .m3input-field.size-md { 168 + --m3input-h: 48px; 169 + } 170 + 171 + .m3input-field.size-lg { 172 + --m3input-h: 56px; 173 + } 174 + 175 + .m3input-field.size-xl { 176 + --m3input-h: 64px; 177 + } 178 + 179 + .m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) { 180 + --m3input-h: 48px; 181 + } 182 + 183 + /* outlined input */ 184 + .m3input-field.m3input-label.m3input-border input { 185 + width: 100%; 186 + height: var(--m3input-h); 187 + border: var(--m3input-border-width) solid var(--m3input-border-color); 188 + border-radius: var(--m3input-radius); 189 + background: var(--m3input-bg); 190 + color: var(--m3input-text-color); 191 + font-size: var(--m3input-font-size); 192 + padding: 0 12px; 193 + box-sizing: border-box; 194 + outline: none; 195 + transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition); 196 + } 197 + 198 + /* focus ring */ 199 + .m3input-field.m3input-label.m3input-border input:focus { 200 + border-color: var(--m3input-focus-color); 201 + /*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/ 202 + } 203 + 204 + /* label */ 205 + .m3input-field.m3input-label.m3input-border label { 206 + position: absolute; 207 + left: 12px; 208 + top: 50%; 209 + transform: translateY(-50%); 210 + background: var(--m3input-bg); 211 + padding: 0 .25em; 212 + color: var(--m3input-label-color); 213 + pointer-events: none; 214 + transition: all var(--m3input-transition); 215 + } 216 + 217 + /* float on focus or when filled */ 218 + .m3input-field.m3input-label.m3input-border input:focus+label, 219 + .m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label { 220 + top: 0; 221 + transform: translateY(-50%) scale(.78); 222 + left: 0; 223 + color: var(--m3input-focus-color); 224 + } 225 + 226 + /* placeholder trick */ 227 + .m3input-field.m3input-label.m3input-border input::placeholder { 228 + color: transparent; 229 + } 230 + 231 + /* radix i love you but like cmon man */ 232 + body[data-scroll-locked]{ 233 + margin-left: var(--removed-body-scroll-bar-size) !important; 108 234 }
+63 -8
src/utils/atoms.ts
··· 1 - import type Agent from "@atproto/api"; 2 - import { atom, createStore } from "jotai"; 3 - import { atomWithStorage } from 'jotai/utils'; 1 + import { atom, createStore, useAtomValue } from "jotai"; 2 + import { atomWithStorage } from "jotai/utils"; 3 + import { useEffect } from "react"; 4 4 5 5 export const store = createStore(); 6 6 7 + export const quickAuthAtom = atomWithStorage<string | null>( 8 + "quickAuth", 9 + null 10 + ); 11 + 7 12 export const selectedFeedUriAtom = atomWithStorage<string | null>( 8 - 'selectedFeedUri', 13 + "selectedFeedUri", 9 14 null 10 15 ); 11 16 12 17 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 18 14 19 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 - 'feedscrollpositions', 20 + "feedscrollpositions", 16 21 {} 17 22 ); 18 23 19 24 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 20 - 'likedPosts', 25 + "likedPosts", 21 26 {} 22 27 ); 23 28 29 + export const defaultconstellationURL = "constellation.microcosm.blue"; 30 + export const constellationURLAtom = atomWithStorage<string>( 31 + "constellationURL", 32 + defaultconstellationURL 33 + ); 34 + export const defaultslingshotURL = "slingshot.microcosm.blue"; 35 + export const slingshotURLAtom = atomWithStorage<string>( 36 + "slingshotURL", 37 + defaultslingshotURL 38 + ); 39 + export const defaultImgCDN = "cdn.bsky.app"; 40 + export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN); 41 + export const defaultVideoCDN = "video.bsky.app"; 42 + export const videoCDNAtom = atomWithStorage<string>( 43 + "videocdnurl", 44 + defaultVideoCDN 45 + ); 46 + 47 + export const defaulthue = 28; 48 + export const hueAtom = atomWithStorage<number>("hue", defaulthue); 49 + 24 50 export const isAtTopAtom = atom<boolean>(true); 25 51 26 - export const agentAtom = atom<Agent|null>(null); 27 - export const authedAtom = atom<boolean>(false); 52 + type ComposerState = 53 + | { kind: "closed" } 54 + | { kind: "root" } 55 + | { kind: "reply"; parent: string } 56 + | { kind: "quote"; subject: string }; 57 + export const composerAtom = atom<ComposerState>({ kind: "closed" }); 58 + 59 + //export const agentAtom = atom<Agent | null>(null); 60 + //export const authedAtom = atom<boolean>(false); 61 + 62 + export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 63 + const value = useAtomValue(atom); 64 + 65 + useEffect(() => { 66 + document.documentElement.style.setProperty(cssVar, value.toString()); 67 + }, [value, cssVar]); 68 + 69 + useEffect(() => { 70 + document.documentElement.style.setProperty(cssVar, value.toString()); 71 + }, []); 72 + } 73 + 74 + hueAtom.onMount = (setAtom) => { 75 + const stored = localStorage.getItem("hue"); 76 + if (stored != null) setAtom(Number(stored)); 77 + }; 78 + // export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) { 79 + // const initial = store.get(atom); 80 + // console.log("atom get ", initial); 81 + // document.documentElement.style.setProperty(cssVar, initial.toString()); 82 + // }
+37 -3
src/utils/followState.ts
··· 1 - import { AtUri, type Agent } from "@atproto/api"; 2 - import { useQueryConstellation, type linksRecordsResponse } from "./useQuery"; 1 + import { type Agent,AtUri } from "@atproto/api"; 2 + import { TID } from "@atproto/common-web"; 3 3 import type { QueryClient } from "@tanstack/react-query"; 4 - import { TID } from "@atproto/common-web"; 4 + 5 + import { type linksRecordsResponse,useQueryConstellation } from "./useQuery"; 5 6 6 7 export function useGetFollowState({ 7 8 target, ··· 127 128 }; 128 129 }); 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 + }
+53 -23
src/utils/useHydrated.ts
··· 9 9 AppBskyFeedPost, 10 10 AtUri, 11 11 } from "@atproto/api"; 12 + import { useAtom } from "jotai"; 12 13 import { useMemo } from "react"; 13 14 14 - import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery"; 15 + import { imgCDNAtom, videoCDNAtom } from "./atoms"; 16 + import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery"; 15 17 16 - type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends 17 - | { data: infer D } 18 - | undefined 19 - ? D 20 - : never; 18 + type QueryResultData<T extends (...args: any) => any> = 19 + ReturnType<T> extends { data: infer D } | undefined ? D : never; 21 20 22 21 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 23 22 return obj as $Typed<T>; ··· 26 25 export function hydrateEmbedImages( 27 26 embed: AppBskyEmbedImages.Main, 28 27 did: string, 28 + cdn: string 29 29 ): $Typed<AppBskyEmbedImages.View> { 30 30 return asTyped({ 31 31 $type: "app.bsky.embed.images#view" as const, ··· 34 34 const link = img.image.ref?.["$link"]; 35 35 if (!link) return null; 36 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`, 37 + thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 + fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 39 alt: img.alt || "", 40 40 aspectRatio: img.aspectRatio, 41 41 }; ··· 47 47 export function hydrateEmbedExternal( 48 48 embed: AppBskyEmbedExternal.Main, 49 49 did: string, 50 + cdn: string 50 51 ): $Typed<AppBskyEmbedExternal.View> { 51 52 return asTyped({ 52 53 $type: "app.bsky.embed.external#view" as const, ··· 55 56 title: embed.external.title, 56 57 description: embed.external.description, 57 58 thumb: embed.external.thumb?.ref?.$link 58 - ? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 + ? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 60 : undefined, 60 61 }, 61 62 }); ··· 64 65 export function hydrateEmbedVideo( 65 66 embed: AppBskyEmbedVideo.Main, 66 67 did: string, 68 + videocdn: string 67 69 ): $Typed<AppBskyEmbedVideo.View> { 68 70 const videoLink = embed.video.ref.$link; 69 71 return asTyped({ 70 72 $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 + playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`, 74 + thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`, 73 75 aspectRatio: embed.aspectRatio, 74 76 cid: videoLink, 75 77 }); ··· 80 82 quotedPost: QueryResultData<typeof useQueryPost>, 81 83 quotedProfile: QueryResultData<typeof useQueryProfile>, 82 84 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 85 + cdn: string 83 86 ): $Typed<AppBskyEmbedRecord.View> | undefined { 84 87 if (!quotedPost || !quotedProfile || !quotedIdentity) { 85 88 return undefined; ··· 91 94 handle: quotedIdentity.handle, 92 95 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 93 96 avatar: quotedProfile.value.avatar?.ref?.$link 94 - ? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 97 + ? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 95 98 : undefined, 96 99 viewer: {}, 97 100 labels: [], ··· 122 125 quotedPost: QueryResultData<typeof useQueryPost>, 123 126 quotedProfile: QueryResultData<typeof useQueryProfile>, 124 127 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 128 + cdn: string 125 129 ): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 126 130 const hydratedRecord = hydrateEmbedRecord( 127 131 embed.record, 128 132 quotedPost, 129 133 quotedProfile, 130 134 quotedIdentity, 135 + cdn 131 136 ); 132 137 133 138 if (!hydratedRecord) return undefined; ··· 148 153 149 154 export function useHydratedEmbed( 150 155 embed: AppBskyFeedPost.Record["embed"], 151 - postAuthorDid: string | undefined, 156 + postAuthorDid: string | undefined 152 157 ) { 153 158 const recordInfo = useMemo(() => { 154 159 if (AppBskyEmbedRecordWithMedia.isMain(embed)) { ··· 181 186 error: profileError, 182 187 } = useQueryProfile(profileUri); 183 188 189 + const [imgcdn] = useAtom(imgCDNAtom); 190 + const [videocdn] = useAtom(videoCDNAtom); 191 + 184 192 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 185 193 186 194 const hydratedEmbed: HydratedEmbedView | undefined = (() => { 187 195 if (!embed || !postAuthorDid) return undefined; 188 196 189 - if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) { 197 + if ( 198 + isRecordType && 199 + (!usequerypostresults?.data || 200 + !quotedProfile || 201 + !queryidentityresult?.data) 202 + ) { 190 203 return undefined; 191 204 } 192 205 193 206 try { 194 207 if (AppBskyEmbedImages.isMain(embed)) { 195 - return hydrateEmbedImages(embed, postAuthorDid); 208 + return hydrateEmbedImages(embed, postAuthorDid, imgcdn); 196 209 } else if (AppBskyEmbedExternal.isMain(embed)) { 197 - return hydrateEmbedExternal(embed, postAuthorDid); 210 + return hydrateEmbedExternal(embed, postAuthorDid, imgcdn); 198 211 } else if (AppBskyEmbedVideo.isMain(embed)) { 199 - return hydrateEmbedVideo(embed, postAuthorDid); 212 + return hydrateEmbedVideo(embed, postAuthorDid, videocdn); 200 213 } else if (AppBskyEmbedRecord.isMain(embed)) { 201 214 return hydrateEmbedRecord( 202 215 embed, 203 216 usequerypostresults?.data, 204 217 quotedProfile, 205 218 queryidentityresult?.data, 219 + imgcdn 206 220 ); 207 221 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 208 222 let hydratedMedia: ··· 212 226 | undefined; 213 227 214 228 if (AppBskyEmbedImages.isMain(embed.media)) { 215 - hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid); 229 + hydratedMedia = hydrateEmbedImages( 230 + embed.media, 231 + postAuthorDid, 232 + imgcdn 233 + ); 216 234 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 217 - hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid); 235 + hydratedMedia = hydrateEmbedExternal( 236 + embed.media, 237 + postAuthorDid, 238 + imgcdn 239 + ); 218 240 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 219 - hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid); 241 + hydratedMedia = hydrateEmbedVideo( 242 + embed.media, 243 + postAuthorDid, 244 + videocdn 245 + ); 220 246 } 221 247 222 248 if (hydratedMedia) { ··· 226 252 usequerypostresults?.data, 227 253 quotedProfile, 228 254 queryidentityresult?.data, 255 + imgcdn 229 256 ); 230 257 } 231 258 } ··· 236 263 })(); 237 264 238 265 const isLoading = isRecordType 239 - ? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading 266 + ? usequerypostresults?.isLoading || 267 + isLoadingProfile || 268 + queryidentityresult?.isLoading 240 269 : false; 241 270 242 - const error = usequerypostresults?.error || profileError || queryidentityresult?.error; 271 + const error = 272 + usequerypostresults?.error || profileError || queryidentityresult?.error; 243 273 244 274 return { data: hydratedEmbed, isLoading, error }; 245 - } 275 + }
+28 -19
src/utils/useQuery.ts
··· 6 6 useInfiniteQuery, 7 7 useQuery, 8 8 type UseQueryResult} from "@tanstack/react-query"; 9 + import { useAtom } from "jotai"; 9 10 10 - export function constructIdentityQuery(didorhandle?: string) { 11 + import { constellationURLAtom, slingshotURLAtom } from "./atoms"; 12 + 13 + export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) { 11 14 return queryOptions({ 12 15 queryKey: ["identity", didorhandle], 13 16 queryFn: async () => { 14 17 if (!didorhandle) return undefined as undefined 15 18 const res = await fetch( 16 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 19 + `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 17 20 ); 18 21 if (!res.ok) throw new Error("Failed to fetch post"); 19 22 try { ··· 55 58 Error 56 59 > 57 60 export function useQueryIdentity(didorhandle?: string) { 58 - return useQuery(constructIdentityQuery(didorhandle)); 61 + const [slingshoturl] = useAtom(slingshotURLAtom) 62 + return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 59 63 } 60 64 61 - export function constructPostQuery(uri?: string) { 65 + export function constructPostQuery(uri?: string, slingshoturl?: string) { 62 66 return queryOptions({ 63 67 queryKey: ["post", uri], 64 68 queryFn: async () => { 65 69 if (!uri) return undefined as undefined 66 70 const res = await fetch( 67 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 71 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 68 72 ); 69 73 let data: any; 70 74 try { ··· 118 122 Error 119 123 > 120 124 export function useQueryPost(uri?: string) { 121 - return useQuery(constructPostQuery(uri)); 125 + const [slingshoturl] = useAtom(slingshotURLAtom) 126 + return useQuery(constructPostQuery(uri, slingshoturl)); 122 127 } 123 128 124 - export function constructProfileQuery(uri?: string) { 129 + export function constructProfileQuery(uri?: string, slingshoturl?: string) { 125 130 return queryOptions({ 126 131 queryKey: ["profile", uri], 127 132 queryFn: async () => { 128 133 if (!uri) return undefined as undefined 129 134 const res = await fetch( 130 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 135 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 131 136 ); 132 137 let data: any; 133 138 try { ··· 181 186 Error 182 187 > 183 188 export function useQueryProfile(uri?: string) { 184 - return useQuery(constructProfileQuery(uri)); 189 + const [slingshoturl] = useAtom(slingshotURLAtom) 190 + return useQuery(constructProfileQuery(uri, slingshoturl)); 185 191 } 186 192 187 193 // export function constructConstellationQuery( ··· 217 223 // target: string 218 224 // ): QueryOptions<linksAllResponse, Error>; 219 225 export function constructConstellationQuery(query?:{ 226 + constellation: string, 220 227 method: 221 228 | "/links" 222 229 | "/links/distinct-dids" ··· 250 257 const cursor = query.cursor 251 258 const dids = query?.dids 252 259 const res = await fetch( 253 - `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("") : ""}` 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("") : ""}` 254 261 ); 255 262 if (!res.ok) throw new Error("Failed to fetch post"); 256 263 try { ··· 339 346 > 340 347 | undefined { 341 348 //if (!query) return; 349 + const [constellationurl] = useAtom(constellationURLAtom) 342 350 return useQuery( 343 - constructConstellationQuery(query) 351 + constructConstellationQuery(query && {constellation: constellationurl, ...query}) 344 352 ); 345 353 } 346 354 ··· 362 370 type linksCountResponse = { 363 371 total: string; 364 372 }; 365 - type linksAllResponse = { 373 + export type linksAllResponse = { 366 374 links: Record< 367 375 string, 368 376 Record< ··· 445 453 446 454 447 455 448 - export function constructArbitraryQuery(uri?: string) { 456 + export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 449 457 return queryOptions({ 450 458 queryKey: ["arbitrary", uri], 451 459 queryFn: async () => { 452 460 if (!uri) return undefined as undefined 453 461 const res = await fetch( 454 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 462 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 455 463 ); 456 464 let data: any; 457 465 try { ··· 504 512 Error 505 513 >; 506 514 export function useQueryArbitrary(uri?: string) { 507 - return useQuery(constructArbitraryQuery(uri)); 515 + const [slingshoturl] = useAtom(slingshotURLAtom) 516 + return useQuery(constructArbitraryQuery(uri, slingshoturl)); 508 517 } 509 518 510 519 export function constructFallbackNothingQuery(){ ··· 606 615 }) { 607 616 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 608 617 609 - return useInfiniteQuery({ 618 + return {...useInfiniteQuery({ 610 619 queryKey, 611 620 queryFn, 612 621 initialPageParam: undefined as never, ··· 614 623 staleTime: Infinity, 615 624 refetchOnWindowFocus: false, 616 625 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 617 - }); 626 + }), queryKey: queryKey}; 618 627 } 619 628 620 629 621 630 export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 631 + constellation: string, 622 632 method: '/links' 623 633 target?: string 624 634 collection: string 625 635 path: string 626 636 }) { 627 - const constellationHost = 'constellation.microcosm.blue' 628 637 console.log( 629 638 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 630 639 query, ··· 650 659 const cursor = pageParam 651 660 652 661 const res = await fetch( 653 - `https://${constellationHost}${method}?target=${encodeURIComponent(target)}${ 662 + `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 654 663 collection ? `&collection=${encodeURIComponent(collection)}` : '' 655 664 }${path ? `&path=${encodeURIComponent(path)}` : ''}${ 656 665 cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
+1 -1
vite.config.ts
··· 10 10 11 11 import { generateMetadataPlugin } from "./oauthdev.mts"; 12 12 13 - const PROD_URL = "https://reddwarf.whey.party" 13 + const PROD_URL = "https://reddwarf.app" 14 14 const DEV_URL = "https://local3768forumtest.whey.party" 15 15 16 16 function shp(url: string): string {