From 3d5cb624ba1bf861d41f4bf42f7ecf02b010718e Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 28 Apr 2026 13:32:27 +0200 Subject: [PATCH] Update design of Collections list page (#38822) --- .../mastodon/components/column_header.tsx | 6 +- .../components/scrollable_list/components.tsx | 6 +- .../mastodon/components/tab_list/index.tsx | 51 ++++++ .../components/tab_list/styles.module.scss | 39 ++++ .../components/tab_list/tab_list.stories.tsx | 33 ++++ .../components/styles.module.scss | 40 +---- .../account_timeline/components/tabs.tsx | 18 +- .../collection_list_item.module.scss | 1 - .../features/collections/editor/index.tsx | 8 +- .../mastodon/features/collections/index.tsx | 170 +++++++++++------- .../features/collections/styles.module.scss | 35 ++++ app/javascript/mastodon/locales/en.json | 8 +- .../mastodon/utils/has_react_children.ts | 12 ++ 13 files changed, 305 insertions(+), 122 deletions(-) create mode 100644 app/javascript/mastodon/components/tab_list/index.tsx create mode 100644 app/javascript/mastodon/components/tab_list/styles.module.scss create mode 100644 app/javascript/mastodon/components/tab_list/tab_list.stories.tsx create mode 100644 app/javascript/mastodon/features/collections/styles.module.scss create mode 100644 app/javascript/mastodon/utils/has_react_children.ts diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx index b8ab2bbe54c..076ca3085e9 100644 --- a/app/javascript/mastodon/components/column_header.tsx +++ b/app/javascript/mastodon/components/column_header.tsx @@ -276,9 +276,11 @@ export const ColumnHeader: React.FC = ({ ); + const HeadingElement = hasTitle ? 'h1' : 'div'; + const component = (
-

+ {hasTitle && ( <> {backButton} @@ -311,7 +313,7 @@ export const ColumnHeader: React.FC = ({ {extraButton} {collapseButton}

- +
(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => { - if (!isLoading && Children.count(children) === 0 && emptyMessage) { + if (!isLoading && !hasReactChildren(children) && emptyMessage) { return (
{emptyMessage} diff --git a/app/javascript/mastodon/components/tab_list/index.tsx b/app/javascript/mastodon/components/tab_list/index.tsx new file mode 100644 index 00000000000..98651ff4d07 --- /dev/null +++ b/app/javascript/mastodon/components/tab_list/index.tsx @@ -0,0 +1,51 @@ +import type { ComponentPropsWithoutRef, FC } from 'react'; + +import classNames from 'classnames'; +import type { NavLinkProps } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; + +import classes from './styles.module.scss'; + +interface TabListProps { + /** + * Setting this will remove the default border and + * horizontal padding from the component + */ + plain?: boolean; +} + +/** + * Display a simple row of links as tabs. + * The current page will be highlighted automatically based on the link destination. + */ +export const TabList: FC> = ({ + plain, + className, + children, + ...otherProps +}) => { + return ( +
+ {children} +
+ ); +}; + +export const TabLink: FC = ({ + className, + children, + ...otherProps +}) => { + return ( + + {children} + + ); +}; diff --git a/app/javascript/mastodon/components/tab_list/styles.module.scss b/app/javascript/mastodon/components/tab_list/styles.module.scss new file mode 100644 index 00000000000..aeadd95f08a --- /dev/null +++ b/app/javascript/mastodon/components/tab_list/styles.module.scss @@ -0,0 +1,39 @@ +.tabList { + display: flex; + gap: 16px; +} + +.withSpaceAndBorder { + padding-inline: 16px; + border-bottom: 1px solid var(--color-border-primary); +} + +.tab { + display: block; + padding: 18px 0; + color: var(--color-text-primary); + font-size: 15px; + font-weight: 500; + text-decoration: none; + border-radius: 0; + transition: color 0.2s ease-in-out; + + @container (width < 500px) { + flex: 1 1 0px; + text-align: center; + } + + &:hover { + text-decoration: none; + } + + &:not(:global(.active)):is(:hover, :focus) { + color: var(--color-text-brand-soft); + } + + &:global(.active) { + color: var(--color-text-brand); + border-bottom: 4px solid var(--color-border-brand); + padding-bottom: 14px; + } +} diff --git a/app/javascript/mastodon/components/tab_list/tab_list.stories.tsx b/app/javascript/mastodon/components/tab_list/tab_list.stories.tsx new file mode 100644 index 00000000000..56edb1b324c --- /dev/null +++ b/app/javascript/mastodon/components/tab_list/tab_list.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { TabList, TabLink } from './index'; + +const meta = { + title: 'Components/TabList', + component: TabList, + subcomponents: { TabLink }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + Activity + Media + Featured + + ), +}; + +export const Plain: Story = { + render: () => ( + + Activity + Media + Featured + + ), +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/styles.module.scss b/app/javascript/mastodon/features/account_timeline/components/styles.module.scss index 9c13026b970..dc9a4459d29 100644 --- a/app/javascript/mastodon/features/account_timeline/components/styles.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/styles.module.scss @@ -355,48 +355,10 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; } } -.tabs, -.noTabs { - border-bottom: 1px solid var(--color-border-primary); -} - -.tabs { - display: flex; - gap: 16px; - padding: 0 16px; - - @container (width < 500px) { - a { - flex: 1 1 0px; - text-align: center; - } - } - - a { - display: block; - font-size: 15px; - font-weight: 500; - padding: 18px 0; - text-decoration: none; - color: var(--color-text-primary); - border-radius: 0; - transition: color 0.2s ease-in-out; - - &:not([aria-current='page']):is(:hover, :focus) { - color: var(--color-text-brand-soft); - } - } - - :global(.active) { - color: var(--color-text-brand); - border-bottom: 4px solid var(--color-border-brand); - padding-bottom: 14px; - } -} - .noTabs { width: 100%; border-width: 0 0 1px; + border-bottom: 1px solid var(--color-border-primary); } .bannerBase { diff --git a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx index b2970412df4..5fc56f8e3a1 100644 --- a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx @@ -3,8 +3,8 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; import type { NavLinkProps } from 'react-router-dom'; -import { NavLink } from 'react-router-dom'; +import { TabLink, TabList } from '@/mastodon/components/tab_list'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccountId } from '@/mastodon/hooks/useAccountId'; @@ -28,20 +28,20 @@ export const AccountTabs: FC = () => { } return ( -
- + + - + {show_media && ( - + - + )} {show_featured && ( - + - + )} -
+ ); }; diff --git a/app/javascript/mastodon/features/collections/components/collection_list_item.module.scss b/app/javascript/mastodon/features/collections/components/collection_list_item.module.scss index 5d440d62f75..1079d1aebf0 100644 --- a/app/javascript/mastodon/features/collections/components/collection_list_item.module.scss +++ b/app/javascript/mastodon/features/collections/components/collection_list_item.module.scss @@ -1,6 +1,5 @@ .wrapper { --list-item-padding: 16px; - --list-item-padding-block: 12px; --list-item-gap: 16px; &:not(.wrapperWithoutBorder) { diff --git a/app/javascript/mastodon/features/collections/editor/index.tsx b/app/javascript/mastodon/features/collections/editor/index.tsx index e48e8718cff..b18091461f2 100644 --- a/app/javascript/mastodon/features/collections/editor/index.tsx +++ b/app/javascript/mastodon/features/collections/editor/index.tsx @@ -144,7 +144,7 @@ export const CollectionEditorPage: React.FC<{ /> ) : ( - + )}
@@ -156,9 +156,11 @@ export const CollectionEditorPage: React.FC<{ ); }; -export const MaxCollectionsCallout: React.FC = () => ( +export const MaxCollectionsCallout: React.FC<{ className?: string }> = ({ + className, +}) => ( ( + + + + +); + export function useAccountCollections(accountId: string | null | undefined) { const dispatch = useAppDispatch(); @@ -62,33 +85,11 @@ export const Collections: React.FC<{ const { collections, status } = useAccountCollections(accountId); - const emptyMessage = - status === 'error' || !accountId ? ( - - ) : ( - <> - - -
- -
- - - - ); - const canCreateMoreCollections = collections.length < userCollectionLimit; const isOwnCollection = accountId === me; + const showCreateButton = + isOwnCollection && status === 'idle' && canCreateMoreCollections; + const titleMessage = isOwnCollection ? messages.headingMe : messages.headingOther; @@ -100,45 +101,88 @@ export const Collections: React.FC<{ name: , }); + const tabMessage = isOwnCollection + ? messages.createdByYou + : messages.createdByAuthor; + + const errorMessage = (status === 'error' || !accountId) && ( + + ); + return ( - - - - ) - } - /> + - {status === 'idle' && !canCreateMoreCollections && ( - +
+

{pageTitleHtml}

+ + + {intl.formatMessage(tabMessage, { + name: , + })} + + +
+ {collections.length > 0 ? ( + <> + {status === 'idle' && ( +
+

+ +

+ {showCreateButton && } +
+ )} + + {!canCreateMoreCollections && ( + + )} + {collections.map((item, index) => ( + + ))} + + + ) : ( + + } + message={ + + } + > + + )} - - {collections.map((item, index) => ( - - ))} -
diff --git a/app/javascript/mastodon/features/collections/styles.module.scss b/app/javascript/mastodon/features/collections/styles.module.scss new file mode 100644 index 00000000000..9ded10dcc2d --- /dev/null +++ b/app/javascript/mastodon/features/collections/styles.module.scss @@ -0,0 +1,35 @@ +.header { + display: flex; + flex-direction: column; + gap: 8px; + padding-top: 24px; + padding-inline: 16px; + border-bottom: 1px solid var(--color-border-primary); +} + +.heading { + font-size: 22px; + font-weight: 500; + line-height: 1.2; + overflow-wrap: break-word; +} + +.listHeader { + display: flex; + justify-content: space-between; + align-items: center; + min-height: 28px; + padding-top: 16px; + padding-inline: 16px; +} + +.subHeading { + font-size: 15px; + font-weight: 500; + line-height: 1.2; + overflow-wrap: break-word; +} + +.maxCollectionsError { + margin: 8px 16px; +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index d72d8b986b1..ead248f26b8 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -387,7 +387,6 @@ "collections.create.accounts_title": "Who will you feature in this collection?", "collections.create.basic_details_title": "Basic details", "collections.create.steps": "Step {step}/{total}", - "collections.create_a_collection_hint": "Create a collection to recommend or share your favourite accounts with others.", "collections.create_collection": "Create collection", "collections.delete_collection": "Delete collection", "collections.description_length_hint": "100 characters limit", @@ -405,6 +404,10 @@ "collections.hidden_accounts_link": "{count, plural, one {# hidden account} other {# hidden accounts}}", "collections.hints.accounts_counter": "{count}/{max} accounts", "collections.last_updated_at": "Last updated: {date}", + "collections.list.collections_with_count": "{count, plural, one {# Collection} other {# Collections}}", + "collections.list.created_by_author": "Created by {name}", + "collections.list.created_by_you": "Created by you", + "collections.list.featuring_you": "Featuring you", "collections.manage_accounts": "Manage accounts", "collections.mark_as_sensitive": "Mark as sensitive", "collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The collection name will still be visible.", @@ -412,7 +415,6 @@ "collections.maximum_collection_count_reached": "You have created the maximum number of collections", "collections.name_length_hint": "40 characters limit", "collections.new_collection": "New collection", - "collections.no_collections_yet": "No collections yet.", "collections.remove_account": "Remove", "collections.report_collection": "Report this collection", "collections.revoke_collection_inclusion": "Remove myself from this collection", @@ -454,11 +456,11 @@ "column.list_members": "Manage list members", "column.lists": "Lists", "column.mutes": "Muted users", - "column.my_collections": "My collections", "column.notifications": "Notifications", "column.other_collections": "Collections by {name}", "column.pins": "Pinned posts", "column.public": "Federated timeline", + "column.your_collections": "Your Collections", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/utils/has_react_children.ts b/app/javascript/mastodon/utils/has_react_children.ts new file mode 100644 index 00000000000..f02762bacec --- /dev/null +++ b/app/javascript/mastodon/utils/has_react_children.ts @@ -0,0 +1,12 @@ +import { Children } from 'react'; +import type { ReactNode } from 'react'; + +export function hasReactChildren(children: ReactNode): boolean { + if (!children) { + return false; + } + + const childrenCount = Children.toArray(children).filter(Boolean).length; + + return childrenCount > 0; +}