v4: the avatar menu shows at every width and holds Log out, so a profile keeps only Invite; the profile's dump cards lay out again — your own profile had four buttons in a row, Invite someone, Settings, Manage and Log out, Invite taller than the rest, and on wide screens it was the only way to reach /settings or /manage. The avatar menu now leads the header's icon row at every width, after the name and Playlists text links on wide screens, and ends with Log out under a separator, muted until hovered. The profile keeps Invite someone alone. The menu's button outweighs .app-header-nav button and the themes' button rules (Brutalist and GeoCities list it among their flat buttons), so it reads as one of the menu's links. Since b9ff9c4, a card under 472px is laid out by the narrow-card container query, but the profile columns' dense-card rules still placed the thumb, body and vote themselves, more specifically: the thumb lost its row span, so the title fell under it and every card was a tall, mostly empty box. Those rules now only make the card denser (a smaller thumb, a 2-line title, smaller type) and leave placement to the container query. 670 frontend tests pass, and tsc, eslint, deno fmt, doc:lint and i18n:compile --strict are clean; checked in screenshots in Smooth dark at 1400 and 390px, Brutalist light at 390px, GeoCities light and Kanagawa dark at 1400px, in French.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -431,7 +431,7 @@ msgstr "← Back"
|
|||||||
msgid "← Back to all dumps"
|
msgid "← Back to all dumps"
|
||||||
msgstr "← Back to all dumps"
|
msgstr "← Back to all dumps"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:80
|
#: src/components/profile/ProfileOwnActions.tsx:76
|
||||||
msgid "+ Invite someone"
|
msgid "+ Invite someone"
|
||||||
msgstr "+ Invite someone"
|
msgstr "+ Invite someone"
|
||||||
|
|
||||||
@@ -518,11 +518,11 @@ msgstr "Add a comment…"
|
|||||||
msgid "Add category"
|
msgid "Add category"
|
||||||
msgstr "Add category"
|
msgstr "Add category"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:154
|
#: src/components/profile/ProfileHeader.tsx:153
|
||||||
msgid "Add email…"
|
msgid "Add email…"
|
||||||
msgstr "Add email…"
|
msgstr "Add email…"
|
||||||
|
|
||||||
#: src/components/shared/ImagePicker.tsx:63
|
#: src/components/shared/ImagePicker.tsx:66
|
||||||
msgid "Add image"
|
msgid "Add image"
|
||||||
msgstr "Add image"
|
msgstr "Add image"
|
||||||
|
|
||||||
@@ -589,7 +589,7 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
|
|||||||
#: src/components/comment/CommentForm.tsx:127
|
#: src/components/comment/CommentForm.tsx:127
|
||||||
#: src/components/dump/create/DumpCreateModal.tsx:752
|
#: src/components/dump/create/DumpCreateModal.tsx:752
|
||||||
#: src/components/form/FormActions.tsx:42
|
#: src/components/form/FormActions.tsx:42
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:199
|
#: src/components/playlist/PlaylistEditForm.tsx:200
|
||||||
#: src/components/profile/DescriptionEditor.tsx:97
|
#: src/components/profile/DescriptionEditor.tsx:97
|
||||||
#: src/components/profile/EmailEditor.tsx:83
|
#: src/components/profile/EmailEditor.tsx:83
|
||||||
#: src/components/shared/ConfirmModal.tsx:63
|
#: src/components/shared/ConfirmModal.tsx:63
|
||||||
@@ -607,7 +607,7 @@ msgstr "Cancel removal"
|
|||||||
msgid "Cancel reply"
|
msgid "Cancel reply"
|
||||||
msgstr "Cancel reply"
|
msgstr "Cancel reply"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:190
|
#: src/components/layout/AppHeader.tsx:188
|
||||||
msgid "Cancel search"
|
msgid "Cancel search"
|
||||||
msgstr "Cancel search"
|
msgstr "Cancel search"
|
||||||
|
|
||||||
@@ -620,11 +620,11 @@ msgstr "Categories"
|
|||||||
msgid "Category"
|
msgid "Category"
|
||||||
msgstr "Category"
|
msgstr "Category"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:96
|
#: src/components/profile/ProfileHeader.tsx:95
|
||||||
msgid "Change avatar"
|
msgid "Change avatar"
|
||||||
msgstr "Change avatar"
|
msgstr "Change avatar"
|
||||||
|
|
||||||
#: src/components/shared/ImagePicker.tsx:63
|
#: src/components/shared/ImagePicker.tsx:66
|
||||||
msgid "Change image"
|
msgid "Change image"
|
||||||
msgstr "Change image"
|
msgstr "Change image"
|
||||||
|
|
||||||
@@ -695,11 +695,11 @@ msgstr "Confirm new password"
|
|||||||
msgid "Connection lost — this message may not have been sent"
|
msgid "Connection lost — this message may not have been sent"
|
||||||
msgstr "Connection lost — this message may not have been sent"
|
msgstr "Connection lost — this message may not have been sent"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:105
|
#: src/components/profile/ProfileOwnActions.tsx:99
|
||||||
msgid "Copied!"
|
msgid "Copied!"
|
||||||
msgstr "Copied!"
|
msgstr "Copied!"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:105
|
#: src/components/profile/ProfileOwnActions.tsx:99
|
||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
msgstr "Copy"
|
msgstr "Copy"
|
||||||
|
|
||||||
@@ -800,7 +800,7 @@ msgid "Delete failed ({0})"
|
|||||||
msgstr "Delete failed ({0})"
|
msgstr "Delete failed ({0})"
|
||||||
|
|
||||||
#: src/components/playlist/PlaylistCard.tsx:142
|
#: src/components/playlist/PlaylistCard.tsx:142
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:190
|
#: src/components/playlist/PlaylistEditForm.tsx:191
|
||||||
#: src/components/profile/ProfilePlaylistsTab.tsx:156
|
#: src/components/profile/ProfilePlaylistsTab.tsx:156
|
||||||
#: src/pages/PlaylistDetail.tsx:141
|
#: src/pages/PlaylistDetail.tsx:141
|
||||||
msgid "Delete playlist"
|
msgid "Delete playlist"
|
||||||
@@ -827,7 +827,7 @@ msgstr "Delete this playlist? This cannot be undone."
|
|||||||
msgid "deleted message"
|
msgid "deleted message"
|
||||||
msgstr "deleted message"
|
msgstr "deleted message"
|
||||||
|
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:175
|
#: src/components/playlist/PlaylistEditForm.tsx:176
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Description"
|
msgstr "Description"
|
||||||
|
|
||||||
@@ -869,7 +869,7 @@ msgstr "Drop a file here, or paste one"
|
|||||||
msgid "Drop a replacement here"
|
msgid "Drop a replacement here"
|
||||||
msgstr "Drop a replacement here"
|
msgstr "Drop a replacement here"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:209
|
#: src/components/layout/AppHeader.tsx:207
|
||||||
msgid "Dump"
|
msgid "Dump"
|
||||||
msgstr "Dump"
|
msgstr "Dump"
|
||||||
|
|
||||||
@@ -914,7 +914,7 @@ msgstr "Edit {0}"
|
|||||||
msgid "Edit description"
|
msgid "Edit description"
|
||||||
msgstr "Edit description"
|
msgstr "Edit description"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:152
|
#: src/components/profile/ProfileHeader.tsx:151
|
||||||
msgid "Edit email"
|
msgid "Edit email"
|
||||||
msgstr "Edit email"
|
msgstr "Edit email"
|
||||||
|
|
||||||
@@ -984,9 +984,9 @@ msgstr "Failed to create playlist"
|
|||||||
msgid "Failed to delete"
|
msgid "Failed to delete"
|
||||||
msgstr "Failed to delete"
|
msgstr "Failed to delete"
|
||||||
|
|
||||||
|
#: src/components/profile/ProfileOwnActions.tsx:46
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:49
|
#: src/components/profile/ProfileOwnActions.tsx:49
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:52
|
#: src/components/profile/ProfileOwnActions.tsx:108
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:114
|
|
||||||
msgid "Failed to generate invite"
|
msgid "Failed to generate invite"
|
||||||
msgstr "Failed to generate invite"
|
msgstr "Failed to generate invite"
|
||||||
|
|
||||||
@@ -1006,7 +1006,7 @@ msgstr "Failed to post comment"
|
|||||||
msgid "Failed to post reply"
|
msgid "Failed to post reply"
|
||||||
msgstr "Failed to post reply"
|
msgstr "Failed to post reply"
|
||||||
|
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:184
|
#: src/components/playlist/PlaylistEditForm.tsx:185
|
||||||
#: src/components/profile/DescriptionEditor.tsx:99
|
#: src/components/profile/DescriptionEditor.tsx:99
|
||||||
#: src/components/profile/EmailEditor.tsx:86
|
#: src/components/profile/EmailEditor.tsx:86
|
||||||
msgid "Failed to save"
|
msgid "Failed to save"
|
||||||
@@ -1020,7 +1020,7 @@ msgstr "Failed to save category"
|
|||||||
msgid "Failed to save edit"
|
msgid "Failed to save edit"
|
||||||
msgstr "Failed to save edit"
|
msgstr "Failed to save edit"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:163
|
#: src/components/profile/ProfileHeader.tsx:162
|
||||||
msgid "Failed to update avatar"
|
msgid "Failed to update avatar"
|
||||||
msgstr "Failed to update avatar"
|
msgstr "Failed to update avatar"
|
||||||
|
|
||||||
@@ -1128,12 +1128,12 @@ msgstr "Invalid invite"
|
|||||||
msgid "Invalid link"
|
msgid "Invalid link"
|
||||||
msgstr "Invalid link"
|
msgstr "Invalid link"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:88
|
#: src/components/profile/ProfileOwnActions.tsx:82
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:95
|
#: src/components/profile/ProfileOwnActions.tsx:89
|
||||||
msgid "Invite link"
|
msgid "Invite link"
|
||||||
msgstr "Invite link"
|
msgstr "Invite link"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:117
|
#: src/components/profile/ProfileHeader.tsx:116
|
||||||
msgid "invited by"
|
msgid "invited by"
|
||||||
msgstr "invited by"
|
msgstr "invited by"
|
||||||
|
|
||||||
@@ -1176,7 +1176,7 @@ msgstr "Link or file"
|
|||||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||||
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:242
|
#: src/components/layout/AppHeader.tsx:240
|
||||||
msgid "Live updates unavailable."
|
msgid "Live updates unavailable."
|
||||||
msgstr "Live updates unavailable."
|
msgstr "Live updates unavailable."
|
||||||
|
|
||||||
@@ -1219,7 +1219,7 @@ msgstr "Loading profile…"
|
|||||||
msgid "Loading…"
|
msgid "Loading…"
|
||||||
msgstr "Loading…"
|
msgstr "Loading…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:216
|
#: src/components/layout/AppHeader.tsx:214
|
||||||
#: src/pages/UserLogin.tsx:41
|
#: src/pages/UserLogin.tsx:41
|
||||||
#: src/pages/UserLogin.tsx:67
|
#: src/pages/UserLogin.tsx:67
|
||||||
#: src/pages/UserLogin.tsx:86
|
#: src/pages/UserLogin.tsx:86
|
||||||
@@ -1234,7 +1234,7 @@ msgstr "Log in to like"
|
|||||||
msgid "Log in to vote"
|
msgid "Log in to vote"
|
||||||
msgstr "Log in to vote"
|
msgstr "Log in to vote"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:188
|
#: src/components/layout/UserMenu.tsx:115
|
||||||
#: src/pages/UserPublicProfile.tsx:131
|
#: src/pages/UserPublicProfile.tsx:131
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr "Log out"
|
msgstr "Log out"
|
||||||
@@ -1251,8 +1251,7 @@ msgstr "Login failed"
|
|||||||
msgid "Lowercase letters, numbers and single hyphens only"
|
msgid "Lowercase letters, numbers and single hyphens only"
|
||||||
msgstr "Lowercase letters, numbers and single hyphens only"
|
msgstr "Lowercase letters, numbers and single hyphens only"
|
||||||
|
|
||||||
#: src/components/layout/UserMenu.tsx:98
|
#: src/components/layout/UserMenu.tsx:102
|
||||||
#: src/components/profile/ProfileHeader.tsx:180
|
|
||||||
#: src/pages/Manage.tsx:24
|
#: src/pages/Manage.tsx:24
|
||||||
#: src/pages/Manage.tsx:29
|
#: src/pages/Manage.tsx:29
|
||||||
#: src/pages/UserPublicProfile.tsx:164
|
#: src/pages/UserPublicProfile.tsx:164
|
||||||
@@ -1465,7 +1464,7 @@ msgstr "Play"
|
|||||||
|
|
||||||
#: src/components/dump/create/StepIndicator.tsx:21
|
#: src/components/dump/create/StepIndicator.tsx:21
|
||||||
#: src/components/layout/AppHeader.tsx:178
|
#: src/components/layout/AppHeader.tsx:178
|
||||||
#: src/components/layout/UserMenu.tsx:81
|
#: src/components/layout/UserMenu.tsx:85
|
||||||
#: src/components/profile/ProfileFollowTab.tsx:167
|
#: src/components/profile/ProfileFollowTab.tsx:167
|
||||||
#: src/components/profile/ProfilePlaylistsTab.tsx:118
|
#: src/components/profile/ProfilePlaylistsTab.tsx:118
|
||||||
#: src/pages/Search.tsx:123
|
#: src/pages/Search.tsx:123
|
||||||
@@ -1616,7 +1615,7 @@ msgstr "Role"
|
|||||||
#: src/components/category/CategoryManager.tsx:216
|
#: src/components/category/CategoryManager.tsx:216
|
||||||
#: src/components/chat/ChatEditBox.tsx:85
|
#: src/components/chat/ChatEditBox.tsx:85
|
||||||
#: src/components/comment/CommentNode.tsx:216
|
#: src/components/comment/CommentNode.tsx:216
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:206
|
#: src/components/playlist/PlaylistEditForm.tsx:207
|
||||||
#: src/components/profile/DescriptionEditor.tsx:89
|
#: src/components/profile/DescriptionEditor.tsx:89
|
||||||
#: src/components/profile/EmailEditor.tsx:75
|
#: src/components/profile/EmailEditor.tsx:75
|
||||||
#: src/pages/Dump.tsx:437
|
#: src/pages/Dump.tsx:437
|
||||||
@@ -1625,7 +1624,7 @@ msgid "Save"
|
|||||||
msgstr "Save"
|
msgstr "Save"
|
||||||
|
|
||||||
#: src/components/comment/CommentNode.tsx:217
|
#: src/components/comment/CommentNode.tsx:217
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:203
|
#: src/components/playlist/PlaylistEditForm.tsx:204
|
||||||
#: src/components/profile/ChangePasswordModal.tsx:119
|
#: src/components/profile/ChangePasswordModal.tsx:119
|
||||||
#: src/components/profile/DescriptionEditor.tsx:86
|
#: src/components/profile/DescriptionEditor.tsx:86
|
||||||
#: src/components/profile/EmailEditor.tsx:72
|
#: src/components/profile/EmailEditor.tsx:72
|
||||||
@@ -1634,7 +1633,7 @@ msgstr "Save"
|
|||||||
msgid "Saving…"
|
msgid "Saving…"
|
||||||
msgstr "Saving…"
|
msgstr "Saving…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:190
|
#: src/components/layout/AppHeader.tsx:188
|
||||||
#: src/components/layout/SearchBar.tsx:83
|
#: src/components/layout/SearchBar.tsx:83
|
||||||
#: src/pages/Search.tsx:69
|
#: src/pages/Search.tsx:69
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
@@ -1677,7 +1676,7 @@ msgstr "Send reset link"
|
|||||||
msgid "Sending…"
|
msgid "Sending…"
|
||||||
msgstr "Sending…"
|
msgstr "Sending…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:206
|
#: src/components/layout/AppHeader.tsx:204
|
||||||
msgid "Server unreachable"
|
msgid "Server unreachable"
|
||||||
msgstr "Server unreachable"
|
msgstr "Server unreachable"
|
||||||
|
|
||||||
@@ -1686,8 +1685,7 @@ msgstr "Server unreachable"
|
|||||||
msgid "Set new password"
|
msgid "Set new password"
|
||||||
msgstr "Set new password"
|
msgstr "Set new password"
|
||||||
|
|
||||||
#: src/components/layout/UserMenu.tsx:89
|
#: src/components/layout/UserMenu.tsx:93
|
||||||
#: src/components/profile/ProfileHeader.tsx:176
|
|
||||||
#: src/pages/Settings.tsx:70
|
#: src/pages/Settings.tsx:70
|
||||||
#: src/pages/Settings.tsx:82
|
#: src/pages/Settings.tsx:82
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
@@ -1697,7 +1695,7 @@ msgstr "Settings"
|
|||||||
msgid "Several people are typing…"
|
msgid "Several people are typing…"
|
||||||
msgstr "Several people are typing…"
|
msgstr "Several people are typing…"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:108
|
#: src/components/profile/ProfileOwnActions.tsx:102
|
||||||
msgid "Single use, valid for 7 days."
|
msgid "Single use, valid for 7 days."
|
||||||
msgstr "Single use, valid for 7 days."
|
msgstr "Single use, valid for 7 days."
|
||||||
|
|
||||||
@@ -1786,7 +1784,7 @@ msgstr "Thumbnail"
|
|||||||
|
|
||||||
#: src/components/dump/create/TitleField.tsx:45
|
#: src/components/dump/create/TitleField.tsx:45
|
||||||
#: src/components/playlist/PlaylistCreateForm.tsx:96
|
#: src/components/playlist/PlaylistCreateForm.tsx:96
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:159
|
#: src/components/playlist/PlaylistEditForm.tsx:160
|
||||||
#: src/pages/DumpEdit.tsx:516
|
#: src/pages/DumpEdit.tsx:516
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr "Title"
|
msgstr "Title"
|
||||||
@@ -1853,7 +1851,7 @@ msgstr "Unsubscribe"
|
|||||||
msgid "Unsubscribed"
|
msgid "Unsubscribed"
|
||||||
msgstr "Unsubscribed"
|
msgstr "Unsubscribed"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:73
|
#: src/components/profile/ProfileHeader.tsx:72
|
||||||
msgid "Upload failed"
|
msgid "Upload failed"
|
||||||
msgstr "Upload failed"
|
msgstr "Upload failed"
|
||||||
|
|
||||||
@@ -1904,7 +1902,7 @@ msgstr "User"
|
|||||||
msgid "User management"
|
msgid "User management"
|
||||||
msgstr "User management"
|
msgstr "User management"
|
||||||
|
|
||||||
#: src/components/layout/UserMenu.tsx:56
|
#: src/components/layout/UserMenu.tsx:60
|
||||||
msgid "User menu"
|
msgid "User menu"
|
||||||
msgstr "User menu"
|
msgstr "User menu"
|
||||||
|
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ msgstr "← Retour"
|
|||||||
msgid "← Back to all dumps"
|
msgid "← Back to all dumps"
|
||||||
msgstr "← Retour à toutes les recos"
|
msgstr "← Retour à toutes les recos"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:80
|
#: src/components/profile/ProfileOwnActions.tsx:76
|
||||||
msgid "+ Invite someone"
|
msgid "+ Invite someone"
|
||||||
msgstr "+ Inviter quelqu'un"
|
msgstr "+ Inviter quelqu'un"
|
||||||
|
|
||||||
@@ -518,11 +518,11 @@ msgstr "Ajouter un commentaire…"
|
|||||||
msgid "Add category"
|
msgid "Add category"
|
||||||
msgstr "Ajouter une catégorie"
|
msgstr "Ajouter une catégorie"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:154
|
#: src/components/profile/ProfileHeader.tsx:153
|
||||||
msgid "Add email…"
|
msgid "Add email…"
|
||||||
msgstr "Ajouter un e-mail…"
|
msgstr "Ajouter un e-mail…"
|
||||||
|
|
||||||
#: src/components/shared/ImagePicker.tsx:63
|
#: src/components/shared/ImagePicker.tsx:66
|
||||||
msgid "Add image"
|
msgid "Add image"
|
||||||
msgstr "Ajouter une image"
|
msgstr "Ajouter une image"
|
||||||
|
|
||||||
@@ -589,7 +589,7 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
|
|||||||
#: src/components/comment/CommentForm.tsx:127
|
#: src/components/comment/CommentForm.tsx:127
|
||||||
#: src/components/dump/create/DumpCreateModal.tsx:752
|
#: src/components/dump/create/DumpCreateModal.tsx:752
|
||||||
#: src/components/form/FormActions.tsx:42
|
#: src/components/form/FormActions.tsx:42
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:199
|
#: src/components/playlist/PlaylistEditForm.tsx:200
|
||||||
#: src/components/profile/DescriptionEditor.tsx:97
|
#: src/components/profile/DescriptionEditor.tsx:97
|
||||||
#: src/components/profile/EmailEditor.tsx:83
|
#: src/components/profile/EmailEditor.tsx:83
|
||||||
#: src/components/shared/ConfirmModal.tsx:63
|
#: src/components/shared/ConfirmModal.tsx:63
|
||||||
@@ -607,7 +607,7 @@ msgstr "Annuler la suppression"
|
|||||||
msgid "Cancel reply"
|
msgid "Cancel reply"
|
||||||
msgstr "Annuler la réponse"
|
msgstr "Annuler la réponse"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:190
|
#: src/components/layout/AppHeader.tsx:188
|
||||||
msgid "Cancel search"
|
msgid "Cancel search"
|
||||||
msgstr "Annuler la recherche"
|
msgstr "Annuler la recherche"
|
||||||
|
|
||||||
@@ -620,11 +620,11 @@ msgstr "Catégories"
|
|||||||
msgid "Category"
|
msgid "Category"
|
||||||
msgstr "Catégorie"
|
msgstr "Catégorie"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:96
|
#: src/components/profile/ProfileHeader.tsx:95
|
||||||
msgid "Change avatar"
|
msgid "Change avatar"
|
||||||
msgstr "Changer l'avatar"
|
msgstr "Changer l'avatar"
|
||||||
|
|
||||||
#: src/components/shared/ImagePicker.tsx:63
|
#: src/components/shared/ImagePicker.tsx:66
|
||||||
msgid "Change image"
|
msgid "Change image"
|
||||||
msgstr "Changer l'image"
|
msgstr "Changer l'image"
|
||||||
|
|
||||||
@@ -695,11 +695,11 @@ msgstr "Confirmer le nouveau mot de passe"
|
|||||||
msgid "Connection lost — this message may not have been sent"
|
msgid "Connection lost — this message may not have been sent"
|
||||||
msgstr "Connexion perdue — ce message n'a peut-être pas été envoyé"
|
msgstr "Connexion perdue — ce message n'a peut-être pas été envoyé"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:105
|
#: src/components/profile/ProfileOwnActions.tsx:99
|
||||||
msgid "Copied!"
|
msgid "Copied!"
|
||||||
msgstr "Copié !"
|
msgstr "Copié !"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:105
|
#: src/components/profile/ProfileOwnActions.tsx:99
|
||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
msgstr "Copier"
|
msgstr "Copier"
|
||||||
|
|
||||||
@@ -800,7 +800,7 @@ msgid "Delete failed ({0})"
|
|||||||
msgstr "Échec de la suppression ({0})"
|
msgstr "Échec de la suppression ({0})"
|
||||||
|
|
||||||
#: src/components/playlist/PlaylistCard.tsx:142
|
#: src/components/playlist/PlaylistCard.tsx:142
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:190
|
#: src/components/playlist/PlaylistEditForm.tsx:191
|
||||||
#: src/components/profile/ProfilePlaylistsTab.tsx:156
|
#: src/components/profile/ProfilePlaylistsTab.tsx:156
|
||||||
#: src/pages/PlaylistDetail.tsx:141
|
#: src/pages/PlaylistDetail.tsx:141
|
||||||
msgid "Delete playlist"
|
msgid "Delete playlist"
|
||||||
@@ -827,7 +827,7 @@ msgstr "Supprimer cette collection ? Cette action est irréversible."
|
|||||||
msgid "deleted message"
|
msgid "deleted message"
|
||||||
msgstr "message supprimé"
|
msgstr "message supprimé"
|
||||||
|
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:175
|
#: src/components/playlist/PlaylistEditForm.tsx:176
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Description"
|
msgstr "Description"
|
||||||
|
|
||||||
@@ -869,7 +869,7 @@ msgstr "Déposez un fichier ici, ou collez-en un"
|
|||||||
msgid "Drop a replacement here"
|
msgid "Drop a replacement here"
|
||||||
msgstr "Déposez un fichier de remplacement ici"
|
msgstr "Déposez un fichier de remplacement ici"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:209
|
#: src/components/layout/AppHeader.tsx:207
|
||||||
msgid "Dump"
|
msgid "Dump"
|
||||||
msgstr "Reco"
|
msgstr "Reco"
|
||||||
|
|
||||||
@@ -914,7 +914,7 @@ msgstr "Modifier {0}"
|
|||||||
msgid "Edit description"
|
msgid "Edit description"
|
||||||
msgstr "Modifier la description"
|
msgstr "Modifier la description"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:152
|
#: src/components/profile/ProfileHeader.tsx:151
|
||||||
msgid "Edit email"
|
msgid "Edit email"
|
||||||
msgstr "Modifier l'e-mail"
|
msgstr "Modifier l'e-mail"
|
||||||
|
|
||||||
@@ -984,9 +984,9 @@ msgstr "Impossible de créer la collection"
|
|||||||
msgid "Failed to delete"
|
msgid "Failed to delete"
|
||||||
msgstr "Suppression échouée"
|
msgstr "Suppression échouée"
|
||||||
|
|
||||||
|
#: src/components/profile/ProfileOwnActions.tsx:46
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:49
|
#: src/components/profile/ProfileOwnActions.tsx:49
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:52
|
#: src/components/profile/ProfileOwnActions.tsx:108
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:114
|
|
||||||
msgid "Failed to generate invite"
|
msgid "Failed to generate invite"
|
||||||
msgstr "Impossible de générer une invitation"
|
msgstr "Impossible de générer une invitation"
|
||||||
|
|
||||||
@@ -1006,7 +1006,7 @@ msgstr "Impossible de publier le commentaire"
|
|||||||
msgid "Failed to post reply"
|
msgid "Failed to post reply"
|
||||||
msgstr "Impossible de publier la réponse"
|
msgstr "Impossible de publier la réponse"
|
||||||
|
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:184
|
#: src/components/playlist/PlaylistEditForm.tsx:185
|
||||||
#: src/components/profile/DescriptionEditor.tsx:99
|
#: src/components/profile/DescriptionEditor.tsx:99
|
||||||
#: src/components/profile/EmailEditor.tsx:86
|
#: src/components/profile/EmailEditor.tsx:86
|
||||||
msgid "Failed to save"
|
msgid "Failed to save"
|
||||||
@@ -1020,7 +1020,7 @@ msgstr "Échec de l’enregistrement de la catégorie"
|
|||||||
msgid "Failed to save edit"
|
msgid "Failed to save edit"
|
||||||
msgstr "Impossible d'enregistrer la modification"
|
msgstr "Impossible d'enregistrer la modification"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:163
|
#: src/components/profile/ProfileHeader.tsx:162
|
||||||
msgid "Failed to update avatar"
|
msgid "Failed to update avatar"
|
||||||
msgstr "Impossible de mettre à jour l'avatar"
|
msgstr "Impossible de mettre à jour l'avatar"
|
||||||
|
|
||||||
@@ -1128,12 +1128,12 @@ msgstr "Invitation invalide"
|
|||||||
msgid "Invalid link"
|
msgid "Invalid link"
|
||||||
msgstr "Lien invalide"
|
msgstr "Lien invalide"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:88
|
#: src/components/profile/ProfileOwnActions.tsx:82
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:95
|
#: src/components/profile/ProfileOwnActions.tsx:89
|
||||||
msgid "Invite link"
|
msgid "Invite link"
|
||||||
msgstr "Lien d'invitation"
|
msgstr "Lien d'invitation"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:117
|
#: src/components/profile/ProfileHeader.tsx:116
|
||||||
msgid "invited by"
|
msgid "invited by"
|
||||||
msgstr "invité par"
|
msgstr "invité par"
|
||||||
|
|
||||||
@@ -1176,7 +1176,7 @@ msgstr "Lien ou fichier"
|
|||||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||||
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
|
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:242
|
#: src/components/layout/AppHeader.tsx:240
|
||||||
msgid "Live updates unavailable."
|
msgid "Live updates unavailable."
|
||||||
msgstr "Mises à jour en direct indisponibles."
|
msgstr "Mises à jour en direct indisponibles."
|
||||||
|
|
||||||
@@ -1219,7 +1219,7 @@ msgstr "Chargement du profil…"
|
|||||||
msgid "Loading…"
|
msgid "Loading…"
|
||||||
msgstr "Chargement…"
|
msgstr "Chargement…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:216
|
#: src/components/layout/AppHeader.tsx:214
|
||||||
#: src/pages/UserLogin.tsx:41
|
#: src/pages/UserLogin.tsx:41
|
||||||
#: src/pages/UserLogin.tsx:67
|
#: src/pages/UserLogin.tsx:67
|
||||||
#: src/pages/UserLogin.tsx:86
|
#: src/pages/UserLogin.tsx:86
|
||||||
@@ -1234,7 +1234,7 @@ msgstr "Se connecter pour aimer"
|
|||||||
msgid "Log in to vote"
|
msgid "Log in to vote"
|
||||||
msgstr "Se connecter pour voter"
|
msgstr "Se connecter pour voter"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:188
|
#: src/components/layout/UserMenu.tsx:115
|
||||||
#: src/pages/UserPublicProfile.tsx:131
|
#: src/pages/UserPublicProfile.tsx:131
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr "Se déconnecter"
|
msgstr "Se déconnecter"
|
||||||
@@ -1251,8 +1251,7 @@ msgstr "Connexion échouée"
|
|||||||
msgid "Lowercase letters, numbers and single hyphens only"
|
msgid "Lowercase letters, numbers and single hyphens only"
|
||||||
msgstr "Minuscules, chiffres et tirets simples seulement"
|
msgstr "Minuscules, chiffres et tirets simples seulement"
|
||||||
|
|
||||||
#: src/components/layout/UserMenu.tsx:98
|
#: src/components/layout/UserMenu.tsx:102
|
||||||
#: src/components/profile/ProfileHeader.tsx:180
|
|
||||||
#: src/pages/Manage.tsx:24
|
#: src/pages/Manage.tsx:24
|
||||||
#: src/pages/Manage.tsx:29
|
#: src/pages/Manage.tsx:29
|
||||||
#: src/pages/UserPublicProfile.tsx:164
|
#: src/pages/UserPublicProfile.tsx:164
|
||||||
@@ -1465,7 +1464,7 @@ msgstr "Lecture"
|
|||||||
|
|
||||||
#: src/components/dump/create/StepIndicator.tsx:21
|
#: src/components/dump/create/StepIndicator.tsx:21
|
||||||
#: src/components/layout/AppHeader.tsx:178
|
#: src/components/layout/AppHeader.tsx:178
|
||||||
#: src/components/layout/UserMenu.tsx:81
|
#: src/components/layout/UserMenu.tsx:85
|
||||||
#: src/components/profile/ProfileFollowTab.tsx:167
|
#: src/components/profile/ProfileFollowTab.tsx:167
|
||||||
#: src/components/profile/ProfilePlaylistsTab.tsx:118
|
#: src/components/profile/ProfilePlaylistsTab.tsx:118
|
||||||
#: src/pages/Search.tsx:123
|
#: src/pages/Search.tsx:123
|
||||||
@@ -1616,7 +1615,7 @@ msgstr "Rôle"
|
|||||||
#: src/components/category/CategoryManager.tsx:216
|
#: src/components/category/CategoryManager.tsx:216
|
||||||
#: src/components/chat/ChatEditBox.tsx:85
|
#: src/components/chat/ChatEditBox.tsx:85
|
||||||
#: src/components/comment/CommentNode.tsx:216
|
#: src/components/comment/CommentNode.tsx:216
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:206
|
#: src/components/playlist/PlaylistEditForm.tsx:207
|
||||||
#: src/components/profile/DescriptionEditor.tsx:89
|
#: src/components/profile/DescriptionEditor.tsx:89
|
||||||
#: src/components/profile/EmailEditor.tsx:75
|
#: src/components/profile/EmailEditor.tsx:75
|
||||||
#: src/pages/Dump.tsx:437
|
#: src/pages/Dump.tsx:437
|
||||||
@@ -1625,7 +1624,7 @@ msgid "Save"
|
|||||||
msgstr "Enregistrer"
|
msgstr "Enregistrer"
|
||||||
|
|
||||||
#: src/components/comment/CommentNode.tsx:217
|
#: src/components/comment/CommentNode.tsx:217
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:203
|
#: src/components/playlist/PlaylistEditForm.tsx:204
|
||||||
#: src/components/profile/ChangePasswordModal.tsx:119
|
#: src/components/profile/ChangePasswordModal.tsx:119
|
||||||
#: src/components/profile/DescriptionEditor.tsx:86
|
#: src/components/profile/DescriptionEditor.tsx:86
|
||||||
#: src/components/profile/EmailEditor.tsx:72
|
#: src/components/profile/EmailEditor.tsx:72
|
||||||
@@ -1634,7 +1633,7 @@ msgstr "Enregistrer"
|
|||||||
msgid "Saving…"
|
msgid "Saving…"
|
||||||
msgstr "Enregistrement…"
|
msgstr "Enregistrement…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:190
|
#: src/components/layout/AppHeader.tsx:188
|
||||||
#: src/components/layout/SearchBar.tsx:83
|
#: src/components/layout/SearchBar.tsx:83
|
||||||
#: src/pages/Search.tsx:69
|
#: src/pages/Search.tsx:69
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
@@ -1677,7 +1676,7 @@ msgstr "Envoyer le lien de réinitialisation"
|
|||||||
msgid "Sending…"
|
msgid "Sending…"
|
||||||
msgstr "Envoi…"
|
msgstr "Envoi…"
|
||||||
|
|
||||||
#: src/components/layout/AppHeader.tsx:206
|
#: src/components/layout/AppHeader.tsx:204
|
||||||
msgid "Server unreachable"
|
msgid "Server unreachable"
|
||||||
msgstr "Serveur inaccessible"
|
msgstr "Serveur inaccessible"
|
||||||
|
|
||||||
@@ -1686,8 +1685,7 @@ msgstr "Serveur inaccessible"
|
|||||||
msgid "Set new password"
|
msgid "Set new password"
|
||||||
msgstr "Définir un nouveau mot de passe"
|
msgstr "Définir un nouveau mot de passe"
|
||||||
|
|
||||||
#: src/components/layout/UserMenu.tsx:89
|
#: src/components/layout/UserMenu.tsx:93
|
||||||
#: src/components/profile/ProfileHeader.tsx:176
|
|
||||||
#: src/pages/Settings.tsx:70
|
#: src/pages/Settings.tsx:70
|
||||||
#: src/pages/Settings.tsx:82
|
#: src/pages/Settings.tsx:82
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
@@ -1697,7 +1695,7 @@ msgstr "Paramètres"
|
|||||||
msgid "Several people are typing…"
|
msgid "Several people are typing…"
|
||||||
msgstr "Plusieurs personnes écrivent…"
|
msgstr "Plusieurs personnes écrivent…"
|
||||||
|
|
||||||
#: src/components/profile/ProfileOwnActions.tsx:108
|
#: src/components/profile/ProfileOwnActions.tsx:102
|
||||||
msgid "Single use, valid for 7 days."
|
msgid "Single use, valid for 7 days."
|
||||||
msgstr "Usage unique, valable 7 jours."
|
msgstr "Usage unique, valable 7 jours."
|
||||||
|
|
||||||
@@ -1786,7 +1784,7 @@ msgstr "Miniature"
|
|||||||
|
|
||||||
#: src/components/dump/create/TitleField.tsx:45
|
#: src/components/dump/create/TitleField.tsx:45
|
||||||
#: src/components/playlist/PlaylistCreateForm.tsx:96
|
#: src/components/playlist/PlaylistCreateForm.tsx:96
|
||||||
#: src/components/playlist/PlaylistEditForm.tsx:159
|
#: src/components/playlist/PlaylistEditForm.tsx:160
|
||||||
#: src/pages/DumpEdit.tsx:516
|
#: src/pages/DumpEdit.tsx:516
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr "Titre"
|
msgstr "Titre"
|
||||||
@@ -1853,7 +1851,7 @@ msgstr "Se désabonner"
|
|||||||
msgid "Unsubscribed"
|
msgid "Unsubscribed"
|
||||||
msgstr "Désabonné"
|
msgstr "Désabonné"
|
||||||
|
|
||||||
#: src/components/profile/ProfileHeader.tsx:73
|
#: src/components/profile/ProfileHeader.tsx:72
|
||||||
msgid "Upload failed"
|
msgid "Upload failed"
|
||||||
msgstr "Envoi échoué"
|
msgstr "Envoi échoué"
|
||||||
|
|
||||||
@@ -1904,7 +1902,7 @@ msgstr "Utilisateur"
|
|||||||
msgid "User management"
|
msgid "User management"
|
||||||
msgstr "Gestion utilisateur"
|
msgstr "Gestion utilisateur"
|
||||||
|
|
||||||
#: src/components/layout/UserMenu.tsx:56
|
#: src/components/layout/UserMenu.tsx:60
|
||||||
msgid "User menu"
|
msgid "User menu"
|
||||||
msgstr "Menu utilisateur"
|
msgstr "Menu utilisateur"
|
||||||
|
|
||||||
|
|||||||
113
src/App.css
113
src/App.css
@@ -2186,54 +2186,28 @@ body.has-player .chat-fab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dense dump cards in the two-column profile view ──
|
/* ── Dense dump cards in the two-column profile view ──
|
||||||
The default card is proportioned for the 860px feed. Dropped into a ~400px
|
A ~400px column is narrow enough for the narrow card's grid (the
|
||||||
column it falls apart: the title wraps four lines deep, the meta row stacks,
|
`@container dump-card` rules), which places the thumb, the text and the
|
||||||
every category chip takes a line of its own and the vote button floats in
|
vote; placing them here as well, more specifically, broke that grid. This
|
||||||
the middle of the resulting void. Below is the same card re-proportioned for
|
only makes the card denser for a column of a dozen: a smaller thumb, the
|
||||||
the column — smaller thumb, title clamped to two lines, no teaser, and meta
|
title clamped to two lines and smaller type. Scoped to the width where the
|
||||||
plus chips sharing the line under the title. Scoped to the width where the
|
columns split; narrower than that the feed is full-width again. */
|
||||||
columns actually split; narrower than that the feed is full-width again and
|
|
||||||
the regular (then the phone) layout applies. */
|
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
.profile-columns .dump-feed {
|
.profile-columns .dump-feed {
|
||||||
padding: 0.75rem 0 0;
|
padding: 0.75rem 0 0;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* preview | body | vote, all top-aligned so the count sits on the title
|
|
||||||
line instead of drifting to the card's vertical middle. */
|
|
||||||
.profile-columns .dump-card-inner {
|
.profile-columns .dump-card-inner {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
||||||
align-items: start;
|
|
||||||
gap: 0.55rem;
|
|
||||||
padding: 0.5rem 0.6rem;
|
padding: 0.5rem 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-columns .dump-card-preview {
|
.profile-columns .dump-card-preview {
|
||||||
grid-area: 1 / 1;
|
|
||||||
width: 88px;
|
width: 88px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-columns .dump-card-vote {
|
|
||||||
grid-area: 1 / 3;
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Row layout with a full-width title: the title takes a line to itself, then
|
|
||||||
meta and chips flow onto the next one and only wrap when they must. */
|
|
||||||
.profile-columns .dump-card-body {
|
|
||||||
grid-area: 1 / 2;
|
|
||||||
margin-left: 0;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.15rem 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-columns .dump-card-title {
|
.profile-columns .dump-card-title {
|
||||||
flex: 1 0 100%;
|
|
||||||
font-size: var(--text-md);
|
font-size: var(--text-md);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -2243,22 +2217,6 @@ body.has-player .chat-fab {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The teaser is the first thing to go, the same call the phone layout makes. */
|
|
||||||
.profile-columns .dump-card-comment {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-columns .dump-card-meta,
|
|
||||||
.profile-columns .dump-card-categories {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-columns .dump-card-meta {
|
|
||||||
gap: 0.4rem;
|
|
||||||
/* Keeps "3 weeks ago" and "1 comment" on one line each. */
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-columns .dump-card-date,
|
.profile-columns .dump-card-date,
|
||||||
.profile-columns .dump-card-comment-count {
|
.profile-columns .dump-card-comment-count {
|
||||||
font-size: var(--text-2xs);
|
font-size: var(--text-2xs);
|
||||||
@@ -2341,10 +2299,6 @@ body.has-player .chat-fab {
|
|||||||
background: var(--color-success);
|
background: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-header .btn-border {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-header .follow-btn {
|
.profile-header .follow-btn {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -2476,8 +2430,8 @@ body.has-player .chat-fab {
|
|||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Grid sized by the button row; the invite panel below only fills that
|
/* Grid sized by the button; the invite panel below only fills that width
|
||||||
width (width: 0 + min-width: 100% keeps it out of the sizing). */
|
(width: 0 + min-width: 100% keeps it out of the sizing). */
|
||||||
.profile-own-actions {
|
.profile-own-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@@ -2485,15 +2439,6 @@ body.has-player .chat-fab {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.profile-own-actions-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.profile-own-actions .btn-border {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-btn {
|
.invite-btn {
|
||||||
padding: 0.3rem 0.9rem;
|
padding: 0.3rem 0.9rem;
|
||||||
@@ -3121,18 +3066,6 @@ body.has-player .chat-fab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Avatar menu — visible only below the text-links breakpoint */
|
|
||||||
.nav-compact {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1150px) {
|
|
||||||
.nav-compact {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header-user {
|
.app-header-user {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3150,7 +3083,7 @@ body.has-player .chat-fab {
|
|||||||
background: var(--color-header-user-bg-hover);
|
background: var(--color-header-user-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── User menu (compact nav) ── */
|
/* ── User menu ── */
|
||||||
/* A flex box, so the inline-flex trigger leaves no line-box gap under it
|
/* A flex box, so the inline-flex trigger leaves no line-box gap under it
|
||||||
that would sit it higher than the buttons beside it. */
|
that would sit it higher than the buttons beside it. */
|
||||||
.user-menu {
|
.user-menu {
|
||||||
@@ -3206,6 +3139,7 @@ body.has-player .chat-fab {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: var(--text-md);
|
font-size: var(--text-md);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
transition: background var(--dur-fast);
|
transition: background var(--dur-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3213,6 +3147,33 @@ body.has-player .chat-fab {
|
|||||||
background: var(--color-header-user-bg-hover);
|
background: var(--color-header-user-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-menu-separator {
|
||||||
|
margin: 0.15rem 0.35rem;
|
||||||
|
border-top: var(--border-width) solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log out is a button among links: it drops the button's own border,
|
||||||
|
background and weight, and is muted until hovered, as ending the session
|
||||||
|
should be. Two classes, to outweigh `.app-header-nav button`, which
|
||||||
|
centres it, and the themes' `[data-style] button`. */
|
||||||
|
.user-menu .user-menu-item--logout {
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu .user-menu-item--logout:hover {
|
||||||
|
background: var(--color-header-user-bg-hover);
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
.user-list-popover {
|
.user-list-popover {
|
||||||
/* position/top/bottom/left/max-height set inline — anchored to the
|
/* position/top/bottom/left/max-height set inline — anchored to the
|
||||||
trigger's viewport rect, flipping above it when there isn't room below */
|
trigger's viewport rect, flipping above it when there isn't room below */
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ export function AppHeader(
|
|||||||
<nav className="app-header-nav">
|
<nav className="app-header-nav">
|
||||||
{
|
{
|
||||||
/* The user's own links lead the row, so the icons after them run
|
/* The user's own links lead the row, so the icons after them run
|
||||||
unbroken to the "+". CSS shows the text links or the avatar's
|
unbroken to the "+": the text links on wide screens, then the
|
||||||
menu, by width. */
|
avatar's menu at every width. */
|
||||||
}
|
}
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
@@ -178,9 +178,7 @@ export function AppHeader(
|
|||||||
<Trans>Playlists</Trans>
|
<Trans>Playlists</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-compact">
|
|
||||||
<UserMenu user={user} />
|
<UserMenu user={user} />
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Renders the avatar menu that replaces the header's profile and playlists
|
* Renders the avatar menu that leads the header's icon row at every width: it
|
||||||
* links on small screens, and leads to the settings and the site's tools.
|
* holds the profile and playlists links (on small screens, the only ones), the
|
||||||
|
* settings, the site's tools, and logging out.
|
||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
@@ -13,6 +14,7 @@ import { Avatar } from "../shared/Avatar.tsx";
|
|||||||
import type { SelfUser } from "../../../shared/user.ts";
|
import type { SelfUser } from "../../../shared/user.ts";
|
||||||
import { can } from "../../utils/auth/permissions.ts";
|
import { can } from "../../utils/auth/permissions.ts";
|
||||||
import { userPath } from "../../../shared/paths.ts";
|
import { userPath } from "../../../shared/paths.ts";
|
||||||
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
||||||
|
|
||||||
/** Passed to {@link UserMenu} by `AppHeader`, for a signed-in user. */
|
/** Passed to {@link UserMenu} by `AppHeader`, for a signed-in user. */
|
||||||
export interface UserMenuProps {
|
export interface UserMenuProps {
|
||||||
@@ -21,10 +23,12 @@ export interface UserMenuProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the signed-in user's avatar, which opens a menu with links to their
|
* Renders the signed-in user's avatar, which opens a menu with links to their
|
||||||
* profile, their playlists and `/settings`, and to `/manage` for those who
|
* profile, their playlists and `/settings`, to `/manage` for those who hold
|
||||||
* hold `category:manage`. A click outside or Escape closes it.
|
* `category:manage`, and a Log out button. A click outside or Escape closes
|
||||||
|
* it.
|
||||||
*/
|
*/
|
||||||
export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
|
export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
|
||||||
|
const { logout } = useAuth();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -98,6 +102,18 @@ export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
|
|||||||
<Trans>Manage</Trans>
|
<Trans>Manage</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<div className="user-menu-separator" role="separator" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="user-menu-item user-menu-item--logout"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
logout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>Log out</Trans>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { can } from "../../utils/auth/permissions.ts";
|
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { API_URL } from "../../config/api.ts";
|
import { API_URL } from "../../config/api.ts";
|
||||||
@@ -36,13 +35,13 @@ export interface ProfileHeaderProps {
|
|||||||
/**
|
/**
|
||||||
* Renders the avatar, username, role and inviter. Visitors get a follow
|
* Renders the avatar, username, role and inviter. Visitors get a follow
|
||||||
* button. On their own profile, users change their avatar
|
* button. On their own profile, users change their avatar
|
||||||
* (`PUT /api/me/avatar`) and email, invite someone, reach their settings (and
|
* (`PUT /api/me/avatar`) and email, and invite someone; their settings, the
|
||||||
* the site's tools, with `category:manage`) and log out.
|
* site's tools and logging out are in the header's avatar menu.
|
||||||
*/
|
*/
|
||||||
export function ProfileHeader(
|
export function ProfileHeader(
|
||||||
{ user, isOwnProfile, onUserChange }: ProfileHeaderProps,
|
{ user, isOwnProfile, onUserChange }: ProfileHeaderProps,
|
||||||
): React.JSX.Element {
|
): React.JSX.Element {
|
||||||
const { user: me, authFetch, updateSession, logout } = useAuth();
|
const { user: me, authFetch, updateSession } = useAuth();
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [avatarError, setAvatarError] = useState<string | null>(null);
|
const [avatarError, setAvatarError] = useState<string | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -170,25 +169,7 @@ export function ProfileHeader(
|
|||||||
targetUsername={user.username}
|
targetUsername={user.username}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOwnProfile && (
|
{isOwnProfile && <ProfileOwnActions />}
|
||||||
<ProfileOwnActions>
|
|
||||||
<Link to="/settings" className="btn-border">
|
|
||||||
<Trans>Settings</Trans>
|
|
||||||
</Link>
|
|
||||||
{can(me, "category:manage") && (
|
|
||||||
<Link to="/manage" className="btn-border">
|
|
||||||
<Trans>Manage</Trans>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-border btn-border--danger"
|
|
||||||
onClick={logout}
|
|
||||||
>
|
|
||||||
<Trans>Log out</Trans>
|
|
||||||
</button>
|
|
||||||
</ProfileOwnActions>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Renders the actions of the signed-in user's own profile header, which invite
|
* Renders the action of the signed-in user's own profile header, which invites
|
||||||
* someone, beside the log out button the header adds.
|
* someone.
|
||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
@@ -14,13 +14,10 @@ import { useHeightTransition } from "../../hooks/shared/useHeightTransition.ts";
|
|||||||
import { ErrorCard } from "../shared/ErrorCard.tsx";
|
import { ErrorCard } from "../shared/ErrorCard.tsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the "Invite someone" button, then `children`, the log out button.
|
* Renders the "Invite someone" button, which opens a panel with an invite link.
|
||||||
* Each opening of the panel creates a new single-use invite link, for
|
* Each opening of the panel creates a new single-use link, for `/register`.
|
||||||
* `/register`.
|
|
||||||
*/
|
*/
|
||||||
export function ProfileOwnActions(
|
export function ProfileOwnActions(): React.JSX.Element {
|
||||||
{ children }: { children: React.ReactNode },
|
|
||||||
): React.JSX.Element {
|
|
||||||
const { authFetch } = useAuth();
|
const { authFetch } = useAuth();
|
||||||
const [inviteUrl, setInviteUrl] = useState<string | null>(null);
|
const [inviteUrl, setInviteUrl] = useState<string | null>(null);
|
||||||
const [pending, setPending] = useState(false);
|
const [pending, setPending] = useState(false);
|
||||||
@@ -69,7 +66,6 @@ export function ProfileOwnActions(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-own-actions" ref={ref}>
|
<div className="profile-own-actions" ref={ref}>
|
||||||
<div className="profile-own-actions-row">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="invite-btn"
|
className="invite-btn"
|
||||||
@@ -79,8 +75,6 @@ export function ProfileOwnActions(
|
|||||||
>
|
>
|
||||||
<Trans>+ Invite someone</Trans>
|
<Trans>+ Invite someone</Trans>
|
||||||
</button>
|
</button>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{inviteUrl && (
|
{inviteUrl && (
|
||||||
<div
|
<div
|
||||||
className="invite-panel animate-in"
|
className="invite-panel animate-in"
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { screen } from "@testing-library/react";
|
|||||||
import { userEvent } from "@testing-library/user-event";
|
import { userEvent } from "@testing-library/user-event";
|
||||||
import { UserMenu } from "../../../components/layout/UserMenu.tsx";
|
import { UserMenu } from "../../../components/layout/UserMenu.tsx";
|
||||||
import { makeSession } from "../../fixtures.ts";
|
import { makeSession } from "../../fixtures.ts";
|
||||||
import { currentPath, renderWithContexts } from "../../render.tsx";
|
import { currentPath, renderApp, renderWithContexts } from "../../render.tsx";
|
||||||
|
import { AUTH_STORAGE_KEY } from "../../../utils/auth/storage.ts";
|
||||||
|
|
||||||
describe("UserMenu", () => {
|
describe("UserMenu", () => {
|
||||||
test("opens, links to profile and playlists, closes on Escape and outside", async () => {
|
test("opens, links to profile and playlists, closes on Escape and outside", async () => {
|
||||||
@@ -41,4 +42,26 @@ describe("UserMenu", () => {
|
|||||||
expect(currentPath()).toBe("/@ann");
|
expect(currentPath()).toBe("/@ann");
|
||||||
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("links to /manage for those who hold category:manage", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderWithContexts(
|
||||||
|
<UserMenu user={makeSession({ role: "admin" }).user} />,
|
||||||
|
);
|
||||||
|
await user.click(screen.getByRole("button", { name: "User menu" }));
|
||||||
|
expect(screen.getByRole("menuitem", { name: "Manage" })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/manage",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs out", async () => {
|
||||||
|
const session = makeSession({ username: "ann" });
|
||||||
|
renderApp(<UserMenu user={session.user} />, { session });
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.click(screen.getByRole("button", { name: "User menu" }));
|
||||||
|
await user.click(screen.getByRole("menuitem", { name: "Log out" }));
|
||||||
|
expect(localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull();
|
||||||
|
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ describe("UserPublicProfile", () => {
|
|||||||
expect(dumpsAll).toHaveAttribute("href", "/@ann/dumps");
|
expect(dumpsAll).toHaveAttribute("href", "/@ann/dumps");
|
||||||
expect(votesAll).toHaveAttribute("href", "/@ann/upvoted");
|
expect(votesAll).toHaveAttribute("href", "/@ann/upvoted");
|
||||||
await waitFor(() => expect(document.title).toMatch(/^ann · /));
|
await waitFor(() => expect(document.title).toMatch(/^ann · /));
|
||||||
expect(screen.queryByRole("link", { name: "Settings" })).not
|
|
||||||
.toBeInTheDocument();
|
|
||||||
// The lists are read by the user's id, which a rename doesn't change.
|
// The lists are read by the user's id, which a rename doesn't change.
|
||||||
expect(calls("GET /api/users/:u/dumps")[0].path).toBe(
|
expect(calls("GET /api/users/:u/dumps")[0].path).toBe(
|
||||||
"/api/users/ann/dumps",
|
"/api/users/ann/dumps",
|
||||||
@@ -275,11 +273,11 @@ describe("UserPublicProfile", () => {
|
|||||||
expect(within(bob).getByRole("link", { name: /@cy/ })).toBeInTheDocument();
|
expect(within(bob).getByRole("link", { name: /@cy/ })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("visitors can follow; a settings link is only on your own profile", async () => {
|
test("visitors can follow; inviting is only on your own profile", async () => {
|
||||||
open({ session: makeSession({ id: "bob" }) });
|
open({ session: makeSession({ id: "bob" }) });
|
||||||
expect(await screen.findByRole("button", { name: "Follow ann" }))
|
expect(await screen.findByRole("button", { name: "Follow ann" }))
|
||||||
.toBeInTheDocument();
|
.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("link", { name: "Settings" })).not
|
expect(screen.queryByRole("button", { name: "+ Invite someone" })).not
|
||||||
.toBeInTheDocument();
|
.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -414,29 +412,18 @@ describe("UserPublicProfile", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("your settings, and the site's tools for an admin, are a link away", async () => {
|
test("an admin's own profile has no manage tab: that's for others'", async () => {
|
||||||
open({
|
open({
|
||||||
session: { ...annSession, user: { ...annSession.user, role: "admin" } },
|
session: { ...annSession, user: { ...annSession.user, role: "admin" } },
|
||||||
user: { ...ann, role: "admin" },
|
user: { ...ann, role: "admin" },
|
||||||
});
|
});
|
||||||
expect(await screen.findByRole("link", { name: "Settings" }))
|
expect(await screen.findByRole("button", { name: "+ Invite someone" }))
|
||||||
.toHaveAttribute("href", "/settings");
|
.toBeInTheDocument();
|
||||||
expect(screen.getByRole("link", { name: "Manage" })).toHaveAttribute(
|
|
||||||
"href",
|
|
||||||
"/manage",
|
|
||||||
);
|
|
||||||
// Their own profile has no manage tab: that's for others'.
|
|
||||||
expect(
|
expect(
|
||||||
within(document.querySelector(".profile-tabs") as HTMLElement)
|
within(document.querySelector(".profile-tabs") as HTMLElement)
|
||||||
.queryByRole("button", { name: "Manage" }),
|
.queryByRole("button", { name: "Manage" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("log out", async () => {
|
|
||||||
const { user } = open({ session: annSession });
|
|
||||||
await user.click(await screen.findByRole("button", { name: "Log out" }));
|
|
||||||
expect(localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("manage", () => {
|
describe("manage", () => {
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
[data-style="brutalist"] .audio-player-vol-btn,
|
[data-style="brutalist"] .audio-player-vol-btn,
|
||||||
[data-style="brutalist"] .rich-content-thumbnail-btn,
|
[data-style="brutalist"] .rich-content-thumbnail-btn,
|
||||||
[data-style="brutalist"] .user-menu-trigger,
|
[data-style="brutalist"] .user-menu-trigger,
|
||||||
|
[data-style="brutalist"] .user-menu-item,
|
||||||
[data-style="brutalist"] .auth-link-btn,
|
[data-style="brutalist"] .auth-link-btn,
|
||||||
[data-style="brutalist"] .form-cancel,
|
[data-style="brutalist"] .form-cancel,
|
||||||
[data-style="brutalist"] .form-label-action {
|
[data-style="brutalist"] .form-label-action {
|
||||||
@@ -115,6 +116,8 @@
|
|||||||
|
|
||||||
[data-style="brutalist"] .user-menu-trigger:hover:not(:disabled),
|
[data-style="brutalist"] .user-menu-trigger:hover:not(:disabled),
|
||||||
[data-style="brutalist"] .user-menu-trigger:active,
|
[data-style="brutalist"] .user-menu-trigger:active,
|
||||||
|
[data-style="brutalist"] .user-menu-item:hover:not(:disabled),
|
||||||
|
[data-style="brutalist"] .user-menu-item:active,
|
||||||
[data-style="brutalist"] .auth-link-btn:hover:not(:disabled),
|
[data-style="brutalist"] .auth-link-btn:hover:not(:disabled),
|
||||||
[data-style="brutalist"] .auth-link-btn:active,
|
[data-style="brutalist"] .auth-link-btn:active,
|
||||||
[data-style="brutalist"] .form-cancel:hover:not(:disabled),
|
[data-style="brutalist"] .form-cancel:hover:not(:disabled),
|
||||||
|
|||||||
@@ -217,6 +217,7 @@
|
|||||||
[data-style="geocities"] .audio-player-vol-btn,
|
[data-style="geocities"] .audio-player-vol-btn,
|
||||||
[data-style="geocities"] .rich-content-thumbnail-btn,
|
[data-style="geocities"] .rich-content-thumbnail-btn,
|
||||||
[data-style="geocities"] .user-menu-trigger,
|
[data-style="geocities"] .user-menu-trigger,
|
||||||
|
[data-style="geocities"] .user-menu-item,
|
||||||
[data-style="geocities"] .auth-link-btn,
|
[data-style="geocities"] .auth-link-btn,
|
||||||
[data-style="geocities"] .form-cancel,
|
[data-style="geocities"] .form-cancel,
|
||||||
[data-style="geocities"] .form-label-action,
|
[data-style="geocities"] .form-label-action,
|
||||||
|
|||||||
Reference in New Issue
Block a user