v4: the playlist and dump edit forms end in one bar of actions, a panel of its own — the playlist editor drew rules between its fields and its actions, and on a phone its Delete, Cancel and Save wrapped into a staircase, then stacked (a0c8b1d), neither of which held together. Both forms are now built as DumpEdit was: surface panels 3px apart (.form-panel, from .dump-edit-panel), the last a bar (.form-panel--bar, from .dump-edit-footer) holding Delete on the left and Cancel and Save on the right, compact (btn-sm), on one line. The new DeleteButton says "Delete" and names what goes to assistive tech ("Delete playlist", "Delete dump"); in a bar too narrow for the three labels in every face and language (a container query, under 18rem), it shows 🗑 instead. While edited, the playlist's header drops its own card for the form's panels. The phone stacking rule is gone. An auto-sized editor also re-fits when its width changes with its text unchanged: opening the form makes the page tall enough for a scrollbar, which narrowed the description after it was measured, so it overflowed; and it hides its overflow below its max-height, where a scrollbar could only come of rounding, and drops the resize grip, which the next keystroke would undo. 669 frontend tests pass, and tsc, eslint, deno fmt, doc:lint and i18n:compile --strict are clean; checked in screenshots in Kanagawa dark (French, 385 and 360px), Smooth light at 1000px and DumpEdit in Smooth at 390px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-08 22:05:43 +00:00
parent b9281da9c9
commit 5acdf79c8d
9 changed files with 313 additions and 224 deletions

View File

@@ -427,7 +427,7 @@ msgstr "← Back"
#: src/pages/Dump.tsx:326
#: src/pages/Dump.tsx:568
#: src/pages/DumpEdit.tsx:231
#: src/pages/DumpEdit.tsx:232
msgid "← Back to all dumps"
msgstr "← Back to all dumps"
@@ -589,12 +589,12 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
#: src/components/comment/CommentForm.tsx:127
#: src/components/dump/create/DumpCreateModal.tsx:752
#: src/components/form/FormActions.tsx:42
#: src/components/playlist/PlaylistEditForm.tsx:194
#: src/components/playlist/PlaylistEditForm.tsx:199
#: src/components/profile/DescriptionEditor.tsx:97
#: src/components/profile/EmailEditor.tsx:83
#: src/components/shared/ConfirmModal.tsx:63
#: src/pages/Dump.tsx:445
#: src/pages/DumpEdit.tsx:570
#: src/pages/DumpEdit.tsx:565
msgid "Cancel"
msgstr "Cancel"
@@ -607,7 +607,7 @@ msgstr "Cancel removal"
msgid "Cancel reply"
msgstr "Cancel reply"
#: src/components/layout/AppHeader.tsx:164
#: src/components/layout/AppHeader.tsx:190
msgid "Cancel search"
msgstr "Cancel search"
@@ -642,13 +642,13 @@ msgid "Change skin tone"
msgstr "Change skin tone"
#: src/components/chat/ChatModal.tsx:716
#: src/components/layout/ChatButton.tsx:27
#: src/components/layout/ChatButton.tsx:30
#: src/components/layout/ChatFab.tsx:54
#: src/components/layout/ChatFab.tsx:55
msgid "Chat"
msgstr "Chat"
#: src/components/layout/ChatButton.tsx:26
#: src/components/layout/ChatButton.tsx:29
#: src/components/layout/ChatFab.tsx:53
msgid "Chat ({unreadChatCount} unread)"
msgstr "Chat ({unreadChatCount} unread)"
@@ -712,7 +712,7 @@ msgid "Could not connect to the server. Check your connection and try again."
msgstr "Could not connect to the server. Check your connection and try again."
#: src/components/profile/ProfilePlaylistsTab.tsx:126
#: src/pages/DumpEdit.tsx:558
#: src/pages/DumpEdit.tsx:559
msgid "Could not delete"
msgstr "Could not delete"
@@ -720,7 +720,7 @@ msgstr "Could not delete"
msgid "Could not rename"
msgstr "Could not rename"
#: src/pages/DumpEdit.tsx:511
#: src/pages/DumpEdit.tsx:512
msgid "Could not save"
msgstr "Could not save"
@@ -740,7 +740,7 @@ msgstr "Couldn't unsubscribe"
msgid "Couldn't update. Try again."
msgstr "Couldn't update. Try again."
#: src/components/playlist/PlaylistEditForm.tsx:133
#: src/components/playlist/PlaylistEditForm.tsx:135
msgid "Cover"
msgstr "Cover"
@@ -775,7 +775,7 @@ msgstr "Default tab"
#: src/components/chat/ChatMessageItem.tsx:214
#: src/components/comment/CommentNode.tsx:265
#: src/components/comment/CommentNode.tsx:271
#: src/components/playlist/PlaylistEditForm.tsx:190
#: src/components/form/DeleteButton.tsx:36
#: src/components/shared/ConfirmModal.tsx:34
msgid "Delete"
msgstr "Delete"
@@ -789,19 +789,20 @@ msgstr "Delete category"
msgid "Delete category \"{0}\"? This cannot be undone."
msgstr "Delete category \"{0}\"? This cannot be undone."
#: src/pages/DumpEdit.tsx:313
#: src/pages/DumpEdit.tsx:566
#: src/pages/DumpEdit.tsx:314
#: src/pages/DumpEdit.tsx:562
msgid "Delete dump"
msgstr "Delete dump"
#. placeholder {0}: res.status
#: src/pages/DumpEdit.tsx:192
#: src/pages/DumpEdit.tsx:193
msgid "Delete failed ({0})"
msgstr "Delete failed ({0})"
#: src/components/playlist/PlaylistCard.tsx:142
#: src/components/playlist/PlaylistEditForm.tsx:190
#: src/components/profile/ProfilePlaylistsTab.tsx:156
#: src/pages/PlaylistDetail.tsx:136
#: src/pages/PlaylistDetail.tsx:141
msgid "Delete playlist"
msgstr "Delete playlist"
@@ -809,7 +810,7 @@ msgstr "Delete playlist"
msgid "Delete this comment?"
msgstr "Delete this comment?"
#: src/pages/DumpEdit.tsx:312
#: src/pages/DumpEdit.tsx:313
msgid "Delete this dump? This cannot be undone."
msgstr "Delete this dump? This cannot be undone."
@@ -818,7 +819,7 @@ msgid "Delete this message?"
msgstr "Delete this message?"
#: src/components/profile/ProfilePlaylistsTab.tsx:155
#: src/pages/PlaylistDetail.tsx:135
#: src/pages/PlaylistDetail.tsx:140
msgid "Delete this playlist? This cannot be undone."
msgstr "Delete this playlist? This cannot be undone."
@@ -826,7 +827,7 @@ msgstr "Delete this playlist? This cannot be undone."
msgid "deleted message"
msgstr "deleted message"
#: src/components/playlist/PlaylistEditForm.tsx:173
#: src/components/playlist/PlaylistEditForm.tsx:175
msgid "Description"
msgstr "Description"
@@ -864,11 +865,11 @@ msgstr "Drop a file here"
msgid "Drop a file here, or paste one"
msgstr "Drop a file here, or paste one"
#: src/pages/DumpEdit.tsx:502
#: src/pages/DumpEdit.tsx:503
msgid "Drop a replacement here"
msgstr "Drop a replacement here"
#: src/components/layout/AppHeader.tsx:203
#: src/components/layout/AppHeader.tsx:209
msgid "Dump"
msgstr "Dump"
@@ -946,12 +947,12 @@ msgstr "edited {0}"
msgid "Edited {0}"
msgstr "Edited {0}"
#: src/pages/DumpEdit.tsx:256
#: src/pages/DumpEdit.tsx:257
msgid "Editing"
msgstr "Editing"
#. placeholder {0}: state.dump.title
#: src/pages/DumpEdit.tsx:79
#: src/pages/DumpEdit.tsx:80
msgid "Editing {0}"
msgstr "Editing {0}"
@@ -1005,7 +1006,7 @@ msgstr "Failed to post comment"
msgid "Failed to post reply"
msgstr "Failed to post reply"
#: src/components/playlist/PlaylistEditForm.tsx:182
#: src/components/playlist/PlaylistEditForm.tsx:184
#: src/components/profile/DescriptionEditor.tsx:99
#: src/components/profile/EmailEditor.tsx:86
msgid "Failed to save"
@@ -1113,8 +1114,8 @@ msgstr "image"
msgid "In collections"
msgstr "In collections"
#: src/components/editor/TextEditor.tsx:544
#: src/components/editor/TextEditor.tsx:546
#: src/components/editor/TextEditor.tsx:573
#: src/components/editor/TextEditor.tsx:575
msgid "Insert emoji"
msgstr "Insert emoji"
@@ -1175,7 +1176,7 @@ msgstr "Link or file"
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
#: src/components/layout/AppHeader.tsx:238
#: src/components/layout/AppHeader.tsx:242
msgid "Live updates unavailable."
msgstr "Live updates unavailable."
@@ -1188,7 +1189,7 @@ msgid "Load older messages"
msgstr "Load older messages"
#: src/pages/Dump.tsx:302
#: src/pages/DumpEdit.tsx:207
#: src/pages/DumpEdit.tsx:208
msgid "Loading dump…"
msgstr "Loading dump…"
@@ -1218,7 +1219,7 @@ msgstr "Loading profile…"
msgid "Loading…"
msgstr "Loading…"
#: src/components/layout/AppHeader.tsx:211
#: src/components/layout/AppHeader.tsx:216
#: src/pages/UserLogin.tsx:41
#: src/pages/UserLogin.tsx:67
#: src/pages/UserLogin.tsx:86
@@ -1366,14 +1367,14 @@ msgstr "Not following anyone yet."
msgid "Nothing here yet."
msgstr "Nothing here yet."
#: src/components/layout/NotificationBell.tsx:55
#: src/components/layout/NotificationBell.tsx:57
#: src/pages/Notifications.tsx:376
#: src/pages/Notifications.tsx:494
#: src/pages/Settings.tsx:128
msgid "Notifications"
msgstr "Notifications"
#: src/components/layout/NotificationBell.tsx:54
#: src/components/layout/NotificationBell.tsx:56
msgid "Notifications ({unreadNotificationCount} unread)"
msgstr "Notifications ({unreadNotificationCount} unread)"
@@ -1385,11 +1386,11 @@ msgstr "Off"
msgid "On"
msgstr "On"
#: src/components/editor/TextEditor.tsx:359
#: src/components/editor/TextEditor.tsx:385
msgid "Only images can be dropped here."
msgstr "Only images can be dropped here."
#: src/pages/DumpEdit.tsx:117
#: src/pages/DumpEdit.tsx:118
msgid "Only its poster or a moderator can edit this dump."
msgstr "Only its poster or a moderator can edit this dump."
@@ -1463,7 +1464,7 @@ msgid "Play"
msgstr "Play"
#: src/components/dump/create/StepIndicator.tsx:21
#: src/components/layout/AppHeader.tsx:184
#: src/components/layout/AppHeader.tsx:178
#: src/components/layout/UserMenu.tsx:81
#: src/components/profile/ProfileFollowTab.tsx:167
#: src/components/profile/ProfilePlaylistsTab.tsx:118
@@ -1512,7 +1513,7 @@ msgstr "public"
msgid "Public"
msgstr "Public"
#: src/pages/DumpEdit.tsx:289
#: src/pages/DumpEdit.tsx:290
msgid "Refresh metadata"
msgstr "Refresh metadata"
@@ -1520,7 +1521,7 @@ msgstr "Refresh metadata"
msgid "Refreshing stream…"
msgstr "Refreshing stream…"
#: src/pages/DumpEdit.tsx:288
#: src/pages/DumpEdit.tsx:289
msgid "Refreshing…"
msgstr "Refreshing…"
@@ -1566,7 +1567,7 @@ msgstr "Rename"
msgid "Renaming…"
msgstr "Renaming…"
#: src/pages/DumpEdit.tsx:494
#: src/pages/DumpEdit.tsx:495
msgid "Replace file"
msgstr "Replace file"
@@ -1592,8 +1593,8 @@ msgstr "Reset failed"
msgid "Reset password"
msgstr "Reset password"
#: src/pages/DumpEdit.tsx:470
#: src/pages/DumpEdit.tsx:525
#: src/pages/DumpEdit.tsx:471
#: src/pages/DumpEdit.tsx:526
msgid "Reset to default"
msgstr "Reset to default"
@@ -1604,7 +1605,7 @@ msgstr "Reset to the suggested title"
#: src/components/chat/PendingItem.tsx:86
#: src/pages/Dump.tsx:319
#: src/pages/DumpEdit.tsx:224
#: src/pages/DumpEdit.tsx:225
msgid "Retry"
msgstr "Retry"
@@ -1615,16 +1616,16 @@ msgstr "Role"
#: src/components/category/CategoryManager.tsx:216
#: src/components/chat/ChatEditBox.tsx:85
#: src/components/comment/CommentNode.tsx:216
#: src/components/playlist/PlaylistEditForm.tsx:200
#: src/components/playlist/PlaylistEditForm.tsx:206
#: src/components/profile/DescriptionEditor.tsx:89
#: src/components/profile/EmailEditor.tsx:75
#: src/pages/Dump.tsx:437
#: src/pages/DumpEdit.tsx:573
#: src/pages/DumpEdit.tsx:568
msgid "Save"
msgstr "Save"
#: src/components/comment/CommentNode.tsx:217
#: src/components/playlist/PlaylistEditForm.tsx:197
#: src/components/playlist/PlaylistEditForm.tsx:203
#: src/components/profile/ChangePasswordModal.tsx:119
#: src/components/profile/DescriptionEditor.tsx:86
#: src/components/profile/EmailEditor.tsx:72
@@ -1633,7 +1634,7 @@ msgstr "Save"
msgid "Saving…"
msgstr "Saving…"
#: src/components/layout/AppHeader.tsx:164
#: src/components/layout/AppHeader.tsx:190
#: src/components/layout/SearchBar.tsx:83
#: src/pages/Search.tsx:69
msgid "Search"
@@ -1676,7 +1677,7 @@ msgstr "Send reset link"
msgid "Sending…"
msgstr "Sending…"
#: src/components/layout/AppHeader.tsx:200
#: src/components/layout/AppHeader.tsx:206
msgid "Server unreachable"
msgstr "Server unreachable"
@@ -1704,14 +1705,14 @@ msgstr "Single use, valid for 7 days."
msgid "slug"
msgstr "slug"
#: src/components/editor/TextEditor.tsx:317
#: src/components/editor/TextEditor.tsx:343
msgid "Some images couldn't be uploaded."
msgstr "Some images couldn't be uploaded."
#: src/components/form/FormError.tsx:31
#: src/components/layout/PageError.tsx:29
#: src/contexts/ws/WSProvider.tsx:729
#: src/pages/PlaylistDetail.tsx:119
#: src/pages/PlaylistDetail.tsx:124
msgid "Something went wrong"
msgstr "Something went wrong"
@@ -1743,7 +1744,7 @@ msgstr "the chat"
msgid "The dump couldn't be removed. Try again."
msgstr "The dump couldn't be removed. Try again."
#: src/components/editor/TextEditor.tsx:318
#: src/components/editor/TextEditor.tsx:344
msgid "The image couldn't be uploaded."
msgstr "The image couldn't be uploaded."
@@ -1779,14 +1780,14 @@ msgstr "This site doesn't send email, so no digest goes out."
msgid "This unsubscribe link is missing or malformed."
msgstr "This unsubscribe link is missing or malformed."
#: src/pages/DumpEdit.tsx:454
#: src/pages/DumpEdit.tsx:455
msgid "Thumbnail"
msgstr "Thumbnail"
#: src/components/dump/create/TitleField.tsx:45
#: src/components/playlist/PlaylistCreateForm.tsx:96
#: src/components/playlist/PlaylistEditForm.tsx:157
#: src/pages/DumpEdit.tsx:515
#: src/components/playlist/PlaylistEditForm.tsx:159
#: src/pages/DumpEdit.tsx:516
msgid "Title"
msgstr "Title"
@@ -1861,7 +1862,7 @@ msgstr "Upload failed"
msgid "Uploading {0}%"
msgstr "Uploading {0}%"
#: src/components/editor/TextEditor.tsx:566
#: src/components/editor/TextEditor.tsx:595
msgid "Uploading image…"
msgstr "Uploading image…"
@@ -1883,7 +1884,7 @@ msgstr "Upvoted"
msgid "Upvoted ({0})"
msgstr "Upvoted ({0})"
#: src/pages/DumpEdit.tsx:486
#: src/pages/DumpEdit.tsx:487
msgid "URL"
msgstr "URL"
@@ -1933,7 +1934,7 @@ msgid "Volume"
msgstr "Volume"
#: src/components/dump/create/DumpCreateModal.tsx:712
#: src/pages/DumpEdit.tsx:543
#: src/pages/DumpEdit.tsx:544
msgid "What makes it worth it?"
msgstr "What makes it worth it?"
@@ -1951,7 +1952,7 @@ msgid "Why & where"
msgstr "Why & where"
#: src/components/dump/create/DumpCreateModal.tsx:711
#: src/pages/DumpEdit.tsx:542
#: src/pages/DumpEdit.tsx:543
msgid "Why?"
msgstr "Why?"

View File

@@ -427,7 +427,7 @@ msgstr "← Retour"
#: src/pages/Dump.tsx:326
#: src/pages/Dump.tsx:568
#: src/pages/DumpEdit.tsx:231
#: src/pages/DumpEdit.tsx:232
msgid "← Back to all dumps"
msgstr "← Retour à toutes les recos"
@@ -589,12 +589,12 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
#: src/components/comment/CommentForm.tsx:127
#: src/components/dump/create/DumpCreateModal.tsx:752
#: src/components/form/FormActions.tsx:42
#: src/components/playlist/PlaylistEditForm.tsx:194
#: src/components/playlist/PlaylistEditForm.tsx:199
#: src/components/profile/DescriptionEditor.tsx:97
#: src/components/profile/EmailEditor.tsx:83
#: src/components/shared/ConfirmModal.tsx:63
#: src/pages/Dump.tsx:445
#: src/pages/DumpEdit.tsx:570
#: src/pages/DumpEdit.tsx:565
msgid "Cancel"
msgstr "Annuler"
@@ -607,7 +607,7 @@ msgstr "Annuler la suppression"
msgid "Cancel reply"
msgstr "Annuler la réponse"
#: src/components/layout/AppHeader.tsx:164
#: src/components/layout/AppHeader.tsx:190
msgid "Cancel search"
msgstr "Annuler la recherche"
@@ -642,13 +642,13 @@ msgid "Change skin tone"
msgstr "Changer la couleur de peau"
#: src/components/chat/ChatModal.tsx:716
#: src/components/layout/ChatButton.tsx:27
#: src/components/layout/ChatButton.tsx:30
#: src/components/layout/ChatFab.tsx:54
#: src/components/layout/ChatFab.tsx:55
msgid "Chat"
msgstr "Tribune"
#: src/components/layout/ChatButton.tsx:26
#: src/components/layout/ChatButton.tsx:29
#: src/components/layout/ChatFab.tsx:53
msgid "Chat ({unreadChatCount} unread)"
msgstr "{unreadChatCount} messages non lus"
@@ -712,7 +712,7 @@ msgid "Could not connect to the server. Check your connection and try again."
msgstr "Impossible de joindre le serveur. Vérifiez votre connexion et réessayez."
#: src/components/profile/ProfilePlaylistsTab.tsx:126
#: src/pages/DumpEdit.tsx:558
#: src/pages/DumpEdit.tsx:559
msgid "Could not delete"
msgstr "Suppression impossible"
@@ -720,7 +720,7 @@ msgstr "Suppression impossible"
msgid "Could not rename"
msgstr "Renommage impossible"
#: src/pages/DumpEdit.tsx:511
#: src/pages/DumpEdit.tsx:512
msgid "Could not save"
msgstr "Sauvegarde impossible"
@@ -740,7 +740,7 @@ msgstr "Désabonnement impossible"
msgid "Couldn't update. Try again."
msgstr "La mise à jour a échoué. Réessayez."
#: src/components/playlist/PlaylistEditForm.tsx:133
#: src/components/playlist/PlaylistEditForm.tsx:135
msgid "Cover"
msgstr "Couverture"
@@ -775,7 +775,7 @@ msgstr "Onglet par défaut"
#: src/components/chat/ChatMessageItem.tsx:214
#: src/components/comment/CommentNode.tsx:265
#: src/components/comment/CommentNode.tsx:271
#: src/components/playlist/PlaylistEditForm.tsx:190
#: src/components/form/DeleteButton.tsx:36
#: src/components/shared/ConfirmModal.tsx:34
msgid "Delete"
msgstr "Supprimer"
@@ -789,19 +789,20 @@ msgstr "Supprimer la catégorie"
msgid "Delete category \"{0}\"? This cannot be undone."
msgstr "Supprimer la catégorie \"{0}\" ? Cette action est irréversible."
#: src/pages/DumpEdit.tsx:313
#: src/pages/DumpEdit.tsx:566
#: src/pages/DumpEdit.tsx:314
#: src/pages/DumpEdit.tsx:562
msgid "Delete dump"
msgstr "Supprimer la reco"
#. placeholder {0}: res.status
#: src/pages/DumpEdit.tsx:192
#: src/pages/DumpEdit.tsx:193
msgid "Delete failed ({0})"
msgstr "Échec de la suppression ({0})"
#: src/components/playlist/PlaylistCard.tsx:142
#: src/components/playlist/PlaylistEditForm.tsx:190
#: src/components/profile/ProfilePlaylistsTab.tsx:156
#: src/pages/PlaylistDetail.tsx:136
#: src/pages/PlaylistDetail.tsx:141
msgid "Delete playlist"
msgstr "Supprimer la collection"
@@ -809,7 +810,7 @@ msgstr "Supprimer la collection"
msgid "Delete this comment?"
msgstr "Supprimer ce commentaire ?"
#: src/pages/DumpEdit.tsx:312
#: src/pages/DumpEdit.tsx:313
msgid "Delete this dump? This cannot be undone."
msgstr "Supprimer cette reco ? Cette action est irréversible."
@@ -818,7 +819,7 @@ msgid "Delete this message?"
msgstr "Supprimer ce message ?"
#: src/components/profile/ProfilePlaylistsTab.tsx:155
#: src/pages/PlaylistDetail.tsx:135
#: src/pages/PlaylistDetail.tsx:140
msgid "Delete this playlist? This cannot be undone."
msgstr "Supprimer cette collection ? Cette action est irréversible."
@@ -826,7 +827,7 @@ msgstr "Supprimer cette collection ? Cette action est irréversible."
msgid "deleted message"
msgstr "message supprimé"
#: src/components/playlist/PlaylistEditForm.tsx:173
#: src/components/playlist/PlaylistEditForm.tsx:175
msgid "Description"
msgstr "Description"
@@ -864,11 +865,11 @@ msgstr "Déposez un fichier ici"
msgid "Drop a file here, or paste one"
msgstr "Déposez un fichier ici, ou collez-en un"
#: src/pages/DumpEdit.tsx:502
#: src/pages/DumpEdit.tsx:503
msgid "Drop a replacement here"
msgstr "Déposez un fichier de remplacement ici"
#: src/components/layout/AppHeader.tsx:203
#: src/components/layout/AppHeader.tsx:209
msgid "Dump"
msgstr "Reco"
@@ -946,12 +947,12 @@ msgstr "modifié {0}"
msgid "Edited {0}"
msgstr "Modifié le {0}"
#: src/pages/DumpEdit.tsx:256
#: src/pages/DumpEdit.tsx:257
msgid "Editing"
msgstr "Modification"
#. placeholder {0}: state.dump.title
#: src/pages/DumpEdit.tsx:79
#: src/pages/DumpEdit.tsx:80
msgid "Editing {0}"
msgstr "Édition de {0}"
@@ -1005,7 +1006,7 @@ msgstr "Impossible de publier le commentaire"
msgid "Failed to post reply"
msgstr "Impossible de publier la réponse"
#: src/components/playlist/PlaylistEditForm.tsx:182
#: src/components/playlist/PlaylistEditForm.tsx:184
#: src/components/profile/DescriptionEditor.tsx:99
#: src/components/profile/EmailEditor.tsx:86
msgid "Failed to save"
@@ -1113,8 +1114,8 @@ msgstr "image"
msgid "In collections"
msgstr "Dans les collections"
#: src/components/editor/TextEditor.tsx:544
#: src/components/editor/TextEditor.tsx:546
#: src/components/editor/TextEditor.tsx:573
#: src/components/editor/TextEditor.tsx:575
msgid "Insert emoji"
msgstr "Insérer un emoji"
@@ -1175,7 +1176,7 @@ msgstr "Lien ou fichier"
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
#: src/components/layout/AppHeader.tsx:238
#: src/components/layout/AppHeader.tsx:242
msgid "Live updates unavailable."
msgstr "Mises à jour en direct indisponibles."
@@ -1188,7 +1189,7 @@ msgid "Load older messages"
msgstr "Charger les messages plus anciens"
#: src/pages/Dump.tsx:302
#: src/pages/DumpEdit.tsx:207
#: src/pages/DumpEdit.tsx:208
msgid "Loading dump…"
msgstr "Chargement de la reco…"
@@ -1218,7 +1219,7 @@ msgstr "Chargement du profil…"
msgid "Loading…"
msgstr "Chargement…"
#: src/components/layout/AppHeader.tsx:211
#: src/components/layout/AppHeader.tsx:216
#: src/pages/UserLogin.tsx:41
#: src/pages/UserLogin.tsx:67
#: src/pages/UserLogin.tsx:86
@@ -1366,14 +1367,14 @@ msgstr "Aucun abonnement pour le moment."
msgid "Nothing here yet."
msgstr "Rien ici pour l'instant."
#: src/components/layout/NotificationBell.tsx:55
#: src/components/layout/NotificationBell.tsx:57
#: src/pages/Notifications.tsx:376
#: src/pages/Notifications.tsx:494
#: src/pages/Settings.tsx:128
msgid "Notifications"
msgstr "Notifications"
#: src/components/layout/NotificationBell.tsx:54
#: src/components/layout/NotificationBell.tsx:56
msgid "Notifications ({unreadNotificationCount} unread)"
msgstr "Notifications ({unreadNotificationCount} non lues)"
@@ -1385,11 +1386,11 @@ msgstr "Désactivés"
msgid "On"
msgstr "Activés"
#: src/components/editor/TextEditor.tsx:359
#: src/components/editor/TextEditor.tsx:385
msgid "Only images can be dropped here."
msgstr "Seules les images peuvent être déposées ici."
#: src/pages/DumpEdit.tsx:117
#: src/pages/DumpEdit.tsx:118
msgid "Only its poster or a moderator can edit this dump."
msgstr "Seuls son auteur et les modérateurs peuvent modifier ce dump."
@@ -1463,7 +1464,7 @@ msgid "Play"
msgstr "Lecture"
#: src/components/dump/create/StepIndicator.tsx:21
#: src/components/layout/AppHeader.tsx:184
#: src/components/layout/AppHeader.tsx:178
#: src/components/layout/UserMenu.tsx:81
#: src/components/profile/ProfileFollowTab.tsx:167
#: src/components/profile/ProfilePlaylistsTab.tsx:118
@@ -1512,7 +1513,7 @@ msgstr "public"
msgid "Public"
msgstr "Public"
#: src/pages/DumpEdit.tsx:289
#: src/pages/DumpEdit.tsx:290
msgid "Refresh metadata"
msgstr "Actualiser les métadonnées"
@@ -1520,7 +1521,7 @@ msgstr "Actualiser les métadonnées"
msgid "Refreshing stream…"
msgstr "Actualisation du flux…"
#: src/pages/DumpEdit.tsx:288
#: src/pages/DumpEdit.tsx:289
msgid "Refreshing…"
msgstr "Actualisation…"
@@ -1566,7 +1567,7 @@ msgstr "Renommer"
msgid "Renaming…"
msgstr "Renommage…"
#: src/pages/DumpEdit.tsx:494
#: src/pages/DumpEdit.tsx:495
msgid "Replace file"
msgstr "Remplacer le fichier"
@@ -1592,8 +1593,8 @@ msgstr "Échec de la réinitialisation"
msgid "Reset password"
msgstr "Réinitialiser le mot de passe"
#: src/pages/DumpEdit.tsx:470
#: src/pages/DumpEdit.tsx:525
#: src/pages/DumpEdit.tsx:471
#: src/pages/DumpEdit.tsx:526
msgid "Reset to default"
msgstr "Réinitialiser par défaut"
@@ -1604,7 +1605,7 @@ msgstr "Rétablir le titre suggéré"
#: src/components/chat/PendingItem.tsx:86
#: src/pages/Dump.tsx:319
#: src/pages/DumpEdit.tsx:224
#: src/pages/DumpEdit.tsx:225
msgid "Retry"
msgstr "Réessayer"
@@ -1615,16 +1616,16 @@ msgstr "Rôle"
#: src/components/category/CategoryManager.tsx:216
#: src/components/chat/ChatEditBox.tsx:85
#: src/components/comment/CommentNode.tsx:216
#: src/components/playlist/PlaylistEditForm.tsx:200
#: src/components/playlist/PlaylistEditForm.tsx:206
#: src/components/profile/DescriptionEditor.tsx:89
#: src/components/profile/EmailEditor.tsx:75
#: src/pages/Dump.tsx:437
#: src/pages/DumpEdit.tsx:573
#: src/pages/DumpEdit.tsx:568
msgid "Save"
msgstr "Enregistrer"
#: src/components/comment/CommentNode.tsx:217
#: src/components/playlist/PlaylistEditForm.tsx:197
#: src/components/playlist/PlaylistEditForm.tsx:203
#: src/components/profile/ChangePasswordModal.tsx:119
#: src/components/profile/DescriptionEditor.tsx:86
#: src/components/profile/EmailEditor.tsx:72
@@ -1633,7 +1634,7 @@ msgstr "Enregistrer"
msgid "Saving…"
msgstr "Enregistrement…"
#: src/components/layout/AppHeader.tsx:164
#: src/components/layout/AppHeader.tsx:190
#: src/components/layout/SearchBar.tsx:83
#: src/pages/Search.tsx:69
msgid "Search"
@@ -1676,7 +1677,7 @@ msgstr "Envoyer le lien de réinitialisation"
msgid "Sending…"
msgstr "Envoi…"
#: src/components/layout/AppHeader.tsx:200
#: src/components/layout/AppHeader.tsx:206
msgid "Server unreachable"
msgstr "Serveur inaccessible"
@@ -1704,14 +1705,14 @@ msgstr "Usage unique, valable 7 jours."
msgid "slug"
msgstr "identifiant"
#: src/components/editor/TextEditor.tsx:317
#: src/components/editor/TextEditor.tsx:343
msgid "Some images couldn't be uploaded."
msgstr "Certaines images n'ont pas pu être envoyées."
#: src/components/form/FormError.tsx:31
#: src/components/layout/PageError.tsx:29
#: src/contexts/ws/WSProvider.tsx:729
#: src/pages/PlaylistDetail.tsx:119
#: src/pages/PlaylistDetail.tsx:124
msgid "Something went wrong"
msgstr "Une erreur est survenue"
@@ -1743,7 +1744,7 @@ msgstr "la tribune"
msgid "The dump couldn't be removed. Try again."
msgstr "Le dump n'a pas pu être retiré. Réessayez."
#: src/components/editor/TextEditor.tsx:318
#: src/components/editor/TextEditor.tsx:344
msgid "The image couldn't be uploaded."
msgstr "L'image n'a pas pu être envoyée."
@@ -1779,14 +1780,14 @@ msgstr "Ce site n'envoie pas d'e-mails, aucun résumé ne part donc."
msgid "This unsubscribe link is missing or malformed."
msgstr "Ce lien de désabonnement est absent ou mal formé."
#: src/pages/DumpEdit.tsx:454
#: src/pages/DumpEdit.tsx:455
msgid "Thumbnail"
msgstr "Miniature"
#: src/components/dump/create/TitleField.tsx:45
#: src/components/playlist/PlaylistCreateForm.tsx:96
#: src/components/playlist/PlaylistEditForm.tsx:157
#: src/pages/DumpEdit.tsx:515
#: src/components/playlist/PlaylistEditForm.tsx:159
#: src/pages/DumpEdit.tsx:516
msgid "Title"
msgstr "Titre"
@@ -1861,7 +1862,7 @@ msgstr "Envoi échoué"
msgid "Uploading {0}%"
msgstr "Envoi {0} %"
#: src/components/editor/TextEditor.tsx:566
#: src/components/editor/TextEditor.tsx:595
msgid "Uploading image…"
msgstr "Envoi de l'image…"
@@ -1883,7 +1884,7 @@ msgstr "Voté"
msgid "Upvoted ({0})"
msgstr "Voté ({0})"
#: src/pages/DumpEdit.tsx:486
#: src/pages/DumpEdit.tsx:487
msgid "URL"
msgstr "URL"
@@ -1933,7 +1934,7 @@ msgid "Volume"
msgstr "Volume"
#: src/components/dump/create/DumpCreateModal.tsx:712
#: src/pages/DumpEdit.tsx:543
#: src/pages/DumpEdit.tsx:544
msgid "What makes it worth it?"
msgstr "Pourquoi on en voudrait ?"
@@ -1951,7 +1952,7 @@ msgid "Why & where"
msgstr "Pourquoi & où"
#: src/components/dump/create/DumpCreateModal.tsx:711
#: src/pages/DumpEdit.tsx:542
#: src/pages/DumpEdit.tsx:543
msgid "Why?"
msgstr "Pourquoi ?"

View File

@@ -164,8 +164,7 @@
.dump-post-header,
.dump-rich-content,
.form-page-header,
.dump-edit-panel,
.dump-edit-footer,
.form-panel,
.playlist-detail-header,
.comment-node-inner {
box-shadow: var(--panel-ring), var(--card-shadow);
@@ -3374,7 +3373,7 @@ body.has-player .chat-fab {
}
.form-page--two-col .form-page-header,
.dump-edit-form,
.dump-edit-footer {
.dump-edit-form > .form-panel--bar {
grid-column: 1 / -1;
}
/* "Why?" soaks up whatever height the source column adds, so a tall
@@ -3422,7 +3421,10 @@ body.has-player .chat-fab {
word-break: break-word;
}
.dump-edit-panel {
/* A form split into panels, a few pixels apart (as on the page's grid), in
place of rules between its parts: the fields, then a bar of actions, which
ends the card. */
.form-panel {
display: flex;
flex-direction: column;
gap: 1rem;
@@ -3435,18 +3437,35 @@ body.has-player .chat-fab {
overflow: hidden;
}
.dump-edit-footer {
.form-panel--bar {
padding: 0.875rem 1.25rem;
background: var(--color-surface);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
container: form-bar / inline-size;
}
.dump-edit-footer .form-actions {
.form-panel--bar .form-actions {
flex-wrap: nowrap;
padding-top: 0;
border-top: 0;
margin-top: 0;
}
/* Too narrow for Delete, Cancel and Save side by side in every face and
language, the bar shows Delete as its icon. */
.form-delete-icon {
display: none;
}
@container form-bar (max-width: 18rem) {
.form-delete-icon {
display: inline;
}
.form-delete-label {
display: none;
}
}
.dump-edit-refresh {
margin-top: 0.75rem;
}
@@ -3481,25 +3500,6 @@ body.has-player .chat-fab {
margin-left: auto;
}
/* A destructive action, Cancel and Save don't fit a phone side by side in
every face, and wrapped they would make a staircase. On a phone they stack:
Cancel and Save across the top, Save taking the room left, the destructive
action the full width under them. */
@media (max-width: 512px) {
.form-actions:has(> .btn-danger) {
flex-direction: column-reverse;
align-items: stretch;
}
.form-actions:has(> .btn-danger) > .form-actions-right {
margin-left: 0;
}
.form-actions:has(> .btn-danger) > .form-actions-right > :last-child {
flex: 1;
}
}
/* Subtle text action. Resets the global <button> chrome so it looks the same
whether rendered as a <button> (modal/page forms) or a <Link> (DumpEdit). */
.form-cancel {
@@ -5327,6 +5327,21 @@ body.has-player .chat-fab {
padding: 1.25rem;
}
/* While edited, the header gives way to the form's own panels. */
.playlist-detail-header--editing {
padding: 0;
background: none;
box-shadow: none;
}
.playlist-edit-form {
gap: 3px;
}
.playlist-edit-form > .form-panel:first-child {
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}
.playlist-detail-header-top {
display: flex;
align-items: flex-start;

View File

@@ -99,6 +99,20 @@ interface PendingInsert {
focus: boolean;
}
/**
* Sizes an `autoResize` textarea to its text. A border-box height counts the
* borders, which `scrollHeight` leaves out. Below its `max-height` the box
* holds all its text, so a scrollbar there could only come of sub-pixel
* rounding, and would narrow the text into more lines; overflow is hidden
* then, and left to the stylesheet once the box is capped.
*/
function fitHeight(el: HTMLTextAreaElement): void {
el.style.height = "auto";
const height = el.scrollHeight + el.offsetHeight - el.clientHeight;
el.style.height = `${height}px`;
el.style.overflowY = el.offsetHeight < height ? "" : "hidden";
}
/**
* Renders a textarea that completes `@mentions` and `:emoji`, opens an emoji
* picker from its button, and counts characters near `maxLength`. A pasted or
@@ -274,17 +288,25 @@ export const TextEditor: React.ForwardRefExoticComponent<
// Before paint, so that the first frame, and any layout effect measuring
// it (such as a height transition), sees the final size.
useLayoutEffect(() => {
if (!autoResize) return;
const el = textareaRef.current;
if (!el) return;
el.style.height = "auto";
// A border-box height counts the borders, which scrollHeight leaves
// out; without them the text overflows by a few pixels and scrolls.
el.style.height = `${
el.scrollHeight + el.offsetHeight - el.clientHeight
}px`;
if (autoResize && textareaRef.current) fitHeight(textareaRef.current);
}, [value, autoResize]);
// The text rewraps when the width changes with the value unchanged: when
// the page's scrollbar shows as the editor makes it taller, or the window
// narrows.
useEffect(() => {
const el = textareaRef.current;
if (!autoResize || !el) return;
let width = el.clientWidth;
const observer = new ResizeObserver(() => {
if (el.clientWidth === width) return;
width = el.clientWidth;
fitHeight(el);
});
observer.observe(el);
return () => observer.disconnect();
}, [autoResize]);
// The textarea is read-only while images upload, so the spot they go to
// can't move. They go in once it is writable again.
useLayoutEffect(() => {
@@ -534,6 +556,9 @@ export const TextEditor: React.ForwardRefExoticComponent<
rows={rows}
id={id}
className={className}
// Its height follows the text, so a grip to drag it would only
// be undone at the next keystroke.
style={autoResize ? { resize: "none" } : undefined}
/>
<button
ref={emojiButtonRef}

View File

@@ -0,0 +1,40 @@
/**
* Renders the destructive action of an edit form's bar, in `DumpEdit` and
* `PlaylistEditForm`.
*
* @module
*/
import { Trans } from "@lingui/react/macro";
/** Passed to {@link DeleteButton} by an edit form. */
export interface DeleteButtonProps {
/** Its accessible name, which names what goes, such as "Delete dump". */
label: string;
/** Opens the page's confirmation, which deletes. */
onClick: () => void;
}
/**
* Renders a compact "Delete" button, at the start of the bar's
* `.form-actions`. A bar too narrow for it beside Cancel and Save shows its
* icon instead (`.form-delete-icon`).
*/
export function DeleteButton(
{ label, onClick }: DeleteButtonProps,
): React.JSX.Element {
return (
<button
type="button"
className="btn-danger btn-sm"
aria-label={label}
title={label}
onClick={onClick}
>
<span className="form-delete-icon" aria-hidden>🗑</span>
<span className="form-delete-label">
<Trans>Delete</Trans>
</span>
</button>
);
}

View File

@@ -26,6 +26,7 @@ import { ImagePicker } from "../shared/ImagePicker.tsx";
import { TextField } from "../form/TextField.tsx";
import { RichTextField } from "../form/RichTextField.tsx";
import { VisibilityToggle } from "../form/SegmentedField.tsx";
import { DeleteButton } from "../form/DeleteButton.tsx";
import { FormError } from "../form/FormError.tsx";
import { SubmitButton } from "../form/SubmitButton.tsx";
import { SlugField } from "../form/SlugField.tsx";
@@ -121,84 +122,90 @@ export function PlaylistEditForm(
return (
<FormProvider {...form}>
<form
className="form animate-in"
className="form playlist-edit-form animate-in"
onSubmit={onSubmit}
onKeyDown={(e) => {
// The editor's popups take their own Escape and prevent it.
if (e.key === "Escape" && !e.defaultPrevented) onCancel();
}}
>
<div className="form-field">
<span className="form-label">
<Trans>Cover</Trans>
</span>
<Controller
name="image"
control={form.control}
render={({ field }) => (
<ImagePicker
src={imagePreview ??
(playlist.imageMime
? playlistImageUrl(playlist, mediaToken)
: null)}
alt="Cover"
size={72}
onChange={(file) => {
field.onChange(file);
setImagePreview(URL.createObjectURL(file));
}}
/>
)}
<div className="form-panel">
<div className="form-field">
<span className="form-label">
<Trans>Cover</Trans>
</span>
<Controller
name="image"
control={form.control}
render={({ field }) => (
<ImagePicker
src={imagePreview ??
(playlist.imageMime
? playlistImageUrl(playlist, mediaToken)
: null)}
alt="Cover"
size={72}
onChange={(file) => {
field.onChange(file);
setImagePreview(URL.createObjectURL(file));
}}
/>
)}
/>
</div>
<TextField<EditValues>
name="title"
label={t`Title`}
autoFocus
rules={{ required: true }}
maxLength={VALIDATION.PLAYLIST_TITLE_MAX}
showCount
/>
<SlugField<EditValues>
name="slug"
titleName="title"
prefix={`${userPath(playlist.ownerUsername, "playlists")}/`}
saved={playlist}
/>
<RichTextField<EditValues>
name="description"
label={t`Description`}
rows={2}
autoResize
maxLength={VALIDATION.PLAYLIST_DESCRIPTION_MAX}
onSubmit={() => void onSubmit()}
/>
<VisibilityToggle<EditValues> name="isPublic" />
<FormError title={t`Failed to save`} />
</div>
<TextField<EditValues>
name="title"
label={t`Title`}
autoFocus
rules={{ required: true }}
maxLength={VALIDATION.PLAYLIST_TITLE_MAX}
showCount
/>
<SlugField<EditValues>
name="slug"
titleName="title"
prefix={`${userPath(playlist.ownerUsername, "playlists")}/`}
saved={playlist}
/>
<RichTextField<EditValues>
name="description"
label={t`Description`}
rows={2}
autoResize
maxLength={VALIDATION.PLAYLIST_DESCRIPTION_MAX}
onSubmit={() => void onSubmit()}
/>
<VisibilityToggle<EditValues> name="isPublic" />
<FormError title={t`Failed to save`} />
<div className="form-actions">
<button
type="button"
className="btn-danger"
onClick={onRequestDelete}
>
<Trans>Delete</Trans>
</button>
<div className="form-actions-right">
<button type="button" className="form-cancel" onClick={onCancel}>
<Trans>Cancel</Trans>
</button>
<SubmitButton
pendingLabel={<Trans>Saving…</Trans>}
disabled={!title.trim()}
>
<Trans>Save</Trans>
</SubmitButton>
<div className="form-panel form-panel--bar">
<div className="form-actions">
<DeleteButton
label={t`Delete playlist`}
onClick={onRequestDelete}
/>
<div className="form-actions-right">
<button
type="button"
className="form-cancel"
onClick={onCancel}
>
<Trans>Cancel</Trans>
</button>
<SubmitButton
className="btn-primary btn-sm"
pendingLabel={<Trans>Saving…</Trans>}
disabled={!title.trim()}
>
<Trans>Save</Trans>
</SubmitButton>
</div>
</div>
</div>
</form>

View File

@@ -47,6 +47,7 @@ import { SubmitButton } from "../components/form/SubmitButton.tsx";
import { TextField } from "../components/form/TextField.tsx";
import { useApiForm } from "../hooks/form/useApiForm.ts";
import { VisibilityToggle } from "../components/form/SegmentedField.tsx";
import { DeleteButton } from "../components/form/DeleteButton.tsx";
import { SlugField } from "../components/form/SlugField.tsx";
import { useCanonicalPath } from "../hooks/shared/useCanonicalPath.ts";
import { userPath } from "../../shared/paths.ts";
@@ -446,7 +447,7 @@ function DumpEditForm({
/* The source panel holds what was dumped, the details panel how it
is presented and who sees it. */
}
<div className="dump-edit-panel dump-edit-panel--source">
<div className="form-panel dump-edit-panel--source">
{preview}
<div className="form-field">
@@ -507,7 +508,7 @@ function DumpEditForm({
)}
</div>
<div className="dump-edit-panel dump-edit-panel--details">
<div className="form-panel dump-edit-panel--details">
<FormError title={t`Could not save`} />
<TextField<EditValues>
@@ -553,23 +554,17 @@ function DumpEditForm({
<VisibilityToggle<EditValues> name="isPublic" />
</div>
<div className="dump-edit-footer">
<div className="form-panel form-panel--bar">
{deleteError && (
<ErrorCard title={t`Could not delete`} message={deleteError} />
)}
<div className="form-actions">
<button
type="button"
onClick={onRequestDelete}
className="btn-danger"
>
<Trans>Delete dump</Trans>
</button>
<DeleteButton label={t`Delete dump`} onClick={onRequestDelete} />
<div className="form-actions-right">
<Link to={dumpUrl(dump)} className="form-cancel">
<Trans>Cancel</Trans>
</Link>
<SubmitButton>
<SubmitButton className="btn-primary btn-sm">
<Trans>Save</Trans>
</SubmitButton>
</div>

View File

@@ -90,7 +90,12 @@ export function PlaylistDetail(): React.JSX.Element {
return (
<PageShell>
<div className="playlist-detail-header" ref={headerRef}>
<div
className={`playlist-detail-header${
editOpen ? " playlist-detail-header--editing" : ""
}`}
ref={headerRef}
>
{editOpen
? (
<PlaylistEditForm

View File

@@ -366,7 +366,7 @@ describe("PlaylistDetail", () => {
);
const { user } = open();
await user.click(await screen.findByRole("button", { name: "Edit" }));
await user.click(screen.getByRole("button", { name: "Delete" }));
await user.click(screen.getByRole("button", { name: "Delete playlist" }));
await user.click(
within(screen.getByRole("alertdialog")).getByRole("button", {
name: "Delete playlist",
@@ -379,7 +379,7 @@ describe("PlaylistDetail", () => {
route("DELETE /api/playlists/:id", () => fail("Forbidden", 403));
const { user } = open();
await user.click(await screen.findByRole("button", { name: "Edit" }));
await user.click(screen.getByRole("button", { name: "Delete" }));
await user.click(screen.getByRole("button", { name: "Delete playlist" }));
await user.click(
within(screen.getByRole("alertdialog")).getByRole("button", {
name: "Delete playlist",