+22
-4
README.md
+22
-4
README.md
···
1
1
# Red Dwarf
2
2
Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS.
3
3
4
-

4
+

5
5
6
6
huge thanks to [Microcosm](https://microcosm.blue/) for making this possible
7
7
8
8
## running dev and build
9
9
in the `vite.config.ts` file you should change these values
10
10
```ts
11
-
const PROD_URL = "https://reddwarf.whey.party"
11
+
const PROD_URL = "https://reddwarf.app"
12
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
13
```
14
14
the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
···
52
52
and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed
53
53
54
54
## Tanstack Router
55
-
it does the job, nothing very specific was used here
55
+
something specific was used here
56
+
57
+
so tanstack router is used as the base, but the home route is using tanstack-router-keepalive to preserve the route for better responsiveness, and it also saves scroll position of feeds into jotai (persistent)
58
+
59
+
i previously used a tanstack router loader to ensure the tanstack query cache is ready to prevent scroll jumps but it is way too slow so i replaced it with tanstack-router-keepalive
60
+
61
+
## Icons
62
+
this project uses Material icons. do not the light variant. sometimes i use `Mdi` if the icon needed doesnt exist in `MaterialSymbols`
56
63
57
-
im planning to use the loader system on select pages to prevent loss of scroll positon and state though its really complex so i havent done it yet but the migration to tanstack query is a huge first step towards this goal
64
+
the project uses unplugin icon auto import, so you can just use the component and itll just work!
65
+
66
+
the format is:
67
+
```tsx
68
+
<IconMaterialSymbols{icon name here} />
69
+
// or
70
+
<IconMdi{icon name here} />
71
+
```
72
+
73
+
you can get the full list of icon names from iconify ([Material Symbols](https://icon-sets.iconify.design/material-symbols/) or [MDI](https://icon-sets.iconify.design/mdi/))
74
+
75
+
while it is nice to keep everything consistent by using material icons, if the icon you need is not provided by either material symbols nor mdi, you are allowed to just grab any icon from any pack (please do prioritize icons that fit in)
+1786
-41
package-lock.json
+1786
-41
package-lock.json
···
8
8
"dependencies": {
9
9
"@atproto/api": "^0.16.6",
10
10
"@atproto/oauth-client-browser": "^0.3.33",
11
+
"@radix-ui/react-dialog": "^1.1.15",
12
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
13
+
"@radix-ui/react-hover-card": "^1.1.15",
14
+
"@radix-ui/react-slider": "^1.3.6",
11
15
"@tailwindcss/vite": "^4.0.6",
12
16
"@tanstack/query-sync-storage-persister": "^5.85.6",
13
17
"@tanstack/react-devtools": "^0.2.2",
···
16
20
"@tanstack/react-router": "^1.130.2",
17
21
"@tanstack/react-router-devtools": "^1.131.5",
18
22
"@tanstack/router-plugin": "^1.121.2",
23
+
"dompurify": "^3.3.0",
19
24
"i": "^0.3.7",
20
25
"idb-keyval": "^6.2.2",
21
26
"jotai": "^2.13.1",
22
27
"npm": "^11.6.2",
28
+
"radix-ui": "^1.4.3",
23
29
"react": "^19.0.0",
24
30
"react-dom": "^19.0.0",
25
31
"react-player": "^3.3.2",
···
31
37
"@iconify-icon/react": "^3.0.1",
32
38
"@iconify-json/material-symbols": "^1.2.42",
33
39
"@iconify-json/mdi": "^1.2.3",
40
+
"@iconify/json": "^2.2.396",
34
41
"@svgr/core": "^8.1.0",
35
42
"@svgr/plugin-jsx": "^8.1.0",
36
43
"@testing-library/dom": "^10.4.0",
···
369
376
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
370
377
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
371
378
"license": "MIT",
372
-
"peer": true,
373
379
"dependencies": {
374
380
"@ampproject/remapping": "^2.2.0",
375
381
"@babel/code-frame": "^7.27.1",
···
877
883
}
878
884
],
879
885
"license": "MIT",
880
-
"peer": true,
881
886
"engines": {
882
887
"node": ">=18"
883
888
},
···
901
906
}
902
907
],
903
908
"license": "MIT",
904
-
"peer": true,
905
909
"engines": {
906
910
"node": ">=18"
907
911
}
···
1490
1494
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
1491
1495
"dev": true,
1492
1496
"license": "Apache-2.0",
1497
+
"peer": true,
1493
1498
"dependencies": {
1494
1499
"@eslint/object-schema": "^2.1.6",
1495
1500
"debug": "^4.3.1",
···
1505
1510
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
1506
1511
"dev": true,
1507
1512
"license": "Apache-2.0",
1513
+
"peer": true,
1508
1514
"dependencies": {
1509
1515
"@eslint/core": "^0.16.0"
1510
1516
},
···
1518
1524
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
1519
1525
"dev": true,
1520
1526
"license": "Apache-2.0",
1527
+
"peer": true,
1521
1528
"dependencies": {
1522
1529
"@types/json-schema": "^7.0.15"
1523
1530
},
···
1531
1538
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
1532
1539
"dev": true,
1533
1540
"license": "MIT",
1541
+
"peer": true,
1534
1542
"dependencies": {
1535
1543
"ajv": "^6.12.4",
1536
1544
"debug": "^4.3.2",
···
1555
1563
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
1556
1564
"dev": true,
1557
1565
"license": "MIT",
1566
+
"peer": true,
1558
1567
"engines": {
1559
1568
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1560
1569
},
···
1568
1577
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
1569
1578
"dev": true,
1570
1579
"license": "Apache-2.0",
1580
+
"peer": true,
1571
1581
"engines": {
1572
1582
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1573
1583
}
···
1578
1588
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
1579
1589
"dev": true,
1580
1590
"license": "Apache-2.0",
1591
+
"peer": true,
1581
1592
"dependencies": {
1582
1593
"@eslint/core": "^0.16.0",
1583
1594
"levn": "^0.4.1"
···
1586
1597
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1587
1598
}
1588
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
+
},
1589
1634
"node_modules/@humanfs/core": {
1590
1635
"version": "0.19.1",
1591
1636
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
1592
1637
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
1593
1638
"dev": true,
1594
1639
"license": "Apache-2.0",
1640
+
"peer": true,
1595
1641
"engines": {
1596
1642
"node": ">=18.18.0"
1597
1643
}
···
1602
1648
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
1603
1649
"dev": true,
1604
1650
"license": "Apache-2.0",
1651
+
"peer": true,
1605
1652
"dependencies": {
1606
1653
"@humanfs/core": "^0.19.1",
1607
1654
"@humanwhocodes/retry": "^0.4.0"
···
1616
1663
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
1617
1664
"dev": true,
1618
1665
"license": "Apache-2.0",
1666
+
"peer": true,
1619
1667
"engines": {
1620
1668
"node": ">=12.22"
1621
1669
},
···
1630
1678
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
1631
1679
"dev": true,
1632
1680
"license": "Apache-2.0",
1681
+
"peer": true,
1633
1682
"engines": {
1634
1683
"node": ">=18.18"
1635
1684
},
···
1674
1723
"@iconify/types": "*"
1675
1724
}
1676
1725
},
1726
+
"node_modules/@iconify/json": {
1727
+
"version": "2.2.396",
1728
+
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.396.tgz",
1729
+
"integrity": "sha512-tijg77JFuYIt32S9N8p7La8C0zp9zKZsX6UP8ip5GVB1F6Mp3pZA5Vc5eAquTY50NoDJX58U6z4Qn3d6Wyossg==",
1730
+
"dev": true,
1731
+
"license": "MIT",
1732
+
"dependencies": {
1733
+
"@iconify/types": "*",
1734
+
"pathe": "^2.0.0"
1735
+
}
1736
+
},
1677
1737
"node_modules/@iconify/types": {
1678
1738
"version": "2.0.0",
1679
1739
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
···
1874
1934
"node": ">= 8"
1875
1935
}
1876
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
+
},
1877
3376
"node_modules/@rolldown/pluginutils": {
1878
3377
"version": "1.0.0-beta.27",
1879
3378
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
···
2357
3856
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
2358
3857
"dev": true,
2359
3858
"license": "MIT",
2360
-
"peer": true,
2361
3859
"dependencies": {
2362
3860
"@babel/core": "^7.21.3",
2363
3861
"@svgr/babel-preset": "8.1.0",
···
2832
4330
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.6.tgz",
2833
4331
"integrity": "sha512-VUAag4ERjh+qlmg0wNivQIVCZUrYndqYu3/wPCVZd4r0E+1IqotbeyGTc+ICroL/PqbpSaGZg02zSWYfcvxbdA==",
2834
4332
"license": "MIT",
2835
-
"peer": true,
2836
4333
"dependencies": {
2837
4334
"@tanstack/query-core": "5.85.6"
2838
4335
},
···
2866
4363
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.131.28.tgz",
2867
4364
"integrity": "sha512-vWExhrqHJuT9v+6/2DCQ4pVvPaYoLazMNw8WXiLNuzBXh1FuEoIGaW3jw3DEP0OJCmMiWtTi34NzQnakkQZlQg==",
2868
4365
"license": "MIT",
2869
-
"peer": true,
2870
4366
"dependencies": {
2871
4367
"@tanstack/history": "1.131.2",
2872
4368
"@tanstack/react-store": "^0.7.0",
···
2931
4427
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz",
2932
4428
"integrity": "sha512-f+vdfr3WKSS/BcqgI5s4vZg9xYb7NkvIolkaMELrbz3l+khkw1aTjx8wqCHRY4dqwIAxq+iZBZtMWXA7pztGJg==",
2933
4429
"license": "MIT",
2934
-
"peer": true,
2935
4430
"dependencies": {
2936
4431
"@tanstack/history": "1.131.2",
2937
4432
"@tanstack/store": "^0.7.0",
···
3104
4599
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
3105
4600
"dev": true,
3106
4601
"license": "MIT",
3107
-
"peer": true,
3108
4602
"dependencies": {
3109
4603
"@babel/code-frame": "^7.10.4",
3110
4604
"@babel/runtime": "^7.12.5",
···
3227
4721
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
3228
4722
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
3229
4723
"dev": true,
3230
-
"license": "MIT"
4724
+
"license": "MIT",
4725
+
"peer": true
3231
4726
},
3232
4727
"node_modules/@types/node": {
3233
4728
"version": "24.3.0",
···
3235
4730
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
3236
4731
"devOptional": true,
3237
4732
"license": "MIT",
3238
-
"peer": true,
3239
4733
"dependencies": {
3240
4734
"undici-types": "~7.10.0"
3241
4735
}
···
3245
4739
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
3246
4740
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
3247
4741
"license": "MIT",
3248
-
"peer": true,
3249
4742
"dependencies": {
3250
4743
"csstype": "^3.0.2"
3251
4744
}
···
3255
4748
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
3256
4749
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
3257
4750
"license": "MIT",
3258
-
"peer": true,
3259
4751
"peerDependencies": {
3260
4752
"@types/react": "^19.0.0"
3261
4753
}
3262
4754
},
4755
+
"node_modules/@types/trusted-types": {
4756
+
"version": "2.0.7",
4757
+
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
4758
+
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
4759
+
"license": "MIT",
4760
+
"optional": true
4761
+
},
3263
4762
"node_modules/@typescript-eslint/eslint-plugin": {
3264
4763
"version": "8.46.1",
3265
4764
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
3266
4765
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
3267
4766
"dev": true,
3268
4767
"license": "MIT",
3269
-
"peer": true,
3270
4768
"dependencies": {
3271
4769
"@eslint-community/regexpp": "^4.10.0",
3272
4770
"@typescript-eslint/scope-manager": "8.46.1",
···
3307
4805
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
3308
4806
"dev": true,
3309
4807
"license": "MIT",
3310
-
"peer": true,
3311
4808
"dependencies": {
3312
4809
"@typescript-eslint/scope-manager": "8.46.1",
3313
4810
"@typescript-eslint/types": "8.46.1",
···
3690
5187
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
3691
5188
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
3692
5189
"license": "MIT",
3693
-
"peer": true,
3694
5190
"bin": {
3695
5191
"acorn": "bin/acorn"
3696
5192
},
···
3704
5200
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
3705
5201
"dev": true,
3706
5202
"license": "MIT",
5203
+
"peer": true,
3707
5204
"peerDependencies": {
3708
5205
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
3709
5206
}
···
3724
5221
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
3725
5222
"dev": true,
3726
5223
"license": "MIT",
5224
+
"peer": true,
3727
5225
"dependencies": {
3728
5226
"fast-deep-equal": "^3.1.1",
3729
5227
"fast-json-stable-stringify": "^2.0.0",
···
3787
5285
"dev": true,
3788
5286
"license": "Python-2.0"
3789
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
+
},
3790
5299
"node_modules/aria-query": {
3791
5300
"version": "5.3.0",
3792
5301
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
···
4118
5627
}
4119
5628
],
4120
5629
"license": "MIT",
4121
-
"peer": true,
4122
5630
"dependencies": {
4123
5631
"caniuse-lite": "^1.0.30001737",
4124
5632
"electron-to-chromium": "^1.5.211",
···
4276
5784
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
4277
5785
"dev": true,
4278
5786
"license": "MIT",
5787
+
"peer": true,
4279
5788
"dependencies": {
4280
5789
"ansi-styles": "^4.1.0",
4281
5790
"supports-color": "^7.1.0"
···
4293
5802
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
4294
5803
"dev": true,
4295
5804
"license": "MIT",
5805
+
"peer": true,
4296
5806
"dependencies": {
4297
5807
"color-convert": "^2.0.1"
4298
5808
},
···
4373
5883
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
4374
5884
"dev": true,
4375
5885
"license": "MIT",
5886
+
"peer": true,
4376
5887
"dependencies": {
4377
5888
"color-name": "~1.1.4"
4378
5889
},
···
4385
5896
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
4386
5897
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
4387
5898
"dev": true,
4388
-
"license": "MIT"
5899
+
"license": "MIT",
5900
+
"peer": true
4389
5901
},
4390
5902
"node_modules/compare-versions": {
4391
5903
"version": "6.1.1",
···
4464
5976
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
4465
5977
"dev": true,
4466
5978
"license": "MIT",
5979
+
"peer": true,
4467
5980
"dependencies": {
4468
5981
"path-key": "^3.1.0",
4469
5982
"shebang-command": "^2.0.0",
···
4491
6004
"version": "3.1.3",
4492
6005
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
4493
6006
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
4494
-
"license": "MIT",
4495
-
"peer": true
6007
+
"license": "MIT"
4496
6008
},
4497
6009
"node_modules/custom-media-element": {
4498
6010
"version": "1.4.5",
···
4635
6147
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
4636
6148
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
4637
6149
"dev": true,
4638
-
"license": "MIT"
6150
+
"license": "MIT",
6151
+
"peer": true
4639
6152
},
4640
6153
"node_modules/define-data-property": {
4641
6154
"version": "1.1.4",
···
4692
6205
"node": ">=8"
4693
6206
}
4694
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
+
},
4695
6213
"node_modules/diff": {
4696
6214
"version": "8.0.2",
4697
6215
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
···
4721
6239
"dev": true,
4722
6240
"license": "MIT"
4723
6241
},
6242
+
"node_modules/dompurify": {
6243
+
"version": "3.3.0",
6244
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
6245
+
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
6246
+
"license": "(MPL-2.0 OR Apache-2.0)",
6247
+
"optionalDependencies": {
6248
+
"@types/trusted-types": "^2.0.7"
6249
+
}
6250
+
},
4724
6251
"node_modules/dot-case": {
4725
6252
"version": "3.0.4",
4726
6253
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
···
5029
6556
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
5030
6557
"dev": true,
5031
6558
"license": "MIT",
6559
+
"peer": true,
5032
6560
"engines": {
5033
6561
"node": ">=10"
5034
6562
},
···
5320
6848
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
5321
6849
"dev": true,
5322
6850
"license": "BSD-2-Clause",
6851
+
"peer": true,
5323
6852
"dependencies": {
5324
6853
"esrecurse": "^4.3.0",
5325
6854
"estraverse": "^5.2.0"
···
5350
6879
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
5351
6880
"dev": true,
5352
6881
"license": "ISC",
6882
+
"peer": true,
5353
6883
"dependencies": {
5354
6884
"is-glob": "^4.0.3"
5355
6885
},
···
5363
6893
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
5364
6894
"dev": true,
5365
6895
"license": "BSD-2-Clause",
6896
+
"peer": true,
5366
6897
"dependencies": {
5367
6898
"acorn": "^8.15.0",
5368
6899
"acorn-jsx": "^5.3.2",
···
5394
6925
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
5395
6926
"dev": true,
5396
6927
"license": "BSD-3-Clause",
6928
+
"peer": true,
5397
6929
"dependencies": {
5398
6930
"estraverse": "^5.1.0"
5399
6931
},
···
5407
6939
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
5408
6940
"dev": true,
5409
6941
"license": "BSD-2-Clause",
6942
+
"peer": true,
5410
6943
"dependencies": {
5411
6944
"estraverse": "^5.2.0"
5412
6945
},
···
5495
7028
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
5496
7029
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
5497
7030
"dev": true,
5498
-
"license": "MIT"
7031
+
"license": "MIT",
7032
+
"peer": true
5499
7033
},
5500
7034
"node_modules/fast-levenshtein": {
5501
7035
"version": "2.0.6",
5502
7036
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
5503
7037
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
5504
7038
"dev": true,
5505
-
"license": "MIT"
7039
+
"license": "MIT",
7040
+
"peer": true
5506
7041
},
5507
7042
"node_modules/fastq": {
5508
7043
"version": "1.19.1",
···
5520
7055
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
5521
7056
"dev": true,
5522
7057
"license": "MIT",
7058
+
"peer": true,
5523
7059
"dependencies": {
5524
7060
"flat-cache": "^4.0.0"
5525
7061
},
···
5545
7081
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
5546
7082
"dev": true,
5547
7083
"license": "MIT",
7084
+
"peer": true,
5548
7085
"dependencies": {
5549
7086
"locate-path": "^6.0.0",
5550
7087
"path-exists": "^4.0.0"
···
5562
7099
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
5563
7100
"dev": true,
5564
7101
"license": "MIT",
7102
+
"peer": true,
5565
7103
"dependencies": {
5566
7104
"flatted": "^3.2.9",
5567
7105
"keyv": "^4.5.4"
···
5575
7113
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
5576
7114
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
5577
7115
"dev": true,
5578
-
"license": "ISC"
7116
+
"license": "ISC",
7117
+
"peer": true
5579
7118
},
5580
7119
"node_modules/for-each": {
5581
7120
"version": "0.3.5",
···
5692
7231
"url": "https://github.com/sponsors/ljharb"
5693
7232
}
5694
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
+
},
5695
7242
"node_modules/get-proto": {
5696
7243
"version": "1.0.1",
5697
7244
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
···
5754
7301
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
5755
7302
"dev": true,
5756
7303
"license": "MIT",
7304
+
"peer": true,
5757
7305
"engines": {
5758
7306
"node": ">=18"
5759
7307
},
···
5831
7379
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
5832
7380
"dev": true,
5833
7381
"license": "MIT",
7382
+
"peer": true,
5834
7383
"engines": {
5835
7384
"node": ">=8"
5836
7385
}
···
6043
7592
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
6044
7593
"dev": true,
6045
7594
"license": "MIT",
7595
+
"peer": true,
6046
7596
"engines": {
6047
7597
"node": ">= 4"
6048
7598
}
···
6085
7635
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
6086
7636
"dev": true,
6087
7637
"license": "MIT",
7638
+
"peer": true,
6088
7639
"engines": {
6089
7640
"node": ">=0.8.19"
6090
7641
}
···
6590
8141
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
6591
8142
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
6592
8143
"dev": true,
6593
-
"license": "ISC"
8144
+
"license": "ISC",
8145
+
"peer": true
6594
8146
},
6595
8147
"node_modules/iso-datestring-validator": {
6596
8148
"version": "2.2.2",
···
6688
8240
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
6689
8241
"dev": true,
6690
8242
"license": "MIT",
6691
-
"peer": true,
6692
8243
"dependencies": {
6693
8244
"cssstyle": "^4.2.1",
6694
8245
"data-urls": "^5.0.0",
···
6740
8291
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
6741
8292
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
6742
8293
"dev": true,
6743
-
"license": "MIT"
8294
+
"license": "MIT",
8295
+
"peer": true
6744
8296
},
6745
8297
"node_modules/json-parse-even-better-errors": {
6746
8298
"version": "2.3.1",
···
6754
8306
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
6755
8307
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
6756
8308
"dev": true,
6757
-
"license": "MIT"
8309
+
"license": "MIT",
8310
+
"peer": true
6758
8311
},
6759
8312
"node_modules/json-stable-stringify-without-jsonify": {
6760
8313
"version": "1.0.1",
6761
8314
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
6762
8315
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
6763
8316
"dev": true,
6764
-
"license": "MIT"
8317
+
"license": "MIT",
8318
+
"peer": true
6765
8319
},
6766
8320
"node_modules/json5": {
6767
8321
"version": "2.2.3",
···
6797
8351
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
6798
8352
"dev": true,
6799
8353
"license": "MIT",
8354
+
"peer": true,
6800
8355
"dependencies": {
6801
8356
"json-buffer": "3.0.1"
6802
8357
}
···
6814
8369
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
6815
8370
"dev": true,
6816
8371
"license": "MIT",
8372
+
"peer": true,
6817
8373
"dependencies": {
6818
8374
"prelude-ls": "^1.2.1",
6819
8375
"type-check": "~0.4.0"
···
7099
8655
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
7100
8656
"dev": true,
7101
8657
"license": "MIT",
8658
+
"peer": true,
7102
8659
"dependencies": {
7103
8660
"p-locate": "^5.0.0"
7104
8661
},
···
7120
8677
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
7121
8678
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
7122
8679
"dev": true,
7123
-
"license": "MIT"
8680
+
"license": "MIT",
8681
+
"peer": true
7124
8682
},
7125
8683
"node_modules/loose-envify": {
7126
8684
"version": "1.4.0",
···
9580
11138
"version": "4.0.3",
9581
11139
"inBundle": true,
9582
11140
"license": "MIT",
9583
-
"peer": true,
9584
11141
"engines": {
9585
11142
"node": ">=12"
9586
11143
},
···
9914
11471
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
9915
11472
"dev": true,
9916
11473
"license": "MIT",
11474
+
"peer": true,
9917
11475
"dependencies": {
9918
11476
"deep-is": "^0.1.3",
9919
11477
"fast-levenshtein": "^2.0.6",
···
9950
11508
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
9951
11509
"dev": true,
9952
11510
"license": "MIT",
11511
+
"peer": true,
9953
11512
"dependencies": {
9954
11513
"yocto-queue": "^0.1.0"
9955
11514
},
···
9966
11525
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
9967
11526
"dev": true,
9968
11527
"license": "MIT",
11528
+
"peer": true,
9969
11529
"dependencies": {
9970
11530
"p-limit": "^3.0.2"
9971
11531
},
···
10040
11600
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
10041
11601
"dev": true,
10042
11602
"license": "MIT",
11603
+
"peer": true,
10043
11604
"engines": {
10044
11605
"node": ">=8"
10045
11606
}
···
10050
11611
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
10051
11612
"dev": true,
10052
11613
"license": "MIT",
11614
+
"peer": true,
10053
11615
"engines": {
10054
11616
"node": ">=8"
10055
11617
}
···
10178
11740
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
10179
11741
"dev": true,
10180
11742
"license": "MIT",
11743
+
"peer": true,
10181
11744
"engines": {
10182
11745
"node": ">= 0.8.0"
10183
11746
}
···
10277
11840
],
10278
11841
"license": "MIT"
10279
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
+
},
10280
11919
"node_modules/react": {
10281
11920
"version": "19.1.1",
10282
11921
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
10283
11922
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
10284
11923
"license": "MIT",
10285
-
"peer": true,
10286
11924
"engines": {
10287
11925
"node": ">=0.10.0"
10288
11926
}
···
10292
11930
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
10293
11931
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
10294
11932
"license": "MIT",
10295
-
"peer": true,
10296
11933
"dependencies": {
10297
11934
"scheduler": "^0.26.0"
10298
11935
},
···
10340
11977
"node": ">=0.10.0"
10341
11978
}
10342
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
+
},
10343
12046
"node_modules/readdirp": {
10344
12047
"version": "3.6.0",
10345
12048
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
···
10647
12350
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
10648
12351
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
10649
12352
"license": "MIT",
10650
-
"peer": true,
10651
12353
"engines": {
10652
12354
"node": ">=10"
10653
12355
}
···
10719
12421
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
10720
12422
"dev": true,
10721
12423
"license": "MIT",
12424
+
"peer": true,
10722
12425
"dependencies": {
10723
12426
"shebang-regex": "^3.0.0"
10724
12427
},
···
10732
12435
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
10733
12436
"dev": true,
10734
12437
"license": "MIT",
12438
+
"peer": true,
10735
12439
"engines": {
10736
12440
"node": ">=8"
10737
12441
}
···
10835
12539
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz",
10836
12540
"integrity": "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==",
10837
12541
"license": "MIT",
10838
-
"peer": true,
10839
12542
"dependencies": {
10840
12543
"csstype": "^3.1.0",
10841
12544
"seroval": "~1.3.0",
···
11005
12708
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
11006
12709
"dev": true,
11007
12710
"license": "MIT",
12711
+
"peer": true,
11008
12712
"engines": {
11009
12713
"node": ">=8"
11010
12714
},
···
11044
12748
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
11045
12749
"dev": true,
11046
12750
"license": "MIT",
12751
+
"peer": true,
11047
12752
"dependencies": {
11048
12753
"has-flag": "^4.0.0"
11049
12754
},
···
11143
12848
"version": "1.3.3",
11144
12849
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
11145
12850
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
11146
-
"license": "MIT",
11147
-
"peer": true
12851
+
"license": "MIT"
11148
12852
},
11149
12853
"node_modules/tiny-warning": {
11150
12854
"version": "1.0.3",
···
11204
12908
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
11205
12909
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
11206
12910
"license": "MIT",
11207
-
"peer": true,
11208
12911
"engines": {
11209
12912
"node": ">=12"
11210
12913
},
···
11402
13105
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
11403
13106
"dev": true,
11404
13107
"license": "MIT",
13108
+
"peer": true,
11405
13109
"dependencies": {
11406
13110
"prelude-ls": "^1.2.1"
11407
13111
},
···
11493
13197
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
11494
13198
"dev": true,
11495
13199
"license": "Apache-2.0",
11496
-
"peer": true,
11497
13200
"bin": {
11498
13201
"tsc": "bin/tsc",
11499
13202
"tsserver": "bin/tsserver"
···
11830
13533
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
11831
13534
"dev": true,
11832
13535
"license": "BSD-2-Clause",
13536
+
"peer": true,
11833
13537
"dependencies": {
11834
13538
"punycode": "^2.1.0"
11835
13539
}
11836
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
+
},
11837
13582
"node_modules/use-sync-external-store": {
11838
13583
"version": "1.5.0",
11839
13584
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
···
11857
13602
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
11858
13603
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
11859
13604
"license": "MIT",
11860
-
"peer": true,
11861
13605
"dependencies": {
11862
13606
"esbuild": "^0.25.0",
11863
13607
"fdir": "^6.4.4",
···
11972
13716
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
11973
13717
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
11974
13718
"license": "MIT",
11975
-
"peer": true,
11976
13719
"engines": {
11977
13720
"node": ">=12"
11978
13721
},
···
12154
13897
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
12155
13898
"dev": true,
12156
13899
"license": "ISC",
13900
+
"peer": true,
12157
13901
"dependencies": {
12158
13902
"isexe": "^2.0.0"
12159
13903
},
···
12285
14029
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
12286
14030
"dev": true,
12287
14031
"license": "MIT",
14032
+
"peer": true,
12288
14033
"engines": {
12289
14034
"node": ">=0.10.0"
12290
14035
}
···
12339
14084
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
12340
14085
"dev": true,
12341
14086
"license": "MIT",
14087
+
"peer": true,
12342
14088
"engines": {
12343
14089
"node": ">=10"
12344
14090
},
···
12357
14103
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
12358
14104
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
12359
14105
"license": "MIT",
12360
-
"peer": true,
12361
14106
"funding": {
12362
14107
"url": "https://github.com/sponsors/colinhacks"
12363
14108
}
+7
package.json
+7
package.json
···
12
12
"dependencies": {
13
13
"@atproto/api": "^0.16.6",
14
14
"@atproto/oauth-client-browser": "^0.3.33",
15
+
"@radix-ui/react-dialog": "^1.1.15",
16
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
17
+
"@radix-ui/react-hover-card": "^1.1.15",
18
+
"@radix-ui/react-slider": "^1.3.6",
15
19
"@tailwindcss/vite": "^4.0.6",
16
20
"@tanstack/query-sync-storage-persister": "^5.85.6",
17
21
"@tanstack/react-devtools": "^0.2.2",
···
20
24
"@tanstack/react-router": "^1.130.2",
21
25
"@tanstack/react-router-devtools": "^1.131.5",
22
26
"@tanstack/router-plugin": "^1.121.2",
27
+
"dompurify": "^3.3.0",
23
28
"i": "^0.3.7",
24
29
"idb-keyval": "^6.2.2",
25
30
"jotai": "^2.13.1",
26
31
"npm": "^11.6.2",
32
+
"radix-ui": "^1.4.3",
27
33
"react": "^19.0.0",
28
34
"react-dom": "^19.0.0",
29
35
"react-player": "^3.3.2",
···
35
41
"@iconify-icon/react": "^3.0.1",
36
42
"@iconify-json/material-symbols": "^1.2.42",
37
43
"@iconify-json/mdi": "^1.2.3",
44
+
"@iconify/json": "^2.2.396",
38
45
"@svgr/core": "^8.1.0",
39
46
"@svgr/plugin-jsx": "^8.1.0",
40
47
"@testing-library/dom": "^10.4.0",
public/screenshot.jpg
public/screenshot.jpg
This is a binary file and will not be displayed.
public/screenshot.png
public/screenshot.png
This is a binary file and will not be displayed.
+13
-1
src/auto-imports.d.ts
+13
-1
src/auto-imports.d.ts
···
6
6
// biome-ignore lint: disable
7
7
export {}
8
8
declare global {
9
-
9
+
const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default
10
+
const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default
11
+
const IconMaterialSymbolsArrowBack: typeof import('~icons/material-symbols/arrow-back.jsx').default
12
+
const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default
13
+
const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default
14
+
const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default
15
+
const IconMaterialSymbolsNotificationsOutline: typeof import('~icons/material-symbols/notifications-outline.jsx').default
16
+
const IconMaterialSymbolsSearch: typeof import('~icons/material-symbols/search.jsx').default
17
+
const IconMaterialSymbolsSettings: typeof import('~icons/material-symbols/settings.jsx').default
18
+
const IconMaterialSymbolsSettingsOutline: typeof import('~icons/material-symbols/settings-outline.jsx').default
19
+
const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default
20
+
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
+
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
10
22
}
+339
src/components/Composer.tsx
+339
src/components/Composer.tsx
···
1
+
import { AppBskyRichtextFacet, RichText } from "@atproto/api";
2
+
import { useAtom } from "jotai";
3
+
import { Dialog } from "radix-ui";
4
+
import { useEffect, useRef, useState } from "react";
5
+
6
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { composerAtom } from "~/utils/atoms";
8
+
import { useQueryPost } from "~/utils/useQuery";
9
+
10
+
import { ProfileThing } from "./Login";
11
+
import { Button } from "./radix-m3-rd/Button";
12
+
import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer";
13
+
14
+
const MAX_POST_LENGTH = 300;
15
+
16
+
export function Composer() {
17
+
const [composerState, setComposerState] = useAtom(composerAtom);
18
+
const [closeConfirmState, setCloseConfirmState] = useState<boolean>(false);
19
+
const { agent } = useAuth();
20
+
21
+
const [postText, setPostText] = useState("");
22
+
const [posting, setPosting] = useState(false);
23
+
const [postSuccess, setPostSuccess] = useState(false);
24
+
const [postError, setPostError] = useState<string | null>(null);
25
+
26
+
useEffect(() => {
27
+
setPostText("");
28
+
setPosting(false);
29
+
setPostSuccess(false);
30
+
setPostError(null);
31
+
}, [composerState.kind]);
32
+
33
+
const parentUri =
34
+
composerState.kind === "reply"
35
+
? composerState.parent
36
+
: composerState.kind === "quote"
37
+
? composerState.subject
38
+
: undefined;
39
+
40
+
const { data: parentPost, isLoading: isParentLoading } =
41
+
useQueryPost(parentUri);
42
+
43
+
async function handlePost() {
44
+
if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return;
45
+
46
+
setPosting(true);
47
+
setPostError(null);
48
+
49
+
try {
50
+
const rt = new RichText({ text: postText });
51
+
await rt.detectFacets(agent);
52
+
53
+
if (rt.facets?.length) {
54
+
rt.facets = rt.facets.filter((item) => {
55
+
if (item.$type !== "app.bsky.richtext.facet") return true;
56
+
if (!item.features?.length) return true;
57
+
58
+
item.features = item.features.filter((feature) => {
59
+
if (feature.$type !== "app.bsky.richtext.facet#mention") return true;
60
+
const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined;
61
+
return typeof did === "string" && did.startsWith("did:");
62
+
});
63
+
64
+
return item.features.length > 0;
65
+
});
66
+
}
67
+
68
+
const record: Record<string, unknown> = {
69
+
$type: "app.bsky.feed.post",
70
+
text: rt.text,
71
+
facets: rt.facets,
72
+
createdAt: new Date().toISOString(),
73
+
};
74
+
75
+
if (composerState.kind === "reply" && parentPost) {
76
+
record.reply = {
77
+
root: parentPost.value?.reply?.root ?? {
78
+
uri: parentPost.uri,
79
+
cid: parentPost.cid,
80
+
},
81
+
parent: {
82
+
uri: parentPost.uri,
83
+
cid: parentPost.cid,
84
+
},
85
+
};
86
+
}
87
+
88
+
if (composerState.kind === "quote" && parentPost) {
89
+
record.embed = {
90
+
$type: "app.bsky.embed.record",
91
+
record: {
92
+
uri: parentPost.uri,
93
+
cid: parentPost.cid,
94
+
},
95
+
};
96
+
}
97
+
98
+
await agent.com.atproto.repo.createRecord({
99
+
collection: "app.bsky.feed.post",
100
+
repo: agent.assertDid,
101
+
record,
102
+
});
103
+
104
+
setPostSuccess(true);
105
+
setPostText("");
106
+
107
+
setTimeout(() => {
108
+
setPostSuccess(false);
109
+
setComposerState({ kind: "closed" });
110
+
}, 1500);
111
+
} catch (e: any) {
112
+
setPostError(e?.message || "Failed to post");
113
+
} finally {
114
+
setPosting(false);
115
+
}
116
+
}
117
+
118
+
const getPlaceholder = () => {
119
+
switch (composerState.kind) {
120
+
case "reply":
121
+
return "Post your reply";
122
+
case "quote":
123
+
return "Add a comment...";
124
+
case "root":
125
+
default:
126
+
return "What's happening?!";
127
+
}
128
+
};
129
+
130
+
const charsLeft = MAX_POST_LENGTH - postText.length;
131
+
const isPostButtonDisabled =
132
+
posting || !postText.trim() || isParentLoading || charsLeft < 0;
133
+
134
+
function handleAttemptClose() {
135
+
if (postText.trim() && !posting) {
136
+
setCloseConfirmState(true);
137
+
} else {
138
+
setComposerState({ kind: "closed" });
139
+
}
140
+
}
141
+
142
+
function handleConfirmClose() {
143
+
setComposerState({ kind: "closed" });
144
+
setCloseConfirmState(false);
145
+
setPostText("");
146
+
}
147
+
148
+
return (
149
+
<>
150
+
<Dialog.Root
151
+
open={composerState.kind !== "closed"}
152
+
onOpenChange={(open) => {
153
+
if (!open) handleAttemptClose();
154
+
}}
155
+
>
156
+
<Dialog.Portal>
157
+
<Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" />
158
+
159
+
<Dialog.Content className="fixed overflow-y-auto gutter inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 pb-[50dvh] sm:pb-[50dvh]">
160
+
<div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4">
161
+
<div className="flex flex-row justify-between p-2">
162
+
<Dialog.Close asChild>
163
+
<button
164
+
className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
165
+
disabled={posting}
166
+
aria-label="Close"
167
+
onClick={handleAttemptClose}
168
+
>
169
+
<svg
170
+
xmlns="http://www.w3.org/2000/svg"
171
+
width="20"
172
+
height="20"
173
+
viewBox="0 0 24 24"
174
+
fill="none"
175
+
stroke="currentColor"
176
+
strokeWidth="2.5"
177
+
strokeLinecap="round"
178
+
strokeLinejoin="round"
179
+
>
180
+
<line x1="18" y1="6" x2="6" y2="18"></line>
181
+
<line x1="6" y1="6" x2="18" y2="18"></line>
182
+
</svg>
183
+
</button>
184
+
</Dialog.Close>
185
+
186
+
<div className="flex-1" />
187
+
<div className="flex items-center gap-4">
188
+
<span
189
+
className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`}
190
+
>
191
+
{charsLeft}
192
+
</span>
193
+
<Button
194
+
onClick={handlePost}
195
+
disabled={isPostButtonDisabled}
196
+
>
197
+
{posting ? "Posting..." : "Post"}
198
+
</Button>
199
+
</div>
200
+
</div>
201
+
202
+
{postSuccess ? (
203
+
<div className="flex flex-col items-center justify-center py-16">
204
+
<span className="text-gray-500 text-6xl mb-4">โ</span>
205
+
<span className="text-xl font-bold text-black dark:text-white">
206
+
Posted!
207
+
</span>
208
+
</div>
209
+
) : (
210
+
<div className="px-4">
211
+
{composerState.kind === "reply" && (
212
+
<div className="mb-1 -mx-4">
213
+
{isParentLoading ? (
214
+
<div className="text-sm text-gray-500 animate-pulse">
215
+
Loading parent post...
216
+
</div>
217
+
) : parentUri ? (
218
+
<UniversalPostRendererATURILoader
219
+
atUri={parentUri}
220
+
bottomReplyLine
221
+
bottomBorder={false}
222
+
/>
223
+
) : (
224
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
225
+
Could not load parent post.
226
+
</div>
227
+
)}
228
+
</div>
229
+
)}
230
+
231
+
<div className="flex w-full gap-1 flex-col">
232
+
<ProfileThing agent={agent} large />
233
+
<div className="flex pl-[50px]">
234
+
<AutoGrowTextarea
235
+
className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2"
236
+
rows={5}
237
+
placeholder={getPlaceholder()}
238
+
value={postText}
239
+
onChange={(e) => setPostText(e.target.value)}
240
+
disabled={posting}
241
+
autoFocus
242
+
/>
243
+
</div>
244
+
</div>
245
+
246
+
{composerState.kind === "quote" && (
247
+
<div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
248
+
{isParentLoading ? (
249
+
<div className="text-sm text-gray-500 animate-pulse">
250
+
Loading parent post...
251
+
</div>
252
+
) : parentUri ? (
253
+
<UniversalPostRendererATURILoader
254
+
atUri={parentUri}
255
+
isQuote
256
+
/>
257
+
) : (
258
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
259
+
Could not load parent post.
260
+
</div>
261
+
)}
262
+
</div>
263
+
)}
264
+
265
+
{postError && (
266
+
<div className="text-red-500 text-sm my-2 text-center">
267
+
{postError}
268
+
</div>
269
+
)}
270
+
</div>
271
+
)}
272
+
</div>
273
+
</Dialog.Content>
274
+
</Dialog.Portal>
275
+
</Dialog.Root>
276
+
277
+
{/* Close confirmation dialog */}
278
+
<Dialog.Root open={closeConfirmState} onOpenChange={setCloseConfirmState}>
279
+
<Dialog.Portal>
280
+
281
+
<Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" />
282
+
283
+
<Dialog.Content className="fixed gutter inset-0 z-50 flex items-start justify-center pt-30 sm:pt-40">
284
+
<div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-md relative mx-4 py-6">
285
+
<div className="text-xl mb-4 text-center">
286
+
Discard your post?
287
+
</div>
288
+
<div className="text-md mb-4 text-center">
289
+
You will lose your draft
290
+
</div>
291
+
<div className="flex justify-end gap-2 px-6">
292
+
<Button
293
+
onClick={handleConfirmClose}
294
+
>
295
+
Discard
296
+
</Button>
297
+
<Button
298
+
variant={"outlined"}
299
+
onClick={() => setCloseConfirmState(false)}
300
+
>
301
+
Cancel
302
+
</Button>
303
+
</div>
304
+
</div>
305
+
</Dialog.Content>
306
+
</Dialog.Portal>
307
+
</Dialog.Root>
308
+
</>
309
+
);
310
+
}
311
+
312
+
function AutoGrowTextarea({
313
+
value,
314
+
className,
315
+
onChange,
316
+
...props
317
+
}: React.DetailedHTMLProps<
318
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
319
+
HTMLTextAreaElement
320
+
>) {
321
+
const ref = useRef<HTMLTextAreaElement>(null);
322
+
323
+
useEffect(() => {
324
+
const el = ref.current;
325
+
if (!el) return;
326
+
el.style.height = "auto";
327
+
el.style.height = el.scrollHeight + "px";
328
+
}, [value]);
329
+
330
+
return (
331
+
<textarea
332
+
ref={ref}
333
+
className={className}
334
+
value={value}
335
+
onChange={onChange}
336
+
{...props}
337
+
/>
338
+
);
339
+
}
+33
src/components/Header.tsx
+33
src/components/Header.tsx
···
1
+
import { Link, useRouter } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
+
4
+
import { isAtTopAtom } from "~/utils/atoms";
5
+
6
+
export function Header({
7
+
backButtonCallback,
8
+
title
9
+
}: {
10
+
backButtonCallback?: () => void;
11
+
title?: string;
12
+
}) {
13
+
const router = useRouter();
14
+
const [isAtTop] = useAtom(isAtTopAtom);
15
+
//const what = router.history.
16
+
return (
17
+
<div className={`flex items-center gap-3 px-3 py-3 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`}>
18
+
{backButtonCallback ? (<Link
19
+
to=".."
20
+
//className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
21
+
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
22
+
onClick={(e) => {
23
+
e.preventDefault();
24
+
backButtonCallback();
25
+
}}
26
+
aria-label="Go back"
27
+
>
28
+
<IconMaterialSymbolsArrowBack className="w-6 h-6" />
29
+
</Link>) : (<div className="w-[0px]" />)}
30
+
<span className="text-[21px] sm:text-[19px] sm:font-semibold font-roboto">{title}</span>
31
+
</div>
32
+
);
33
+
}
+150
src/components/Import.tsx
+150
src/components/Import.tsx
···
1
+
import { AtUri } from "@atproto/api";
2
+
import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3
+
import { useState } from "react";
4
+
5
+
/**
6
+
* Basically the best equivalent to Search that i can do
7
+
*/
8
+
export function Import() {
9
+
const [textInput, setTextInput] = useState<string | undefined>();
10
+
const navigate = useNavigate();
11
+
12
+
const handleEnter = () => {
13
+
if (!textInput) return;
14
+
handleImport({
15
+
text: textInput,
16
+
navigate,
17
+
});
18
+
};
19
+
20
+
return (
21
+
<div className="w-full relative">
22
+
<IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
23
+
24
+
<input
25
+
type="text"
26
+
placeholder="Import..."
27
+
value={textInput}
28
+
onChange={(e) => setTextInput(e.target.value)}
29
+
onKeyDown={(e) => {
30
+
if (e.key === "Enter") handleEnter();
31
+
}}
32
+
className="w-full h-12 pl-12 pr-4 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-500 box-border transition"
33
+
/>
34
+
</div>
35
+
);
36
+
}
37
+
38
+
function handleImport({
39
+
text,
40
+
navigate,
41
+
}: {
42
+
text: string;
43
+
navigate: UseNavigateResult<string>;
44
+
}) {
45
+
const trimmed = text.trim();
46
+
// parse text
47
+
/**
48
+
* text might be
49
+
* 1. bsky dot app url (reddwarf link segments might be uri encoded,)
50
+
* 2. aturi
51
+
* 3. plain handle
52
+
* 4. plain did
53
+
*/
54
+
55
+
// 1. Check if itโs a URL
56
+
try {
57
+
const url = new URL(text);
58
+
const knownHosts = [
59
+
"bsky.app",
60
+
"social.daniela.lol",
61
+
"deer.social",
62
+
"reddwarf.whey.party",
63
+
"reddwarf.app",
64
+
"main.bsky.dev",
65
+
"catsky.social",
66
+
"blacksky.community",
67
+
"red-dwarf-social-app.whey.party",
68
+
"zeppelin.social",
69
+
];
70
+
if (knownHosts.includes(url.hostname)) {
71
+
// parse path to get URI or handle
72
+
const path = decodeURIComponent(url.pathname.slice(1)); // remove leading /
73
+
console.log("BSky URL path:", path);
74
+
navigate({
75
+
to: `/${path}`,
76
+
});
77
+
return;
78
+
}
79
+
} catch {
80
+
// not a URL, continue
81
+
}
82
+
83
+
// 2. Check if text looks like an at-uri
84
+
try {
85
+
if (text.startsWith("at://")) {
86
+
console.log("AT URI detected:", text);
87
+
const aturi = new AtUri(text);
88
+
switch (aturi.collection) {
89
+
case "app.bsky.feed.post": {
90
+
navigate({
91
+
to: "/profile/$did/post/$rkey",
92
+
params: {
93
+
did: aturi.host,
94
+
rkey: aturi.rkey,
95
+
},
96
+
});
97
+
return;
98
+
}
99
+
case "app.bsky.actor.profile": {
100
+
navigate({
101
+
to: "/profile/$did",
102
+
params: {
103
+
did: aturi.host,
104
+
},
105
+
});
106
+
return;
107
+
}
108
+
// todo add more handlers as more routes are added. like feeds, lists, etc etc thanks!
109
+
default: {
110
+
// continue
111
+
}
112
+
}
113
+
}
114
+
} catch {
115
+
// continue
116
+
}
117
+
118
+
// 3. Plain handle (starts with @)
119
+
try {
120
+
if (text.startsWith("@")) {
121
+
const handle = text.slice(1);
122
+
console.log("Handle detected:", handle);
123
+
navigate({ to: "/profile/$did", params: { did: handle } });
124
+
return;
125
+
}
126
+
} catch {
127
+
// continue
128
+
}
129
+
130
+
// 4. Plain DID (starts with did:)
131
+
try {
132
+
if (text.startsWith("did:")) {
133
+
console.log("did detected:", text);
134
+
navigate({ to: "/profile/$did", params: { did: text } });
135
+
return;
136
+
}
137
+
} catch {
138
+
// continue
139
+
}
140
+
141
+
// if all else fails
142
+
143
+
// try {
144
+
// // probably a user?
145
+
// navigate({ to: "/profile/$did", params: { did: text } });
146
+
// return;
147
+
// } catch {
148
+
// // continue
149
+
// }
150
+
}
+36
-9
src/components/InfiniteCustomFeed.tsx
+36
-9
src/components/InfiniteCustomFeed.tsx
···
1
+
import { useQueryClient } from "@tanstack/react-query";
1
2
import * as React from "react";
3
+
2
4
//import { useInView } from "react-intersection-observer";
3
5
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
7
import {
6
-
useQueryArbitrary,
7
-
useQueryIdentity,
8
8
useInfiniteQueryFeedSkeleton,
9
+
// useQueryArbitrary,
10
+
// useQueryIdentity,
9
11
} from "~/utils/useQuery";
10
12
11
13
interface InfiniteCustomFeedProps {
···
36
38
isFetchingNextPage,
37
39
refetch,
38
40
isRefetching,
41
+
queryKey,
39
42
} = useInfiniteQueryFeedSkeleton({
40
43
feedUri: feedUri,
41
44
agent: agent ?? undefined,
···
43
46
pdsUrl: pdsUrl,
44
47
feedServiceDid: feedServiceDid,
45
48
});
49
+
const queryClient = useQueryClient();
50
+
46
51
47
52
const handleRefresh = () => {
53
+
queryClient.removeQueries({queryKey: queryKey});
54
+
//queryClient.invalidateQueries(["infinite-feed", feedUri] as const);
48
55
refetch();
49
56
};
50
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
+
51
76
//const { ref, inView } = useInView();
52
77
53
78
// React.useEffect(() => {
···
66
91
);
67
92
}
68
93
69
-
const allPosts =
70
-
data?.pages.flatMap((page) => {
71
-
if (page) return page.feed;
72
-
}) ?? [];
94
+
// const allPosts =
95
+
// data?.pages.flatMap((page) => {
96
+
// if (page) return page.feed;
97
+
// }) ?? [];
73
98
74
99
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
75
100
return (
···
112
137
<button
113
138
onClick={handleRefresh}
114
139
disabled={isRefetching}
115
-
className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
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"
116
141
aria-label="Refresh feed"
117
142
>
118
-
{isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />}
143
+
<RefreshIcon
144
+
className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`}
145
+
/>
119
146
</button>
120
147
</>
121
148
);
···
138
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"
139
166
></path>
140
167
</svg>
141
-
);
168
+
);
+192
-60
src/components/Login.tsx
+192
-60
src/components/Login.tsx
···
1
1
// src/components/Login.tsx
2
-
import React, { useEffect, useState, useRef } from "react";
2
+
import AtpAgent, { Agent } from "@atproto/api";
3
+
import { useAtom } from "jotai";
4
+
import React, { useEffect, useRef, useState } from "react";
5
+
3
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
4
-
import { Agent } from "@atproto/api";
7
+
import { imgCDNAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery";
9
+
10
+
import { Button } from "./radix-m3-rd/Button";
5
11
6
12
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
7
-
export default function Login({ compact = false }: { compact?: boolean }) {
13
+
export default function Login({
14
+
compact = false,
15
+
popup = false,
16
+
}: {
17
+
compact?: boolean;
18
+
popup?: boolean;
19
+
}) {
8
20
const { status, agent, logout } = useAuth();
9
21
10
22
// Loading state can be styled differently based on the prop
···
14
26
className={
15
27
compact
16
28
? "flex items-center justify-center p-1"
17
-
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4 flex justify-center items-center h-[280px]"
29
+
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-4 mx-4 flex justify-center items-center h-[280px]"
18
30
}
19
31
>
20
32
<span
···
33
45
// Large view
34
46
if (!compact) {
35
47
return (
36
-
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
48
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
37
49
<div className="flex flex-col items-center justify-center text-center">
38
50
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
39
51
You are logged in!
40
52
</p>
41
53
<ProfileThing agent={agent} large />
42
-
<button
54
+
<Button
43
55
onClick={logout}
44
-
className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors"
56
+
className="mt-4"
45
57
>
46
58
Log out
47
-
</button>
59
+
</Button>
48
60
</div>
49
61
</div>
50
62
);
···
67
79
if (!compact) {
68
80
// Large view renders the form directly in the card
69
81
return (
70
-
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
82
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
71
83
<UnifiedLoginForm />
72
84
</div>
73
85
);
74
86
}
75
87
76
88
// Compact view renders a button that toggles the form in a dropdown
77
-
return <CompactLoginButton />;
89
+
return <CompactLoginButton popup={popup} />;
78
90
}
79
91
80
92
// --- 2. The Reusable, Self-Contained Login Form Component ---
···
83
95
84
96
return (
85
97
<div>
86
-
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-4">
98
+
<div className="flex bg-gray-300 rounded-full dark:bg-gray-700 mb-4">
87
99
<TabButton
88
100
label="OAuth"
89
101
active={mode === "oauth"}
···
103
115
// --- 3. Helper components for layouts, forms, and UI ---
104
116
105
117
// A new component to contain the logic for the compact dropdown
106
-
const CompactLoginButton = () => {
118
+
const CompactLoginButton = ({ popup }: { popup?: boolean }) => {
107
119
const [showForm, setShowForm] = useState(false);
108
120
const formRef = useRef<HTMLDivElement>(null);
109
121
···
125
137
<div className="relative" ref={formRef}>
126
138
<button
127
139
onClick={() => setShowForm(!showForm)}
128
-
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
140
+
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors"
129
141
>
130
142
Log in
131
143
</button>
132
144
{showForm && (
133
-
<div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
145
+
<div
146
+
className={`absolute ${popup ? `bottom-[calc(100%)]` : `top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`}
147
+
>
134
148
<UnifiedLoginForm />
135
149
</div>
136
150
)}
···
138
152
);
139
153
};
140
154
141
-
const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => (
155
+
const TabButton = ({
156
+
label,
157
+
active,
158
+
onClick,
159
+
}: {
160
+
label: string;
161
+
active: boolean;
162
+
onClick: () => void;
163
+
}) => (
142
164
<button
143
165
onClick={onClick}
144
-
className={`px-4 py-2 text-sm font-medium transition-colors ${
166
+
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
145
167
active
146
-
? "text-gray-600 dark:text-gray-200 border-b-2 border-gray-500"
147
-
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
168
+
? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
169
+
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
148
170
}`}
149
171
>
150
172
{label}
···
169
191
};
170
192
return (
171
193
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
172
-
<p className="text-xs text-gray-500 dark:text-gray-400">Sign in with AT. Your password is never shared.</p>
173
-
<input type="text" placeholder="handle.bsky.social" value={handle} onChange={(e) => setHandle(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" />
174
-
<button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button>
194
+
<p className="text-xs text-gray-500 dark:text-gray-400">
195
+
Sign in with AT. Your password is never shared.
196
+
</p>
197
+
{/* <input
198
+
type="text"
199
+
placeholder="handle.bsky.social"
200
+
value={handle}
201
+
onChange={(e) => setHandle(e.target.value)}
202
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
203
+
/> */}
204
+
<div className="flex flex-col gap-3">
205
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
206
+
<input
207
+
type="text"
208
+
placeholder=" "
209
+
value={handle}
210
+
onChange={(e) => setHandle(e.target.value)}
211
+
/>
212
+
<label>AT Handle</label>
213
+
</div>
214
+
<button
215
+
type="submit"
216
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
217
+
>
218
+
Log in
219
+
</button>
220
+
</div>
175
221
</form>
176
222
);
177
223
};
···
201
247
202
248
return (
203
249
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
204
-
<p className="text-xs text-red-500 dark:text-red-400">Warning: Less secure. Use an App Password.</p>
205
-
<input type="text" placeholder="handle.bsky.social" value={user} onChange={(e) => setUser(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="username" />
206
-
<input type="password" placeholder="App Password" value={password} onChange={(e) => setPassword(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="current-password" />
207
-
<input type="text" placeholder="PDS (e.g., bsky.social)" value={serviceURL} onChange={(e) => setServiceURL(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" />
250
+
<p className="text-xs text-red-500 dark:text-red-400">
251
+
Warning: Less secure. Use an App Password.
252
+
</p>
253
+
{/* <input
254
+
type="text"
255
+
placeholder="handle.bsky.social"
256
+
value={user}
257
+
onChange={(e) => setUser(e.target.value)}
258
+
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"
259
+
autoComplete="username"
260
+
/>
261
+
<input
262
+
type="password"
263
+
placeholder="App Password"
264
+
value={password}
265
+
onChange={(e) => setPassword(e.target.value)}
266
+
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"
267
+
autoComplete="current-password"
268
+
/>
269
+
<input
270
+
type="text"
271
+
placeholder="PDS (e.g., bsky.social)"
272
+
value={serviceURL}
273
+
onChange={(e) => setServiceURL(e.target.value)}
274
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
275
+
/> */}
276
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
277
+
<input
278
+
type="text"
279
+
placeholder=" "
280
+
value={user}
281
+
onChange={(e) => setUser(e.target.value)}
282
+
/>
283
+
<label>AT Handle</label>
284
+
</div>
285
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
286
+
<input
287
+
type="text"
288
+
placeholder=" "
289
+
value={password}
290
+
onChange={(e) => setPassword(e.target.value)}
291
+
/>
292
+
<label>App Password</label>
293
+
</div>
294
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
295
+
<input
296
+
type="text"
297
+
placeholder=" "
298
+
value={serviceURL}
299
+
onChange={(e) => setServiceURL(e.target.value)}
300
+
/>
301
+
<label>PDS</label>
302
+
</div>
208
303
{error && <p className="text-xs text-red-500">{error}</p>}
209
-
<button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button>
304
+
<button
305
+
type="submit"
306
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
307
+
>
308
+
Log in
309
+
</button>
210
310
</form>
211
311
);
212
312
};
213
313
214
314
// --- Profile Component (now supports a `large` prop for styling) ---
215
-
export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => {
216
-
const [profile, setProfile] = useState<any>(null);
315
+
export const ProfileThing = ({
316
+
agent,
317
+
large = false,
318
+
}: {
319
+
agent: Agent | null;
320
+
large?: boolean;
321
+
}) => {
322
+
const did = ((agent as AtpAgent)?.session?.did ??
323
+
(agent as AtpAgent)?.assertDid ??
324
+
agent?.did) as string | undefined;
325
+
const { data: identity } = useQueryIdentity(did);
326
+
const { data: profiledata } = useQueryProfile(
327
+
`at://${did}/app.bsky.actor.profile/self`
328
+
);
329
+
const profile = profiledata?.value;
217
330
218
-
useEffect(() => {
219
-
const fetchUser = async () => {
220
-
const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
221
-
if (!did) return;
222
-
try {
223
-
const res = await agent!.getProfile({ actor: did });
224
-
setProfile(res.data);
225
-
} catch (e) { console.error("Failed to fetch profile", e); }
226
-
};
227
-
if (agent) fetchUser();
228
-
}, [agent]);
331
+
const [imgcdn] = useAtom(imgCDNAtom)
229
332
230
-
if (!profile) {
231
-
return ( // Skeleton loader
232
-
<div className={`flex items-center gap-2.5 animate-pulse ${large ? 'mb-1' : ''}`}>
233
-
<div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
234
-
<div className="flex flex-col gap-2">
235
-
<div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-28' : 'h-3 w-20'}`} />
236
-
<div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-20' : 'h-3 w-16'}`} />
237
-
</div>
238
-
</div>
239
-
);
240
-
}
241
-
242
-
return (
243
-
<div className={`flex flex-row items-center gap-2.5 ${large ? 'mb-1' : ''}`}>
244
-
<img src={profile?.avatar} alt="avatar" className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
245
-
<div className="flex flex-col items-start text-left">
246
-
<div className={`font-medium ${large ? 'text-gray-800 dark:text-gray-100 text-md' : 'text-gray-800 dark:text-gray-100 text-sm'}`}>{profile?.displayName}</div>
247
-
<div className={` ${large ? 'text-gray-500 dark:text-gray-400 text-sm' : 'text-gray-500 dark:text-gray-400 text-xs'}`}>@{profile?.handle}</div>
248
-
</div>
333
+
function getAvatarUrl(p: typeof profile) {
334
+
const link = p?.avatar?.ref?.["$link"];
335
+
if (!link || !did) return null;
336
+
return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`;
337
+
}
338
+
339
+
if (!profiledata) {
340
+
return (
341
+
// Skeleton loader
342
+
<div
343
+
className={`flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`}
344
+
>
345
+
<div
346
+
className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
347
+
/>
348
+
<div className="flex flex-col gap-2">
349
+
<div
350
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`}
351
+
/>
352
+
<div
353
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`}
354
+
/>
355
+
</div>
356
+
</div>
357
+
);
358
+
}
359
+
360
+
return (
361
+
<div
362
+
className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`}
363
+
>
364
+
<img
365
+
src={getAvatarUrl(profile) ?? undefined}
366
+
alt="avatar"
367
+
className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
368
+
/>
369
+
<div className="flex flex-col items-start text-left">
370
+
<div
371
+
className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`}
372
+
>
373
+
{profile?.displayName}
374
+
</div>
375
+
<div
376
+
className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`}
377
+
>
378
+
@{identity?.handle}
249
379
</div>
250
-
);
251
-
};
380
+
</div>
381
+
</div>
382
+
);
383
+
};
+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
+
}
+477
-230
src/components/UniversalPostRenderer.tsx
+477
-230
src/components/UniversalPostRenderer.tsx
···
1
1
import { useNavigate } from "@tanstack/react-router";
2
+
import DOMPurify from "dompurify";
2
3
import { useAtom } from "jotai";
4
+
import { DropdownMenu } from "radix-ui";
5
+
import { HoverCard } from "radix-ui";
3
6
import * as React from "react";
4
7
import { type SVGProps } from "react";
5
-
import { createPortal } from "react-dom";
6
8
7
-
import { ProfilePostComponent } from "~/routes/profile.$did/post.$rkey";
8
-
import { likedPostsAtom } from "~/utils/atoms";
9
+
import {
10
+
composerAtom,
11
+
constellationURLAtom,
12
+
imgCDNAtom,
13
+
likedPostsAtom,
14
+
} from "~/utils/atoms";
9
15
import { useHydratedEmbed } from "~/utils/useHydrated";
10
16
import {
11
17
useQueryConstellation,
12
18
useQueryIdentity,
13
19
useQueryPost,
14
20
useQueryProfile,
21
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
15
22
} from "~/utils/useQuery";
16
23
17
24
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
···
34
41
ref?: React.Ref<HTMLDivElement>;
35
42
dataIndexPropPass?: number;
36
43
nopics?: boolean;
44
+
lightboxCallback?: (d: LightboxProps) => void;
45
+
maxReplies?: number;
46
+
isQuote?: boolean;
37
47
}
38
48
39
49
// export async function cachedGetRecord({
···
142
152
ref,
143
153
dataIndexPropPass,
144
154
nopics,
155
+
lightboxCallback,
156
+
maxReplies,
157
+
isQuote,
145
158
}: UniversalPostRendererATURILoaderProps) {
159
+
// todo remove this once tree rendering is implemented, use a prop like isTree
160
+
const TEMPLINEAR = true;
146
161
// /*mass comment*/ console.log("atUri", atUri);
147
162
//const { get, set } = usePersistentStore();
148
163
//const [record, setRecord] = React.useState<any>(null);
···
387
402
);
388
403
}, [links]);
389
404
405
+
// const { data: repliesData } = useQueryConstellation({
406
+
// method: "/links",
407
+
// target: atUri,
408
+
// collection: "app.bsky.feed.post",
409
+
// path: ".reply.parent.uri",
410
+
// });
411
+
412
+
const [constellationurl] = useAtom(constellationURLAtom);
413
+
414
+
const infinitequeryresults = useInfiniteQuery({
415
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
416
+
{
417
+
constellation: constellationurl,
418
+
method: "/links",
419
+
target: atUri,
420
+
collection: "app.bsky.feed.post",
421
+
path: ".reply.parent.uri",
422
+
}
423
+
),
424
+
enabled: !!atUri && !!maxReplies && !isQuote,
425
+
});
426
+
427
+
const {
428
+
data: repliesData,
429
+
// fetchNextPage,
430
+
// hasNextPage,
431
+
// isFetchingNextPage,
432
+
} = infinitequeryresults;
433
+
434
+
// auto-fetch all pages
435
+
useEffect(() => {
436
+
if (!maxReplies || isQuote || TEMPLINEAR) return;
437
+
if (
438
+
infinitequeryresults.hasNextPage &&
439
+
!infinitequeryresults.isFetchingNextPage
440
+
) {
441
+
console.log("Fetching the next page...");
442
+
infinitequeryresults.fetchNextPage();
443
+
}
444
+
}, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]);
445
+
446
+
const replyAturis = repliesData
447
+
? repliesData.pages.flatMap((page) =>
448
+
page
449
+
? page.linking_records.map((record) => {
450
+
const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
451
+
return aturi;
452
+
})
453
+
: []
454
+
)
455
+
: [];
456
+
457
+
//const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined);
458
+
459
+
const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => {
460
+
if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies)
461
+
return {
462
+
oldestOpsReply: undefined,
463
+
oldestOpsReplyElseNewestNonOpsReply: undefined,
464
+
};
465
+
466
+
const opdid = new AtUri(
467
+
//postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri
468
+
atUri
469
+
).host;
470
+
471
+
const opReplies = replyAturis.filter(
472
+
(aturi) => new AtUri(aturi).host === opdid
473
+
);
474
+
475
+
if (opReplies.length > 0) {
476
+
const opreply = opReplies[opReplies.length - 1];
477
+
//setOldestOpsReply(opreply);
478
+
return {
479
+
oldestOpsReply: opreply,
480
+
oldestOpsReplyElseNewestNonOpsReply: opreply,
481
+
};
482
+
} else {
483
+
return {
484
+
oldestOpsReply: undefined,
485
+
oldestOpsReplyElseNewestNonOpsReply: replyAturis[0],
486
+
};
487
+
}
488
+
})();
489
+
390
490
// const navigateToProfile = (e: React.MouseEvent) => {
391
491
// e.stopPropagation();
392
492
// if (resolved?.did) {
···
402
502
}
403
503
404
504
return (
405
-
<UniversalPostRendererRawRecordShim
406
-
detailed={detailed}
407
-
postRecord={postQuery}
408
-
profileRecord={opProfile}
409
-
aturi={atUri}
410
-
resolved={resolved}
411
-
likesCount={likes}
412
-
repostsCount={reposts}
413
-
repliesCount={replies}
414
-
bottomReplyLine={bottomReplyLine}
415
-
topReplyLine={topReplyLine}
416
-
bottomBorder={bottomBorder}
417
-
feedviewpost={feedviewpost}
418
-
repostedby={repostedby}
419
-
style={style}
420
-
ref={ref}
421
-
dataIndexPropPass={dataIndexPropPass}
422
-
nopics={nopics}
423
-
/>
505
+
<>
506
+
{/* <span>uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"}</span> */}
507
+
<UniversalPostRendererRawRecordShim
508
+
detailed={detailed}
509
+
postRecord={postQuery}
510
+
profileRecord={opProfile}
511
+
aturi={atUri}
512
+
resolved={resolved}
513
+
likesCount={likes}
514
+
repostsCount={reposts}
515
+
repliesCount={replies}
516
+
bottomReplyLine={
517
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
518
+
? true
519
+
: maxReplies && !oldestOpsReplyElseNewestNonOpsReply
520
+
? false
521
+
: (maxReplies === 0 && (!replies || (!!replies && replies === 0))) ? false : bottomReplyLine
522
+
}
523
+
topReplyLine={topReplyLine}
524
+
//bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder}
525
+
bottomBorder={
526
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
527
+
? false
528
+
: maxReplies === 0
529
+
? false
530
+
: bottomBorder
531
+
}
532
+
feedviewpost={feedviewpost}
533
+
repostedby={repostedby}
534
+
//style={{...style, background: oldestOpsReply === atUri ? "Red" : undefined}}
535
+
style={style}
536
+
ref={ref}
537
+
dataIndexPropPass={dataIndexPropPass}
538
+
nopics={nopics}
539
+
lightboxCallback={lightboxCallback}
540
+
maxReplies={maxReplies}
541
+
isQuote={isQuote}
542
+
/>
543
+
<>
544
+
{(maxReplies && maxReplies === 0 && replies && replies > 0) ? (
545
+
<>
546
+
{/* <div>hello</div> */}
547
+
<MoreReplies atUri={atUri} />
548
+
</>
549
+
) : (<></>)}
550
+
</>
551
+
{!isQuote && oldestOpsReplyElseNewestNonOpsReply && (
552
+
<>
553
+
{/* <span>hello {maxReplies}</span> */}
554
+
<UniversalPostRendererATURILoader
555
+
//detailed={detailed}
556
+
atUri={oldestOpsReplyElseNewestNonOpsReply}
557
+
bottomReplyLine={(maxReplies ?? 0) > 0}
558
+
topReplyLine={
559
+
(!!(maxReplies && maxReplies - 1 === 0) &&
560
+
!!(replies && replies > 0)) ||
561
+
!!((maxReplies ?? 0) > 1)
562
+
}
563
+
bottomBorder={bottomBorder}
564
+
feedviewpost={feedviewpost}
565
+
repostedby={repostedby}
566
+
style={style}
567
+
ref={ref}
568
+
dataIndexPropPass={dataIndexPropPass}
569
+
nopics={nopics}
570
+
lightboxCallback={lightboxCallback}
571
+
maxReplies={
572
+
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
573
+
}
574
+
/>
575
+
</>
576
+
)}
577
+
</>
424
578
);
425
579
}
426
580
427
-
function getAvatarUrl(opProfile: any, did: string) {
581
+
function MoreReplies({ atUri }: { atUri: string }) {
582
+
const navigate = useNavigate();
583
+
const aturio = new AtUri(atUri);
584
+
return (
585
+
<div
586
+
onClick={() =>
587
+
navigate({
588
+
to: "/profile/$did/post/$rkey",
589
+
params: { did: aturio.host, rkey: aturio.rkey },
590
+
})
591
+
}
592
+
className="border-b border-gray-300 dark:border-gray-800 flex flex-row px-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
593
+
>
594
+
<div className="w-[42px] h-12 flex flex-col items-center justify-center">
595
+
<div
596
+
style={{
597
+
width: 2,
598
+
height: "100%",
599
+
backgroundImage:
600
+
"repeating-linear-gradient(to bottom, var(--color-gray-500) 0, var(--color-gray-500) 4px, transparent 4px, transparent 8px)",
601
+
opacity: 0.5,
602
+
}}
603
+
className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]"
604
+
//className="border-gray-400 dark:border-gray-500"
605
+
/>
606
+
</div>
607
+
608
+
<div className="flex items-center pl-3 text-sm text-gray-500 dark:text-gray-400 select-none">
609
+
More Replies
610
+
</div>
611
+
</div>
612
+
);
613
+
}
614
+
615
+
function getAvatarUrl(opProfile: any, did: string, cdn: string) {
428
616
const link = opProfile?.value?.avatar?.ref?.["$link"];
429
617
if (!link) return null;
430
-
return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`;
618
+
return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`;
431
619
}
432
620
433
621
export function UniversalPostRendererRawRecordShim({
···
448
636
ref,
449
637
dataIndexPropPass,
450
638
nopics,
639
+
lightboxCallback,
640
+
maxReplies,
641
+
isQuote,
451
642
}: {
452
643
postRecord: any;
453
644
profileRecord: any;
···
466
657
ref?: React.Ref<HTMLDivElement>;
467
658
dataIndexPropPass?: number;
468
659
nopics?: boolean;
660
+
lightboxCallback?: (d: LightboxProps) => void;
661
+
maxReplies?: number;
662
+
isQuote?: boolean;
469
663
}) {
470
664
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
471
665
const navigate = useNavigate();
···
541
735
isLoading: isEmbedLoading,
542
736
error: embedError,
543
737
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
738
+
739
+
const [imgcdn] = useAtom(imgCDNAtom);
544
740
545
741
const parsedaturi = new AtUri(aturi); //parseAtUri(aturi);
546
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
+
547
766
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
548
767
() => ({
549
768
$type: "app.bsky.feed.defs#postView",
550
769
uri: aturi,
551
770
cid: postRecord?.cid || "",
552
-
author: {
553
-
did: resolved?.did || "",
554
-
handle: resolved?.handle || "",
555
-
displayName: profileRecord?.value?.displayName || "",
556
-
avatar: getAvatarUrl(profileRecord, resolved?.did) || "",
557
-
viewer: undefined,
558
-
labels: profileRecord?.labels || undefined,
559
-
verification: undefined,
560
-
},
771
+
author: fakeprofileviewbasic,
561
772
record: postRecord?.value || {},
562
773
embed: hydratedEmbed ?? undefined,
563
774
replyCount: repliesCount ?? 0,
···
574
785
postRecord?.cid,
575
786
postRecord?.value,
576
787
postRecord?.labels,
577
-
resolved?.did,
578
-
resolved?.handle,
579
-
profileRecord,
788
+
fakeprofileviewbasic,
580
789
hydratedEmbed,
581
790
repliesCount,
582
791
repostsCount,
···
653
862
}
654
863
}}
655
864
post={fakepost}
865
+
uprrrsauthor={fakeprofileviewdetailed}
656
866
salt={aturi}
657
867
bottomReplyLine={bottomReplyLine}
658
868
topReplyLine={topReplyLine}
···
664
874
ref={ref}
665
875
dataIndexPropPass={dataIndexPropPass}
666
876
nopics={nopics}
877
+
lightboxCallback={lightboxCallback}
878
+
maxReplies={maxReplies}
879
+
isQuote={isQuote}
667
880
/>
668
881
</>
669
882
);
···
702
915
{...props}
703
916
>
704
917
<path
705
-
fill="oklch(0.704 0.05 28)"
918
+
fill="var(--color-gray-400)"
706
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"
707
920
></path>
708
921
</svg>
···
719
932
{...props}
720
933
>
721
934
<path
722
-
fill="oklch(0.704 0.05 28)"
935
+
fill="var(--color-gray-400)"
723
936
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
724
937
></path>
725
938
</svg>
···
770
983
{...props}
771
984
>
772
985
<path
773
-
fill="oklch(0.704 0.05 28)"
986
+
fill="var(--color-gray-400)"
774
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"
775
988
></path>
776
989
</svg>
···
787
1000
{...props}
788
1001
>
789
1002
<path
790
-
fill="oklch(0.704 0.05 28)"
1003
+
fill="var(--color-gray-400)"
791
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"
792
1005
></path>
793
1006
</svg>
···
804
1017
{...props}
805
1018
>
806
1019
<path
807
-
fill="oklch(0.704 0.05 28)"
1020
+
fill="var(--color-gray-400)"
808
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"
809
1022
></path>
810
1023
</svg>
···
821
1034
{...props}
822
1035
>
823
1036
<path
824
-
fill="oklch(0.704 0.05 28)"
1037
+
fill="var(--color-gray-400)"
825
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"
826
1039
></path>
827
1040
</svg>
···
855
1068
{...props}
856
1069
>
857
1070
<path
858
-
fill="oklch(0.704 0.05 28)"
1071
+
fill="var(--color-gray-400)"
859
1072
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
860
1073
></path>
861
1074
</svg>
···
909
1122
{...props}
910
1123
>
911
1124
<path
912
-
fill="oklch(0.704 0.05 28)"
1125
+
fill="var(--color-gray-400)"
913
1126
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
914
1127
></path>
915
1128
</svg>
···
926
1139
{...props}
927
1140
>
928
1141
<path
929
-
fill="oklch(0.704 0.05 28)"
1142
+
fill="var(--color-gray-400)"
930
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"
931
1144
></path>
932
1145
</svg>
···
954
1167
//import Masonry from "@mui/lab/Masonry";
955
1168
import {
956
1169
type $Typed,
1170
+
AppBskyActorDefs,
957
1171
AppBskyEmbedDefs,
958
1172
AppBskyEmbedExternal,
959
1173
AppBskyEmbedImages,
···
977
1191
PostView,
978
1192
//ThreadViewPost,
979
1193
} from "@atproto/api/dist/client/types/app/bsky/feed/defs";
1194
+
import { useInfiniteQuery } from "@tanstack/react-query";
980
1195
import { useEffect, useRef, useState } from "react";
981
1196
import ReactPlayer from "react-player";
982
1197
983
1198
import defaultpfp from "~/../public/favicon.png";
984
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";
985
1202
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
986
1203
// import type {
987
1204
// ViewRecord,
···
1089
1306
1090
1307
function UniversalPostRenderer({
1091
1308
post,
1309
+
uprrrsauthor,
1092
1310
//setMainItem,
1093
1311
//isMainItem,
1094
1312
onPostClick,
···
1109
1327
ref,
1110
1328
dataIndexPropPass,
1111
1329
nopics,
1330
+
lightboxCallback,
1331
+
maxReplies,
1112
1332
}: {
1113
1333
post: PostView;
1334
+
uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed;
1114
1335
// optional for now because i havent ported every use to this yet
1115
1336
// setMainItem?: React.Dispatch<
1116
1337
// React.SetStateAction<AppBskyFeedDefs.FeedViewPost>
···
1132
1353
ref?: React.Ref<HTMLDivElement>;
1133
1354
dataIndexPropPass?: number;
1134
1355
nopics?: boolean;
1356
+
lightboxCallback?: (d: LightboxProps) => void;
1357
+
maxReplies?: number;
1135
1358
}) {
1136
1359
const parsed = new AtUri(post.uri);
1137
1360
const navigate = useNavigate();
···
1142
1365
const [hasLiked, setHasLiked] = useState<boolean>(
1143
1366
post.uri in likedPosts || post.viewer?.like ? true : false
1144
1367
);
1368
+
const [, setComposerPost] = useAtom(composerAtom);
1145
1369
const { agent } = useAuth();
1146
1370
const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like);
1147
1371
const [retweetUri, setRetweetUri] = useState<string | undefined>(
···
1202
1426
: undefined;
1203
1427
1204
1428
const emergencySalt = randomString();
1429
+
const fedi = (post.record as { bridgyOriginalText?: string })
1430
+
.bridgyOriginalText;
1205
1431
1206
1432
/* fuck you */
1207
1433
const isMainItem = false;
···
1236
1462
paddingLeft: isQuote ? 12 : 16,
1237
1463
paddingRight: isQuote ? 12 : 16,
1238
1464
//paddingTop: 16,
1239
-
paddingTop: isRepost ? 10 : isQuote ? 12 : 16,
1465
+
paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16,
1240
1466
//paddingBottom: bottomReplyLine ? 0 : 16,
1241
1467
paddingBottom: 0,
1242
1468
fontFamily: "system-ui, sans-serif",
···
1245
1471
// dont cursor: "pointer",
1246
1472
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1247
1473
}}
1248
-
className="border-gray-300 dark:border-gray-600"
1474
+
className="border-gray-300 dark:border-gray-800"
1249
1475
>
1250
1476
{isRepost && (
1251
1477
<div
···
1277
1503
//left: 16 + (42 / 2),
1278
1504
width: 2,
1279
1505
//height: "100%",
1280
-
height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6,
1506
+
height: isRepost
1507
+
? "calc(16px + 1rem - 6px)"
1508
+
: topReplyLine
1509
+
? 8 - 6
1510
+
: 16 - 6,
1281
1511
// background: theme.textSecondary,
1282
1512
//opacity: 0.5,
1283
1513
// no flex here
···
1285
1515
className="bg-gray-500 dark:bg-gray-400"
1286
1516
/>
1287
1517
)}
1288
-
<div
1289
-
style={{
1290
-
position: "absolute",
1291
-
//top: isRepost ? "calc(16px + 1rem)" : 16,
1292
-
//left: 16,
1293
-
zIndex: 1,
1294
-
top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16,
1295
-
left: isQuote ? 12 : 16,
1296
-
}}
1297
-
onClick={onProfileClick}
1298
-
>
1299
-
<img
1300
-
src={post.author.avatar || defaultpfp}
1301
-
alt="avatar"
1302
-
// transition={{
1303
-
// type: "spring",
1304
-
// stiffness: 260,
1305
-
// damping: 20,
1306
-
// }}
1307
-
style={{
1308
-
borderRadius: "50%",
1309
-
marginRight: 12,
1310
-
objectFit: "cover",
1311
-
//background: theme.border,
1312
-
//border: `1px solid ${theme.border}`,
1313
-
width: isQuote ? 16 : 42,
1314
-
height: isQuote ? 16 : 42,
1315
-
}}
1316
-
className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600"
1317
-
/>
1318
-
</div>
1518
+
<HoverCard.Root>
1519
+
<HoverCard.Trigger asChild>
1520
+
<div
1521
+
className={`absolute`}
1522
+
style={{
1523
+
top: isRepost
1524
+
? "calc(16px + 1rem)"
1525
+
: isQuote
1526
+
? 12
1527
+
: topReplyLine
1528
+
? 8
1529
+
: 16,
1530
+
left: isQuote ? 12 : 16,
1531
+
}}
1532
+
onClick={onProfileClick}
1533
+
>
1534
+
<img
1535
+
src={post.author.avatar || defaultpfp}
1536
+
alt="avatar"
1537
+
className={`rounded-full object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600`}
1538
+
style={{
1539
+
width: isQuote ? 16 : 42,
1540
+
height: isQuote ? 16 : 42,
1541
+
}}
1542
+
/>
1543
+
</div>
1544
+
</HoverCard.Trigger>
1545
+
<HoverCard.Portal>
1546
+
<HoverCard.Content
1547
+
className="rounded-md p-4 w-72 bg-gray-50 dark:bg-gray-900 shadow-lg border border-gray-300 dark:border-gray-800 animate-slide-fade z-50"
1548
+
side={"bottom"}
1549
+
sideOffset={5}
1550
+
onClick={onProfileClick}
1551
+
>
1552
+
<div className="flex flex-col gap-2">
1553
+
<div className="flex flex-row">
1554
+
<img
1555
+
src={post.author.avatar || defaultpfp}
1556
+
alt="avatar"
1557
+
className="rounded-full w-[58px] h-[58px] object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600"
1558
+
/>
1559
+
<div className=" flex-1 flex flex-row align-middle justify-end">
1560
+
<div className=" flex flex-col justify-start">
1561
+
<FollowButton targetdidorhandle={post.author.did} />
1562
+
</div>
1563
+
</div>
1564
+
</div>
1565
+
<div className="flex flex-col gap-3">
1566
+
<div>
1567
+
<div className="text-gray-900 dark:text-gray-100 font-medium text-md">
1568
+
{post.author.displayName || post.author.handle}{" "}
1569
+
</div>
1570
+
<div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1">
1571
+
<Mutual targetdidorhandle={post.author.did} />@{post.author.handle}{" "}
1572
+
</div>
1573
+
</div>
1574
+
{uprrrsauthor?.description && (
1575
+
<div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3">
1576
+
{uprrrsauthor.description}
1577
+
</div>
1578
+
)}
1579
+
{/* <div className="flex gap-4">
1580
+
<div className="flex gap-1">
1581
+
<div className="font-medium text-gray-900 dark:text-gray-100">
1582
+
0
1583
+
</div>
1584
+
<div className="text-gray-500 dark:text-gray-400">
1585
+
Following
1586
+
</div>
1587
+
</div>
1588
+
<div className="flex gap-1">
1589
+
<div className="font-medium text-gray-900 dark:text-gray-100">
1590
+
2,900
1591
+
</div>
1592
+
<div className="text-gray-500 dark:text-gray-400">
1593
+
Followers
1594
+
</div>
1595
+
</div>
1596
+
</div> */}
1597
+
</div>
1598
+
</div>
1599
+
1600
+
{/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */}
1601
+
</HoverCard.Content>
1602
+
</HoverCard.Portal>
1603
+
</HoverCard.Root>
1604
+
1319
1605
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
1320
1606
<div
1321
1607
style={{
···
1329
1615
}}
1330
1616
>
1331
1617
{/* dummy for later use */}
1332
-
<div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} />
1618
+
<div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} />
1333
1619
{/* reply line !!!! bottomReplyLine */}
1334
1620
{bottomReplyLine && (
1335
1621
<div
···
1484
1770
}}
1485
1771
className="text-gray-900 dark:text-gray-100"
1486
1772
>
1487
-
{renderTextWithFacets({
1488
-
text: (post.record as { text?: string }).text ?? "",
1489
-
facets: (post.record.facets as Facet[]) ?? [],
1490
-
navigate: navigate,
1491
-
})}
1492
-
{}
1773
+
{fedi ? (
1774
+
<>
1775
+
<span
1776
+
className="dangerousFediContent"
1777
+
dangerouslySetInnerHTML={{
1778
+
__html: DOMPurify.sanitize(fedi),
1779
+
}}
1780
+
/>
1781
+
</>
1782
+
) : (
1783
+
<>
1784
+
{renderTextWithFacets({
1785
+
text: (post.record as { text?: string }).text ?? "",
1786
+
facets: (post.record.facets as Facet[]) ?? [],
1787
+
navigate: navigate,
1788
+
})}
1789
+
</>
1790
+
)}
1493
1791
</div>
1494
1792
{post.embed && depth < 1 ? (
1495
1793
<PostEmbeds
···
1500
1798
navigate={navigate}
1501
1799
postid={{ did: post.author.did, rkey: parsed.rkey }}
1502
1800
nopics={nopics}
1801
+
lightboxCallback={lightboxCallback}
1503
1802
/>
1504
1803
) : null}
1505
1804
{post.embed && depth > 0 && (
···
1507
1806
hydrate embeds this deep but the connection here is implicit
1508
1807
todo: idk make this a real part of the embed shim so its not implicit */
1509
1808
<>
1510
-
<div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1809
+
<div className="border-gray-300 dark:border-gray-800 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1511
1810
(there is an embed here thats too deep to render)
1512
1811
</div>
1513
1812
</>
···
1530
1829
borderBottomWidth: 1,
1531
1830
marginBottom: 8,
1532
1831
}} // important for height animation
1533
-
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"
1832
+
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7"
1534
1833
>
1535
1834
{fullDateTimeFormat(post.indexedAt)}
1536
1835
</div>
···
1549
1848
}}
1550
1849
className="text-gray-500 dark:text-gray-400"
1551
1850
>
1552
-
<span style={btnstyle}>
1553
-
<MdiCommentOutline />
1554
-
{post.replyCount}
1555
-
</span>
1556
1851
<HitSlopButton
1557
1852
onClick={() => {
1558
-
repostOrUnrepostPost();
1853
+
setComposerPost({ kind: "reply", parent: post.uri });
1559
1854
}}
1560
1855
style={{
1561
1856
...btnstyle,
1562
-
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1563
1857
}}
1564
1858
>
1565
-
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1566
-
{(post.repostCount || 0) + (hasRetweeted ? 1 : 0)}
1859
+
<MdiCommentOutline />
1860
+
{post.replyCount}
1567
1861
</HitSlopButton>
1862
+
<DropdownMenu.Root modal={false}>
1863
+
<DropdownMenu.Trigger asChild>
1864
+
<div
1865
+
style={{
1866
+
...btnstyle,
1867
+
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1868
+
}}
1869
+
aria-label="Repost or quote post"
1870
+
>
1871
+
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1872
+
{post.repostCount ?? 0}
1873
+
</div>
1874
+
</DropdownMenu.Trigger>
1875
+
1876
+
<DropdownMenu.Portal>
1877
+
<DropdownMenu.Content
1878
+
align="start"
1879
+
sideOffset={5}
1880
+
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-32 z-50 overflow-hidden"
1881
+
>
1882
+
<DropdownMenu.Item
1883
+
onSelect={repostOrUnrepostPost}
1884
+
className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
1885
+
>
1886
+
<MdiRepeat
1887
+
className={hasRetweeted ? "text-green-400" : ""}
1888
+
/>
1889
+
<span>{hasRetweeted ? "Undo Repost" : "Repost"}</span>
1890
+
</DropdownMenu.Item>
1891
+
1892
+
<DropdownMenu.Item
1893
+
onSelect={() => {
1894
+
setComposerPost({
1895
+
kind: "quote",
1896
+
subject: post.uri,
1897
+
});
1898
+
}}
1899
+
className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
1900
+
>
1901
+
{/* You might want a specific quote icon here */}
1902
+
<MdiCommentOutline />
1903
+
<span>Quote</span>
1904
+
</DropdownMenu.Item>
1905
+
</DropdownMenu.Content>
1906
+
</DropdownMenu.Portal>
1907
+
</DropdownMenu.Root>
1568
1908
<HitSlopButton
1569
1909
onClick={() => {
1570
1910
likeOrUnlikePost();
···
1706
2046
navigate,
1707
2047
postid,
1708
2048
nopics,
2049
+
lightboxCallback,
1709
2050
}: {
1710
2051
embed?: Embed;
1711
2052
moderation?: ModerationDecision;
···
1716
2057
navigate: (_: any) => void;
1717
2058
postid?: { did: string; rkey: string };
1718
2059
nopics?: boolean;
2060
+
lightboxCallback?: (d: LightboxProps) => void;
1719
2061
}) {
1720
-
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
2062
+
//const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
2063
+
function setLightboxIndex(number: number) {
2064
+
navigate({
2065
+
to: "/profile/$did/post/$rkey/image/$i",
2066
+
params: {
2067
+
did: postid?.did,
2068
+
rkey: postid?.rkey,
2069
+
i: number.toString(),
2070
+
},
2071
+
});
2072
+
}
1721
2073
if (
1722
2074
AppBskyEmbedRecordWithMedia.isView(embed) &&
1723
2075
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
···
1753
2105
navigate={navigate}
1754
2106
postid={postid}
1755
2107
nopics={nopics}
2108
+
lightboxCallback={lightboxCallback}
1756
2109
/>
1757
2110
{/* padding empty div of 8px height */}
1758
2111
<div style={{ height: 12 }} />
···
1766
2119
//boxShadow: theme.cardShadow,
1767
2120
overflow: "hidden",
1768
2121
}}
1769
-
className="shadow border border-gray-200 dark:border-gray-700"
2122
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1770
2123
>
1771
2124
<UniversalPostRenderer
1772
2125
post={post}
···
1883
2236
//boxShadow: theme.cardShadow,
1884
2237
overflow: "hidden",
1885
2238
}}
1886
-
className="shadow border border-gray-200 dark:border-gray-700"
2239
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1887
2240
>
1888
2241
<UniversalPostRenderer
1889
2242
post={post}
···
1920
2273
1921
2274
// image embed
1922
2275
// =
1923
-
if (AppBskyEmbedImages.isView(embed) && !nopics) {
2276
+
if (AppBskyEmbedImages.isView(embed)) {
1924
2277
const { images } = embed;
1925
2278
1926
2279
const lightboxImages = images.map((img) => ({
1927
2280
src: img.fullsize,
1928
2281
alt: img.alt,
1929
2282
}));
2283
+
console.log("rendering images");
2284
+
if (lightboxCallback) {
2285
+
lightboxCallback({ images: lightboxImages });
2286
+
console.log("rendering images");
2287
+
}
2288
+
2289
+
if (nopics) return;
1930
2290
1931
2291
if (images.length > 0) {
1932
2292
// const items = embed.images.map(img => ({
···
1956
2316
//border: `1px solid ${theme.border}`,
1957
2317
overflow: "hidden",
1958
2318
}}
1959
-
className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900"
2319
+
className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900"
1960
2320
>
1961
-
{lightboxIndex !== null && (
2321
+
{/* {lightboxIndex !== null && (
1962
2322
<Lightbox
1963
2323
images={lightboxImages}
1964
2324
index={lightboxIndex}
···
1966
2326
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
1967
2327
post={postid}
1968
2328
/>
1969
-
)}
2329
+
)} */}
1970
2330
<img
1971
2331
src={image.fullsize}
1972
2332
alt={image.alt}
···
1997
2357
overflow: "hidden",
1998
2358
//border: `1px solid ${theme.border}`,
1999
2359
}}
2000
-
className="border border-gray-200 dark:border-gray-700"
2360
+
className="border border-gray-200 dark:border-gray-800 was7"
2001
2361
>
2002
-
{lightboxIndex !== null && (
2362
+
{/* {lightboxIndex !== null && (
2003
2363
<Lightbox
2004
2364
images={lightboxImages}
2005
2365
index={lightboxIndex}
···
2007
2367
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2008
2368
post={postid}
2009
2369
/>
2010
-
)}
2370
+
)} */}
2011
2371
{images.map((img, i) => (
2012
2372
<div
2013
2373
key={i}
···
2047
2407
//border: `1px solid ${theme.border}`,
2048
2408
// height: 240, // fixed height for cropping
2049
2409
}}
2050
-
className="border border-gray-200 dark:border-gray-700"
2410
+
className="border border-gray-200 dark:border-gray-800 was7"
2051
2411
>
2052
-
{lightboxIndex !== null && (
2412
+
{/* {lightboxIndex !== null && (
2053
2413
<Lightbox
2054
2414
images={lightboxImages}
2055
2415
index={lightboxIndex}
···
2057
2417
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2058
2418
post={postid}
2059
2419
/>
2060
-
)}
2420
+
)} */}
2061
2421
{/* Left: 1:1 */}
2062
2422
<div
2063
2423
style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }}
···
2132
2492
//border: `1px solid ${theme.border}`,
2133
2493
//aspectRatio: "3 / 2", // overall grid aspect
2134
2494
}}
2135
-
className="border border-gray-200 dark:border-gray-700"
2495
+
className="border border-gray-200 dark:border-gray-800 was7"
2136
2496
>
2137
-
{lightboxIndex !== null && (
2497
+
{/* {lightboxIndex !== null && (
2138
2498
<Lightbox
2139
2499
images={lightboxImages}
2140
2500
index={lightboxIndex}
···
2142
2502
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2143
2503
post={postid}
2144
2504
/>
2145
-
)}
2505
+
)} */}
2146
2506
{images.map((img, i) => (
2147
2507
<div
2148
2508
key={i}
···
2233
2593
return <div />;
2234
2594
}
2235
2595
2236
-
type LightboxProps = {
2237
-
images: { src: string; alt?: string }[];
2238
-
index: number;
2239
-
onClose: () => void;
2240
-
onNavigate?: (newIndex: number) => void;
2241
-
post?: { did: string; rkey: string };
2242
-
};
2243
-
export function Lightbox({
2244
-
images,
2245
-
index,
2246
-
onClose,
2247
-
onNavigate,
2248
-
post,
2249
-
}: LightboxProps) {
2250
-
const image = images[index];
2251
-
2252
-
useEffect(() => {
2253
-
function handleKey(e: KeyboardEvent) {
2254
-
if (e.key === "Escape") onClose();
2255
-
if (e.key === "ArrowRight" && onNavigate)
2256
-
onNavigate((index + 1) % images.length);
2257
-
if (e.key === "ArrowLeft" && onNavigate)
2258
-
onNavigate((index - 1 + images.length) % images.length);
2259
-
}
2260
-
window.addEventListener("keydown", handleKey);
2261
-
return () => window.removeEventListener("keydown", handleKey);
2262
-
}, [index, images.length, onClose, onNavigate]);
2263
-
2264
-
return createPortal(
2265
-
<>
2266
-
{post && (
2267
-
<div
2268
-
onClick={(e) => {
2269
-
e.stopPropagation();
2270
-
e.nativeEvent.stopImmediatePropagation();
2271
-
}}
2272
-
className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-700 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white"
2273
-
>
2274
-
<ProfilePostComponent
2275
-
did={post.did}
2276
-
rkey={post.rkey}
2277
-
nopics={onClose}
2278
-
/>
2279
-
</div>
2280
-
)}
2281
-
<div
2282
-
className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px)] lg:max-w-[calc(100vw-350px)]"
2283
-
onClick={(e) => {
2284
-
e.stopPropagation();
2285
-
onClose();
2286
-
}}
2287
-
>
2288
-
<img
2289
-
src={image.src}
2290
-
alt={image.alt}
2291
-
className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg"
2292
-
onClick={(e) => e.stopPropagation()}
2293
-
/>
2294
-
2295
-
{images.length > 1 && (
2296
-
<>
2297
-
<button
2298
-
onClick={(e) => {
2299
-
e.stopPropagation();
2300
-
onNavigate?.((index - 1 + images.length) % images.length);
2301
-
}}
2302
-
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
2303
-
>
2304
-
<svg
2305
-
xmlns="http://www.w3.org/2000/svg"
2306
-
width={28}
2307
-
height={28}
2308
-
viewBox="0 0 24 24"
2309
-
>
2310
-
<g fill="none" fillRule="evenodd">
2311
-
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
2312
-
<path
2313
-
fill="currentColor"
2314
-
d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z"
2315
-
></path>
2316
-
</g>
2317
-
</svg>
2318
-
</button>
2319
-
<button
2320
-
onClick={(e) => {
2321
-
e.stopPropagation();
2322
-
onNavigate?.((index + 1) % images.length);
2323
-
}}
2324
-
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
2325
-
>
2326
-
<svg
2327
-
xmlns="http://www.w3.org/2000/svg"
2328
-
width={28}
2329
-
height={28}
2330
-
viewBox="0 0 24 24"
2331
-
>
2332
-
<g fill="none" fillRule="evenodd">
2333
-
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
2334
-
<path
2335
-
fill="currentColor"
2336
-
d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z"
2337
-
></path>
2338
-
</g>
2339
-
</svg>
2340
-
</button>
2341
-
</>
2342
-
)}
2343
-
</div>
2344
-
</>,
2345
-
document.body
2346
-
);
2347
-
}
2348
-
2349
2596
function getDomain(url: string) {
2350
2597
try {
2351
2598
const { hostname } = new URL(url);
···
2410
2657
return { start, end, feature: f.features[0] };
2411
2658
});
2412
2659
}
2413
-
function renderTextWithFacets({
2660
+
export function renderTextWithFacets({
2414
2661
text,
2415
2662
facets,
2416
2663
navigate,
···
2573
2820
>
2574
2821
<div
2575
2822
style={containerStyle as React.CSSProperties}
2576
-
className="border border-gray-200 dark:border-gray-700"
2823
+
className="border border-gray-200 dark:border-gray-800 was7"
2577
2824
>
2578
2825
{thumb && (
2579
2826
<div
···
2587
2834
marginBottom: 8,
2588
2835
//borderBottom: `1px solid ${theme.border}`,
2589
2836
}}
2590
-
className="border-b border-gray-200 dark:border-gray-700"
2837
+
className="border-b border-gray-200 dark:border-gray-800 was7"
2591
2838
>
2592
2839
<img
2593
2840
src={thumb}
···
2713
2960
borderRadius: 12,
2714
2961
//border: `1px solid ${theme.border}`,
2715
2962
}}
2716
-
className="border border-gray-200 dark:border-gray-700"
2963
+
className="border border-gray-200 dark:border-gray-800 was7"
2717
2964
onClick={async (e) => {
2718
2965
e.stopPropagation();
2719
2966
setPlaying(true);
···
2754
3001
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
2755
3002
}%`, // 16:9 = 56.25%, 4:3 = 75%
2756
3003
}}
2757
-
className="border border-gray-200 dark:border-gray-700"
3004
+
className="border border-gray-200 dark:border-gray-800 was7"
2758
3005
>
2759
3006
<ReactPlayer
2760
3007
src={url}
+59
src/components/radix-m3-rd/Button.tsx
+59
src/components/radix-m3-rd/Button.tsx
···
1
+
import { Slot } from "@radix-ui/react-slot";
2
+
import clsx from "clsx";
3
+
import * as React from "react";
4
+
5
+
export type ButtonVariant = "filled" | "outlined" | "text" | "secondary";
6
+
export type ButtonSize = "sm" | "md" | "lg";
7
+
8
+
const variantClasses: Record<ButtonVariant, string> = {
9
+
filled:
10
+
"bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500",
11
+
secondary:
12
+
"bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500",
13
+
outlined:
14
+
"border border-gray-800 text-gray-800 hover:bg-gray-100 dark:border-gray-200 dark:text-gray-200 dark:hover:bg-gray-800/10",
15
+
text: "bg-transparent text-gray-800 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-800/10",
16
+
};
17
+
18
+
const sizeClasses: Record<ButtonSize, string> = {
19
+
sm: "px-3 py-1.5 text-sm",
20
+
md: "px-4 py-2 text-base",
21
+
lg: "px-6 py-3 text-lg",
22
+
};
23
+
24
+
export function Button({
25
+
variant = "filled",
26
+
size = "md",
27
+
asChild = false,
28
+
ref,
29
+
className,
30
+
children,
31
+
...props
32
+
}: {
33
+
variant?: ButtonVariant;
34
+
size?: ButtonSize;
35
+
asChild?: boolean;
36
+
className?: string;
37
+
children?: React.ReactNode;
38
+
ref?: React.Ref<HTMLButtonElement>;
39
+
} & React.ComponentPropsWithoutRef<"button">) {
40
+
const Comp = asChild ? Slot : "button";
41
+
42
+
return (
43
+
<Comp
44
+
ref={ref}
45
+
className={clsx(
46
+
//focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-500 dark:focus:ring-gray-300
47
+
"inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
48
+
variantClasses[variant],
49
+
sizeClasses[size],
50
+
className
51
+
)}
52
+
{...props}
53
+
>
54
+
{children}
55
+
</Comp>
56
+
);
57
+
}
58
+
59
+
Button.displayName = "Button";
+54
-9
src/main.tsx
+54
-9
src/main.tsx
···
1
1
import "~/styles/app.css";
2
2
3
3
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
4
-
import { QueryClient, QueryClientProvider, } from "@tanstack/react-query";
5
-
import {
6
-
persistQueryClient,
7
-
} from "@tanstack/react-query-persist-client";
8
-
import { createRouter,RouterProvider } from "@tanstack/react-router";
4
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+
import { persistQueryClient } from "@tanstack/react-query-persist-client";
6
+
import { createRouter, RouterProvider } from "@tanstack/react-router";
7
+
import { useSetAtom } from "jotai";
8
+
import { useEffect } from "react";
9
9
//import { StrictMode } from "react";
10
10
import ReactDOM from "react-dom/client";
11
11
12
12
import reportWebVitals from "./reportWebVitals.ts";
13
13
// Import the generated route tree
14
14
import { routeTree } from "./routeTree.gen";
15
+
import { isAtTopAtom } from "./utils/atoms.ts";
15
16
17
+
//initAtomToCssVar(hueAtom, "--tw-gray-hue")
16
18
17
19
const queryClient = new QueryClient({
18
20
defaultOptions: {
···
28
30
persistQueryClient({
29
31
queryClient,
30
32
persister: localStoragePersister,
31
-
})
33
+
});
32
34
33
35
// Create a new router instance
34
36
const router = createRouter({
···
54
56
root.render(
55
57
// double queries annoys me
56
58
// <StrictMode>
57
-
<QueryClientProvider client={queryClient}>
58
-
<RouterProvider router={router} />
59
-
</QueryClientProvider>
59
+
<QueryClientProvider client={queryClient}>
60
+
<ScrollTopWatcher />
61
+
<RouterProvider router={router} />
62
+
</QueryClientProvider>
60
63
// </StrictMode>
61
64
);
62
65
}
···
65
68
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
66
69
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
67
70
reportWebVitals();
71
+
72
+
export default function ScrollTopWatcher() {
73
+
const setIsAtTop = useSetAtom(isAtTopAtom);
74
+
useEffect(() => {
75
+
const meta = document.querySelector('meta[name="theme-color"]');
76
+
let lastAtTop = window.scrollY === 0;
77
+
let timeoutId: number | undefined;
78
+
79
+
const setVars = (atTop: boolean) => {
80
+
const root = document.documentElement;
81
+
root.style.setProperty("--is-top", atTop ? "1" : "0");
82
+
83
+
const bg = getComputedStyle(root).getPropertyValue("--header-bg").trim();
84
+
if (meta && bg) meta.setAttribute("content", bg);
85
+
setIsAtTop(atTop);
86
+
};
87
+
88
+
const check = () => {
89
+
const atTop = window.scrollY === 0;
90
+
if (atTop !== lastAtTop) {
91
+
lastAtTop = atTop;
92
+
setVars(atTop);
93
+
}
94
+
};
95
+
96
+
const handleScroll = () => {
97
+
if (timeoutId) clearTimeout(timeoutId);
98
+
timeoutId = window.setTimeout(check, 2);
99
+
};
100
+
101
+
// initialize
102
+
setVars(lastAtTop);
103
+
window.addEventListener("scroll", handleScroll, { passive: true });
104
+
105
+
return () => {
106
+
window.removeEventListener("scroll", handleScroll);
107
+
if (timeoutId) clearTimeout(timeoutId);
108
+
};
109
+
}, []);
110
+
111
+
return null;
112
+
}
+26
-23
src/providers/UnifiedAuthProvider.tsx
+26
-23
src/providers/UnifiedAuthProvider.tsx
···
1
-
// src/providers/UnifiedAuthProvider.tsx
2
-
// Import both Agent and the (soon to be deprecated) AtpAgent
3
1
import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
4
2
import {
5
3
type OAuthSession,
···
7
5
TokenRefreshError,
8
6
TokenRevokedError,
9
7
} from "@atproto/oauth-client-browser";
8
+
import { useAtom } from "jotai";
10
9
import React, {
11
10
createContext,
12
11
use,
···
15
14
useState,
16
15
} from "react";
17
16
18
-
import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed
17
+
import { quickAuthAtom } from "~/utils/atoms";
18
+
19
+
import { oauthClient } from "../utils/oauthClient";
19
20
20
-
// Define the unified status and authentication method
21
21
type AuthStatus = "loading" | "signedIn" | "signedOut";
22
22
type AuthMethod = "password" | "oauth" | null;
23
23
24
24
interface AuthContextValue {
25
-
agent: Agent | null; // The agent is typed as the base class `Agent`
25
+
agent: Agent | null;
26
26
status: AuthStatus;
27
27
authMethod: AuthMethod;
28
28
loginWithPassword: (
···
41
41
}: {
42
42
children: React.ReactNode;
43
43
}) => {
44
-
// The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances.
45
44
const [agent, setAgent] = useState<Agent | null>(null);
46
45
const [status, setStatus] = useState<AuthStatus>("loading");
47
46
const [authMethod, setAuthMethod] = useState<AuthMethod>(null);
48
47
const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null);
48
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
49
49
50
-
// Unified Initialization Logic
51
50
const initialize = useCallback(async () => {
52
-
// --- 1. Try OAuth initialization first ---
53
51
try {
54
52
const oauthResult = await oauthClient.init();
55
53
if (oauthResult) {
56
54
// /*mass comment*/ console.log("OAuth session restored.");
57
-
const apiAgent = new Agent(oauthResult.session); // Standard Agent
55
+
const apiAgent = new Agent(oauthResult.session);
58
56
setAgent(apiAgent);
59
57
setOauthSession(oauthResult.session);
60
58
setAuthMethod("oauth");
61
59
setStatus("signedIn");
62
-
return; // Success
60
+
setQuickAuth(apiAgent?.did || null);
61
+
return;
63
62
}
64
63
} catch (e) {
65
64
console.error("OAuth init failed, checking password session.", e);
65
+
if (!quickAuth) {
66
+
// quickAuth restoration. if last used method is oauth we immediately call for oauth redo
67
+
// (and set a persistent atom somewhere to not retry again if it failed)
68
+
}
66
69
}
67
70
68
-
// --- 2. If no OAuth, try password-based session using AtpAgent ---
69
71
try {
70
72
const service = localStorage.getItem("service");
71
73
const sessionString = localStorage.getItem("sess");
72
74
73
75
if (service && sessionString) {
74
76
// /*mass comment*/ console.log("Resuming password-based session using AtpAgent...");
75
-
// Use the original, working AtpAgent logic
76
77
const apiAgent = new AtpAgent({ service });
77
78
const session: AtpSessionData = JSON.parse(sessionString);
78
79
await apiAgent.resumeSession(session);
79
80
80
81
// /*mass comment*/ console.log("Password-based session resumed successfully.");
81
-
setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent
82
+
setAgent(apiAgent);
82
83
setAuthMethod("password");
83
84
setStatus("signedIn");
84
-
return; // Success
85
+
setQuickAuth(apiAgent?.did || null);
86
+
return;
85
87
}
86
88
} catch (e) {
87
89
console.error("Failed to resume password-based session.", e);
···
89
91
localStorage.removeItem("service");
90
92
}
91
93
92
-
// --- 3. If neither worked, user is signed out ---
93
94
// /*mass comment*/ console.log("No active session found.");
94
95
setStatus("signedOut");
95
96
setAgent(null);
96
97
setAuthMethod(null);
97
-
}, []);
98
+
// do we want to null it here?
99
+
setQuickAuth(null);
100
+
}, [quickAuth, setQuickAuth]);
98
101
99
102
useEffect(() => {
100
103
const handleOAuthSessionDeleted = (
···
105
108
setOauthSession(null);
106
109
setAuthMethod(null);
107
110
setStatus("signedOut");
111
+
setQuickAuth(null);
108
112
};
109
113
110
114
oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
···
113
117
return () => {
114
118
oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener);
115
119
};
116
-
}, [initialize]);
120
+
}, [initialize, setQuickAuth]);
117
121
118
-
// --- Login Methods ---
119
122
const loginWithPassword = async (
120
123
user: string,
121
124
password: string,
···
125
128
setStatus("loading");
126
129
try {
127
130
let sessionData: AtpSessionData | undefined;
128
-
// Use the AtpAgent for its simple login and session persistence
129
131
const apiAgent = new AtpAgent({
130
132
service,
131
133
persistSession: (_evt, sess) => {
···
137
139
if (sessionData) {
138
140
localStorage.setItem("service", service);
139
141
localStorage.setItem("sess", JSON.stringify(sessionData));
140
-
setAgent(apiAgent); // Store the AtpAgent instance in our state
142
+
setAgent(apiAgent);
141
143
setAuthMethod("password");
142
144
setStatus("signedIn");
145
+
setQuickAuth(apiAgent?.did || null);
143
146
// /*mass comment*/ console.log("Successfully logged in with password.");
144
147
} else {
145
148
throw new Error("Session data not persisted after login.");
···
147
150
} catch (e) {
148
151
console.error("Password login failed:", e);
149
152
setStatus("signedOut");
153
+
setQuickAuth(null);
150
154
throw e;
151
155
}
152
156
};
···
161
165
}
162
166
}, [status]);
163
167
164
-
// --- Unified Logout ---
165
168
const logout = useCallback(async () => {
166
169
if (status !== "signedIn" || !agent) return;
167
170
setStatus("loading");
···
173
176
} else if (authMethod === "password") {
174
177
localStorage.removeItem("service");
175
178
localStorage.removeItem("sess");
176
-
// AtpAgent has its own logout methods
177
179
await (agent as AtpAgent).com.atproto.server.deleteSession();
178
180
// /*mass comment*/ console.log("Password-based session deleted.");
179
181
}
···
184
186
setAuthMethod(null);
185
187
setOauthSession(null);
186
188
setStatus("signedOut");
189
+
setQuickAuth(null);
187
190
}
188
-
}, [status, authMethod, agent, oauthSession]);
191
+
}, [status, agent, authMethod, oauthSession, setQuickAuth]);
189
192
190
193
return (
191
194
<AuthContext
+36
-5
src/routeTree.gen.ts
+36
-5
src/routeTree.gen.ts
···
21
21
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
22
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
23
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
24
+
import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i'
24
25
25
26
const SettingsRoute = SettingsRouteImport.update({
26
27
id: '/settings',
···
83
84
path: '/profile/$did/post/$rkey',
84
85
getParentRoute: () => rootRouteImport,
85
86
} as any)
87
+
const ProfileDidPostRkeyImageIRoute =
88
+
ProfileDidPostRkeyImageIRouteImport.update({
89
+
id: '/image/$i',
90
+
path: '/image/$i',
91
+
getParentRoute: () => ProfileDidPostRkeyRoute,
92
+
} as any)
86
93
87
94
export interface FileRoutesByFullPath {
88
95
'/': typeof IndexRoute
···
94
101
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
95
102
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
96
103
'/profile/$did': typeof ProfileDidIndexRoute
97
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
104
+
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
105
+
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
98
106
}
99
107
export interface FileRoutesByTo {
100
108
'/': typeof IndexRoute
···
106
114
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
107
115
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
108
116
'/profile/$did': typeof ProfileDidIndexRoute
109
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
117
+
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
118
+
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
110
119
}
111
120
export interface FileRoutesById {
112
121
__root__: typeof rootRouteImport
···
121
130
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
122
131
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
123
132
'/profile/$did/': typeof ProfileDidIndexRoute
124
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
133
+
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
134
+
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
125
135
}
126
136
export interface FileRouteTypes {
127
137
fileRoutesByFullPath: FileRoutesByFullPath
···
136
146
| '/route-b'
137
147
| '/profile/$did'
138
148
| '/profile/$did/post/$rkey'
149
+
| '/profile/$did/post/$rkey/image/$i'
139
150
fileRoutesByTo: FileRoutesByTo
140
151
to:
141
152
| '/'
···
148
159
| '/route-b'
149
160
| '/profile/$did'
150
161
| '/profile/$did/post/$rkey'
162
+
| '/profile/$did/post/$rkey/image/$i'
151
163
id:
152
164
| '__root__'
153
165
| '/'
···
162
174
| '/_pathlessLayout/_nested-layout/route-b'
163
175
| '/profile/$did/'
164
176
| '/profile/$did/post/$rkey'
177
+
| '/profile/$did/post/$rkey/image/$i'
165
178
fileRoutesById: FileRoutesById
166
179
}
167
180
export interface RootRouteChildren {
···
173
186
SettingsRoute: typeof SettingsRoute
174
187
CallbackIndexRoute: typeof CallbackIndexRoute
175
188
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
176
-
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute
189
+
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren
177
190
}
178
191
179
192
declare module '@tanstack/react-router' {
···
262
275
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
263
276
parentRoute: typeof rootRouteImport
264
277
}
278
+
'/profile/$did/post/$rkey/image/$i': {
279
+
id: '/profile/$did/post/$rkey/image/$i'
280
+
path: '/image/$i'
281
+
fullPath: '/profile/$did/post/$rkey/image/$i'
282
+
preLoaderRoute: typeof ProfileDidPostRkeyImageIRouteImport
283
+
parentRoute: typeof ProfileDidPostRkeyRoute
284
+
}
265
285
}
266
286
}
267
287
···
295
315
PathlessLayoutRouteChildren,
296
316
)
297
317
318
+
interface ProfileDidPostRkeyRouteChildren {
319
+
ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute
320
+
}
321
+
322
+
const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = {
323
+
ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute,
324
+
}
325
+
326
+
const ProfileDidPostRkeyRouteWithChildren =
327
+
ProfileDidPostRkeyRoute._addFileChildren(ProfileDidPostRkeyRouteChildren)
328
+
298
329
const rootRouteChildren: RootRouteChildren = {
299
330
IndexRoute: IndexRoute,
300
331
PathlessLayoutRoute: PathlessLayoutRouteWithChildren,
···
304
335
SettingsRoute: SettingsRoute,
305
336
CallbackIndexRoute: CallbackIndexRoute,
306
337
ProfileDidIndexRoute: ProfileDidIndexRoute,
307
-
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute,
338
+
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren,
308
339
}
309
340
export const routeTree = rootRouteImport
310
341
._addFileChildren(rootRouteChildren)
+559
-219
src/routes/__root.tsx
+559
-219
src/routes/__root.tsx
···
5
5
import type { QueryClient } from "@tanstack/react-query";
6
6
import {
7
7
createRootRouteWithContext,
8
-
Link,
8
+
// Link,
9
9
// Outlet,
10
10
Scripts,
11
11
useLocation,
12
12
useNavigate,
13
13
} from "@tanstack/react-router";
14
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
-
import { useState } from "react";
15
+
import { useAtom } from "jotai";
16
16
import * as React from "react";
17
17
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
18
18
19
+
import { Composer } from "~/components/Composer";
19
20
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
21
+
import { Import } from "~/components/Import";
20
22
import Login from "~/components/Login";
21
23
import { NotFound } from "~/components/NotFound";
24
+
import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
22
25
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
26
+
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
23
27
import { seo } from "~/utils/seo";
24
-
import IconHome from "~icons/material-symbols/home"
25
-
import IconHomeOutline from "~icons/material-symbols/home-outline"
26
-
import IconNotifications from "~icons/material-symbols/notifications"
27
-
import IconNotificationsOutline from "~icons/material-symbols/notifications-outline"
28
-
import IconSearch from "~icons/material-symbols/search"
29
-
import IconSettings from "~icons/material-symbols/settings"
30
-
import IconSettingsOutline from "~icons/material-symbols/settings-outline"
31
-
import IconTag from "~icons/material-symbols/tag"
32
-
import IconAccountCircleOutline from "~icons/mdi/account-circle-outline"
33
-
import IconPencilOutline from "~icons/mdi/pencil-outline"
34
28
35
29
export const Route = createRootRouteWithContext<{
36
30
queryClient: QueryClient;
···
95
89
}
96
90
97
91
function RootDocument({ children }: { children: React.ReactNode }) {
92
+
useAtomCssVar(hueAtom, "--tw-gray-hue");
98
93
const location = useLocation();
99
94
const navigate = useNavigate();
100
95
const { agent } = useAuth();
···
105
100
agent &&
106
101
(location.pathname === `/profile/${agent?.did}` ||
107
102
location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`);
103
+
const isSettings = location.pathname.startsWith("/settings");
104
+
const isSearch = location.pathname.startsWith("/search");
105
+
const isFeeds = location.pathname.startsWith("/feeds");
108
106
109
-
const [postOpen, setPostOpen] = useState(false);
110
-
const [postText, setPostText] = useState("");
111
-
const [posting, setPosting] = useState(false);
112
-
const [postSuccess, setPostSuccess] = useState(false);
113
-
const [postError, setPostError] = useState<string | null>(null);
107
+
const locationEnum:
108
+
| "feeds"
109
+
| "search"
110
+
| "settings"
111
+
| "notifications"
112
+
| "profile"
113
+
| "home" = isFeeds
114
+
? "feeds"
115
+
: isSearch
116
+
? "search"
117
+
: isSettings
118
+
? "settings"
119
+
: isNotifications
120
+
? "notifications"
121
+
: isProfile
122
+
? "profile"
123
+
: "home";
114
124
115
-
async function handlePost() {
116
-
if (!agent) return;
117
-
setPosting(true);
118
-
setPostError(null);
119
-
try {
120
-
await agent.com.atproto.repo.createRecord({
121
-
collection: "app.bsky.feed.post",
122
-
repo: agent.assertDid,
123
-
record: {
124
-
$type: "app.bsky.feed.post",
125
-
text: postText,
126
-
createdAt: new Date().toISOString(),
127
-
},
128
-
});
129
-
setPostSuccess(true);
130
-
setPostText("");
131
-
setTimeout(() => {
132
-
setPostSuccess(false);
133
-
setPostOpen(false);
134
-
}, 1500);
135
-
} catch (e: any) {
136
-
setPostError(e?.message || "Failed to post");
137
-
} finally {
138
-
setPosting(false);
139
-
}
140
-
}
125
+
const [, setComposerPost] = useAtom(composerAtom);
141
126
142
127
return (
143
128
<>
144
-
{postOpen && (
145
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
146
-
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md relative">
147
-
<button
148
-
className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
149
-
onClick={() => !posting && setPostOpen(false)}
150
-
disabled={posting}
151
-
aria-label="Close"
152
-
>
153
-
ร
154
-
</button>
155
-
<h2 className="text-lg font-bold mb-2">Create Post</h2>
156
-
{postSuccess ? (
157
-
<div className="flex flex-col items-center justify-center py-8">
158
-
<span className="text-green-500 text-4xl mb-2">โ</span>
159
-
<span className="text-green-600">Posted!</span>
160
-
</div>
161
-
) : (
162
-
<>
163
-
<textarea
164
-
className="w-full border rounded p-2 mb-2 dark:bg-gray-800 dark:border-gray-700"
165
-
rows={4}
166
-
placeholder="What's on your mind?"
167
-
value={postText}
168
-
onChange={(e) => setPostText(e.target.value)}
169
-
disabled={posting}
170
-
autoFocus
171
-
/>
172
-
{postError && (
173
-
<div className="text-red-500 text-sm mb-2">{postError}</div>
174
-
)}
175
-
<button
176
-
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
177
-
onClick={handlePost}
178
-
disabled={posting || !postText.trim()}
179
-
>
180
-
{posting ? "Posting..." : "Post"}
181
-
</button>
182
-
</>
183
-
)}
184
-
</div>
185
-
</div>
186
-
)}
129
+
<Composer />
187
130
188
131
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
189
-
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
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">
190
133
<div className="flex items-center gap-3 mb-4">
191
-
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
134
+
<FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
192
135
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
193
136
Red Dwarf{" "}
194
137
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
196
139
</span> */}
197
140
</span>
198
141
</div>
199
-
<Link
142
+
<MaterialNavItem
143
+
InactiveIcon={
144
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
145
+
}
146
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
147
+
active={locationEnum === "home"}
148
+
onClickCallbback={() =>
149
+
navigate({
150
+
to: "/",
151
+
//params: { did: agent.assertDid },
152
+
})
153
+
}
154
+
text="Home"
155
+
/>
156
+
157
+
<MaterialNavItem
158
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
159
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
160
+
active={locationEnum === "search"}
161
+
onClickCallbback={() =>
162
+
navigate({
163
+
to: "/search",
164
+
//params: { did: agent.assertDid },
165
+
})
166
+
}
167
+
text="Explore"
168
+
/>
169
+
<MaterialNavItem
170
+
InactiveIcon={
171
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
172
+
}
173
+
ActiveIcon={
174
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
175
+
}
176
+
active={locationEnum === "notifications"}
177
+
onClickCallbback={() =>
178
+
navigate({
179
+
to: "/notifications",
180
+
//params: { did: agent.assertDid },
181
+
})
182
+
}
183
+
text="Notifications"
184
+
/>
185
+
<MaterialNavItem
186
+
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
187
+
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
188
+
active={locationEnum === "feeds"}
189
+
onClickCallbback={() =>
190
+
navigate({
191
+
to: "/feeds",
192
+
//params: { did: agent.assertDid },
193
+
})
194
+
}
195
+
text="Feeds"
196
+
/>
197
+
<MaterialNavItem
198
+
InactiveIcon={
199
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
200
+
}
201
+
ActiveIcon={
202
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
203
+
}
204
+
active={locationEnum === "profile"}
205
+
onClickCallbback={() => {
206
+
if (authed && agent && agent.assertDid) {
207
+
//window.location.href = `/profile/${agent.assertDid}`;
208
+
navigate({
209
+
to: "/profile/$did",
210
+
params: { did: agent.assertDid },
211
+
});
212
+
}
213
+
}}
214
+
text="Profile"
215
+
/>
216
+
<MaterialNavItem
217
+
InactiveIcon={
218
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
219
+
}
220
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
221
+
active={locationEnum === "settings"}
222
+
onClickCallbback={() =>
223
+
navigate({
224
+
to: "/settings",
225
+
//params: { did: agent.assertDid },
226
+
})
227
+
}
228
+
text="Settings"
229
+
/>
230
+
<div className="flex flex-row items-center justify-center mt-3">
231
+
<MaterialPillButton
232
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
233
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
234
+
//active={true}
235
+
onClickCallbback={() => setComposerPost({ kind: 'root' })}
236
+
text="Post"
237
+
/>
238
+
</div>
239
+
{/* <Link
200
240
to="/"
201
241
className={
202
242
`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
···
204
244
}
205
245
>
206
246
{!isHome ? (
207
-
<IconHomeOutline width={28} height={28} />
247
+
<IconMaterialSymbolsHomeOutline width={28} height={28} />
208
248
) : (
209
-
<IconHome width={28} height={28} />
249
+
<IconMaterialSymbolsHome width={28} height={28} />
210
250
)}
211
251
<span>Home</span>
212
252
</Link>
···
218
258
}
219
259
>
220
260
{!isNotifications ? (
221
-
<IconNotificationsOutline width={28} height={28} />
261
+
<IconMaterialSymbolsNotificationsOutline width={28} height={28} />
222
262
) : (
223
-
<IconNotifications width={28} height={28} />
263
+
<IconMaterialSymbolsNotifications width={28} height={28} />
224
264
)}
225
265
<span>Notifications</span>
226
266
</Link>
···
231
271
}`}
232
272
>
233
273
{location.pathname.startsWith("/feeds") ? (
234
-
<IconTag width={28} height={28} />
274
+
<IconMaterialSymbolsTag width={28} height={28} />
235
275
) : (
236
-
<IconTag width={28} height={28} />
276
+
<IconMaterialSymbolsTag width={28} height={28} />
237
277
)}
238
278
<span>Feeds</span>
239
279
</Link>
···
245
285
}`}
246
286
>
247
287
{location.pathname.startsWith("/search") ? (
248
-
<IconSearch width={28} height={28} />
288
+
<IconMaterialSymbolsSearch width={28} height={28} />
249
289
) : (
250
-
<IconSearch width={28} height={28} />
290
+
<IconMaterialSymbolsSearch width={28} height={28} />
251
291
)}
252
292
<span>Search</span>
253
293
</Link>
···
266
306
}}
267
307
type="button"
268
308
>
269
-
<IconAccountCircleOutline width={28} height={28} />
309
+
{!isProfile ? (
310
+
<IconMaterialSymbolsAccountCircleOutline width={28} height={28} />
311
+
) : (
312
+
<IconMaterialSymbolsAccountCircle width={28} height={28} />
313
+
)}
270
314
<span>Profile</span>
271
315
</button>
272
316
<Link
···
276
320
}`}
277
321
>
278
322
{!location.pathname.startsWith("/settings") ? (
279
-
<IconSettingsOutline width={28} height={28} />
323
+
<IconMaterialSymbolsSettingsOutline width={28} height={28} />
280
324
) : (
281
-
<IconSettings width={28} height={28} />
325
+
<IconMaterialSymbolsSettings width={28} height={28} />
282
326
)}
283
327
<span>Settings</span>
284
-
</Link>
285
-
<button
328
+
</Link> */}
329
+
{/* <button
286
330
className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow"
287
331
onClick={() => setPostOpen(true)}
288
332
type="button"
289
333
>
290
-
<IconPencilOutline
334
+
<IconMdiPencilOutline
291
335
width={24}
292
336
height={24}
293
337
className="text-gray-600 dark:text-gray-400"
294
338
/>
295
339
<span>Post</span>
296
-
</button>
340
+
</button> */}
297
341
<div className="flex-1"></div>
298
342
<a
299
343
href="https://tangled.sh/@whey.party/red-dwarf"
···
324
368
</div>
325
369
</nav>
326
370
327
-
<button
328
-
className="lg:hidden fixed bottom-20 right-6 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-blue-600 dark:text-blue-400 rounded-full shadow-lg w-16 h-16 flex items-center justify-center border-4 border-white dark:border-gray-950 transition-all"
329
-
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
330
-
onClick={() => setPostOpen(true)}
331
-
type="button"
332
-
aria-label="Create Post"
333
-
>
334
-
<IconPencilOutline
335
-
width={24}
336
-
height={24}
337
-
className="text-gray-600 dark:text-gray-400"
371
+
<nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
372
+
<div className="flex items-center gap-3 mb-4">
373
+
<FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
374
+
</div>
375
+
<MaterialNavItem
376
+
small
377
+
InactiveIcon={
378
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
379
+
}
380
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
381
+
active={locationEnum === "home"}
382
+
onClickCallbback={() =>
383
+
navigate({
384
+
to: "/",
385
+
//params: { did: agent.assertDid },
386
+
})
387
+
}
388
+
text="Home"
338
389
/>
339
-
</button>
340
390
341
-
<main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0">
342
-
<div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950">
343
-
<div className="flex items-center gap-2">
344
-
<img
345
-
src="/redstar.png"
346
-
alt="Red Dwarf Logo"
347
-
className="w-6 h-6"
348
-
/>
349
-
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
350
-
Red Dwarf{" "}
351
-
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
352
-
lite
353
-
</span> */}
354
-
</span>
355
-
</div>
356
-
<div className="flex items-center gap-2">
357
-
<Login compact={true} />
358
-
</div>
391
+
<MaterialNavItem
392
+
small
393
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
394
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
395
+
active={locationEnum === "search"}
396
+
onClickCallbback={() =>
397
+
navigate({
398
+
to: "/search",
399
+
//params: { did: agent.assertDid },
400
+
})
401
+
}
402
+
text="Explore"
403
+
/>
404
+
<MaterialNavItem
405
+
small
406
+
InactiveIcon={
407
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
408
+
}
409
+
ActiveIcon={
410
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
411
+
}
412
+
active={locationEnum === "notifications"}
413
+
onClickCallbback={() =>
414
+
navigate({
415
+
to: "/notifications",
416
+
//params: { did: agent.assertDid },
417
+
})
418
+
}
419
+
text="Notifications"
420
+
/>
421
+
<MaterialNavItem
422
+
small
423
+
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
424
+
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
425
+
active={locationEnum === "feeds"}
426
+
onClickCallbback={() =>
427
+
navigate({
428
+
to: "/feeds",
429
+
//params: { did: agent.assertDid },
430
+
})
431
+
}
432
+
text="Feeds"
433
+
/>
434
+
<MaterialNavItem
435
+
small
436
+
InactiveIcon={
437
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
438
+
}
439
+
ActiveIcon={
440
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
441
+
}
442
+
active={locationEnum === "profile"}
443
+
onClickCallbback={() => {
444
+
if (authed && agent && agent.assertDid) {
445
+
//window.location.href = `/profile/${agent.assertDid}`;
446
+
navigate({
447
+
to: "/profile/$did",
448
+
params: { did: agent.assertDid },
449
+
});
450
+
}
451
+
}}
452
+
text="Profile"
453
+
/>
454
+
<MaterialNavItem
455
+
small
456
+
InactiveIcon={
457
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
458
+
}
459
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
460
+
active={locationEnum === "settings"}
461
+
onClickCallbback={() =>
462
+
navigate({
463
+
to: "/settings",
464
+
//params: { did: agent.assertDid },
465
+
})
466
+
}
467
+
text="Settings"
468
+
/>
469
+
<div className="flex flex-row items-center justify-center mt-3">
470
+
<MaterialPillButton
471
+
small
472
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
473
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
474
+
//active={true}
475
+
onClickCallbback={() => setComposerPost({ kind: 'root' })}
476
+
text="Post"
477
+
/>
359
478
</div>
479
+
</nav>
360
480
481
+
{agent?.did && (
482
+
<button
483
+
className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
484
+
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
485
+
onClick={() => setComposerPost({ kind: 'root' })}
486
+
type="button"
487
+
aria-label="Create Post"
488
+
>
489
+
<IconMdiPencilOutline
490
+
width={24}
491
+
height={24}
492
+
className="text-gray-600 dark:text-gray-400"
493
+
/>
494
+
</button>
495
+
)}
496
+
497
+
<main className="w-full max-w-[600px] sm:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip">
361
498
{children}
362
499
</main>
363
500
364
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>
365
503
<Login />
366
504
367
505
<div className="flex-1"></div>
368
506
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
369
-
Red Dwarf is a bluesky client that uses Constellation and direct PDS
370
-
queries. Skylite would be a self-hosted bluesky "instance". Stay
371
-
tuned for the release of Skylite.
507
+
Red Dwarf is a Bluesky client that does not rely on any Bluesky API App Servers. Instead, it uses Microcosm to fetch records directly from each users' PDS (via Slingshot) and connect them using backlinks (via Constellation)
372
508
</p>
373
509
</aside>
374
510
</div>
375
511
376
-
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40">
377
-
<div className="flex justify-around items-center py-2">
378
-
<Link
379
-
to="/"
380
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
381
-
isHome
382
-
? "text-gray-900 dark:text-gray-100"
383
-
: "text-gray-600 dark:text-gray-400"
384
-
}`}
385
-
>
386
-
{!isHome ? (
387
-
<IconHomeOutline width={24} height={24} />
388
-
) : (
389
-
<IconHome width={24} height={24} />
390
-
)}
391
-
<span className="text-xs mt-1">Home</span>
392
-
</Link>
393
-
<Link
394
-
to="/search"
395
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
396
-
location.pathname.startsWith("/search")
397
-
? "text-gray-900 dark:text-gray-100"
398
-
: "text-gray-600 dark:text-gray-400"
399
-
}`}
400
-
>
401
-
{!location.pathname.startsWith("/search") ? (
402
-
<IconSearch width={24} height={24} />
403
-
) : (
404
-
<IconSearch width={24} height={24} />
405
-
)}
406
-
<span className="text-xs mt-1">Search</span>
407
-
</Link>
408
-
<Link
409
-
to="/notifications"
410
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
411
-
isNotifications
412
-
? "text-gray-900 dark:text-gray-100"
413
-
: "text-gray-600 dark:text-gray-400"
414
-
}`}
415
-
>
416
-
{!isNotifications ? (
417
-
<IconNotificationsOutline width={24} height={24} />
418
-
) : (
419
-
<IconNotifications width={24} height={24} />
420
-
)}
421
-
<span className="text-xs mt-1">Notifications</span>
422
-
</Link>
423
-
<button
424
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
425
-
isProfile
426
-
? "text-gray-900 dark:text-gray-100"
427
-
: "text-gray-600 dark:text-gray-400"
428
-
}`}
429
-
onClick={() => {
430
-
if (authed && agent && agent.assertDid) {
431
-
//window.location.href = `/profile/${agent.assertDid}`;
512
+
{agent?.did ? (
513
+
<nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40">
514
+
<div className="flex justify-around items-center p-2">
515
+
<MaterialNavItem
516
+
small
517
+
InactiveIcon={
518
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
519
+
}
520
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
521
+
active={locationEnum === "home"}
522
+
onClickCallbback={() =>
523
+
navigate({
524
+
to: "/",
525
+
//params: { did: agent.assertDid },
526
+
})
527
+
}
528
+
text="Home"
529
+
/>
530
+
{/* <Link
531
+
to="/"
532
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
533
+
isHome
534
+
? "text-gray-900 dark:text-gray-100"
535
+
: "text-gray-600 dark:text-gray-400"
536
+
}`}
537
+
>
538
+
{!isHome ? (
539
+
<IconMaterialSymbolsHomeOutline width={24} height={24} />
540
+
) : (
541
+
<IconMaterialSymbolsHome width={24} height={24} />
542
+
)}
543
+
<span className="text-xs mt-1">Home</span>
544
+
</Link> */}
545
+
<MaterialNavItem
546
+
small
547
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
548
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
549
+
active={locationEnum === "search"}
550
+
onClickCallbback={() =>
551
+
navigate({
552
+
to: "/search",
553
+
//params: { did: agent.assertDid },
554
+
})
555
+
}
556
+
text="Explore"
557
+
/>
558
+
{/* <Link
559
+
to="/search"
560
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
561
+
location.pathname.startsWith("/search")
562
+
? "text-gray-900 dark:text-gray-100"
563
+
: "text-gray-600 dark:text-gray-400"
564
+
}`}
565
+
>
566
+
{!location.pathname.startsWith("/search") ? (
567
+
<IconMaterialSymbolsSearch width={24} height={24} />
568
+
) : (
569
+
<IconMaterialSymbolsSearch width={24} height={24} />
570
+
)}
571
+
<span className="text-xs mt-1">Search</span>
572
+
</Link> */}
573
+
<MaterialNavItem
574
+
small
575
+
InactiveIcon={
576
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
577
+
}
578
+
ActiveIcon={
579
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
580
+
}
581
+
active={locationEnum === "notifications"}
582
+
onClickCallbback={() =>
583
+
navigate({
584
+
to: "/notifications",
585
+
//params: { did: agent.assertDid },
586
+
})
587
+
}
588
+
text="Notifications"
589
+
/>
590
+
{/* <Link
591
+
to="/notifications"
592
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
593
+
isNotifications
594
+
? "text-gray-900 dark:text-gray-100"
595
+
: "text-gray-600 dark:text-gray-400"
596
+
}`}
597
+
>
598
+
{!isNotifications ? (
599
+
<IconMaterialSymbolsNotificationsOutline
600
+
width={24}
601
+
height={24}
602
+
/>
603
+
) : (
604
+
<IconMaterialSymbolsNotifications width={24} height={24} />
605
+
)}
606
+
<span className="text-xs mt-1">Notifications</span>
607
+
</Link> */}
608
+
<MaterialNavItem
609
+
small
610
+
InactiveIcon={
611
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
612
+
}
613
+
ActiveIcon={
614
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
615
+
}
616
+
active={locationEnum === "profile"}
617
+
onClickCallbback={() => {
618
+
if (authed && agent && agent.assertDid) {
619
+
//window.location.href = `/profile/${agent.assertDid}`;
620
+
navigate({
621
+
to: "/profile/$did",
622
+
params: { did: agent.assertDid },
623
+
});
624
+
}
625
+
}}
626
+
text="Profile"
627
+
/>
628
+
{/* <button
629
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
630
+
isProfile
631
+
? "text-gray-900 dark:text-gray-100"
632
+
: "text-gray-600 dark:text-gray-400"
633
+
}`}
634
+
onClick={() => {
635
+
if (authed && agent && agent.assertDid) {
636
+
//window.location.href = `/profile/${agent.assertDid}`;
637
+
navigate({
638
+
to: "/profile/$did",
639
+
params: { did: agent.assertDid },
640
+
});
641
+
}
642
+
}}
643
+
type="button"
644
+
>
645
+
<IconMaterialSymbolsAccountCircleOutline width={24} height={24} />
646
+
<span className="text-xs mt-1">Profile</span>
647
+
</button> */}
648
+
<MaterialNavItem
649
+
small
650
+
InactiveIcon={
651
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
652
+
}
653
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
654
+
active={locationEnum === "settings"}
655
+
onClickCallbback={() =>
432
656
navigate({
433
-
to: "/profile/$did",
434
-
params: { did: agent.assertDid },
435
-
});
657
+
to: "/settings",
658
+
//params: { did: agent.assertDid },
659
+
})
436
660
}
437
-
}}
438
-
type="button"
439
-
>
440
-
<IconAccountCircleOutline width={24} height={24} />
441
-
<span className="text-xs mt-1">Profile</span>
442
-
</button>
443
-
<Link
444
-
to="/settings"
445
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
446
-
location.pathname.startsWith("/settings")
447
-
? "text-gray-900 dark:text-gray-100"
448
-
: "text-gray-600 dark:text-gray-400"
449
-
}`}
450
-
>
451
-
{!location.pathname.startsWith("/settings") ? (
452
-
<IconSettingsOutline width={24} height={24} />
453
-
) : (
454
-
<IconSettings width={24} height={24} />
455
-
)}
456
-
<span className="text-xs mt-1">Settings</span>
457
-
</Link>
661
+
text="Settings"
662
+
/>
663
+
{/* <Link
664
+
to="/settings"
665
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
666
+
location.pathname.startsWith("/settings")
667
+
? "text-gray-900 dark:text-gray-100"
668
+
: "text-gray-600 dark:text-gray-400"
669
+
}`}
670
+
>
671
+
{!location.pathname.startsWith("/settings") ? (
672
+
<IconMaterialSymbolsSettingsOutline width={24} height={24} />
673
+
) : (
674
+
<IconMaterialSymbolsSettings width={24} height={24} />
675
+
)}
676
+
<span className="text-xs mt-1">Settings</span>
677
+
</Link> */}
678
+
</div>
679
+
</nav>
680
+
) : (
681
+
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
682
+
<div className="flex items-center gap-2">
683
+
<FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
684
+
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
685
+
Red Dwarf{" "}
686
+
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
687
+
lite
688
+
</span> */}
689
+
</span>
690
+
</div>
691
+
<div className="flex items-center gap-2">
692
+
<Login compact={true} popup={true} />
693
+
</div>
458
694
</div>
459
-
</nav>
695
+
)}
460
696
461
-
<TanStackRouterDevtools position="bottom-right" />
697
+
<TanStackRouterDevtools position="bottom-left" />
462
698
<Scripts />
463
699
</>
464
700
);
465
701
}
702
+
703
+
function MaterialNavItem({
704
+
InactiveIcon,
705
+
ActiveIcon,
706
+
text,
707
+
active,
708
+
onClickCallbback,
709
+
small,
710
+
}: {
711
+
InactiveIcon: React.ReactElement;
712
+
ActiveIcon: React.ReactElement;
713
+
text: string;
714
+
active: boolean;
715
+
onClickCallbback: () => void;
716
+
small?: boolean | string;
717
+
}) {
718
+
if (small)
719
+
return (
720
+
<button
721
+
className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${
722
+
active
723
+
? "text-gray-900 dark:text-gray-100"
724
+
: "text-gray-600 dark:text-gray-400"
725
+
}`}
726
+
onClick={() => {
727
+
onClickCallbback();
728
+
}}
729
+
>
730
+
<div
731
+
className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`}
732
+
>
733
+
{active ? ActiveIcon : InactiveIcon}
734
+
</div>
735
+
<span
736
+
className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`}
737
+
>
738
+
{text}
739
+
</span>
740
+
</button>
741
+
);
742
+
743
+
return (
744
+
<button
745
+
className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${
746
+
active
747
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700"
748
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900"
749
+
}`}
750
+
onClick={() => {
751
+
onClickCallbback();
752
+
}}
753
+
>
754
+
<div className={`mr-4 ${active ? " " : " "}`}>
755
+
{active ? ActiveIcon : InactiveIcon}
756
+
</div>
757
+
<span
758
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
759
+
>
760
+
{text}
761
+
</span>
762
+
</button>
763
+
);
764
+
}
765
+
766
+
function MaterialPillButton({
767
+
InactiveIcon,
768
+
ActiveIcon,
769
+
text,
770
+
//active,
771
+
onClickCallbback,
772
+
small,
773
+
}: {
774
+
InactiveIcon: React.ReactElement;
775
+
ActiveIcon: React.ReactElement;
776
+
text: string;
777
+
//active: boolean;
778
+
onClickCallbback: () => void;
779
+
small?: boolean | string;
780
+
}) {
781
+
const active = false;
782
+
return (
783
+
<button
784
+
className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${
785
+
active
786
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
787
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
788
+
}`}
789
+
onClick={() => {
790
+
onClickCallbback();
791
+
}}
792
+
>
793
+
<div className={`${!small && "mr-2"} ${active ? " " : " "}`}>
794
+
{active ? ActiveIcon : InactiveIcon}
795
+
</div>
796
+
{!small && (
797
+
<span
798
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
799
+
>
800
+
{text}
801
+
</span>
802
+
)}
803
+
</button>
804
+
);
805
+
}
+66
-52
src/routes/index.tsx
+66
-52
src/routes/index.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
import { useAtom } from "jotai";
3
3
import * as React from "react";
4
-
import { useEffect, useLayoutEffect } from "react";
4
+
import { useLayoutEffect, useState } from "react";
5
5
6
+
import { Header } from "~/components/Header";
6
7
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
7
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
9
import {
9
-
agentAtom,
10
-
authedAtom,
11
10
feedScrollPositionsAtom,
11
+
isAtTopAtom,
12
+
quickAuthAtom,
12
13
selectedFeedUriAtom,
13
-
store,
14
14
} from "~/utils/atoms";
15
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
16
import {
···
105
105
} = useAuth();
106
106
const authed = !!agent?.did;
107
107
108
-
useEffect(() => {
109
-
if (agent?.did) {
110
-
store.set(authedAtom, true);
111
-
} else {
112
-
store.set(authedAtom, false);
113
-
}
114
-
}, [status, agent, authed]);
115
-
useEffect(() => {
116
-
if (agent) {
117
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
118
-
// @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
119
-
store.set(agentAtom, agent);
120
-
} else {
121
-
store.set(agentAtom, null);
122
-
}
123
-
}, [status, agent, authed]);
108
+
// i dont remember why this is even here
109
+
// useEffect(() => {
110
+
// if (agent?.did) {
111
+
// store.set(authedAtom, true);
112
+
// } else {
113
+
// store.set(authedAtom, false);
114
+
// }
115
+
// }, [status, agent, authed]);
116
+
// useEffect(() => {
117
+
// if (agent) {
118
+
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
119
+
// // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
120
+
// store.set(agentAtom, agent);
121
+
// } else {
122
+
// store.set(agentAtom, null);
123
+
// }
124
+
// }, [status, agent, authed]);
124
125
125
126
//const { get, set } = usePersistentStore();
126
127
// const [feed, setFeed] = React.useState<any[]>([]);
···
160
161
161
162
// const savedFeeds = savedFeedsPref?.items || [];
162
163
163
-
const identityresultmaybe = useQueryIdentity(agent?.did);
164
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
165
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
166
+
167
+
const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined);
164
168
const identity = identityresultmaybe?.data;
165
169
166
170
const prefsresultmaybe = useQueryPreferences({
167
-
agent: agent ?? undefined,
168
-
pdsUrl: identity?.pds,
171
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
172
+
pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined,
169
173
});
170
174
const prefs = prefsresultmaybe?.data;
171
175
···
176
180
return savedFeedsPref?.items || [];
177
181
}, [prefs]);
178
182
179
-
const [persistentSelectedFeed, setPersistentSelectedFeed] =
180
-
useAtom(selectedFeedUriAtom); // React.useState<string | null>(null);
181
-
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState(
182
-
persistentSelectedFeed
183
-
); // React.useState<string | null>(null);
183
+
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
184
+
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed);
184
185
const selectedFeed = agent?.did
185
186
? persistentSelectedFeed
186
187
: unauthedSelectedFeed;
···
304
305
}, [scrollPositions]);
305
306
306
307
useLayoutEffect(() => {
308
+
if (isAuthRestoring) return;
307
309
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
308
310
309
311
window.scrollTo({ top: savedPosition, behavior: "instant" });
310
312
// eslint-disable-next-line react-hooks/exhaustive-deps
311
-
}, [selectedFeed]);
313
+
}, [selectedFeed, isAuthRestoring]);
312
314
313
315
useLayoutEffect(() => {
314
-
if (!selectedFeed) return;
316
+
if (!selectedFeed || isAuthRestoring) return;
315
317
316
318
const handleScroll = () => {
317
319
scrollPositionsRef.current = {
···
326
328
327
329
setScrollPositions(scrollPositionsRef.current);
328
330
};
329
-
}, [selectedFeed, setScrollPositions]);
331
+
}, [isAuthRestoring, selectedFeed, setScrollPositions]);
330
332
331
-
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
332
-
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
333
+
const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined);
334
+
const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined;
333
335
334
336
// const {
335
337
// data: feedData,
···
345
347
346
348
// const feed = feedData?.feed || [];
347
349
348
-
const isReadyForAuthedFeed =
349
-
authed && agent && identity?.pds && feedServiceDid;
350
-
const isReadyForUnauthedFeed = !authed && selectedFeed;
350
+
const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid;
351
+
const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed;
352
+
353
+
354
+
const [isAtTop] = useAtom(isAtTopAtom);
351
355
352
356
return (
353
357
<div
354
358
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
355
359
>
356
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin">
357
-
{savedFeeds.length > 0 ? (
358
-
savedFeeds.map((item: any, idx: number) => {
360
+
{!isAuthRestoring && savedFeeds.length > 0 ? (
361
+
<div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}>
362
+
{savedFeeds.map((item: any, idx: number) => {
359
363
const label = item.value.split("/").pop() || item.value;
360
364
const isActive = selectedFeed === item.value;
361
365
return (
···
363
367
key={item.value || idx}
364
368
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${
365
369
isActive
366
-
? "bg-gray-500 text-white"
367
-
: item.pinned
368
-
? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
369
-
: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
370
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
371
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
372
+
// ? "bg-gray-500 text-white"
373
+
// : item.pinned
374
+
// ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
375
+
// : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
370
376
}`}
371
377
onClick={() => setSelectedFeed(item.value)}
372
378
title={item.value}
373
379
>
374
380
{label}
375
381
{item.pinned && (
376
-
<span className="ml-1 text-xs text-gray-700 dark:text-gray-200">
382
+
<span
383
+
className={`ml-1 text-xs ${
384
+
isActive
385
+
? "text-gray-900 dark:text-gray-100"
386
+
: "text-gray-600 dark:text-gray-400"
387
+
}`}
388
+
>
377
389
โ
378
390
</span>
379
391
)}
380
392
</button>
381
393
);
382
-
})
383
-
) : (
384
-
<span className="text-xl font-bold ml-2">Home</span>
385
-
)}
386
-
</div>
394
+
})}
395
+
</div>
396
+
) : (
397
+
// <span className="text-xl font-bold ml-2">Home</span>
398
+
<Header title="Home" />
399
+
)}
387
400
{/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>}
388
401
{feedError && <div className="p-4 text-red-500">{feedError.message}</div>}
389
402
{!isFeedLoading && !feedError && feed.length === 0 && (
···
396
409
/>
397
410
))} */}
398
411
399
-
{authed && (!identity?.pds || !feedServiceDid) && (
412
+
{isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && (
400
413
<div className="p-4 text-center text-gray-500">
401
414
Preparing your feed...
402
415
</div>
403
416
)}
404
417
405
-
{isReadyForAuthedFeed || isReadyForUnauthedFeed ? (
418
+
{!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? (
406
419
<InfiniteCustomFeed
420
+
key={selectedFeed!}
407
421
feedUri={selectedFeed!}
408
422
pdsUrl={identity?.pds}
409
423
feedServiceDid={feedServiceDid}
410
424
/>
411
425
) : (
412
426
<div className="p-4 text-center text-gray-500">
413
-
Select a feed to get started.
427
+
Loading.......
414
428
</div>
415
429
)}
416
430
{/* {false && restoringScrollPosition && (
+7
-3
src/routes/notifications.tsx
+7
-3
src/routes/notifications.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
2
3
import React, { useEffect, useRef,useState } from "react";
3
4
4
5
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
+
import { constellationURLAtom } from "~/utils/atoms";
5
7
6
8
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
9
···
70
72
}
71
73
}
72
74
75
+
const [constellationURL] = useAtom(constellationURLAtom)
76
+
73
77
useEffect(() => {
74
78
if (!did) return;
75
79
setLoading(true);
76
80
setError(null);
77
81
const urls = [
78
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`,
79
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
80
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
82
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`,
83
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
84
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
81
85
];
82
86
let ignore = false;
83
87
Promise.all(
+194
-55
src/routes/profile.$did/index.tsx
+194
-55
src/routes/profile.$did/index.tsx
···
1
+
import { RichText } from "@atproto/api";
1
2
import { useQueryClient } from "@tanstack/react-query";
2
-
import { createFileRoute, Link } from "@tanstack/react-router";
3
-
import React from "react";
3
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
import React, { type ReactNode, useEffect, useState } from "react";
4
6
5
-
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
7
+
import { Header } from "~/components/Header";
8
+
import { Button } from "~/components/radix-m3-rd/Button";
9
+
import {
10
+
renderTextWithFacets,
11
+
UniversalPostRendererATURILoader,
12
+
} from "~/components/UniversalPostRenderer";
6
13
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
-
import { toggleFollow, useGetFollowState } from "~/utils/followState";
14
+
import { imgCDNAtom } from "~/utils/atoms";
15
+
import {
16
+
toggleFollow,
17
+
useGetFollowState,
18
+
useGetOneToOneState,
19
+
} from "~/utils/followState";
8
20
import {
9
21
useInfiniteQueryAuthorFeed,
10
22
useQueryIdentity,
···
18
30
function ProfileComponent() {
19
31
// booo bad this is not always the did it might be a handle, use identity.did instead
20
32
const { did } = Route.useParams();
33
+
const navigate = useNavigate();
21
34
const queryClient = useQueryClient();
22
-
const { agent } = useAuth();
23
35
const {
24
36
data: identity,
25
37
isLoading: isIdentityLoading,
26
38
error: identityError,
27
39
} = useQueryIdentity(did);
28
-
29
-
const followRecords = useGetFollowState({
30
-
target: identity?.did || did,
31
-
user: agent?.did,
32
-
});
33
40
34
41
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
35
42
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
···
66
73
[postsData]
67
74
);
68
75
76
+
const [imgcdn] = useAtom(imgCDNAtom);
77
+
69
78
function getAvatarUrl(p: typeof profile) {
70
79
const link = p?.avatar?.ref?.["$link"];
71
80
if (!link || !resolvedDid) return null;
72
-
return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
81
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
73
82
}
74
83
function getBannerUrl(p: typeof profile) {
75
84
const link = p?.banner?.ref?.["$link"];
76
85
if (!link || !resolvedDid) return null;
77
-
return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`;
86
+
return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`;
78
87
}
79
88
80
89
const displayName =
···
104
113
105
114
return (
106
115
<>
107
-
<div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
116
+
<Header
117
+
title={`Profile`}
118
+
backButtonCallback={() => {
119
+
if (window.history.length > 1) {
120
+
window.history.back();
121
+
} else {
122
+
window.location.assign("/");
123
+
}
124
+
}}
125
+
/>
126
+
{/* <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
108
127
<Link
109
128
to=".."
110
129
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
···
121
140
โ
122
141
</Link>
123
142
<span className="text-xl font-bold ml-2">Profile</span>
124
-
</div>
143
+
</div> */}
125
144
126
145
{/* Profile Header */}
127
146
<div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900">
···
151
170
also delay the backfill to be on demand because it would be pretty intense
152
171
also save it persistently
153
172
*/}
154
-
{identity?.did !== agent?.did ? (
155
-
<>
156
-
{!(followRecords?.length && followRecords?.length > 0) ? (
157
-
<button
158
-
onClick={() =>
159
-
toggleFollow({
160
-
agent: agent || undefined,
161
-
targetDid: identity?.did,
162
-
followRecords: followRecords,
163
-
queryClient: queryClient,
164
-
})
165
-
}
166
-
className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"
167
-
>
168
-
Follow
169
-
</button>
170
-
) : (
171
-
<button
172
-
onClick={() =>
173
-
toggleFollow({
174
-
agent: agent || undefined,
175
-
targetDid: identity?.did,
176
-
followRecords: followRecords,
177
-
queryClient: queryClient,
178
-
})
179
-
}
180
-
className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"
181
-
>
182
-
Unfollow
183
-
</button>
184
-
)}
185
-
</>
186
-
) : (
187
-
<button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]">
188
-
Edit Profile
189
-
</button>
190
-
)}
191
-
<button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]">
173
+
<FollowButton targetdidorhandle={did} />
174
+
<Button className="rounded-full" variant={"secondary"}>
192
175
... {/* todo: icon */}
193
-
</button>
176
+
</Button>
194
177
</div>
195
178
196
179
{/* Info Card */}
197
180
<div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100">
198
181
<div className="font-bold text-2xl">{displayName}</div>
199
-
<div className="text-gray-500 dark:text-gray-400 text-base mb-3">
182
+
<div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1">
183
+
<Mutual targetdidorhandle={did} />
200
184
{handle}
201
185
</div>
202
186
{description && (
203
187
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
204
-
{description}
188
+
{/* {description} */}
189
+
<RichTextRenderer key={did} description={description} />
205
190
</div>
206
191
)}
207
192
</div>
···
244
229
</>
245
230
);
246
231
}
232
+
233
+
export function FollowButton({
234
+
targetdidorhandle,
235
+
}: {
236
+
targetdidorhandle: string;
237
+
}) {
238
+
const { agent } = useAuth();
239
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
240
+
const queryClient = useQueryClient();
241
+
242
+
const followRecords = useGetFollowState({
243
+
target: identity?.did ?? targetdidorhandle,
244
+
user: agent?.did,
245
+
});
246
+
247
+
return (
248
+
<>
249
+
{identity?.did !== agent?.did ? (
250
+
<>
251
+
{!(followRecords?.length && followRecords?.length > 0) ? (
252
+
<Button
253
+
onClick={(e) => {
254
+
e.stopPropagation();
255
+
toggleFollow({
256
+
agent: agent || undefined,
257
+
targetDid: identity?.did,
258
+
followRecords: followRecords,
259
+
queryClient: queryClient,
260
+
});
261
+
}}
262
+
>
263
+
Follow
264
+
</Button>
265
+
) : (
266
+
<Button
267
+
onClick={(e) => {
268
+
e.stopPropagation();
269
+
toggleFollow({
270
+
agent: agent || undefined,
271
+
targetDid: identity?.did,
272
+
followRecords: followRecords,
273
+
queryClient: queryClient,
274
+
});
275
+
}}
276
+
>
277
+
Unfollow
278
+
</Button>
279
+
)}
280
+
</>
281
+
) : (
282
+
<Button variant={"secondary"}>Edit Profile</Button>
283
+
)}
284
+
</>
285
+
);
286
+
}
287
+
288
+
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
289
+
const { agent } = useAuth();
290
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
291
+
292
+
const theyFollowYouRes = useGetOneToOneState(
293
+
agent?.did
294
+
? {
295
+
target: agent?.did,
296
+
user: identity?.did ?? targetdidorhandle,
297
+
collection: "app.bsky.graph.follow",
298
+
path: ".subject",
299
+
}
300
+
: undefined
301
+
);
302
+
303
+
const youFollowThemRes = useGetFollowState({
304
+
target: identity?.did ?? targetdidorhandle,
305
+
user: agent?.did,
306
+
});
307
+
308
+
const theyFollowYou: boolean =
309
+
!!theyFollowYouRes?.length && theyFollowYouRes.length > 0;
310
+
const youFollowThem: boolean =
311
+
!!youFollowThemRes?.length && youFollowThemRes.length > 0;
312
+
313
+
return (
314
+
<>
315
+
{/* if not self */}
316
+
{identity?.did !== agent?.did ? (
317
+
<>
318
+
{theyFollowYou ? (
319
+
<>
320
+
{youFollowThem ? (
321
+
<div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
322
+
mutuals
323
+
</div>
324
+
) : (
325
+
<div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
326
+
follows you
327
+
</div>
328
+
)}
329
+
</>
330
+
) : (
331
+
<></>
332
+
)}
333
+
</>
334
+
) : (
335
+
// lmao can someone be mutuals with themselves ??
336
+
<></>
337
+
)}
338
+
</>
339
+
);
340
+
}
341
+
342
+
export function RichTextRenderer({ description }: { description: string }) {
343
+
const [richDescription, setRichDescription] = useState<string | ReactNode[]>(
344
+
description
345
+
);
346
+
const { agent } = useAuth();
347
+
const navigate = useNavigate();
348
+
349
+
useEffect(() => {
350
+
let mounted = true;
351
+
352
+
// setRichDescription(description);
353
+
354
+
async function processRichText() {
355
+
try {
356
+
if (!agent?.did) return;
357
+
const rt = new RichText({ text: description });
358
+
await rt.detectFacets(agent);
359
+
360
+
if (!mounted) return;
361
+
362
+
if (rt.facets) {
363
+
setRichDescription(
364
+
renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate })
365
+
);
366
+
} else {
367
+
setRichDescription(rt.text);
368
+
}
369
+
} catch (error) {
370
+
console.error("Failed to detect facets:", error);
371
+
if (mounted) {
372
+
setRichDescription(description);
373
+
}
374
+
}
375
+
}
376
+
377
+
processRichText();
378
+
379
+
return () => {
380
+
mounted = false;
381
+
};
382
+
}, [description, agent, navigate]);
383
+
384
+
return <>{richDescription}</>;
385
+
}
+165
src/routes/profile.$did/post.$rkey.image.$i.tsx
+165
src/routes/profile.$did/post.$rkey.image.$i.tsx
···
1
+
import {
2
+
createFileRoute,
3
+
useNavigate,
4
+
type UseNavigateResult,
5
+
} from "@tanstack/react-router";
6
+
import { useEffect, useState } from "react";
7
+
import { createPortal } from "react-dom";
8
+
9
+
import { ProfilePostComponent } from "./post.$rkey";
10
+
11
+
export const Route = createFileRoute("/profile/$did/post/$rkey/image/$i")({
12
+
component: Lightbox,
13
+
});
14
+
15
+
export type LightboxProps = {
16
+
images: { src: string; alt?: string }[];
17
+
};
18
+
19
+
function nextprev({
20
+
index,
21
+
images,
22
+
navigate,
23
+
did,
24
+
rkey,
25
+
prev,
26
+
}: {
27
+
index?: number;
28
+
images?: LightboxProps["images"];
29
+
navigate: UseNavigateResult<string>;
30
+
did: string;
31
+
rkey: string;
32
+
prev?: boolean;
33
+
}) {
34
+
const len = images?.length ?? 0;
35
+
if (len === 0) return;
36
+
37
+
const nextIndex = ((index ?? 0) + (prev ? -1 : 1) + len) % len;
38
+
39
+
navigate({
40
+
to: "/profile/$did/post/$rkey/image/$i",
41
+
params: {
42
+
did,
43
+
rkey,
44
+
i: nextIndex.toString(),
45
+
},
46
+
replace: true,
47
+
});
48
+
}
49
+
50
+
export function Lightbox() {
51
+
console.log("hey the $i route is loaded w!!!");
52
+
const { did, rkey, i } = Route.useParams();
53
+
const [images, setImages] = useState<LightboxProps["images"] | undefined>(
54
+
undefined
55
+
);
56
+
const index = Number(i);
57
+
const navigate = useNavigate();
58
+
const post = true;
59
+
const image = images?.[index] ?? undefined;
60
+
61
+
function lightboxCallback(d: LightboxProps) {
62
+
console.log("callback actually called!");
63
+
setImages(d.images);
64
+
}
65
+
66
+
useEffect(() => {
67
+
function handleKey(e: KeyboardEvent) {
68
+
if (e.key === "Escape") window.history.back();
69
+
if (e.key === "ArrowRight")
70
+
nextprev({ index, images, navigate, did, rkey });
71
+
//onNavigate((index + 1) % images.length);
72
+
if (e.key === "ArrowLeft")
73
+
nextprev({ index, images, navigate, did, rkey, prev: true });
74
+
//onNavigate((index - 1 + images.length) % images.length);
75
+
}
76
+
window.addEventListener("keydown", handleKey);
77
+
return () => window.removeEventListener("keydown", handleKey);
78
+
}, [index, navigate, did, rkey, images]);
79
+
80
+
return createPortal(
81
+
<>
82
+
{post && (
83
+
<div
84
+
onClick={(e) => {
85
+
e.stopPropagation();
86
+
e.nativeEvent.stopImmediatePropagation();
87
+
}}
88
+
className="lightbox-sidebar hidden lg:flex overscroll-none disablegutter disablescroll border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white"
89
+
>
90
+
<ProfilePostComponent
91
+
key={`/profile/${did}/post/${rkey}`}
92
+
did={did}
93
+
rkey={rkey}
94
+
nopics
95
+
lightboxCallback={lightboxCallback}
96
+
/>
97
+
</div>
98
+
)}
99
+
<div
100
+
className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px)] lg:max-w-[calc(100vw-350px)]"
101
+
onClick={(e) => {
102
+
e.stopPropagation();
103
+
window.history.back();
104
+
}}
105
+
>
106
+
<img
107
+
src={image?.src}
108
+
alt={image?.alt}
109
+
className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg"
110
+
onClick={(e) => e.stopPropagation()}
111
+
/>
112
+
113
+
{(images?.length ?? 0) > 1 && (
114
+
<>
115
+
<button
116
+
onClick={(e) => {
117
+
e.stopPropagation();
118
+
nextprev({ index, images, navigate, did, rkey, prev: true });
119
+
}}
120
+
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
121
+
>
122
+
<svg
123
+
xmlns="http://www.w3.org/2000/svg"
124
+
width={28}
125
+
height={28}
126
+
viewBox="0 0 24 24"
127
+
>
128
+
<g fill="none" fillRule="evenodd">
129
+
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
130
+
<path
131
+
fill="currentColor"
132
+
d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z"
133
+
></path>
134
+
</g>
135
+
</svg>
136
+
</button>
137
+
<button
138
+
onClick={(e) => {
139
+
e.stopPropagation();
140
+
nextprev({ index, images, navigate, did, rkey });
141
+
}}
142
+
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
143
+
>
144
+
<svg
145
+
xmlns="http://www.w3.org/2000/svg"
146
+
width={28}
147
+
height={28}
148
+
viewBox="0 0 24 24"
149
+
>
150
+
<g fill="none" fillRule="evenodd">
151
+
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
152
+
<path
153
+
fill="currentColor"
154
+
d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z"
155
+
></path>
156
+
</g>
157
+
</svg>
158
+
</button>
159
+
</>
160
+
)}
161
+
</div>
162
+
</>,
163
+
document.body
164
+
);
165
+
}
+229
-80
src/routes/profile.$did/post.$rkey.tsx
+229
-80
src/routes/profile.$did/post.$rkey.tsx
···
1
-
import { useQueryClient } from "@tanstack/react-query";
2
-
import { createFileRoute } from "@tanstack/react-router";
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";
3
5
import React, { useLayoutEffect } from "react";
4
6
7
+
import { Header } from "~/components/Header";
5
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
9
+
import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms";
6
10
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
7
11
import {
8
12
constructPostQuery,
13
+
type linksAllResponse,
14
+
type linksRecordsResponse,
9
15
useQueryConstellation,
10
16
useQueryIdentity,
11
17
useQueryPost,
18
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
12
19
} from "~/utils/useQuery";
20
+
21
+
import type { LightboxProps } from "./post.$rkey.image.$i";
13
22
14
23
//const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
15
24
···
36
45
did,
37
46
rkey,
38
47
nopics,
48
+
lightboxCallback,
39
49
}: {
40
50
did: string;
41
51
rkey: string;
42
-
nopics?: () => void;
52
+
nopics?: boolean;
53
+
lightboxCallback?: (d: LightboxProps) => void;
43
54
}) {
44
55
//const { get, set } = usePersistentStore();
45
56
const queryClient = useQueryClient();
···
187
198
() =>
188
199
resolvedDid
189
200
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
190
-
: "",
201
+
: undefined,
191
202
[resolvedDid, rkey]
192
203
);
193
204
194
205
const { data: mainPost } = useQueryPost(atUri);
195
206
196
-
const { data: repliesData } = useQueryConstellation({
197
-
method: "/links",
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",
198
220
target: atUri,
199
-
collection: "app.bsky.feed.post",
200
-
path: ".reply.parent.uri",
221
+
} : {
222
+
method: "undefined",
223
+
target: ""
224
+
})as { data: linksAllResponse | undefined };
225
+
226
+
//const [likes, setLikes] = React.useState<number | null>(null);
227
+
//const [reposts, setReposts] = React.useState<number | null>(null);
228
+
const [replyCount, setReplyCount] = React.useState<number | null>(null);
229
+
230
+
React.useEffect(() => {
231
+
// /*mass comment*/ console.log(JSON.stringify(links, null, 2));
232
+
// setLikes(
233
+
// links
234
+
// ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0
235
+
// : null
236
+
// );
237
+
// setReposts(
238
+
// links
239
+
// ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0
240
+
// : null
241
+
// );
242
+
setReplyCount(
243
+
links
244
+
? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]
245
+
?.records || 0
246
+
: null
247
+
);
248
+
}, [links]);
249
+
250
+
const { data: opreplies } = useQueryConstellation(
251
+
!!opdid && replyCount && replyCount >= 25
252
+
? {
253
+
method: "/links",
254
+
target: atUri,
255
+
// @ts-expect-error overloading sucks so much
256
+
collection: "app.bsky.feed.post",
257
+
path: ".reply.parent.uri",
258
+
//cursor?: string;
259
+
dids: [opdid],
260
+
}
261
+
: {
262
+
method: "undefined",
263
+
target: "",
264
+
}
265
+
) as { data: linksRecordsResponse | undefined };
266
+
267
+
const opReplyAturis =
268
+
opreplies?.linking_records.map(
269
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`,
270
+
) ?? [];
271
+
272
+
273
+
// const { data: repliesData } = useQueryConstellation({
274
+
// method: "/links",
275
+
// target: atUri,
276
+
// collection: "app.bsky.feed.post",
277
+
// path: ".reply.parent.uri",
278
+
// });
279
+
// const replies = repliesData?.linking_records.slice(0, 50) ?? [];
280
+
const [constellationurl] = useAtom(constellationURLAtom)
281
+
282
+
const infinitequeryresults = useInfiniteQuery({
283
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
284
+
{
285
+
constellation: constellationurl,
286
+
method: "/links",
287
+
target: atUri,
288
+
collection: "app.bsky.feed.post",
289
+
path: ".reply.parent.uri",
290
+
}
291
+
),
292
+
enabled: !!atUri,
201
293
});
202
-
const replies = repliesData?.linking_records.slice(0, 50) ?? [];
294
+
295
+
const {
296
+
data: infiniteRepliesData,
297
+
fetchNextPage,
298
+
hasNextPage,
299
+
isFetchingNextPage,
300
+
} = infinitequeryresults;
301
+
302
+
// // auto-fetch all pages
303
+
// useEffect(() => {
304
+
// if (
305
+
// infinitequeryresults.hasNextPage &&
306
+
// !infinitequeryresults.isFetchingNextPage
307
+
// ) {
308
+
// console.log("Fetching the next page...");
309
+
// infinitequeryresults.fetchNextPage();
310
+
// }
311
+
// }, [infinitequeryresults]);
312
+
313
+
// const replyAturis = repliesData
314
+
// ? repliesData.pages.flatMap((page) =>
315
+
// page
316
+
// ? page.linking_records.map((record) => {
317
+
// const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
318
+
// return aturi;
319
+
// })
320
+
// : []
321
+
// )
322
+
// : [];
323
+
324
+
const replyAturis = React.useMemo(() => {
325
+
// Get all replies from the standard infinite query
326
+
const allReplies =
327
+
infiniteRepliesData?.pages.flatMap(
328
+
(page) =>
329
+
page?.linking_records.map(
330
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`,
331
+
) ?? [],
332
+
) ?? [];
333
+
334
+
if (replyCount && (replyCount < 25)) {
335
+
// If count is low, just use the standard list and find the oldest OP reply to move to the top
336
+
const opdidFromUri = atUri ? new AtUri(atUri).host : undefined;
337
+
const oldestOpsIndex = allReplies.findIndex(
338
+
(aturi) => new AtUri(aturi).host === opdidFromUri,
339
+
);
340
+
if (oldestOpsIndex > 0) {
341
+
const [oldestOpsReply] = allReplies.splice(oldestOpsIndex, 1);
342
+
allReplies.unshift(oldestOpsReply);
343
+
}
344
+
return allReplies;
345
+
} else {
346
+
// If count is high, prioritize OP replies from the special query
347
+
// and filter them out from the main list to avoid duplication.
348
+
const opReplySet = new Set(opReplyAturis);
349
+
const otherReplies = allReplies.filter((uri) => !opReplySet.has(uri));
350
+
return [...opReplyAturis, ...otherReplies];
351
+
}
352
+
}, [infiniteRepliesData, opReplyAturis, replyCount, atUri]);
353
+
354
+
// Find oldest OP reply
355
+
const oldestOpsIndex = replyAturis.findIndex(
356
+
(aturi) => new AtUri(aturi).host === opdid
357
+
);
358
+
359
+
// Reorder: move oldest OP reply to the front
360
+
if (oldestOpsIndex > 0) {
361
+
const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1);
362
+
replyAturis.unshift(oldestOpsReply);
363
+
}
203
364
204
365
const [parents, setParents] = React.useState<any[]>([]);
205
366
const [parentsLoading, setParentsLoading] = React.useState(false);
206
367
207
368
const mainPostRef = React.useRef<HTMLDivElement>(null);
208
-
const userHasScrolled = React.useRef(false);
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;
209
380
210
-
const scrollAnchor = React.useRef<{ top: number } | null>(null);
381
+
const targetScrollY = elementTop - headerOffset;
211
382
212
-
React.useEffect(() => {
213
-
const onScroll = () => {
214
-
if (window.scrollY > 50) {
215
-
userHasScrolled.current = true;
383
+
window.scrollBy(0, targetScrollY);
216
384
217
-
window.removeEventListener("scroll", onScroll);
385
+
hasPerformedInitialLayout.current = true;
218
386
}
219
-
};
220
-
221
-
if (!userHasScrolled.current) {
222
-
window.addEventListener("scroll", onScroll, { passive: true });
387
+
388
+
// todo idk what to do with this
389
+
// eslint-disable-next-line react-hooks/set-state-in-effect
390
+
setLayoutReady(true);
223
391
}
224
-
return () => window.removeEventListener("scroll", onScroll);
225
-
}, []);
392
+
}, [parents, layoutReady]);
226
393
227
-
useLayoutEffect(() => {
228
-
if (parentsLoading && mainPostRef.current && !userHasScrolled.current) {
229
-
scrollAnchor.current = {
230
-
top: mainPostRef.current.getBoundingClientRect().top,
231
-
};
394
+
395
+
const [slingshoturl] = useAtom(slingshotURLAtom)
396
+
397
+
React.useEffect(() => {
398
+
if (parentsLoading) {
399
+
setLayoutReady(false);
232
400
}
233
-
}, [parentsLoading]);
234
401
235
-
useLayoutEffect(() => {
236
-
if (
237
-
scrollAnchor.current &&
238
-
mainPostRef.current &&
239
-
!userHasScrolled.current
240
-
) {
241
-
const newTop = mainPostRef.current.getBoundingClientRect().top;
242
-
const topDiff = newTop - scrollAnchor.current.top;
243
-
if (topDiff > 0) {
244
-
window.scrollBy(0, topDiff);
245
-
}
246
-
scrollAnchor.current = null;
402
+
if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) {
403
+
setLayoutReady(true);
404
+
hasPerformedInitialLayout.current = true;
247
405
}
248
-
}, [parents]);
406
+
}, [parentsLoading, mainPost]);
249
407
250
408
React.useEffect(() => {
251
409
if (!mainPost?.value?.reply?.parent?.uri) {
···
264
422
while (currentParentUri && safetyCounter < MAX_PARENTS) {
265
423
try {
266
424
const parentPost = await queryClient.fetchQuery(
267
-
constructPostQuery(currentParentUri)
425
+
constructPostQuery(currentParentUri, slingshoturl)
268
426
);
269
427
if (!parentPost) break;
270
428
parentChain.push(parentPost);
···
296
454
297
455
return (
298
456
<>
299
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
300
-
{!nopics ? (
301
-
<button
302
-
//to=".."
303
-
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
304
-
onClick={(e) => {
305
-
e.preventDefault();
306
-
if (window.history.length > 1) {
307
-
window.history.back();
308
-
} else {
309
-
window.location.assign("/");
310
-
}
311
-
}}
312
-
aria-label="Go back"
313
-
>
314
-
โ
315
-
</button>
316
-
) : (
317
-
<button
318
-
//to=".."
319
-
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
320
-
onClick={(e) => {
321
-
e.preventDefault();
322
-
nopics();
323
-
}}
324
-
aria-label="Go back"
325
-
>
326
-
โ
327
-
</button>
328
-
)}
329
-
<span className="text-xl font-bold ml-2">Post</span>
330
-
</div>
457
+
<Outlet />
458
+
<Header
459
+
title={`Post`}
460
+
backButtonCallback={() => {
461
+
if (window.history.length > 1) {
462
+
window.history.back();
463
+
} else {
464
+
window.location.assign("/");
465
+
}
466
+
}}
467
+
/>
331
468
332
469
{parentsLoading && (
333
470
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
···
359
496
detailed={true}
360
497
topReplyLine={parentsLoading || parents.length > 0}
361
498
nopics={!!nopics}
499
+
lightboxCallback={lightboxCallback}
362
500
/>
363
501
</div>
364
502
<div
···
366
504
maxWidth: 600,
367
505
//margin: "0px auto 0",
368
506
padding: 0,
369
-
minHeight: "100dvh",
507
+
minHeight: "80dvh",
508
+
paddingBottom: "20dvh",
370
509
}}
371
510
>
372
511
<div
···
380
519
Replies
381
520
</div>
382
521
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
383
-
{replies.length > 0 &&
384
-
replies.map((reply) => {
385
-
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
522
+
{replyAturis.length > 0 &&
523
+
replyAturis.map((reply) => {
524
+
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
386
525
return (
387
526
<UniversalPostRendererATURILoader
388
-
key={replyAtUri}
389
-
atUri={replyAtUri}
527
+
key={reply}
528
+
atUri={reply}
529
+
maxReplies={4}
390
530
/>
391
531
);
392
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
+
)}
393
542
</div>
394
543
</div>
395
544
</>
+50
-1
src/routes/search.tsx
+50
-1
src/routes/search.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
3
+
import { Header } from "~/components/Header";
4
+
import { Import } from "~/components/Import";
5
+
3
6
export const Route = createFileRoute("/search")({
4
7
component: Search,
5
8
});
6
9
7
10
export function Search() {
8
-
return <div className="p-6">Search page (coming soon)</div>;
11
+
return (
12
+
<>
13
+
<Header
14
+
title="Explore"
15
+
backButtonCallback={() => {
16
+
if (window.history.length > 1) {
17
+
window.history.back();
18
+
} else {
19
+
window.location.assign("/");
20
+
}
21
+
}}
22
+
/>
23
+
<div className=" flex flex-col items-center mt-4 mx-4 gap-4">
24
+
<Import />
25
+
<div className="flex flex-col">
26
+
<p className="text-gray-600 dark:text-gray-400">
27
+
Sorry we dont have search. But instead, you can load some of these
28
+
types of content into Red Dwarf:
29
+
</p>
30
+
<ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400">
31
+
<li>
32
+
Bluesky URLs from supported clients (like{" "}
33
+
<code className="text-sm">bsky.app</code> or{" "}
34
+
<code className="text-sm">deer.social</code>).
35
+
</li>
36
+
<li>
37
+
AT-URIs (e.g.,{" "}
38
+
<code className="text-sm">at://did:example/collection/item</code>
39
+
).
40
+
</li>
41
+
<li>
42
+
Plain handles (like{" "}
43
+
<code className="text-sm">@username.bsky.social</code>).
44
+
</li>
45
+
<li>
46
+
Direct DIDs (Decentralized Identifiers, starting with{" "}
47
+
<code className="text-sm">did:</code>).
48
+
</li>
49
+
</ul>
50
+
<p className="mt-2 text-gray-600 dark:text-gray-400">
51
+
Simply paste one of these into the import field above and press
52
+
Enter to load the content.
53
+
</p>
54
+
</div>
55
+
</div>
56
+
</>
57
+
);
9
58
}
+181
-1
src/routes/settings.tsx
+181
-1
src/routes/settings.tsx
···
1
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
+
constellationURLAtom,
9
+
defaultconstellationURL,
10
+
defaulthue,
11
+
defaultImgCDN,
12
+
defaultslingshotURL,
13
+
defaultVideoCDN,
14
+
hueAtom,
15
+
imgCDNAtom,
16
+
slingshotURLAtom,
17
+
videoCDNAtom,
18
+
} from "~/utils/atoms";
2
19
3
20
export const Route = createFileRoute("/settings")({
4
21
component: Settings,
5
22
});
6
23
7
24
export function Settings() {
8
-
return <div className="p-6">Settings page (coming soon)</div>;
25
+
return (
26
+
<>
27
+
<Header
28
+
title="Settings"
29
+
backButtonCallback={() => {
30
+
if (window.history.length > 1) {
31
+
window.history.back();
32
+
} else {
33
+
window.location.assign("/");
34
+
}
35
+
}}
36
+
/>
37
+
<div className="lg:hidden">
38
+
<Login />
39
+
</div>
40
+
<div className="h-4" />
41
+
<TextInputSetting
42
+
atom={constellationURLAtom}
43
+
title={"Constellation"}
44
+
description={
45
+
"Customize the Constellation instance to be used by Red Dwarf"
46
+
}
47
+
init={defaultconstellationURL}
48
+
/>
49
+
<TextInputSetting
50
+
atom={slingshotURLAtom}
51
+
title={"Slingshot"}
52
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
53
+
init={defaultslingshotURL}
54
+
/>
55
+
<TextInputSetting
56
+
atom={imgCDNAtom}
57
+
title={"Image CDN"}
58
+
description={
59
+
"Customize the Constellation instance to be used by Red Dwarf"
60
+
}
61
+
init={defaultImgCDN}
62
+
/>
63
+
<TextInputSetting
64
+
atom={videoCDNAtom}
65
+
title={"Video CDN"}
66
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
67
+
init={defaultVideoCDN}
68
+
/>
69
+
70
+
<Hue />
71
+
<p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm">
72
+
please restart/refresh the app if changes arent applying correctly
73
+
</p>
74
+
</>
75
+
);
76
+
}
77
+
function Hue() {
78
+
const [hue, setHue] = useAtom(hueAtom);
79
+
return (
80
+
<div className="flex flex-col px-4 mt-4 ">
81
+
<span className="z-10">Hue</span>
82
+
<div className="flex flex-row items-center gap-4">
83
+
<SliderComponent
84
+
atom={hueAtom}
85
+
max={360}
86
+
/>
87
+
<button
88
+
onClick={() => setHue(defaulthue ?? 28)}
89
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
90
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
91
+
>
92
+
Reset
93
+
</button>
94
+
</div>
95
+
</div>
96
+
);
9
97
}
98
+
99
+
export function TextInputSetting({
100
+
atom,
101
+
title,
102
+
description,
103
+
init,
104
+
}: {
105
+
atom: typeof constellationURLAtom;
106
+
title?: string;
107
+
description?: string;
108
+
init?: string;
109
+
}) {
110
+
const [value, setValue] = useAtom(atom);
111
+
return (
112
+
<div className="flex flex-col gap-2 px-4 py-2">
113
+
{/* <div>
114
+
{title && (
115
+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
116
+
{title}
117
+
</h3>
118
+
)}
119
+
{description && (
120
+
<p className="text-sm text-gray-500 dark:text-gray-400">
121
+
{description}
122
+
</p>
123
+
)}
124
+
</div> */}
125
+
126
+
<div className="flex flex-row gap-2 items-center">
127
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
128
+
<input
129
+
type="text"
130
+
placeholder=" "
131
+
value={value}
132
+
onChange={(e) => setValue(e.target.value)}
133
+
/>
134
+
<label>{title}</label>
135
+
</div>
136
+
{/* <input
137
+
type="text"
138
+
value={value}
139
+
onChange={(e) => setValue(e.target.value)}
140
+
className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700
141
+
text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400
142
+
focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600"
143
+
placeholder="Enter value..."
144
+
/> */}
145
+
<button
146
+
onClick={() => setValue(init ?? "")}
147
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
148
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
149
+
>
150
+
Reset
151
+
</button>
152
+
</div>
153
+
</div>
154
+
);
155
+
}
156
+
157
+
158
+
interface SliderProps {
159
+
atom: typeof hueAtom;
160
+
min?: number;
161
+
max?: number;
162
+
step?: number;
163
+
}
164
+
165
+
export const SliderComponent: React.FC<SliderProps> = ({
166
+
atom,
167
+
min = 0,
168
+
max = 100,
169
+
step = 1,
170
+
}) => {
171
+
172
+
const [value, setValue] = useAtom(atom)
173
+
174
+
return (
175
+
<Slider.Root
176
+
className="relative flex items-center w-full h-4"
177
+
value={[value]}
178
+
min={min}
179
+
max={max}
180
+
step={step}
181
+
onValueChange={(v: number[]) => setValue(v[0])}
182
+
>
183
+
<Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
184
+
<Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" />
185
+
</Slider.Track>
186
+
<Slider.Thumb className="shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" />
187
+
</Slider.Root>
188
+
);
189
+
};
+174
-13
src/styles/app.css
+174
-13
src/styles/app.css
···
1
+
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Spectral+SC:wght@500&display=swap');
1
2
@import "tailwindcss";
2
3
3
4
/* @theme {
···
14
15
--color-gray-950: oklch(0.129 0.050 222.000);
15
16
} */
16
17
18
+
:root {
19
+
--safe-hue: var(--tw-gray-hue, 28)
20
+
}
21
+
17
22
@theme {
18
-
--color-gray-50: oklch(0.984 0.012 28);
19
-
--color-gray-100: oklch(0.968 0.017 28);
20
-
--color-gray-200: oklch(0.929 0.025 28);
21
-
--color-gray-300: oklch(0.869 0.035 28);
22
-
--color-gray-400: oklch(0.704 0.05 28);
23
-
--color-gray-500: oklch(0.554 0.06 28);
24
-
--color-gray-600: oklch(0.446 0.058 28);
25
-
--color-gray-700: oklch(0.372 0.058 28);
26
-
--color-gray-800: oklch(0.279 0.055 28);
27
-
--color-gray-900: oklch(0.208 0.055 28);
28
-
--color-gray-950: oklch(0.129 0.055 28);
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));
29
34
}
30
35
31
36
@layer base {
···
47
52
}
48
53
}
49
54
55
+
.gutter{
56
+
scrollbar-gutter: stable both-edges;
57
+
}
58
+
50
59
@media (width >= 64rem /* 1024px */) {
51
60
html:not(:has(.disablegutter)),
52
61
body:not(:has(.disablegutter)) {
53
62
scrollbar-gutter: stable both-edges !important;
54
63
}
55
-
html:has(.disablegutter),
56
-
body:has(.disablegutter) {
64
+
html:has(.disablescroll),
65
+
body:has(.disablescroll) {
57
66
scrollbar-width: none;
58
67
overflow-y: hidden;
59
68
}
···
71
80
.scroll-none {
72
81
scrollbar-width: none;
73
82
}
83
+
84
+
.dangerousFediContent {
85
+
& a[href]{
86
+
text-decoration: none;
87
+
color: rgb(29, 122, 242);
88
+
word-break: break-all;
89
+
}
90
+
}
91
+
92
+
.font-inter {
93
+
font-family: "Inter", sans-serif;
94
+
}
95
+
.font-roboto {
96
+
font-family: "Roboto", sans-serif;
97
+
}
98
+
99
+
:root {
100
+
--header-bg-light: color-mix(in srgb, var(--color-white) calc(var(--is-top) * 100%), var(--color-gray-50));
101
+
--header-bg-dark: color-mix(in srgb, var(--color-gray-950) calc(var(--is-top) * 100%), var(--color-gray-900));
102
+
}
103
+
104
+
:root {
105
+
--header-bg: var(--header-bg-light);
106
+
}
107
+
@media (prefers-color-scheme: dark) {
108
+
:root {
109
+
--header-bg: var(--header-bg-dark);
110
+
}
111
+
}
112
+
113
+
:root {
114
+
--shadow-opacity: calc(1 - var(--is-top));
115
+
--tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15));
116
+
}
117
+
118
+
119
+
/* m3 input */
120
+
:root {
121
+
--m3input-radius: 6px;
122
+
--m3input-border-width: .0625rem;
123
+
--m3input-font-size: 16px;
124
+
--m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1);
125
+
/* light theme */
126
+
--m3input-bg: var(--color-gray-50);
127
+
--m3input-border-color: var(--color-gray-400);
128
+
--m3input-label-color: var(--color-gray-500);
129
+
--m3input-text-color: var(--color-gray-900);
130
+
--m3input-focus-color: var(--color-gray-600);
131
+
}
132
+
133
+
@media (prefers-color-scheme: dark) {
134
+
:root {
135
+
--m3input-bg: var(--color-gray-950);
136
+
--m3input-border-color: var(--color-gray-700);
137
+
--m3input-label-color: var(--color-gray-400);
138
+
--m3input-text-color: var(--color-gray-50);
139
+
--m3input-focus-color: var(--color-gray-400);
140
+
}
141
+
}
142
+
143
+
/* reset page *//*
144
+
html,
145
+
body {
146
+
background: var(--m3input-bg);
147
+
margin: 0;
148
+
padding: 1rem;
149
+
color: var(--m3input-text-color);
150
+
font-family: system-ui, sans-serif;
151
+
font-size: var(--m3input-font-size);
152
+
}*/
153
+
154
+
/* base wrapper */
155
+
.m3input-field.m3input-label.m3input-border {
156
+
position: relative;
157
+
display: inline-block;
158
+
width: 100%;
159
+
/*max-width: 400px;*/
160
+
}
161
+
162
+
/* size variants */
163
+
.m3input-field.size-sm {
164
+
--m3input-h: 40px;
165
+
}
166
+
167
+
.m3input-field.size-md {
168
+
--m3input-h: 48px;
169
+
}
170
+
171
+
.m3input-field.size-lg {
172
+
--m3input-h: 56px;
173
+
}
174
+
175
+
.m3input-field.size-xl {
176
+
--m3input-h: 64px;
177
+
}
178
+
179
+
.m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) {
180
+
--m3input-h: 48px;
181
+
}
182
+
183
+
/* outlined input */
184
+
.m3input-field.m3input-label.m3input-border input {
185
+
width: 100%;
186
+
height: var(--m3input-h);
187
+
border: var(--m3input-border-width) solid var(--m3input-border-color);
188
+
border-radius: var(--m3input-radius);
189
+
background: var(--m3input-bg);
190
+
color: var(--m3input-text-color);
191
+
font-size: var(--m3input-font-size);
192
+
padding: 0 12px;
193
+
box-sizing: border-box;
194
+
outline: none;
195
+
transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition);
196
+
}
197
+
198
+
/* focus ring */
199
+
.m3input-field.m3input-label.m3input-border input:focus {
200
+
border-color: var(--m3input-focus-color);
201
+
/*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/
202
+
}
203
+
204
+
/* label */
205
+
.m3input-field.m3input-label.m3input-border label {
206
+
position: absolute;
207
+
left: 12px;
208
+
top: 50%;
209
+
transform: translateY(-50%);
210
+
background: var(--m3input-bg);
211
+
padding: 0 .25em;
212
+
color: var(--m3input-label-color);
213
+
pointer-events: none;
214
+
transition: all var(--m3input-transition);
215
+
}
216
+
217
+
/* float on focus or when filled */
218
+
.m3input-field.m3input-label.m3input-border input:focus+label,
219
+
.m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label {
220
+
top: 0;
221
+
transform: translateY(-50%) scale(.78);
222
+
left: 0;
223
+
color: var(--m3input-focus-color);
224
+
}
225
+
226
+
/* placeholder trick */
227
+
.m3input-field.m3input-label.m3input-border input::placeholder {
228
+
color: transparent;
229
+
}
230
+
231
+
/* radix i love you but like cmon man */
232
+
body[data-scroll-locked]{
233
+
margin-left: var(--removed-body-scroll-bar-size) !important;
234
+
}
+65
-8
src/utils/atoms.ts
+65
-8
src/utils/atoms.ts
···
1
-
import type Agent from "@atproto/api";
2
-
import { atom, createStore } from "jotai";
3
-
import { atomWithStorage } from 'jotai/utils';
1
+
import { atom, createStore, useAtomValue } from "jotai";
2
+
import { atomWithStorage } from "jotai/utils";
3
+
import { useEffect } from "react";
4
4
5
5
export const store = createStore();
6
6
7
+
export const quickAuthAtom = atomWithStorage<string | null>(
8
+
"quickAuth",
9
+
null
10
+
);
11
+
7
12
export const selectedFeedUriAtom = atomWithStorage<string | null>(
8
-
'selectedFeedUri',
13
+
"selectedFeedUri",
9
14
null
10
15
);
11
16
12
17
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
18
14
19
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
15
-
'feedscrollpositions',
20
+
"feedscrollpositions",
16
21
{}
17
22
);
18
23
19
24
export const likedPostsAtom = atomWithStorage<Record<string, string>>(
20
-
'likedPosts',
25
+
"likedPosts",
21
26
{}
22
27
);
23
28
24
-
export const agentAtom = atom<Agent|null>(null);
25
-
export const authedAtom = atom<boolean>(false);
29
+
export const defaultconstellationURL = "constellation.microcosm.blue";
30
+
export const constellationURLAtom = atomWithStorage<string>(
31
+
"constellationURL",
32
+
defaultconstellationURL
33
+
);
34
+
export const defaultslingshotURL = "slingshot.microcosm.blue";
35
+
export const slingshotURLAtom = atomWithStorage<string>(
36
+
"slingshotURL",
37
+
defaultslingshotURL
38
+
);
39
+
export const defaultImgCDN = "cdn.bsky.app";
40
+
export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN);
41
+
export const defaultVideoCDN = "video.bsky.app";
42
+
export const videoCDNAtom = atomWithStorage<string>(
43
+
"videocdnurl",
44
+
defaultVideoCDN
45
+
);
46
+
47
+
export const defaulthue = 28;
48
+
export const hueAtom = atomWithStorage<number>("hue", defaulthue);
49
+
50
+
export const isAtTopAtom = atom<boolean>(true);
51
+
52
+
type ComposerState =
53
+
| { kind: "closed" }
54
+
| { kind: "root" }
55
+
| { kind: "reply"; parent: string }
56
+
| { kind: "quote"; subject: string };
57
+
export const composerAtom = atom<ComposerState>({ kind: "closed" });
58
+
59
+
//export const agentAtom = atom<Agent | null>(null);
60
+
//export const authedAtom = atom<boolean>(false);
61
+
62
+
export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) {
63
+
const value = useAtomValue(atom);
64
+
65
+
useEffect(() => {
66
+
document.documentElement.style.setProperty(cssVar, value.toString());
67
+
}, [value, cssVar]);
68
+
69
+
useEffect(() => {
70
+
document.documentElement.style.setProperty(cssVar, value.toString());
71
+
}, []);
72
+
}
73
+
74
+
hueAtom.onMount = (setAtom) => {
75
+
const stored = localStorage.getItem("hue");
76
+
if (stored != null) setAtom(Number(stored));
77
+
};
78
+
// export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) {
79
+
// const initial = store.get(atom);
80
+
// console.log("atom get ", initial);
81
+
// document.documentElement.style.setProperty(cssVar, initial.toString());
82
+
// }
+37
-3
src/utils/followState.ts
+37
-3
src/utils/followState.ts
···
1
-
import { AtUri, type Agent } from "@atproto/api";
2
-
import { useQueryConstellation, type linksRecordsResponse } from "./useQuery";
1
+
import { type Agent,AtUri } from "@atproto/api";
2
+
import { TID } from "@atproto/common-web";
3
3
import type { QueryClient } from "@tanstack/react-query";
4
-
import { TID } from "@atproto/common-web";
4
+
5
+
import { type linksRecordsResponse,useQueryConstellation } from "./useQuery";
5
6
6
7
export function useGetFollowState({
7
8
target,
···
127
128
};
128
129
});
129
130
}
131
+
132
+
133
+
134
+
export function useGetOneToOneState(params?: {
135
+
target: string;
136
+
user: string;
137
+
collection: string;
138
+
path: string;
139
+
}): string[] | undefined {
140
+
const { data: arbitrarydata } = useQueryConstellation(
141
+
params && params.user
142
+
? {
143
+
method: "/links",
144
+
target: params.target,
145
+
// @ts-expect-error overloading sucks so much
146
+
collection: params.collection,
147
+
path: params.path,
148
+
dids: [params.user],
149
+
}
150
+
: { method: "undefined", target: "whatever" }
151
+
// overloading sucks so much
152
+
) as { data: linksRecordsResponse | undefined };
153
+
if (!params || !params.user) return undefined;
154
+
const data = arbitrarydata?.linking_records.slice(0, 50) ?? [];
155
+
156
+
if (data.length > 0) {
157
+
return data.map((linksRecord) => {
158
+
return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`;
159
+
});
160
+
}
161
+
162
+
return undefined;
163
+
}
+53
-23
src/utils/useHydrated.ts
+53
-23
src/utils/useHydrated.ts
···
9
9
AppBskyFeedPost,
10
10
AtUri,
11
11
} from "@atproto/api";
12
+
import { useAtom } from "jotai";
12
13
import { useMemo } from "react";
13
14
14
-
import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery";
15
+
import { imgCDNAtom, videoCDNAtom } from "./atoms";
16
+
import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery";
15
17
16
-
type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends
17
-
| { data: infer D }
18
-
| undefined
19
-
? D
20
-
: never;
18
+
type QueryResultData<T extends (...args: any) => any> =
19
+
ReturnType<T> extends { data: infer D } | undefined ? D : never;
21
20
22
21
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
23
22
return obj as $Typed<T>;
···
26
25
export function hydrateEmbedImages(
27
26
embed: AppBskyEmbedImages.Main,
28
27
did: string,
28
+
cdn: string
29
29
): $Typed<AppBskyEmbedImages.View> {
30
30
return asTyped({
31
31
$type: "app.bsky.embed.images#view" as const,
···
34
34
const link = img.image.ref?.["$link"];
35
35
if (!link) return null;
36
36
return {
37
-
thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
-
fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`,
37
+
thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
+
fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
39
alt: img.alt || "",
40
40
aspectRatio: img.aspectRatio,
41
41
};
···
47
47
export function hydrateEmbedExternal(
48
48
embed: AppBskyEmbedExternal.Main,
49
49
did: string,
50
+
cdn: string
50
51
): $Typed<AppBskyEmbedExternal.View> {
51
52
return asTyped({
52
53
$type: "app.bsky.embed.external#view" as const,
···
55
56
title: embed.external.title,
56
57
description: embed.external.description,
57
58
thumb: embed.external.thumb?.ref?.$link
58
-
? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
+
? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
60
: undefined,
60
61
},
61
62
});
···
64
65
export function hydrateEmbedVideo(
65
66
embed: AppBskyEmbedVideo.Main,
66
67
did: string,
68
+
videocdn: string
67
69
): $Typed<AppBskyEmbedVideo.View> {
68
70
const videoLink = embed.video.ref.$link;
69
71
return asTyped({
70
72
$type: "app.bsky.embed.video#view" as const,
71
-
playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`,
72
-
thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`,
73
+
playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`,
74
+
thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`,
73
75
aspectRatio: embed.aspectRatio,
74
76
cid: videoLink,
75
77
});
···
80
82
quotedPost: QueryResultData<typeof useQueryPost>,
81
83
quotedProfile: QueryResultData<typeof useQueryProfile>,
82
84
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
85
+
cdn: string
83
86
): $Typed<AppBskyEmbedRecord.View> | undefined {
84
87
if (!quotedPost || !quotedProfile || !quotedIdentity) {
85
88
return undefined;
···
91
94
handle: quotedIdentity.handle,
92
95
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
93
96
avatar: quotedProfile.value.avatar?.ref?.$link
94
-
? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
97
+
? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
95
98
: undefined,
96
99
viewer: {},
97
100
labels: [],
···
122
125
quotedPost: QueryResultData<typeof useQueryPost>,
123
126
quotedProfile: QueryResultData<typeof useQueryProfile>,
124
127
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
128
+
cdn: string
125
129
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
126
130
const hydratedRecord = hydrateEmbedRecord(
127
131
embed.record,
128
132
quotedPost,
129
133
quotedProfile,
130
134
quotedIdentity,
135
+
cdn
131
136
);
132
137
133
138
if (!hydratedRecord) return undefined;
···
148
153
149
154
export function useHydratedEmbed(
150
155
embed: AppBskyFeedPost.Record["embed"],
151
-
postAuthorDid: string | undefined,
156
+
postAuthorDid: string | undefined
152
157
) {
153
158
const recordInfo = useMemo(() => {
154
159
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
···
181
186
error: profileError,
182
187
} = useQueryProfile(profileUri);
183
188
189
+
const [imgcdn] = useAtom(imgCDNAtom);
190
+
const [videocdn] = useAtom(videoCDNAtom);
191
+
184
192
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
185
193
186
194
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
187
195
if (!embed || !postAuthorDid) return undefined;
188
196
189
-
if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) {
197
+
if (
198
+
isRecordType &&
199
+
(!usequerypostresults?.data ||
200
+
!quotedProfile ||
201
+
!queryidentityresult?.data)
202
+
) {
190
203
return undefined;
191
204
}
192
205
193
206
try {
194
207
if (AppBskyEmbedImages.isMain(embed)) {
195
-
return hydrateEmbedImages(embed, postAuthorDid);
208
+
return hydrateEmbedImages(embed, postAuthorDid, imgcdn);
196
209
} else if (AppBskyEmbedExternal.isMain(embed)) {
197
-
return hydrateEmbedExternal(embed, postAuthorDid);
210
+
return hydrateEmbedExternal(embed, postAuthorDid, imgcdn);
198
211
} else if (AppBskyEmbedVideo.isMain(embed)) {
199
-
return hydrateEmbedVideo(embed, postAuthorDid);
212
+
return hydrateEmbedVideo(embed, postAuthorDid, videocdn);
200
213
} else if (AppBskyEmbedRecord.isMain(embed)) {
201
214
return hydrateEmbedRecord(
202
215
embed,
203
216
usequerypostresults?.data,
204
217
quotedProfile,
205
218
queryidentityresult?.data,
219
+
imgcdn
206
220
);
207
221
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
208
222
let hydratedMedia:
···
212
226
| undefined;
213
227
214
228
if (AppBskyEmbedImages.isMain(embed.media)) {
215
-
hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid);
229
+
hydratedMedia = hydrateEmbedImages(
230
+
embed.media,
231
+
postAuthorDid,
232
+
imgcdn
233
+
);
216
234
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
217
-
hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid);
235
+
hydratedMedia = hydrateEmbedExternal(
236
+
embed.media,
237
+
postAuthorDid,
238
+
imgcdn
239
+
);
218
240
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
219
-
hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid);
241
+
hydratedMedia = hydrateEmbedVideo(
242
+
embed.media,
243
+
postAuthorDid,
244
+
videocdn
245
+
);
220
246
}
221
247
222
248
if (hydratedMedia) {
···
226
252
usequerypostresults?.data,
227
253
quotedProfile,
228
254
queryidentityresult?.data,
255
+
imgcdn
229
256
);
230
257
}
231
258
}
···
236
263
})();
237
264
238
265
const isLoading = isRecordType
239
-
? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading
266
+
? usequerypostresults?.isLoading ||
267
+
isLoadingProfile ||
268
+
queryidentityresult?.isLoading
240
269
: false;
241
270
242
-
const error = usequerypostresults?.error || profileError || queryidentityresult?.error;
271
+
const error =
272
+
usequerypostresults?.error || profileError || queryidentityresult?.error;
243
273
244
274
return { data: hydratedEmbed, isLoading, error };
245
-
}
275
+
}
+81
-17
src/utils/useQuery.ts
+81
-17
src/utils/useQuery.ts
···
1
1
import * as ATPAPI from "@atproto/api";
2
2
import {
3
+
infiniteQueryOptions,
3
4
type QueryFunctionContext,
4
5
queryOptions,
5
6
useInfiniteQuery,
6
7
useQuery,
7
8
type UseQueryResult} from "@tanstack/react-query";
9
+
import { useAtom } from "jotai";
8
10
9
-
export function constructIdentityQuery(didorhandle?: string) {
11
+
import { constellationURLAtom, slingshotURLAtom } from "./atoms";
12
+
13
+
export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) {
10
14
return queryOptions({
11
15
queryKey: ["identity", didorhandle],
12
16
queryFn: async () => {
13
17
if (!didorhandle) return undefined as undefined
14
18
const res = await fetch(
15
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
19
+
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
16
20
);
17
21
if (!res.ok) throw new Error("Failed to fetch post");
18
22
try {
···
54
58
Error
55
59
>
56
60
export function useQueryIdentity(didorhandle?: string) {
57
-
return useQuery(constructIdentityQuery(didorhandle));
61
+
const [slingshoturl] = useAtom(slingshotURLAtom)
62
+
return useQuery(constructIdentityQuery(didorhandle, slingshoturl));
58
63
}
59
64
60
-
export function constructPostQuery(uri?: string) {
65
+
export function constructPostQuery(uri?: string, slingshoturl?: string) {
61
66
return queryOptions({
62
67
queryKey: ["post", uri],
63
68
queryFn: async () => {
64
69
if (!uri) return undefined as undefined
65
70
const res = await fetch(
66
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
71
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
67
72
);
68
73
let data: any;
69
74
try {
···
117
122
Error
118
123
>
119
124
export function useQueryPost(uri?: string) {
120
-
return useQuery(constructPostQuery(uri));
125
+
const [slingshoturl] = useAtom(slingshotURLAtom)
126
+
return useQuery(constructPostQuery(uri, slingshoturl));
121
127
}
122
128
123
-
export function constructProfileQuery(uri?: string) {
129
+
export function constructProfileQuery(uri?: string, slingshoturl?: string) {
124
130
return queryOptions({
125
131
queryKey: ["profile", uri],
126
132
queryFn: async () => {
127
133
if (!uri) return undefined as undefined
128
134
const res = await fetch(
129
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
135
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
130
136
);
131
137
let data: any;
132
138
try {
···
180
186
Error
181
187
>
182
188
export function useQueryProfile(uri?: string) {
183
-
return useQuery(constructProfileQuery(uri));
189
+
const [slingshoturl] = useAtom(slingshotURLAtom)
190
+
return useQuery(constructProfileQuery(uri, slingshoturl));
184
191
}
185
192
186
193
// export function constructConstellationQuery(
···
216
223
// target: string
217
224
// ): QueryOptions<linksAllResponse, Error>;
218
225
export function constructConstellationQuery(query?:{
226
+
constellation: string,
219
227
method:
220
228
| "/links"
221
229
| "/links/distinct-dids"
···
249
257
const cursor = query.cursor
250
258
const dids = query?.dids
251
259
const res = await fetch(
252
-
`https://constellation.microcosm.blue${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
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("") : ""}`
253
261
);
254
262
if (!res.ok) throw new Error("Failed to fetch post");
255
263
try {
···
338
346
>
339
347
| undefined {
340
348
//if (!query) return;
349
+
const [constellationurl] = useAtom(constellationURLAtom)
341
350
return useQuery(
342
-
constructConstellationQuery(query)
351
+
constructConstellationQuery(query && {constellation: constellationurl, ...query})
343
352
);
344
353
}
345
354
···
361
370
type linksCountResponse = {
362
371
total: string;
363
372
};
364
-
type linksAllResponse = {
373
+
export type linksAllResponse = {
365
374
links: Record<
366
375
string,
367
376
Record<
···
444
453
445
454
446
455
447
-
export function constructArbitraryQuery(uri?: string) {
456
+
export function constructArbitraryQuery(uri?: string, slingshoturl?: string) {
448
457
return queryOptions({
449
458
queryKey: ["arbitrary", uri],
450
459
queryFn: async () => {
451
460
if (!uri) return undefined as undefined
452
461
const res = await fetch(
453
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
462
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
454
463
);
455
464
let data: any;
456
465
try {
···
503
512
Error
504
513
>;
505
514
export function useQueryArbitrary(uri?: string) {
506
-
return useQuery(constructArbitraryQuery(uri));
515
+
const [slingshoturl] = useAtom(slingshotURLAtom)
516
+
return useQuery(constructArbitraryQuery(uri, slingshoturl));
507
517
}
508
518
509
519
export function constructFallbackNothingQuery(){
···
605
615
}) {
606
616
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
607
617
608
-
return useInfiniteQuery({
618
+
return {...useInfiniteQuery({
609
619
queryKey,
610
620
queryFn,
611
621
initialPageParam: undefined as never,
···
613
623
staleTime: Infinity,
614
624
refetchOnWindowFocus: false,
615
625
enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true),
616
-
});
626
+
}), queryKey: queryKey};
627
+
}
628
+
629
+
630
+
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
631
+
constellation: string,
632
+
method: '/links'
633
+
target?: string
634
+
collection: string
635
+
path: string
636
+
}) {
637
+
console.log(
638
+
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
639
+
query,
640
+
)
641
+
642
+
return infiniteQueryOptions({
643
+
enabled: !!query?.target,
644
+
queryKey: [
645
+
'reddwarf_constellation',
646
+
query?.method,
647
+
query?.target,
648
+
query?.collection,
649
+
query?.path,
650
+
] as const,
651
+
652
+
queryFn: async ({pageParam}: {pageParam?: string}) => {
653
+
if (!query || !query?.target) return undefined
654
+
655
+
const method = query.method
656
+
const target = query.target
657
+
const collection = query.collection
658
+
const path = query.path
659
+
const cursor = pageParam
660
+
661
+
const res = await fetch(
662
+
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${
663
+
collection ? `&collection=${encodeURIComponent(collection)}` : ''
664
+
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
665
+
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
666
+
}`,
667
+
)
668
+
669
+
if (!res.ok) throw new Error('Failed to fetch')
670
+
671
+
return (await res.json()) as linksRecordsResponse
672
+
},
673
+
674
+
getNextPageParam: lastPage => {
675
+
return (lastPage as any)?.cursor ?? undefined
676
+
},
677
+
initialPageParam: undefined,
678
+
staleTime: 5 * 60 * 1000,
679
+
gcTime: 5 * 60 * 1000,
680
+
})
617
681
}
+3
-2
vite.config.ts
+3
-2
vite.config.ts
···
10
10
11
11
import { generateMetadataPlugin } from "./oauthdev.mts";
12
12
13
-
const PROD_URL = "https://reddwarf.whey.party"
13
+
const PROD_URL = "https://reddwarf.app"
14
14
const DEV_URL = "https://local3768forumtest.whey.party"
15
15
16
16
function shp(url: string): string {
···
39
39
IconsResolver({
40
40
prefix: 'Icon',
41
41
extension: 'jsx',
42
+
enabledCollections: ['mdi','material-symbols'],
42
43
}),
43
44
],
44
45
dts: 'src/auto-imports.d.ts',
45
46
}),
46
47
Icons({
47
-
autoInstall: true,
48
+
//autoInstall: true,
48
49
compiler: 'jsx',
49
50
jsx: 'react'
50
51
}),