[client] Fix the Files tab layout and add sending from the Files page

The Files rows overflowed the fixed-width panel instead of truncating.
Radix wraps ScrollArea.Viewport children in a div carrying an inline
`min-width:100%; display:table`, and a table box is shrink-to-fit, so it
grew to the widest row's intrinsic width and never gave `truncate` a
finite bound. Since overflowX is hidden when only the vertical scrollbar
is enabled, the result was clipped content rather than a scrollbar. A
scoped `.nb-scroll-clamp` rule overrides that display (the inline style
needs `!important`), leaving other ScrollAreas untouched.

Each transfer row is now three lines -- file name, to/from peer, then
time and size -- with the file name and the peer label both truncating
and revealing the full text on hover via TruncatedText. The status label
keeps its own right-hand column, vertically centred against the block,
so a refusal or failure stays scannable.

The Files page could not start a transfer at all: the send buttons lived
only on the peer detail panel, and an empty history replaced the whole
page, hiding the header. The header now always renders and carries one
primary Send button opening a two-step picker -- payload first, then the
peer list with search. One entry point means the peer list is built in
one place instead of two, and one button leaves the search field room to
survive the longer translations.

Clipboard sends showed nothing before leaving the machine. Both the
picker and the peer panel now preview the text first, and both send the
previewed string rather than re-reading the clipboard, which could have
changed between the preview and the confirmation. The picker disables
its clipboard entry when the clipboard is empty, so the failure surfaces
before the recipient is chosen. PeerSendActions is keyed on the peer so
a staged confirmation cannot carry over to a different recipient.

Deleting a history entry now asks first, reusing the app-wide useConfirm
hook. The wording states that received files stay on disk, matching what
DeleteTransfer does -- it drops the history record and cancels a live
transfer, and never touches delivered files. The dropdown is
modal={false} because DropdownMenuItem calls preventDefault, which keeps
a modal menu's focus trap alive against the dialog.
This commit is contained in:
Zoltán Papp
2026-09-01 14:25:28 +02:00
parent fa47b32b92
commit 2be390f426
17 changed files with 785 additions and 78 deletions
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Löschen"
},
"files.delete.title": {
"message": "Aus dem Verlauf entfernen?"
},
"files.delete.message": {
"message": "Dieser Eintrag wird aus dem Übertragungsverlauf entfernt.\nBereits empfangene Dateien bleiben auf der Festplatte."
},
"files.send.search": {
"message": "Peers suchen"
},
"files.send.empty": {
"message": "Kein passender Peer"
},
"files.send.noPeers": {
"message": "Keine Peers verfügbar"
},
"files.send.trigger": {
"message": "Senden…"
},
"files.send.back": {
"message": "Zurück"
},
"files.send.clipboardPreview": {
"message": "Vorschau der Zwischenablage"
},
"peers.dropToSend": {
"message": "Zum Senden ablegen"
},
+32
View File
@@ -1943,6 +1943,38 @@
"message": "Delete",
"description": "Remove the entry from the history."
},
"files.delete.title": {
"message": "Remove from history?",
"description": "Confirmation-dialog title for removing a transfer from the file history."
},
"files.delete.message": {
"message": "This entry is removed from the transfer history.\nAlready received files are kept on disk.",
"description": "Confirmation body for removing a history entry; states that received files stay on disk. Contains a line break (\\n) — keep it."
},
"files.send.search": {
"message": "Search peers",
"description": "Placeholder of the peer-picker search field."
},
"files.send.empty": {
"message": "No matching peer",
"description": "Shown when the peer-picker search matches nothing."
},
"files.send.noPeers": {
"message": "No peers available",
"description": "Shown in the peer picker when the peer list is empty."
},
"files.send.trigger": {
"message": "Send…",
"description": "Label of the single send button in the Files header. Keep short."
},
"files.send.back": {
"message": "Back",
"description": "Returns from the what-to-send step to the peer list. Keep short."
},
"files.send.clipboardPreview": {
"message": "Clipboard preview",
"description": "Accessible label of the clipboard-text preview shown before sending."
},
"peers.dropToSend": {
"message": "Drop to send",
"description": "Overlay shown while dragging files over a peer row."
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Eliminar"
},
"files.delete.title": {
"message": "¿Quitar del historial?"
},
"files.delete.message": {
"message": "Esta entrada se quitará del historial de transferencias.\nLos archivos ya recibidos se conservan en el disco."
},
"files.send.search": {
"message": "Buscar peers"
},
"files.send.empty": {
"message": "Ningún peer coincide"
},
"files.send.noPeers": {
"message": "No hay peers disponibles"
},
"files.send.trigger": {
"message": "Enviar…"
},
"files.send.back": {
"message": "Atrás"
},
"files.send.clipboardPreview": {
"message": "Vista previa del portapapeles"
},
"peers.dropToSend": {
"message": "Suelta para enviar"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Supprimer"
},
"files.delete.title": {
"message": "Retirer de l’historique ?"
},
"files.delete.message": {
"message": "Cette entrée est retirée de l’historique des transferts.\nLes fichiers déjà reçus sont conservés sur le disque."
},
"files.send.search": {
"message": "Rechercher des pairs"
},
"files.send.empty": {
"message": "Aucun pair correspondant"
},
"files.send.noPeers": {
"message": "Aucun pair disponible"
},
"files.send.trigger": {
"message": "Envoyer…"
},
"files.send.back": {
"message": "Retour"
},
"files.send.clipboardPreview": {
"message": "Aperçu du presse-papiers"
},
"peers.dropToSend": {
"message": "Déposer pour envoyer"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Törlés"
},
"files.delete.title": {
"message": "Eltávolítás az előzményekből?"
},
"files.delete.message": {
"message": "A bejegyzés törlődik az átviteli előzményekből.\nA már fogadott fájlok a lemezen maradnak."
},
"files.send.search": {
"message": "Gépek keresése"
},
"files.send.empty": {
"message": "Nincs találat"
},
"files.send.noPeers": {
"message": "Nincs elérhető gép"
},
"files.send.trigger": {
"message": "Küldés…"
},
"files.send.back": {
"message": "Vissza"
},
"files.send.clipboardPreview": {
"message": "Vágólap előnézete"
},
"peers.dropToSend": {
"message": "Engedd el a küldéshez"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Elimina"
},
"files.delete.title": {
"message": "Rimuovere dalla cronologia?"
},
"files.delete.message": {
"message": "Questa voce viene rimossa dalla cronologia dei trasferimenti.\nI file già ricevuti vengono conservati su disco."
},
"files.send.search": {
"message": "Cerca peer"
},
"files.send.empty": {
"message": "Nessun peer corrispondente"
},
"files.send.noPeers": {
"message": "Nessun peer disponibile"
},
"files.send.trigger": {
"message": "Invia…"
},
"files.send.back": {
"message": "Indietro"
},
"files.send.clipboardPreview": {
"message": "Anteprima degli appunti"
},
"peers.dropToSend": {
"message": "Rilascia per inviare"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "削除"
},
"files.delete.title": {
"message": "履歴から削除しますか?"
},
"files.delete.message": {
"message": "この項目は転送履歴から削除されます。\n受信済みのファイルはディスクに残ります。"
},
"files.send.search": {
"message": "ピアを検索"
},
"files.send.empty": {
"message": "一致するピアがありません"
},
"files.send.noPeers": {
"message": "利用可能なピアがありません"
},
"files.send.trigger": {
"message": "送信…"
},
"files.send.back": {
"message": "戻る"
},
"files.send.clipboardPreview": {
"message": "クリップボードのプレビュー"
},
"peers.dropToSend": {
"message": "ドロップして送信"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Excluir"
},
"files.delete.title": {
"message": "Remover do histórico?"
},
"files.delete.message": {
"message": "Esta entrada é removida do histórico de transferências.\nOs ficheiros já recebidos são mantidos no disco."
},
"files.send.search": {
"message": "Pesquisar peers"
},
"files.send.empty": {
"message": "Nenhum peer correspondente"
},
"files.send.noPeers": {
"message": "Nenhum peer disponível"
},
"files.send.trigger": {
"message": "Enviar…"
},
"files.send.back": {
"message": "Voltar"
},
"files.send.clipboardPreview": {
"message": "Pré-visualização da área de transferência"
},
"peers.dropToSend": {
"message": "Solte para enviar"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "Удалить"
},
"files.delete.title": {
"message": "Удалить из истории?"
},
"files.delete.message": {
"message": "Запись будет удалена из истории передач.\nУже полученные файлы останутся на диске."
},
"files.send.search": {
"message": "Поиск узлов"
},
"files.send.empty": {
"message": "Нет подходящих узлов"
},
"files.send.noPeers": {
"message": "Нет доступных узлов"
},
"files.send.trigger": {
"message": "Отправить…"
},
"files.send.back": {
"message": "Назад"
},
"files.send.clipboardPreview": {
"message": "Предпросмотр буфера обмена"
},
"peers.dropToSend": {
"message": "Отпустите для отправки"
},
+24
View File
@@ -1435,6 +1435,30 @@
"files.action.delete": {
"message": "删除"
},
"files.delete.title": {
"message": "从历史记录中移除?"
},
"files.delete.message": {
"message": "此条目将从传输历史记录中移除。\n已接收的文件会保留在磁盘上。"
},
"files.send.search": {
"message": "搜索对等节点"
},
"files.send.empty": {
"message": "没有匹配的对等节点"
},
"files.send.noPeers": {
"message": "没有可用的对等节点"
},
"files.send.trigger": {
"message": "发送…"
},
"files.send.back": {
"message": "返回"
},
"files.send.clipboardPreview": {
"message": "剪贴板预览"
},
"peers.dropToSend": {
"message": "放开即发送"
},