Profile redesign: Design fixes (#37892)

This commit is contained in:
Echo
2026-02-18 13:05:02 +01:00
committed by GitHub
parent b62ba9e29e
commit 079f8615fe
9 changed files with 150 additions and 97 deletions

View File

@@ -2,9 +2,9 @@
<path fill="url(#VerifiedGradient)" d="M8 .837a3.168 3.168 0 0 1 2.47 1.187 3.166 3.166 0 0 1 2.601.906 3.168 3.168 0 0 1 .905 2.6A3.167 3.167 0 0 1 15.164 8a3.172 3.172 0 0 1-1.188 2.47 3.167 3.167 0 0 1-.903 2.597 3.168 3.168 0 0 1-2.596.909 3.167 3.167 0 0 1-4.95.001 3.166 3.166 0 0 1-3.397-2.258 3.169 3.169 0 0 1-.107-1.24A3.168 3.168 0 0 1 .826 8a3.17 3.17 0 0 1 1.197-2.479 3.168 3.168 0 0 1 .91-2.593 3.166 3.166 0 0 1 2.596-.905A3.169 3.169 0 0 1 8 .837Z"/> <path fill="url(#VerifiedGradient)" d="M8 .837a3.168 3.168 0 0 1 2.47 1.187 3.166 3.166 0 0 1 2.601.906 3.168 3.168 0 0 1 .905 2.6A3.167 3.167 0 0 1 15.164 8a3.172 3.172 0 0 1-1.188 2.47 3.167 3.167 0 0 1-.903 2.597 3.168 3.168 0 0 1-2.596.909 3.167 3.167 0 0 1-4.95.001 3.166 3.166 0 0 1-3.397-2.258 3.169 3.169 0 0 1-.107-1.24A3.168 3.168 0 0 1 .826 8a3.17 3.17 0 0 1 1.197-2.479 3.168 3.168 0 0 1 .91-2.593 3.166 3.166 0 0 1 2.596-.905A3.169 3.169 0 0 1 8 .837Z"/>
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="m6 8 1.333 1.333L10 6.667"/> <path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="m6 8 1.333 1.333L10 6.667"/>
<defs> <defs>
<linearGradient id="VerifiedGradient" x1="-.966" x2="12.162" y1="2.629" y2="17.493" gradientUnits="userSpaceOnUse"> <linearGradient id="VerifiedGradient" x1="7.99512" y1="0.836914" x2="7.99512" y2="15.1689" gradientUnits="userSpaceOnUse">
<stop offset=".13" stop-color="#5638CC"/> <stop stop-color="#00BC7D"/>
<stop offset=".995" stop-color="#DC03F0"/> <stop offset="1" stop-color="#00BBA7"/>
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 921 B

View File

@@ -311,6 +311,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
status?: ImmutableMap<string, unknown>; status?: ImmutableMap<string, unknown>;
needsStatusRefresh?: boolean; needsStatusRefresh?: boolean;
forceDropdown?: boolean; forceDropdown?: boolean;
className?: string;
renderItem?: RenderItemFn<Item>; renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>; renderHeader?: RenderHeaderFn<Item>;
onOpen?: // Must use a union type for the full function as a union with void is not allowed. onOpen?: // Must use a union type for the full function as a union with void is not allowed.
@@ -335,6 +336,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
status, status,
needsStatusRefresh, needsStatusRefresh,
forceDropdown = false, forceDropdown = false,
className,
renderItem, renderItem,
renderHeader, renderHeader,
onOpen, onOpen,
@@ -434,6 +436,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
modalProps: { modalProps: {
actions: items, actions: items,
onClick: handleItemClick, onClick: handleItemClick,
className,
}, },
}), }),
); );
@@ -462,6 +465,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
handleClose, handleClose,
statusId, statusId,
needsStatusRefresh, needsStatusRefresh,
className,
], ],
); );
@@ -515,7 +519,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
popperConfig={popperConfig} popperConfig={popperConfig}
> >
{({ props, arrowProps, placement }) => ( {({ props, arrowProps, placement }) => (
<div {...props} id={menuId}> <div {...props} className={className} id={menuId}>
<div className={`dropdown-animation dropdown-menu ${placement}`}> <div className={`dropdown-animation dropdown-menu ${placement}`}>
<div <div
className={`dropdown-menu__arrow ${placement}`} className={`dropdown-menu__arrow ${placement}`}

View File

@@ -214,7 +214,10 @@ export const AccountHeader: React.FC<{
<> <>
<AccountBio <AccountBio
accountId={accountId} accountId={accountId}
className='account__header__content' className={classNames(
'account__header__content',
isRedesign && redesignClasses.bio,
)}
/> />
<AccountHeaderFields accountId={accountId} /> <AccountHeaderFields accountId={accountId} />
</> </>

View File

@@ -15,8 +15,7 @@ import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import { Icon } from '@/mastodon/components/icon'; import { Icon } from '@/mastodon/components/icon';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccount } from '@/mastodon/hooks/useAccount';
import type { Account } from '@/mastodon/models/account'; import type { Account, AccountFieldShape } from '@/mastodon/models/account';
import { isValidUrl } from '@/mastodon/utils/checks';
import type { OnElementHandler } from '@/mastodon/utils/html'; import type { OnElementHandler } from '@/mastodon/utils/html';
import { cleanExtraEmojis } from '../../emoji/normalize'; import { cleanExtraEmojis } from '../../emoji/normalize';
@@ -76,8 +75,8 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
[account.emojis], [account.emojis],
); );
const textHasCustomEmoji = useCallback( const textHasCustomEmoji = useCallback(
(text: string) => { (text?: string | null) => {
if (!emojis) { if (!emojis || !text) {
return false; return false;
} }
for (const emoji of Object.keys(emojis)) { for (const emoji of Object.keys(emojis)) {
@@ -92,62 +91,96 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
const htmlHandlers = useElementHandledLink({ const htmlHandlers = useElementHandledLink({
hashtagAccountId: account.id, hashtagAccountId: account.id,
}); });
const intl = useIntl();
if (account.fields.isEmpty()) {
return null;
}
return ( return (
<CustomEmojiProvider emojis={emojis}> <CustomEmojiProvider emojis={emojis}>
<dl className={classes.fieldList}> <dl className={classes.fieldList}>
{account.fields.map( {account.fields.map((field, key) => (
( <FieldRow
{ name, name_emojified, value_emojified, value_plain, verified_at }, key={key}
key, {...field.toJSON()}
) => ( htmlHandlers={htmlHandlers}
<div textHasCustomEmoji={textHasCustomEmoji}
key={key} />
className={classNames( ))}
classes.fieldRow,
verified_at && classes.fieldVerified,
)}
>
<FieldHTML
as='dt'
text={name}
textEmojified={name_emojified}
textHasCustomEmoji={textHasCustomEmoji(name)}
titleLength={50}
className='translate'
{...htmlHandlers}
/>
<FieldHTML
as='dd'
text={value_plain ?? ''}
textEmojified={value_emojified}
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
titleLength={120}
{...htmlHandlers}
/>
{verified_at && (
<Icon
id='verified'
icon={IconVerified}
className={classes.fieldVerifiedIcon}
aria-label={intl.formatMessage(verifyMessage, {
date: intl.formatDate(verified_at, dateFormatOptions),
})}
noFill
/>
)}
</div>
),
)}
</dl> </dl>
</CustomEmojiProvider> </CustomEmojiProvider>
); );
}; };
const FieldRow: FC<
{
textHasCustomEmoji: (text?: string | null) => boolean;
htmlHandlers: ReturnType<typeof useElementHandledLink>;
} & AccountFieldShape
> = ({
textHasCustomEmoji,
htmlHandlers,
name,
name_emojified,
value_emojified,
value_plain,
verified_at,
}) => {
const intl = useIntl();
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
return (
/* eslint-disable -- This method of showing field contents is not very accessible, but it's what we've got for now */
<div
className={classNames(
classes.fieldRow,
verified_at && classes.fieldVerified,
showAll && classes.fieldShowAll,
)}
onClick={handleClick}
/* eslint-enable */
>
<FieldHTML
as='dt'
text={name}
textEmojified={name_emojified}
textHasCustomEmoji={textHasCustomEmoji(name)}
titleLength={50}
className='translate'
{...htmlHandlers}
/>
<dd>
<FieldHTML
as='span'
text={value_plain ?? ''}
textEmojified={value_emojified}
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
titleLength={120}
{...htmlHandlers}
/>
{verified_at && (
<Icon
id='verified'
icon={IconVerified}
className={classes.fieldVerifiedIcon}
aria-label={intl.formatMessage(verifyMessage, {
date: intl.formatDate(verified_at, dateFormatOptions),
})}
noFill
/>
)}
</dd>
</div>
);
};
const FieldHTML: FC< const FieldHTML: FC<
{ {
as: 'dd' | 'dt'; as?: 'span' | 'dt';
text: string; text: string;
textEmojified: string; textEmojified: string;
textHasCustomEmoji: boolean; textHasCustomEmoji: boolean;
@@ -164,11 +197,6 @@ const FieldHTML: FC<
onElement, onElement,
...props ...props
}) => { }) => {
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
const handleElement: OnElementHandler = useCallback( const handleElement: OnElementHandler = useCallback(
(element, props, children, extra) => { (element, props, children, extra) => {
if (element instanceof HTMLAnchorElement) { if (element instanceof HTMLAnchorElement) {
@@ -186,17 +214,13 @@ const FieldHTML: FC<
}, },
[onElement, textHasCustomEmoji], [onElement, textHasCustomEmoji],
); );
return ( return (
<EmojiHTML <EmojiHTML
as={as} as={as}
htmlString={textEmojified} htmlString={textEmojified}
title={showTitleOnLength(text, titleLength)} title={showTitleOnLength(text, titleLength)}
className={classNames( className={className}
className,
text && isValidUrl(text) && classes.fieldLink,
showAll && classes.fieldShowAll,
)}
onClick={handleClick}
onElement={handleElement} onElement={handleElement}
{...props} {...props}
/> />

View File

@@ -42,6 +42,8 @@ import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common'; import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl(); const intl = useIntl();
const { signedIn, permissions } = useIdentity(); const { signedIn, permissions } = useIdentity();
@@ -86,6 +88,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
items={menuItems} items={menuItems}
icon='ellipsis-v' icon='ellipsis-v'
iconComponent={MoreHorizIcon} iconComponent={MoreHorizIcon}
className={classes.buttonMenu}
/> />
); );
}; };

View File

@@ -24,11 +24,10 @@
.name { .name {
flex-grow: 1; flex-grow: 1;
font-size: 22px;
white-space: initial;
line-height: normal;
> h1 { > h1 {
font-size: 22px;
line-height: normal;
white-space: initial; white-space: initial;
} }
} }
@@ -149,11 +148,33 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
border-top: 1px solid var(--color-border-primary); border-top: 1px solid var(--color-border-primary);
} }
.buttonMenu {
// Override the modal for mobile.
&:global(.actions-modal) {
max-height: none;
}
li :global(.icon) {
width: 20px;
height: 20px;
}
}
.bio {
font-size: 15px;
}
.badge { .badge {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
border: none; border: none;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500; font-weight: 500;
padding: 4px;
font-size: 13px;
:global(.account__header__badges) > & {
line-height: 1;
}
> span { > span {
font-weight: unset; font-weight: unset;
@@ -194,12 +215,13 @@ svg.badgeIcon {
.fieldList { .fieldList {
display: grid; display: grid;
grid-template-columns: 160px 1fr min-content; grid-template-columns: 160px 1fr;
column-gap: 12px; column-gap: 12px;
margin: 4px 0 16px; margin: 16px 0;
border-top: 0.5px solid var(--color-border-primary);
@container (width < 420px) { @container (width < 420px) {
grid-template-columns: 100px 1fr min-content; grid-template-columns: 100px 1fr;
} }
} }
@@ -208,11 +230,10 @@ svg.badgeIcon {
grid-column: 1 / -1; grid-column: 1 / -1;
align-items: start; align-items: start;
grid-template-columns: subgrid; grid-template-columns: subgrid;
padding: 0 4px; padding: 8px;
border-bottom: 0.5px solid var(--color-border-primary);
> :is(dt, dd) { > :is(dt, dd) {
margin: 8px 0;
&:not(.fieldShowAll) { &:not(.fieldShowAll) {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@@ -227,43 +248,34 @@ svg.badgeIcon {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
&:not(.fieldVerified) > dd { > dd {
grid-column: span 2; display: flex;
align-items: center;
gap: 4px;
} }
a { a {
font-weight: 500; color: inherit;
color: var(--color-text-brand);
text-decoration: none; text-decoration: none;
transition: 0.2s ease-in-out;
&:hover, &:hover,
&:focus { &:focus {
color: var(--color-text-brand-soft); text-decoration: underline;
} }
} }
} }
.fieldVerified { .fieldVerified {
background-color: var(--color-bg-brand-softer); background-color: var(--color-bg-success-softer);
}
.fieldLink:is(dd, dt) {
margin: 0;
}
.fieldLink > a {
display: block;
padding: 8px 0;
} }
.fieldVerifiedIcon { .fieldVerifiedIcon {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-top: 8px;
} }
.fieldNumbersWrapper { .fieldNumbersWrapper {
font-size: 13px;
padding: 0; padding: 0;
a { a {
@@ -323,10 +335,15 @@ svg.badgeIcon {
border-bottom: 1px solid var(--color-border-primary); border-bottom: 1px solid var(--color-border-primary);
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 0 12px; padding: 0 24px;
@container (width >= 500px) { @container (width < 500px) {
padding: 0 24px; padding: 0 12px;
a {
flex: 1 1 0px;
text-align: center;
}
} }
a { a {

View File

@@ -39,6 +39,7 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px;
} }
} }

View File

@@ -11,8 +11,9 @@ import {
export const ActionsModal: React.FC<{ export const ActionsModal: React.FC<{
actions: MenuItem[]; actions: MenuItem[];
onClick: React.MouseEventHandler; onClick: React.MouseEventHandler;
}> = ({ actions, onClick }) => ( className?: string;
<div className='modal-root__modal actions-modal'> }> = ({ actions, onClick, className }) => (
<div className={classNames('modal-root__modal actions-modal', className)}>
<ul> <ul>
{actions.map((option, i: number) => { {actions.map((option, i: number) => {
if (option === null) { if (option === null) {

View File

@@ -14,7 +14,7 @@ import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji'; import type { CustomEmoji } from './custom_emoji';
// AccountField // AccountField
interface AccountFieldShape extends Required<ApiAccountFieldJSON> { export interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
name_emojified: string; name_emojified: string;
value_emojified: string; value_emojified: string;
value_plain: string | null; value_plain: string | null;