[Glitch] Implement new collection page design

Port 098d698a7e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2026-03-27 16:30:06 +01:00
committed by Claire
parent 37b4329c75
commit 1a816434cf
8 changed files with 154 additions and 204 deletions

View File

@@ -76,6 +76,7 @@ interface AccountProps {
withMenu?: boolean; withMenu?: boolean;
withBorder?: boolean; withBorder?: boolean;
extraAccountInfo?: React.ReactNode; extraAccountInfo?: React.ReactNode;
className?: string;
children?: React.ReactNode; children?: React.ReactNode;
} }
@@ -89,6 +90,7 @@ export const Account: React.FC<AccountProps> = ({
withMenu = true, withMenu = true,
withBorder = true, withBorder = true,
extraAccountInfo, extraAccountInfo,
className,
children, children,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -291,7 +293,7 @@ export const Account: React.FC<AccountProps> = ({
return ( return (
<div <div
className={classNames('account', { className={classNames('account', className, {
'account--minimal': minimal, 'account--minimal': minimal,
'account--without-border': !withBorder, 'account--without-border': !withBorder,
})} })}

View File

@@ -6,6 +6,7 @@
background-color: var(--color-bg-brand-softest); background-color: var(--color-bg-brand-softest);
color: var(--color-text-primary); color: var(--color-text-primary);
border-radius: 12px; border-radius: 12px;
font-size: 15px;
} }
.icon { .icon {

View File

@@ -2,6 +2,8 @@ import { useCallback, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Callout } from '@/flavours/glitch/components/callout';
import { FollowButton } from '@/flavours/glitch/components/follow_button';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import type { import type {
ApiCollectionJSON, ApiCollectionJSON,
@@ -27,10 +29,6 @@ const messages = defineMessages({
id: 'collections.accounts.empty_title', id: 'collections.accounts.empty_title',
defaultMessage: 'This collection is empty', defaultMessage: 'This collection is empty',
}, },
accounts: {
id: 'collections.detail.accounts_heading',
defaultMessage: 'Accounts',
},
}); });
const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => { const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
@@ -41,8 +39,9 @@ const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
const AccountItem: React.FC<{ const AccountItem: React.FC<{
accountId: string | undefined; accountId: string | undefined;
collectionOwnerId: string; collectionOwnerId: string;
withBio?: boolean;
withBorder?: boolean; withBorder?: boolean;
}> = ({ accountId, withBorder = true, collectionOwnerId }) => { }> = ({ accountId, withBio = true, withBorder = true, collectionOwnerId }) => {
const relationship = useRelationship(accountId); const relationship = useRelationship(accountId);
if (!accountId) { if (!accountId) {
@@ -59,12 +58,17 @@ const AccountItem: React.FC<{
(relationship.following || relationship.requested)); (relationship.following || relationship.requested));
return ( return (
<Account <div className={classes.accountItemWrapper} data-with-border={withBorder}>
minimal={withoutButton} <Account
withMenu={false} minimal
withBorder={withBorder} id={accountId}
id={accountId} withBio={withBio}
/> withBorder={false}
withMenu={false}
className={classes.accountItem}
/>
{!withoutButton && <FollowButton accountId={accountId} />}
</div>
); );
}; };
@@ -131,19 +135,27 @@ const SensitiveScreen: React.FC<{
} }
return ( return (
<div className={classes.sensitiveWarning}> <Callout
variant='warning'
title={
<FormattedMessage
id='collections.detail.sensitive_content'
defaultMessage='Sensitive content'
/>
}
primaryLabel={
<FormattedMessage
id='content_warning.show_short'
defaultMessage='Show'
/>
}
onPrimary={showAnyway}
>
<FormattedMessage <FormattedMessage
id='collections.detail.sensitive_note' id='collections.detail.sensitive_note'
defaultMessage='This collection contains accounts and content that may be sensitive to some users.' defaultMessage='The description and accounts may not be suitable for all viewers.'
tagName='p'
/> />
<Button onClick={showAnyway}> </Callout>
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
</Button>
</div>
); );
}; };
@@ -192,13 +204,14 @@ export const CollectionAccountsList: React.FC<{
<ItemList <ItemList
isLoading={isLoading} isLoading={isLoading}
emptyMessage={intl.formatMessage(messages.empty)} emptyMessage={intl.formatMessage(messages.empty)}
className={classes.itemList}
> >
{collection && currentUserInCollection ? ( {collection && currentUserInCollection ? (
<> <>
<h3 className={classes.columnSubheading}> <h3 className={classes.columnSubheading}>
<FormattedMessage <FormattedMessage
id='collections.detail.author_added_you' id='collections.detail.you_were_added_to_this_collection'
defaultMessage='{author} added you to this collection' defaultMessage='You were added to this collection'
values={{ values={{
author: <SimpleAuthorName id={collection.account_id} />, author: <SimpleAuthorName id={collection.account_id} />,
}} }}
@@ -208,9 +221,11 @@ export const CollectionAccountsList: React.FC<{
key={currentUserInCollection.account_id} key={currentUserInCollection.account_id}
aria-posinset={1} aria-posinset={1}
aria-setsize={items.length} aria-setsize={items.length}
className={classes.youWereAddedWrapper}
> >
<AccountItem <AccountItem
withBorder={false} withBorder={false}
withBio={false}
accountId={currentUserInCollection.account_id} accountId={currentUserInCollection.account_id}
collectionOwnerId={collection.account_id} collectionOwnerId={collection.account_id}
/> />
@@ -225,18 +240,30 @@ export const CollectionAccountsList: React.FC<{
ref={listHeadingRef} ref={listHeadingRef}
> >
<FormattedMessage <FormattedMessage
id='collections.detail.other_accounts_in_collection' id='collections.detail.other_accounts_count'
defaultMessage='Others in this collection:' defaultMessage='{count, plural, one {# other account} other {# other accounts}}'
values={{ count: collection.item_count - 1 }}
/> />
</h3> </h3>
</> </>
) : ( ) : (
<h3 <h3
className='column-subheading sr-only' className={classes.columnSubheading}
tabIndex={-1} tabIndex={-1}
ref={listHeadingRef} ref={listHeadingRef}
> >
{intl.formatMessage(messages.accounts)} {collection ? (
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
/>
) : (
<FormattedMessage
id='collections.detail.accounts_heading'
defaultMessage='Accounts'
/>
)}
</h3> </h3>
)} )}
{collection && ( {collection && (

View File

@@ -43,7 +43,7 @@
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 8px; border-radius: 8px;
color: var(--color-text-primary); fill: var(--color-text-primary);
background: var(--color-bg-warning-softest); background: var(--color-bg-warning-softest);
} }

View File

@@ -4,24 +4,19 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useHistory, useLocation, useParams } from 'react-router'; import { useHistory, useLocation, useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { openModal } from '@/flavours/glitch/actions/modal'; import { openModal } from '@/flavours/glitch/actions/modal';
import { RelativeTimestamp } from '@/flavours/glitch/components/relative_timestamp'; import { useAccountHandle } from '@/flavours/glitch/components/display_name/default';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections'; import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Column } from 'flavours/glitch/components/column'; import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header'; import { ColumnHeader } from 'flavours/glitch/components/column_header';
import {
DisplayName,
LinkedDisplayName,
} from 'flavours/glitch/components/display_name';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components'; import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
import { Tag } from 'flavours/glitch/components/tags/tag';
import { useAccount } from 'flavours/glitch/hooks/useAccount'; import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { me } from 'flavours/glitch/initial_state'; import { domain } from 'flavours/glitch/initial_state';
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections'; import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
@@ -40,88 +35,29 @@ const messages = defineMessages({
}, },
}); });
const CollectionMetaData: React.FC<{ export const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
collection: ApiCollectionJSON;
extended?: boolean;
}> = ({ collection, extended }) => {
return (
<ul className={classes.metaList}>
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
{extended && (
<>
{collection.discoverable ? (
<FormattedMessage
id='collections.visibility_public'
defaultMessage='Public'
tagName='li'
/>
) : (
<FormattedMessage
id='collections.visibility_unlisted'
defaultMessage='Unlisted'
tagName='li'
/>
)}
{collection.sensitive && (
<FormattedMessage
id='collections.sensitive'
defaultMessage='Sensitive'
tagName='li'
/>
)}
</>
)}
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: <RelativeTimestamp timestamp={collection.updated_at} long />,
}}
tagName='li'
/>
</ul>
);
};
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
id,
// When previewMode is enabled, your own display name
// will not be replaced with "you"
previewMode = false,
}) => {
const account = useAccount(id); const account = useAccount(id);
const authorHandle = useAccountHandle(account, domain);
if (!account) {
return null;
}
const author = ( const author = (
<span className={classes.displayNameWithAvatar}> <Link to={`/@${account.acct}`} data-hover-card-account={account.id}>
<Avatar size={18} account={account} /> {authorHandle}
{previewMode ? ( </Link>
<DisplayName account={account} variant='simple' />
) : (
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
)}
</span>
); );
const displayAsYou = id === me && !previewMode;
return ( return (
<p className={previewMode ? classes.previewAuthorNote : classes.authorNote}> <p className={classes.authorNote}>
{displayAsYou ? ( <FormattedMessage
<FormattedMessage id='collections.by_account'
id='collections.detail.curated_by_you' defaultMessage='by {account_handle}'
defaultMessage='Curated by you' values={{
/> account_handle: author,
) : ( }}
<FormattedMessage />
id='collections.detail.curated_by_author'
defaultMessage='Curated by {author}'
values={{ author }}
/>
)}
</p> </p>
); );
}; };
@@ -156,14 +92,12 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
}, [history, handleShare, isNewCollection, location.pathname]); }, [history, handleShare, isNewCollection, location.pathname]);
return ( return (
<div className={classes.header}> <header className={classes.header}>
<div className={classes.titleWithMenu}> <div className={classes.titleWithMenu}>
<div className={classes.titleWrapper}> <div className={classes.titleWrapper}>
{tag && ( {tag && <span className={classes.tag}>#{tag.name}</span>}
// TODO: Make non-interactive tag component
<Tag name={tag.name} className={classes.tag} />
)}
<h2 className={classes.name}>{name}</h2> <h2 className={classes.name}>{name}</h2>
<AuthorNote id={account_id} />
</div> </div>
<div className={classes.headerButtonWrapper}> <div className={classes.headerButtonWrapper}>
<IconButton <IconButton
@@ -181,12 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
</div> </div>
</div> </div>
{description && <p className={classes.description}>{description}</p>} {description && <p className={classes.description}>{description}</p>}
<AuthorNote id={collection.account_id} /> </header>
<CollectionMetaData
extended={account_id === me}
collection={collection}
/>
</div>
); );
}; };

View File

@@ -97,7 +97,7 @@ export const CollectionShareModal: React.FC<{
<div className={classes.preview}> <div className={classes.preview}>
<div> <div>
<h2 className={classes.previewHeading}>{collection.name}</h2> <h2 className={classes.previewHeading}>{collection.name}</h2>
<AuthorNote previewMode id={collection.account_id} /> <AuthorNote id={collection.account_id} />
</div> </div>
<AvatarGroup> <AvatarGroup>
{collection.items.slice(0, 5).map(({ account_id }) => { {collection.items.slice(0, 5).map(({ account_id }) => {

View File

@@ -1,6 +1,5 @@
.header { .header {
padding: 16px; padding: 24px;
border-bottom: 1px solid var(--color-border-primary);
} }
.titleWithMenu { .titleWithMenu {
@@ -11,23 +10,46 @@
.titleWrapper { .titleWrapper {
flex-grow: 1; flex-grow: 1;
display: flex;
flex-direction: column;
align-items: start;
gap: 4px;
min-width: 0; min-width: 0;
} }
.tag { .tag {
margin-bottom: 4px; display: inline-block;
margin-inline-start: -8px; padding: 4px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-bg-secondary);
} }
.name { .name {
font-size: 28px; font-size: 22px;
font-weight: 500;
line-height: 1.2; line-height: 1.2;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.authorNote {
font-size: 13px;
color: var(--color-text-secondary);
a {
color: inherit;
text-decoration-color: rgb(from var(--color-text-secondary) r g b / 50%);
&:hover {
text-decoration: none;
}
}
}
.description { .description {
font-size: 15px; font-size: 15px;
margin-top: 8px; margin-top: 12px;
} }
.headerButtonWrapper { .headerButtonWrapper {
@@ -39,79 +61,41 @@
box-sizing: content-box; box-sizing: content-box;
padding: 5px; padding: 5px;
border-radius: 4px; border-radius: 4px;
border: 1px solid var(--color-border-primary);
} }
.authorNote { .itemList {
margin-top: 8px; padding-inline: 24px;
font-size: 13px;
color: var(--color-text-secondary);
}
.previewAuthorNote {
font-size: 13px;
}
.metaList {
--gap: 0.75ch;
display: flex;
flex-wrap: wrap;
margin-top: 16px;
gap: var(--gap);
font-size: 15px;
& > li:not(:last-child)::after {
content: '·';
margin-inline-start: var(--gap);
}
} }
.columnSubheading { .columnSubheading {
background: var(--color-bg-secondary); padding-bottom: 12px;
padding: 15px 20px;
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: -2px;
}
} }
.displayNameWithAvatar { .accountItemWrapper {
display: inline-flex;
gap: 4px;
align-items: baseline;
a {
color: inherit;
text-decoration: underline;
&:hover,
&:focus {
text-decoration: none;
}
}
> :global(.account__avatar) {
align-self: center;
}
}
.sensitiveWarning {
display: flex; display: flex;
flex-direction: column; align-items: start;
align-items: center; padding-block: 16px;
max-width: 460px;
margin: auto; &[data-with-border='true'] {
padding: 60px 30px; border-bottom: 1px solid var(--color-border-primary);
gap: 20px; }
text-align: center; }
text-wrap: balance;
font-size: 15px; .accountItem {
line-height: 1.5; --account-name-size: 15px;
cursor: default; --account-handle-color: var(--color-text-secondary);
--account-handle-size: 13px;
--account-bio-color: var(--color-text-primary);
--account-bio-size: 13px;
--account-outer-spacing: 0;
flex-grow: 1;
}
.youWereAddedWrapper {
padding-bottom: 16px;
} }
.revokeControlWrapper { .revokeControlWrapper {
@@ -119,9 +103,7 @@
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-top: -10px; margin-bottom: 8px;
padding-bottom: 16px;
padding-inline: calc(26px + var(--avatar-width)) 16px;
:global(.button) { :global(.button) {
min-width: 30%; min-width: 30%;

View File

@@ -2077,7 +2077,15 @@ body > [data-popper-placement] {
} }
.account { .account {
padding: 10px; // glitch: reduced padding --account-outer-spacing: 10px; // glitch: reduced padding
--account-name-color: var(--color-text-primary);
--account-name-size: 14px;
--account-handle-color: var(--color-text-secondary);
--account-handle-size: 14px;
--account-bio-color: var(--color-text-secondary);
--account-bio-size: 14px;
padding: var(--account-outer-spacing);
// Using :where keeps specificity low, allowing for existing // Using :where keeps specificity low, allowing for existing
// .account overrides to still apply // .account overrides to still apply
@@ -2090,10 +2098,10 @@ body > [data-popper-placement] {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
color: var(--color-text-secondary); color: var(--account-handle-color);
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: var(--account-handle-size);
.display-name { .display-name {
margin-bottom: 4px; margin-bottom: 4px;
@@ -2101,7 +2109,8 @@ body > [data-popper-placement] {
.display-name strong { .display-name strong {
display: inline; display: inline;
color: var(--color-text-primary); font-size: var(--account-name-size);
color: var(--account-name-color);
} }
} }
@@ -2239,7 +2248,7 @@ body > [data-popper-placement] {
} }
&__note { &__note {
font-size: 14px; font-size: var(--account-bio-size);
font-weight: 400; font-weight: 400;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -2248,7 +2257,7 @@ body > [data-popper-placement] {
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
margin-top: 10px; margin-top: 10px;
color: var(--color-text-secondary); color: var(--account-bio-color);
&--missing { &--missing {
color: var(--color-text-tertiary); color: var(--color-text-tertiary);