mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 09:35:54 -05:00
Redesign: Callouts (#40211)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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' } };
|
||||
95
app/javascript/mastodon/components/callout/redesign.tsx
Normal file
95
app/javascript/mastodon/components/callout/redesign.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user