[Glitch] Update collection list item design

Port 2124be8a81 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2026-03-26 15:56:36 +01:00
committed by Claire
parent d4e7ee4855
commit ffac9e53c6
9 changed files with 214 additions and 77 deletions

View File

@@ -96,7 +96,7 @@ export const Avatar: React.FC<Props> = ({
};
export const AvatarById: React.FC<
{ accountId: string } & Omit<Props, 'account'>
{ accountId: string | undefined } & Omit<Props, 'account'>
> = ({ accountId, ...otherProps }) => {
const account = useAccount(accountId);
return <Avatar account={account} {...otherProps} />;

View File

@@ -6,10 +6,11 @@ import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
import { DisplayNameWithoutDomain } from './no-domain';
export const DisplayNameDefault: FC<
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, localDomain, className, ...props }) => {
const username = useMemo(() => {
export function useAccountHandle(
account: DisplayNameProps['account'],
localDomain: DisplayNameProps['localDomain'],
) {
return useMemo(() => {
if (!account) {
return null;
}
@@ -20,6 +21,12 @@ export const DisplayNameDefault: FC<
}
return `@${acct}`;
}, [account, localDomain]);
}
export const DisplayNameDefault: FC<
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, localDomain, className, ...props }) => {
const username = useAccountHandle(account, localDomain);
return (
<DisplayNameWithoutDomain

View File

@@ -154,6 +154,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
key={item.id}
collection={item}
withoutBorder={index === listedCollections.length - 1}
withAuthorHandle={false}
positionInList={index + 1}
listSize={listedCollections.length}
/>

View File

@@ -1,8 +1,9 @@
.wrapper {
display: flex;
align-items: center;
align-items: start;
margin-inline: 24px;
padding-block: 12px;
gap: 16px;
padding-inline: 16px;
&:not(.wrapperWithoutBorder) {
border-bottom: 1px solid var(--color-border-primary);
@@ -12,12 +13,42 @@
.content {
position: relative;
flex-grow: 1;
padding-block: 15px;
display: flex;
align-items: center;
column-gap: 12px;
}
.avatarGrid {
position: relative;
display: grid;
grid-template-columns: repeat(2, min-content);
gap: 2px;
&.avatarGridSensitive {
.avatar {
filter: blur(4px);
}
}
}
.avatar {
background: var(--color-bg-brand-softest);
}
.avatarSensitiveBadge {
position: absolute;
inset: 0;
margin: auto;
padding: 3px;
width: 18px;
height: 18px;
border-radius: 8px;
color: var(--color-text-primary);
background: var(--color-bg-warning-softest);
}
.link {
display: block;
margin-bottom: 2px;
font-size: 15px;
font-weight: 500;
text-decoration: none;
@@ -40,15 +71,12 @@
color: var(--color-text-secondary);
}
.metaList {
--gap: 0.75ch;
.menuButton {
padding: 4px;
margin-top: -2px;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
& > li:not(:last-child)::after {
content: '·';
margin-inline-start: var(--gap);
svg {
width: 20px;
height: 20px;
}
}

View File

@@ -5,70 +5,65 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { AvatarById } from 'flavours/glitch/components/avatar';
import { useAccountHandle } from 'flavours/glitch/components/display_name/default';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import { Article } from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { domain } from 'flavours/glitch/initial_state';
import classes from './collection_list_item.module.scss';
import { CollectionMenu } from './collection_menu';
export const CollectionMetaData: React.FC<{
collection: ApiCollectionJSON;
extended?: boolean;
className?: string;
}> = ({ collection, extended, className }) => {
export const AvatarGrid: React.FC<{
accountIds: (string | undefined)[];
sensitive?: boolean;
}> = ({ accountIds: ids, sensitive }) => {
const avatarIds = [ids[0], ids[1], ids[2], ids[3]];
return (
<ul className={classNames(classes.metaList, className)}>
<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'
/>
)}
</>
<div
className={classNames(
classes.avatarGrid,
sensitive ? classes.avatarGridSensitive : null,
)}
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: <RelativeTimestamp timestamp={collection.updated_at} long />,
}}
tagName='li'
/>
</ul>
>
{avatarIds.map((id) => (
<AvatarById
animate={false}
key={id}
accountId={id}
className={classes.avatar}
size={25}
/>
))}
{sensitive && <WarningIcon className={classes.avatarSensitiveBadge} />}
</div>
);
};
export const CollectionListItem: React.FC<{
collection: ApiCollectionJSON;
withoutBorder?: boolean;
withAuthorHandle?: boolean;
withTimestamp?: boolean;
positionInList: number;
listSize: number;
}> = ({ collection, withoutBorder, positionInList, listSize }) => {
}> = ({
collection,
withoutBorder,
withAuthorHandle = true,
withTimestamp,
positionInList,
listSize,
}) => {
const { id, name } = collection;
const linkId = useId();
const uniqueId = useId();
const linkId = `${uniqueId}-link`;
const infoId = `${uniqueId}-info`;
const authorAccount = useAccount(collection.account_id);
const authorHandle = useAccountHandle(authorAccount, domain);
return (
<Article
@@ -78,19 +73,67 @@ export const CollectionListItem: React.FC<{
withoutBorder && classes.wrapperWithoutBorder,
)}
aria-labelledby={linkId}
aria-describedby={infoId}
aria-posinset={positionInList}
aria-setsize={listSize}
>
<div className={classes.content}>
<h2 id={linkId}>
<Link to={`/collections/${id}`} className={classes.link}>
{name}
</Link>
</h2>
<CollectionMetaData collection={collection} className={classes.info} />
<AvatarGrid
accountIds={collection.items.map((item) => item.account_id)}
sensitive={collection.sensitive}
/>
<div>
<h2 id={linkId}>
<Link to={`/collections/${id}`} className={classes.link}>
{name}
</Link>
</h2>
<ul className={classes.info} id={infoId}>
{collection.sensitive && (
<li className='sr-only'>
<FormattedMessage
id='collections.sensitive'
defaultMessage='Sensitive'
/>
</li>
)}
{withAuthorHandle && authorAccount && (
<FormattedMessage
id='collections.by_account'
defaultMessage='by {account_handle}'
values={{
account_handle: authorHandle,
}}
tagName='li'
/>
)}
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
{withTimestamp && (
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: (
<RelativeTimestamp timestamp={collection.updated_at} long />
),
}}
tagName='li'
/>
)}
</ul>
</div>
</div>
<CollectionMenu context='list' collection={collection} />
<CollectionMenu
context='list'
collection={collection}
className={classes.menuButton}
/>
</Article>
);
};

View File

@@ -6,6 +6,7 @@ import { Helmet } from 'react-helmet';
import { useHistory, useLocation, useParams } from 'react-router';
import { openModal } from '@/flavours/glitch/actions/modal';
import { RelativeTimestamp } from '@/flavours/glitch/components/relative_timestamp';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
@@ -25,7 +26,6 @@ import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollectionAccountsList } from './accounts_list';
import { CollectionMetaData } from './collection_list_item';
import { CollectionMenu } from './collection_menu';
import classes from './styles.module.scss';
@@ -40,6 +40,54 @@ const messages = defineMessages({
},
});
const CollectionMetaData: React.FC<{
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
@@ -137,7 +185,6 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
<CollectionMetaData
extended={account_id === me}
collection={collection}
className={classes.metaData}
/>
</div>
);

View File

@@ -52,9 +52,19 @@
font-size: 13px;
}
.metaData {
.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 {

View File

@@ -93,6 +93,8 @@ export const Collections: React.FC<{
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
{collections.map((item, index) => (
<CollectionListItem
withTimestamp
withAuthorHandle={false}
key={item.id}
collection={item}
positionInList={index + 1}

View File

@@ -4164,11 +4164,10 @@ a.account__display-name {
.column-subheading {
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
padding: 8px 20px;
font-size: 12px;
padding: 12px 24px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
cursor: default;
}
.getting-started__wrapper {