mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add router-backed app navigation and skeleton loading states (#259)
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.168.10",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
@@ -60,6 +61,8 @@
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tanstack/router-plugin": "^1.167.12",
|
||||
"@tanstack/virtual-file-routes": "^1.161.7",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
|
||||
Generated
+359
-12
@@ -44,6 +44,9 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.90.21
|
||||
version: 5.90.21(react@19.2.4)
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.168.10
|
||||
version: 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2
|
||||
version: 2.10.1
|
||||
@@ -111,6 +114,12 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: ^1.58.2
|
||||
version: 1.58.2
|
||||
'@tanstack/router-plugin':
|
||||
specifier: ^1.167.12
|
||||
version: 1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@tanstack/virtual-file-routes':
|
||||
specifier: ^1.161.7
|
||||
version: 1.161.7
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2
|
||||
version: 2.10.1
|
||||
@@ -122,7 +131,7 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.14)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.6.0
|
||||
version: 4.7.0(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(yaml@2.8.3))
|
||||
version: 4.7.0(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.27
|
||||
version: 10.4.27(postcss@8.5.8)
|
||||
@@ -134,16 +143,16 @@ importers:
|
||||
version: 8.5.8
|
||||
tailwindcss:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.19(yaml@2.8.3)
|
||||
version: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.19(yaml@2.8.3))
|
||||
version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
|
||||
typescript:
|
||||
specifier: ~5.8.3
|
||||
version: 5.8.3
|
||||
vite:
|
||||
specifier: ^7.0.4
|
||||
version: 7.3.1(@types/node@25.5.2)(jiti@1.21.7)(yaml@2.8.3)
|
||||
version: 7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -210,6 +219,18 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/plugin-syntax-jsx@7.28.6':
|
||||
resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/plugin-syntax-typescript@7.28.6':
|
||||
resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/plugin-transform-react-jsx-self@7.27.1':
|
||||
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -950,6 +971,7 @@ packages:
|
||||
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
|
||||
@@ -960,21 +982,25 @@ packages:
|
||||
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
@@ -985,11 +1011,13 @@ packages:
|
||||
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
|
||||
@@ -1000,11 +1028,13 @@ packages:
|
||||
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
@@ -1081,6 +1111,10 @@ packages:
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@tanstack/history@1.161.6':
|
||||
resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/query-core@5.90.20':
|
||||
resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
|
||||
|
||||
@@ -1089,6 +1123,62 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-router@1.168.10':
|
||||
resolution: {integrity: sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA==}
|
||||
engines: {node: '>=20.19'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0 || >=19.0.0'
|
||||
react-dom: '>=18.0.0 || >=19.0.0'
|
||||
|
||||
'@tanstack/react-store@0.9.3':
|
||||
resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tanstack/router-core@1.168.9':
|
||||
resolution: {integrity: sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==}
|
||||
engines: {node: '>=20.19'}
|
||||
hasBin: true
|
||||
|
||||
'@tanstack/router-generator@1.166.24':
|
||||
resolution: {integrity: sha512-vdaGKwuH+r+DPe6R1mjk+TDDmDH6NTG7QqwxHqGEvOH4aGf9sPjhmRKNJZqQr8cPIbfp6u5lXyZ1TeDcSNMVEA==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/router-plugin@1.167.12':
|
||||
resolution: {integrity: sha512-StEHcctCuFI5taSjO+lhR/yQ+EK63BdyYa+ne6FoNQPB3MMrOUrz2ZVnbqILRLkh2b+p2EfBKt65sgAKdKygPQ==}
|
||||
engines: {node: '>=20.19'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@rsbuild/core': '>=1.0.2'
|
||||
'@tanstack/react-router': ^1.168.10
|
||||
vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
|
||||
vite-plugin-solid: ^2.11.10
|
||||
webpack: '>=5.92.0'
|
||||
peerDependenciesMeta:
|
||||
'@rsbuild/core':
|
||||
optional: true
|
||||
'@tanstack/react-router':
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
vite-plugin-solid:
|
||||
optional: true
|
||||
webpack:
|
||||
optional: true
|
||||
|
||||
'@tanstack/router-utils@1.161.6':
|
||||
resolution: {integrity: sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/store@0.9.3':
|
||||
resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
|
||||
|
||||
'@tanstack/virtual-file-routes@1.161.7':
|
||||
resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==}
|
||||
engines: {node: '>=20.19'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/api@2.10.1':
|
||||
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
|
||||
|
||||
@@ -1231,6 +1321,15 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ansis@4.2.0:
|
||||
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
@@ -1245,6 +1344,10 @@ packages:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ast-types@0.16.1:
|
||||
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
autoprefixer@10.4.27:
|
||||
resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1252,6 +1355,9 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
babel-dead-code-elimination@1.0.12:
|
||||
resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
|
||||
|
||||
bail@2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
|
||||
@@ -1322,6 +1428,9 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-es@2.0.1:
|
||||
resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1358,6 +1467,10 @@ packages:
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
diff@8.0.4:
|
||||
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
@@ -1380,6 +1493,11 @@ packages:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
estree-util-is-identifier-name@3.0.0:
|
||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||
|
||||
@@ -1430,6 +1548,9 @@ packages:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
get-tsconfig@4.13.7:
|
||||
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
|
||||
|
||||
gitdiff-parser@0.3.1:
|
||||
resolution: {integrity: sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==}
|
||||
|
||||
@@ -1499,6 +1620,10 @@ packages:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
isbot@5.1.37:
|
||||
resolution: {integrity: sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
jdenticon@3.3.0:
|
||||
resolution: {integrity: sha512-DhuBRNRIybGPeAjMjdHbkIfiwZCCmf8ggu7C49jhp6aJ7DYsZfudnvnTY5/1vgUhrGA7JaDAx1WevnpjCPvaGg==}
|
||||
engines: {node: '>=6.4.0'}
|
||||
@@ -1738,6 +1863,9 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1814,6 +1942,11 @@ packages:
|
||||
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prettier@3.8.1:
|
||||
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
@@ -1881,6 +2014,10 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
recast@0.23.11:
|
||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
|
||||
|
||||
@@ -1905,6 +2042,9 @@ packages:
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
resolve@1.22.11:
|
||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1929,6 +2069,16 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
seroval-plugins@1.5.2:
|
||||
resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
seroval: ^1.0
|
||||
|
||||
seroval@1.5.2:
|
||||
resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
shallow-equal@3.1.0:
|
||||
resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==}
|
||||
|
||||
@@ -1940,6 +2090,14 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map@0.7.6:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
@@ -1981,6 +2139,9 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -2001,6 +2162,11 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -2027,6 +2193,10 @@ packages:
|
||||
unist-util-visit@5.1.0:
|
||||
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
|
||||
|
||||
unplugin@2.3.11:
|
||||
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
@@ -2053,6 +2223,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -2105,6 +2280,9 @@ packages:
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@@ -2113,6 +2291,9 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -2199,6 +2380,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
@@ -2928,6 +3119,8 @@ snapshots:
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@tanstack/history@1.161.6': {}
|
||||
|
||||
'@tanstack/query-core@5.90.20': {}
|
||||
|
||||
'@tanstack/react-query@5.90.21(react@19.2.4)':
|
||||
@@ -2935,6 +3128,81 @@ snapshots:
|
||||
'@tanstack/query-core': 5.90.20
|
||||
react: 19.2.4
|
||||
|
||||
'@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.161.6
|
||||
'@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@tanstack/router-core': 1.168.9
|
||||
isbot: 5.1.37
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
'@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@tanstack/store': 0.9.3
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||
|
||||
'@tanstack/router-core@1.168.9':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.161.6
|
||||
cookie-es: 2.0.1
|
||||
seroval: 1.5.2
|
||||
seroval-plugins: 1.5.2(seroval@1.5.2)
|
||||
|
||||
'@tanstack/router-generator@1.166.24':
|
||||
dependencies:
|
||||
'@tanstack/router-core': 1.168.9
|
||||
'@tanstack/router-utils': 1.161.6
|
||||
'@tanstack/virtual-file-routes': 1.161.7
|
||||
prettier: 3.8.1
|
||||
recast: 0.23.11
|
||||
source-map: 0.7.6
|
||||
tsx: 4.21.0
|
||||
zod: 3.25.76
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
'@tanstack/router-core': 1.168.9
|
||||
'@tanstack/router-generator': 1.166.24
|
||||
'@tanstack/router-utils': 1.161.6
|
||||
'@tanstack/virtual-file-routes': 1.161.7
|
||||
chokidar: 3.6.0
|
||||
unplugin: 2.3.11
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
'@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
vite: 7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tanstack/router-utils@1.161.6':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/generator': 7.29.1
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
ansis: 4.2.0
|
||||
babel-dead-code-elimination: 1.0.12
|
||||
diff: 8.0.4
|
||||
pathe: 2.0.3
|
||||
tinyglobby: 0.2.15
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tanstack/store@0.9.3': {}
|
||||
|
||||
'@tanstack/virtual-file-routes@1.161.7': {}
|
||||
|
||||
'@tauri-apps/api@2.10.1': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.10.1':
|
||||
@@ -3059,7 +3327,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(yaml@2.8.3))':
|
||||
'@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||
@@ -3067,10 +3335,14 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 7.3.1(@types/node@25.5.2)(jiti@1.21.7)(yaml@2.8.3)
|
||||
vite: 7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ansis@4.2.0: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -3084,6 +3356,10 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
ast-types@0.16.1:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
autoprefixer@10.4.27(postcss@8.5.8):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
@@ -3093,6 +3369,15 @@ snapshots:
|
||||
postcss: 8.5.8
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
babel-dead-code-elimination@1.0.12:
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
baseline-browser-mapping@2.10.0: {}
|
||||
@@ -3155,6 +3440,8 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-es@2.0.1: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
@@ -3179,6 +3466,8 @@ snapshots:
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
diff@8.0.4: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
electron-to-chromium@1.5.307: {}
|
||||
@@ -3218,6 +3507,8 @@ snapshots:
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
estree-util-is-identifier-name@3.0.0: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
@@ -3256,6 +3547,10 @@ snapshots:
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
get-tsconfig@4.13.7:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
gitdiff-parser@0.3.1: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
@@ -3343,6 +3638,8 @@ snapshots:
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
isbot@5.1.37: {}
|
||||
|
||||
jdenticon@3.3.0:
|
||||
dependencies:
|
||||
canvas-renderer: 2.2.1
|
||||
@@ -3785,6 +4082,8 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
@@ -3815,12 +4114,13 @@ snapshots:
|
||||
camelcase-css: 2.0.1
|
||||
postcss: 8.5.8
|
||||
|
||||
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3):
|
||||
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
jiti: 1.21.7
|
||||
postcss: 8.5.8
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
postcss-nested@6.2.0(postcss@8.5.8):
|
||||
@@ -3841,6 +4141,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prettier@3.8.1: {}
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
@@ -3917,6 +4219,14 @@ snapshots:
|
||||
dependencies:
|
||||
picomatch: 2.3.2
|
||||
|
||||
recast@0.23.11:
|
||||
dependencies:
|
||||
ast-types: 0.16.1
|
||||
esprima: 4.0.1
|
||||
source-map: 0.6.1
|
||||
tiny-invariant: 1.3.3
|
||||
tslib: 2.8.1
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
@@ -3967,6 +4277,8 @@ snapshots:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
resolve@1.22.11:
|
||||
dependencies:
|
||||
is-core-module: 2.16.1
|
||||
@@ -4014,6 +4326,12 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
seroval-plugins@1.5.2(seroval@1.5.2):
|
||||
dependencies:
|
||||
seroval: 1.5.2
|
||||
|
||||
seroval@1.5.2: {}
|
||||
|
||||
shallow-equal@3.1.0: {}
|
||||
|
||||
shiki@4.0.2:
|
||||
@@ -4029,6 +4347,10 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
@@ -4058,11 +4380,11 @@ snapshots:
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.19(yaml@2.8.3)):
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
tailwindcss: 3.4.19(yaml@2.8.3)
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
tailwindcss@3.4.19(yaml@2.8.3):
|
||||
tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
arg: 5.0.2
|
||||
@@ -4081,7 +4403,7 @@ snapshots:
|
||||
postcss: 8.5.8
|
||||
postcss-import: 15.1.0(postcss@8.5.8)
|
||||
postcss-js: 4.1.0(postcss@8.5.8)
|
||||
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3)
|
||||
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3)
|
||||
postcss-nested: 6.2.0(postcss@8.5.8)
|
||||
postcss-selector-parser: 6.1.2
|
||||
resolve: 1.22.11
|
||||
@@ -4098,6 +4420,8 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -4115,6 +4439,13 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
get-tsconfig: 4.13.7
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
@@ -4152,6 +4483,13 @@ snapshots:
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
acorn: 8.16.0
|
||||
picomatch: 4.0.3
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
@@ -4173,6 +4511,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
@@ -4185,7 +4527,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(yaml@2.8.3):
|
||||
vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -4197,14 +4539,19 @@ snapshots:
|
||||
'@types/node': 25.5.2
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.3: {}
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
import { AppShell } from "@/app/AppShell";
|
||||
import { router } from "@/app/router";
|
||||
|
||||
export function App() {
|
||||
useLayoutEffect(() => {
|
||||
void getCurrentWindow().show();
|
||||
}, []);
|
||||
|
||||
return <AppShell />;
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
+225
-307
@@ -1,10 +1,15 @@
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Outlet, useLocation } from "@tanstack/react-router";
|
||||
|
||||
import { AppShellProvider } from "@/app/AppShellContext";
|
||||
import {
|
||||
AppShellOverlays,
|
||||
type BrowseDialogType,
|
||||
} from "@/app/AppShellOverlays";
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useBackForwardControls } from "@/app/navigation/useBackForwardControls";
|
||||
import { useWebviewZoomShortcuts } from "@/app/useWebviewZoomShortcuts";
|
||||
import {
|
||||
channelsQueryKey,
|
||||
@@ -12,10 +17,8 @@ import {
|
||||
useCreateChannelMutation,
|
||||
useHideDmMutation,
|
||||
useOpenDmMutation,
|
||||
useSelectedChannel,
|
||||
} from "@/features/channels/hooks";
|
||||
import { useUnreadChannels } from "@/features/channels/useUnreadChannels";
|
||||
import { HomeScreen } from "@/features/home/ui/HomeScreen";
|
||||
import { useHomeFeedNotifications } from "@/features/notifications/hooks";
|
||||
import {
|
||||
listenForDesktopNotificationActions,
|
||||
@@ -30,10 +33,10 @@ import { SettingsScreen } from "@/features/settings/ui/SettingsScreen";
|
||||
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { getEventById, joinChannel } from "@/shared/api/tauri";
|
||||
import type { Channel, RelayEvent, SearchHit } from "@/shared/api/types";
|
||||
import { joinChannel } from "@/shared/api/tauri";
|
||||
import type { SearchHit } from "@/shared/api/types";
|
||||
import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationContext";
|
||||
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
@@ -43,20 +46,7 @@ import {
|
||||
type AppView = "home" | "channel" | "agents" | "workflows";
|
||||
const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile";
|
||||
|
||||
const AgentsScreen = React.lazy(async () => {
|
||||
const module = await import("@/features/agents/ui/AgentsScreen");
|
||||
return { default: module.AgentsScreen };
|
||||
});
|
||||
const ChannelScreen = React.lazy(async () => {
|
||||
const module = await import("@/features/channels/ui/ChannelScreen");
|
||||
return { default: module.ChannelScreen };
|
||||
});
|
||||
const WorkflowsScreen = React.lazy(async () => {
|
||||
const module = await import("@/features/workflows/ui/WorkflowsScreen");
|
||||
return { default: module.WorkflowsScreen };
|
||||
});
|
||||
|
||||
function toSearchAnchor(target: DesktopNotificationTarget): SearchHit | null {
|
||||
function toSearchHit(target: DesktopNotificationTarget): SearchHit | null {
|
||||
if (!target.eventId) {
|
||||
return null;
|
||||
}
|
||||
@@ -73,10 +63,41 @@ function toSearchAnchor(target: DesktopNotificationTarget): SearchHit | null {
|
||||
};
|
||||
}
|
||||
|
||||
function deriveShellRoute(pathname: string): {
|
||||
selectedChannelId: string | null;
|
||||
selectedView: AppView;
|
||||
} {
|
||||
if (pathname.startsWith("/channels/")) {
|
||||
const [, , rawChannelId] = pathname.split("/");
|
||||
return {
|
||||
selectedChannelId: rawChannelId ? decodeURIComponent(rawChannelId) : null,
|
||||
selectedView: "channel",
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === "/agents") {
|
||||
return {
|
||||
selectedChannelId: null,
|
||||
selectedView: "agents",
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === "/workflows" || pathname.startsWith("/workflows/")) {
|
||||
return {
|
||||
selectedChannelId: null,
|
||||
selectedView: "workflows",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
selectedChannelId: null,
|
||||
selectedView: "home",
|
||||
};
|
||||
}
|
||||
|
||||
export function AppShell() {
|
||||
useWebviewZoomShortcuts();
|
||||
|
||||
const [selectedView, setSelectedView] = React.useState<AppView>("home");
|
||||
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
||||
const [settingsSection, setSettingsSection] = React.useState<SettingsSection>(
|
||||
DEFAULT_SETTINGS_SECTION,
|
||||
@@ -86,21 +107,16 @@ export function AppShell() {
|
||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||
const [browseDialogType, setBrowseDialogType] =
|
||||
React.useState<BrowseDialogType>(null);
|
||||
const [searchAnchor, setSearchAnchor] = React.useState<SearchHit | null>(
|
||||
null,
|
||||
);
|
||||
const [searchAnchorChannelId, setSearchAnchorChannelId] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [searchAnchorEvent, setSearchAnchorEvent] =
|
||||
React.useState<RelayEvent | null>(null);
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const selectView = React.useCallback((view: AppView) => {
|
||||
React.startTransition(() => {
|
||||
setSelectedView(view);
|
||||
});
|
||||
}, []);
|
||||
const { goAgents, goChannel, goHome, goWorkflows, openSearchHit } =
|
||||
useAppNavigation();
|
||||
const { canGoBack, canGoForward, goBack, goForward } =
|
||||
useBackForwardControls();
|
||||
const { selectedChannelId, selectedView } = React.useMemo(
|
||||
() => deriveShellRoute(location.pathname),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const identityQuery = useIdentityQuery();
|
||||
const profileQuery = useProfileQuery();
|
||||
@@ -121,15 +137,13 @@ export function AppShell() {
|
||||
() => channels.filter((channel) => channel.isMember),
|
||||
[channels],
|
||||
);
|
||||
const availableChannelIds = React.useMemo(
|
||||
() => new Set(channels.map((channel) => channel.id)),
|
||||
[channels],
|
||||
const activeChannel = React.useMemo(
|
||||
() =>
|
||||
selectedChannelId
|
||||
? (channels.find((channel) => channel.id === selectedChannelId) ?? null)
|
||||
: null,
|
||||
[channels, selectedChannelId],
|
||||
);
|
||||
const { selectedChannel, setSelectedChannelId } = useSelectedChannel(
|
||||
channels,
|
||||
null,
|
||||
);
|
||||
const activeChannel = selectedView === "channel" ? selectedChannel : null;
|
||||
|
||||
const { markChannelRead, unreadChannelIds } = useUnreadChannels(
|
||||
channels,
|
||||
@@ -166,54 +180,6 @@ export function AppShell() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resolveChannel = React.useCallback(
|
||||
async (channelId: string): Promise<Channel | null> => {
|
||||
const cachedChannels =
|
||||
queryClient.getQueryData<Channel[]>(channelsQueryKey);
|
||||
const knownChannel =
|
||||
channels.find((channel) => channel.id === channelId) ??
|
||||
cachedChannels?.find((channel) => channel.id === channelId) ??
|
||||
null;
|
||||
|
||||
if (knownChannel) {
|
||||
return knownChannel;
|
||||
}
|
||||
|
||||
const refreshed = await refetchChannels();
|
||||
return (
|
||||
refreshed.data?.find((channel) => channel.id === channelId) ?? null
|
||||
);
|
||||
},
|
||||
[channels, queryClient, refetchChannels],
|
||||
);
|
||||
|
||||
const openChannelView = React.useCallback(
|
||||
(channelId: string) => {
|
||||
React.startTransition(() => {
|
||||
setSelectedChannelId(channelId);
|
||||
setSelectedView("channel");
|
||||
});
|
||||
},
|
||||
[setSelectedChannelId],
|
||||
);
|
||||
|
||||
const handleOpenChannel = React.useCallback(
|
||||
async (channelId: string) => {
|
||||
try {
|
||||
const channel = await resolveChannel(channelId);
|
||||
if (!channel) {
|
||||
console.error("Failed to resolve channel before opening", channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
openChannelView(channel.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to open channel", channelId, error);
|
||||
}
|
||||
},
|
||||
[openChannelView, resolveChannel],
|
||||
);
|
||||
|
||||
const handleBrowseChannelJoin = React.useCallback(
|
||||
async (channelId: string) => {
|
||||
await joinChannel(channelId);
|
||||
@@ -230,11 +196,11 @@ export function AppShell() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChannel?.id === channelId) {
|
||||
selectView("home");
|
||||
if (selectedChannelId === channelId) {
|
||||
void goHome();
|
||||
}
|
||||
},
|
||||
[hideDmMutation, selectView, selectedChannel?.id],
|
||||
[goHome, hideDmMutation, selectedChannelId],
|
||||
);
|
||||
|
||||
const handleOpenSettings = React.useCallback(
|
||||
@@ -251,55 +217,11 @@ export function AppShell() {
|
||||
setSettingsOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleTargetReached = React.useCallback((messageId: string) => {
|
||||
setSearchAnchor((current) =>
|
||||
current?.eventId === messageId ? null : current,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const openEventAnchor = React.useCallback(
|
||||
(hit: SearchHit) => {
|
||||
setSearchAnchor(hit);
|
||||
setSearchAnchorChannelId(hit.channelId);
|
||||
setSearchAnchorEvent({
|
||||
id: hit.eventId,
|
||||
pubkey: hit.pubkey,
|
||||
created_at: hit.createdAt,
|
||||
kind: hit.kind,
|
||||
tags: hit.channelId ? [["h", hit.channelId]] : [],
|
||||
content: hit.content,
|
||||
sig: "",
|
||||
});
|
||||
if (hit.channelId) {
|
||||
void handleOpenChannel(hit.channelId);
|
||||
}
|
||||
|
||||
void getEventById(hit.eventId)
|
||||
.then((event) => {
|
||||
setSearchAnchorEvent((current) => {
|
||||
if (current?.id !== hit.eventId) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return event;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Failed to load search result event",
|
||||
hit.eventId,
|
||||
error,
|
||||
);
|
||||
});
|
||||
},
|
||||
[handleOpenChannel],
|
||||
);
|
||||
|
||||
const handleOpenSearchResult = React.useCallback(
|
||||
(hit: SearchHit) => {
|
||||
openEventAnchor(hit);
|
||||
void openSearchHit(hit);
|
||||
},
|
||||
[openEventAnchor],
|
||||
[openSearchHit],
|
||||
);
|
||||
|
||||
const handleDesktopNotificationAction = React.useEffectEvent(
|
||||
@@ -307,23 +229,17 @@ export function AppShell() {
|
||||
await revealDesktopAppWindow();
|
||||
|
||||
if (!target.channelId) {
|
||||
setSearchAnchor(null);
|
||||
setSearchAnchorChannelId(null);
|
||||
setSearchAnchorEvent(null);
|
||||
selectView("home");
|
||||
void goHome();
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = toSearchAnchor(target);
|
||||
const anchor = toSearchHit(target);
|
||||
if (!anchor) {
|
||||
setSearchAnchor(null);
|
||||
setSearchAnchorChannelId(null);
|
||||
setSearchAnchorEvent(null);
|
||||
await handleOpenChannel(target.channelId);
|
||||
await goChannel(target.channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
openEventAnchor(anchor);
|
||||
await openSearchHit(anchor);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -427,169 +343,171 @@ export function AppShell() {
|
||||
}, [handleCloseSettings, handleOpenSettings, settingsOpen]);
|
||||
|
||||
return (
|
||||
<ChannelNavigationProvider
|
||||
channels={channels}
|
||||
onOpenChannel={handleOpenChannel}
|
||||
>
|
||||
<SidebarProvider className="h-dvh overflow-hidden overscroll-none">
|
||||
<SidebarTrigger className="fixed left-[80px] top-[8px] z-50 h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
|
||||
<AppSidebar
|
||||
channels={memberChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={
|
||||
channelsQuery.error instanceof Error
|
||||
? channelsQuery.error.message
|
||||
: undefined
|
||||
}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isCreatingForum={createForumMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
isOpeningDm={openDmMutation.isPending}
|
||||
isPresencePending={presenceSession.isPending}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
onCreateChannel={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdChannel = await createChannelMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "stream",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
openChannelView(createdChannel.id);
|
||||
}}
|
||||
onCreateForum={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdForum = await createForumMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "forum",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
openChannelView(createdForum.id);
|
||||
}}
|
||||
onHideDm={handleHideDm}
|
||||
onOpenBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenBrowseForums={handleOpenBrowseForums}
|
||||
onOpenDm={async ({ pubkeys }) => {
|
||||
const directMessage = await openDmMutation.mutateAsync({
|
||||
pubkeys,
|
||||
});
|
||||
openChannelView(directMessage.id);
|
||||
}}
|
||||
onOpenSearch={handleOpenSearch}
|
||||
onSelectAgents={() => selectView("agents")}
|
||||
onSelectChannel={handleOpenChannel}
|
||||
onSelectHome={() => {
|
||||
selectView("home");
|
||||
}}
|
||||
onSelectSettings={handleOpenSettings}
|
||||
onSelectWorkflows={() => selectView("workflows")}
|
||||
onSetPresenceStatus={(status) => presenceSession.setStatus(status)}
|
||||
profile={profileQuery.data}
|
||||
selectedChannelId={selectedChannel?.id ?? null}
|
||||
selectedView={selectedView}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
/>
|
||||
|
||||
<SidebarInset className="min-h-0 min-w-0 overflow-hidden">
|
||||
{selectedView === "home" ? (
|
||||
<HomeScreen
|
||||
availableChannelIds={availableChannelIds}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
onOpenChannel={handleOpenChannel}
|
||||
/>
|
||||
) : selectedView === "agents" ? (
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading agents..." />}
|
||||
<ChannelNavigationProvider channels={channels}>
|
||||
<AppShellProvider
|
||||
value={{
|
||||
markChannelRead,
|
||||
openChannelManagement: () => {
|
||||
setIsChannelManagementOpen(true);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SidebarProvider className="h-dvh overflow-hidden overscroll-none">
|
||||
<div className="fixed left-[80px] top-[8px] z-50 flex items-center gap-1.5">
|
||||
<SidebarTrigger className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
|
||||
<Button
|
||||
aria-label="Go back"
|
||||
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
|
||||
data-testid="global-back"
|
||||
disabled={!canGoBack}
|
||||
onClick={goBack}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<AgentsScreen />
|
||||
</React.Suspense>
|
||||
) : selectedView === "workflows" ? (
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading workflows..." />}
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Go forward"
|
||||
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
|
||||
data-testid="global-forward"
|
||||
disabled={!canGoForward}
|
||||
onClick={goForward}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<WorkflowsScreen channels={memberChannels} />
|
||||
</React.Suspense>
|
||||
) : (
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading channel..." />}
|
||||
>
|
||||
<ChannelScreen
|
||||
activeChannel={activeChannel}
|
||||
currentIdentity={identityQuery.data}
|
||||
currentProfile={profileQuery.data}
|
||||
onManageChannel={() => {
|
||||
setIsChannelManagementOpen(true);
|
||||
}}
|
||||
onMarkChannelRead={markChannelRead}
|
||||
onTargetReached={handleTargetReached}
|
||||
searchAnchor={searchAnchor}
|
||||
searchAnchorChannelId={searchAnchorChannelId}
|
||||
searchAnchorEvent={searchAnchorEvent}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)}
|
||||
</SidebarInset>
|
||||
|
||||
<AppShellOverlays
|
||||
activeChannel={activeChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
isSearchOpen={isSearchOpen}
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={setIsChannelManagementOpen}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
selectView("home");
|
||||
}}
|
||||
onOpenSearchResult={handleOpenSearchResult}
|
||||
onSearchOpenChange={setIsSearchOpen}
|
||||
onSelectChannel={handleOpenChannel}
|
||||
/>
|
||||
|
||||
{settingsOpen ? (
|
||||
<SettingsScreen
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<AppSidebar
|
||||
channels={memberChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={
|
||||
channelsQuery.error instanceof Error
|
||||
? channelsQuery.error.message
|
||||
: undefined
|
||||
}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
isUpdatingDesktopNotifications={
|
||||
notificationSettings.isUpdatingDesktopEnabled
|
||||
}
|
||||
notificationErrorMessage={notificationSettings.errorMessage}
|
||||
notificationPermission={notificationSettings.permission}
|
||||
notificationSettings={notificationSettings.settings}
|
||||
onClose={handleCloseSettings}
|
||||
onSectionChange={setSettingsSection}
|
||||
onSetDesktopNotificationsEnabled={
|
||||
notificationSettings.setDesktopEnabled
|
||||
}
|
||||
onSetHomeBadgeEnabled={notificationSettings.setHomeBadgeEnabled}
|
||||
onSetMentionNotificationsEnabled={
|
||||
notificationSettings.setMentionsEnabled
|
||||
}
|
||||
onSetNeedsActionNotificationsEnabled={
|
||||
notificationSettings.setNeedsActionEnabled
|
||||
}
|
||||
section={settingsSection}
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isCreatingForum={createForumMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
isOpeningDm={openDmMutation.isPending}
|
||||
isPresencePending={presenceSession.isPending}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
onCreateChannel={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdChannel = await createChannelMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "stream",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
await goChannel(createdChannel.id);
|
||||
}}
|
||||
onCreateForum={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdForum = await createForumMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "forum",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
await goChannel(createdForum.id);
|
||||
}}
|
||||
onHideDm={handleHideDm}
|
||||
onOpenBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenBrowseForums={handleOpenBrowseForums}
|
||||
onOpenDm={async ({ pubkeys }) => {
|
||||
const directMessage = await openDmMutation.mutateAsync({
|
||||
pubkeys,
|
||||
});
|
||||
await goChannel(directMessage.id);
|
||||
}}
|
||||
onOpenSearch={handleOpenSearch}
|
||||
onSelectAgents={() => {
|
||||
void goAgents();
|
||||
}}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
onSelectHome={() => {
|
||||
void goHome();
|
||||
}}
|
||||
onSelectSettings={handleOpenSettings}
|
||||
onSelectWorkflows={() => {
|
||||
void goWorkflows();
|
||||
}}
|
||||
onSetPresenceStatus={(status) => presenceSession.setStatus(status)}
|
||||
profile={profileQuery.data}
|
||||
selectedChannelId={selectedChannelId}
|
||||
selectedView={selectedView}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarProvider>
|
||||
|
||||
<SidebarInset className="min-h-0 min-w-0 overflow-hidden">
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
|
||||
<AppShellOverlays
|
||||
activeChannel={activeChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
isSearchOpen={isSearchOpen}
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={setIsChannelManagementOpen}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
void goHome({ replace: true });
|
||||
}}
|
||||
onOpenSearchResult={handleOpenSearchResult}
|
||||
onSearchOpenChange={setIsSearchOpen}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
|
||||
{settingsOpen ? (
|
||||
<SettingsScreen
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
isUpdatingDesktopNotifications={
|
||||
notificationSettings.isUpdatingDesktopEnabled
|
||||
}
|
||||
notificationErrorMessage={notificationSettings.errorMessage}
|
||||
notificationPermission={notificationSettings.permission}
|
||||
notificationSettings={notificationSettings.settings}
|
||||
onClose={handleCloseSettings}
|
||||
onSectionChange={setSettingsSection}
|
||||
onSetDesktopNotificationsEnabled={
|
||||
notificationSettings.setDesktopEnabled
|
||||
}
|
||||
onSetHomeBadgeEnabled={notificationSettings.setHomeBadgeEnabled}
|
||||
onSetMentionNotificationsEnabled={
|
||||
notificationSettings.setMentionsEnabled
|
||||
}
|
||||
onSetNeedsActionNotificationsEnabled={
|
||||
notificationSettings.setNeedsActionEnabled
|
||||
}
|
||||
section={settingsSection}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarProvider>
|
||||
</AppShellProvider>
|
||||
</ChannelNavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
|
||||
type AppShellContextValue = {
|
||||
markChannelRead: (
|
||||
channelId: string,
|
||||
readAt: string | null | undefined,
|
||||
) => void;
|
||||
openChannelManagement: () => void;
|
||||
};
|
||||
|
||||
const AppShellContext = React.createContext<AppShellContextValue>({
|
||||
markChannelRead: () => {},
|
||||
openChannelManagement: () => {},
|
||||
});
|
||||
|
||||
export function AppShellProvider({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: AppShellContextValue;
|
||||
}) {
|
||||
return (
|
||||
<AppShellContext.Provider value={value}>
|
||||
{children}
|
||||
</AppShellContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAppShell() {
|
||||
return React.useContext(AppShellContext);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { getThreadReference } from "@/features/messages/lib/threading";
|
||||
import { getEventById } from "@/shared/api/tauri";
|
||||
import type { SearchHit } from "@/shared/api/types";
|
||||
import { KIND_FORUM_COMMENT, KIND_FORUM_POST } from "@/shared/constants/kinds";
|
||||
|
||||
export type SearchHitDestination =
|
||||
| {
|
||||
kind: "channel";
|
||||
channelId: string;
|
||||
messageId?: string;
|
||||
}
|
||||
| {
|
||||
kind: "forum-post";
|
||||
channelId: string;
|
||||
postId: string;
|
||||
replyId?: string;
|
||||
};
|
||||
|
||||
export async function resolveSearchHitDestination(
|
||||
hit: SearchHit,
|
||||
): Promise<SearchHitDestination | null> {
|
||||
if (!hit.channelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hit.kind === KIND_FORUM_POST) {
|
||||
return {
|
||||
kind: "forum-post",
|
||||
channelId: hit.channelId,
|
||||
postId: hit.eventId,
|
||||
};
|
||||
}
|
||||
|
||||
if (hit.kind === KIND_FORUM_COMMENT) {
|
||||
try {
|
||||
const event = await getEventById(hit.eventId);
|
||||
const thread = getThreadReference(event.tags);
|
||||
const postId = thread.rootId ?? thread.parentId ?? null;
|
||||
|
||||
if (!postId) {
|
||||
return {
|
||||
kind: "channel",
|
||||
channelId: hit.channelId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "forum-post",
|
||||
channelId: hit.channelId,
|
||||
postId,
|
||||
replyId: hit.eventId,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to resolve forum reply search destination",
|
||||
hit.eventId,
|
||||
error,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: "channel",
|
||||
channelId: hit.channelId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "channel",
|
||||
channelId: hit.channelId,
|
||||
messageId: hit.eventId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { RelayEvent, SearchHit } from "@/shared/api/types";
|
||||
|
||||
const MAX_CACHED_EVENTS = 200;
|
||||
const searchHitEventCache = new Map<string, RelayEvent>();
|
||||
|
||||
function trimCache() {
|
||||
if (searchHitEventCache.size <= MAX_CACHED_EVENTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overflow = searchHitEventCache.size - MAX_CACHED_EVENTS;
|
||||
let removed = 0;
|
||||
for (const key of searchHitEventCache.keys()) {
|
||||
if (removed >= overflow) {
|
||||
break;
|
||||
}
|
||||
searchHitEventCache.delete(key);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSearchHitEvent(hit: SearchHit): RelayEvent {
|
||||
return {
|
||||
id: hit.eventId,
|
||||
pubkey: hit.pubkey,
|
||||
created_at: hit.createdAt,
|
||||
kind: hit.kind,
|
||||
tags: hit.channelId ? [["h", hit.channelId]] : [],
|
||||
content: hit.content,
|
||||
sig: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function cacheSearchHitEvent(hit: SearchHit): RelayEvent {
|
||||
const event = buildSearchHitEvent(hit);
|
||||
searchHitEventCache.set(event.id, event);
|
||||
trimCache();
|
||||
return event;
|
||||
}
|
||||
|
||||
export function getCachedSearchHitEvent(
|
||||
eventId: string | null | undefined,
|
||||
): RelayEvent | null {
|
||||
if (!eventId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return searchHitEventCache.get(eventId) ?? null;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
useCanGoBack,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
import { cacheSearchHitEvent } from "@/app/navigation/searchHitEventCache";
|
||||
import { resolveSearchHitDestination } from "@/app/navigation/resolveSearchHitDestination";
|
||||
import type { SearchHit } from "@/shared/api/types";
|
||||
|
||||
type NavigationBehavior = {
|
||||
replace?: boolean;
|
||||
resetScroll?: boolean;
|
||||
};
|
||||
|
||||
export function useAppNavigation() {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const commitNavigation = React.useCallback(
|
||||
async (
|
||||
next: {
|
||||
to: string;
|
||||
params?: Record<string, string>;
|
||||
search?: Record<string, string | undefined>;
|
||||
},
|
||||
behavior: NavigationBehavior = {},
|
||||
) => {
|
||||
const nextLocation = router.buildLocation(next as never);
|
||||
|
||||
if (location.href === nextLocation.href) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await navigate({
|
||||
...next,
|
||||
replace: behavior.replace,
|
||||
resetScroll: behavior.resetScroll,
|
||||
} as never);
|
||||
return true;
|
||||
},
|
||||
[location.href, navigate, router],
|
||||
);
|
||||
|
||||
const goHome = React.useCallback(
|
||||
(behavior?: NavigationBehavior) =>
|
||||
commitNavigation(
|
||||
{
|
||||
to: "/",
|
||||
},
|
||||
behavior,
|
||||
),
|
||||
[commitNavigation],
|
||||
);
|
||||
|
||||
const goAgents = React.useCallback(
|
||||
(behavior?: NavigationBehavior) =>
|
||||
commitNavigation(
|
||||
{
|
||||
to: "/agents",
|
||||
},
|
||||
behavior,
|
||||
),
|
||||
[commitNavigation],
|
||||
);
|
||||
|
||||
const goWorkflows = React.useCallback(
|
||||
(behavior?: NavigationBehavior) =>
|
||||
commitNavigation(
|
||||
{
|
||||
to: "/workflows",
|
||||
},
|
||||
behavior,
|
||||
),
|
||||
[commitNavigation],
|
||||
);
|
||||
|
||||
const goWorkflow = React.useCallback(
|
||||
(workflowId: string, behavior?: NavigationBehavior) =>
|
||||
commitNavigation(
|
||||
{
|
||||
to: "/workflows/$workflowId",
|
||||
params: {
|
||||
workflowId,
|
||||
},
|
||||
},
|
||||
behavior,
|
||||
),
|
||||
[commitNavigation],
|
||||
);
|
||||
|
||||
const goChannel = React.useCallback(
|
||||
(
|
||||
channelId: string,
|
||||
options?: {
|
||||
messageId?: string;
|
||||
replace?: boolean;
|
||||
},
|
||||
) =>
|
||||
commitNavigation(
|
||||
{
|
||||
to: "/channels/$channelId",
|
||||
params: {
|
||||
channelId,
|
||||
},
|
||||
search: options?.messageId ? { messageId: options.messageId } : {},
|
||||
},
|
||||
{
|
||||
replace: options?.replace,
|
||||
resetScroll: options?.messageId ? false : undefined,
|
||||
},
|
||||
),
|
||||
[commitNavigation],
|
||||
);
|
||||
|
||||
const goForumPost = React.useCallback(
|
||||
(
|
||||
channelId: string,
|
||||
postId: string,
|
||||
options?: {
|
||||
replace?: boolean;
|
||||
replyId?: string;
|
||||
},
|
||||
) =>
|
||||
commitNavigation(
|
||||
{
|
||||
to: "/channels/$channelId/posts/$postId",
|
||||
params: {
|
||||
channelId,
|
||||
postId,
|
||||
},
|
||||
search: options?.replyId ? { replyId: options.replyId } : {},
|
||||
},
|
||||
{
|
||||
replace: options?.replace,
|
||||
resetScroll: false,
|
||||
},
|
||||
),
|
||||
[commitNavigation],
|
||||
);
|
||||
|
||||
const closeWorkflowDetail = React.useCallback(() => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
void goWorkflows({ replace: true });
|
||||
}, [canGoBack, goWorkflows, router.history]);
|
||||
|
||||
const closeForumPost = React.useCallback(
|
||||
(channelId: string) => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
void goChannel(channelId, { replace: true });
|
||||
},
|
||||
[canGoBack, goChannel, router.history],
|
||||
);
|
||||
|
||||
const openSearchHit = React.useCallback(
|
||||
async (hit: SearchHit) => {
|
||||
cacheSearchHitEvent(hit);
|
||||
|
||||
const destination = await resolveSearchHitDestination(hit);
|
||||
if (!destination) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (destination.kind === "forum-post") {
|
||||
return goForumPost(destination.channelId, destination.postId, {
|
||||
replyId: destination.replyId,
|
||||
});
|
||||
}
|
||||
|
||||
return goChannel(destination.channelId, {
|
||||
messageId: destination.messageId,
|
||||
});
|
||||
},
|
||||
[goChannel, goForumPost],
|
||||
);
|
||||
|
||||
return {
|
||||
closeForumPost,
|
||||
closeWorkflowDetail,
|
||||
goAgents,
|
||||
goChannel,
|
||||
goForumPost,
|
||||
goHome,
|
||||
goWorkflow,
|
||||
goWorkflows,
|
||||
openSearchHit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
useCanGoBack,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
type RouterHistoryState = {
|
||||
__TSR_index?: number;
|
||||
__TSR_key?: string;
|
||||
key?: string;
|
||||
};
|
||||
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
target.isContentEditable ||
|
||||
target.closest(
|
||||
'input, textarea, select, [contenteditable=""], [contenteditable="true"]',
|
||||
) !== null
|
||||
);
|
||||
}
|
||||
|
||||
function isMacPlatform() {
|
||||
return window.navigator.platform.toLowerCase().includes("mac");
|
||||
}
|
||||
|
||||
export function useBackForwardControls() {
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
const locationState = useRouterState({
|
||||
select: (state) => state.location.state,
|
||||
}) as RouterHistoryState;
|
||||
const locationIndex = locationState.__TSR_index ?? 0;
|
||||
const locationKey =
|
||||
locationState.__TSR_key ?? locationState.key ?? String(locationIndex);
|
||||
const keysByIndexRef = React.useRef(new Map<number, string>());
|
||||
const [maxIndex, setMaxIndex] = React.useState(locationIndex);
|
||||
|
||||
React.useEffect(() => {
|
||||
const keysByIndex = keysByIndexRef.current;
|
||||
const currentKey = keysByIndex.get(locationIndex);
|
||||
|
||||
if (currentKey && currentKey !== locationKey) {
|
||||
for (const storedIndex of [...keysByIndex.keys()]) {
|
||||
if (storedIndex >= locationIndex) {
|
||||
keysByIndex.delete(storedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keysByIndex.set(locationIndex, locationKey);
|
||||
setMaxIndex((current: number) => {
|
||||
if (currentKey && currentKey !== locationKey) {
|
||||
return locationIndex;
|
||||
}
|
||||
|
||||
return Math.max(current, locationIndex);
|
||||
});
|
||||
}, [locationIndex, locationKey]);
|
||||
|
||||
const canGoForward = locationIndex < maxIndex;
|
||||
|
||||
const goBack = React.useCallback(() => {
|
||||
if (!canGoBack) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.history.back();
|
||||
}, [canGoBack, router.history]);
|
||||
|
||||
const goForward = React.useCallback(() => {
|
||||
if (!canGoForward) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.history.forward();
|
||||
}, [canGoForward, router.history]);
|
||||
|
||||
const handleKeyDown = React.useEffectEvent((event: KeyboardEvent) => {
|
||||
if (isEditableTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMac = isMacPlatform();
|
||||
const isBackShortcut = isMac
|
||||
? event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
(event.key === "[" || event.code === "BracketLeft")
|
||||
: event.altKey &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
event.key === "ArrowLeft";
|
||||
const isForwardShortcut = isMac
|
||||
? event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
(event.key === "]" || event.code === "BracketRight")
|
||||
: event.altKey &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
event.key === "ArrowRight";
|
||||
|
||||
if (isBackShortcut) {
|
||||
event.preventDefault();
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isForwardShortcut) {
|
||||
event.preventDefault();
|
||||
goForward();
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
goBack,
|
||||
goForward,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// biome-ignore-all lint: generated by TanStack Router
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/root";
|
||||
import { Route as workflowsRouteImport } from "./routes/workflows";
|
||||
import { Route as agentsRouteImport } from "./routes/agents";
|
||||
import { Route as indexRouteImport } from "./routes/index";
|
||||
import { Route as workflowsDotworkflowIdRouteImport } from "./routes/workflows.$workflowId";
|
||||
import { Route as channelsDotchannelIdRouteImport } from "./routes/channels.$channelId";
|
||||
import { Route as channelsDotchannelIdDotpostsDotpostIdRouteImport } from "./routes/channels.$channelId.posts.$postId";
|
||||
|
||||
const workflowsRoute = workflowsRouteImport.update({
|
||||
id: "/workflows",
|
||||
path: "/workflows",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const agentsRoute = agentsRouteImport.update({
|
||||
id: "/agents",
|
||||
path: "/agents",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const indexRoute = indexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const workflowsDotworkflowIdRoute = workflowsDotworkflowIdRouteImport.update({
|
||||
id: "/workflows/$workflowId",
|
||||
path: "/workflows/$workflowId",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const channelsDotchannelIdRoute = channelsDotchannelIdRouteImport.update({
|
||||
id: "/channels/$channelId",
|
||||
path: "/channels/$channelId",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const channelsDotchannelIdDotpostsDotpostIdRoute =
|
||||
channelsDotchannelIdDotpostsDotpostIdRouteImport.update({
|
||||
id: "/channels/$channelId/posts/$postId",
|
||||
path: "/channels/$channelId/posts/$postId",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof indexRoute;
|
||||
"/agents": typeof agentsRoute;
|
||||
"/workflows": typeof workflowsRoute;
|
||||
"/channels/$channelId": typeof channelsDotchannelIdRoute;
|
||||
"/workflows/$workflowId": typeof workflowsDotworkflowIdRoute;
|
||||
"/channels/$channelId/posts/$postId": typeof channelsDotchannelIdDotpostsDotpostIdRoute;
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof indexRoute;
|
||||
"/agents": typeof agentsRoute;
|
||||
"/workflows": typeof workflowsRoute;
|
||||
"/channels/$channelId": typeof channelsDotchannelIdRoute;
|
||||
"/workflows/$workflowId": typeof workflowsDotworkflowIdRoute;
|
||||
"/channels/$channelId/posts/$postId": typeof channelsDotchannelIdDotpostsDotpostIdRoute;
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof indexRoute;
|
||||
"/agents": typeof agentsRoute;
|
||||
"/workflows": typeof workflowsRoute;
|
||||
"/channels/$channelId": typeof channelsDotchannelIdRoute;
|
||||
"/workflows/$workflowId": typeof workflowsDotworkflowIdRoute;
|
||||
"/channels/$channelId/posts/$postId": typeof channelsDotchannelIdDotpostsDotpostIdRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths:
|
||||
| "/"
|
||||
| "/agents"
|
||||
| "/workflows"
|
||||
| "/channels/$channelId"
|
||||
| "/workflows/$workflowId"
|
||||
| "/channels/$channelId/posts/$postId";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to:
|
||||
| "/"
|
||||
| "/agents"
|
||||
| "/workflows"
|
||||
| "/channels/$channelId"
|
||||
| "/workflows/$workflowId"
|
||||
| "/channels/$channelId/posts/$postId";
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/agents"
|
||||
| "/workflows"
|
||||
| "/channels/$channelId"
|
||||
| "/workflows/$workflowId"
|
||||
| "/channels/$channelId/posts/$postId";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
indexRoute: typeof indexRoute;
|
||||
agentsRoute: typeof agentsRoute;
|
||||
workflowsRoute: typeof workflowsRoute;
|
||||
channelsDotchannelIdRoute: typeof channelsDotchannelIdRoute;
|
||||
workflowsDotworkflowIdRoute: typeof workflowsDotworkflowIdRoute;
|
||||
channelsDotchannelIdDotpostsDotpostIdRoute: typeof channelsDotchannelIdDotpostsDotpostIdRoute;
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
"/workflows": {
|
||||
id: "/workflows";
|
||||
path: "/workflows";
|
||||
fullPath: "/workflows";
|
||||
preLoaderRoute: typeof workflowsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/agents": {
|
||||
id: "/agents";
|
||||
path: "/agents";
|
||||
fullPath: "/agents";
|
||||
preLoaderRoute: typeof agentsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof indexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/workflows/$workflowId": {
|
||||
id: "/workflows/$workflowId";
|
||||
path: "/workflows/$workflowId";
|
||||
fullPath: "/workflows/$workflowId";
|
||||
preLoaderRoute: typeof workflowsDotworkflowIdRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/channels/$channelId": {
|
||||
id: "/channels/$channelId";
|
||||
path: "/channels/$channelId";
|
||||
fullPath: "/channels/$channelId";
|
||||
preLoaderRoute: typeof channelsDotchannelIdRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/channels/$channelId/posts/$postId": {
|
||||
id: "/channels/$channelId/posts/$postId";
|
||||
path: "/channels/$channelId/posts/$postId";
|
||||
fullPath: "/channels/$channelId/posts/$postId";
|
||||
preLoaderRoute: typeof channelsDotchannelIdDotpostsDotpostIdRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
indexRoute: indexRoute,
|
||||
agentsRoute: agentsRoute,
|
||||
workflowsRoute: workflowsRoute,
|
||||
channelsDotchannelIdRoute: channelsDotchannelIdRoute,
|
||||
workflowsDotworkflowIdRoute: workflowsDotworkflowIdRoute,
|
||||
channelsDotchannelIdDotpostsDotpostIdRoute:
|
||||
channelsDotchannelIdDotpostsDotpostIdRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createHashHistory, createRouter } from "@tanstack/react-router";
|
||||
|
||||
import { routeTree } from "@/app/routeTree.gen";
|
||||
|
||||
export const router = createRouter({
|
||||
routeTree,
|
||||
history: createHashHistory(),
|
||||
scrollRestoration: true,
|
||||
getScrollRestorationKey: (location: { pathname: string }) =>
|
||||
location.pathname,
|
||||
});
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { index, rootRoute, route } from "@tanstack/virtual-file-routes";
|
||||
|
||||
export const routes = rootRoute("root.tsx", [
|
||||
index("index.tsx"),
|
||||
route("/agents", "agents.tsx"),
|
||||
route("/workflows", "workflows.tsx"),
|
||||
route("/workflows/$workflowId", "workflows.$workflowId.tsx"),
|
||||
route("/channels/$channelId", "channels.$channelId.tsx"),
|
||||
route(
|
||||
"/channels/$channelId/posts/$postId",
|
||||
"channels.$channelId.posts.$postId.tsx",
|
||||
),
|
||||
]);
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
|
||||
const AgentsScreen = React.lazy(async () => {
|
||||
const module = await import("@/features/agents/ui/AgentsScreen");
|
||||
return { default: module.AgentsScreen };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/agents")({
|
||||
component: AgentsRouteComponent,
|
||||
});
|
||||
|
||||
function AgentsRouteComponent() {
|
||||
return (
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback includeHeader kind="agents" />}
|
||||
>
|
||||
<AgentsScreen />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { ChannelRouteScreen } from "@/app/routes/channels.$channelId";
|
||||
|
||||
type ForumPostRouteSearch = {
|
||||
replyId?: string;
|
||||
};
|
||||
|
||||
function validateForumPostSearch(
|
||||
search: Record<string, unknown>,
|
||||
): ForumPostRouteSearch {
|
||||
return {
|
||||
replyId:
|
||||
typeof search.replyId === "string" && search.replyId.length > 0
|
||||
? search.replyId
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/channels/$channelId/posts/$postId")({
|
||||
validateSearch: validateForumPostSearch,
|
||||
component: ForumPostRouteComponent,
|
||||
});
|
||||
|
||||
function ForumPostRouteComponent() {
|
||||
const { channelId, postId } = Route.useParams();
|
||||
const search = Route.useSearch();
|
||||
|
||||
return (
|
||||
<ChannelRouteScreen
|
||||
channelId={channelId}
|
||||
selectedPostId={postId}
|
||||
targetMessageId={null}
|
||||
targetReplyId={search.replyId ?? null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import * as React from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { getCachedSearchHitEvent } from "@/app/navigation/searchHitEventCache";
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { ChannelScreen } from "@/features/channels/ui/ChannelScreen";
|
||||
import { useProfileQuery } from "@/features/profile/hooks";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { getEventById } from "@/shared/api/tauri";
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
|
||||
type ChannelRouteSearch = {
|
||||
messageId?: string;
|
||||
};
|
||||
|
||||
function validateChannelSearch(
|
||||
search: Record<string, unknown>,
|
||||
): ChannelRouteSearch {
|
||||
return {
|
||||
messageId:
|
||||
typeof search.messageId === "string" && search.messageId.length > 0
|
||||
? search.messageId
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/channels/$channelId")({
|
||||
validateSearch: validateChannelSearch,
|
||||
component: ChannelRouteComponent,
|
||||
});
|
||||
|
||||
export function ChannelRouteScreen({
|
||||
channelId,
|
||||
selectedPostId,
|
||||
targetMessageId,
|
||||
targetReplyId,
|
||||
}: {
|
||||
channelId: string;
|
||||
selectedPostId: string | null;
|
||||
targetMessageId: string | null;
|
||||
targetReplyId: string | null;
|
||||
}) {
|
||||
const { closeForumPost, goForumPost } = useAppNavigation();
|
||||
const channelsQuery = useChannelsQuery();
|
||||
const identityQuery = useIdentityQuery();
|
||||
const profileQuery = useProfileQuery();
|
||||
const channels = channelsQuery.data ?? [];
|
||||
const activeChannel =
|
||||
channels.find((channel) => channel.id === channelId) ?? null;
|
||||
const [targetMessageEvent, setTargetMessageEvent] =
|
||||
React.useState<RelayEvent | null>(() =>
|
||||
getCachedSearchHitEvent(targetMessageId),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
if (!targetMessageId || selectedPostId) {
|
||||
setTargetMessageEvent(null);
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
setTargetMessageEvent(getCachedSearchHitEvent(targetMessageId));
|
||||
void getEventById(targetMessageId)
|
||||
.then((event) => {
|
||||
if (!isCancelled) {
|
||||
setTargetMessageEvent(event);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!isCancelled) {
|
||||
console.error(
|
||||
"Failed to load route target event",
|
||||
targetMessageId,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [selectedPostId, targetMessageId]);
|
||||
|
||||
if (channelsQuery.isPending && !activeChannel) {
|
||||
return <ViewLoadingFallback includeHeader kind="channel" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelScreen
|
||||
activeChannel={activeChannel}
|
||||
currentIdentity={identityQuery.data}
|
||||
currentProfile={profileQuery.data}
|
||||
onCloseForumPost={() => {
|
||||
void closeForumPost(channelId);
|
||||
}}
|
||||
onSelectForumPost={(postId) => {
|
||||
void goForumPost(channelId, postId);
|
||||
}}
|
||||
selectedForumPostId={selectedPostId}
|
||||
targetForumReplyId={targetReplyId}
|
||||
targetMessageEvent={targetMessageEvent}
|
||||
targetMessageId={targetMessageId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelRouteComponent() {
|
||||
const { channelId } = Route.useParams();
|
||||
const search = Route.useSearch();
|
||||
|
||||
return (
|
||||
<ChannelRouteScreen
|
||||
channelId={channelId}
|
||||
selectedPostId={null}
|
||||
targetMessageId={search.messageId ?? null}
|
||||
targetReplyId={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { HomeScreen } from "@/features/home/ui/HomeScreen";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomeRouteComponent,
|
||||
});
|
||||
|
||||
function HomeRouteComponent() {
|
||||
const { goChannel } = useAppNavigation();
|
||||
const channelsQuery = useChannelsQuery();
|
||||
const identityQuery = useIdentityQuery();
|
||||
const channels = channelsQuery.data ?? [];
|
||||
const availableChannelIds = new Set(channels.map((channel) => channel.id));
|
||||
|
||||
return (
|
||||
<HomeScreen
|
||||
availableChannelIds={availableChannelIds}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
onOpenChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createRootRoute } from "@tanstack/react-router";
|
||||
|
||||
import { AppShell } from "@/app/AppShell";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: AppShell,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { WorkflowsRouteScreen } from "@/app/routes/workflows";
|
||||
|
||||
export const Route = createFileRoute("/workflows/$workflowId")({
|
||||
component: WorkflowDetailRouteComponent,
|
||||
});
|
||||
|
||||
function WorkflowDetailRouteComponent() {
|
||||
const { workflowId } = Route.useParams();
|
||||
|
||||
return <WorkflowsRouteScreen selectedWorkflowId={workflowId} />;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { WorkflowsScreen } from "@/features/workflows/ui/WorkflowsScreen";
|
||||
|
||||
export const Route = createFileRoute("/workflows")({
|
||||
component: WorkflowsRouteComponent,
|
||||
});
|
||||
|
||||
export function WorkflowsRouteScreen({
|
||||
selectedWorkflowId,
|
||||
}: {
|
||||
selectedWorkflowId: string | null;
|
||||
}) {
|
||||
const { closeWorkflowDetail, goWorkflow } = useAppNavigation();
|
||||
const channelsQuery = useChannelsQuery();
|
||||
const channels = channelsQuery.data ?? [];
|
||||
const memberChannels = channels.filter((channel) => channel.isMember);
|
||||
|
||||
return (
|
||||
<WorkflowsScreen
|
||||
channels={memberChannels}
|
||||
onCloseWorkflow={closeWorkflowDetail}
|
||||
onSelectWorkflow={(workflowId) => {
|
||||
void goWorkflow(workflowId);
|
||||
}}
|
||||
selectedWorkflowId={selectedWorkflowId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowsRouteComponent() {
|
||||
return <WorkflowsRouteScreen selectedWorkflowId={null} />;
|
||||
}
|
||||
@@ -17,9 +17,7 @@ export function AgentsScreen() {
|
||||
title="Agents"
|
||||
/>
|
||||
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading agents..." />}
|
||||
>
|
||||
<React.Suspense fallback={<ViewLoadingFallback kind="agents" />}>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<AgentsView />
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ type ChannelPaneProps = {
|
||||
mentionPubkeys: string[],
|
||||
mediaTags?: string[][],
|
||||
) => Promise<void>;
|
||||
onTargetReached: (messageId: string) => void;
|
||||
onTargetReached?: (messageId: string) => void;
|
||||
onToggleReaction?: (
|
||||
message: TimelineMessage,
|
||||
emoji: string,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAppShell } from "@/app/AppShellContext";
|
||||
import { ChatHeader } from "@/features/chat/ui/ChatHeader";
|
||||
import { useActiveChannelHeader } from "@/features/channels/useActiveChannelHeader";
|
||||
import { useChannelPaneHandlers } from "@/features/channels/useChannelPaneHandlers";
|
||||
@@ -36,7 +37,6 @@ import type {
|
||||
Identity,
|
||||
Profile,
|
||||
RelayEvent,
|
||||
SearchHit,
|
||||
} from "@/shared/api/types";
|
||||
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
|
||||
@@ -54,29 +54,27 @@ type ChannelScreenProps = {
|
||||
activeChannel: Channel | null;
|
||||
currentIdentity?: Identity;
|
||||
currentProfile?: Profile;
|
||||
onManageChannel: () => void;
|
||||
onMarkChannelRead: (
|
||||
channelId: string,
|
||||
readAt: string | null | undefined,
|
||||
) => void;
|
||||
onTargetReached: (messageId: string) => void;
|
||||
searchAnchor: SearchHit | null;
|
||||
searchAnchorChannelId: string | null;
|
||||
searchAnchorEvent: RelayEvent | null;
|
||||
onCloseForumPost: () => void;
|
||||
onSelectForumPost: (postId: string) => void;
|
||||
selectedForumPostId: string | null;
|
||||
targetForumReplyId: string | null;
|
||||
targetMessageEvent: RelayEvent | null;
|
||||
targetMessageId: string | null;
|
||||
};
|
||||
|
||||
export function ChannelScreen({
|
||||
activeChannel,
|
||||
currentIdentity,
|
||||
currentProfile,
|
||||
onManageChannel,
|
||||
onMarkChannelRead,
|
||||
onTargetReached,
|
||||
searchAnchor,
|
||||
searchAnchorChannelId,
|
||||
searchAnchorEvent,
|
||||
onCloseForumPost,
|
||||
onSelectForumPost,
|
||||
selectedForumPostId,
|
||||
targetForumReplyId,
|
||||
targetMessageEvent,
|
||||
targetMessageId,
|
||||
}: ChannelScreenProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { markChannelRead, openChannelManagement } = useAppShell();
|
||||
const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false);
|
||||
const [replyTargetId, setReplyTargetId] = React.useState<string | null>(null);
|
||||
const [editTargetId, setEditTargetId] = React.useState<string | null>(null);
|
||||
@@ -98,8 +96,8 @@ export function ChannelScreen({
|
||||
return;
|
||||
}
|
||||
|
||||
onMarkChannelRead(activeChannelId, activeReadAt);
|
||||
}, [activeChannelId, activeReadAt, onMarkChannelRead]);
|
||||
markChannelRead(activeChannelId, activeReadAt);
|
||||
}, [activeChannelId, activeReadAt, markChannelRead]);
|
||||
|
||||
const { activeChannelTitle, activeDmPresenceStatus } = useActiveChannelHeader(
|
||||
activeChannel,
|
||||
@@ -116,21 +114,12 @@ export function ChannelScreen({
|
||||
const resolvedMessages = React.useMemo(() => {
|
||||
const currentMessages = messagesQuery.data ?? [];
|
||||
|
||||
if (
|
||||
!activeChannel ||
|
||||
!searchAnchorEvent ||
|
||||
searchAnchorChannelId !== activeChannel.id
|
||||
) {
|
||||
if (!activeChannel || !targetMessageEvent) {
|
||||
return currentMessages;
|
||||
}
|
||||
|
||||
return mergeMessages(currentMessages, searchAnchorEvent);
|
||||
}, [
|
||||
activeChannel,
|
||||
messagesQuery.data,
|
||||
searchAnchorChannelId,
|
||||
searchAnchorEvent,
|
||||
]);
|
||||
return mergeMessages(currentMessages, targetMessageEvent);
|
||||
}, [activeChannel, messagesQuery.data, targetMessageEvent]);
|
||||
const messageAuthorPubkeys = React.useMemo(
|
||||
() => collectMessageAuthorPubkeys(resolvedMessages),
|
||||
[resolvedMessages],
|
||||
@@ -190,24 +179,6 @@ export function ChannelScreen({
|
||||
timelineMessages.find((message) => message.id === editTargetId) ?? null,
|
||||
[editTargetId, timelineMessages],
|
||||
);
|
||||
const forumTargetEventId =
|
||||
activeChannel &&
|
||||
activeChannel.channelType === "forum" &&
|
||||
searchAnchorChannelId === activeChannel.id
|
||||
? (searchAnchor?.eventId ?? null)
|
||||
: null;
|
||||
const forumTargetEventKind =
|
||||
activeChannel &&
|
||||
activeChannel.channelType === "forum" &&
|
||||
searchAnchorChannelId === activeChannel.id
|
||||
? (searchAnchor?.kind ?? null)
|
||||
: null;
|
||||
const forumTargetEvent =
|
||||
activeChannel &&
|
||||
activeChannel.channelType === "forum" &&
|
||||
searchAnchorChannelId === activeChannel.id
|
||||
? searchAnchorEvent
|
||||
: null;
|
||||
|
||||
const {
|
||||
handleCancelEdit,
|
||||
@@ -372,7 +343,7 @@ export function ChannelScreen({
|
||||
<ChannelMembersBar
|
||||
channel={activeChannel}
|
||||
currentPubkey={currentPubkey}
|
||||
onManageChannel={onManageChannel}
|
||||
onManageChannel={openChannelManagement}
|
||||
onToggleMembers={() => setIsMembersSidebarOpen((prev) => !prev)}
|
||||
/>
|
||||
) : null
|
||||
@@ -394,22 +365,18 @@ export function ChannelScreen({
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{activeChannel ? (
|
||||
activeChannel.channelType === "forum" ? (
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading forum..." />}
|
||||
>
|
||||
<React.Suspense fallback={<ViewLoadingFallback kind="forum" />}>
|
||||
<ForumView
|
||||
channel={activeChannel}
|
||||
currentPubkey={currentPubkey}
|
||||
onTargetReached={onTargetReached}
|
||||
targetEvent={forumTargetEvent}
|
||||
targetEventId={forumTargetEventId}
|
||||
targetEventKind={forumTargetEventKind}
|
||||
onClosePost={onCloseForumPost}
|
||||
onSelectPost={onSelectForumPost}
|
||||
selectedPostId={selectedForumPostId}
|
||||
targetReplyId={targetForumReplyId}
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : (
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading channel..." />}
|
||||
>
|
||||
<React.Suspense fallback={<ViewLoadingFallback kind="channel" />}>
|
||||
<ChannelPane
|
||||
activeChannel={activeChannel}
|
||||
currentPubkey={currentPubkey}
|
||||
@@ -435,16 +402,11 @@ export function ChannelScreen({
|
||||
onEditSave={handleEditSave}
|
||||
onReply={handleReply}
|
||||
onSend={handleSend}
|
||||
onTargetReached={onTargetReached}
|
||||
onToggleReaction={effectiveToggleReaction}
|
||||
profiles={messageProfiles}
|
||||
replyTargetId={replyTargetId}
|
||||
replyTargetMessage={replyTargetMessage}
|
||||
targetMessageId={
|
||||
activeChannel && searchAnchor?.channelId === activeChannel.id
|
||||
? searchAnchor.eventId
|
||||
: null
|
||||
}
|
||||
targetMessageId={targetMessageId}
|
||||
typingPubkeys={typingPubkeys}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
@@ -267,7 +267,11 @@ export function ForumThreadPanel({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto" ref={scrollRef}>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto"
|
||||
data-scroll-restoration-id={`forum-thread:${channelId}`}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{/* Original post */}
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { MessageSquareText } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { getThreadReference } from "@/features/messages/lib/threading";
|
||||
import { useProfileQuery, useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
import { mergeCurrentProfileIntoLookup } from "@/features/profile/lib/identity";
|
||||
import type { Channel, RelayEvent } from "@/shared/api/types";
|
||||
import { KIND_FORUM_COMMENT, KIND_FORUM_POST } from "@/shared/constants/kinds";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
|
||||
import {
|
||||
@@ -23,10 +21,11 @@ import { ForumThreadPanel } from "./ForumThreadPanel";
|
||||
type ForumViewProps = {
|
||||
channel: Channel;
|
||||
currentPubkey?: string;
|
||||
onClosePost: () => void;
|
||||
onSelectPost: (postId: string) => void;
|
||||
onTargetReached?: (messageId: string) => void;
|
||||
targetEvent: RelayEvent | null;
|
||||
targetEventId: string | null;
|
||||
targetEventKind: number | null;
|
||||
selectedPostId: string | null;
|
||||
targetReplyId: string | null;
|
||||
};
|
||||
|
||||
function canDelete(postPubkey: string, currentPubkey?: string): boolean {
|
||||
@@ -39,28 +38,26 @@ function canDelete(postPubkey: string, currentPubkey?: string): boolean {
|
||||
export function ForumView({
|
||||
channel,
|
||||
currentPubkey,
|
||||
onClosePost,
|
||||
onSelectPost,
|
||||
onTargetReached,
|
||||
targetEvent,
|
||||
targetEventId,
|
||||
targetEventKind,
|
||||
selectedPostId,
|
||||
targetReplyId,
|
||||
}: ForumViewProps) {
|
||||
const [expandedPostId, setExpandedPostId] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isComposerOpen, setIsComposerOpen] = React.useState(false);
|
||||
|
||||
const profileQuery = useProfileQuery();
|
||||
const postsQuery = useForumPostsQuery(channel);
|
||||
const threadQuery = useForumThreadQuery(
|
||||
expandedPostId ? channel.id : null,
|
||||
expandedPostId,
|
||||
selectedPostId ? channel.id : null,
|
||||
selectedPostId,
|
||||
);
|
||||
const createPostMutation = useCreateForumPostMutation(channel);
|
||||
const createReplyMutation = useCreateForumReplyMutation(channel);
|
||||
const deletePostMutation = useDeleteForumPostMutation(channel);
|
||||
const deleteReplyMutation = useDeleteForumReplyMutation(
|
||||
channel,
|
||||
expandedPostId,
|
||||
selectedPostId,
|
||||
);
|
||||
|
||||
const posts = postsQuery.data?.posts ?? [];
|
||||
@@ -97,40 +94,18 @@ export function ForumView({
|
||||
),
|
||||
[profileQuery.data, profilesQuery.data?.profiles],
|
||||
);
|
||||
const targetThreadId = React.useMemo(() => {
|
||||
if (!targetEventId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (targetEventKind === KIND_FORUM_POST) {
|
||||
return targetEventId;
|
||||
}
|
||||
|
||||
if (!targetEvent || targetEvent.kind !== KIND_FORUM_COMMENT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const thread = getThreadReference(targetEvent.tags);
|
||||
return thread.rootId ?? thread.parentId ?? null;
|
||||
}, [targetEvent, targetEventId, targetEventKind]);
|
||||
|
||||
// Reset expanded post when channel changes
|
||||
const previousChannelIdRef = React.useRef(channel.id);
|
||||
if (previousChannelIdRef.current !== channel.id) {
|
||||
previousChannelIdRef.current = channel.id;
|
||||
setExpandedPostId(null);
|
||||
setIsComposerOpen(false);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!targetThreadId || expandedPostId === targetThreadId) {
|
||||
if (previousChannelIdRef.current === channel.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedPostId(targetThreadId);
|
||||
}, [expandedPostId, targetThreadId]);
|
||||
previousChannelIdRef.current = channel.id;
|
||||
setIsComposerOpen(false);
|
||||
}, [channel.id]);
|
||||
|
||||
if (expandedPostId) {
|
||||
if (selectedPostId) {
|
||||
const threadPost = threadQuery.data?.post;
|
||||
const canDeleteExpandedPost = threadPost
|
||||
? canDelete(threadPost.pubkey, effectiveCurrentPubkey)
|
||||
@@ -143,12 +118,9 @@ export function ForumView({
|
||||
isDeletingPost={deletePostMutation.isPending}
|
||||
isLoading={threadQuery.isLoading}
|
||||
isSendingReply={createReplyMutation.isPending}
|
||||
onBack={() => setExpandedPostId(null)}
|
||||
onBack={onClosePost}
|
||||
onDeletePost={(eventId) => {
|
||||
deletePostMutation.mutate(
|
||||
{ eventId },
|
||||
{ onSuccess: () => setExpandedPostId(null) },
|
||||
);
|
||||
deletePostMutation.mutate({ eventId }, { onSuccess: onClosePost });
|
||||
}}
|
||||
onDeleteReply={(eventId) => {
|
||||
deleteReplyMutation.mutate({ eventId });
|
||||
@@ -157,13 +129,13 @@ export function ForumView({
|
||||
onReply={(content, mentionPubkeys) => {
|
||||
createReplyMutation.mutate({
|
||||
content,
|
||||
parentEventId: expandedPostId,
|
||||
parentEventId: selectedPostId,
|
||||
mentionPubkeys,
|
||||
});
|
||||
}}
|
||||
onTargetReached={onTargetReached}
|
||||
profiles={profiles}
|
||||
targetEventId={targetEventId}
|
||||
targetEventId={targetReplyId}
|
||||
thread={threadQuery.data}
|
||||
/>
|
||||
);
|
||||
@@ -208,7 +180,10 @@ export function ForumView({
|
||||
</div>
|
||||
|
||||
{/* Post list */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div
|
||||
className="flex-1 overflow-y-auto"
|
||||
data-scroll-restoration-id={`forum-list:${channel.id}`}
|
||||
>
|
||||
{postsQuery.isLoading ? (
|
||||
<div className="space-y-3 p-4">
|
||||
<Skeleton className="h-24 w-full rounded-xl" />
|
||||
@@ -233,13 +208,13 @@ export function ForumView({
|
||||
<ForumPostCard
|
||||
canDelete={canDelete(post.pubkey, effectiveCurrentPubkey)}
|
||||
currentPubkey={effectiveCurrentPubkey}
|
||||
isActive={false}
|
||||
isActive={selectedPostId === post.eventId}
|
||||
isDeleting={
|
||||
deletePostMutation.isPending &&
|
||||
deletePostMutation.variables?.eventId === post.eventId
|
||||
}
|
||||
key={post.eventId}
|
||||
onClick={() => setExpandedPostId(post.eventId)}
|
||||
onClick={() => onSelectPost(post.eventId)}
|
||||
onDelete={(eventId) => {
|
||||
deletePostMutation.mutate({ eventId });
|
||||
}}
|
||||
|
||||
@@ -102,6 +102,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
) : null}
|
||||
<div
|
||||
className="h-full overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 [overflow-anchor:none] sm:px-6"
|
||||
data-scroll-restoration-id="message-timeline"
|
||||
data-testid="message-timeline"
|
||||
onScroll={syncScrollState}
|
||||
ref={scrollContainerRef}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type WorkflowCardProps = {
|
||||
workflow: Workflow;
|
||||
channelName?: string;
|
||||
isActive?: boolean;
|
||||
onSelect: (workflowId: string) => void;
|
||||
onTrigger: (workflowId: string) => void;
|
||||
onEdit: (workflow: Workflow) => void;
|
||||
@@ -51,6 +52,7 @@ function StatusBadge({ status }: { status: Workflow["status"] }) {
|
||||
export function WorkflowCard({
|
||||
workflow,
|
||||
channelName,
|
||||
isActive = false,
|
||||
onSelect,
|
||||
onTrigger,
|
||||
onEdit,
|
||||
@@ -63,7 +65,9 @@ export function WorkflowCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full rounded-lg border bg-card p-3 text-left transition-colors hover:bg-muted/50"
|
||||
className={`relative w-full rounded-lg border bg-card p-3 text-left transition-colors hover:bg-muted/50 ${
|
||||
isActive ? "border-primary/40 bg-primary/5 shadow-sm" : ""
|
||||
}`}
|
||||
data-testid={`workflow-card-${workflow.id}`}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { WorkflowRunTrace } from "@/features/workflows/ui/WorkflowRunTrace";
|
||||
import type { Workflow } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
import {
|
||||
getWorkflowDescription,
|
||||
getWorkflowDisplayStatus,
|
||||
@@ -60,20 +61,28 @@ export function WorkflowDetailPanel({
|
||||
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="truncate text-sm font-semibold">
|
||||
{workflow?.name ?? "Loading..."}
|
||||
</h3>
|
||||
{workflow ? (
|
||||
<h3 className="truncate text-sm font-semibold">
|
||||
{workflow.name}
|
||||
</h3>
|
||||
) : (
|
||||
<Skeleton className="h-4 w-36" />
|
||||
)}
|
||||
{workflowStatus ? <RunStatusBadge status={workflowStatus} /> : null}
|
||||
</div>
|
||||
{workflowDescription ? (
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{workflowDescription}
|
||||
</p>
|
||||
) : workflowQuery.isLoading ? (
|
||||
<Skeleton className="mt-1 h-3 w-full max-w-64" />
|
||||
) : null}
|
||||
{triggerSummary ? (
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{triggerSummary}
|
||||
</p>
|
||||
) : workflowQuery.isLoading ? (
|
||||
<Skeleton className="mt-1 h-3 w-40" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -88,7 +97,7 @@ export function WorkflowDetailPanel({
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
disabled={triggerMutation.isPending}
|
||||
disabled={triggerMutation.isPending || workflowQuery.isLoading}
|
||||
onClick={() => void handleTrigger()}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -113,7 +122,10 @@ export function WorkflowDetailPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div
|
||||
className="flex-1 overflow-y-auto"
|
||||
data-scroll-restoration-id={`workflow-detail:${workflowId}`}
|
||||
>
|
||||
{workflow ? (
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
@@ -234,8 +246,18 @@ export function WorkflowDetailPanel({
|
||||
<p className="text-sm text-red-400">Failed to load workflow</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<Skeleton className="mb-2 h-4 w-28" />
|
||||
<Skeleton className="h-40 w-full rounded-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="mb-2 h-4 w-24" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-16 w-full rounded-xl" />
|
||||
<Skeleton className="h-16 w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,17 @@ const WorkflowsView = React.lazy(async () => {
|
||||
|
||||
type WorkflowsScreenProps = {
|
||||
channels: Channel[];
|
||||
onCloseWorkflow: () => void;
|
||||
onSelectWorkflow: (workflowId: string) => void;
|
||||
selectedWorkflowId: string | null;
|
||||
};
|
||||
|
||||
export function WorkflowsScreen({ channels }: WorkflowsScreenProps) {
|
||||
export function WorkflowsScreen({
|
||||
channels,
|
||||
onCloseWorkflow,
|
||||
onSelectWorkflow,
|
||||
selectedWorkflowId,
|
||||
}: WorkflowsScreenProps) {
|
||||
return (
|
||||
<>
|
||||
<ChatHeader
|
||||
@@ -22,11 +30,14 @@ export function WorkflowsScreen({ channels }: WorkflowsScreenProps) {
|
||||
title="Workflows"
|
||||
/>
|
||||
|
||||
<React.Suspense
|
||||
fallback={<ViewLoadingFallback label="Loading workflows..." />}
|
||||
>
|
||||
<React.Suspense fallback={<ViewLoadingFallback kind="workflows" />}>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<WorkflowsView channels={channels} />
|
||||
<WorkflowsView
|
||||
channels={channels}
|
||||
onCloseWorkflow={onCloseWorkflow}
|
||||
onSelectWorkflow={onSelectWorkflow}
|
||||
selectedWorkflowId={selectedWorkflowId}
|
||||
/>
|
||||
</div>
|
||||
</React.Suspense>
|
||||
</>
|
||||
|
||||
@@ -14,9 +14,13 @@ import {
|
||||
triggerWorkflow,
|
||||
} from "@/shared/api/tauriWorkflows";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
|
||||
type WorkflowsViewProps = {
|
||||
channels: Channel[];
|
||||
onCloseWorkflow: () => void;
|
||||
onSelectWorkflow: (workflowId: string) => void;
|
||||
selectedWorkflowId: string | null;
|
||||
};
|
||||
|
||||
type WorkflowWithChannel = {
|
||||
@@ -30,14 +34,48 @@ type DialogState =
|
||||
| { mode: "edit"; workflow: Workflow }
|
||||
| { mode: "duplicate"; workflow: Workflow };
|
||||
|
||||
export function WorkflowsView({ channels }: WorkflowsViewProps) {
|
||||
function WorkflowsListSkeleton() {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{["first", "second", "third", "fourth"].map((card) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-4 shadow-sm"
|
||||
key={card}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-5 w-44" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full max-w-2xl" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-5 w-20 rounded-full" />
|
||||
<Skeleton className="h-5 w-24 rounded-full" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden shrink-0 gap-2 sm:flex">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowsView({
|
||||
channels,
|
||||
onCloseWorkflow,
|
||||
onSelectWorkflow,
|
||||
selectedWorkflowId,
|
||||
}: WorkflowsViewProps) {
|
||||
const [dialogState, setDialogState] = React.useState<DialogState>({
|
||||
mode: "closed",
|
||||
});
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<Workflow | null>(null);
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const memberChannels = channels.filter((c) => c.isMember);
|
||||
@@ -77,9 +115,9 @@ export function WorkflowsView({ channels }: WorkflowsViewProps) {
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (workflowId: string) => deleteWorkflow(workflowId),
|
||||
onSuccess: (_data, workflowId) => {
|
||||
setSelectedWorkflowId((current) =>
|
||||
current === workflowId ? null : current,
|
||||
);
|
||||
if (selectedWorkflowId === workflowId) {
|
||||
onCloseWorkflow();
|
||||
}
|
||||
void queryClient.invalidateQueries({
|
||||
predicate: (query) =>
|
||||
query.queryKey[0] === "workflows" ||
|
||||
@@ -129,7 +167,10 @@ export function WorkflowsView({ channels }: WorkflowsViewProps) {
|
||||
className="flex min-h-0 flex-1 overflow-hidden"
|
||||
data-testid="workflows-view"
|
||||
>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
|
||||
<div
|
||||
className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4"
|
||||
data-scroll-restoration-id="workflows-list"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold">Workflows</h2>
|
||||
@@ -152,11 +193,7 @@ export function WorkflowsView({ channels }: WorkflowsViewProps) {
|
||||
</div>
|
||||
|
||||
{allWorkflowsQuery.isLoading ? (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading workflows...
|
||||
</p>
|
||||
</div>
|
||||
<WorkflowsListSkeleton />
|
||||
) : allWorkflowsQuery.isError ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||
<p className="text-sm text-red-400">Failed to load workflows</p>
|
||||
@@ -186,11 +223,12 @@ export function WorkflowsView({ channels }: WorkflowsViewProps) {
|
||||
{allWorkflows.map(({ workflow, channelName }) => (
|
||||
<WorkflowCard
|
||||
channelName={channelName}
|
||||
isActive={selectedWorkflowId === workflow.id}
|
||||
key={workflow.id}
|
||||
onDelete={handleDelete}
|
||||
onDuplicate={handleDuplicate}
|
||||
onEdit={handleEdit}
|
||||
onSelect={setSelectedWorkflowId}
|
||||
onSelect={onSelectWorkflow}
|
||||
onTrigger={handleTrigger}
|
||||
workflow={workflow}
|
||||
/>
|
||||
@@ -203,7 +241,7 @@ export function WorkflowsView({ channels }: WorkflowsViewProps) {
|
||||
<div className="w-[400px] shrink-0">
|
||||
<WorkflowDetailPanel
|
||||
key={selectedWorkflowId}
|
||||
onClose={() => setSelectedWorkflowId(null)}
|
||||
onClose={onCloseWorkflow}
|
||||
onEdit={handleEdit}
|
||||
workflowId={selectedWorkflowId}
|
||||
/>
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare module "jdenticon" {
|
||||
export function toSvg(hashOrValue: string, size?: number): string;
|
||||
}
|
||||
@@ -4,28 +4,21 @@ import type { Channel } from "@/shared/api/types";
|
||||
|
||||
type ChannelNavigationContextValue = {
|
||||
channels: Channel[];
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
};
|
||||
|
||||
const ChannelNavigationContext =
|
||||
React.createContext<ChannelNavigationContextValue>({
|
||||
channels: [],
|
||||
onOpenChannel: () => {},
|
||||
});
|
||||
|
||||
export function ChannelNavigationProvider({
|
||||
channels,
|
||||
children,
|
||||
onOpenChannel,
|
||||
}: {
|
||||
channels: Channel[];
|
||||
children: React.ReactNode;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
}) {
|
||||
const value = React.useMemo(
|
||||
() => ({ channels, onOpenChannel }),
|
||||
[channels, onOpenChannel],
|
||||
);
|
||||
const value = React.useMemo(() => ({ channels }), [channels]);
|
||||
|
||||
return (
|
||||
<ChannelNavigationContext.Provider value={value}>
|
||||
|
||||
@@ -1,11 +1,260 @@
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
|
||||
type ViewLoadingFallbackKind = "agents" | "channel" | "forum" | "workflows";
|
||||
|
||||
type ViewLoadingFallbackProps = {
|
||||
label: string;
|
||||
includeHeader?: boolean;
|
||||
kind: ViewLoadingFallbackKind;
|
||||
};
|
||||
|
||||
export function ViewLoadingFallback({ label }: ViewLoadingFallbackProps) {
|
||||
function LoadingHeaderSkeleton() {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center px-6 py-8">
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
<header
|
||||
className="flex min-w-0 items-center gap-3 border-b border-border/80 bg-background px-4 pb-3 pt-8 sm:px-6"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Skeleton className="h-5 w-5 rounded-sm" />
|
||||
<Skeleton className="h-6 w-36 max-w-[50vw]" />
|
||||
</div>
|
||||
<Skeleton className="mt-2 h-4 w-full max-w-2xl" />
|
||||
</div>
|
||||
<div className="hidden shrink-0 items-center gap-2 sm:flex">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageRowsSkeleton() {
|
||||
return (
|
||||
<>
|
||||
{["first", "second", "third", "fourth", "fifth"].map((row) => (
|
||||
<div className="flex gap-3" key={row}>
|
||||
<Skeleton className="h-8 w-8 shrink-0 rounded-lg" />
|
||||
<div className="min-w-0 flex-1 space-y-2 pt-0.5">
|
||||
<Skeleton className="h-3.5 w-40 max-w-[40%]" />
|
||||
<Skeleton className="h-4 w-full max-w-3xl" />
|
||||
<Skeleton className="h-4 w-full max-w-xl" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentsLoadingBody() {
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto p-4">
|
||||
<div className="space-y-6">
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-28" />
|
||||
<Skeleton className="h-4 w-72 max-w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
||||
{["first", "second", "third", "fourth"].map((card) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-3 shadow-sm"
|
||||
key={card}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
<Skeleton className="h-4 w-80 max-w-full" />
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{["alpha", "beta", "gamma"].map((card) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-4 shadow-sm"
|
||||
key={card}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-3 w-20 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-7 w-16 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-4/5" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-36" />
|
||||
<Skeleton className="h-4 w-72 max-w-full" />
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-xl border border-border/70 bg-card/80 shadow-sm">
|
||||
{["one", "two", "three"].map((row) => (
|
||||
<div
|
||||
className="flex items-center gap-4 border-b border-border/60 px-4 py-3 last:border-b-0"
|
||||
key={row}
|
||||
>
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="ml-auto h-8 w-24 rounded-lg" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowsLoadingBody() {
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto p-4">
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-6 w-28" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-36 rounded-lg" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{["first", "second", "third", "fourth"].map((card) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-4 shadow-sm"
|
||||
key={card}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-5 w-44" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full max-w-2xl" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-5 w-20 rounded-full" />
|
||||
<Skeleton className="h-5 w-24 rounded-full" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden shrink-0 gap-2 sm:flex">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden w-[400px] shrink-0 border-l border-border/60 bg-background/70 lg:block">
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-40" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-4/5" />
|
||||
</div>
|
||||
<Skeleton className="h-40 w-full rounded-xl" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-16 w-full rounded-xl" />
|
||||
<Skeleton className="h-16 w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelLoadingBody() {
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4">
|
||||
<MessageRowsSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 bg-background px-4 py-4 sm:px-6">
|
||||
<div className="mx-auto w-full max-w-4xl space-y-3">
|
||||
<Skeleton className="h-10 w-full rounded-2xl" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-8 w-20 rounded-lg" />
|
||||
<Skeleton className="h-8 w-16 rounded-lg" />
|
||||
<Skeleton className="ml-auto h-8 w-24 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ForumLoadingBody() {
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="border-b border-border/60 p-4">
|
||||
<Skeleton className="h-10 w-full rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-3">
|
||||
{["first", "second", "third"].map((card) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-4 shadow-sm"
|
||||
key={card}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-14" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ViewLoadingFallback({
|
||||
includeHeader = false,
|
||||
kind,
|
||||
}: ViewLoadingFallbackProps) {
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{includeHeader ? <LoadingHeaderSkeleton /> : null}
|
||||
{kind === "agents" ? <AgentsLoadingBody /> : null}
|
||||
{kind === "workflows" ? <WorkflowsLoadingBody /> : null}
|
||||
{kind === "channel" ? <ChannelLoadingBody /> : null}
|
||||
{kind === "forum" ? <ForumLoadingBody /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
|
||||
import remarkChannelLinks from "@/shared/lib/remarkChannelLinks";
|
||||
import remarkMentions from "@/shared/lib/remarkMentions";
|
||||
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
|
||||
type MarkdownProps = {
|
||||
channelNames?: string[];
|
||||
@@ -205,11 +206,15 @@ function MarkdownInner({
|
||||
: compact
|
||||
? "compact"
|
||||
: "default";
|
||||
const { channels, onOpenChannel } = useChannelNavigation();
|
||||
const { channels } = useChannelNavigation();
|
||||
const { goChannel } = useAppNavigation();
|
||||
|
||||
const components = React.useMemo(
|
||||
() => createMarkdownComponents(variant, channels, onOpenChannel),
|
||||
[variant, channels, onOpenChannel],
|
||||
() =>
|
||||
createMarkdownComponents(variant, channels, (channelId) => {
|
||||
void goChannel(channelId);
|
||||
}),
|
||||
[goChannel, variant, channels],
|
||||
);
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: PluggableList type not directly importable
|
||||
|
||||
@@ -3260,6 +3260,16 @@ async function handleSearchMessages(
|
||||
created_at: now - 90 * 60,
|
||||
score: 5.8,
|
||||
},
|
||||
{
|
||||
event_id: "mock-forum-release-reply",
|
||||
content: "Looks good to me. We should ship it.",
|
||||
kind: 45003,
|
||||
pubkey: ALICE_PUBKEY,
|
||||
channel_id: "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11",
|
||||
channel_name: "watercooler",
|
||||
created_at: now - 80 * 60,
|
||||
score: 5.2,
|
||||
},
|
||||
];
|
||||
|
||||
const hits = mockHits
|
||||
@@ -3464,6 +3474,21 @@ async function handleGetEvent(
|
||||
content: "Release checklist: async feedback thread.",
|
||||
sig: "mocksig".repeat(20).slice(0, 128),
|
||||
},
|
||||
{
|
||||
id: "mock-forum-release-reply",
|
||||
pubkey: ALICE_PUBKEY,
|
||||
created_at: Math.floor(Date.now() / 1000) - 80 * 60,
|
||||
kind: 45003,
|
||||
tags: buildReplyMessageTags(
|
||||
"a27e1ee9-76a6-5bdf-a5d5-1d85610dad11",
|
||||
ALICE_PUBKEY,
|
||||
"mock-forum-release-thread",
|
||||
"mock-forum-release-thread",
|
||||
undefined,
|
||||
),
|
||||
content: "Looks good to me. We should ship it.",
|
||||
sig: "mocksig".repeat(20).slice(0, 128),
|
||||
},
|
||||
];
|
||||
const event = knownEvents.find((item) => item.id === args.eventId);
|
||||
if (!event) {
|
||||
|
||||
@@ -94,6 +94,7 @@ test("joining a channel from browser adds it to the sidebar", async ({
|
||||
|
||||
// Dialog should close and navigate to the joined channel
|
||||
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/#\/channels\//);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("design");
|
||||
|
||||
// Channel should now appear in the sidebar
|
||||
@@ -112,6 +113,7 @@ test("clicking a joined channel in browser navigates to it", async ({
|
||||
await page.getByTestId("browse-channel-general").click();
|
||||
|
||||
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/#\/channels\//);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
|
||||
const WATERCOLOR_CHANNEL_ID = "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11";
|
||||
const FORUM_POST_ID = "mock-forum-release-thread";
|
||||
const FORUM_REPLY_ID = "mock-forum-release-reply";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
});
|
||||
|
||||
async function navigateToWorkflows(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("open-workflows-view").click();
|
||||
await expect(page).toHaveURL(/#\/workflows$/);
|
||||
await expect(page.getByTestId("workflows-view")).toBeVisible();
|
||||
}
|
||||
|
||||
async function createWorkflow(
|
||||
page: import("@playwright/test").Page,
|
||||
name: string,
|
||||
) {
|
||||
await page.getByRole("button", { name: "Create Workflow" }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByLabel("Workflow name").fill(name);
|
||||
await dialog.getByRole("button", { name: "Add step" }).click();
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
|
||||
test("global back and forward move across channel routes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
await page.getByTestId("channel-random").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
||||
|
||||
await page.getByTestId("global-back").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
await page.getByTestId("global-forward").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
||||
});
|
||||
|
||||
test("direct forum thread links close back to the forum route", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(
|
||||
`/#/channels/${WATERCOLOR_CHANNEL_ID}/posts/${FORUM_POST_ID}`,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Back to posts" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Back to posts" }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
/#\/channels\/a27e1ee9-76a6-5bdf-a5d5-1d85610dad11$/,
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Release checklist: async feedback thread."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("direct workflow detail links close back to workflows", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workflowName = `workflow_nav_${Date.now()}`;
|
||||
|
||||
await navigateToWorkflows(page);
|
||||
await createWorkflow(page, workflowName);
|
||||
|
||||
const workflowCard = page
|
||||
.locator('[data-testid^="workflow-card-"]')
|
||||
.filter({ hasText: workflowName })
|
||||
.first();
|
||||
const workflowTestId = await workflowCard.getAttribute("data-testid");
|
||||
const workflowId = workflowTestId?.replace("workflow-card-", "");
|
||||
|
||||
expect(workflowId).toBeTruthy();
|
||||
|
||||
await page.goto(`/#/workflows/${workflowId}`);
|
||||
|
||||
await expect(page.getByTestId("workflow-detail-panel")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close detail panel" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/#\/workflows$/);
|
||||
await expect(page.getByTestId("workflows-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("forum reply deep links survive reload", async ({ page }) => {
|
||||
await page.goto(
|
||||
`/#/channels/${WATERCOLOR_CHANNEL_ID}/posts/${FORUM_POST_ID}?replyId=${FORUM_REPLY_ID}`,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
||||
await expect(
|
||||
page.getByText("Looks good to me. We should ship it."),
|
||||
).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
||||
await expect(
|
||||
page.getByText("Looks good to me. We should ship it."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("message deep links survive reload", async ({ page }) => {
|
||||
await page.goto(
|
||||
`/#/channels/${ENGINEERING_CHANNEL_ID}?messageId=mock-engineering-shipped`,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(
|
||||
"Engineering shipped the desktop build.",
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(
|
||||
"Engineering shipped the desktop build.",
|
||||
);
|
||||
});
|
||||
@@ -157,6 +157,9 @@ test("opens relay-backed search from the sidebar and loads the exact result", as
|
||||
.getByText("Engineering shipped the desktop build.")
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
/#\/channels\/1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9\?messageId=mock-engineering-shipped$/,
|
||||
);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(
|
||||
"Engineering shipped the desktop build.",
|
||||
|
||||
@@ -9,6 +9,7 @@ test.beforeEach(async ({ page }) => {
|
||||
async function navigateToWorkflows(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("open-workflows-view").click();
|
||||
await expect(page).toHaveURL(/#\/workflows$/);
|
||||
await expect(page.getByTestId("workflows-view")).toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
+15
-1
@@ -1,12 +1,26 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
routesDirectory: "./src/app/routes",
|
||||
generatedRouteTree: "./src/app/routeTree.gen.ts",
|
||||
virtualRouteConfig: "./src/app/routes.ts",
|
||||
quoteStyle: "double",
|
||||
semicolons: true,
|
||||
routeTreeFileHeader: [
|
||||
"// biome-ignore-all lint: generated by TanStack Router",
|
||||
],
|
||||
}),
|
||||
react(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
|
||||
Reference in New Issue
Block a user