Redesign: Callouts (#40211)

This commit is contained in:
Echo
2026-08-19 15:52:52 +00:00
committed by GitHub
parent 656fcf857d
commit 87db34be63
4 changed files with 183 additions and 2 deletions

View File

@@ -0,0 +1,28 @@
@use '@/styles/mastodon/mixins';
.rootLarge,
.rootSmall {
align-items: center;
background-color: var(--color-bg-highlight);
border-radius: var(--radius-sm);
box-sizing: border-box;
display: flex;
gap: var(--space-xs);
}
.rootLarge {
padding: var(--space-sm);
}
.rootSmall {
padding: var(--space-xs);
}
.textWrapper {
flex-grow: 1;
}
.icon {
width: var(--space-lg);
height: var(--space-lg);
}

View File

@@ -0,0 +1,58 @@
import { InfoIcon } from '@phosphor-icons/react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import type { CalloutProps } from './redesign';
import { Callout } from './redesign';
const meta = {
title: 'Redesign/Callout',
render(props) {
const actionProps = props.actionText
? fn().mockName('actionClick')
: undefined;
const secondaryActionProps = props.secondaryActionText
? fn().mockName('secondaryActionClick')
: undefined;
return (
<Callout
{...props}
{...actionProps}
{...secondaryActionProps}
icon={props.hasIcon ? InfoIcon : undefined}
/>
);
},
args: {
children: 'Main text goes here',
actionText: 'Action 1',
secondaryActionText: 'Action 2',
hasIcon: false,
},
argTypes: {
children: { control: 'text' },
size: { control: 'inline-radio', options: ['sm', 'lg'] },
actionText: { control: 'text' },
secondaryActionText: { control: 'text', if: { arg: 'size', neq: 'sm' } },
hasIcon: { control: 'boolean', if: { arg: 'size', neq: 'sm' } },
},
} satisfies Meta<
CalloutProps<React.ElementType> & {
as?: never;
hasIcon?: boolean;
children: string;
}
>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Large: Story = {
args: {
size: 'lg',
hasIcon: true,
},
};
export const Small: Story = { args: { size: 'sm' } };

View File

@@ -0,0 +1,95 @@
import type React from 'react';
import classNames from 'classnames';
import { Button } from '../button/redesign';
import { Icon } from '../icon';
import type { IconProp } from '../icon';
import classes from './redesign.module.scss';
type RenameKeysStartingWith<
TObject extends Record<string, unknown>,
TPrefix extends string,
TReplacement extends string,
> = {
[TKey in keyof TObject as TKey extends `${TPrefix}${infer TSuffix}`
? `${TReplacement}${TSuffix}`
: TKey]: TObject[TKey];
};
type ActionProps =
| { actionClick: () => void; actionText: React.ReactNode }
| { actionClick?: never; actionText?: never };
type MiniCalloutProps = {
size: 'sm';
} & ActionProps;
type LargeCalloutProps = {
size?: 'lg';
icon?: IconProp;
} & (
| (ActionProps & {
secondaryActionClick?: never;
secondaryActionText?: never;
})
| (Required<ActionProps> &
RenameKeysStartingWith<ActionProps, 'action', 'secondaryAction'>)
);
export type CalloutProps<As extends React.ElementType> = (
| MiniCalloutProps
| LargeCalloutProps
) & {
as?: As;
children: React.ReactNode;
className?: string;
} & React.ComponentPropsWithoutRef<As>;
export const Callout = <As extends React.ElementType>({
as: asComp,
size,
children,
className,
actionText,
actionClick,
...props
}: CalloutProps<As>) => {
const Comp = asComp ?? 'div';
const wrappedChildren = <div className={classes.textWrapper}>{children}</div>;
const actionButton = actionText ? (
<Button onClick={actionClick} size={size === 'sm' ? 'xs' : 'sm'}>
{actionText}
</Button>
) : null;
if (size === 'sm') {
return (
<Comp {...props} className={classNames(className, classes.rootSmall)}>
{wrappedChildren}
{actionButton}
</Comp>
);
}
const { icon, secondaryActionClick, secondaryActionText, ...restProps } =
props;
const secondaryButton = secondaryActionText ? (
<Button onClick={secondaryActionClick} size='sm' variant='ghost'>
{secondaryActionText}
</Button>
) : null;
return (
<Comp {...restProps} className={classNames(className, classes.rootLarge)}>
{icon && <Icon icon={icon} className={classes.icon} />}
{wrappedChildren}
{actionButton}
{secondaryButton}
</Comp>
);
};

View File

@@ -11,7 +11,7 @@ export type IconProp = React.FC<SVGPropsWithTitle>;
interface Props extends React.SVGProps<SVGSVGElement> {
children?: never;
id: string;
id?: string;
icon: IconProp;
noFill?: boolean;
}
@@ -46,7 +46,7 @@ export const Icon: React.FC<Props> = ({
<IconComponent
className={classNames(
'icon',
`icon-${id}`,
id && `icon-${id}`,
noFill && 'icon--no-fill',
className,
)}