+1
-1
README.md
+1
-1
README.md
···
8
## running dev and build
9
in the `vite.config.ts` file you should change these values
10
```ts
11
-
const PROD_URL = "https://reddwarf.whey.party"
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
```
14
the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
···
8
## running dev and build
9
in the `vite.config.ts` file you should change these values
10
```ts
11
+
const PROD_URL = "https://reddwarf.app"
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
```
14
the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
+1685
package-lock.json
+1685
package-lock.json
···
8
"dependencies": {
9
"@atproto/api": "^0.16.6",
10
"@atproto/oauth-client-browser": "^0.3.33",
11
"@tailwindcss/vite": "^4.0.6",
12
"@tanstack/query-sync-storage-persister": "^5.85.6",
13
"@tanstack/react-devtools": "^0.2.2",
···
21
"idb-keyval": "^6.2.2",
22
"jotai": "^2.13.1",
23
"npm": "^11.6.2",
24
"react": "^19.0.0",
25
"react-dom": "^19.0.0",
26
"react-player": "^3.3.2",
···
1592
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1593
}
1594
},
1595
"node_modules/@humanfs/core": {
1596
"version": "0.19.1",
1597
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
···
1895
"node": ">= 8"
1896
}
1897
},
1898
"node_modules/@rolldown/pluginutils": {
1899
"version": "1.0.0-beta.27",
1900
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
···
3806
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
3807
"dev": true,
3808
"license": "Python-2.0"
3809
},
3810
"node_modules/aria-query": {
3811
"version": "5.3.0",
···
4716
"node": ">=8"
4717
}
4718
},
4719
"node_modules/diff": {
4720
"version": "8.0.2",
4721
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
···
5735
},
5736
"funding": {
5737
"url": "https://github.com/sponsors/ljharb"
5738
}
5739
},
5740
"node_modules/get-proto": {
···
10338
],
10339
"license": "MIT"
10340
},
10341
"node_modules/react": {
10342
"version": "19.1.1",
10343
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
···
10399
"node": ">=0.10.0"
10400
}
10401
},
10402
"node_modules/readdirp": {
10403
"version": "3.6.0",
10404
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
···
11892
"peer": true,
11893
"dependencies": {
11894
"punycode": "^2.1.0"
11895
}
11896
},
11897
"node_modules/use-sync-external-store": {
···
8
"dependencies": {
9
"@atproto/api": "^0.16.6",
10
"@atproto/oauth-client-browser": "^0.3.33",
11
+
"@radix-ui/react-dialog": "^1.1.15",
12
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
13
+
"@radix-ui/react-hover-card": "^1.1.15",
14
+
"@radix-ui/react-slider": "^1.3.6",
15
"@tailwindcss/vite": "^4.0.6",
16
"@tanstack/query-sync-storage-persister": "^5.85.6",
17
"@tanstack/react-devtools": "^0.2.2",
···
25
"idb-keyval": "^6.2.2",
26
"jotai": "^2.13.1",
27
"npm": "^11.6.2",
28
+
"radix-ui": "^1.4.3",
29
"react": "^19.0.0",
30
"react-dom": "^19.0.0",
31
"react-player": "^3.3.2",
···
1597
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1598
}
1599
},
1600
+
"node_modules/@floating-ui/core": {
1601
+
"version": "1.7.3",
1602
+
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
1603
+
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
1604
+
"dependencies": {
1605
+
"@floating-ui/utils": "^0.2.10"
1606
+
}
1607
+
},
1608
+
"node_modules/@floating-ui/dom": {
1609
+
"version": "1.7.4",
1610
+
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
1611
+
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
1612
+
"dependencies": {
1613
+
"@floating-ui/core": "^1.7.3",
1614
+
"@floating-ui/utils": "^0.2.10"
1615
+
}
1616
+
},
1617
+
"node_modules/@floating-ui/react-dom": {
1618
+
"version": "2.1.6",
1619
+
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
1620
+
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
1621
+
"dependencies": {
1622
+
"@floating-ui/dom": "^1.7.4"
1623
+
},
1624
+
"peerDependencies": {
1625
+
"react": ">=16.8.0",
1626
+
"react-dom": ">=16.8.0"
1627
+
}
1628
+
},
1629
+
"node_modules/@floating-ui/utils": {
1630
+
"version": "0.2.10",
1631
+
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
1632
+
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
1633
+
},
1634
"node_modules/@humanfs/core": {
1635
"version": "0.19.1",
1636
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
···
1934
"node": ">= 8"
1935
}
1936
},
1937
+
"node_modules/@radix-ui/number": {
1938
+
"version": "1.1.1",
1939
+
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
1940
+
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="
1941
+
},
1942
+
"node_modules/@radix-ui/primitive": {
1943
+
"version": "1.1.3",
1944
+
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
1945
+
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
1946
+
},
1947
+
"node_modules/@radix-ui/react-accessible-icon": {
1948
+
"version": "1.1.7",
1949
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz",
1950
+
"integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==",
1951
+
"dependencies": {
1952
+
"@radix-ui/react-visually-hidden": "1.2.3"
1953
+
},
1954
+
"peerDependencies": {
1955
+
"@types/react": "*",
1956
+
"@types/react-dom": "*",
1957
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1958
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1959
+
},
1960
+
"peerDependenciesMeta": {
1961
+
"@types/react": {
1962
+
"optional": true
1963
+
},
1964
+
"@types/react-dom": {
1965
+
"optional": true
1966
+
}
1967
+
}
1968
+
},
1969
+
"node_modules/@radix-ui/react-accordion": {
1970
+
"version": "1.2.12",
1971
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
1972
+
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
1973
+
"dependencies": {
1974
+
"@radix-ui/primitive": "1.1.3",
1975
+
"@radix-ui/react-collapsible": "1.1.12",
1976
+
"@radix-ui/react-collection": "1.1.7",
1977
+
"@radix-ui/react-compose-refs": "1.1.2",
1978
+
"@radix-ui/react-context": "1.1.2",
1979
+
"@radix-ui/react-direction": "1.1.1",
1980
+
"@radix-ui/react-id": "1.1.1",
1981
+
"@radix-ui/react-primitive": "2.1.3",
1982
+
"@radix-ui/react-use-controllable-state": "1.2.2"
1983
+
},
1984
+
"peerDependencies": {
1985
+
"@types/react": "*",
1986
+
"@types/react-dom": "*",
1987
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1988
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1989
+
},
1990
+
"peerDependenciesMeta": {
1991
+
"@types/react": {
1992
+
"optional": true
1993
+
},
1994
+
"@types/react-dom": {
1995
+
"optional": true
1996
+
}
1997
+
}
1998
+
},
1999
+
"node_modules/@radix-ui/react-alert-dialog": {
2000
+
"version": "1.1.15",
2001
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
2002
+
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
2003
+
"dependencies": {
2004
+
"@radix-ui/primitive": "1.1.3",
2005
+
"@radix-ui/react-compose-refs": "1.1.2",
2006
+
"@radix-ui/react-context": "1.1.2",
2007
+
"@radix-ui/react-dialog": "1.1.15",
2008
+
"@radix-ui/react-primitive": "2.1.3",
2009
+
"@radix-ui/react-slot": "1.2.3"
2010
+
},
2011
+
"peerDependencies": {
2012
+
"@types/react": "*",
2013
+
"@types/react-dom": "*",
2014
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2015
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2016
+
},
2017
+
"peerDependenciesMeta": {
2018
+
"@types/react": {
2019
+
"optional": true
2020
+
},
2021
+
"@types/react-dom": {
2022
+
"optional": true
2023
+
}
2024
+
}
2025
+
},
2026
+
"node_modules/@radix-ui/react-arrow": {
2027
+
"version": "1.1.7",
2028
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
2029
+
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
2030
+
"dependencies": {
2031
+
"@radix-ui/react-primitive": "2.1.3"
2032
+
},
2033
+
"peerDependencies": {
2034
+
"@types/react": "*",
2035
+
"@types/react-dom": "*",
2036
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2037
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2038
+
},
2039
+
"peerDependenciesMeta": {
2040
+
"@types/react": {
2041
+
"optional": true
2042
+
},
2043
+
"@types/react-dom": {
2044
+
"optional": true
2045
+
}
2046
+
}
2047
+
},
2048
+
"node_modules/@radix-ui/react-aspect-ratio": {
2049
+
"version": "1.1.7",
2050
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz",
2051
+
"integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==",
2052
+
"dependencies": {
2053
+
"@radix-ui/react-primitive": "2.1.3"
2054
+
},
2055
+
"peerDependencies": {
2056
+
"@types/react": "*",
2057
+
"@types/react-dom": "*",
2058
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2059
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2060
+
},
2061
+
"peerDependenciesMeta": {
2062
+
"@types/react": {
2063
+
"optional": true
2064
+
},
2065
+
"@types/react-dom": {
2066
+
"optional": true
2067
+
}
2068
+
}
2069
+
},
2070
+
"node_modules/@radix-ui/react-avatar": {
2071
+
"version": "1.1.10",
2072
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
2073
+
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
2074
+
"dependencies": {
2075
+
"@radix-ui/react-context": "1.1.2",
2076
+
"@radix-ui/react-primitive": "2.1.3",
2077
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2078
+
"@radix-ui/react-use-is-hydrated": "0.1.0",
2079
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2080
+
},
2081
+
"peerDependencies": {
2082
+
"@types/react": "*",
2083
+
"@types/react-dom": "*",
2084
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2085
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2086
+
},
2087
+
"peerDependenciesMeta": {
2088
+
"@types/react": {
2089
+
"optional": true
2090
+
},
2091
+
"@types/react-dom": {
2092
+
"optional": true
2093
+
}
2094
+
}
2095
+
},
2096
+
"node_modules/@radix-ui/react-checkbox": {
2097
+
"version": "1.3.3",
2098
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
2099
+
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
2100
+
"dependencies": {
2101
+
"@radix-ui/primitive": "1.1.3",
2102
+
"@radix-ui/react-compose-refs": "1.1.2",
2103
+
"@radix-ui/react-context": "1.1.2",
2104
+
"@radix-ui/react-presence": "1.1.5",
2105
+
"@radix-ui/react-primitive": "2.1.3",
2106
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2107
+
"@radix-ui/react-use-previous": "1.1.1",
2108
+
"@radix-ui/react-use-size": "1.1.1"
2109
+
},
2110
+
"peerDependencies": {
2111
+
"@types/react": "*",
2112
+
"@types/react-dom": "*",
2113
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2114
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2115
+
},
2116
+
"peerDependenciesMeta": {
2117
+
"@types/react": {
2118
+
"optional": true
2119
+
},
2120
+
"@types/react-dom": {
2121
+
"optional": true
2122
+
}
2123
+
}
2124
+
},
2125
+
"node_modules/@radix-ui/react-collapsible": {
2126
+
"version": "1.1.12",
2127
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
2128
+
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
2129
+
"dependencies": {
2130
+
"@radix-ui/primitive": "1.1.3",
2131
+
"@radix-ui/react-compose-refs": "1.1.2",
2132
+
"@radix-ui/react-context": "1.1.2",
2133
+
"@radix-ui/react-id": "1.1.1",
2134
+
"@radix-ui/react-presence": "1.1.5",
2135
+
"@radix-ui/react-primitive": "2.1.3",
2136
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2137
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2138
+
},
2139
+
"peerDependencies": {
2140
+
"@types/react": "*",
2141
+
"@types/react-dom": "*",
2142
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2143
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2144
+
},
2145
+
"peerDependenciesMeta": {
2146
+
"@types/react": {
2147
+
"optional": true
2148
+
},
2149
+
"@types/react-dom": {
2150
+
"optional": true
2151
+
}
2152
+
}
2153
+
},
2154
+
"node_modules/@radix-ui/react-collection": {
2155
+
"version": "1.1.7",
2156
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
2157
+
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
2158
+
"dependencies": {
2159
+
"@radix-ui/react-compose-refs": "1.1.2",
2160
+
"@radix-ui/react-context": "1.1.2",
2161
+
"@radix-ui/react-primitive": "2.1.3",
2162
+
"@radix-ui/react-slot": "1.2.3"
2163
+
},
2164
+
"peerDependencies": {
2165
+
"@types/react": "*",
2166
+
"@types/react-dom": "*",
2167
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2168
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2169
+
},
2170
+
"peerDependenciesMeta": {
2171
+
"@types/react": {
2172
+
"optional": true
2173
+
},
2174
+
"@types/react-dom": {
2175
+
"optional": true
2176
+
}
2177
+
}
2178
+
},
2179
+
"node_modules/@radix-ui/react-compose-refs": {
2180
+
"version": "1.1.2",
2181
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
2182
+
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
2183
+
"peerDependencies": {
2184
+
"@types/react": "*",
2185
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2186
+
},
2187
+
"peerDependenciesMeta": {
2188
+
"@types/react": {
2189
+
"optional": true
2190
+
}
2191
+
}
2192
+
},
2193
+
"node_modules/@radix-ui/react-context": {
2194
+
"version": "1.1.2",
2195
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
2196
+
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
2197
+
"peerDependencies": {
2198
+
"@types/react": "*",
2199
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2200
+
},
2201
+
"peerDependenciesMeta": {
2202
+
"@types/react": {
2203
+
"optional": true
2204
+
}
2205
+
}
2206
+
},
2207
+
"node_modules/@radix-ui/react-context-menu": {
2208
+
"version": "2.2.16",
2209
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
2210
+
"integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
2211
+
"dependencies": {
2212
+
"@radix-ui/primitive": "1.1.3",
2213
+
"@radix-ui/react-context": "1.1.2",
2214
+
"@radix-ui/react-menu": "2.1.16",
2215
+
"@radix-ui/react-primitive": "2.1.3",
2216
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2217
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2218
+
},
2219
+
"peerDependencies": {
2220
+
"@types/react": "*",
2221
+
"@types/react-dom": "*",
2222
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2223
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2224
+
},
2225
+
"peerDependenciesMeta": {
2226
+
"@types/react": {
2227
+
"optional": true
2228
+
},
2229
+
"@types/react-dom": {
2230
+
"optional": true
2231
+
}
2232
+
}
2233
+
},
2234
+
"node_modules/@radix-ui/react-dialog": {
2235
+
"version": "1.1.15",
2236
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
2237
+
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
2238
+
"dependencies": {
2239
+
"@radix-ui/primitive": "1.1.3",
2240
+
"@radix-ui/react-compose-refs": "1.1.2",
2241
+
"@radix-ui/react-context": "1.1.2",
2242
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2243
+
"@radix-ui/react-focus-guards": "1.1.3",
2244
+
"@radix-ui/react-focus-scope": "1.1.7",
2245
+
"@radix-ui/react-id": "1.1.1",
2246
+
"@radix-ui/react-portal": "1.1.9",
2247
+
"@radix-ui/react-presence": "1.1.5",
2248
+
"@radix-ui/react-primitive": "2.1.3",
2249
+
"@radix-ui/react-slot": "1.2.3",
2250
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2251
+
"aria-hidden": "^1.2.4",
2252
+
"react-remove-scroll": "^2.6.3"
2253
+
},
2254
+
"peerDependencies": {
2255
+
"@types/react": "*",
2256
+
"@types/react-dom": "*",
2257
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2258
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2259
+
},
2260
+
"peerDependenciesMeta": {
2261
+
"@types/react": {
2262
+
"optional": true
2263
+
},
2264
+
"@types/react-dom": {
2265
+
"optional": true
2266
+
}
2267
+
}
2268
+
},
2269
+
"node_modules/@radix-ui/react-direction": {
2270
+
"version": "1.1.1",
2271
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
2272
+
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
2273
+
"peerDependencies": {
2274
+
"@types/react": "*",
2275
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2276
+
},
2277
+
"peerDependenciesMeta": {
2278
+
"@types/react": {
2279
+
"optional": true
2280
+
}
2281
+
}
2282
+
},
2283
+
"node_modules/@radix-ui/react-dismissable-layer": {
2284
+
"version": "1.1.11",
2285
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
2286
+
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
2287
+
"dependencies": {
2288
+
"@radix-ui/primitive": "1.1.3",
2289
+
"@radix-ui/react-compose-refs": "1.1.2",
2290
+
"@radix-ui/react-primitive": "2.1.3",
2291
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2292
+
"@radix-ui/react-use-escape-keydown": "1.1.1"
2293
+
},
2294
+
"peerDependencies": {
2295
+
"@types/react": "*",
2296
+
"@types/react-dom": "*",
2297
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2298
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2299
+
},
2300
+
"peerDependenciesMeta": {
2301
+
"@types/react": {
2302
+
"optional": true
2303
+
},
2304
+
"@types/react-dom": {
2305
+
"optional": true
2306
+
}
2307
+
}
2308
+
},
2309
+
"node_modules/@radix-ui/react-dropdown-menu": {
2310
+
"version": "2.1.16",
2311
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
2312
+
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
2313
+
"dependencies": {
2314
+
"@radix-ui/primitive": "1.1.3",
2315
+
"@radix-ui/react-compose-refs": "1.1.2",
2316
+
"@radix-ui/react-context": "1.1.2",
2317
+
"@radix-ui/react-id": "1.1.1",
2318
+
"@radix-ui/react-menu": "2.1.16",
2319
+
"@radix-ui/react-primitive": "2.1.3",
2320
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2321
+
},
2322
+
"peerDependencies": {
2323
+
"@types/react": "*",
2324
+
"@types/react-dom": "*",
2325
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2326
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2327
+
},
2328
+
"peerDependenciesMeta": {
2329
+
"@types/react": {
2330
+
"optional": true
2331
+
},
2332
+
"@types/react-dom": {
2333
+
"optional": true
2334
+
}
2335
+
}
2336
+
},
2337
+
"node_modules/@radix-ui/react-focus-guards": {
2338
+
"version": "1.1.3",
2339
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
2340
+
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
2341
+
"peerDependencies": {
2342
+
"@types/react": "*",
2343
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2344
+
},
2345
+
"peerDependenciesMeta": {
2346
+
"@types/react": {
2347
+
"optional": true
2348
+
}
2349
+
}
2350
+
},
2351
+
"node_modules/@radix-ui/react-focus-scope": {
2352
+
"version": "1.1.7",
2353
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
2354
+
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
2355
+
"dependencies": {
2356
+
"@radix-ui/react-compose-refs": "1.1.2",
2357
+
"@radix-ui/react-primitive": "2.1.3",
2358
+
"@radix-ui/react-use-callback-ref": "1.1.1"
2359
+
},
2360
+
"peerDependencies": {
2361
+
"@types/react": "*",
2362
+
"@types/react-dom": "*",
2363
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2364
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2365
+
},
2366
+
"peerDependenciesMeta": {
2367
+
"@types/react": {
2368
+
"optional": true
2369
+
},
2370
+
"@types/react-dom": {
2371
+
"optional": true
2372
+
}
2373
+
}
2374
+
},
2375
+
"node_modules/@radix-ui/react-form": {
2376
+
"version": "0.1.8",
2377
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz",
2378
+
"integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==",
2379
+
"dependencies": {
2380
+
"@radix-ui/primitive": "1.1.3",
2381
+
"@radix-ui/react-compose-refs": "1.1.2",
2382
+
"@radix-ui/react-context": "1.1.2",
2383
+
"@radix-ui/react-id": "1.1.1",
2384
+
"@radix-ui/react-label": "2.1.7",
2385
+
"@radix-ui/react-primitive": "2.1.3"
2386
+
},
2387
+
"peerDependencies": {
2388
+
"@types/react": "*",
2389
+
"@types/react-dom": "*",
2390
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2391
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2392
+
},
2393
+
"peerDependenciesMeta": {
2394
+
"@types/react": {
2395
+
"optional": true
2396
+
},
2397
+
"@types/react-dom": {
2398
+
"optional": true
2399
+
}
2400
+
}
2401
+
},
2402
+
"node_modules/@radix-ui/react-hover-card": {
2403
+
"version": "1.1.15",
2404
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
2405
+
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
2406
+
"license": "MIT",
2407
+
"dependencies": {
2408
+
"@radix-ui/primitive": "1.1.3",
2409
+
"@radix-ui/react-compose-refs": "1.1.2",
2410
+
"@radix-ui/react-context": "1.1.2",
2411
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2412
+
"@radix-ui/react-popper": "1.2.8",
2413
+
"@radix-ui/react-portal": "1.1.9",
2414
+
"@radix-ui/react-presence": "1.1.5",
2415
+
"@radix-ui/react-primitive": "2.1.3",
2416
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2417
+
},
2418
+
"peerDependencies": {
2419
+
"@types/react": "*",
2420
+
"@types/react-dom": "*",
2421
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2422
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2423
+
},
2424
+
"peerDependenciesMeta": {
2425
+
"@types/react": {
2426
+
"optional": true
2427
+
},
2428
+
"@types/react-dom": {
2429
+
"optional": true
2430
+
}
2431
+
}
2432
+
},
2433
+
"node_modules/@radix-ui/react-id": {
2434
+
"version": "1.1.1",
2435
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
2436
+
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
2437
+
"dependencies": {
2438
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2439
+
},
2440
+
"peerDependencies": {
2441
+
"@types/react": "*",
2442
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2443
+
},
2444
+
"peerDependenciesMeta": {
2445
+
"@types/react": {
2446
+
"optional": true
2447
+
}
2448
+
}
2449
+
},
2450
+
"node_modules/@radix-ui/react-label": {
2451
+
"version": "2.1.7",
2452
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
2453
+
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
2454
+
"dependencies": {
2455
+
"@radix-ui/react-primitive": "2.1.3"
2456
+
},
2457
+
"peerDependencies": {
2458
+
"@types/react": "*",
2459
+
"@types/react-dom": "*",
2460
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2461
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2462
+
},
2463
+
"peerDependenciesMeta": {
2464
+
"@types/react": {
2465
+
"optional": true
2466
+
},
2467
+
"@types/react-dom": {
2468
+
"optional": true
2469
+
}
2470
+
}
2471
+
},
2472
+
"node_modules/@radix-ui/react-menu": {
2473
+
"version": "2.1.16",
2474
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
2475
+
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
2476
+
"dependencies": {
2477
+
"@radix-ui/primitive": "1.1.3",
2478
+
"@radix-ui/react-collection": "1.1.7",
2479
+
"@radix-ui/react-compose-refs": "1.1.2",
2480
+
"@radix-ui/react-context": "1.1.2",
2481
+
"@radix-ui/react-direction": "1.1.1",
2482
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2483
+
"@radix-ui/react-focus-guards": "1.1.3",
2484
+
"@radix-ui/react-focus-scope": "1.1.7",
2485
+
"@radix-ui/react-id": "1.1.1",
2486
+
"@radix-ui/react-popper": "1.2.8",
2487
+
"@radix-ui/react-portal": "1.1.9",
2488
+
"@radix-ui/react-presence": "1.1.5",
2489
+
"@radix-ui/react-primitive": "2.1.3",
2490
+
"@radix-ui/react-roving-focus": "1.1.11",
2491
+
"@radix-ui/react-slot": "1.2.3",
2492
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2493
+
"aria-hidden": "^1.2.4",
2494
+
"react-remove-scroll": "^2.6.3"
2495
+
},
2496
+
"peerDependencies": {
2497
+
"@types/react": "*",
2498
+
"@types/react-dom": "*",
2499
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2500
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2501
+
},
2502
+
"peerDependenciesMeta": {
2503
+
"@types/react": {
2504
+
"optional": true
2505
+
},
2506
+
"@types/react-dom": {
2507
+
"optional": true
2508
+
}
2509
+
}
2510
+
},
2511
+
"node_modules/@radix-ui/react-menubar": {
2512
+
"version": "1.1.16",
2513
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
2514
+
"integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
2515
+
"dependencies": {
2516
+
"@radix-ui/primitive": "1.1.3",
2517
+
"@radix-ui/react-collection": "1.1.7",
2518
+
"@radix-ui/react-compose-refs": "1.1.2",
2519
+
"@radix-ui/react-context": "1.1.2",
2520
+
"@radix-ui/react-direction": "1.1.1",
2521
+
"@radix-ui/react-id": "1.1.1",
2522
+
"@radix-ui/react-menu": "2.1.16",
2523
+
"@radix-ui/react-primitive": "2.1.3",
2524
+
"@radix-ui/react-roving-focus": "1.1.11",
2525
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2526
+
},
2527
+
"peerDependencies": {
2528
+
"@types/react": "*",
2529
+
"@types/react-dom": "*",
2530
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2531
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2532
+
},
2533
+
"peerDependenciesMeta": {
2534
+
"@types/react": {
2535
+
"optional": true
2536
+
},
2537
+
"@types/react-dom": {
2538
+
"optional": true
2539
+
}
2540
+
}
2541
+
},
2542
+
"node_modules/@radix-ui/react-navigation-menu": {
2543
+
"version": "1.2.14",
2544
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
2545
+
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
2546
+
"dependencies": {
2547
+
"@radix-ui/primitive": "1.1.3",
2548
+
"@radix-ui/react-collection": "1.1.7",
2549
+
"@radix-ui/react-compose-refs": "1.1.2",
2550
+
"@radix-ui/react-context": "1.1.2",
2551
+
"@radix-ui/react-direction": "1.1.1",
2552
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2553
+
"@radix-ui/react-id": "1.1.1",
2554
+
"@radix-ui/react-presence": "1.1.5",
2555
+
"@radix-ui/react-primitive": "2.1.3",
2556
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2557
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2558
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2559
+
"@radix-ui/react-use-previous": "1.1.1",
2560
+
"@radix-ui/react-visually-hidden": "1.2.3"
2561
+
},
2562
+
"peerDependencies": {
2563
+
"@types/react": "*",
2564
+
"@types/react-dom": "*",
2565
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2566
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2567
+
},
2568
+
"peerDependenciesMeta": {
2569
+
"@types/react": {
2570
+
"optional": true
2571
+
},
2572
+
"@types/react-dom": {
2573
+
"optional": true
2574
+
}
2575
+
}
2576
+
},
2577
+
"node_modules/@radix-ui/react-one-time-password-field": {
2578
+
"version": "0.1.8",
2579
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz",
2580
+
"integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==",
2581
+
"dependencies": {
2582
+
"@radix-ui/number": "1.1.1",
2583
+
"@radix-ui/primitive": "1.1.3",
2584
+
"@radix-ui/react-collection": "1.1.7",
2585
+
"@radix-ui/react-compose-refs": "1.1.2",
2586
+
"@radix-ui/react-context": "1.1.2",
2587
+
"@radix-ui/react-direction": "1.1.1",
2588
+
"@radix-ui/react-primitive": "2.1.3",
2589
+
"@radix-ui/react-roving-focus": "1.1.11",
2590
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2591
+
"@radix-ui/react-use-effect-event": "0.0.2",
2592
+
"@radix-ui/react-use-is-hydrated": "0.1.0",
2593
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2594
+
},
2595
+
"peerDependencies": {
2596
+
"@types/react": "*",
2597
+
"@types/react-dom": "*",
2598
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2599
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2600
+
},
2601
+
"peerDependenciesMeta": {
2602
+
"@types/react": {
2603
+
"optional": true
2604
+
},
2605
+
"@types/react-dom": {
2606
+
"optional": true
2607
+
}
2608
+
}
2609
+
},
2610
+
"node_modules/@radix-ui/react-password-toggle-field": {
2611
+
"version": "0.1.3",
2612
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz",
2613
+
"integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==",
2614
+
"dependencies": {
2615
+
"@radix-ui/primitive": "1.1.3",
2616
+
"@radix-ui/react-compose-refs": "1.1.2",
2617
+
"@radix-ui/react-context": "1.1.2",
2618
+
"@radix-ui/react-id": "1.1.1",
2619
+
"@radix-ui/react-primitive": "2.1.3",
2620
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2621
+
"@radix-ui/react-use-effect-event": "0.0.2",
2622
+
"@radix-ui/react-use-is-hydrated": "0.1.0"
2623
+
},
2624
+
"peerDependencies": {
2625
+
"@types/react": "*",
2626
+
"@types/react-dom": "*",
2627
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2628
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2629
+
},
2630
+
"peerDependenciesMeta": {
2631
+
"@types/react": {
2632
+
"optional": true
2633
+
},
2634
+
"@types/react-dom": {
2635
+
"optional": true
2636
+
}
2637
+
}
2638
+
},
2639
+
"node_modules/@radix-ui/react-popover": {
2640
+
"version": "1.1.15",
2641
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
2642
+
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
2643
+
"dependencies": {
2644
+
"@radix-ui/primitive": "1.1.3",
2645
+
"@radix-ui/react-compose-refs": "1.1.2",
2646
+
"@radix-ui/react-context": "1.1.2",
2647
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2648
+
"@radix-ui/react-focus-guards": "1.1.3",
2649
+
"@radix-ui/react-focus-scope": "1.1.7",
2650
+
"@radix-ui/react-id": "1.1.1",
2651
+
"@radix-ui/react-popper": "1.2.8",
2652
+
"@radix-ui/react-portal": "1.1.9",
2653
+
"@radix-ui/react-presence": "1.1.5",
2654
+
"@radix-ui/react-primitive": "2.1.3",
2655
+
"@radix-ui/react-slot": "1.2.3",
2656
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2657
+
"aria-hidden": "^1.2.4",
2658
+
"react-remove-scroll": "^2.6.3"
2659
+
},
2660
+
"peerDependencies": {
2661
+
"@types/react": "*",
2662
+
"@types/react-dom": "*",
2663
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2664
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2665
+
},
2666
+
"peerDependenciesMeta": {
2667
+
"@types/react": {
2668
+
"optional": true
2669
+
},
2670
+
"@types/react-dom": {
2671
+
"optional": true
2672
+
}
2673
+
}
2674
+
},
2675
+
"node_modules/@radix-ui/react-popper": {
2676
+
"version": "1.2.8",
2677
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
2678
+
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
2679
+
"dependencies": {
2680
+
"@floating-ui/react-dom": "^2.0.0",
2681
+
"@radix-ui/react-arrow": "1.1.7",
2682
+
"@radix-ui/react-compose-refs": "1.1.2",
2683
+
"@radix-ui/react-context": "1.1.2",
2684
+
"@radix-ui/react-primitive": "2.1.3",
2685
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2686
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2687
+
"@radix-ui/react-use-rect": "1.1.1",
2688
+
"@radix-ui/react-use-size": "1.1.1",
2689
+
"@radix-ui/rect": "1.1.1"
2690
+
},
2691
+
"peerDependencies": {
2692
+
"@types/react": "*",
2693
+
"@types/react-dom": "*",
2694
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2695
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2696
+
},
2697
+
"peerDependenciesMeta": {
2698
+
"@types/react": {
2699
+
"optional": true
2700
+
},
2701
+
"@types/react-dom": {
2702
+
"optional": true
2703
+
}
2704
+
}
2705
+
},
2706
+
"node_modules/@radix-ui/react-portal": {
2707
+
"version": "1.1.9",
2708
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
2709
+
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
2710
+
"dependencies": {
2711
+
"@radix-ui/react-primitive": "2.1.3",
2712
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2713
+
},
2714
+
"peerDependencies": {
2715
+
"@types/react": "*",
2716
+
"@types/react-dom": "*",
2717
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2718
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2719
+
},
2720
+
"peerDependenciesMeta": {
2721
+
"@types/react": {
2722
+
"optional": true
2723
+
},
2724
+
"@types/react-dom": {
2725
+
"optional": true
2726
+
}
2727
+
}
2728
+
},
2729
+
"node_modules/@radix-ui/react-presence": {
2730
+
"version": "1.1.5",
2731
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
2732
+
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
2733
+
"dependencies": {
2734
+
"@radix-ui/react-compose-refs": "1.1.2",
2735
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2736
+
},
2737
+
"peerDependencies": {
2738
+
"@types/react": "*",
2739
+
"@types/react-dom": "*",
2740
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2741
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2742
+
},
2743
+
"peerDependenciesMeta": {
2744
+
"@types/react": {
2745
+
"optional": true
2746
+
},
2747
+
"@types/react-dom": {
2748
+
"optional": true
2749
+
}
2750
+
}
2751
+
},
2752
+
"node_modules/@radix-ui/react-primitive": {
2753
+
"version": "2.1.3",
2754
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
2755
+
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
2756
+
"dependencies": {
2757
+
"@radix-ui/react-slot": "1.2.3"
2758
+
},
2759
+
"peerDependencies": {
2760
+
"@types/react": "*",
2761
+
"@types/react-dom": "*",
2762
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2763
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2764
+
},
2765
+
"peerDependenciesMeta": {
2766
+
"@types/react": {
2767
+
"optional": true
2768
+
},
2769
+
"@types/react-dom": {
2770
+
"optional": true
2771
+
}
2772
+
}
2773
+
},
2774
+
"node_modules/@radix-ui/react-progress": {
2775
+
"version": "1.1.7",
2776
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
2777
+
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
2778
+
"dependencies": {
2779
+
"@radix-ui/react-context": "1.1.2",
2780
+
"@radix-ui/react-primitive": "2.1.3"
2781
+
},
2782
+
"peerDependencies": {
2783
+
"@types/react": "*",
2784
+
"@types/react-dom": "*",
2785
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2786
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2787
+
},
2788
+
"peerDependenciesMeta": {
2789
+
"@types/react": {
2790
+
"optional": true
2791
+
},
2792
+
"@types/react-dom": {
2793
+
"optional": true
2794
+
}
2795
+
}
2796
+
},
2797
+
"node_modules/@radix-ui/react-radio-group": {
2798
+
"version": "1.3.8",
2799
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
2800
+
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
2801
+
"dependencies": {
2802
+
"@radix-ui/primitive": "1.1.3",
2803
+
"@radix-ui/react-compose-refs": "1.1.2",
2804
+
"@radix-ui/react-context": "1.1.2",
2805
+
"@radix-ui/react-direction": "1.1.1",
2806
+
"@radix-ui/react-presence": "1.1.5",
2807
+
"@radix-ui/react-primitive": "2.1.3",
2808
+
"@radix-ui/react-roving-focus": "1.1.11",
2809
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2810
+
"@radix-ui/react-use-previous": "1.1.1",
2811
+
"@radix-ui/react-use-size": "1.1.1"
2812
+
},
2813
+
"peerDependencies": {
2814
+
"@types/react": "*",
2815
+
"@types/react-dom": "*",
2816
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2817
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2818
+
},
2819
+
"peerDependenciesMeta": {
2820
+
"@types/react": {
2821
+
"optional": true
2822
+
},
2823
+
"@types/react-dom": {
2824
+
"optional": true
2825
+
}
2826
+
}
2827
+
},
2828
+
"node_modules/@radix-ui/react-roving-focus": {
2829
+
"version": "1.1.11",
2830
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
2831
+
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
2832
+
"dependencies": {
2833
+
"@radix-ui/primitive": "1.1.3",
2834
+
"@radix-ui/react-collection": "1.1.7",
2835
+
"@radix-ui/react-compose-refs": "1.1.2",
2836
+
"@radix-ui/react-context": "1.1.2",
2837
+
"@radix-ui/react-direction": "1.1.1",
2838
+
"@radix-ui/react-id": "1.1.1",
2839
+
"@radix-ui/react-primitive": "2.1.3",
2840
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2841
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2842
+
},
2843
+
"peerDependencies": {
2844
+
"@types/react": "*",
2845
+
"@types/react-dom": "*",
2846
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2847
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2848
+
},
2849
+
"peerDependenciesMeta": {
2850
+
"@types/react": {
2851
+
"optional": true
2852
+
},
2853
+
"@types/react-dom": {
2854
+
"optional": true
2855
+
}
2856
+
}
2857
+
},
2858
+
"node_modules/@radix-ui/react-scroll-area": {
2859
+
"version": "1.2.10",
2860
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
2861
+
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
2862
+
"dependencies": {
2863
+
"@radix-ui/number": "1.1.1",
2864
+
"@radix-ui/primitive": "1.1.3",
2865
+
"@radix-ui/react-compose-refs": "1.1.2",
2866
+
"@radix-ui/react-context": "1.1.2",
2867
+
"@radix-ui/react-direction": "1.1.1",
2868
+
"@radix-ui/react-presence": "1.1.5",
2869
+
"@radix-ui/react-primitive": "2.1.3",
2870
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2871
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2872
+
},
2873
+
"peerDependencies": {
2874
+
"@types/react": "*",
2875
+
"@types/react-dom": "*",
2876
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2877
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2878
+
},
2879
+
"peerDependenciesMeta": {
2880
+
"@types/react": {
2881
+
"optional": true
2882
+
},
2883
+
"@types/react-dom": {
2884
+
"optional": true
2885
+
}
2886
+
}
2887
+
},
2888
+
"node_modules/@radix-ui/react-select": {
2889
+
"version": "2.2.6",
2890
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
2891
+
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
2892
+
"dependencies": {
2893
+
"@radix-ui/number": "1.1.1",
2894
+
"@radix-ui/primitive": "1.1.3",
2895
+
"@radix-ui/react-collection": "1.1.7",
2896
+
"@radix-ui/react-compose-refs": "1.1.2",
2897
+
"@radix-ui/react-context": "1.1.2",
2898
+
"@radix-ui/react-direction": "1.1.1",
2899
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2900
+
"@radix-ui/react-focus-guards": "1.1.3",
2901
+
"@radix-ui/react-focus-scope": "1.1.7",
2902
+
"@radix-ui/react-id": "1.1.1",
2903
+
"@radix-ui/react-popper": "1.2.8",
2904
+
"@radix-ui/react-portal": "1.1.9",
2905
+
"@radix-ui/react-primitive": "2.1.3",
2906
+
"@radix-ui/react-slot": "1.2.3",
2907
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2908
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2909
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2910
+
"@radix-ui/react-use-previous": "1.1.1",
2911
+
"@radix-ui/react-visually-hidden": "1.2.3",
2912
+
"aria-hidden": "^1.2.4",
2913
+
"react-remove-scroll": "^2.6.3"
2914
+
},
2915
+
"peerDependencies": {
2916
+
"@types/react": "*",
2917
+
"@types/react-dom": "*",
2918
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2919
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2920
+
},
2921
+
"peerDependenciesMeta": {
2922
+
"@types/react": {
2923
+
"optional": true
2924
+
},
2925
+
"@types/react-dom": {
2926
+
"optional": true
2927
+
}
2928
+
}
2929
+
},
2930
+
"node_modules/@radix-ui/react-separator": {
2931
+
"version": "1.1.7",
2932
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
2933
+
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
2934
+
"dependencies": {
2935
+
"@radix-ui/react-primitive": "2.1.3"
2936
+
},
2937
+
"peerDependencies": {
2938
+
"@types/react": "*",
2939
+
"@types/react-dom": "*",
2940
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2941
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2942
+
},
2943
+
"peerDependenciesMeta": {
2944
+
"@types/react": {
2945
+
"optional": true
2946
+
},
2947
+
"@types/react-dom": {
2948
+
"optional": true
2949
+
}
2950
+
}
2951
+
},
2952
+
"node_modules/@radix-ui/react-slider": {
2953
+
"version": "1.3.6",
2954
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
2955
+
"integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
2956
+
"dependencies": {
2957
+
"@radix-ui/number": "1.1.1",
2958
+
"@radix-ui/primitive": "1.1.3",
2959
+
"@radix-ui/react-collection": "1.1.7",
2960
+
"@radix-ui/react-compose-refs": "1.1.2",
2961
+
"@radix-ui/react-context": "1.1.2",
2962
+
"@radix-ui/react-direction": "1.1.1",
2963
+
"@radix-ui/react-primitive": "2.1.3",
2964
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2965
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2966
+
"@radix-ui/react-use-previous": "1.1.1",
2967
+
"@radix-ui/react-use-size": "1.1.1"
2968
+
},
2969
+
"peerDependencies": {
2970
+
"@types/react": "*",
2971
+
"@types/react-dom": "*",
2972
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2973
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2974
+
},
2975
+
"peerDependenciesMeta": {
2976
+
"@types/react": {
2977
+
"optional": true
2978
+
},
2979
+
"@types/react-dom": {
2980
+
"optional": true
2981
+
}
2982
+
}
2983
+
},
2984
+
"node_modules/@radix-ui/react-slot": {
2985
+
"version": "1.2.3",
2986
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
2987
+
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
2988
+
"dependencies": {
2989
+
"@radix-ui/react-compose-refs": "1.1.2"
2990
+
},
2991
+
"peerDependencies": {
2992
+
"@types/react": "*",
2993
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2994
+
},
2995
+
"peerDependenciesMeta": {
2996
+
"@types/react": {
2997
+
"optional": true
2998
+
}
2999
+
}
3000
+
},
3001
+
"node_modules/@radix-ui/react-switch": {
3002
+
"version": "1.2.6",
3003
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
3004
+
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
3005
+
"dependencies": {
3006
+
"@radix-ui/primitive": "1.1.3",
3007
+
"@radix-ui/react-compose-refs": "1.1.2",
3008
+
"@radix-ui/react-context": "1.1.2",
3009
+
"@radix-ui/react-primitive": "2.1.3",
3010
+
"@radix-ui/react-use-controllable-state": "1.2.2",
3011
+
"@radix-ui/react-use-previous": "1.1.1",
3012
+
"@radix-ui/react-use-size": "1.1.1"
3013
+
},
3014
+
"peerDependencies": {
3015
+
"@types/react": "*",
3016
+
"@types/react-dom": "*",
3017
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3018
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3019
+
},
3020
+
"peerDependenciesMeta": {
3021
+
"@types/react": {
3022
+
"optional": true
3023
+
},
3024
+
"@types/react-dom": {
3025
+
"optional": true
3026
+
}
3027
+
}
3028
+
},
3029
+
"node_modules/@radix-ui/react-tabs": {
3030
+
"version": "1.1.13",
3031
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
3032
+
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
3033
+
"dependencies": {
3034
+
"@radix-ui/primitive": "1.1.3",
3035
+
"@radix-ui/react-context": "1.1.2",
3036
+
"@radix-ui/react-direction": "1.1.1",
3037
+
"@radix-ui/react-id": "1.1.1",
3038
+
"@radix-ui/react-presence": "1.1.5",
3039
+
"@radix-ui/react-primitive": "2.1.3",
3040
+
"@radix-ui/react-roving-focus": "1.1.11",
3041
+
"@radix-ui/react-use-controllable-state": "1.2.2"
3042
+
},
3043
+
"peerDependencies": {
3044
+
"@types/react": "*",
3045
+
"@types/react-dom": "*",
3046
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3047
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3048
+
},
3049
+
"peerDependenciesMeta": {
3050
+
"@types/react": {
3051
+
"optional": true
3052
+
},
3053
+
"@types/react-dom": {
3054
+
"optional": true
3055
+
}
3056
+
}
3057
+
},
3058
+
"node_modules/@radix-ui/react-toast": {
3059
+
"version": "1.2.15",
3060
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
3061
+
"integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
3062
+
"dependencies": {
3063
+
"@radix-ui/primitive": "1.1.3",
3064
+
"@radix-ui/react-collection": "1.1.7",
3065
+
"@radix-ui/react-compose-refs": "1.1.2",
3066
+
"@radix-ui/react-context": "1.1.2",
3067
+
"@radix-ui/react-dismissable-layer": "1.1.11",
3068
+
"@radix-ui/react-portal": "1.1.9",
3069
+
"@radix-ui/react-presence": "1.1.5",
3070
+
"@radix-ui/react-primitive": "2.1.3",
3071
+
"@radix-ui/react-use-callback-ref": "1.1.1",
3072
+
"@radix-ui/react-use-controllable-state": "1.2.2",
3073
+
"@radix-ui/react-use-layout-effect": "1.1.1",
3074
+
"@radix-ui/react-visually-hidden": "1.2.3"
3075
+
},
3076
+
"peerDependencies": {
3077
+
"@types/react": "*",
3078
+
"@types/react-dom": "*",
3079
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3080
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3081
+
},
3082
+
"peerDependenciesMeta": {
3083
+
"@types/react": {
3084
+
"optional": true
3085
+
},
3086
+
"@types/react-dom": {
3087
+
"optional": true
3088
+
}
3089
+
}
3090
+
},
3091
+
"node_modules/@radix-ui/react-toggle": {
3092
+
"version": "1.1.10",
3093
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
3094
+
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
3095
+
"dependencies": {
3096
+
"@radix-ui/primitive": "1.1.3",
3097
+
"@radix-ui/react-primitive": "2.1.3",
3098
+
"@radix-ui/react-use-controllable-state": "1.2.2"
3099
+
},
3100
+
"peerDependencies": {
3101
+
"@types/react": "*",
3102
+
"@types/react-dom": "*",
3103
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3104
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3105
+
},
3106
+
"peerDependenciesMeta": {
3107
+
"@types/react": {
3108
+
"optional": true
3109
+
},
3110
+
"@types/react-dom": {
3111
+
"optional": true
3112
+
}
3113
+
}
3114
+
},
3115
+
"node_modules/@radix-ui/react-toggle-group": {
3116
+
"version": "1.1.11",
3117
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
3118
+
"integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
3119
+
"dependencies": {
3120
+
"@radix-ui/primitive": "1.1.3",
3121
+
"@radix-ui/react-context": "1.1.2",
3122
+
"@radix-ui/react-direction": "1.1.1",
3123
+
"@radix-ui/react-primitive": "2.1.3",
3124
+
"@radix-ui/react-roving-focus": "1.1.11",
3125
+
"@radix-ui/react-toggle": "1.1.10",
3126
+
"@radix-ui/react-use-controllable-state": "1.2.2"
3127
+
},
3128
+
"peerDependencies": {
3129
+
"@types/react": "*",
3130
+
"@types/react-dom": "*",
3131
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3132
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3133
+
},
3134
+
"peerDependenciesMeta": {
3135
+
"@types/react": {
3136
+
"optional": true
3137
+
},
3138
+
"@types/react-dom": {
3139
+
"optional": true
3140
+
}
3141
+
}
3142
+
},
3143
+
"node_modules/@radix-ui/react-toolbar": {
3144
+
"version": "1.1.11",
3145
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz",
3146
+
"integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==",
3147
+
"dependencies": {
3148
+
"@radix-ui/primitive": "1.1.3",
3149
+
"@radix-ui/react-context": "1.1.2",
3150
+
"@radix-ui/react-direction": "1.1.1",
3151
+
"@radix-ui/react-primitive": "2.1.3",
3152
+
"@radix-ui/react-roving-focus": "1.1.11",
3153
+
"@radix-ui/react-separator": "1.1.7",
3154
+
"@radix-ui/react-toggle-group": "1.1.11"
3155
+
},
3156
+
"peerDependencies": {
3157
+
"@types/react": "*",
3158
+
"@types/react-dom": "*",
3159
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3160
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3161
+
},
3162
+
"peerDependenciesMeta": {
3163
+
"@types/react": {
3164
+
"optional": true
3165
+
},
3166
+
"@types/react-dom": {
3167
+
"optional": true
3168
+
}
3169
+
}
3170
+
},
3171
+
"node_modules/@radix-ui/react-tooltip": {
3172
+
"version": "1.2.8",
3173
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
3174
+
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
3175
+
"dependencies": {
3176
+
"@radix-ui/primitive": "1.1.3",
3177
+
"@radix-ui/react-compose-refs": "1.1.2",
3178
+
"@radix-ui/react-context": "1.1.2",
3179
+
"@radix-ui/react-dismissable-layer": "1.1.11",
3180
+
"@radix-ui/react-id": "1.1.1",
3181
+
"@radix-ui/react-popper": "1.2.8",
3182
+
"@radix-ui/react-portal": "1.1.9",
3183
+
"@radix-ui/react-presence": "1.1.5",
3184
+
"@radix-ui/react-primitive": "2.1.3",
3185
+
"@radix-ui/react-slot": "1.2.3",
3186
+
"@radix-ui/react-use-controllable-state": "1.2.2",
3187
+
"@radix-ui/react-visually-hidden": "1.2.3"
3188
+
},
3189
+
"peerDependencies": {
3190
+
"@types/react": "*",
3191
+
"@types/react-dom": "*",
3192
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3193
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3194
+
},
3195
+
"peerDependenciesMeta": {
3196
+
"@types/react": {
3197
+
"optional": true
3198
+
},
3199
+
"@types/react-dom": {
3200
+
"optional": true
3201
+
}
3202
+
}
3203
+
},
3204
+
"node_modules/@radix-ui/react-use-callback-ref": {
3205
+
"version": "1.1.1",
3206
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
3207
+
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
3208
+
"peerDependencies": {
3209
+
"@types/react": "*",
3210
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3211
+
},
3212
+
"peerDependenciesMeta": {
3213
+
"@types/react": {
3214
+
"optional": true
3215
+
}
3216
+
}
3217
+
},
3218
+
"node_modules/@radix-ui/react-use-controllable-state": {
3219
+
"version": "1.2.2",
3220
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
3221
+
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
3222
+
"dependencies": {
3223
+
"@radix-ui/react-use-effect-event": "0.0.2",
3224
+
"@radix-ui/react-use-layout-effect": "1.1.1"
3225
+
},
3226
+
"peerDependencies": {
3227
+
"@types/react": "*",
3228
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3229
+
},
3230
+
"peerDependenciesMeta": {
3231
+
"@types/react": {
3232
+
"optional": true
3233
+
}
3234
+
}
3235
+
},
3236
+
"node_modules/@radix-ui/react-use-effect-event": {
3237
+
"version": "0.0.2",
3238
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
3239
+
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
3240
+
"dependencies": {
3241
+
"@radix-ui/react-use-layout-effect": "1.1.1"
3242
+
},
3243
+
"peerDependencies": {
3244
+
"@types/react": "*",
3245
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3246
+
},
3247
+
"peerDependenciesMeta": {
3248
+
"@types/react": {
3249
+
"optional": true
3250
+
}
3251
+
}
3252
+
},
3253
+
"node_modules/@radix-ui/react-use-escape-keydown": {
3254
+
"version": "1.1.1",
3255
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
3256
+
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
3257
+
"dependencies": {
3258
+
"@radix-ui/react-use-callback-ref": "1.1.1"
3259
+
},
3260
+
"peerDependencies": {
3261
+
"@types/react": "*",
3262
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3263
+
},
3264
+
"peerDependenciesMeta": {
3265
+
"@types/react": {
3266
+
"optional": true
3267
+
}
3268
+
}
3269
+
},
3270
+
"node_modules/@radix-ui/react-use-is-hydrated": {
3271
+
"version": "0.1.0",
3272
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
3273
+
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
3274
+
"dependencies": {
3275
+
"use-sync-external-store": "^1.5.0"
3276
+
},
3277
+
"peerDependencies": {
3278
+
"@types/react": "*",
3279
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3280
+
},
3281
+
"peerDependenciesMeta": {
3282
+
"@types/react": {
3283
+
"optional": true
3284
+
}
3285
+
}
3286
+
},
3287
+
"node_modules/@radix-ui/react-use-layout-effect": {
3288
+
"version": "1.1.1",
3289
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
3290
+
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
3291
+
"peerDependencies": {
3292
+
"@types/react": "*",
3293
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3294
+
},
3295
+
"peerDependenciesMeta": {
3296
+
"@types/react": {
3297
+
"optional": true
3298
+
}
3299
+
}
3300
+
},
3301
+
"node_modules/@radix-ui/react-use-previous": {
3302
+
"version": "1.1.1",
3303
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
3304
+
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
3305
+
"peerDependencies": {
3306
+
"@types/react": "*",
3307
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3308
+
},
3309
+
"peerDependenciesMeta": {
3310
+
"@types/react": {
3311
+
"optional": true
3312
+
}
3313
+
}
3314
+
},
3315
+
"node_modules/@radix-ui/react-use-rect": {
3316
+
"version": "1.1.1",
3317
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
3318
+
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
3319
+
"dependencies": {
3320
+
"@radix-ui/rect": "1.1.1"
3321
+
},
3322
+
"peerDependencies": {
3323
+
"@types/react": "*",
3324
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3325
+
},
3326
+
"peerDependenciesMeta": {
3327
+
"@types/react": {
3328
+
"optional": true
3329
+
}
3330
+
}
3331
+
},
3332
+
"node_modules/@radix-ui/react-use-size": {
3333
+
"version": "1.1.1",
3334
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
3335
+
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
3336
+
"dependencies": {
3337
+
"@radix-ui/react-use-layout-effect": "1.1.1"
3338
+
},
3339
+
"peerDependencies": {
3340
+
"@types/react": "*",
3341
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3342
+
},
3343
+
"peerDependenciesMeta": {
3344
+
"@types/react": {
3345
+
"optional": true
3346
+
}
3347
+
}
3348
+
},
3349
+
"node_modules/@radix-ui/react-visually-hidden": {
3350
+
"version": "1.2.3",
3351
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
3352
+
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
3353
+
"dependencies": {
3354
+
"@radix-ui/react-primitive": "2.1.3"
3355
+
},
3356
+
"peerDependencies": {
3357
+
"@types/react": "*",
3358
+
"@types/react-dom": "*",
3359
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3360
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3361
+
},
3362
+
"peerDependenciesMeta": {
3363
+
"@types/react": {
3364
+
"optional": true
3365
+
},
3366
+
"@types/react-dom": {
3367
+
"optional": true
3368
+
}
3369
+
}
3370
+
},
3371
+
"node_modules/@radix-ui/rect": {
3372
+
"version": "1.1.1",
3373
+
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
3374
+
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
3375
+
},
3376
"node_modules/@rolldown/pluginutils": {
3377
"version": "1.0.0-beta.27",
3378
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
···
5284
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
5285
"dev": true,
5286
"license": "Python-2.0"
5287
+
},
5288
+
"node_modules/aria-hidden": {
5289
+
"version": "1.2.6",
5290
+
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
5291
+
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
5292
+
"dependencies": {
5293
+
"tslib": "^2.0.0"
5294
+
},
5295
+
"engines": {
5296
+
"node": ">=10"
5297
+
}
5298
},
5299
"node_modules/aria-query": {
5300
"version": "5.3.0",
···
6205
"node": ">=8"
6206
}
6207
},
6208
+
"node_modules/detect-node-es": {
6209
+
"version": "1.1.0",
6210
+
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
6211
+
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
6212
+
},
6213
"node_modules/diff": {
6214
"version": "8.0.2",
6215
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
···
7229
},
7230
"funding": {
7231
"url": "https://github.com/sponsors/ljharb"
7232
+
}
7233
+
},
7234
+
"node_modules/get-nonce": {
7235
+
"version": "1.0.1",
7236
+
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
7237
+
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
7238
+
"engines": {
7239
+
"node": ">=6"
7240
}
7241
},
7242
"node_modules/get-proto": {
···
11840
],
11841
"license": "MIT"
11842
},
11843
+
"node_modules/radix-ui": {
11844
+
"version": "1.4.3",
11845
+
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
11846
+
"integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==",
11847
+
"dependencies": {
11848
+
"@radix-ui/primitive": "1.1.3",
11849
+
"@radix-ui/react-accessible-icon": "1.1.7",
11850
+
"@radix-ui/react-accordion": "1.2.12",
11851
+
"@radix-ui/react-alert-dialog": "1.1.15",
11852
+
"@radix-ui/react-arrow": "1.1.7",
11853
+
"@radix-ui/react-aspect-ratio": "1.1.7",
11854
+
"@radix-ui/react-avatar": "1.1.10",
11855
+
"@radix-ui/react-checkbox": "1.3.3",
11856
+
"@radix-ui/react-collapsible": "1.1.12",
11857
+
"@radix-ui/react-collection": "1.1.7",
11858
+
"@radix-ui/react-compose-refs": "1.1.2",
11859
+
"@radix-ui/react-context": "1.1.2",
11860
+
"@radix-ui/react-context-menu": "2.2.16",
11861
+
"@radix-ui/react-dialog": "1.1.15",
11862
+
"@radix-ui/react-direction": "1.1.1",
11863
+
"@radix-ui/react-dismissable-layer": "1.1.11",
11864
+
"@radix-ui/react-dropdown-menu": "2.1.16",
11865
+
"@radix-ui/react-focus-guards": "1.1.3",
11866
+
"@radix-ui/react-focus-scope": "1.1.7",
11867
+
"@radix-ui/react-form": "0.1.8",
11868
+
"@radix-ui/react-hover-card": "1.1.15",
11869
+
"@radix-ui/react-label": "2.1.7",
11870
+
"@radix-ui/react-menu": "2.1.16",
11871
+
"@radix-ui/react-menubar": "1.1.16",
11872
+
"@radix-ui/react-navigation-menu": "1.2.14",
11873
+
"@radix-ui/react-one-time-password-field": "0.1.8",
11874
+
"@radix-ui/react-password-toggle-field": "0.1.3",
11875
+
"@radix-ui/react-popover": "1.1.15",
11876
+
"@radix-ui/react-popper": "1.2.8",
11877
+
"@radix-ui/react-portal": "1.1.9",
11878
+
"@radix-ui/react-presence": "1.1.5",
11879
+
"@radix-ui/react-primitive": "2.1.3",
11880
+
"@radix-ui/react-progress": "1.1.7",
11881
+
"@radix-ui/react-radio-group": "1.3.8",
11882
+
"@radix-ui/react-roving-focus": "1.1.11",
11883
+
"@radix-ui/react-scroll-area": "1.2.10",
11884
+
"@radix-ui/react-select": "2.2.6",
11885
+
"@radix-ui/react-separator": "1.1.7",
11886
+
"@radix-ui/react-slider": "1.3.6",
11887
+
"@radix-ui/react-slot": "1.2.3",
11888
+
"@radix-ui/react-switch": "1.2.6",
11889
+
"@radix-ui/react-tabs": "1.1.13",
11890
+
"@radix-ui/react-toast": "1.2.15",
11891
+
"@radix-ui/react-toggle": "1.1.10",
11892
+
"@radix-ui/react-toggle-group": "1.1.11",
11893
+
"@radix-ui/react-toolbar": "1.1.11",
11894
+
"@radix-ui/react-tooltip": "1.2.8",
11895
+
"@radix-ui/react-use-callback-ref": "1.1.1",
11896
+
"@radix-ui/react-use-controllable-state": "1.2.2",
11897
+
"@radix-ui/react-use-effect-event": "0.0.2",
11898
+
"@radix-ui/react-use-escape-keydown": "1.1.1",
11899
+
"@radix-ui/react-use-is-hydrated": "0.1.0",
11900
+
"@radix-ui/react-use-layout-effect": "1.1.1",
11901
+
"@radix-ui/react-use-size": "1.1.1",
11902
+
"@radix-ui/react-visually-hidden": "1.2.3"
11903
+
},
11904
+
"peerDependencies": {
11905
+
"@types/react": "*",
11906
+
"@types/react-dom": "*",
11907
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
11908
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
11909
+
},
11910
+
"peerDependenciesMeta": {
11911
+
"@types/react": {
11912
+
"optional": true
11913
+
},
11914
+
"@types/react-dom": {
11915
+
"optional": true
11916
+
}
11917
+
}
11918
+
},
11919
"node_modules/react": {
11920
"version": "19.1.1",
11921
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
···
11977
"node": ">=0.10.0"
11978
}
11979
},
11980
+
"node_modules/react-remove-scroll": {
11981
+
"version": "2.7.1",
11982
+
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
11983
+
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
11984
+
"dependencies": {
11985
+
"react-remove-scroll-bar": "^2.3.7",
11986
+
"react-style-singleton": "^2.2.3",
11987
+
"tslib": "^2.1.0",
11988
+
"use-callback-ref": "^1.3.3",
11989
+
"use-sidecar": "^1.1.3"
11990
+
},
11991
+
"engines": {
11992
+
"node": ">=10"
11993
+
},
11994
+
"peerDependencies": {
11995
+
"@types/react": "*",
11996
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
11997
+
},
11998
+
"peerDependenciesMeta": {
11999
+
"@types/react": {
12000
+
"optional": true
12001
+
}
12002
+
}
12003
+
},
12004
+
"node_modules/react-remove-scroll-bar": {
12005
+
"version": "2.3.8",
12006
+
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
12007
+
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
12008
+
"dependencies": {
12009
+
"react-style-singleton": "^2.2.2",
12010
+
"tslib": "^2.0.0"
12011
+
},
12012
+
"engines": {
12013
+
"node": ">=10"
12014
+
},
12015
+
"peerDependencies": {
12016
+
"@types/react": "*",
12017
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
12018
+
},
12019
+
"peerDependenciesMeta": {
12020
+
"@types/react": {
12021
+
"optional": true
12022
+
}
12023
+
}
12024
+
},
12025
+
"node_modules/react-style-singleton": {
12026
+
"version": "2.2.3",
12027
+
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
12028
+
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
12029
+
"dependencies": {
12030
+
"get-nonce": "^1.0.0",
12031
+
"tslib": "^2.0.0"
12032
+
},
12033
+
"engines": {
12034
+
"node": ">=10"
12035
+
},
12036
+
"peerDependencies": {
12037
+
"@types/react": "*",
12038
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
12039
+
},
12040
+
"peerDependenciesMeta": {
12041
+
"@types/react": {
12042
+
"optional": true
12043
+
}
12044
+
}
12045
+
},
12046
"node_modules/readdirp": {
12047
"version": "3.6.0",
12048
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
···
13536
"peer": true,
13537
"dependencies": {
13538
"punycode": "^2.1.0"
13539
+
}
13540
+
},
13541
+
"node_modules/use-callback-ref": {
13542
+
"version": "1.3.3",
13543
+
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
13544
+
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
13545
+
"dependencies": {
13546
+
"tslib": "^2.0.0"
13547
+
},
13548
+
"engines": {
13549
+
"node": ">=10"
13550
+
},
13551
+
"peerDependencies": {
13552
+
"@types/react": "*",
13553
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
13554
+
},
13555
+
"peerDependenciesMeta": {
13556
+
"@types/react": {
13557
+
"optional": true
13558
+
}
13559
+
}
13560
+
},
13561
+
"node_modules/use-sidecar": {
13562
+
"version": "1.1.3",
13563
+
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
13564
+
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
13565
+
"dependencies": {
13566
+
"detect-node-es": "^1.1.0",
13567
+
"tslib": "^2.0.0"
13568
+
},
13569
+
"engines": {
13570
+
"node": ">=10"
13571
+
},
13572
+
"peerDependencies": {
13573
+
"@types/react": "*",
13574
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
13575
+
},
13576
+
"peerDependenciesMeta": {
13577
+
"@types/react": {
13578
+
"optional": true
13579
+
}
13580
}
13581
},
13582
"node_modules/use-sync-external-store": {
+5
package.json
+5
package.json
···
12
"dependencies": {
13
"@atproto/api": "^0.16.6",
14
"@atproto/oauth-client-browser": "^0.3.33",
15
"@tailwindcss/vite": "^4.0.6",
16
"@tanstack/query-sync-storage-persister": "^5.85.6",
17
"@tanstack/react-devtools": "^0.2.2",
···
25
"idb-keyval": "^6.2.2",
26
"jotai": "^2.13.1",
27
"npm": "^11.6.2",
28
"react": "^19.0.0",
29
"react-dom": "^19.0.0",
30
"react-player": "^3.3.2",
···
12
"dependencies": {
13
"@atproto/api": "^0.16.6",
14
"@atproto/oauth-client-browser": "^0.3.33",
15
+
"@radix-ui/react-dialog": "^1.1.15",
16
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
17
+
"@radix-ui/react-hover-card": "^1.1.15",
18
+
"@radix-ui/react-slider": "^1.3.6",
19
"@tailwindcss/vite": "^4.0.6",
20
"@tanstack/query-sync-storage-persister": "^5.85.6",
21
"@tanstack/react-devtools": "^0.2.2",
···
29
"idb-keyval": "^6.2.2",
30
"jotai": "^2.13.1",
31
"npm": "^11.6.2",
32
+
"radix-ui": "^1.4.3",
33
"react": "^19.0.0",
34
"react-dom": "^19.0.0",
35
"react-player": "^3.3.2",
+292
src/components/Composer.tsx
+292
src/components/Composer.tsx
···
···
1
+
import { AppBskyRichtextFacet, RichText } from "@atproto/api";
2
+
import { useAtom } from "jotai";
3
+
import { Dialog } from "radix-ui";
4
+
import { useEffect, useRef, useState } from "react";
5
+
6
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { composerAtom } from "~/utils/atoms";
8
+
import { useQueryPost } from "~/utils/useQuery";
9
+
10
+
import { ProfileThing } from "./Login";
11
+
import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer";
12
+
13
+
const MAX_POST_LENGTH = 300;
14
+
15
+
export function Composer() {
16
+
const [composerState, setComposerState] = useAtom(composerAtom);
17
+
const { agent } = useAuth();
18
+
19
+
const [postText, setPostText] = useState("");
20
+
const [posting, setPosting] = useState(false);
21
+
const [postSuccess, setPostSuccess] = useState(false);
22
+
const [postError, setPostError] = useState<string | null>(null);
23
+
24
+
useEffect(() => {
25
+
setPostText("");
26
+
setPosting(false);
27
+
setPostSuccess(false);
28
+
setPostError(null);
29
+
}, [composerState.kind]);
30
+
31
+
const parentUri =
32
+
composerState.kind === "reply"
33
+
? composerState.parent
34
+
: composerState.kind === "quote"
35
+
? composerState.subject
36
+
: undefined;
37
+
38
+
const { data: parentPost, isLoading: isParentLoading } =
39
+
useQueryPost(parentUri);
40
+
41
+
async function handlePost() {
42
+
if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return;
43
+
44
+
setPosting(true);
45
+
setPostError(null);
46
+
47
+
try {
48
+
const rt = new RichText({ text: postText });
49
+
await rt.detectFacets(agent);
50
+
51
+
if (rt.facets?.length) {
52
+
rt.facets = rt.facets.filter((item) => {
53
+
if (item.$type !== "app.bsky.richtext.facet") return true;
54
+
if (!item.features?.length) return true;
55
+
56
+
item.features = item.features.filter((feature) => {
57
+
if (feature.$type !== "app.bsky.richtext.facet#mention") return true;
58
+
const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined;
59
+
return typeof did === "string" && did.startsWith("did:");
60
+
});
61
+
62
+
return item.features.length > 0;
63
+
});
64
+
}
65
+
66
+
const record: Record<string, unknown> = {
67
+
$type: "app.bsky.feed.post",
68
+
text: rt.text,
69
+
facets: rt.facets,
70
+
createdAt: new Date().toISOString(),
71
+
};
72
+
73
+
if (composerState.kind === "reply" && parentPost) {
74
+
record.reply = {
75
+
root: parentPost.value?.reply?.root ?? {
76
+
uri: parentPost.uri,
77
+
cid: parentPost.cid,
78
+
},
79
+
parent: {
80
+
uri: parentPost.uri,
81
+
cid: parentPost.cid,
82
+
},
83
+
};
84
+
}
85
+
86
+
if (composerState.kind === "quote" && parentPost) {
87
+
record.embed = {
88
+
$type: "app.bsky.embed.record",
89
+
record: {
90
+
uri: parentPost.uri,
91
+
cid: parentPost.cid,
92
+
},
93
+
};
94
+
}
95
+
96
+
await agent.com.atproto.repo.createRecord({
97
+
collection: "app.bsky.feed.post",
98
+
repo: agent.assertDid,
99
+
record,
100
+
});
101
+
102
+
setPostSuccess(true);
103
+
setPostText("");
104
+
105
+
setTimeout(() => {
106
+
setPostSuccess(false);
107
+
setComposerState({ kind: "closed" });
108
+
}, 1500);
109
+
} catch (e: any) {
110
+
setPostError(e?.message || "Failed to post");
111
+
} finally {
112
+
setPosting(false);
113
+
}
114
+
}
115
+
// if (composerState.kind === "closed") {
116
+
// return null;
117
+
// }
118
+
119
+
const getPlaceholder = () => {
120
+
switch (composerState.kind) {
121
+
case "reply":
122
+
return "Post your reply";
123
+
case "quote":
124
+
return "Add a comment...";
125
+
case "root":
126
+
default:
127
+
return "What's happening?!";
128
+
}
129
+
};
130
+
131
+
const charsLeft = MAX_POST_LENGTH - postText.length;
132
+
const isPostButtonDisabled =
133
+
posting || !postText.trim() || isParentLoading || charsLeft < 0;
134
+
135
+
return (
136
+
<Dialog.Root
137
+
open={composerState.kind !== "closed"}
138
+
onOpenChange={(open) => {
139
+
if (!open) setComposerState({ kind: "closed" });
140
+
}}
141
+
>
142
+
<Dialog.Portal>
143
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" />
144
+
145
+
<Dialog.Content className="fixed overflow-y-scroll inset-0 z-50 flex items-start justify-center py-10 sm:py-20">
146
+
<div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4">
147
+
<div className="flex flex-row justify-between p-2">
148
+
<Dialog.Close asChild>
149
+
<button
150
+
className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
151
+
disabled={posting}
152
+
aria-label="Close"
153
+
>
154
+
<svg
155
+
xmlns="http://www.w3.org/2000/svg"
156
+
width="20"
157
+
height="20"
158
+
viewBox="0 0 24 24"
159
+
fill="none"
160
+
stroke="currentColor"
161
+
strokeWidth="2.5"
162
+
strokeLinecap="round"
163
+
strokeLinejoin="round"
164
+
>
165
+
<line x1="18" y1="6" x2="6" y2="18"></line>
166
+
<line x1="6" y1="6" x2="18" y2="18"></line>
167
+
</svg>
168
+
</button>
169
+
</Dialog.Close>
170
+
171
+
<div className="flex-1" />
172
+
<div className="flex items-center gap-4">
173
+
<span
174
+
className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`}
175
+
>
176
+
{charsLeft}
177
+
</span>
178
+
<button
179
+
className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
180
+
onClick={handlePost}
181
+
disabled={isPostButtonDisabled}
182
+
>
183
+
{posting ? "Posting..." : "Post"}
184
+
</button>
185
+
</div>
186
+
</div>
187
+
188
+
{postSuccess ? (
189
+
<div className="flex flex-col items-center justify-center py-16">
190
+
<span className="text-gray-500 text-6xl mb-4">โ</span>
191
+
<span className="text-xl font-bold text-black dark:text-white">
192
+
Posted!
193
+
</span>
194
+
</div>
195
+
) : (
196
+
<div className="px-4">
197
+
{composerState.kind === "reply" && (
198
+
<div className="mb-1 -mx-4">
199
+
{isParentLoading ? (
200
+
<div className="text-sm text-gray-500 animate-pulse">
201
+
Loading parent post...
202
+
</div>
203
+
) : parentUri ? (
204
+
<UniversalPostRendererATURILoader
205
+
atUri={parentUri}
206
+
bottomReplyLine
207
+
bottomBorder={false}
208
+
/>
209
+
) : (
210
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
211
+
Could not load parent post.
212
+
</div>
213
+
)}
214
+
</div>
215
+
)}
216
+
217
+
<div className="flex w-full gap-1 flex-col">
218
+
<ProfileThing agent={agent} large />
219
+
<div className="flex pl-[50px]">
220
+
<AutoGrowTextarea
221
+
className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2"
222
+
rows={5}
223
+
placeholder={getPlaceholder()}
224
+
value={postText}
225
+
onChange={(e) => setPostText(e.target.value)}
226
+
disabled={posting}
227
+
autoFocus
228
+
/>
229
+
</div>
230
+
</div>
231
+
232
+
{composerState.kind === "quote" && (
233
+
<div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
234
+
{isParentLoading ? (
235
+
<div className="text-sm text-gray-500 animate-pulse">
236
+
Loading parent post...
237
+
</div>
238
+
) : parentUri ? (
239
+
<UniversalPostRendererATURILoader
240
+
atUri={parentUri}
241
+
isQuote
242
+
/>
243
+
) : (
244
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
245
+
Could not load parent post.
246
+
</div>
247
+
)}
248
+
</div>
249
+
)}
250
+
251
+
{postError && (
252
+
<div className="text-red-500 text-sm my-2 text-center">
253
+
{postError}
254
+
</div>
255
+
)}
256
+
</div>
257
+
)}
258
+
</div>
259
+
</Dialog.Content>
260
+
</Dialog.Portal>
261
+
</Dialog.Root>
262
+
);
263
+
}
264
+
265
+
function AutoGrowTextarea({
266
+
value,
267
+
className,
268
+
onChange,
269
+
...props
270
+
}: React.DetailedHTMLProps<
271
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
272
+
HTMLTextAreaElement
273
+
>) {
274
+
const ref = useRef<HTMLTextAreaElement>(null);
275
+
276
+
useEffect(() => {
277
+
const el = ref.current;
278
+
if (!el) return;
279
+
el.style.height = "auto";
280
+
el.style.height = el.scrollHeight + "px";
281
+
}, [value]);
282
+
283
+
return (
284
+
<textarea
285
+
ref={ref}
286
+
className={className}
287
+
value={value}
288
+
onChange={onChange}
289
+
{...props}
290
+
/>
291
+
);
292
+
}
+150
src/components/Import.tsx
+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
+32
-6
src/components/InfiniteCustomFeed.tsx
···
1
import * as React from "react";
2
3
//import { useInView } from "react-intersection-observer";
···
37
isFetchingNextPage,
38
refetch,
39
isRefetching,
40
} = useInfiniteQueryFeedSkeleton({
41
feedUri: feedUri,
42
agent: agent ?? undefined,
···
44
pdsUrl: pdsUrl,
45
feedServiceDid: feedServiceDid,
46
});
47
48
const handleRefresh = () => {
49
refetch();
50
};
51
52
//const { ref, inView } = useInView();
53
54
// React.useEffect(() => {
···
67
);
68
}
69
70
-
const allPosts =
71
-
data?.pages.flatMap((page) => {
72
-
if (page) return page.feed;
73
-
}) ?? [];
74
75
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
76
return (
···
116
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
aria-label="Refresh feed"
118
>
119
-
<RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} />
120
</button>
121
</>
122
);
···
139
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
></path>
141
</svg>
142
-
);
···
1
+
import { useQueryClient } from "@tanstack/react-query";
2
import * as React from "react";
3
4
//import { useInView } from "react-intersection-observer";
···
38
isFetchingNextPage,
39
refetch,
40
isRefetching,
41
+
queryKey,
42
} = useInfiniteQueryFeedSkeleton({
43
feedUri: feedUri,
44
agent: agent ?? undefined,
···
46
pdsUrl: pdsUrl,
47
feedServiceDid: feedServiceDid,
48
});
49
+
const queryClient = useQueryClient();
50
+
51
52
const handleRefresh = () => {
53
+
queryClient.removeQueries({queryKey: queryKey});
54
+
//queryClient.invalidateQueries(["infinite-feed", feedUri] as const);
55
refetch();
56
};
57
58
+
const allPosts = React.useMemo(() => {
59
+
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
60
+
61
+
const seenUris = new Set<string>();
62
+
63
+
return flattenedPosts.filter((item) => {
64
+
if (!item?.post) return false;
65
+
66
+
if (seenUris.has(item.post)) {
67
+
return false;
68
+
}
69
+
70
+
seenUris.add(item.post);
71
+
72
+
return true;
73
+
});
74
+
}, [data]);
75
+
76
//const { ref, inView } = useInView();
77
78
// React.useEffect(() => {
···
91
);
92
}
93
94
+
// const allPosts =
95
+
// data?.pages.flatMap((page) => {
96
+
// if (page) return page.feed;
97
+
// }) ?? [];
98
99
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
100
return (
···
140
className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
141
aria-label="Refresh feed"
142
>
143
+
<RefreshIcon
144
+
className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`}
145
+
/>
146
</button>
147
</>
148
);
···
165
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
166
></path>
167
</svg>
168
+
);
+78
-34
src/components/Login.tsx
+78
-34
src/components/Login.tsx
···
1
// src/components/Login.tsx
2
-
import { Agent } from "@atproto/api";
3
import React, { useEffect, useRef, useState } from "react";
4
5
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
7
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
8
export default function Login({
···
21
className={
22
compact
23
? "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]"
25
}
26
>
27
<span
···
40
// Large view
41
if (!compact) {
42
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">
44
<div className="flex flex-col items-center justify-center text-center">
45
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
46
You are logged in!
···
74
if (!compact) {
75
// Large view renders the form directly in the card
76
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">
78
<UnifiedLoginForm />
79
</div>
80
);
···
110
// --- 3. Helper components for layouts, forms, and UI ---
111
112
// A new component to contain the logic for the compact dropdown
113
-
const CompactLoginButton = ({popup}:{popup?: boolean}) => {
114
const [showForm, setShowForm] = useState(false);
115
const formRef = useRef<HTMLDivElement>(null);
116
···
137
Log in
138
</button>
139
{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`}>
141
<UnifiedLoginForm />
142
</div>
143
)}
···
158
onClick={onClick}
159
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
160
active
161
-
? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
162
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
163
}`}
164
>
···
187
<p className="text-xs text-gray-500 dark:text-gray-400">
188
Sign in with AT. Your password is never shared.
189
</p>
190
-
<input
191
type="text"
192
placeholder="handle.bsky.social"
193
value={handle}
194
onChange={(e) => setHandle(e.target.value)}
195
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
</form>
204
);
205
};
···
232
<p className="text-xs text-red-500 dark:text-red-400">
233
Warning: Less secure. Use an App Password.
234
</p>
235
-
<input
236
type="text"
237
placeholder="handle.bsky.social"
238
value={user}
···
254
value={serviceURL}
255
onChange={(e) => setServiceURL(e.target.value)}
256
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
257
-
/>
258
{error && <p className="text-xs text-red-500">{error}</p>}
259
<button
260
type="submit"
···
274
agent: Agent | null;
275
large?: boolean;
276
}) => {
277
-
const [profile, setProfile] = useState<any>(null);
278
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]);
292
293
-
if (!profile) {
294
return (
295
// Skeleton loader
296
<div
···
316
className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`}
317
>
318
<img
319
-
src={profile?.avatar}
320
alt="avatar"
321
className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
322
/>
···
329
<div
330
className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`}
331
>
332
-
@{profile?.handle}
333
</div>
334
</div>
335
</div>
···
1
// src/components/Login.tsx
2
+
import AtpAgent, { Agent } from "@atproto/api";
3
+
import { useAtom } from "jotai";
4
import React, { useEffect, useRef, useState } from "react";
5
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { imgCDNAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery";
9
10
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
11
export default function Login({
···
24
className={
25
compact
26
? "flex items-center justify-center p-1"
27
+
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-4 mx-4 flex justify-center items-center h-[280px]"
28
}
29
>
30
<span
···
43
// Large view
44
if (!compact) {
45
return (
46
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
47
<div className="flex flex-col items-center justify-center text-center">
48
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
49
You are logged in!
···
77
if (!compact) {
78
// Large view renders the form directly in the card
79
return (
80
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
81
<UnifiedLoginForm />
82
</div>
83
);
···
113
// --- 3. Helper components for layouts, forms, and UI ---
114
115
// A new component to contain the logic for the compact dropdown
116
+
const CompactLoginButton = ({ popup }: { popup?: boolean }) => {
117
const [showForm, setShowForm] = useState(false);
118
const formRef = useRef<HTMLDivElement>(null);
119
···
140
Log in
141
</button>
142
{showForm && (
143
+
<div
144
+
className={`absolute ${popup ? `bottom-[calc(100%)]` : `top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`}
145
+
>
146
<UnifiedLoginForm />
147
</div>
148
)}
···
163
onClick={onClick}
164
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
165
active
166
+
? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
167
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
168
}`}
169
>
···
192
<p className="text-xs text-gray-500 dark:text-gray-400">
193
Sign in with AT. Your password is never shared.
194
</p>
195
+
{/* <input
196
type="text"
197
placeholder="handle.bsky.social"
198
value={handle}
199
onChange={(e) => setHandle(e.target.value)}
200
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
201
+
/> */}
202
+
<div className="flex flex-col gap-3">
203
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
204
+
<input
205
+
type="text"
206
+
placeholder=" "
207
+
value={handle}
208
+
onChange={(e) => setHandle(e.target.value)}
209
+
/>
210
+
<label>AT Handle</label>
211
+
</div>
212
+
<button
213
+
type="submit"
214
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
215
+
>
216
+
Log in
217
+
</button>
218
+
</div>
219
</form>
220
);
221
};
···
248
<p className="text-xs text-red-500 dark:text-red-400">
249
Warning: Less secure. Use an App Password.
250
</p>
251
+
{/* <input
252
type="text"
253
placeholder="handle.bsky.social"
254
value={user}
···
270
value={serviceURL}
271
onChange={(e) => setServiceURL(e.target.value)}
272
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
273
+
/> */}
274
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
275
+
<input
276
+
type="text"
277
+
placeholder=" "
278
+
value={user}
279
+
onChange={(e) => setUser(e.target.value)}
280
+
/>
281
+
<label>AT Handle</label>
282
+
</div>
283
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
284
+
<input
285
+
type="text"
286
+
placeholder=" "
287
+
value={password}
288
+
onChange={(e) => setPassword(e.target.value)}
289
+
/>
290
+
<label>App Password</label>
291
+
</div>
292
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
293
+
<input
294
+
type="text"
295
+
placeholder=" "
296
+
value={serviceURL}
297
+
onChange={(e) => setServiceURL(e.target.value)}
298
+
/>
299
+
<label>PDS</label>
300
+
</div>
301
{error && <p className="text-xs text-red-500">{error}</p>}
302
<button
303
type="submit"
···
317
agent: Agent | null;
318
large?: boolean;
319
}) => {
320
+
const did = ((agent as AtpAgent)?.session?.did ??
321
+
(agent as AtpAgent)?.assertDid ??
322
+
agent?.did) as string | undefined;
323
+
const { data: identity } = useQueryIdentity(did);
324
+
const { data: profiledata } = useQueryProfile(
325
+
`at://${did}/app.bsky.actor.profile/self`
326
+
);
327
+
const profile = profiledata?.value;
328
329
+
const [imgcdn] = useAtom(imgCDNAtom)
330
331
+
function getAvatarUrl(p: typeof profile) {
332
+
const link = p?.avatar?.ref?.["$link"];
333
+
if (!link || !did) return null;
334
+
return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`;
335
+
}
336
+
337
+
if (!profiledata) {
338
return (
339
// Skeleton loader
340
<div
···
360
className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`}
361
>
362
<img
363
+
src={getAvatarUrl(profile) ?? undefined}
364
alt="avatar"
365
className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
366
/>
···
373
<div
374
className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`}
375
>
376
+
@{identity?.handle}
377
</div>
378
</div>
379
</div>
+6
src/components/Star.tsx
+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
+
}
+224
-77
src/components/UniversalPostRenderer.tsx
+224
-77
src/components/UniversalPostRenderer.tsx
···
1
import { useNavigate } from "@tanstack/react-router";
2
import DOMPurify from "dompurify";
3
import { useAtom } from "jotai";
4
import * as React from "react";
5
import { type SVGProps } from "react";
6
7
-
import { likedPostsAtom } from "~/utils/atoms";
8
import { useHydratedEmbed } from "~/utils/useHydrated";
9
import {
10
useQueryConstellation,
···
36
nopics?: boolean;
37
lightboxCallback?: (d: LightboxProps) => void;
38
maxReplies?: number;
39
}
40
41
// export async function cachedGetRecord({
···
146
nopics,
147
lightboxCallback,
148
maxReplies,
149
}: UniversalPostRendererATURILoaderProps) {
150
// /*mass comment*/ console.log("atUri", atUri);
151
//const { get, set } = usePersistentStore();
152
//const [record, setRecord] = React.useState<any>(null);
···
398
// path: ".reply.parent.uri",
399
// });
400
401
const infinitequeryresults = useInfiniteQuery({
402
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
403
{
404
method: "/links",
405
target: atUri,
406
collection: "app.bsky.feed.post",
407
path: ".reply.parent.uri",
408
}
409
),
410
-
enabled: !!atUri && !!maxReplies,
411
});
412
413
const {
···
419
420
// auto-fetch all pages
421
useEffect(() => {
422
-
if (!maxReplies) return;
423
if (
424
infinitequeryresults.hasNextPage &&
425
!infinitequeryresults.isFetchingNextPage
···
427
console.log("Fetching the next page...");
428
infinitequeryresults.fetchNextPage();
429
}
430
-
}, [infinitequeryresults]);
431
432
const replyAturis = repliesData
433
? repliesData.pages.flatMap((page) =>
···
443
//const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined);
444
445
const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => {
446
-
if (!replyAturis || replyAturis.length === 0 || !maxReplies)
447
return {
448
oldestOpsReply: undefined,
449
oldestOpsReplyElseNewestNonOpsReply: undefined,
···
504
? true
505
: maxReplies && !oldestOpsReplyElseNewestNonOpsReply
506
? false
507
-
: bottomReplyLine
508
}
509
topReplyLine={topReplyLine}
510
//bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder}
···
524
nopics={nopics}
525
lightboxCallback={lightboxCallback}
526
maxReplies={maxReplies}
527
/>
528
-
{oldestOpsReplyElseNewestNonOpsReply && (
529
<>
530
{/* <span>hello {maxReplies}</span> */}
531
<UniversalPostRendererATURILoader
532
//detailed={detailed}
533
atUri={oldestOpsReplyElseNewestNonOpsReply}
534
bottomReplyLine={(maxReplies ?? 0) > 0}
535
-
topReplyLine={(maxReplies ?? 0) > 1}
536
bottomBorder={bottomBorder}
537
feedviewpost={feedviewpost}
538
repostedby={repostedby}
···
545
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
546
}
547
/>
548
-
{maxReplies && maxReplies - 1 === 0 && (
549
-
<MoreReplies atUri={oldestOpsReplyElseNewestNonOpsReply} />
550
-
)}
551
</>
552
)}
553
</>
···
588
);
589
}
590
591
-
function getAvatarUrl(opProfile: any, did: string) {
592
const link = opProfile?.value?.avatar?.ref?.["$link"];
593
if (!link) return null;
594
-
return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`;
595
}
596
597
export function UniversalPostRendererRawRecordShim({
···
614
nopics,
615
lightboxCallback,
616
maxReplies,
617
}: {
618
postRecord: any;
619
profileRecord: any;
···
634
nopics?: boolean;
635
lightboxCallback?: (d: LightboxProps) => void;
636
maxReplies?: number;
637
}) {
638
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
639
const navigate = useNavigate();
···
710
error: embedError,
711
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
712
713
const parsedaturi = new AtUri(aturi); //parseAtUri(aturi);
714
715
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
716
() => ({
717
$type: "app.bsky.feed.defs#postView",
718
uri: aturi,
719
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
-
},
729
record: postRecord?.value || {},
730
embed: hydratedEmbed ?? undefined,
731
replyCount: repliesCount ?? 0,
···
742
postRecord?.cid,
743
postRecord?.value,
744
postRecord?.labels,
745
-
resolved?.did,
746
-
resolved?.handle,
747
-
profileRecord,
748
hydratedEmbed,
749
repliesCount,
750
repostsCount,
···
821
}
822
}}
823
post={fakepost}
824
salt={aturi}
825
bottomReplyLine={bottomReplyLine}
826
topReplyLine={topReplyLine}
···
834
nopics={nopics}
835
lightboxCallback={lightboxCallback}
836
maxReplies={maxReplies}
837
/>
838
</>
839
);
···
872
{...props}
873
>
874
<path
875
-
fill="oklch(0.704 0.05 28)"
876
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
></path>
878
</svg>
···
889
{...props}
890
>
891
<path
892
-
fill="oklch(0.704 0.05 28)"
893
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
894
></path>
895
</svg>
···
940
{...props}
941
>
942
<path
943
-
fill="oklch(0.704 0.05 28)"
944
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
></path>
946
</svg>
···
957
{...props}
958
>
959
<path
960
-
fill="oklch(0.704 0.05 28)"
961
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
></path>
963
</svg>
···
974
{...props}
975
>
976
<path
977
-
fill="oklch(0.704 0.05 28)"
978
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
></path>
980
</svg>
···
991
{...props}
992
>
993
<path
994
-
fill="oklch(0.704 0.05 28)"
995
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
></path>
997
</svg>
···
1025
{...props}
1026
>
1027
<path
1028
-
fill="oklch(0.704 0.05 28)"
1029
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
1030
></path>
1031
</svg>
···
1079
{...props}
1080
>
1081
<path
1082
-
fill="oklch(0.704 0.05 28)"
1083
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
1084
></path>
1085
</svg>
···
1096
{...props}
1097
>
1098
<path
1099
-
fill="oklch(0.704 0.05 28)"
1100
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
></path>
1102
</svg>
···
1124
//import Masonry from "@mui/lab/Masonry";
1125
import {
1126
type $Typed,
1127
AppBskyEmbedDefs,
1128
AppBskyEmbedExternal,
1129
AppBskyEmbedImages,
···
1153
1154
import defaultpfp from "~/../public/favicon.png";
1155
import { useAuth } from "~/providers/UnifiedAuthProvider";
1156
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
1157
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1158
// import type {
···
1261
1262
function UniversalPostRenderer({
1263
post,
1264
//setMainItem,
1265
//isMainItem,
1266
onPostClick,
···
1285
maxReplies,
1286
}: {
1287
post: PostView;
1288
// optional for now because i havent ported every use to this yet
1289
// setMainItem?: React.Dispatch<
1290
// React.SetStateAction<AppBskyFeedDefs.FeedViewPost>
···
1318
const [hasLiked, setHasLiked] = useState<boolean>(
1319
post.uri in likedPosts || post.viewer?.like ? true : false
1320
);
1321
const { agent } = useAuth();
1322
const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like);
1323
const [retweetUri, setRetweetUri] = useState<string | undefined>(
···
1414
paddingLeft: isQuote ? 12 : 16,
1415
paddingRight: isQuote ? 12 : 16,
1416
//paddingTop: 16,
1417
-
paddingTop: isRepost ? 10 : isQuote ? 12 : 16,
1418
//paddingBottom: bottomReplyLine ? 0 : 16,
1419
paddingBottom: 0,
1420
fontFamily: "system-ui, sans-serif",
···
1455
//left: 16 + (42 / 2),
1456
width: 2,
1457
//height: "100%",
1458
-
height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6,
1459
// background: theme.textSecondary,
1460
//opacity: 0.5,
1461
// no flex here
···
1463
className="bg-gray-500 dark:bg-gray-400"
1464
/>
1465
)}
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>
1497
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
1498
<div
1499
style={{
···
1507
}}
1508
>
1509
{/* dummy for later use */}
1510
-
<div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} />
1511
{/* reply line !!!! bottomReplyLine */}
1512
{bottomReplyLine && (
1513
<div
···
1740
}}
1741
className="text-gray-500 dark:text-gray-400"
1742
>
1743
-
<span style={btnstyle}>
1744
-
<MdiCommentOutline />
1745
-
{post.replyCount}
1746
-
</span>
1747
<HitSlopButton
1748
onClick={() => {
1749
-
repostOrUnrepostPost();
1750
}}
1751
style={{
1752
...btnstyle,
1753
-
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1754
}}
1755
>
1756
-
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1757
-
{(post.repostCount || 0) + (hasRetweeted ? 1 : 0)}
1758
</HitSlopButton>
1759
<HitSlopButton
1760
onClick={() => {
1761
likeOrUnlikePost();
···
2508
return { start, end, feature: f.features[0] };
2509
});
2510
}
2511
-
function renderTextWithFacets({
2512
text,
2513
facets,
2514
navigate,
···
1
import { useNavigate } from "@tanstack/react-router";
2
import DOMPurify from "dompurify";
3
import { useAtom } from "jotai";
4
+
import { DropdownMenu } from "radix-ui";
5
+
import { HoverCard } from "radix-ui";
6
import * as React from "react";
7
import { type SVGProps } from "react";
8
9
+
import {
10
+
composerAtom,
11
+
constellationURLAtom,
12
+
imgCDNAtom,
13
+
likedPostsAtom,
14
+
} from "~/utils/atoms";
15
import { useHydratedEmbed } from "~/utils/useHydrated";
16
import {
17
useQueryConstellation,
···
43
nopics?: boolean;
44
lightboxCallback?: (d: LightboxProps) => void;
45
maxReplies?: number;
46
+
isQuote?: boolean;
47
}
48
49
// export async function cachedGetRecord({
···
154
nopics,
155
lightboxCallback,
156
maxReplies,
157
+
isQuote,
158
}: UniversalPostRendererATURILoaderProps) {
159
+
// todo remove this once tree rendering is implemented, use a prop like isTree
160
+
const TEMPLINEAR = true;
161
// /*mass comment*/ console.log("atUri", atUri);
162
//const { get, set } = usePersistentStore();
163
//const [record, setRecord] = React.useState<any>(null);
···
409
// path: ".reply.parent.uri",
410
// });
411
412
+
const [constellationurl] = useAtom(constellationURLAtom);
413
+
414
const infinitequeryresults = useInfiniteQuery({
415
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
416
{
417
+
constellation: constellationurl,
418
method: "/links",
419
target: atUri,
420
collection: "app.bsky.feed.post",
421
path: ".reply.parent.uri",
422
}
423
),
424
+
enabled: !!atUri && !!maxReplies && !isQuote,
425
});
426
427
const {
···
433
434
// auto-fetch all pages
435
useEffect(() => {
436
+
if (!maxReplies || isQuote || TEMPLINEAR) return;
437
if (
438
infinitequeryresults.hasNextPage &&
439
!infinitequeryresults.isFetchingNextPage
···
441
console.log("Fetching the next page...");
442
infinitequeryresults.fetchNextPage();
443
}
444
+
}, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]);
445
446
const replyAturis = repliesData
447
? repliesData.pages.flatMap((page) =>
···
457
//const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined);
458
459
const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => {
460
+
if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies)
461
return {
462
oldestOpsReply: undefined,
463
oldestOpsReplyElseNewestNonOpsReply: undefined,
···
518
? true
519
: maxReplies && !oldestOpsReplyElseNewestNonOpsReply
520
? false
521
+
: (maxReplies === 0 && (!replies || (!!replies && replies === 0))) ? false : bottomReplyLine
522
}
523
topReplyLine={topReplyLine}
524
//bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder}
···
538
nopics={nopics}
539
lightboxCallback={lightboxCallback}
540
maxReplies={maxReplies}
541
+
isQuote={isQuote}
542
/>
543
+
<>
544
+
{(maxReplies && maxReplies === 0 && replies && replies > 0) ? (
545
+
<>
546
+
{/* <div>hello</div> */}
547
+
<MoreReplies atUri={atUri} />
548
+
</>
549
+
) : (<></>)}
550
+
</>
551
+
{!isQuote && oldestOpsReplyElseNewestNonOpsReply && (
552
<>
553
{/* <span>hello {maxReplies}</span> */}
554
<UniversalPostRendererATURILoader
555
//detailed={detailed}
556
atUri={oldestOpsReplyElseNewestNonOpsReply}
557
bottomReplyLine={(maxReplies ?? 0) > 0}
558
+
topReplyLine={
559
+
(!!(maxReplies && maxReplies - 1 === 0) &&
560
+
!!(replies && replies > 0)) ||
561
+
!!((maxReplies ?? 0) > 1)
562
+
}
563
bottomBorder={bottomBorder}
564
feedviewpost={feedviewpost}
565
repostedby={repostedby}
···
572
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
573
}
574
/>
575
</>
576
)}
577
</>
···
612
);
613
}
614
615
+
function getAvatarUrl(opProfile: any, did: string, cdn: string) {
616
const link = opProfile?.value?.avatar?.ref?.["$link"];
617
if (!link) return null;
618
+
return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`;
619
}
620
621
export function UniversalPostRendererRawRecordShim({
···
638
nopics,
639
lightboxCallback,
640
maxReplies,
641
+
isQuote,
642
}: {
643
postRecord: any;
644
profileRecord: any;
···
659
nopics?: boolean;
660
lightboxCallback?: (d: LightboxProps) => void;
661
maxReplies?: number;
662
+
isQuote?: boolean;
663
}) {
664
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
665
const navigate = useNavigate();
···
736
error: embedError,
737
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
738
739
+
const [imgcdn] = useAtom(imgCDNAtom);
740
+
741
const parsedaturi = new AtUri(aturi); //parseAtUri(aturi);
742
743
+
const fakeprofileviewbasic = React.useMemo<AppBskyActorDefs.ProfileViewBasic>(
744
+
() => ({
745
+
did: resolved?.did || "",
746
+
handle: resolved?.handle || "",
747
+
displayName: profileRecord?.value?.displayName || "",
748
+
avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "",
749
+
viewer: undefined,
750
+
labels: profileRecord?.labels || undefined,
751
+
verification: undefined,
752
+
}),
753
+
[imgcdn, profileRecord, resolved?.did, resolved?.handle]
754
+
);
755
+
756
+
const fakeprofileviewdetailed =
757
+
React.useMemo<AppBskyActorDefs.ProfileViewDetailed>(
758
+
() => ({
759
+
...fakeprofileviewbasic,
760
+
$type: "app.bsky.actor.defs#profileViewDetailed",
761
+
description: profileRecord?.value?.description || undefined,
762
+
}),
763
+
[fakeprofileviewbasic, profileRecord?.value?.description]
764
+
);
765
+
766
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
767
() => ({
768
$type: "app.bsky.feed.defs#postView",
769
uri: aturi,
770
cid: postRecord?.cid || "",
771
+
author: fakeprofileviewbasic,
772
record: postRecord?.value || {},
773
embed: hydratedEmbed ?? undefined,
774
replyCount: repliesCount ?? 0,
···
785
postRecord?.cid,
786
postRecord?.value,
787
postRecord?.labels,
788
+
fakeprofileviewbasic,
789
hydratedEmbed,
790
repliesCount,
791
repostsCount,
···
862
}
863
}}
864
post={fakepost}
865
+
uprrrsauthor={fakeprofileviewdetailed}
866
salt={aturi}
867
bottomReplyLine={bottomReplyLine}
868
topReplyLine={topReplyLine}
···
876
nopics={nopics}
877
lightboxCallback={lightboxCallback}
878
maxReplies={maxReplies}
879
+
isQuote={isQuote}
880
/>
881
</>
882
);
···
915
{...props}
916
>
917
<path
918
+
fill="var(--color-gray-400)"
919
d="M9 22a1 1 0 0 1-1-1v-3H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6.1l-3.7 3.71c-.2.19-.45.29-.7.29zm1-6v3.08L13.08 16H20V4H4v12z"
920
></path>
921
</svg>
···
932
{...props}
933
>
934
<path
935
+
fill="var(--color-gray-400)"
936
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
937
></path>
938
</svg>
···
983
{...props}
984
>
985
<path
986
+
fill="var(--color-gray-400)"
987
d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3"
988
></path>
989
</svg>
···
1000
{...props}
1001
>
1002
<path
1003
+
fill="var(--color-gray-400)"
1004
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3a3 3 0 0 0-3-3a3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"
1005
></path>
1006
</svg>
···
1017
{...props}
1018
>
1019
<path
1020
+
fill="var(--color-gray-400)"
1021
d="M16 12a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"
1022
></path>
1023
</svg>
···
1034
{...props}
1035
>
1036
<path
1037
+
fill="var(--color-gray-400)"
1038
d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"
1039
></path>
1040
</svg>
···
1068
{...props}
1069
>
1070
<path
1071
+
fill="var(--color-gray-400)"
1072
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
1073
></path>
1074
</svg>
···
1122
{...props}
1123
>
1124
<path
1125
+
fill="var(--color-gray-400)"
1126
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
1127
></path>
1128
</svg>
···
1139
{...props}
1140
>
1141
<path
1142
+
fill="var(--color-gray-400)"
1143
d="M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z"
1144
></path>
1145
</svg>
···
1167
//import Masonry from "@mui/lab/Masonry";
1168
import {
1169
type $Typed,
1170
+
AppBskyActorDefs,
1171
AppBskyEmbedDefs,
1172
AppBskyEmbedExternal,
1173
AppBskyEmbedImages,
···
1197
1198
import defaultpfp from "~/../public/favicon.png";
1199
import { useAuth } from "~/providers/UnifiedAuthProvider";
1200
+
import { FollowButton, Mutual } from "~/routes/profile.$did";
1201
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
1202
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1203
// import type {
···
1306
1307
function UniversalPostRenderer({
1308
post,
1309
+
uprrrsauthor,
1310
//setMainItem,
1311
//isMainItem,
1312
onPostClick,
···
1331
maxReplies,
1332
}: {
1333
post: PostView;
1334
+
uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed;
1335
// optional for now because i havent ported every use to this yet
1336
// setMainItem?: React.Dispatch<
1337
// React.SetStateAction<AppBskyFeedDefs.FeedViewPost>
···
1365
const [hasLiked, setHasLiked] = useState<boolean>(
1366
post.uri in likedPosts || post.viewer?.like ? true : false
1367
);
1368
+
const [, setComposerPost] = useAtom(composerAtom);
1369
const { agent } = useAuth();
1370
const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like);
1371
const [retweetUri, setRetweetUri] = useState<string | undefined>(
···
1462
paddingLeft: isQuote ? 12 : 16,
1463
paddingRight: isQuote ? 12 : 16,
1464
//paddingTop: 16,
1465
+
paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16,
1466
//paddingBottom: bottomReplyLine ? 0 : 16,
1467
paddingBottom: 0,
1468
fontFamily: "system-ui, sans-serif",
···
1503
//left: 16 + (42 / 2),
1504
width: 2,
1505
//height: "100%",
1506
+
height: isRepost
1507
+
? "calc(16px + 1rem - 6px)"
1508
+
: topReplyLine
1509
+
? 8 - 6
1510
+
: 16 - 6,
1511
// background: theme.textSecondary,
1512
//opacity: 0.5,
1513
// no flex here
···
1515
className="bg-gray-500 dark:bg-gray-400"
1516
/>
1517
)}
1518
+
<HoverCard.Root>
1519
+
<HoverCard.Trigger asChild>
1520
+
<div
1521
+
className={`absolute`}
1522
+
style={{
1523
+
top: isRepost
1524
+
? "calc(16px + 1rem)"
1525
+
: isQuote
1526
+
? 12
1527
+
: topReplyLine
1528
+
? 8
1529
+
: 16,
1530
+
left: isQuote ? 12 : 16,
1531
+
}}
1532
+
onClick={onProfileClick}
1533
+
>
1534
+
<img
1535
+
src={post.author.avatar || defaultpfp}
1536
+
alt="avatar"
1537
+
className={`rounded-full object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600`}
1538
+
style={{
1539
+
width: isQuote ? 16 : 42,
1540
+
height: isQuote ? 16 : 42,
1541
+
}}
1542
+
/>
1543
+
</div>
1544
+
</HoverCard.Trigger>
1545
+
<HoverCard.Portal>
1546
+
<HoverCard.Content
1547
+
className="rounded-md p-4 w-72 bg-gray-50 dark:bg-gray-900 shadow-lg border border-gray-300 dark:border-gray-800 animate-slide-fade z-50"
1548
+
side={"bottom"}
1549
+
sideOffset={5}
1550
+
onClick={onProfileClick}
1551
+
>
1552
+
<div className="flex flex-col gap-2">
1553
+
<div className="flex flex-row">
1554
+
<img
1555
+
src={post.author.avatar || defaultpfp}
1556
+
alt="avatar"
1557
+
className="rounded-full w-[58px] h-[58px] object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600"
1558
+
/>
1559
+
<div className=" flex-1 flex flex-row align-middle justify-end">
1560
+
<FollowButton targetdidorhandle={post.author.did} />
1561
+
</div>
1562
+
</div>
1563
+
<div className="flex flex-col gap-3">
1564
+
<div>
1565
+
<div className="text-gray-900 dark:text-gray-100 font-medium text-md">
1566
+
{post.author.displayName || post.author.handle}{" "}
1567
+
</div>
1568
+
<div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1">
1569
+
<Mutual targetdidorhandle={post.author.did} />@{post.author.handle}{" "}
1570
+
</div>
1571
+
</div>
1572
+
{uprrrsauthor?.description && (
1573
+
<div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3">
1574
+
{uprrrsauthor.description}
1575
+
</div>
1576
+
)}
1577
+
{/* <div className="flex gap-4">
1578
+
<div className="flex gap-1">
1579
+
<div className="font-medium text-gray-900 dark:text-gray-100">
1580
+
0
1581
+
</div>
1582
+
<div className="text-gray-500 dark:text-gray-400">
1583
+
Following
1584
+
</div>
1585
+
</div>
1586
+
<div className="flex gap-1">
1587
+
<div className="font-medium text-gray-900 dark:text-gray-100">
1588
+
2,900
1589
+
</div>
1590
+
<div className="text-gray-500 dark:text-gray-400">
1591
+
Followers
1592
+
</div>
1593
+
</div>
1594
+
</div> */}
1595
+
</div>
1596
+
</div>
1597
+
1598
+
{/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */}
1599
+
</HoverCard.Content>
1600
+
</HoverCard.Portal>
1601
+
</HoverCard.Root>
1602
+
1603
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
1604
<div
1605
style={{
···
1613
}}
1614
>
1615
{/* dummy for later use */}
1616
+
<div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} />
1617
{/* reply line !!!! bottomReplyLine */}
1618
{bottomReplyLine && (
1619
<div
···
1846
}}
1847
className="text-gray-500 dark:text-gray-400"
1848
>
1849
<HitSlopButton
1850
onClick={() => {
1851
+
setComposerPost({ kind: "reply", parent: post.uri });
1852
}}
1853
style={{
1854
...btnstyle,
1855
}}
1856
>
1857
+
<MdiCommentOutline />
1858
+
{post.replyCount}
1859
</HitSlopButton>
1860
+
<DropdownMenu.Root modal={false}>
1861
+
<DropdownMenu.Trigger asChild>
1862
+
<div
1863
+
style={{
1864
+
...btnstyle,
1865
+
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1866
+
}}
1867
+
aria-label="Repost or quote post"
1868
+
>
1869
+
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1870
+
{post.repostCount ?? 0}
1871
+
</div>
1872
+
</DropdownMenu.Trigger>
1873
+
1874
+
<DropdownMenu.Portal>
1875
+
<DropdownMenu.Content
1876
+
align="start"
1877
+
sideOffset={5}
1878
+
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-32 z-50 overflow-hidden"
1879
+
>
1880
+
<DropdownMenu.Item
1881
+
onSelect={repostOrUnrepostPost}
1882
+
className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
1883
+
>
1884
+
<MdiRepeat
1885
+
className={hasRetweeted ? "text-green-400" : ""}
1886
+
/>
1887
+
<span>{hasRetweeted ? "Undo Repost" : "Repost"}</span>
1888
+
</DropdownMenu.Item>
1889
+
1890
+
<DropdownMenu.Item
1891
+
onSelect={() => {
1892
+
setComposerPost({
1893
+
kind: "quote",
1894
+
subject: post.uri,
1895
+
});
1896
+
}}
1897
+
className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
1898
+
>
1899
+
{/* You might want a specific quote icon here */}
1900
+
<MdiCommentOutline />
1901
+
<span>Quote</span>
1902
+
</DropdownMenu.Item>
1903
+
</DropdownMenu.Content>
1904
+
</DropdownMenu.Portal>
1905
+
</DropdownMenu.Root>
1906
<HitSlopButton
1907
onClick={() => {
1908
likeOrUnlikePost();
···
2655
return { start, end, feature: f.features[0] };
2656
});
2657
}
2658
+
export function renderTextWithFacets({
2659
text,
2660
facets,
2661
navigate,
+2
src/main.tsx
+2
src/main.tsx
+42
-111
src/routes/__root.tsx
+42
-111
src/routes/__root.tsx
···
12
useNavigate,
13
} from "@tanstack/react-router";
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
-
import { useState } from "react";
16
import * as React from "react";
17
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
18
19
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
20
import Login from "~/components/Login";
21
import { NotFound } from "~/components/NotFound";
22
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
23
import { seo } from "~/utils/seo";
24
25
export const Route = createRootRouteWithContext<{
···
85
}
86
87
function RootDocument({ children }: { children: React.ReactNode }) {
88
const location = useLocation();
89
const navigate = useNavigate();
90
const { agent } = useAuth();
···
117
? "profile"
118
: "home";
119
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
-
}
152
153
return (
154
<>
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
-
)}
198
199
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
200
<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
<div className="flex items-center gap-3 mb-4">
202
-
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
203
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
204
Red Dwarf{" "}
205
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
223
/>
224
225
<MaterialNavItem
226
InactiveIcon={
227
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
228
}
···
249
})
250
}
251
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
/>
265
<MaterialNavItem
266
InactiveIcon={
···
300
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
301
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
302
//active={true}
303
-
onClickCallbback={() => setPostOpen(true)}
304
text="Post"
305
/>
306
</div>
···
438
439
<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
<div className="flex items-center gap-3 mb-4">
441
-
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
442
</div>
443
<MaterialNavItem
444
small
···
458
459
<MaterialNavItem
460
small
461
InactiveIcon={
462
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
463
}
···
488
/>
489
<MaterialNavItem
490
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
InactiveIcon={
505
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
506
}
···
540
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
541
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
542
//active={true}
543
-
onClickCallbback={() => setPostOpen(true)}
544
text="Post"
545
/>
546
</div>
···
550
<button
551
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
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
553
-
onClick={() => setPostOpen(true)}
554
type="button"
555
aria-label="Create Post"
556
>
···
567
</main>
568
569
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
570
<Login />
571
572
<div className="flex-1"></div>
573
<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.
577
</p>
578
</aside>
579
</div>
···
622
//params: { did: agent.assertDid },
623
})
624
}
625
-
text="Search"
626
/>
627
{/* <Link
628
to="/search"
···
749
) : (
750
<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
<div className="flex items-center gap-2">
752
-
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" />
753
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
754
Red Dwarf{" "}
755
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
12
useNavigate,
13
} from "@tanstack/react-router";
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
+
import { useAtom } from "jotai";
16
import * as React from "react";
17
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
18
19
+
import { Composer } from "~/components/Composer";
20
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
21
+
import { Import } from "~/components/Import";
22
import Login from "~/components/Login";
23
import { NotFound } from "~/components/NotFound";
24
+
import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
25
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
26
+
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
27
import { seo } from "~/utils/seo";
28
29
export const Route = createRootRouteWithContext<{
···
89
}
90
91
function RootDocument({ children }: { children: React.ReactNode }) {
92
+
useAtomCssVar(hueAtom, "--tw-gray-hue");
93
const location = useLocation();
94
const navigate = useNavigate();
95
const { agent } = useAuth();
···
122
? "profile"
123
: "home";
124
125
+
const [, setComposerPost] = useAtom(composerAtom);
126
127
return (
128
<>
129
+
<Composer />
130
131
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
132
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start">
133
<div className="flex items-center gap-3 mb-4">
134
+
<FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
135
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
136
Red Dwarf{" "}
137
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
155
/>
156
157
<MaterialNavItem
158
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
159
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
160
+
active={locationEnum === "search"}
161
+
onClickCallbback={() =>
162
+
navigate({
163
+
to: "/search",
164
+
//params: { did: agent.assertDid },
165
+
})
166
+
}
167
+
text="Explore"
168
+
/>
169
+
<MaterialNavItem
170
InactiveIcon={
171
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
172
}
···
193
})
194
}
195
text="Feeds"
196
/>
197
<MaterialNavItem
198
InactiveIcon={
···
232
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
233
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
234
//active={true}
235
+
onClickCallbback={() => setComposerPost({ kind: 'root' })}
236
text="Post"
237
/>
238
</div>
···
370
371
<nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
372
<div className="flex items-center gap-3 mb-4">
373
+
<FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
374
</div>
375
<MaterialNavItem
376
small
···
390
391
<MaterialNavItem
392
small
393
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
394
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
395
+
active={locationEnum === "search"}
396
+
onClickCallbback={() =>
397
+
navigate({
398
+
to: "/search",
399
+
//params: { did: agent.assertDid },
400
+
})
401
+
}
402
+
text="Explore"
403
+
/>
404
+
<MaterialNavItem
405
+
small
406
InactiveIcon={
407
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
408
}
···
433
/>
434
<MaterialNavItem
435
small
436
InactiveIcon={
437
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
438
}
···
472
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
473
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
474
//active={true}
475
+
onClickCallbback={() => setComposerPost({ kind: 'root' })}
476
text="Post"
477
/>
478
</div>
···
482
<button
483
className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
484
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
485
+
onClick={() => setComposerPost({ kind: 'root' })}
486
type="button"
487
aria-label="Create Post"
488
>
···
499
</main>
500
501
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
502
+
<div className="px-4 pt-4"><Import /></div>
503
<Login />
504
505
<div className="flex-1"></div>
506
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
507
+
Red Dwarf is a Bluesky client that does not rely on any Bluesky API App Servers. Instead, it uses Microcosm to fetch records directly from each users' PDS (via Slingshot) and connect them using backlinks (via Constellation)
508
</p>
509
</aside>
510
</div>
···
553
//params: { did: agent.assertDid },
554
})
555
}
556
+
text="Explore"
557
/>
558
{/* <Link
559
to="/search"
···
680
) : (
681
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
682
<div className="flex items-center gap-2">
683
+
<FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
684
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
685
Red Dwarf{" "}
686
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
+1
src/routes/index.tsx
+1
src/routes/index.tsx
+7
-3
src/routes/notifications.tsx
+7
-3
src/routes/notifications.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import React, { useEffect, useRef,useState } from "react";
3
4
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
6
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
···
70
}
71
}
72
73
useEffect(() => {
74
if (!did) return;
75
setLoading(true);
76
setError(null);
77
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`,
81
];
82
let ignore = false;
83
Promise.all(
···
1
import { createFileRoute } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
import React, { useEffect, useRef,useState } from "react";
4
5
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
+
import { constellationURLAtom } from "~/utils/atoms";
7
8
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
9
···
72
}
73
}
74
75
+
const [constellationURL] = useAtom(constellationURLAtom)
76
+
77
useEffect(() => {
78
if (!did) return;
79
setLoading(true);
80
setError(null);
81
const urls = [
82
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`,
83
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
84
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
85
];
86
let ignore = false;
87
Promise.all(
+194
-56
src/routes/profile.$did/index.tsx
+194
-56
src/routes/profile.$did/index.tsx
···
1
import { useQueryClient } from "@tanstack/react-query";
2
-
import { createFileRoute } from "@tanstack/react-router";
3
-
import React from "react";
4
5
import { Header } from "~/components/Header";
6
-
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
-
import { toggleFollow, useGetFollowState } from "~/utils/followState";
9
import {
10
-
useInfiniteQueryAuthorFeed,
11
useQueryIdentity,
12
useQueryProfile,
13
} from "~/utils/useQuery";
···
19
function ProfileComponent() {
20
// booo bad this is not always the did it might be a handle, use identity.did instead
21
const { did } = Route.useParams();
22
const queryClient = useQueryClient();
23
-
const { agent } = useAuth();
24
const {
25
data: identity,
26
isLoading: isIdentityLoading,
27
error: identityError,
28
} = useQueryIdentity(did);
29
30
-
const followRecords = useGetFollowState({
31
-
target: identity?.did || did,
32
-
user: agent?.did,
33
-
});
34
-
35
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
36
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
37
-
const pdsUrl = identity?.pds;
38
39
const profileUri = resolvedDid
40
? `at://${resolvedDid}/app.bsky.actor.profile/self`
···
42
const { data: profileRecord } = useQueryProfile(profileUri);
43
const profile = profileRecord?.value;
44
45
const {
46
data: postsData,
47
fetchNextPage,
48
hasNextPage,
49
isFetchingNextPage,
50
isLoading: arePostsLoading,
51
-
} = useInfiniteQueryAuthorFeed(resolvedDid, pdsUrl);
52
53
React.useEffect(() => {
54
if (postsData) {
55
postsData.pages.forEach((page) => {
56
-
page.records.forEach((record) => {
57
if (!queryClient.getQueryData(["post", record.uri])) {
58
queryClient.setQueryData(["post", record.uri], record);
59
}
···
63
}, [postsData, queryClient]);
64
65
const posts = React.useMemo(
66
-
() => postsData?.pages.flatMap((page) => page.records) ?? [],
67
[postsData]
68
);
69
70
function getAvatarUrl(p: typeof profile) {
71
const link = p?.avatar?.ref?.["$link"];
72
if (!link || !resolvedDid) return null;
73
-
return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
74
}
75
function getBannerUrl(p: typeof profile) {
76
const link = p?.banner?.ref?.["$link"];
77
if (!link || !resolvedDid) return null;
78
-
return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`;
79
}
80
81
const displayName =
···
162
also delay the backfill to be on demand because it would be pretty intense
163
also save it persistently
164
*/}
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]">
203
... {/* todo: icon */}
204
</button>
···
207
{/* Info Card */}
208
<div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100">
209
<div className="font-bold text-2xl">{displayName}</div>
210
-
<div className="text-gray-500 dark:text-gray-400 text-base mb-3">
211
{handle}
212
</div>
213
{description && (
214
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
215
-
{description}
216
</div>
217
)}
218
</div>
···
255
</>
256
);
257
}
···
1
+
import { RichText } from "@atproto/api";
2
import { useQueryClient } from "@tanstack/react-query";
3
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
import React, { type ReactNode, useEffect, useState } from "react";
6
7
import { Header } from "~/components/Header";
8
+
import {
9
+
renderTextWithFacets,
10
+
UniversalPostRendererATURILoader,
11
+
} from "~/components/UniversalPostRenderer";
12
import { useAuth } from "~/providers/UnifiedAuthProvider";
13
+
import { aturiListServiceAtom, imgCDNAtom } from "~/utils/atoms";
14
import {
15
+
toggleFollow,
16
+
useGetFollowState,
17
+
useGetOneToOneState,
18
+
} from "~/utils/followState";
19
+
import {
20
+
useInfiniteQueryAturiList,
21
useQueryIdentity,
22
useQueryProfile,
23
} from "~/utils/useQuery";
···
29
function ProfileComponent() {
30
// booo bad this is not always the did it might be a handle, use identity.did instead
31
const { did } = Route.useParams();
32
+
//const navigate = useNavigate();
33
const queryClient = useQueryClient();
34
const {
35
data: identity,
36
isLoading: isIdentityLoading,
37
error: identityError,
38
} = useQueryIdentity(did);
39
40
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
41
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
42
+
//const pdsUrl = identity?.pds;
43
44
const profileUri = resolvedDid
45
? `at://${resolvedDid}/app.bsky.actor.profile/self`
···
47
const { data: profileRecord } = useQueryProfile(profileUri);
48
const profile = profileRecord?.value;
49
50
+
const [aturilistservice] = useAtom(aturiListServiceAtom);
51
+
52
const {
53
data: postsData,
54
fetchNextPage,
55
hasNextPage,
56
isFetchingNextPage,
57
isLoading: arePostsLoading,
58
+
} = useInfiniteQueryAturiList({
59
+
aturilistservice: aturilistservice,
60
+
did: resolvedDid,
61
+
collection: "app.bsky.feed.post",
62
+
reverse: true
63
+
});
64
65
React.useEffect(() => {
66
if (postsData) {
67
postsData.pages.forEach((page) => {
68
+
page.forEach((record) => {
69
if (!queryClient.getQueryData(["post", record.uri])) {
70
queryClient.setQueryData(["post", record.uri], record);
71
}
···
75
}, [postsData, queryClient]);
76
77
const posts = React.useMemo(
78
+
() => postsData?.pages.flatMap((page) => page) ?? [],
79
[postsData]
80
);
81
+
82
+
const [imgcdn] = useAtom(imgCDNAtom);
83
84
function getAvatarUrl(p: typeof profile) {
85
const link = p?.avatar?.ref?.["$link"];
86
if (!link || !resolvedDid) return null;
87
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
88
}
89
function getBannerUrl(p: typeof profile) {
90
const link = p?.banner?.ref?.["$link"];
91
if (!link || !resolvedDid) return null;
92
+
return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`;
93
}
94
95
const displayName =
···
176
also delay the backfill to be on demand because it would be pretty intense
177
also save it persistently
178
*/}
179
+
<FollowButton targetdidorhandle={did} />
180
<button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]">
181
... {/* todo: icon */}
182
</button>
···
185
{/* Info Card */}
186
<div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100">
187
<div className="font-bold text-2xl">{displayName}</div>
188
+
<div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1">
189
+
<Mutual targetdidorhandle={did} />
190
{handle}
191
</div>
192
{description && (
193
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
194
+
{/* {description} */}
195
+
<RichTextRenderer key={did} description={description} />
196
</div>
197
)}
198
</div>
···
235
</>
236
);
237
}
238
+
239
+
export function FollowButton({
240
+
targetdidorhandle,
241
+
}: {
242
+
targetdidorhandle: string;
243
+
}) {
244
+
const { agent } = useAuth();
245
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
246
+
const queryClient = useQueryClient();
247
+
248
+
const followRecords = useGetFollowState({
249
+
target: identity?.did ?? targetdidorhandle,
250
+
user: agent?.did,
251
+
});
252
+
253
+
return (
254
+
<>
255
+
{identity?.did !== agent?.did ? (
256
+
<>
257
+
{!(followRecords?.length && followRecords?.length > 0) ? (
258
+
<button
259
+
onClick={(e) => {
260
+
e.stopPropagation();
261
+
toggleFollow({
262
+
agent: agent || undefined,
263
+
targetDid: identity?.did,
264
+
followRecords: followRecords,
265
+
queryClient: queryClient,
266
+
});
267
+
}}
268
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
269
+
>
270
+
Follow
271
+
</button>
272
+
) : (
273
+
<button
274
+
onClick={(e) => {
275
+
e.stopPropagation();
276
+
toggleFollow({
277
+
agent: agent || undefined,
278
+
targetDid: identity?.did,
279
+
followRecords: followRecords,
280
+
queryClient: queryClient,
281
+
});
282
+
}}
283
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
284
+
>
285
+
Unfollow
286
+
</button>
287
+
)}
288
+
</>
289
+
) : (
290
+
<button className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]">
291
+
Edit Profile
292
+
</button>
293
+
)}
294
+
</>
295
+
);
296
+
}
297
+
298
+
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
299
+
const { agent } = useAuth();
300
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
301
+
302
+
const theyFollowYouRes = useGetOneToOneState(
303
+
agent?.did
304
+
? {
305
+
target: agent?.did,
306
+
user: identity?.did ?? targetdidorhandle,
307
+
collection: "app.bsky.graph.follow",
308
+
path: ".subject",
309
+
}
310
+
: undefined
311
+
);
312
+
313
+
const youFollowThemRes = useGetFollowState({
314
+
target: identity?.did ?? targetdidorhandle,
315
+
user: agent?.did,
316
+
});
317
+
318
+
const theyFollowYou: boolean =
319
+
!!theyFollowYouRes?.length && theyFollowYouRes.length > 0;
320
+
const youFollowThem: boolean =
321
+
!!youFollowThemRes?.length && youFollowThemRes.length > 0;
322
+
323
+
return (
324
+
<>
325
+
{/* if not self */}
326
+
{identity?.did !== agent?.did ? (
327
+
<>
328
+
{theyFollowYou ? (
329
+
<>
330
+
{youFollowThem ? (
331
+
<div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
332
+
mutuals
333
+
</div>
334
+
) : (
335
+
<div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
336
+
follows you
337
+
</div>
338
+
)}
339
+
</>
340
+
) : (
341
+
<></>
342
+
)}
343
+
</>
344
+
) : (
345
+
// lmao can someone be mutuals with themselves ??
346
+
<></>
347
+
)}
348
+
</>
349
+
);
350
+
}
351
+
352
+
export function RichTextRenderer({ description }: { description: string }) {
353
+
const [richDescription, setRichDescription] = useState<string | ReactNode[]>(
354
+
description
355
+
);
356
+
const { agent } = useAuth();
357
+
const navigate = useNavigate();
358
+
359
+
useEffect(() => {
360
+
let mounted = true;
361
+
362
+
// setRichDescription(description);
363
+
364
+
async function processRichText() {
365
+
try {
366
+
if (!agent?.did) return;
367
+
const rt = new RichText({ text: description });
368
+
await rt.detectFacets(agent);
369
+
370
+
if (!mounted) return;
371
+
372
+
if (rt.facets) {
373
+
setRichDescription(
374
+
renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate })
375
+
);
376
+
} else {
377
+
setRichDescription(rt.text);
378
+
}
379
+
} catch (error) {
380
+
console.error("Failed to detect facets:", error);
381
+
if (mounted) {
382
+
setRichDescription(description);
383
+
}
384
+
}
385
+
}
386
+
387
+
processRichText();
388
+
389
+
return () => {
390
+
mounted = false;
391
+
};
392
+
}, [description, agent, navigate]);
393
+
394
+
return <>{richDescription}</>;
395
+
}
+171
-64
src/routes/profile.$did/post.$rkey.tsx
+171
-64
src/routes/profile.$did/post.$rkey.tsx
···
1
import { AtUri } from "@atproto/api";
2
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
import { createFileRoute, Outlet } from "@tanstack/react-router";
4
-
import React, { useEffect, useLayoutEffect } from "react";
5
6
import { Header } from "~/components/Header";
7
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
8
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
9
import {
10
constructPostQuery,
11
useQueryIdentity,
12
useQueryPost,
13
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
···
40
did,
41
rkey,
42
nopics,
43
-
lightboxCallback
44
}: {
45
did: string;
46
rkey: string;
47
nopics?: boolean;
48
-
lightboxCallback?: (d:LightboxProps) => void;
49
}) {
50
//const { get, set } = usePersistentStore();
51
const queryClient = useQueryClient();
···
193
() =>
194
resolvedDid
195
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
196
-
: "",
197
[resolvedDid, rkey]
198
);
199
200
const { data: mainPost } = useQueryPost(atUri);
201
202
// const { data: repliesData } = useQueryConstellation({
203
// method: "/links",
204
// target: atUri,
···
206
// path: ".reply.parent.uri",
207
// });
208
// const replies = repliesData?.linking_records.slice(0, 50) ?? [];
209
const infinitequeryresults = useInfiniteQuery({
210
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
211
{
212
method: "/links",
213
target: atUri,
214
collection: "app.bsky.feed.post",
···
219
});
220
221
const {
222
-
data: repliesData,
223
-
// fetchNextPage,
224
-
// hasNextPage,
225
-
// isFetchingNextPage,
226
} = infinitequeryresults;
227
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]);
238
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
-
: [];
249
250
-
const opdid = new AtUri(atUri).host;
251
252
// Find oldest OP reply
253
const oldestOpsIndex = replyAturis.findIndex(
···
260
replyAturis.unshift(oldestOpsReply);
261
}
262
263
-
264
const [parents, setParents] = React.useState<any[]>([]);
265
const [parentsLoading, setParentsLoading] = React.useState(false);
266
267
const mainPostRef = React.useRef<HTMLDivElement>(null);
268
-
const userHasScrolled = React.useRef(false);
269
270
-
const scrollAnchor = React.useRef<{ top: number } | null>(null);
271
272
-
React.useEffect(() => {
273
-
const onScroll = () => {
274
-
if (window.scrollY > 50) {
275
-
userHasScrolled.current = true;
276
277
-
window.removeEventListener("scroll", onScroll);
278
}
279
-
};
280
-
281
-
if (!userHasScrolled.current) {
282
-
window.addEventListener("scroll", onScroll, { passive: true });
283
}
284
-
return () => window.removeEventListener("scroll", onScroll);
285
-
}, []);
286
287
-
useLayoutEffect(() => {
288
-
if (parentsLoading && mainPostRef.current && !userHasScrolled.current) {
289
-
scrollAnchor.current = {
290
-
top: mainPostRef.current.getBoundingClientRect().top,
291
-
};
292
}
293
-
}, [parentsLoading]);
294
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;
307
}
308
-
}, [parents]);
309
310
React.useEffect(() => {
311
if (!mainPost?.value?.reply?.parent?.uri) {
···
324
while (currentParentUri && safetyCounter < MAX_PARENTS) {
325
try {
326
const parentPost = await queryClient.fetchQuery(
327
-
constructPostQuery(currentParentUri)
328
);
329
if (!parentPost) break;
330
parentChain.push(parentPost);
···
407
//margin: "0px auto 0",
408
padding: 0,
409
minHeight: "80dvh",
410
-
paddingBottom: "20dvh"
411
}}
412
>
413
<div
···
432
/>
433
);
434
})}
435
</div>
436
</div>
437
</>
···
1
import { AtUri } from "@atproto/api";
2
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
import { createFileRoute, Outlet } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
import React, { useLayoutEffect } from "react";
6
7
import { Header } from "~/components/Header";
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
9
+
import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms";
10
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
11
import {
12
constructPostQuery,
13
+
type linksAllResponse,
14
+
type linksRecordsResponse,
15
+
useQueryConstellation,
16
useQueryIdentity,
17
useQueryPost,
18
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
···
45
did,
46
rkey,
47
nopics,
48
+
lightboxCallback,
49
}: {
50
did: string;
51
rkey: string;
52
nopics?: boolean;
53
+
lightboxCallback?: (d: LightboxProps) => void;
54
}) {
55
//const { get, set } = usePersistentStore();
56
const queryClient = useQueryClient();
···
198
() =>
199
resolvedDid
200
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
201
+
: undefined,
202
[resolvedDid, rkey]
203
);
204
205
const { data: mainPost } = useQueryPost(atUri);
206
207
+
console.log("atUri",atUri)
208
+
209
+
const opdid = React.useMemo(
210
+
() =>
211
+
atUri
212
+
? new AtUri(atUri).host
213
+
: undefined,
214
+
[atUri]
215
+
);
216
+
217
+
// @ts-expect-error i hate overloads
218
+
const { data: links } = useQueryConstellation(atUri?{
219
+
method: "/links/all",
220
+
target: atUri,
221
+
} : {
222
+
method: "undefined",
223
+
target: ""
224
+
})as { data: linksAllResponse | undefined };
225
+
226
+
//const [likes, setLikes] = React.useState<number | null>(null);
227
+
//const [reposts, setReposts] = React.useState<number | null>(null);
228
+
const [replyCount, setReplyCount] = React.useState<number | null>(null);
229
+
230
+
React.useEffect(() => {
231
+
// /*mass comment*/ console.log(JSON.stringify(links, null, 2));
232
+
// setLikes(
233
+
// links
234
+
// ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0
235
+
// : null
236
+
// );
237
+
// setReposts(
238
+
// links
239
+
// ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0
240
+
// : null
241
+
// );
242
+
setReplyCount(
243
+
links
244
+
? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]
245
+
?.records || 0
246
+
: null
247
+
);
248
+
}, [links]);
249
+
250
+
const { data: opreplies } = useQueryConstellation(
251
+
!!opdid && replyCount && replyCount >= 25
252
+
? {
253
+
method: "/links",
254
+
target: atUri,
255
+
// @ts-expect-error overloading sucks so much
256
+
collection: "app.bsky.feed.post",
257
+
path: ".reply.parent.uri",
258
+
//cursor?: string;
259
+
dids: [opdid],
260
+
}
261
+
: {
262
+
method: "undefined",
263
+
target: "",
264
+
}
265
+
) as { data: linksRecordsResponse | undefined };
266
+
267
+
const opReplyAturis =
268
+
opreplies?.linking_records.map(
269
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`,
270
+
) ?? [];
271
+
272
+
273
// const { data: repliesData } = useQueryConstellation({
274
// method: "/links",
275
// target: atUri,
···
277
// path: ".reply.parent.uri",
278
// });
279
// const replies = repliesData?.linking_records.slice(0, 50) ?? [];
280
+
const [constellationurl] = useAtom(constellationURLAtom)
281
+
282
const infinitequeryresults = useInfiniteQuery({
283
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
284
{
285
+
constellation: constellationurl,
286
method: "/links",
287
target: atUri,
288
collection: "app.bsky.feed.post",
···
293
});
294
295
const {
296
+
data: infiniteRepliesData,
297
+
fetchNextPage,
298
+
hasNextPage,
299
+
isFetchingNextPage,
300
} = infinitequeryresults;
301
302
+
// // auto-fetch all pages
303
+
// useEffect(() => {
304
+
// if (
305
+
// infinitequeryresults.hasNextPage &&
306
+
// !infinitequeryresults.isFetchingNextPage
307
+
// ) {
308
+
// console.log("Fetching the next page...");
309
+
// infinitequeryresults.fetchNextPage();
310
+
// }
311
+
// }, [infinitequeryresults]);
312
+
313
+
// const replyAturis = repliesData
314
+
// ? repliesData.pages.flatMap((page) =>
315
+
// page
316
+
// ? page.linking_records.map((record) => {
317
+
// const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
318
+
// return aturi;
319
+
// })
320
+
// : []
321
+
// )
322
+
// : [];
323
324
+
const replyAturis = React.useMemo(() => {
325
+
// Get all replies from the standard infinite query
326
+
const allReplies =
327
+
infiniteRepliesData?.pages.flatMap(
328
+
(page) =>
329
+
page?.linking_records.map(
330
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`,
331
+
) ?? [],
332
+
) ?? [];
333
334
+
if (replyCount && (replyCount < 25)) {
335
+
// If count is low, just use the standard list and find the oldest OP reply to move to the top
336
+
const opdidFromUri = atUri ? new AtUri(atUri).host : undefined;
337
+
const oldestOpsIndex = allReplies.findIndex(
338
+
(aturi) => new AtUri(aturi).host === opdidFromUri,
339
+
);
340
+
if (oldestOpsIndex > 0) {
341
+
const [oldestOpsReply] = allReplies.splice(oldestOpsIndex, 1);
342
+
allReplies.unshift(oldestOpsReply);
343
+
}
344
+
return allReplies;
345
+
} else {
346
+
// If count is high, prioritize OP replies from the special query
347
+
// and filter them out from the main list to avoid duplication.
348
+
const opReplySet = new Set(opReplyAturis);
349
+
const otherReplies = allReplies.filter((uri) => !opReplySet.has(uri));
350
+
return [...opReplyAturis, ...otherReplies];
351
+
}
352
+
}, [infiniteRepliesData, opReplyAturis, replyCount, atUri]);
353
354
// Find oldest OP reply
355
const oldestOpsIndex = replyAturis.findIndex(
···
362
replyAturis.unshift(oldestOpsReply);
363
}
364
365
const [parents, setParents] = React.useState<any[]>([]);
366
const [parentsLoading, setParentsLoading] = React.useState(false);
367
368
const mainPostRef = React.useRef<HTMLDivElement>(null);
369
+
const hasPerformedInitialLayout = React.useRef(false);
370
+
371
+
const [layoutReady, setLayoutReady] = React.useState(false);
372
373
+
useLayoutEffect(() => {
374
+
if (parents.length > 0 && !layoutReady && mainPostRef.current) {
375
+
const mainPostElement = mainPostRef.current;
376
377
+
if (window.scrollY === 0 && !hasPerformedInitialLayout.current) {
378
+
const elementTop = mainPostElement.getBoundingClientRect().top;
379
+
const headerOffset = 70;
380
+
381
+
const targetScrollY = elementTop - headerOffset;
382
+
383
+
window.scrollBy(0, targetScrollY);
384
385
+
hasPerformedInitialLayout.current = true;
386
}
387
+
388
+
// todo idk what to do with this
389
+
// eslint-disable-next-line react-hooks/set-state-in-effect
390
+
setLayoutReady(true);
391
}
392
+
}, [parents, layoutReady]);
393
394
+
395
+
const [slingshoturl] = useAtom(slingshotURLAtom)
396
+
397
+
React.useEffect(() => {
398
+
if (parentsLoading) {
399
+
setLayoutReady(false);
400
}
401
402
+
if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) {
403
+
setLayoutReady(true);
404
+
hasPerformedInitialLayout.current = true;
405
}
406
+
}, [parentsLoading, mainPost]);
407
408
React.useEffect(() => {
409
if (!mainPost?.value?.reply?.parent?.uri) {
···
422
while (currentParentUri && safetyCounter < MAX_PARENTS) {
423
try {
424
const parentPost = await queryClient.fetchQuery(
425
+
constructPostQuery(currentParentUri, slingshoturl)
426
);
427
if (!parentPost) break;
428
parentChain.push(parentPost);
···
505
//margin: "0px auto 0",
506
padding: 0,
507
minHeight: "80dvh",
508
+
paddingBottom: "20dvh",
509
}}
510
>
511
<div
···
530
/>
531
);
532
})}
533
+
{hasNextPage && (
534
+
<button
535
+
onClick={() => fetchNextPage()}
536
+
disabled={isFetchingNextPage}
537
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
538
+
>
539
+
{isFetchingNextPage ? "Loading..." : "Load More"}
540
+
</button>
541
+
)}
542
</div>
543
</div>
544
</>
+50
-1
src/routes/search.tsx
+50
-1
src/routes/search.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
3
+
import { Header } from "~/components/Header";
4
+
import { Import } from "~/components/Import";
5
+
6
export const Route = createFileRoute("/search")({
7
component: Search,
8
});
9
10
export function Search() {
11
+
return (
12
+
<>
13
+
<Header
14
+
title="Explore"
15
+
backButtonCallback={() => {
16
+
if (window.history.length > 1) {
17
+
window.history.back();
18
+
} else {
19
+
window.location.assign("/");
20
+
}
21
+
}}
22
+
/>
23
+
<div className=" flex flex-col items-center mt-4 mx-4 gap-4">
24
+
<Import />
25
+
<div className="flex flex-col">
26
+
<p className="text-gray-600 dark:text-gray-400">
27
+
Sorry we dont have search. But instead, you can load some of these
28
+
types of content into Red Dwarf:
29
+
</p>
30
+
<ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400">
31
+
<li>
32
+
Bluesky URLs from supported clients (like{" "}
33
+
<code className="text-sm">bsky.app</code> or{" "}
34
+
<code className="text-sm">deer.social</code>).
35
+
</li>
36
+
<li>
37
+
AT-URIs (e.g.,{" "}
38
+
<code className="text-sm">at://did:example/collection/item</code>
39
+
).
40
+
</li>
41
+
<li>
42
+
Plain handles (like{" "}
43
+
<code className="text-sm">@username.bsky.social</code>).
44
+
</li>
45
+
<li>
46
+
Direct DIDs (Decentralized Identifiers, starting with{" "}
47
+
<code className="text-sm">did:</code>).
48
+
</li>
49
+
</ul>
50
+
<p className="mt-2 text-gray-600 dark:text-gray-400">
51
+
Simply paste one of these into the import field above and press
52
+
Enter to load the content.
53
+
</p>
54
+
</div>
55
+
</div>
56
+
</>
57
+
);
58
}
+172
-1
src/routes/settings.tsx
+172
-1
src/routes/settings.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
+
import { Slider } from "radix-ui";
4
5
import { Header } from "~/components/Header";
6
import Login from "~/components/Login";
7
+
import {
8
+
aturiListServiceAtom,
9
+
constellationURLAtom,
10
+
defaultaturilistservice,
11
+
defaultconstellationURL,
12
+
defaulthue,
13
+
defaultImgCDN,
14
+
defaultslingshotURL,
15
+
defaultVideoCDN,
16
+
hueAtom,
17
+
imgCDNAtom,
18
+
slingshotURLAtom,
19
+
videoCDNAtom,
20
+
} from "~/utils/atoms";
21
22
export const Route = createFileRoute("/settings")({
23
component: Settings,
···
36
}
37
}}
38
/>
39
+
<div className="lg:hidden">
40
+
<Login />
41
+
</div>
42
+
<div className="h-4" />
43
+
<TextInputSetting
44
+
atom={constellationURLAtom}
45
+
title={"Constellation"}
46
+
description={
47
+
"Customize the Constellation instance to be used by Red Dwarf"
48
+
}
49
+
init={defaultconstellationURL}
50
+
/>
51
+
<TextInputSetting
52
+
atom={slingshotURLAtom}
53
+
title={"Slingshot"}
54
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
55
+
init={defaultslingshotURL}
56
+
/>
57
+
<TextInputSetting
58
+
atom={aturiListServiceAtom}
59
+
title={"AtUriListService"}
60
+
description={"Customize the AtUriListService instance to be used by Red Dwarf"}
61
+
init={defaultaturilistservice}
62
+
/>
63
+
<TextInputSetting
64
+
atom={imgCDNAtom}
65
+
title={"Image CDN"}
66
+
description={
67
+
"Customize the Constellation instance to be used by Red Dwarf"
68
+
}
69
+
init={defaultImgCDN}
70
+
/>
71
+
<TextInputSetting
72
+
atom={videoCDNAtom}
73
+
title={"Video CDN"}
74
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
75
+
init={defaultVideoCDN}
76
+
/>
77
+
78
+
<Hue />
79
+
<p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm">
80
+
please restart/refresh the app if changes arent applying correctly
81
+
</p>
82
</>
83
);
84
}
85
+
function Hue() {
86
+
const [hue, setHue] = useAtom(hueAtom);
87
+
return (
88
+
<div className="flex flex-col px-4 mt-4 ">
89
+
<span className="z-10">Hue</span>
90
+
<div className="flex flex-row items-center gap-4">
91
+
<SliderComponent
92
+
atom={hueAtom}
93
+
max={360}
94
+
/>
95
+
<button
96
+
onClick={() => setHue(defaulthue ?? 28)}
97
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
98
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
99
+
>
100
+
Reset
101
+
</button>
102
+
</div>
103
+
</div>
104
+
);
105
+
}
106
+
107
+
export function TextInputSetting({
108
+
atom,
109
+
title,
110
+
description,
111
+
init,
112
+
}: {
113
+
atom: typeof constellationURLAtom;
114
+
title?: string;
115
+
description?: string;
116
+
init?: string;
117
+
}) {
118
+
const [value, setValue] = useAtom(atom);
119
+
return (
120
+
<div className="flex flex-col gap-2 px-4 py-2">
121
+
{/* <div>
122
+
{title && (
123
+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
124
+
{title}
125
+
</h3>
126
+
)}
127
+
{description && (
128
+
<p className="text-sm text-gray-500 dark:text-gray-400">
129
+
{description}
130
+
</p>
131
+
)}
132
+
</div> */}
133
+
134
+
<div className="flex flex-row gap-2 items-center">
135
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
136
+
<input
137
+
type="text"
138
+
placeholder=" "
139
+
value={value}
140
+
onChange={(e) => setValue(e.target.value)}
141
+
/>
142
+
<label>{title}</label>
143
+
</div>
144
+
{/* <input
145
+
type="text"
146
+
value={value}
147
+
onChange={(e) => setValue(e.target.value)}
148
+
className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700
149
+
text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400
150
+
focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600"
151
+
placeholder="Enter value..."
152
+
/> */}
153
+
<button
154
+
onClick={() => setValue(init ?? "")}
155
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
156
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
157
+
>
158
+
Reset
159
+
</button>
160
+
</div>
161
+
</div>
162
+
);
163
+
}
164
+
165
+
166
+
interface SliderProps {
167
+
atom: typeof hueAtom;
168
+
min?: number;
169
+
max?: number;
170
+
step?: number;
171
+
}
172
+
173
+
export const SliderComponent: React.FC<SliderProps> = ({
174
+
atom,
175
+
min = 0,
176
+
max = 100,
177
+
step = 1,
178
+
}) => {
179
+
180
+
const [value, setValue] = useAtom(atom)
181
+
182
+
return (
183
+
<Slider.Root
184
+
className="relative flex items-center w-full h-4"
185
+
value={[value]}
186
+
min={min}
187
+
max={max}
188
+
step={step}
189
+
onValueChange={(v: number[]) => setValue(v[0])}
190
+
>
191
+
<Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
192
+
<Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" />
193
+
</Slider.Track>
194
+
<Slider.Thumb className="shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" />
195
+
</Slider.Root>
196
+
);
197
+
};
+133
-11
src/styles/app.css
+133
-11
src/styles/app.css
···
15
--color-gray-950: oklch(0.129 0.050 222.000);
16
} */
17
18
@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);
30
}
31
32
@layer base {
···
105
:root {
106
--shadow-opacity: calc(1 - var(--is-top));
107
--tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15));
108
}
···
15
--color-gray-950: oklch(0.129 0.050 222.000);
16
} */
17
18
+
:root {
19
+
--safe-hue: var(--tw-gray-hue, 28)
20
+
}
21
+
22
@theme {
23
+
--color-gray-50: oklch(0.984 0.012 var(--safe-hue));
24
+
--color-gray-100: oklch(0.968 0.017 var(--safe-hue));
25
+
--color-gray-200: oklch(0.929 0.025 var(--safe-hue));
26
+
--color-gray-300: oklch(0.869 0.035 var(--safe-hue));
27
+
--color-gray-400: oklch(0.704 0.05 var(--safe-hue));
28
+
--color-gray-500: oklch(0.554 0.06 var(--safe-hue));
29
+
--color-gray-600: oklch(0.446 0.058 var(--safe-hue));
30
+
--color-gray-700: oklch(0.372 0.058 var(--safe-hue));
31
+
--color-gray-800: oklch(0.279 0.055 var(--safe-hue));
32
+
--color-gray-900: oklch(0.208 0.055 var(--safe-hue));
33
+
--color-gray-950: oklch(0.129 0.055 var(--safe-hue));
34
}
35
36
@layer base {
···
109
:root {
110
--shadow-opacity: calc(1 - var(--is-top));
111
--tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15));
112
+
}
113
+
114
+
115
+
/* m3 input */
116
+
:root {
117
+
--m3input-radius: 6px;
118
+
--m3input-border-width: .0625rem;
119
+
--m3input-font-size: 16px;
120
+
--m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1);
121
+
/* light theme */
122
+
--m3input-bg: var(--color-gray-50);
123
+
--m3input-border-color: var(--color-gray-400);
124
+
--m3input-label-color: var(--color-gray-500);
125
+
--m3input-text-color: var(--color-gray-900);
126
+
--m3input-focus-color: var(--color-gray-600);
127
+
}
128
+
129
+
@media (prefers-color-scheme: dark) {
130
+
:root {
131
+
--m3input-bg: var(--color-gray-950);
132
+
--m3input-border-color: var(--color-gray-700);
133
+
--m3input-label-color: var(--color-gray-400);
134
+
--m3input-text-color: var(--color-gray-50);
135
+
--m3input-focus-color: var(--color-gray-400);
136
+
}
137
+
}
138
+
139
+
/* reset page *//*
140
+
html,
141
+
body {
142
+
background: var(--m3input-bg);
143
+
margin: 0;
144
+
padding: 1rem;
145
+
color: var(--m3input-text-color);
146
+
font-family: system-ui, sans-serif;
147
+
font-size: var(--m3input-font-size);
148
+
}*/
149
+
150
+
/* base wrapper */
151
+
.m3input-field.m3input-label.m3input-border {
152
+
position: relative;
153
+
display: inline-block;
154
+
width: 100%;
155
+
/*max-width: 400px;*/
156
+
}
157
+
158
+
/* size variants */
159
+
.m3input-field.size-sm {
160
+
--m3input-h: 40px;
161
+
}
162
+
163
+
.m3input-field.size-md {
164
+
--m3input-h: 48px;
165
+
}
166
+
167
+
.m3input-field.size-lg {
168
+
--m3input-h: 56px;
169
+
}
170
+
171
+
.m3input-field.size-xl {
172
+
--m3input-h: 64px;
173
+
}
174
+
175
+
.m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) {
176
+
--m3input-h: 48px;
177
+
}
178
+
179
+
/* outlined input */
180
+
.m3input-field.m3input-label.m3input-border input {
181
+
width: 100%;
182
+
height: var(--m3input-h);
183
+
border: var(--m3input-border-width) solid var(--m3input-border-color);
184
+
border-radius: var(--m3input-radius);
185
+
background: var(--m3input-bg);
186
+
color: var(--m3input-text-color);
187
+
font-size: var(--m3input-font-size);
188
+
padding: 0 12px;
189
+
box-sizing: border-box;
190
+
outline: none;
191
+
transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition);
192
+
}
193
+
194
+
/* focus ring */
195
+
.m3input-field.m3input-label.m3input-border input:focus {
196
+
border-color: var(--m3input-focus-color);
197
+
/*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/
198
+
}
199
+
200
+
/* label */
201
+
.m3input-field.m3input-label.m3input-border label {
202
+
position: absolute;
203
+
left: 12px;
204
+
top: 50%;
205
+
transform: translateY(-50%);
206
+
background: var(--m3input-bg);
207
+
padding: 0 .25em;
208
+
color: var(--m3input-label-color);
209
+
pointer-events: none;
210
+
transition: all var(--m3input-transition);
211
+
}
212
+
213
+
/* float on focus or when filled */
214
+
.m3input-field.m3input-label.m3input-border input:focus+label,
215
+
.m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label {
216
+
top: 0;
217
+
transform: translateY(-50%) scale(.78);
218
+
left: 0;
219
+
color: var(--m3input-focus-color);
220
+
}
221
+
222
+
/* placeholder trick */
223
+
.m3input-field.m3input-label.m3input-border input::placeholder {
224
+
color: transparent;
225
+
}
226
+
227
+
/* radix i love you but like cmon man */
228
+
body[data-scroll-locked]{
229
+
margin-left: var(--removed-body-scroll-bar-size) !important;
230
}
+62
-6
src/utils/atoms.ts
+62
-6
src/utils/atoms.ts
···
1
import type Agent from "@atproto/api";
2
-
import { atom, createStore } from "jotai";
3
-
import { atomWithStorage } from 'jotai/utils';
4
5
export const store = createStore();
6
7
export const selectedFeedUriAtom = atomWithStorage<string | null>(
8
-
'selectedFeedUri',
9
null
10
);
11
12
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
14
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
15
-
'feedscrollpositions',
16
{}
17
);
18
19
export const likedPostsAtom = atomWithStorage<Record<string, string>>(
20
-
'likedPosts',
21
{}
22
);
23
24
export const isAtTopAtom = atom<boolean>(true);
25
26
-
export const agentAtom = atom<Agent|null>(null);
27
export const authedAtom = atom<boolean>(false);
···
1
import type Agent from "@atproto/api";
2
+
import { atom, createStore, useAtomValue } from "jotai";
3
+
import { atomWithStorage } from "jotai/utils";
4
+
import { useEffect } from "react";
5
6
export const store = createStore();
7
8
export const selectedFeedUriAtom = atomWithStorage<string | null>(
9
+
"selectedFeedUri",
10
null
11
);
12
13
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
14
15
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
16
+
"feedscrollpositions",
17
{}
18
);
19
20
export const likedPostsAtom = atomWithStorage<Record<string, string>>(
21
+
"likedPosts",
22
{}
23
);
24
25
+
export const defaultconstellationURL = "constellation.microcosm.blue";
26
+
export const constellationURLAtom = atomWithStorage<string>(
27
+
"constellationURL",
28
+
defaultconstellationURL
29
+
);
30
+
export const defaultslingshotURL = "slingshot.microcosm.blue";
31
+
export const slingshotURLAtom = atomWithStorage<string>(
32
+
"slingshotURL",
33
+
defaultslingshotURL
34
+
);
35
+
export const defaultaturilistservice = "aturilistservice.reddwarf.app";
36
+
export const aturiListServiceAtom = atomWithStorage<string>(
37
+
"aturilistservice",
38
+
defaultaturilistservice
39
+
);
40
+
export const defaultImgCDN = "cdn.bsky.app";
41
+
export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN);
42
+
export const defaultVideoCDN = "video.bsky.app";
43
+
export const videoCDNAtom = atomWithStorage<string>(
44
+
"videocdnurl",
45
+
defaultVideoCDN
46
+
);
47
+
48
+
export const defaulthue = 28;
49
+
export const hueAtom = atomWithStorage<number>("hue", defaulthue);
50
+
51
export const isAtTopAtom = atom<boolean>(true);
52
53
+
type ComposerState =
54
+
| { kind: "closed" }
55
+
| { kind: "root" }
56
+
| { kind: "reply"; parent: string }
57
+
| { kind: "quote"; subject: string };
58
+
export const composerAtom = atom<ComposerState>({ kind: "closed" });
59
+
60
+
export const agentAtom = atom<Agent | null>(null);
61
export const authedAtom = atom<boolean>(false);
62
+
63
+
export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) {
64
+
const value = useAtomValue(atom);
65
+
66
+
useEffect(() => {
67
+
document.documentElement.style.setProperty(cssVar, value.toString());
68
+
}, [value, cssVar]);
69
+
70
+
useEffect(() => {
71
+
document.documentElement.style.setProperty(cssVar, value.toString());
72
+
}, []);
73
+
}
74
+
75
+
hueAtom.onMount = (setAtom) => {
76
+
const stored = localStorage.getItem("hue");
77
+
if (stored != null) setAtom(Number(stored));
78
+
};
79
+
// export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) {
80
+
// const initial = store.get(atom);
81
+
// console.log("atom get ", initial);
82
+
// document.documentElement.style.setProperty(cssVar, initial.toString());
83
+
// }
+37
-3
src/utils/followState.ts
+37
-3
src/utils/followState.ts
···
1
-
import { AtUri, type Agent } from "@atproto/api";
2
-
import { useQueryConstellation, type linksRecordsResponse } from "./useQuery";
3
import type { QueryClient } from "@tanstack/react-query";
4
-
import { TID } from "@atproto/common-web";
5
6
export function useGetFollowState({
7
target,
···
127
};
128
});
129
}
···
1
+
import { type Agent,AtUri } from "@atproto/api";
2
+
import { TID } from "@atproto/common-web";
3
import type { QueryClient } from "@tanstack/react-query";
4
+
5
+
import { type linksRecordsResponse,useQueryConstellation } from "./useQuery";
6
7
export function useGetFollowState({
8
target,
···
128
};
129
});
130
}
131
+
132
+
133
+
134
+
export function useGetOneToOneState(params?: {
135
+
target: string;
136
+
user: string;
137
+
collection: string;
138
+
path: string;
139
+
}): string[] | undefined {
140
+
const { data: arbitrarydata } = useQueryConstellation(
141
+
params && params.user
142
+
? {
143
+
method: "/links",
144
+
target: params.target,
145
+
// @ts-expect-error overloading sucks so much
146
+
collection: params.collection,
147
+
path: params.path,
148
+
dids: [params.user],
149
+
}
150
+
: { method: "undefined", target: "whatever" }
151
+
// overloading sucks so much
152
+
) as { data: linksRecordsResponse | undefined };
153
+
if (!params || !params.user) return undefined;
154
+
const data = arbitrarydata?.linking_records.slice(0, 50) ?? [];
155
+
156
+
if (data.length > 0) {
157
+
return data.map((linksRecord) => {
158
+
return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`;
159
+
});
160
+
}
161
+
162
+
return undefined;
163
+
}
+53
-23
src/utils/useHydrated.ts
+53
-23
src/utils/useHydrated.ts
···
9
AppBskyFeedPost,
10
AtUri,
11
} from "@atproto/api";
12
import { useMemo } from "react";
13
14
-
import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery";
15
16
-
type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends
17
-
| { data: infer D }
18
-
| undefined
19
-
? D
20
-
: never;
21
22
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
23
return obj as $Typed<T>;
···
26
export function hydrateEmbedImages(
27
embed: AppBskyEmbedImages.Main,
28
did: string,
29
): $Typed<AppBskyEmbedImages.View> {
30
return asTyped({
31
$type: "app.bsky.embed.images#view" as const,
···
34
const link = img.image.ref?.["$link"];
35
if (!link) return null;
36
return {
37
-
thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
-
fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
alt: img.alt || "",
40
aspectRatio: img.aspectRatio,
41
};
···
47
export function hydrateEmbedExternal(
48
embed: AppBskyEmbedExternal.Main,
49
did: string,
50
): $Typed<AppBskyEmbedExternal.View> {
51
return asTyped({
52
$type: "app.bsky.embed.external#view" as const,
···
55
title: embed.external.title,
56
description: embed.external.description,
57
thumb: embed.external.thumb?.ref?.$link
58
-
? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
: undefined,
60
},
61
});
···
64
export function hydrateEmbedVideo(
65
embed: AppBskyEmbedVideo.Main,
66
did: string,
67
): $Typed<AppBskyEmbedVideo.View> {
68
const videoLink = embed.video.ref.$link;
69
return asTyped({
70
$type: "app.bsky.embed.video#view" as const,
71
-
playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`,
72
-
thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`,
73
aspectRatio: embed.aspectRatio,
74
cid: videoLink,
75
});
···
80
quotedPost: QueryResultData<typeof useQueryPost>,
81
quotedProfile: QueryResultData<typeof useQueryProfile>,
82
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
83
): $Typed<AppBskyEmbedRecord.View> | undefined {
84
if (!quotedPost || !quotedProfile || !quotedIdentity) {
85
return undefined;
···
91
handle: quotedIdentity.handle,
92
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
93
avatar: quotedProfile.value.avatar?.ref?.$link
94
-
? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
95
: undefined,
96
viewer: {},
97
labels: [],
···
122
quotedPost: QueryResultData<typeof useQueryPost>,
123
quotedProfile: QueryResultData<typeof useQueryProfile>,
124
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
125
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
126
const hydratedRecord = hydrateEmbedRecord(
127
embed.record,
128
quotedPost,
129
quotedProfile,
130
quotedIdentity,
131
);
132
133
if (!hydratedRecord) return undefined;
···
148
149
export function useHydratedEmbed(
150
embed: AppBskyFeedPost.Record["embed"],
151
-
postAuthorDid: string | undefined,
152
) {
153
const recordInfo = useMemo(() => {
154
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
···
181
error: profileError,
182
} = useQueryProfile(profileUri);
183
184
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
185
186
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
187
if (!embed || !postAuthorDid) return undefined;
188
189
-
if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) {
190
return undefined;
191
}
192
193
try {
194
if (AppBskyEmbedImages.isMain(embed)) {
195
-
return hydrateEmbedImages(embed, postAuthorDid);
196
} else if (AppBskyEmbedExternal.isMain(embed)) {
197
-
return hydrateEmbedExternal(embed, postAuthorDid);
198
} else if (AppBskyEmbedVideo.isMain(embed)) {
199
-
return hydrateEmbedVideo(embed, postAuthorDid);
200
} else if (AppBskyEmbedRecord.isMain(embed)) {
201
return hydrateEmbedRecord(
202
embed,
203
usequerypostresults?.data,
204
quotedProfile,
205
queryidentityresult?.data,
206
);
207
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
208
let hydratedMedia:
···
212
| undefined;
213
214
if (AppBskyEmbedImages.isMain(embed.media)) {
215
-
hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid);
216
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
217
-
hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid);
218
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
219
-
hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid);
220
}
221
222
if (hydratedMedia) {
···
226
usequerypostresults?.data,
227
quotedProfile,
228
queryidentityresult?.data,
229
);
230
}
231
}
···
236
})();
237
238
const isLoading = isRecordType
239
-
? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading
240
: false;
241
242
-
const error = usequerypostresults?.error || profileError || queryidentityresult?.error;
243
244
return { data: hydratedEmbed, isLoading, error };
245
-
}
···
9
AppBskyFeedPost,
10
AtUri,
11
} from "@atproto/api";
12
+
import { useAtom } from "jotai";
13
import { useMemo } from "react";
14
15
+
import { imgCDNAtom, videoCDNAtom } from "./atoms";
16
+
import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery";
17
18
+
type QueryResultData<T extends (...args: any) => any> =
19
+
ReturnType<T> extends { data: infer D } | undefined ? D : never;
20
21
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
22
return obj as $Typed<T>;
···
25
export function hydrateEmbedImages(
26
embed: AppBskyEmbedImages.Main,
27
did: string,
28
+
cdn: string
29
): $Typed<AppBskyEmbedImages.View> {
30
return asTyped({
31
$type: "app.bsky.embed.images#view" as const,
···
34
const link = img.image.ref?.["$link"];
35
if (!link) return null;
36
return {
37
+
thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
+
fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
alt: img.alt || "",
40
aspectRatio: img.aspectRatio,
41
};
···
47
export function hydrateEmbedExternal(
48
embed: AppBskyEmbedExternal.Main,
49
did: string,
50
+
cdn: string
51
): $Typed<AppBskyEmbedExternal.View> {
52
return asTyped({
53
$type: "app.bsky.embed.external#view" as const,
···
56
title: embed.external.title,
57
description: embed.external.description,
58
thumb: embed.external.thumb?.ref?.$link
59
+
? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
60
: undefined,
61
},
62
});
···
65
export function hydrateEmbedVideo(
66
embed: AppBskyEmbedVideo.Main,
67
did: string,
68
+
videocdn: string
69
): $Typed<AppBskyEmbedVideo.View> {
70
const videoLink = embed.video.ref.$link;
71
return asTyped({
72
$type: "app.bsky.embed.video#view" as const,
73
+
playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`,
74
+
thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`,
75
aspectRatio: embed.aspectRatio,
76
cid: videoLink,
77
});
···
82
quotedPost: QueryResultData<typeof useQueryPost>,
83
quotedProfile: QueryResultData<typeof useQueryProfile>,
84
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
85
+
cdn: string
86
): $Typed<AppBskyEmbedRecord.View> | undefined {
87
if (!quotedPost || !quotedProfile || !quotedIdentity) {
88
return undefined;
···
94
handle: quotedIdentity.handle,
95
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
96
avatar: quotedProfile.value.avatar?.ref?.$link
97
+
? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
98
: undefined,
99
viewer: {},
100
labels: [],
···
125
quotedPost: QueryResultData<typeof useQueryPost>,
126
quotedProfile: QueryResultData<typeof useQueryProfile>,
127
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
128
+
cdn: string
129
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
130
const hydratedRecord = hydrateEmbedRecord(
131
embed.record,
132
quotedPost,
133
quotedProfile,
134
quotedIdentity,
135
+
cdn
136
);
137
138
if (!hydratedRecord) return undefined;
···
153
154
export function useHydratedEmbed(
155
embed: AppBskyFeedPost.Record["embed"],
156
+
postAuthorDid: string | undefined
157
) {
158
const recordInfo = useMemo(() => {
159
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
···
186
error: profileError,
187
} = useQueryProfile(profileUri);
188
189
+
const [imgcdn] = useAtom(imgCDNAtom);
190
+
const [videocdn] = useAtom(videoCDNAtom);
191
+
192
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
193
194
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
195
if (!embed || !postAuthorDid) return undefined;
196
197
+
if (
198
+
isRecordType &&
199
+
(!usequerypostresults?.data ||
200
+
!quotedProfile ||
201
+
!queryidentityresult?.data)
202
+
) {
203
return undefined;
204
}
205
206
try {
207
if (AppBskyEmbedImages.isMain(embed)) {
208
+
return hydrateEmbedImages(embed, postAuthorDid, imgcdn);
209
} else if (AppBskyEmbedExternal.isMain(embed)) {
210
+
return hydrateEmbedExternal(embed, postAuthorDid, imgcdn);
211
} else if (AppBskyEmbedVideo.isMain(embed)) {
212
+
return hydrateEmbedVideo(embed, postAuthorDid, videocdn);
213
} else if (AppBskyEmbedRecord.isMain(embed)) {
214
return hydrateEmbedRecord(
215
embed,
216
usequerypostresults?.data,
217
quotedProfile,
218
queryidentityresult?.data,
219
+
imgcdn
220
);
221
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
222
let hydratedMedia:
···
226
| undefined;
227
228
if (AppBskyEmbedImages.isMain(embed.media)) {
229
+
hydratedMedia = hydrateEmbedImages(
230
+
embed.media,
231
+
postAuthorDid,
232
+
imgcdn
233
+
);
234
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
235
+
hydratedMedia = hydrateEmbedExternal(
236
+
embed.media,
237
+
postAuthorDid,
238
+
imgcdn
239
+
);
240
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
241
+
hydratedMedia = hydrateEmbedVideo(
242
+
embed.media,
243
+
postAuthorDid,
244
+
videocdn
245
+
);
246
}
247
248
if (hydratedMedia) {
···
252
usequerypostresults?.data,
253
quotedProfile,
254
queryidentityresult?.data,
255
+
imgcdn
256
);
257
}
258
}
···
263
})();
264
265
const isLoading = isRecordType
266
+
? usequerypostresults?.isLoading ||
267
+
isLoadingProfile ||
268
+
queryidentityresult?.isLoading
269
: false;
270
271
+
const error =
272
+
usequerypostresults?.error || profileError || queryidentityresult?.error;
273
274
return { data: hydratedEmbed, isLoading, error };
275
+
}
+108
-19
src/utils/useQuery.ts
+108
-19
src/utils/useQuery.ts
···
6
useInfiniteQuery,
7
useQuery,
8
type UseQueryResult} from "@tanstack/react-query";
9
10
-
export function constructIdentityQuery(didorhandle?: string) {
11
return queryOptions({
12
queryKey: ["identity", didorhandle],
13
queryFn: async () => {
14
if (!didorhandle) return undefined as undefined
15
const res = await fetch(
16
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
17
);
18
if (!res.ok) throw new Error("Failed to fetch post");
19
try {
···
55
Error
56
>
57
export function useQueryIdentity(didorhandle?: string) {
58
-
return useQuery(constructIdentityQuery(didorhandle));
59
}
60
61
-
export function constructPostQuery(uri?: string) {
62
return queryOptions({
63
queryKey: ["post", uri],
64
queryFn: async () => {
65
if (!uri) return undefined as undefined
66
const res = await fetch(
67
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
68
);
69
let data: any;
70
try {
···
118
Error
119
>
120
export function useQueryPost(uri?: string) {
121
-
return useQuery(constructPostQuery(uri));
122
}
123
124
-
export function constructProfileQuery(uri?: string) {
125
return queryOptions({
126
queryKey: ["profile", uri],
127
queryFn: async () => {
128
if (!uri) return undefined as undefined
129
const res = await fetch(
130
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
131
);
132
let data: any;
133
try {
···
181
Error
182
>
183
export function useQueryProfile(uri?: string) {
184
-
return useQuery(constructProfileQuery(uri));
185
}
186
187
// export function constructConstellationQuery(
···
217
// target: string
218
// ): QueryOptions<linksAllResponse, Error>;
219
export function constructConstellationQuery(query?:{
220
method:
221
| "/links"
222
| "/links/distinct-dids"
···
250
const cursor = query.cursor
251
const dids = query?.dids
252
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("") : ""}`
254
);
255
if (!res.ok) throw new Error("Failed to fetch post");
256
try {
···
339
>
340
| undefined {
341
//if (!query) return;
342
return useQuery(
343
-
constructConstellationQuery(query)
344
);
345
}
346
···
362
type linksCountResponse = {
363
total: string;
364
};
365
-
type linksAllResponse = {
366
links: Record<
367
string,
368
Record<
···
445
446
447
448
-
export function constructArbitraryQuery(uri?: string) {
449
return queryOptions({
450
queryKey: ["arbitrary", uri],
451
queryFn: async () => {
452
if (!uri) return undefined as undefined
453
const res = await fetch(
454
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
455
);
456
let data: any;
457
try {
···
504
Error
505
>;
506
export function useQueryArbitrary(uri?: string) {
507
-
return useQuery(constructArbitraryQuery(uri));
508
}
509
510
export function constructFallbackNothingQuery(){
···
556
});
557
}
558
559
type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
560
561
export function constructInfiniteFeedSkeletonQuery(options: {
···
606
}) {
607
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
608
609
-
return useInfiniteQuery({
610
queryKey,
611
queryFn,
612
initialPageParam: undefined as never,
···
614
staleTime: Infinity,
615
refetchOnWindowFocus: false,
616
enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true),
617
-
});
618
}
619
620
621
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
622
method: '/links'
623
target?: string
624
collection: string
625
path: string
626
}) {
627
-
const constellationHost = 'constellation.microcosm.blue'
628
console.log(
629
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
630
query,
···
650
const cursor = pageParam
651
652
const res = await fetch(
653
-
`https://${constellationHost}${method}?target=${encodeURIComponent(target)}${
654
collection ? `&collection=${encodeURIComponent(collection)}` : ''
655
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
656
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
···
6
useInfiniteQuery,
7
useQuery,
8
type UseQueryResult} from "@tanstack/react-query";
9
+
import { useAtom } from "jotai";
10
11
+
import { constellationURLAtom, slingshotURLAtom } from "./atoms";
12
+
13
+
export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) {
14
return queryOptions({
15
queryKey: ["identity", didorhandle],
16
queryFn: async () => {
17
if (!didorhandle) return undefined as undefined
18
const res = await fetch(
19
+
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
20
);
21
if (!res.ok) throw new Error("Failed to fetch post");
22
try {
···
58
Error
59
>
60
export function useQueryIdentity(didorhandle?: string) {
61
+
const [slingshoturl] = useAtom(slingshotURLAtom)
62
+
return useQuery(constructIdentityQuery(didorhandle, slingshoturl));
63
}
64
65
+
export function constructPostQuery(uri?: string, slingshoturl?: string) {
66
return queryOptions({
67
queryKey: ["post", uri],
68
queryFn: async () => {
69
if (!uri) return undefined as undefined
70
const res = await fetch(
71
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
72
);
73
let data: any;
74
try {
···
122
Error
123
>
124
export function useQueryPost(uri?: string) {
125
+
const [slingshoturl] = useAtom(slingshotURLAtom)
126
+
return useQuery(constructPostQuery(uri, slingshoturl));
127
}
128
129
+
export function constructProfileQuery(uri?: string, slingshoturl?: string) {
130
return queryOptions({
131
queryKey: ["profile", uri],
132
queryFn: async () => {
133
if (!uri) return undefined as undefined
134
const res = await fetch(
135
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
136
);
137
let data: any;
138
try {
···
186
Error
187
>
188
export function useQueryProfile(uri?: string) {
189
+
const [slingshoturl] = useAtom(slingshotURLAtom)
190
+
return useQuery(constructProfileQuery(uri, slingshoturl));
191
}
192
193
// export function constructConstellationQuery(
···
223
// target: string
224
// ): QueryOptions<linksAllResponse, Error>;
225
export function constructConstellationQuery(query?:{
226
+
constellation: string,
227
method:
228
| "/links"
229
| "/links/distinct-dids"
···
257
const cursor = query.cursor
258
const dids = query?.dids
259
const res = await fetch(
260
+
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
261
);
262
if (!res.ok) throw new Error("Failed to fetch post");
263
try {
···
346
>
347
| undefined {
348
//if (!query) return;
349
+
const [constellationurl] = useAtom(constellationURLAtom)
350
return useQuery(
351
+
constructConstellationQuery(query && {constellation: constellationurl, ...query})
352
);
353
}
354
···
370
type linksCountResponse = {
371
total: string;
372
};
373
+
export type linksAllResponse = {
374
links: Record<
375
string,
376
Record<
···
453
454
455
456
+
export function constructArbitraryQuery(uri?: string, slingshoturl?: string) {
457
return queryOptions({
458
queryKey: ["arbitrary", uri],
459
queryFn: async () => {
460
if (!uri) return undefined as undefined
461
const res = await fetch(
462
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
463
);
464
let data: any;
465
try {
···
512
Error
513
>;
514
export function useQueryArbitrary(uri?: string) {
515
+
const [slingshoturl] = useAtom(slingshotURLAtom)
516
+
return useQuery(constructArbitraryQuery(uri, slingshoturl));
517
}
518
519
export function constructFallbackNothingQuery(){
···
565
});
566
}
567
568
+
export const ATURI_PAGE_LIMIT = 100;
569
+
570
+
export interface AturiDirectoryAturisItem {
571
+
uri: string;
572
+
cid: string;
573
+
rkey: string;
574
+
}
575
+
576
+
export type AturiDirectoryAturis = AturiDirectoryAturisItem[];
577
+
578
+
export function constructAturiListQuery(aturilistservice: string, did: string, collection: string, reverse?: boolean) {
579
+
return queryOptions({
580
+
// A unique key for this query, including all parameters that affect the data.
581
+
queryKey: ["aturiList", did, collection, { reverse }],
582
+
583
+
// The function that fetches the data.
584
+
queryFn: async ({ pageParam }: QueryFunctionContext) => {
585
+
const cursor = pageParam as string | undefined;
586
+
587
+
// Use URLSearchParams for safe and clean URL construction.
588
+
const params = new URLSearchParams({
589
+
did,
590
+
collection,
591
+
});
592
+
593
+
if (cursor) {
594
+
params.set("cursor", cursor);
595
+
}
596
+
597
+
// Add the reverse parameter if it's true
598
+
if (reverse) {
599
+
params.set("reverse", "true");
600
+
}
601
+
602
+
const url = `https://${aturilistservice}/aturis?${params.toString()}`;
603
+
604
+
const res = await fetch(url);
605
+
if (!res.ok) {
606
+
// You can add more specific error handling here
607
+
throw new Error(`Failed to fetch AT-URI list for ${did}`);
608
+
}
609
+
610
+
return res.json() as Promise<AturiDirectoryAturis>;
611
+
},
612
+
});
613
+
}
614
+
615
+
export function useInfiniteQueryAturiList({aturilistservice, did, collection, reverse}:{aturilistservice: string, did: string | undefined, collection: string | undefined, reverse?: boolean}) {
616
+
// We only enable the query if both `did` and `collection` are provided.
617
+
const isEnabled = !!did && !!collection;
618
+
619
+
const { queryKey, queryFn } = constructAturiListQuery(aturilistservice, did!, collection!, reverse);
620
+
621
+
return useInfiniteQuery({
622
+
queryKey,
623
+
queryFn,
624
+
initialPageParam: undefined as never, // ???? what is this shit
625
+
626
+
// @ts-expect-error i wouldve used as null | undefined, anyways
627
+
getNextPageParam: (lastPage: AturiDirectoryAturis) => {
628
+
// If the last page returned no records, we're at the end.
629
+
if (!lastPage || lastPage.length === 0) {
630
+
return undefined;
631
+
}
632
+
633
+
// If the number of records is less than our page limit, it must be the last page.
634
+
if (lastPage.length < ATURI_PAGE_LIMIT) {
635
+
return undefined;
636
+
}
637
+
638
+
// The cursor for the next page is the `rkey` of the last item we received.
639
+
const lastItem = lastPage[lastPage.length - 1];
640
+
return lastItem.rkey;
641
+
},
642
+
643
+
enabled: isEnabled,
644
+
});
645
+
}
646
+
647
+
648
type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
649
650
export function constructInfiniteFeedSkeletonQuery(options: {
···
695
}) {
696
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
697
698
+
return {...useInfiniteQuery({
699
queryKey,
700
queryFn,
701
initialPageParam: undefined as never,
···
703
staleTime: Infinity,
704
refetchOnWindowFocus: false,
705
enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true),
706
+
}), queryKey: queryKey};
707
}
708
709
710
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
711
+
constellation: string,
712
method: '/links'
713
target?: string
714
collection: string
715
path: string
716
}) {
717
console.log(
718
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
719
query,
···
739
const cursor = pageParam
740
741
const res = await fetch(
742
+
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${
743
collection ? `&collection=${encodeURIComponent(collection)}` : ''
744
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
745
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
+1
-1
vite.config.ts
+1
-1
vite.config.ts