Profile redesign: Fields iteration (#37682)

This commit is contained in:
Echo authored and GitHub committed 2026-02-03 13:02:33 +00:00
1 parent 346ca87ee8
commit 0923e2cb26
13 files changed
+449 -383

No files matched your search

@@ -1,33 +1,57 @@
import type { FC, ReactNode } from 'react';
import { forwardRef } from 'react';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import classNames from 'classnames';
import type { OmitUnion } from '@/mastodon/utils/types';
import { Icon } from '../icon';
import type { IconProp } from '../icon';
import classes from './styles.module.css';
export interface MiniCardProps {
label: ReactNode;
value: ReactNode;
className?: string;
hidden?: boolean;
}
export const MiniCard: FC<MiniCardProps> = ({
label,
value,
className,
hidden,
}) => {
if (!label) {
return null;
export type MiniCardProps = OmitUnion<
ComponentPropsWithoutRef<'div'>,
{
label: ReactNode;
value: ReactNode;
icon?: IconProp;
iconId?: string;
iconClassName?: string;
}
>;
return (
<div
className={classNames(classes.card, className)}
inert={hidden ? '' : undefined}
>
<dt className={classes.label}>{label}</dt>
<dd className={classes.value}>{value}</dd>
</div>
);
};
export const MiniCard = forwardRef<HTMLDivElement, MiniCardProps>(
(
{ label, value, className, hidden, icon, iconId, iconClassName, ...props },
ref,
) => {
if (!label) {
return null;
}
return (
<div
{...props}
className={classNames(
classes.card,
icon && classes.cardWithIcon,
className,
)}
ref={ref}
>
{icon && (
<Icon
id={iconId ?? 'minicard'}
icon={icon}
className={classNames(classes.icon, iconClassName)}
noFill
/>
)}
<dt className={classes.label}>{label}</dt>
<dd className={classes.value}>{value}</dd>
</div>
);
},
);
MiniCard.displayName = 'MiniCard';
@@ -1,69 +1,37 @@
import type { FC, Key, MouseEventHandler } from 'react';
import { FormattedMessage } from 'react-intl';
import { forwardRef } from 'react';
import type { ComponentPropsWithoutRef, Key } from 'react';
import classNames from 'classnames';
import { useOverflow } from '@/mastodon/hooks/useOverflow';
import type { OmitUnion } from '@/mastodon/utils/types';
import { MiniCard } from '.';
import type { MiniCardProps } from '.';
import type { MiniCardProps as BaseCardProps } from '.';
import classes from './styles.module.css';
export type MiniCardProps = BaseCardProps & {
key?: Key;
};
interface MiniCardListProps {
cards?: (Pick<MiniCardProps, 'label' | 'value' | 'className'> & {
key?: Key;
})[];
className?: string;
onOverflowClick?: MouseEventHandler;
cards?: MiniCardProps[];
}
export const MiniCardList: FC<MiniCardListProps> = ({
cards = [],
className,
onOverflowClick,
}) => {
const {
wrapperRef,
listRef,
hiddenCount,
hasOverflow,
hiddenIndex,
maxWidth,
} = useOverflow();
export const MiniCardList = forwardRef<
HTMLDListElement,
OmitUnion<ComponentPropsWithoutRef<'dl'>, MiniCardListProps>
>(({ cards = [], className, children, ...props }, ref) => {
if (!cards.length) {
return null;
}
return (
<div className={classNames(classes.wrapper, className)} ref={wrapperRef}>
<dl className={classes.list} ref={listRef} style={{ maxWidth }}>
{cards.map((card, index) => (
<MiniCard
key={card.key ?? index}
label={card.label}
value={card.value}
hidden={hasOverflow && index >= hiddenIndex}
className={card.className}
/>
))}
</dl>
{cards.length > 1 && (
<div>
<button
type='button'
className={classNames(classes.more, !hasOverflow && classes.hidden)}
onClick={onOverflowClick}
>
<FormattedMessage
id='minicard.more_items'
defaultMessage='+{count}'
values={{ count: hiddenCount }}
/>
</button>
</div>
)}
</div>
<dl {...props} className={classNames(classes.list, className)} ref={ref}>
{cards.map((card, index) => (
<MiniCard key={card.key ?? index} {...card} />
))}
{children}
</dl>
);
};
});
MiniCardList.displayName = 'MiniCardList';
@@ -1,30 +1,12 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { action } from 'storybook/actions';
import LinkIcon from '@/material-icons/400-24px/link_2.svg?react';
import { MiniCardList } from './list';
const meta = {
title: 'Components/MiniCard',
component: MiniCardList,
args: {
onOverflowClick: action('Overflow clicked'),
},
render(args) {
return (
<div
style={{
resize: 'horizontal',
padding: '1rem',
border: '1px solid gray',
overflow: 'auto',
width: '400px',
minWidth: '100px',
}}
>
<MiniCardList {...args} />
</div>
);
},
} satisfies Meta<typeof MiniCardList>;
export default meta;
@@ -38,10 +20,12 @@ export const Default: Story = {
{
label: 'Website',
value: <a href='https://example.com'>bowie-the-db.meow</a>,
icon: LinkIcon,
},
{
label: 'Free playlists',
value: <a href='https://soundcloud.com/bowie-the-dj'>soundcloud.com</a>,
icon: LinkIcon,
},
{ label: 'Location', value: 'Purris, France' },
],
@@ -54,11 +38,13 @@ export const LongValue: Story = {
{
label: 'Username',
value: 'bowie-the-dj',
style: { maxWidth: '250px' },
},
{
label: 'Bio',
value:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
style: { maxWidth: '250px' },
},
],
},
@@ -1,52 +1,49 @@
.wrapper {
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;
gap: 4px;
}
.list {
display: flex;
gap: 4px;
overflow: hidden;
position: relative;
}
.card,
.more {
.card {
border: 1px solid var(--color-border-primary);
padding: 8px;
border-radius: 8px;
flex-shrink: 0;
}
.more {
display: grid;
grid-template-rows: repeat(2, 1fr);
column-gap: 8px;
font-size: 13px;
line-height: 1rem;
color: var(--color-text-secondary);
font-weight: 600;
appearance: none;
background: none;
aspect-ratio: 1;
height: 100%;
transition: all 300ms linear;
}
.more:hover {
background-color: var(--color-bg-brand-softer);
color: var(--color-text-primary);
.cardWithIcon {
grid-template-columns: 16px 1fr;
}
.hidden {
display: none;
}
.label {
color: var(--color-text-secondary);
margin-bottom: 2px;
.icon {
grid-row: span 2;
grid-column: 1;
width: 100%;
height: auto;
align-self: center;
fill: currentColor;
}
.value {
color: var(--color-text-primary);
font-weight: 600;
font-weight: 500;
a {
color: var(--color-text-brand);
text-decoration: none;
transition: color 0.2s ease-in-out;
outline: none;
&:hover,
&:focus {
color: var(--color-text-brand-soft);
}
}
}
.label,
@@ -54,4 +51,8 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.cardWithIcon & {
grid-column: 2;
}
}