Refactor modal (#18377)
## Summary - Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`, `ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as stateless, reusable components - Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai state (`isModalOpenedComponentState`) to the stateless `Modal` via an `isOpen` prop - Rename `modalVariant` prop to `overlay` with clearer values: `'dark'` (default), `'light'` (in-container), `'transparent'` (invisible panel). Remove unused `'medium'` overlay - Rename `modalId` to `modalInstanceId` across the entire modal zone (~30 consumer files) - Extract `ModalProps` to its own file in `twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to its own file using `Pick<ModalProps, ...>` for shared props - Extract `ModalBackdrop` to its own file and export from `twenty-ui`; use it in `UserOrMetadataLoader` instead of a local styled component - Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in `SpreadsheetImportStepperContainer` instead of duplicated `styled.div` definitions - Remove unused `onClose` prop from stateless `Modal`; fix `typeof document` guard in `ModalStatefulWrapper` - Split shared types into individual files: `ModalSize.ts`, `ModalPadding.ts`, `ModalOverlay.ts` - Extract wyw profiling instrumentation from `vite.config.ts` into reusable `createWywProfilingPlugin` with parametrized threshold and improved logging - Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`, `ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front` - Add comprehensive Storybook stories in `twenty-ui` covering Default, Confirmation, Small, ExtraLarge, Closed, and Interactive variants
This commit is contained in:
@@ -67,10 +67,7 @@ export const AvatarOrIcon = ({
|
||||
|
||||
if (isIconInverted || isDefined(IconBackgroundColor)) {
|
||||
return (
|
||||
<StyledAvatarOrIconWrapper
|
||||
isClickable={isClickable}
|
||||
onClick={onClick}
|
||||
>
|
||||
<StyledAvatarOrIconWrapper isClickable={isClickable} onClick={onClick}>
|
||||
<StyledIconWithBackgroundContainer
|
||||
backgroundColor={
|
||||
IconBackgroundColor ?? theme.background.invertedSecondary
|
||||
@@ -87,10 +84,7 @@ export const AvatarOrIcon = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAvatarOrIconWrapper
|
||||
isClickable={isClickable}
|
||||
onClick={onClick}
|
||||
>
|
||||
<StyledAvatarOrIconWrapper isClickable={isClickable} onClick={onClick}>
|
||||
<Icon
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
|
||||
@@ -140,8 +140,8 @@ const StyledContainer = styled.div<
|
||||
`;
|
||||
|
||||
const StyledRightComponentDivider = styled.div`
|
||||
border-left: 1px solid ${themeCssVariables.border.color.light};
|
||||
align-self: stretch;
|
||||
border-left: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
|
||||
const renderRightComponent = (
|
||||
@@ -155,7 +155,7 @@ const renderRightComponent = (
|
||||
const rendered =
|
||||
typeof rightComponent === 'function' ? rightComponent() : rightComponent;
|
||||
|
||||
if (rightComponentDivider) {
|
||||
if (rightComponentDivider === true) {
|
||||
return (
|
||||
<>
|
||||
<StyledRightComponentDivider />
|
||||
|
||||
@@ -13,4 +13,3 @@ export * from './layout';
|
||||
export * from './navigation';
|
||||
export * from './theme';
|
||||
export * from './utilities';
|
||||
|
||||
|
||||
@@ -41,10 +41,12 @@ const StyledJsonListBase = styled.ul<{
|
||||
padding: 0;
|
||||
display: grid;
|
||||
row-gap: ${themeCssVariables.spacing[2]};
|
||||
padding-left: ${({ depth }) => (depth > 0 ? themeCssVariables.spacing[8] : '0')};
|
||||
padding-left: ${({ depth }) =>
|
||||
depth > 0 ? themeCssVariables.spacing[8] : '0'};
|
||||
|
||||
> :first-of-type {
|
||||
margin-top: ${({ depth }) => (depth > 0 ? themeCssVariables.spacing[2] : '0')};
|
||||
margin-top: ${({ depth }) =>
|
||||
depth > 0 ? themeCssVariables.spacing[2] : '0'};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -39,6 +39,18 @@ export { Card } from './card/components/Card';
|
||||
export { CardContent } from './card/components/CardContent';
|
||||
export { CardFooter } from './card/components/CardFooter';
|
||||
export { CardHeader } from './card/components/CardHeader';
|
||||
export { Modal } from './modal/components/Modal';
|
||||
export { ModalBackdrop } from './modal/components/ModalBackdrop';
|
||||
export type { ModalContentProps } from './modal/components/ModalContent';
|
||||
export { ModalContent } from './modal/components/ModalContent';
|
||||
export type { ModalFooterProps } from './modal/components/ModalFooter';
|
||||
export { ModalFooter } from './modal/components/ModalFooter';
|
||||
export type { ModalHeaderProps } from './modal/components/ModalHeader';
|
||||
export { ModalHeader } from './modal/components/ModalHeader';
|
||||
export type { ModalOverlay } from './modal/types/ModalOverlay';
|
||||
export type { ModalPadding } from './modal/types/ModalPadding';
|
||||
export type { ModalProps } from './modal/types/ModalProps';
|
||||
export type { ModalSize } from './modal/types/ModalSize';
|
||||
export {
|
||||
SectionAlignment,
|
||||
SectionFontColor,
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
import { ThemeContext } from '@ui/theme';
|
||||
|
||||
import { type ModalOverlay } from '../types/ModalOverlay';
|
||||
import { type ModalPadding } from '../types/ModalPadding';
|
||||
import { type ModalProps } from '../types/ModalProps';
|
||||
import { type ModalSize } from '../types/ModalSize';
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
|
||||
const DEFAULT_MODAL_Z_INDEX = 40;
|
||||
const DEFAULT_BACKDROP_Z_INDEX = 39;
|
||||
|
||||
const StyledModalDiv = styled.div<{
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
isMobile: boolean;
|
||||
overlay: ModalOverlay;
|
||||
gap?: number;
|
||||
smallBorderRadius?: boolean;
|
||||
narrowWidth?: boolean;
|
||||
autoHeight?: boolean;
|
||||
modalZIndex: number;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: ${({ overlay }) =>
|
||||
overlay === 'dark'
|
||||
? themeCssVariables.boxShadow.superHeavy
|
||||
: overlay === 'transparent'
|
||||
? 'none'
|
||||
: themeCssVariables.boxShadow.strong};
|
||||
background: ${({ overlay }) =>
|
||||
overlay === 'transparent'
|
||||
? 'transparent'
|
||||
: themeCssVariables.background.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
border-radius: ${({ isMobile, overlay, smallBorderRadius }) => {
|
||||
if (isMobile === true || overlay === 'transparent') return '0';
|
||||
if (smallBorderRadius === true) return themeCssVariables.spacing[1];
|
||||
return themeCssVariables.border.radius.md;
|
||||
}};
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
z-index: ${({ modalZIndex }) => modalZIndex};
|
||||
|
||||
gap: ${({ gap }) =>
|
||||
gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'};
|
||||
|
||||
width: ${({ isMobile, size, narrowWidth }) => {
|
||||
if (narrowWidth === true)
|
||||
return `calc(400px - ${themeCssVariables.spacing[32]})`;
|
||||
if (isMobile)
|
||||
return themeCssVariables.modal.size.fullscreen.width ?? 'auto';
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return themeCssVariables.modal.size.sm.width ?? 'auto';
|
||||
case 'medium':
|
||||
return themeCssVariables.modal.size.md.width ?? 'auto';
|
||||
case 'large':
|
||||
return themeCssVariables.modal.size.lg.width ?? 'auto';
|
||||
case 'extraLarge':
|
||||
return themeCssVariables.modal.size.xl.width ?? 'auto';
|
||||
default:
|
||||
return 'auto';
|
||||
}
|
||||
}};
|
||||
|
||||
padding: ${({ padding }) => {
|
||||
switch (padding) {
|
||||
case 'none':
|
||||
return themeCssVariables.spacing[0];
|
||||
case 'small':
|
||||
return themeCssVariables.spacing[2];
|
||||
case 'medium':
|
||||
return themeCssVariables.spacing[4];
|
||||
case 'large':
|
||||
return themeCssVariables.spacing[6];
|
||||
default:
|
||||
return 'auto';
|
||||
}
|
||||
}};
|
||||
height: ${({ isMobile, size, autoHeight }) => {
|
||||
if (autoHeight === true) return 'auto';
|
||||
if (isMobile)
|
||||
return themeCssVariables.modal.size.fullscreen.height ?? 'auto';
|
||||
switch (size) {
|
||||
case 'extraLarge':
|
||||
return themeCssVariables.modal.size.xl.height ?? 'auto';
|
||||
default:
|
||||
return 'auto';
|
||||
}
|
||||
}};
|
||||
max-height: ${({ isMobile }) => (isMobile ? 'none' : '90dvh')};
|
||||
`;
|
||||
|
||||
const AnimatedModalDiv = motion.create(StyledModalDiv);
|
||||
const AnimatedBackdrop = motion.create(ModalBackdrop);
|
||||
|
||||
const modalAnimation = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
};
|
||||
|
||||
export const Modal = ({
|
||||
isOpen,
|
||||
children,
|
||||
size = 'medium',
|
||||
padding = 'medium',
|
||||
overlay = 'dark',
|
||||
isMobile = false,
|
||||
isInContainer = false,
|
||||
container,
|
||||
gap,
|
||||
smallBorderRadius,
|
||||
narrowWidth,
|
||||
autoHeight,
|
||||
modalZIndex = DEFAULT_MODAL_Z_INDEX,
|
||||
backdropZIndex = DEFAULT_BACKDROP_Z_INDEX,
|
||||
backdropTestId = 'modal-backdrop',
|
||||
backdropClickOutsideId,
|
||||
preventClickOutside,
|
||||
onBackdropMouseDown,
|
||||
modalRef: externalRef,
|
||||
}: ModalProps) => {
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const resolvedRef = externalRef ?? internalRef;
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const handleBackdropMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onBackdropMouseDown?.(e);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<AnimatePresence mode="wait">
|
||||
{isOpen && (
|
||||
<AnimatedBackdrop
|
||||
data-testid={backdropTestId}
|
||||
data-click-outside-id={backdropClickOutsideId}
|
||||
onMouseDown={handleBackdropMouseDown}
|
||||
overlay={overlay}
|
||||
backdropZIndex={backdropZIndex}
|
||||
isInContainer={isInContainer}
|
||||
>
|
||||
<AnimatedModalDiv
|
||||
ref={resolvedRef}
|
||||
size={size}
|
||||
padding={padding}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
layout
|
||||
overlay={overlay}
|
||||
variants={modalAnimation}
|
||||
transition={{ duration: theme.animation.duration.normal }}
|
||||
isMobile={isMobile}
|
||||
gap={gap}
|
||||
smallBorderRadius={smallBorderRadius}
|
||||
narrowWidth={narrowWidth}
|
||||
autoHeight={autoHeight}
|
||||
modalZIndex={modalZIndex}
|
||||
data-globally-prevent-click-outside={preventClickOutside}
|
||||
>
|
||||
{children}
|
||||
</AnimatedModalDiv>
|
||||
</AnimatedBackdrop>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
if (isDefined(container)) {
|
||||
return createPortal(content, container);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
import { type ModalOverlay } from '../types/ModalOverlay';
|
||||
|
||||
const StyledModalBackdrop = styled.div<{
|
||||
overlay: ModalOverlay;
|
||||
backdropZIndex: number;
|
||||
isInContainer?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ overlay, isInContainer }) =>
|
||||
isInContainer || overlay === 'light'
|
||||
? themeCssVariables.background.overlayTertiary
|
||||
: themeCssVariables.background.overlayPrimary};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
pointer-events: auto;
|
||||
position: ${({ isInContainer }) => (isInContainer ? 'absolute' : 'fixed')};
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: ${({ backdropZIndex }) => backdropZIndex};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const ModalBackdrop = StyledModalBackdrop;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledContent = styled.div<{
|
||||
isVerticallyCentered?: boolean;
|
||||
isHorizontallyCentered?: boolean;
|
||||
noPadding?: boolean;
|
||||
overflowHidden?: boolean;
|
||||
gap?: number;
|
||||
contentPadding?: number;
|
||||
}>`
|
||||
align-items: ${({ isVerticallyCentered }) =>
|
||||
isVerticallyCentered ? 'center' : 'stretch'};
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
gap: ${({ gap }) =>
|
||||
gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'};
|
||||
justify-content: ${({ isHorizontallyCentered }) =>
|
||||
isHorizontallyCentered ? 'center' : 'flex-start'};
|
||||
overflow: ${({ overflowHidden }) => (overflowHidden ? 'hidden' : 'visible')};
|
||||
padding: ${({ noPadding, contentPadding }) => {
|
||||
if (noPadding === true) return '0';
|
||||
if (contentPadding !== undefined)
|
||||
return `var(--t-spacing-${contentPadding})`;
|
||||
return themeCssVariables.spacing[10];
|
||||
}};
|
||||
`;
|
||||
|
||||
export type ModalContentProps = React.PropsWithChildren & {
|
||||
isVerticallyCentered?: boolean;
|
||||
isHorizontallyCentered?: boolean;
|
||||
noPadding?: boolean;
|
||||
overflowHidden?: boolean;
|
||||
gap?: number;
|
||||
contentPadding?: number;
|
||||
};
|
||||
|
||||
export const ModalContent = ({
|
||||
children,
|
||||
isVerticallyCentered,
|
||||
isHorizontallyCentered,
|
||||
noPadding,
|
||||
overflowHidden,
|
||||
gap,
|
||||
contentPadding,
|
||||
}: ModalContentProps) => (
|
||||
<StyledContent
|
||||
isVerticallyCentered={isVerticallyCentered}
|
||||
isHorizontallyCentered={isHorizontallyCentered}
|
||||
noPadding={noPadding}
|
||||
overflowHidden={overflowHidden}
|
||||
gap={gap}
|
||||
contentPadding={contentPadding}
|
||||
>
|
||||
{children}
|
||||
</StyledContent>
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledFooter = styled.div<{
|
||||
autoHeight?: boolean;
|
||||
centered?: boolean;
|
||||
smallPadding?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
height: ${({ autoHeight }) => (autoHeight ? 'auto' : '60px')};
|
||||
justify-content: ${({ centered }) => (centered ? 'center' : 'flex-end')};
|
||||
overflow: hidden;
|
||||
padding: ${({ smallPadding }) =>
|
||||
smallPadding ? themeCssVariables.spacing[3] : themeCssVariables.spacing[5]};
|
||||
`;
|
||||
|
||||
export type ModalFooterProps = React.PropsWithChildren & {
|
||||
autoHeight?: boolean;
|
||||
centered?: boolean;
|
||||
smallPadding?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModalFooter = ({
|
||||
children,
|
||||
autoHeight,
|
||||
centered,
|
||||
smallPadding,
|
||||
className,
|
||||
}: ModalFooterProps) => (
|
||||
<StyledFooter
|
||||
autoHeight={autoHeight}
|
||||
centered={centered}
|
||||
smallPadding={smallPadding}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</StyledFooter>
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { MOBILE_VIEWPORT, themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledHeader = styled.div<{
|
||||
noPadding?: boolean;
|
||||
autoHeight?: boolean;
|
||||
hasBorderBottom?: boolean;
|
||||
paddingHorizontal?: number;
|
||||
backgroundColor?: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
height: ${({ autoHeight }) => (autoHeight ? 'auto' : '60px')};
|
||||
overflow: hidden;
|
||||
padding: ${({ noPadding, paddingHorizontal }) => {
|
||||
if (paddingHorizontal !== undefined)
|
||||
return `0 var(--t-spacing-${paddingHorizontal})`;
|
||||
if (noPadding === true) return '0';
|
||||
return themeCssVariables.spacing[5];
|
||||
}};
|
||||
background-color: ${({ backgroundColor }) => backgroundColor ?? 'unset'};
|
||||
border-bottom: ${({ hasBorderBottom }) =>
|
||||
hasBorderBottom
|
||||
? `1px solid ${themeCssVariables.border.color.medium}`
|
||||
: 'none'};
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
${({ paddingHorizontal }) =>
|
||||
paddingHorizontal !== undefined
|
||||
? `padding-left: ${themeCssVariables.spacing[4]}; padding-right: ${themeCssVariables.spacing[4]};`
|
||||
: ''}
|
||||
}
|
||||
`;
|
||||
|
||||
export type ModalHeaderProps = React.PropsWithChildren & {
|
||||
noPadding?: boolean;
|
||||
autoHeight?: boolean;
|
||||
hasBorderBottom?: boolean;
|
||||
paddingHorizontal?: number;
|
||||
backgroundColor?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModalHeader = ({
|
||||
children,
|
||||
noPadding,
|
||||
autoHeight,
|
||||
hasBorderBottom,
|
||||
paddingHorizontal,
|
||||
backgroundColor,
|
||||
className,
|
||||
}: ModalHeaderProps) => (
|
||||
<StyledHeader
|
||||
noPadding={noPadding}
|
||||
autoHeight={autoHeight}
|
||||
hasBorderBottom={hasBorderBottom}
|
||||
paddingHorizontal={paddingHorizontal}
|
||||
backgroundColor={backgroundColor}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</StyledHeader>
|
||||
);
|
||||
@@ -0,0 +1,244 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { H1Title, H1TitleFontColor, H2Title, IconX } from '@ui/display';
|
||||
import { Button, IconButton } from '@ui/input';
|
||||
import { Section, SectionAlignment, SectionFontColor } from '@ui/layout';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
import { Modal } from '../Modal';
|
||||
import { ModalContent } from '../ModalContent';
|
||||
import { ModalFooter } from '../ModalFooter';
|
||||
import { ModalHeader } from '../ModalHeader';
|
||||
|
||||
const StyledCenteredTitle = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledSection = styled(Section)`
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof Modal> = {
|
||||
title: 'UI/Layout/Modal/Modal',
|
||||
component: Modal,
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'medium', 'large', 'extraLarge'],
|
||||
},
|
||||
padding: {
|
||||
control: 'select',
|
||||
options: ['none', 'small', 'medium', 'large'],
|
||||
},
|
||||
overlay: {
|
||||
control: 'select',
|
||||
options: ['light', 'dark', 'transparent'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Modal>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'medium',
|
||||
padding: 'none',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalHeader>
|
||||
<H2Title
|
||||
title="Edit workspace"
|
||||
description="Update your workspace settings"
|
||||
/>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>
|
||||
Workspace name and subdomain can be changed from the settings panel.
|
||||
These changes will be reflected across all members.
|
||||
</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button title="Cancel" variant="secondary" />
|
||||
<Button title="Save" variant="primary" accent="blue" />
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const Confirmation: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
padding: 'large',
|
||||
overlay: 'dark',
|
||||
smallBorderRadius: true,
|
||||
narrowWidth: true,
|
||||
autoHeight: true,
|
||||
gap: 2,
|
||||
},
|
||||
render: ({
|
||||
isOpen,
|
||||
padding,
|
||||
overlay,
|
||||
smallBorderRadius,
|
||||
narrowWidth,
|
||||
autoHeight,
|
||||
gap,
|
||||
}) => (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
padding={padding}
|
||||
overlay={overlay}
|
||||
smallBorderRadius={smallBorderRadius}
|
||||
narrowWidth={narrowWidth}
|
||||
autoHeight={autoHeight}
|
||||
gap={gap}
|
||||
>
|
||||
<StyledCenteredTitle>
|
||||
<H1Title title="Delete record?" fontColor={H1TitleFontColor.Primary} />
|
||||
</StyledCenteredTitle>
|
||||
<StyledSection
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
This action cannot be undone. The record and all of its data will be
|
||||
permanently removed.
|
||||
</StyledSection>
|
||||
<Button title="Cancel" variant="secondary" fullWidth justify="center" />
|
||||
<Button
|
||||
title="Delete"
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
fullWidth
|
||||
justify="center"
|
||||
/>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'small',
|
||||
padding: 'none',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalHeader>
|
||||
<H2Title title="Archive item" />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>Are you sure you want to archive this item?</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button title="No" variant="secondary" />
|
||||
<Button title="Yes, archive" variant="primary" accent="blue" />
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'extraLarge',
|
||||
padding: 'none',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalHeader>
|
||||
<H2Title
|
||||
title="Import contacts"
|
||||
description="Upload a CSV file to import your contacts"
|
||||
/>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>
|
||||
The file should include columns for name, email, phone, and company.
|
||||
Drag and drop your CSV file here, or click to browse.
|
||||
</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button title="Cancel" variant="secondary" />
|
||||
<Button title="Upload & import" variant="primary" accent="blue" />
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const Closed: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
size: 'medium',
|
||||
padding: 'medium',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalContent>This should not be visible.</ModalContent>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
const InteractiveModal = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title="Open Modal"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size="medium"
|
||||
padding="none"
|
||||
overlay="dark"
|
||||
onBackdropMouseDown={() => setIsOpen(false)}
|
||||
>
|
||||
<ModalHeader>
|
||||
<H2Title title="Create record" />
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>
|
||||
Fill in the details below to create a new record. All fields are
|
||||
optional.
|
||||
</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
title="Cancel"
|
||||
variant="secondary"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<Button
|
||||
title="Create"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
render: () => <InteractiveModal />,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export { Modal } from './components/Modal';
|
||||
export { ModalBackdrop } from './components/ModalBackdrop';
|
||||
export { ModalContent } from './components/ModalContent';
|
||||
export type { ModalContentProps } from './components/ModalContent';
|
||||
export { ModalFooter } from './components/ModalFooter';
|
||||
export type { ModalFooterProps } from './components/ModalFooter';
|
||||
export { ModalHeader } from './components/ModalHeader';
|
||||
export type { ModalHeaderProps } from './components/ModalHeader';
|
||||
export type { ModalOverlay } from './types/ModalOverlay';
|
||||
export type { ModalPadding } from './types/ModalPadding';
|
||||
export type { ModalProps } from './types/ModalProps';
|
||||
export type { ModalSize } from './types/ModalSize';
|
||||
@@ -0,0 +1 @@
|
||||
export type ModalOverlay = 'light' | 'dark' | 'transparent';
|
||||
@@ -0,0 +1 @@
|
||||
export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
|
||||
@@ -0,0 +1,26 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { type ModalOverlay } from './ModalOverlay';
|
||||
import { type ModalPadding } from './ModalPadding';
|
||||
import { type ModalSize } from './ModalSize';
|
||||
|
||||
export type ModalProps = React.PropsWithChildren & {
|
||||
isOpen: boolean;
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
overlay?: ModalOverlay;
|
||||
isMobile?: boolean;
|
||||
isInContainer?: boolean;
|
||||
container?: HTMLElement | null;
|
||||
gap?: number;
|
||||
smallBorderRadius?: boolean;
|
||||
narrowWidth?: boolean;
|
||||
autoHeight?: boolean;
|
||||
modalZIndex?: number;
|
||||
backdropZIndex?: number;
|
||||
backdropTestId?: string;
|
||||
backdropClickOutsideId?: string;
|
||||
preventClickOutside?: boolean;
|
||||
onBackdropMouseDown?: (e: React.MouseEvent) => void;
|
||||
modalRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type ModalSize = 'small' | 'medium' | 'large' | 'extraLarge';
|
||||
File diff suppressed because it is too large
Load Diff
+1024
-991
File diff suppressed because one or more lines are too long
+1018
-991
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user