diff --git a/app/javascript/mastodon/components/modal_shell/redesign.module.scss b/app/javascript/mastodon/components/modal_shell/redesign.module.scss new file mode 100644 index 00000000000..01f99a914f0 --- /dev/null +++ b/app/javascript/mastodon/components/modal_shell/redesign.module.scss @@ -0,0 +1,38 @@ +@use '@/styles/mastodon/mixins'; + +.modal { + @include mixins.elevation-1; + + border-radius: var(--radius-xl); + background: var(--color-bg-primary); + display: flex; + flex-direction: column; + gap: var(--space-md); + padding: var(--space-md); + pointer-events: auto; + user-select: text; + overflow: hidden; +} + +.modalElevated { + @include mixins.elevation-2; +} + +.title { + @include mixins.type-heading-sm; +} + +.actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-md); +} + +.actionsLeft { + justify-content: flex-start; +} + +.actionsRight { + justify-content: flex-end; +} diff --git a/app/javascript/mastodon/components/modal_shell/redesign.stories.tsx b/app/javascript/mastodon/components/modal_shell/redesign.stories.tsx new file mode 100644 index 00000000000..ee17187fb1c --- /dev/null +++ b/app/javascript/mastodon/components/modal_shell/redesign.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Button } from '../button/redesign'; + +import type { ModalShellProps } from './redesign'; +import { ModalShell, ModalActions, ModalTitle } from './redesign'; + +const meta = { + title: 'Redesign/Modal', + args: { + actionAlign: 'stretch', + children: 'This is a modal.', + elevation: 2, + maxWidth: 0, + }, + argTypes: { + actionAlign: { + control: 'inline-radio', + options: ['left', 'right', 'stretch'], + }, + elevation: { + control: 'inline-radio', + options: [1, 2], + }, + }, + render({ actionAlign, children, ...props }) { + return ( + + Modal Example + + {children + .split('\n') + .filter((line) => !!line.trim()) + .map((line, index) => ( +

{line}

+ ))} + + + + + + +
+ ); + }, +} satisfies Meta< + Pick & { + children: string; + actionAlign?: 'left' | 'right' | 'stretch'; + } +>; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/app/javascript/mastodon/components/modal_shell/redesign.tsx b/app/javascript/mastodon/components/modal_shell/redesign.tsx new file mode 100644 index 00000000000..e224cae091b --- /dev/null +++ b/app/javascript/mastodon/components/modal_shell/redesign.tsx @@ -0,0 +1,101 @@ +import type React from 'react'; + +import classNames from 'classnames'; + +import type { PolymorphicProps } from '@/types/polymorphic'; + +import { NavigationFocusTarget } from '../navigation_focus_target'; + +import classes from './redesign.module.scss'; + +export interface ModalShellProps { + children: React.ReactNode; + className?: string; + elevation?: 1 | 2; + maxWidth?: number; + style?: React.CSSProperties; +} + +export const ModalShell = ({ + as: asComp, + elevation, + className, + children, + maxWidth, + style, + ...props +}: PolymorphicProps) => { + const Comp = asComp ?? 'div'; + return ( + + {children} + + ); +}; + +type HeadingLevels = 1 | 2 | 3 | 4 | 5 | 6; + +type ModalTitleProps = { children: React.ReactNode; level?: HeadingLevels } & ( + | { noFocus: true } + | { noFocus?: false; focusTargetName?: string } +); + +export const ModalTitle: React.FC< + ModalTitleProps & React.ComponentPropsWithRef<`h${HeadingLevels}`> +> = ({ level = 1, children, className, noFocus, ...props }) => { + const Header = `h${level}` as const; + if (!noFocus) { + return ( + + {children} + + ); + } + return ( +
+ {children} +
+ ); +}; + +export const ModalActions = ({ + align, + as: asComp, + children, + className, + ...props +}: PolymorphicProps< + { + align?: 'left' | 'right' | 'stretch'; + children?: React.ReactNode; + className?: string; + }, + As +>) => { + const Comp = asComp ?? 'div'; + + return ( + + {children} + + ); +}; diff --git a/app/javascript/types/polymorphic.ts b/app/javascript/types/polymorphic.ts index e58aa7b75ee..32c948b992f 100644 --- a/app/javascript/types/polymorphic.ts +++ b/app/javascript/types/polymorphic.ts @@ -38,7 +38,7 @@ type ElementRef = * Merge additional props with intrinsic/element props for `as`. * Additional props win on conflicts. */ -type PolymorphicProps< +type OldPolymorphicProps< As extends ElementType, AdditionalProps extends object = object, > = AdditionalProps & @@ -52,7 +52,9 @@ type PolymorphicWithRef< DefaultAs extends ElementType, AdditionalProps extends object = object, > = ( - props: PolymorphicProps & { ref?: Ref> }, + props: OldPolymorphicProps & { + ref?: Ref>; + }, ) => ReactElement | null; /** @@ -64,12 +66,22 @@ type PolyRefFunction = < >( render: ForwardRefRenderFunction< ElementRef, - PolymorphicProps + OldPolymorphicProps >, ) => PolymorphicWithRef & - ForwardRefExoticComponent>; + ForwardRefExoticComponent>; /** * Polymorphic `forwardRef` function. */ export const polymorphicForwardRef = forwardRef as PolyRefFunction; + +// Use this for modern React 19 props + +export type PolymorphicProps< + AdditionalProps extends object, + As extends React.ElementType, +> = { + as?: As; +} & Omit, keyof AdditionalProps | 'as'> & + AdditionalProps;