From 87db34be6379f89bd7f78c0680e075e98e861e11 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 19 Aug 2026 15:52:52 +0000 Subject: [PATCH] Redesign: Callouts (#40211) --- .../components/callout/redesign.module.scss | 28 ++++++ .../components/callout/redesign.stories.tsx | 58 +++++++++++ .../mastodon/components/callout/redesign.tsx | 95 +++++++++++++++++++ app/javascript/mastodon/components/icon.tsx | 4 +- 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 app/javascript/mastodon/components/callout/redesign.module.scss create mode 100644 app/javascript/mastodon/components/callout/redesign.stories.tsx create mode 100644 app/javascript/mastodon/components/callout/redesign.tsx diff --git a/app/javascript/mastodon/components/callout/redesign.module.scss b/app/javascript/mastodon/components/callout/redesign.module.scss new file mode 100644 index 00000000000..08917807b14 --- /dev/null +++ b/app/javascript/mastodon/components/callout/redesign.module.scss @@ -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); +} diff --git a/app/javascript/mastodon/components/callout/redesign.stories.tsx b/app/javascript/mastodon/components/callout/redesign.stories.tsx new file mode 100644 index 00000000000..2a91f5be9a6 --- /dev/null +++ b/app/javascript/mastodon/components/callout/redesign.stories.tsx @@ -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 ( + + ); + }, + 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 & { + as?: never; + hasIcon?: boolean; + children: string; + } +>; + +export default meta; + +type Story = StoryObj; + +export const Large: Story = { + args: { + size: 'lg', + hasIcon: true, + }, +}; + +export const Small: Story = { args: { size: 'sm' } }; diff --git a/app/javascript/mastodon/components/callout/redesign.tsx b/app/javascript/mastodon/components/callout/redesign.tsx new file mode 100644 index 00000000000..982e50a1420 --- /dev/null +++ b/app/javascript/mastodon/components/callout/redesign.tsx @@ -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, + 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 & + RenameKeysStartingWith) +); + +export type CalloutProps = ( + | MiniCalloutProps + | LargeCalloutProps +) & { + as?: As; + children: React.ReactNode; + className?: string; +} & React.ComponentPropsWithoutRef; + +export const Callout = ({ + as: asComp, + size, + children, + className, + actionText, + actionClick, + ...props +}: CalloutProps) => { + const Comp = asComp ?? 'div'; + + const wrappedChildren =
{children}
; + + const actionButton = actionText ? ( + + ) : null; + + if (size === 'sm') { + return ( + + {wrappedChildren} + {actionButton} + + ); + } + + const { icon, secondaryActionClick, secondaryActionText, ...restProps } = + props; + + const secondaryButton = secondaryActionText ? ( + + ) : null; + + return ( + + {icon && } + {wrappedChildren} + {actionButton} + {secondaryButton} + + ); +}; diff --git a/app/javascript/mastodon/components/icon.tsx b/app/javascript/mastodon/components/icon.tsx index f93e3757a38..688a5a5781f 100644 --- a/app/javascript/mastodon/components/icon.tsx +++ b/app/javascript/mastodon/components/icon.tsx @@ -11,7 +11,7 @@ export type IconProp = React.FC; interface Props extends React.SVGProps { children?: never; - id: string; + id?: string; icon: IconProp; noFill?: boolean; } @@ -46,7 +46,7 @@ export const Icon: React.FC = ({