mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Polish desktop dialogs, presence, and diff viewer (#44)
This commit is contained in:
@@ -19,7 +19,6 @@
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
@@ -30,10 +29,9 @@
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"diff2html": "^3.4.56",
|
||||
"dompurify": "^3.3.3",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.1.0",
|
||||
"react-diff-view": "^3.3.2",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
|
||||
Generated
+61
-115
@@ -8,9 +8,6 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -41,18 +38,15 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
diff2html:
|
||||
specifier: ^3.4.56
|
||||
version: 3.4.56
|
||||
dompurify:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@19.2.4)
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.4
|
||||
react-diff-view:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2(react@19.2.4)
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
@@ -441,16 +435,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@monaco-editor/loader@1.7.0':
|
||||
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
|
||||
|
||||
'@monaco-editor/react@4.7.0':
|
||||
resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
|
||||
peerDependencies:
|
||||
monaco-editor: '>= 0.25.0 < 1'
|
||||
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
|
||||
|
||||
'@noble/ciphers@2.1.1':
|
||||
resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
@@ -480,10 +464,6 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@profoundlogic/hogan@3.0.4':
|
||||
resolution: {integrity: sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==}
|
||||
hasBin: true
|
||||
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
@@ -861,6 +841,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==}
|
||||
@@ -871,6 +852,7 @@ 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==}
|
||||
@@ -888,6 +870,7 @@ packages:
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
@@ -904,17 +887,18 @@ packages:
|
||||
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==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
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==}
|
||||
@@ -926,7 +910,6 @@ packages:
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
|
||||
@@ -1090,9 +1073,6 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
@@ -1108,9 +1088,6 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
@@ -1182,6 +1159,9 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1229,23 +1209,12 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
diff2html@3.4.56:
|
||||
resolution: {integrity: sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
diff@8.0.3:
|
||||
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
dompurify@3.2.7:
|
||||
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
|
||||
|
||||
dompurify@3.3.3:
|
||||
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
|
||||
|
||||
electron-to-chromium@1.5.307:
|
||||
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
|
||||
|
||||
@@ -1312,6 +1281,9 @@ packages:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
gitdiff-parser@0.3.1:
|
||||
resolution: {integrity: sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1330,10 +1302,6 @@ packages:
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
@@ -1400,9 +1368,16 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
lodash@4.17.23:
|
||||
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
||||
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -1414,11 +1389,6 @@ packages:
|
||||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
marked@14.0.0:
|
||||
resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
|
||||
|
||||
@@ -1559,9 +1529,6 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1576,10 +1543,6 @@ packages:
|
||||
node-releases@2.0.36:
|
||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||
|
||||
nopt@1.0.10:
|
||||
resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==}
|
||||
hasBin: true
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1685,6 +1648,11 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
react-diff-view@3.3.2:
|
||||
resolution: {integrity: sha512-wPVq4ktTcGOHbhnWKU/gHLtd3N2Xd+OZ/XQWcKA06dsxlSsESePAumQILwHtiak2nMCMiWcIfBpqZ5OiharUPA==}
|
||||
peerDependencies:
|
||||
react: '>=16.14.0'
|
||||
|
||||
react-dom@19.2.4:
|
||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||
peerDependencies:
|
||||
@@ -1780,6 +1748,9 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
shallow-equal@3.1.0:
|
||||
resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1787,9 +1758,6 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
state-local@1.0.7:
|
||||
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
@@ -1946,6 +1914,9 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@@ -2222,17 +2193,6 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@monaco-editor/loader@1.7.0':
|
||||
dependencies:
|
||||
state-local: 1.0.7
|
||||
|
||||
'@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@monaco-editor/loader': 1.7.0
|
||||
monaco-editor: 0.55.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
'@noble/ciphers@2.1.1': {}
|
||||
|
||||
'@noble/curves@2.0.1':
|
||||
@@ -2257,10 +2217,6 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.58.2
|
||||
|
||||
'@profoundlogic/hogan@3.0.4':
|
||||
dependencies:
|
||||
nopt: 1.0.10
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
@@ -2776,9 +2732,6 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
optional: true
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@@ -2797,8 +2750,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -2869,6 +2820,8 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
@@ -2899,25 +2852,10 @@ snapshots:
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff2html@3.4.56:
|
||||
dependencies:
|
||||
'@profoundlogic/hogan': 3.0.4
|
||||
diff: 8.0.3
|
||||
optionalDependencies:
|
||||
highlight.js: 11.11.1
|
||||
|
||||
diff@8.0.3: {}
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
dompurify@3.2.7:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dompurify@3.3.3:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
electron-to-chromium@1.5.307: {}
|
||||
|
||||
esbuild@0.27.3:
|
||||
@@ -2991,6 +2929,8 @@ snapshots:
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
gitdiff-parser@0.3.1: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -3027,9 +2967,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
highlight.js@11.11.1:
|
||||
optional: true
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
@@ -3075,8 +3012,14 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
lodash@4.17.23: {}
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -3087,8 +3030,6 @@ snapshots:
|
||||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
marked@14.0.0: {}
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -3445,11 +3386,6 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.2.7
|
||||
marked: 14.0.0
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mz@2.7.0:
|
||||
@@ -3462,10 +3398,6 @@ snapshots:
|
||||
|
||||
node-releases@2.0.36: {}
|
||||
|
||||
nopt@1.0.10:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
nostr-tools@2.23.3(typescript@5.8.3):
|
||||
@@ -3557,6 +3489,16 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
react-diff-view@3.3.2(react@19.2.4):
|
||||
dependencies:
|
||||
classnames: 2.5.1
|
||||
diff-match-patch: 1.0.5
|
||||
gitdiff-parser: 0.3.1
|
||||
lodash: 4.17.23
|
||||
react: 19.2.4
|
||||
shallow-equal: 3.1.0
|
||||
warning: 4.0.3
|
||||
|
||||
react-dom@19.2.4(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -3706,12 +3648,12 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
shallow-equal@3.1.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
state-local@1.0.7: {}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
@@ -3876,6 +3818,10 @@ snapshots:
|
||||
jiti: 1.21.7
|
||||
yaml: 2.8.2
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
@@ -306,7 +306,7 @@ export function AppShell() {
|
||||
|
||||
return (
|
||||
<SidebarProvider className="h-dvh overflow-hidden overscroll-none">
|
||||
<SidebarTrigger className="fixed left-[80px] top-[9px] z-50 h-6 w-6" />
|
||||
<SidebarTrigger className="fixed left-[80px] top-[9px] z-50 h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
|
||||
<AppSidebar
|
||||
channels={memberChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
const BROWSE_CHANNELS_SHORTCUT_KEY = "o";
|
||||
const BROWSE_CHANNELS_SHORTCUT_HINT = "\u21E7\u2318O";
|
||||
|
||||
function formatRelativeTime(isoString: string | null) {
|
||||
if (!isoString) {
|
||||
return "No activity";
|
||||
@@ -128,7 +131,7 @@ export function ChannelBrowserDialog({
|
||||
React.useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (
|
||||
event.key.toLowerCase() !== "b" ||
|
||||
event.key.toLowerCase() !== BROWSE_CHANNELS_SHORTCUT_KEY ||
|
||||
!(event.metaKey || event.ctrlKey) ||
|
||||
event.altKey ||
|
||||
!event.shiftKey
|
||||
@@ -250,9 +253,9 @@ export function ChannelBrowserDialog({
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
/>
|
||||
<div className="hidden items-center gap-1 rounded-lg border border-border bg-background px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground sm:flex">
|
||||
⇧⌘B
|
||||
</div>
|
||||
<span className="hidden shrink-0 text-xs text-muted-foreground/50 sm:block">
|
||||
{BROWSE_CHANNELS_SHORTCUT_HINT}
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ChatHeader({
|
||||
}: ChatHeaderProps) {
|
||||
return (
|
||||
<header
|
||||
className="flex min-w-0 items-center gap-3 border-b border-border/80 bg-background px-4 py-3 sm:px-6"
|
||||
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-testid="chat-header"
|
||||
onPointerDown={handlePointerDown}
|
||||
>
|
||||
|
||||
@@ -1,52 +1,74 @@
|
||||
import { parse } from "diff2html";
|
||||
import {
|
||||
isDelete,
|
||||
isInsert,
|
||||
parseDiff,
|
||||
type DiffType,
|
||||
type FileData,
|
||||
} from "react-diff-view";
|
||||
|
||||
type ParsedDiffResult = {
|
||||
files: FileData[];
|
||||
parseError: boolean;
|
||||
};
|
||||
|
||||
function isRenderableFile(file: FileData) {
|
||||
return file.hunks.length > 0 || Boolean(file.oldPath || file.newPath);
|
||||
}
|
||||
|
||||
export function parseUnifiedDiff(content: string): ParsedDiffResult {
|
||||
if (!content.trim()) {
|
||||
return { files: [], parseError: false };
|
||||
}
|
||||
|
||||
export function parseDiffToOldNew(unifiedDiff: string): {
|
||||
original: string;
|
||||
modified: string;
|
||||
} {
|
||||
try {
|
||||
const files = parse(unifiedDiff);
|
||||
const files = parseDiff(content).filter(isRenderableFile);
|
||||
|
||||
if (!files.length) {
|
||||
// diff2html couldn't parse any files — show raw diff as fallback
|
||||
return { original: "", modified: unifiedDiff };
|
||||
return { files: [], parseError: true };
|
||||
}
|
||||
|
||||
const originalLines: string[] = [];
|
||||
const modifiedLines: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
// Add file header separator for multi-file diffs
|
||||
if (files.length > 1) {
|
||||
const header = `// ── ${file.newName || file.oldName || "unknown"} ──`;
|
||||
originalLines.push(header);
|
||||
modifiedLines.push(header);
|
||||
}
|
||||
|
||||
for (const block of file.blocks) {
|
||||
for (const line of block.lines) {
|
||||
if (line.content.startsWith("\\ ")) continue;
|
||||
if (line.type === "context" || line.type === "delete") {
|
||||
originalLines.push(line.content.slice(1));
|
||||
}
|
||||
if (line.type === "context" || line.type === "insert") {
|
||||
modifiedLines.push(line.content.slice(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add blank line between files
|
||||
if (files.length > 1) {
|
||||
originalLines.push("");
|
||||
modifiedLines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
original: originalLines.join("\n"),
|
||||
modified: modifiedLines.join("\n"),
|
||||
};
|
||||
return { files, parseError: false };
|
||||
} catch {
|
||||
// Malformed diff — return raw content as fallback
|
||||
return { original: "", modified: unifiedDiff };
|
||||
return { files: [], parseError: true };
|
||||
}
|
||||
}
|
||||
|
||||
export function getDiffFileLabel(
|
||||
file: FileData,
|
||||
fallbackFilePath?: string,
|
||||
): string {
|
||||
if (file.oldPath && file.newPath && file.oldPath !== file.newPath) {
|
||||
return `${file.oldPath} -> ${file.newPath}`;
|
||||
}
|
||||
|
||||
return file.newPath || file.oldPath || fallbackFilePath || "diff";
|
||||
}
|
||||
|
||||
export function countDiffFileChanges(file: FileData) {
|
||||
let additions = 0;
|
||||
let deletions = 0;
|
||||
|
||||
for (const hunk of file.hunks) {
|
||||
for (const change of hunk.changes) {
|
||||
if (isInsert(change)) {
|
||||
additions += 1;
|
||||
} else if (isDelete(change)) {
|
||||
deletions += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { additions, deletions };
|
||||
}
|
||||
|
||||
export function normalizeDiffType(type: string | undefined): DiffType {
|
||||
switch (type) {
|
||||
case "add":
|
||||
case "copy":
|
||||
case "delete":
|
||||
case "rename":
|
||||
return type;
|
||||
default:
|
||||
return "modify";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,7 @@
|
||||
import { html } from "diff2html";
|
||||
import "diff2html/bundles/css/diff2html.min.css";
|
||||
import DOMPurify from "dompurify";
|
||||
import { FileDiff, Maximize2 } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
/**
|
||||
* Override diff2html styles that break in constrained containers.
|
||||
* The default CSS uses position:absolute for line numbers which causes
|
||||
* overflow and misalignment in narrow message bubbles.
|
||||
*/
|
||||
const diffStyleOverrides = `
|
||||
.d2h-wrapper { text-align: left; }
|
||||
.d2h-file-wrapper { border: none; margin: 0; border-radius: 0; }
|
||||
.d2h-file-header { display: none; }
|
||||
.d2h-diff-table { font-size: 12px; width: 100%; table-layout: fixed; }
|
||||
.d2h-code-linenumber {
|
||||
position: static;
|
||||
display: table-cell;
|
||||
width: 3em;
|
||||
min-width: 3em;
|
||||
max-width: 3em;
|
||||
padding: 0 0.4em;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
user-select: none;
|
||||
border-right: 1px solid var(--d2h-line-border-color, #eee);
|
||||
font-size: 11px;
|
||||
}
|
||||
.d2h-code-line {
|
||||
display: table-cell;
|
||||
padding: 0 0.5em;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
width: auto;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.d2h-info {
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
`;
|
||||
import { DiffViewer } from "./DiffViewer";
|
||||
|
||||
type DiffMessageProps = {
|
||||
content: string;
|
||||
@@ -73,37 +31,6 @@ function getHostname(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
const ALLOWED_TAGS = [
|
||||
"div",
|
||||
"span",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"pre",
|
||||
"code",
|
||||
"ins",
|
||||
"del",
|
||||
"a",
|
||||
"i",
|
||||
"em",
|
||||
"strong",
|
||||
"small",
|
||||
];
|
||||
|
||||
const ALLOWED_ATTR = [
|
||||
"class",
|
||||
"id",
|
||||
"data-line-number",
|
||||
"href",
|
||||
"title",
|
||||
"aria-label",
|
||||
];
|
||||
|
||||
const ALLOWED_URI_REGEXP = /^https?:\/\//i;
|
||||
|
||||
export function DiffMessage({
|
||||
content,
|
||||
repoUrl,
|
||||
@@ -113,24 +40,6 @@ export function DiffMessage({
|
||||
truncated,
|
||||
onExpand,
|
||||
}: DiffMessageProps) {
|
||||
const { diffHtml, renderError } = useMemo(() => {
|
||||
try {
|
||||
const rawHtml = html(content, {
|
||||
drawFileList: false,
|
||||
matching: "lines",
|
||||
outputFormat: "line-by-line",
|
||||
});
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS,
|
||||
ALLOWED_ATTR,
|
||||
ALLOWED_URI_REGEXP,
|
||||
});
|
||||
return { diffHtml: sanitized, renderError: false };
|
||||
} catch {
|
||||
return { diffHtml: "", renderError: true };
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const safeRepoUrl = isSafeUrl(repoUrl) ? repoUrl : undefined;
|
||||
|
||||
const commitUrl =
|
||||
@@ -197,20 +106,12 @@ export function DiffMessage({
|
||||
|
||||
{/* Diff content — max 400px height, scrollable */}
|
||||
<div className="max-h-[400px] overflow-auto text-xs">
|
||||
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: static CSS overrides */}
|
||||
<style dangerouslySetInnerHTML={{ __html: diffStyleOverrides }} />
|
||||
{renderError ? (
|
||||
<pre className="p-3 whitespace-pre-wrap font-mono text-muted-foreground">
|
||||
{content}
|
||||
</pre>
|
||||
) : diffHtml ? (
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: sanitized by DOMPurify
|
||||
<div dangerouslySetInnerHTML={{ __html: diffHtml }} />
|
||||
) : (
|
||||
<div className="p-3 text-xs text-muted-foreground italic">
|
||||
No diff content
|
||||
</div>
|
||||
)}
|
||||
<DiffViewer
|
||||
className="p-3"
|
||||
content={content}
|
||||
fallbackFilePath={filePath}
|
||||
viewType="unified"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Truncation warning */}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import { useMemo } from "react";
|
||||
import { Rows3, SplitSquareVertical } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { parseDiffToOldNew } from "@/features/messages/lib/parseDiff";
|
||||
import { DiffViewer } from "@/features/messages/ui/DiffViewer";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,21 +12,16 @@ import {
|
||||
|
||||
type DiffMessageExpandedProps = {
|
||||
content: string;
|
||||
language?: string;
|
||||
filePath?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function DiffMessageExpanded({
|
||||
content,
|
||||
language,
|
||||
filePath,
|
||||
onClose,
|
||||
}: DiffMessageExpandedProps) {
|
||||
const { original, modified } = useMemo(
|
||||
() => parseDiffToOldNew(content),
|
||||
[content],
|
||||
);
|
||||
const [viewType, setViewType] = useState<"split" | "unified">("unified");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -35,28 +31,44 @@ export default function DiffMessageExpanded({
|
||||
open
|
||||
>
|
||||
<DialogContent className="max-w-5xl w-full h-[80vh] flex flex-col p-0 gap-0">
|
||||
<DialogHeader className="px-4 py-3 border-b border-border/50 shrink-0">
|
||||
<DialogTitle className="text-sm font-mono font-medium truncate">
|
||||
{filePath ?? "Diff Viewer"}{" "}
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
(hunks-only preview — shows changed regions, not full file)
|
||||
</span>
|
||||
</DialogTitle>
|
||||
<DialogHeader className="shrink-0 border-b border-border/50 px-4 py-3 pr-14">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DialogTitle className="min-w-0 flex-1 truncate font-mono text-sm font-medium">
|
||||
{filePath ?? "Diff Viewer"}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-1 rounded-lg border border-border/60 bg-muted/30 p-1">
|
||||
<Button
|
||||
className="h-7 px-2"
|
||||
onClick={() => {
|
||||
setViewType("unified");
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={viewType === "unified" ? "secondary" : "ghost"}
|
||||
>
|
||||
<Rows3 className="h-3.5 w-3.5" />
|
||||
Unified
|
||||
</Button>
|
||||
<Button
|
||||
className="h-7 px-2"
|
||||
onClick={() => {
|
||||
setViewType("split");
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={viewType === "split" ? "secondary" : "ghost"}
|
||||
>
|
||||
<SplitSquareVertical className="h-3.5 w-3.5" />
|
||||
Split
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 min-h-0">
|
||||
<DiffEditor
|
||||
height="100%"
|
||||
language={language ?? "plaintext"}
|
||||
modified={modified}
|
||||
options={{
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 13,
|
||||
}}
|
||||
original={original}
|
||||
theme="vs-dark"
|
||||
<div className="min-h-0 flex-1 overflow-auto px-4 py-4">
|
||||
<DiffViewer
|
||||
content={content}
|
||||
fallbackFilePath={filePath}
|
||||
viewType={viewType}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
:root {
|
||||
--sprout-diff-insert-gutter-bg: 143 58% 91%;
|
||||
--sprout-diff-insert-code-bg: 140 56% 96%;
|
||||
--sprout-diff-insert-edit-bg: 142 44% 82%;
|
||||
--sprout-diff-insert-text: 143 63% 20%;
|
||||
--sprout-diff-delete-gutter-bg: 352 73% 93%;
|
||||
--sprout-diff-delete-code-bg: 356 87% 97%;
|
||||
--sprout-diff-delete-edit-bg: 353 64% 84%;
|
||||
--sprout-diff-delete-text: 356 55% 24%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--sprout-diff-insert-gutter-bg: 143 33% 20%;
|
||||
--sprout-diff-insert-code-bg: 143 29% 15%;
|
||||
--sprout-diff-insert-edit-bg: 143 30% 27%;
|
||||
--sprout-diff-insert-text: 143 47% 78%;
|
||||
--sprout-diff-delete-gutter-bg: 353 28% 20%;
|
||||
--sprout-diff-delete-code-bg: 353 25% 15%;
|
||||
--sprout-diff-delete-edit-bg: 353 31% 27%;
|
||||
--sprout-diff-delete-text: 352 71% 83%;
|
||||
}
|
||||
|
||||
.sprout-diff-theme {
|
||||
--diff-background-color: transparent;
|
||||
--diff-text-color: hsl(var(--foreground));
|
||||
--diff-font-family:
|
||||
"SFMono-Regular", "Geist Mono", ui-monospace, "Cascadia Code",
|
||||
"Source Code Pro", Menlo, Consolas, monospace;
|
||||
--diff-selection-background-color: hsl(var(--accent) / 0.9);
|
||||
--diff-selection-text-color: hsl(var(--accent-foreground));
|
||||
--diff-gutter-insert-background-color: hsl(
|
||||
var(--sprout-diff-insert-gutter-bg)
|
||||
);
|
||||
--diff-gutter-insert-text-color: hsl(var(--sprout-diff-insert-text));
|
||||
--diff-gutter-delete-background-color: hsl(
|
||||
var(--sprout-diff-delete-gutter-bg)
|
||||
);
|
||||
--diff-gutter-delete-text-color: hsl(var(--sprout-diff-delete-text));
|
||||
--diff-gutter-selected-background-color: hsl(var(--accent) / 0.45);
|
||||
--diff-gutter-selected-text-color: hsl(var(--foreground));
|
||||
--diff-code-insert-background-color: hsl(var(--sprout-diff-insert-code-bg));
|
||||
--diff-code-insert-text-color: hsl(var(--sprout-diff-insert-text));
|
||||
--diff-code-delete-background-color: hsl(var(--sprout-diff-delete-code-bg));
|
||||
--diff-code-delete-text-color: hsl(var(--sprout-diff-delete-text));
|
||||
--diff-code-insert-edit-background-color: hsl(
|
||||
var(--sprout-diff-insert-edit-bg)
|
||||
);
|
||||
--diff-code-insert-edit-text-color: hsl(var(--sprout-diff-insert-text));
|
||||
--diff-code-delete-edit-background-color: hsl(
|
||||
var(--sprout-diff-delete-edit-bg)
|
||||
);
|
||||
--diff-code-delete-edit-text-color: hsl(var(--sprout-diff-delete-text));
|
||||
--diff-code-selected-background-color: hsl(var(--accent) / 0.35);
|
||||
--diff-code-selected-text-color: hsl(var(--foreground));
|
||||
--diff-omit-gutter-line-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
.sprout-diff-theme .diff {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .diff td {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .sprout-diff-line {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .sprout-diff-gutter {
|
||||
min-width: 3.5rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-right: 1px solid hsl(var(--border) / 0.65);
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .sprout-diff-code {
|
||||
padding: 0.125rem 0.75rem 0.125rem 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .diff-code-edit {
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .diff-decoration-content {
|
||||
padding: 0.2rem 0.75rem;
|
||||
background: hsl(var(--muted) / 0.35);
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sprout-diff-theme .diff-gutter-omit::before {
|
||||
margin-left: 2.65rem;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Diff, Hunk, type ViewType } from "react-diff-view";
|
||||
import "react-diff-view/style/index.css";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
countDiffFileChanges,
|
||||
getDiffFileLabel,
|
||||
normalizeDiffType,
|
||||
parseUnifiedDiff,
|
||||
} from "@/features/messages/lib/parseDiff";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import "./DiffViewer.css";
|
||||
|
||||
type DiffViewerProps = {
|
||||
content: string;
|
||||
fallbackFilePath?: string;
|
||||
viewType?: ViewType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const DIFF_TYPE_LABELS = {
|
||||
add: "New file",
|
||||
copy: "Copied",
|
||||
delete: "Deleted",
|
||||
modify: "Modified",
|
||||
rename: "Renamed",
|
||||
} as const;
|
||||
|
||||
function FileChangeBadge({
|
||||
tone,
|
||||
value,
|
||||
}: {
|
||||
tone: "positive" | "negative";
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md px-1.5 py-0.5 font-mono text-[10px] font-semibold",
|
||||
tone === "positive"
|
||||
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
||||
: "bg-rose-500/10 text-rose-700 dark:text-rose-300",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function DiffViewer({
|
||||
content,
|
||||
fallbackFilePath,
|
||||
viewType = "unified",
|
||||
className,
|
||||
}: DiffViewerProps) {
|
||||
const { files, parseError } = useMemo(
|
||||
() => parseUnifiedDiff(content),
|
||||
[content],
|
||||
);
|
||||
|
||||
if (parseError) {
|
||||
return (
|
||||
<pre className="p-3 whitespace-pre-wrap font-mono text-xs text-muted-foreground">
|
||||
{content}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
if (!files.length) {
|
||||
return (
|
||||
<div className="p-3 text-xs italic text-muted-foreground">
|
||||
No diff content
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("sprout-diff-theme", className)}>
|
||||
<div className="space-y-3">
|
||||
{files.map((file) => {
|
||||
const label = getDiffFileLabel(file, fallbackFilePath);
|
||||
const showFileHeader =
|
||||
files.length > 1 || !fallbackFilePath || label !== fallbackFilePath;
|
||||
const { additions, deletions } = countDiffFileChanges(file);
|
||||
const diffType = normalizeDiffType(file.type);
|
||||
const fileKey = [
|
||||
file.oldPath || "",
|
||||
file.newPath || "",
|
||||
file.oldRevision || "",
|
||||
file.newRevision || "",
|
||||
].join(":");
|
||||
|
||||
return (
|
||||
<section
|
||||
className="overflow-hidden rounded-xl border border-border/60 bg-background/40"
|
||||
key={fileKey || label}
|
||||
>
|
||||
{showFileHeader ? (
|
||||
<div className="flex items-center gap-2 border-b border-border/60 bg-muted/35 px-3 py-2">
|
||||
<span className="truncate font-mono text-[11px] text-foreground/85">
|
||||
{label}
|
||||
</span>
|
||||
<span className="rounded-md border border-border/60 px-1.5 py-0.5 text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{DIFF_TYPE_LABELS[diffType]}
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
{additions > 0 ? (
|
||||
<FileChangeBadge
|
||||
tone="positive"
|
||||
value={`+${additions}`}
|
||||
/>
|
||||
) : null}
|
||||
{deletions > 0 ? (
|
||||
<FileChangeBadge
|
||||
tone="negative"
|
||||
value={`-${deletions}`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{file.hunks.length > 0 ? (
|
||||
<Diff
|
||||
className={cn(
|
||||
"sprout-diff-table",
|
||||
viewType === "split" ? "min-w-[780px]" : "w-full",
|
||||
)}
|
||||
codeClassName="sprout-diff-code"
|
||||
diffType={diffType}
|
||||
gutterClassName="sprout-diff-gutter"
|
||||
hunks={file.hunks}
|
||||
lineClassName="sprout-diff-line"
|
||||
viewType={viewType}
|
||||
>
|
||||
{(hunks) =>
|
||||
hunks.map((hunk) => (
|
||||
<Hunk
|
||||
hunk={hunk}
|
||||
key={`${fileKey}:${hunk.oldStart}:${hunk.newStart}:${hunk.content}`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Diff>
|
||||
) : (
|
||||
<div className="px-3 py-3 text-xs text-muted-foreground">
|
||||
No textual hunks in this diff.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ function MessageRow({ message }: { message: TimelineMessage }) {
|
||||
{message.avatarUrl && !hasAvatarError ? (
|
||||
<img
|
||||
alt={`${message.author} avatar`}
|
||||
className="h-9 w-9 shrink-0 rounded-xl border border-border/80 object-cover shadow-sm"
|
||||
className="h-9 w-9 shrink-0 rounded-xl object-cover shadow-sm"
|
||||
data-testid="message-avatar-image"
|
||||
onError={() => {
|
||||
setHasAvatarError(true);
|
||||
@@ -131,7 +131,6 @@ function MessageRow({ message }: { message: TimelineMessage }) {
|
||||
<DiffMessageExpanded
|
||||
content={message.body}
|
||||
filePath={getTag("file")}
|
||||
language={getTag("l")}
|
||||
onClose={() => {
|
||||
setExpandedDiffId(null);
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,12 @@ import { getPresence, setPresence } from "@/shared/api/tauri";
|
||||
import type { PresenceLookup, PresenceStatus } from "@/shared/api/types";
|
||||
|
||||
const PRESENCE_HEARTBEAT_INTERVAL_MS = 60_000;
|
||||
const PRESENCE_IDLE_TIMEOUT_MS = 5 * 60_000;
|
||||
const PRESENCE_STATUS_TICK_INTERVAL_MS = 30_000;
|
||||
const PRESENCE_TTL_SECONDS = 90;
|
||||
const PRESENCE_PREFERENCE_STORAGE_KEY = "sprout-presence-preference";
|
||||
|
||||
type PresencePreference = "auto" | "away" | "offline" | null;
|
||||
|
||||
function normalizePubkeys(pubkeys: string[]) {
|
||||
return [...new Set(pubkeys.map((pubkey) => pubkey.trim().toLowerCase()))]
|
||||
@@ -18,6 +23,51 @@ function presenceQueryKey(pubkeys: string[]) {
|
||||
return ["presence", ...normalizePubkeys(pubkeys)] as const;
|
||||
}
|
||||
|
||||
function presencePreferenceStorageKey(pubkey: string) {
|
||||
return `${PRESENCE_PREFERENCE_STORAGE_KEY}:${pubkey}`;
|
||||
}
|
||||
|
||||
function readStoredPresencePreference(pubkey: string): PresencePreference {
|
||||
if (typeof window === "undefined" || pubkey.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = window.localStorage.getItem(
|
||||
presencePreferenceStorageKey(pubkey),
|
||||
);
|
||||
return value === "auto" || value === "away" || value === "offline"
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function writeStoredPresencePreference(
|
||||
pubkey: string,
|
||||
preference: PresencePreference,
|
||||
) {
|
||||
if (typeof window === "undefined" || pubkey.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preference === null) {
|
||||
window.localStorage.removeItem(presencePreferenceStorageKey(pubkey));
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(presencePreferenceStorageKey(pubkey), preference);
|
||||
}
|
||||
|
||||
function resolveAutomaticPresenceStatus(
|
||||
isDocumentHidden: boolean,
|
||||
lastActivityAt: number,
|
||||
now: number,
|
||||
): PresenceStatus {
|
||||
if (isDocumentHidden) {
|
||||
return "away";
|
||||
}
|
||||
|
||||
return now - lastActivityAt >= PRESENCE_IDLE_TIMEOUT_MS ? "away" : "online";
|
||||
}
|
||||
|
||||
export function usePresenceQuery(
|
||||
pubkeys: string[],
|
||||
options?: {
|
||||
@@ -87,65 +137,180 @@ export function usePresenceSession(pubkey?: string) {
|
||||
{ enabled: normalizedPubkey.length > 0 },
|
||||
);
|
||||
const setPresenceMutation = useSetPresenceMutation(normalizedPubkey);
|
||||
const [desiredStatus, setDesiredStatus] =
|
||||
React.useState<PresenceStatus | null>(null);
|
||||
const previousPubkeyRef = React.useRef(normalizedPubkey);
|
||||
const [presencePreference, setPresencePreference] =
|
||||
React.useState<PresencePreference>(() =>
|
||||
readStoredPresencePreference(normalizedPubkey),
|
||||
);
|
||||
const [lastActivityAt, setLastActivityAt] = React.useState(() => Date.now());
|
||||
const [statusClock, setStatusClock] = React.useState(() => Date.now());
|
||||
const [isDocumentHidden, setIsDocumentHidden] = React.useState(() =>
|
||||
typeof document === "undefined" ? false : document.hidden,
|
||||
);
|
||||
const skipNextSyncRef = React.useRef<PresenceStatus | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (previousPubkeyRef.current === normalizedPubkey) {
|
||||
const now = Date.now();
|
||||
setPresencePreference(readStoredPresencePreference(normalizedPubkey));
|
||||
setLastActivityAt(now);
|
||||
setStatusClock(now);
|
||||
setIsDocumentHidden(
|
||||
typeof document === "undefined" ? false : document.hidden,
|
||||
);
|
||||
}, [normalizedPubkey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
writeStoredPresencePreference(normalizedPubkey, presencePreference);
|
||||
}, [normalizedPubkey, presencePreference]);
|
||||
|
||||
const recordActivity = React.useEffectEvent(() => {
|
||||
const now = Date.now();
|
||||
setLastActivityAt(now);
|
||||
setStatusClock(now);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (normalizedPubkey.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousPubkeyRef.current = normalizedPubkey;
|
||||
setDesiredStatus(null);
|
||||
});
|
||||
function handleUserActivity() {
|
||||
if (typeof document !== "undefined" && document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordActivity();
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
setIsDocumentHidden(false);
|
||||
recordActivity();
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
const hidden = document.hidden;
|
||||
setIsDocumentHidden(hidden);
|
||||
|
||||
if (!hidden) {
|
||||
recordActivity();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("pointerdown", handleUserActivity, true);
|
||||
window.addEventListener("keydown", handleUserActivity, true);
|
||||
window.addEventListener("focus", handleFocus);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerdown", handleUserActivity, true);
|
||||
window.removeEventListener("keydown", handleUserActivity, true);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [normalizedPubkey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (normalizedPubkey.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
setStatusClock(Date.now());
|
||||
}, PRESENCE_STATUS_TICK_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(intervalId);
|
||||
};
|
||||
}, [normalizedPubkey]);
|
||||
|
||||
const automaticStatus = React.useMemo(
|
||||
() =>
|
||||
resolveAutomaticPresenceStatus(
|
||||
isDocumentHidden,
|
||||
lastActivityAt,
|
||||
statusClock,
|
||||
),
|
||||
[isDocumentHidden, lastActivityAt, statusClock],
|
||||
);
|
||||
const relayStatus =
|
||||
normalizedPubkey.length > 0
|
||||
? (presenceQuery.data?.[normalizedPubkey] ?? "offline")
|
||||
: "offline";
|
||||
const currentStatus = desiredStatus ?? relayStatus;
|
||||
const currentStatus =
|
||||
normalizedPubkey.length === 0
|
||||
? "offline"
|
||||
: presencePreference === "offline"
|
||||
? "offline"
|
||||
: presencePreference === "away"
|
||||
? "away"
|
||||
: presencePreference === "auto"
|
||||
? automaticStatus
|
||||
: relayStatus;
|
||||
|
||||
const updatePresence = React.useCallback(
|
||||
async (status: PresenceStatus) => {
|
||||
const previousStatus = currentStatus;
|
||||
setDesiredStatus(status);
|
||||
const previousPreference = presencePreference;
|
||||
const nextPreference: PresencePreference =
|
||||
status === "online" ? "auto" : status;
|
||||
|
||||
if (nextPreference === "auto") {
|
||||
const now = Date.now();
|
||||
setLastActivityAt(now);
|
||||
setStatusClock(now);
|
||||
setIsDocumentHidden(
|
||||
typeof document === "undefined" ? false : document.hidden,
|
||||
);
|
||||
}
|
||||
|
||||
setPresencePreference(nextPreference);
|
||||
skipNextSyncRef.current = status;
|
||||
|
||||
try {
|
||||
await setPresenceMutation.mutateAsync(status);
|
||||
} catch (error) {
|
||||
setDesiredStatus(
|
||||
previousStatus === relayStatus ? null : previousStatus,
|
||||
);
|
||||
skipNextSyncRef.current = null;
|
||||
setPresencePreference(previousPreference);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[currentStatus, relayStatus, setPresenceMutation],
|
||||
[presencePreference, setPresenceMutation],
|
||||
);
|
||||
|
||||
const sendHeartbeat = React.useEffectEvent((status: PresenceStatus) => {
|
||||
const syncPresence = React.useEffectEvent((status: PresenceStatus) => {
|
||||
void setPresenceMutation.mutateAsync(status).catch(() => {
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (normalizedPubkey.length === 0 || presencePreference === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipNextSyncRef.current === currentStatus) {
|
||||
skipNextSyncRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
syncPresence(currentStatus);
|
||||
}, [currentStatus, normalizedPubkey, presencePreference]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
normalizedPubkey.length === 0 ||
|
||||
desiredStatus === null ||
|
||||
desiredStatus === "offline"
|
||||
presencePreference === null ||
|
||||
currentStatus === "offline"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
sendHeartbeat(desiredStatus);
|
||||
syncPresence(currentStatus);
|
||||
}, PRESENCE_HEARTBEAT_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(intervalId);
|
||||
};
|
||||
}, [desiredStatus, normalizedPubkey]);
|
||||
}, [currentStatus, normalizedPubkey, presencePreference]);
|
||||
|
||||
return {
|
||||
currentStatus,
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ProfileAvatar({
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
const baseClassName = cn(
|
||||
"flex shrink-0 items-center justify-center overflow-hidden border border-border/80 bg-primary/10 text-primary shadow-sm",
|
||||
"flex shrink-0 items-center justify-center overflow-hidden bg-primary/10 text-primary shadow-sm",
|
||||
className,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Command,
|
||||
FileText,
|
||||
Hash,
|
||||
LoaderCircle,
|
||||
@@ -270,9 +269,9 @@ export function SearchDialog({
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
/>
|
||||
<div className="hidden items-center gap-1 rounded-lg border border-border bg-background px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground sm:flex">
|
||||
<Command className="h-3 w-3" />K
|
||||
</div>
|
||||
<span className="hidden shrink-0 text-xs text-muted-foreground/50 sm:block">
|
||||
⌘K
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -71,17 +71,19 @@ const presenceOptions: Array<{
|
||||
{
|
||||
value: "online",
|
||||
label: "Online",
|
||||
description: "Visible as active and refreshed while the app is open.",
|
||||
description:
|
||||
"Automatically active while you use the app and away when idle.",
|
||||
},
|
||||
{
|
||||
value: "away",
|
||||
label: "Away",
|
||||
description: "Visible as idle and refreshed while the app is open.",
|
||||
description:
|
||||
"Forces this desktop session to appear idle until you change it.",
|
||||
},
|
||||
{
|
||||
value: "offline",
|
||||
label: "Offline",
|
||||
description: "Clears your relay presence entry immediately.",
|
||||
description: "Hides this desktop session and stops presence heartbeats.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -254,8 +256,9 @@ function PresenceSettingsCard({
|
||||
className="mt-4 text-sm text-muted-foreground"
|
||||
data-testid="presence-help"
|
||||
>
|
||||
Online and away are refreshed automatically every minute while Sprout is
|
||||
running. The relay expires presence after 90 seconds.
|
||||
Sprout refreshes presence every minute while it is running. Online will
|
||||
switch to away after a few minutes of inactivity or when the app is
|
||||
hidden. The relay expires presence after 90 seconds.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -408,7 +408,7 @@ export function AppSidebar({
|
||||
variant="sidebar"
|
||||
>
|
||||
<SidebarHeader
|
||||
className="gap-3 pt-12"
|
||||
className="gap-3 pt-10"
|
||||
onPointerDown={handleDragPointerDown}
|
||||
>
|
||||
<Button
|
||||
@@ -497,7 +497,7 @@ export function AppSidebar({
|
||||
selectedChannelId={selectedChannelId}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
/>
|
||||
<SidebarMenu>
|
||||
<SidebarMenu className="px-2">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="text-sidebar-foreground/60 hover:text-sidebar-foreground"
|
||||
|
||||
@@ -32,20 +32,22 @@ const DialogContent = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"fixed left-1/2 top-[12vh] z-50 grid w-[calc(100vw-2rem)] max-w-2xl -translate-x-1/2 gap-4 rounded-3xl border border-border bg-background p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-1 focus:ring-ring">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
<div className="fixed inset-0 z-50 grid place-items-center overflow-y-auto p-4 pointer-events-none">
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"pointer-events-auto relative grid w-[calc(100vw-2rem)] max-w-2xl gap-4 rounded-3xl border border-border bg-background p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-1 focus:ring-ring">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</div>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
@@ -20,7 +20,9 @@ test("keyboard shortcut opens the channel browser dialog", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Meta+Shift+B");
|
||||
await page.keyboard.press(
|
||||
process.platform === "darwin" ? "Meta+Shift+O" : "Control+Shift+O",
|
||||
);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user