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:
107
locales/en.po
107
locales/en.po
@@ -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?"
|
||||
|
||||
|
||||
107
locales/fr.po
107
locales/fr.po
@@ -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 ?"
|
||||
|
||||
|
||||
67
src/App.css
67
src/App.css
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
40
src/components/form/DeleteButton.tsx
Normal file
40
src/components/form/DeleteButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user