Profile redesign: Fields iteration (#37682)
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user