Refactor UI folder (#2016)

* Added Overview page

* Revised Getting Started page

* Minor revision

* Edited readme, minor modifications to docs

* Removed sweep.yaml, .devcontainer, .ergomake

* Moved security.md to .github, added contributing.md

* changes as per code review

* updated contributing.md

* fixed broken links & added missing links in doc, improved structure

* fixed link in wsl setup

* fixed server link, added https cloning in yarn-setup

* removed package-lock.json

* added doc card, admonitions

* removed underline from nav buttons

* refactoring modules/ui

* refactoring modules/ui

* Change folder case

* Fix theme location

* Fix case 2

* Fix storybook

---------

Co-authored-by: Nimra Ahmed <nimra1408@gmail.com>
Co-authored-by: Nimra Ahmed <50912134+nimraahmed@users.noreply.github.com>
This commit is contained in:
Charles Bochet
2023-10-14 00:04:29 +02:00
committed by GitHub
parent a35ea5e8f9
commit 258685467b
732 changed files with 1106 additions and 1010 deletions

View File

@ -0,0 +1,40 @@
import { useTheme } from '@emotion/react';
import { motion } from 'framer-motion';
export type AnimatedCheckmarkProps = React.ComponentProps<
typeof motion.path
> & {
isAnimating?: boolean;
color?: string;
duration?: number;
size?: number;
};
export const AnimatedCheckmark = ({
isAnimating = false,
color,
duration = 0.5,
size = 28,
}: AnimatedCheckmarkProps) => {
const theme = useTheme();
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 52 52"
width={size}
height={size}
>
<motion.path
fill="none"
stroke={color ?? theme.grayScale.gray0}
strokeWidth={4}
d="M14 27l7.8 7.8L38 14"
pathLength="1"
strokeDasharray="1"
strokeDashoffset={isAnimating ? '1' : '0'}
animate={{ strokeDashoffset: isAnimating ? '0' : '1' }}
transition={{ duration }}
/>
</svg>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/display/icon';
const StyledContainer = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.color.blue};
border-radius: 50%;
display: flex;
height: 20px;
justify-content: center;
width: 20px;
`;
export type CheckmarkProps = React.ComponentPropsWithoutRef<'div'>;
export const Checkmark = (_props: CheckmarkProps) => {
const theme = useTheme();
return (
<StyledContainer>
<IconCheck color={theme.grayScale.gray0} size={14} />
</StyledContainer>
);
};

View File

@ -0,0 +1,20 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Checkmark } from '../Checkmark';
const meta: Meta<typeof Checkmark> = {
title: 'UI/Checkmark/Checkmark',
component: Checkmark,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof Checkmark>;
export const Default: Story = { args: {} };
export const WithCustomStyles: Story = {
args: { style: { backgroundColor: 'red', height: 40, width: 40 } },
};

View File

@ -0,0 +1,145 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
export enum ChipSize {
Large = 'large',
Small = 'small',
}
export enum ChipAccent {
TextPrimary = 'text-primary',
TextSecondary = 'text-secondary',
}
export enum ChipVariant {
Highlighted = 'highlighted',
Regular = 'regular',
Transparent = 'transparent',
Rounded = 'rounded',
}
type ChipProps = {
size?: ChipSize;
disabled?: boolean;
clickable?: boolean;
label: string;
maxWidth?: string;
variant?: ChipVariant;
accent?: ChipAccent;
leftComponent?: React.ReactNode;
rightComponent?: React.ReactNode;
className?: string;
};
const StyledContainer = styled.div<Partial<ChipProps>>`
align-items: center;
background-color: ${({ theme, variant }) =>
variant === ChipVariant.Highlighted
? theme.background.transparent.light
: variant === ChipVariant.Rounded
? theme.background.transparent.lighter
: 'transparent'};
border-color: ${({ theme, variant }) =>
variant === ChipVariant.Rounded ? theme.border.color.medium : 'none'};
border-radius: ${({ theme, variant }) =>
variant === ChipVariant.Rounded ? '50px' : theme.border.radius.sm};
border-style: ${({ variant }) =>
variant === ChipVariant.Rounded ? 'solid' : 'none'};
border-width: ${({ variant }) =>
variant === ChipVariant.Rounded ? '1px' : '0px'};
color: ${({ theme, disabled, accent }) =>
disabled
? theme.font.color.light
: accent === ChipAccent.TextPrimary
? theme.font.color.primary
: theme.font.color.secondary};
cursor: ${({ clickable, disabled, variant }) =>
disabled || variant === ChipVariant.Transparent
? 'inherit'
: clickable
? 'pointer'
: 'inherit'};
display: inline-flex;
font-weight: ${({ theme, accent }) =>
accent === ChipAccent.TextSecondary ? theme.font.weight.medium : 'inherit'};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === ChipSize.Large ? '16px' : '12px')};
max-width: ${({ maxWidth }) => (maxWidth ? maxWidth : '200px')};
overflow: hidden;
padding: ${({ theme, variant }) =>
variant === ChipVariant.Rounded ? '3px 8px' : theme.spacing(1)};
user-select: none;
:hover {
${({ variant, theme, disabled }) => {
if (!disabled) {
return (
'background-color: ' +
(variant === ChipVariant.Highlighted
? theme.background.transparent.medium
: variant === ChipVariant.Regular
? theme.background.transparent.light
: 'transparent') +
';'
);
}
}}
}
:active {
${({ variant, theme, disabled }) => {
if (!disabled) {
return (
'background-color: ' +
(variant === ChipVariant.Highlighted
? theme.background.transparent.strong
: variant === ChipVariant.Regular
? theme.background.transparent.medium
: 'transparent') +
';'
);
}
}}
}
`;
const StyledLabel = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const Chip = ({
size = ChipSize.Small,
label,
disabled = false,
clickable = true,
variant = ChipVariant.Regular,
leftComponent,
rightComponent,
accent = ChipAccent.TextPrimary,
maxWidth,
className,
}: ChipProps) => (
<StyledContainer
data-testid="chip"
clickable={clickable}
variant={variant}
accent={accent}
size={size}
disabled={disabled}
className={className}
maxWidth={maxWidth}
>
{leftComponent}
<StyledLabel>
<OverflowingTextWithTooltip text={label} />
</StyledLabel>
{rightComponent}
</StyledContainer>
);

View File

@ -0,0 +1,76 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { Avatar, AvatarType } from '@/users/components/Avatar';
import { isNonEmptyString } from '~/utils/isNonEmptyString';
import { Chip, ChipVariant } from './Chip';
type EntityChipProps = {
linkToEntity?: string;
entityId: string;
name: string;
pictureUrl?: string;
avatarType?: AvatarType;
variant?: EntityChipVariant;
LeftIcon?: IconComponent;
};
export enum EntityChipVariant {
Regular = 'regular',
Transparent = 'transparent',
}
export const EntityChip = ({
linkToEntity,
entityId,
name,
pictureUrl,
avatarType = 'rounded',
variant = EntityChipVariant.Regular,
LeftIcon,
}: EntityChipProps) => {
const navigate = useNavigate();
const theme = useTheme();
const handleLinkClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (linkToEntity) {
event.preventDefault();
event.stopPropagation();
navigate(linkToEntity);
}
};
return isNonEmptyString(name) ? (
<div onClick={handleLinkClick}>
<Chip
label={name}
variant={
linkToEntity
? variant === EntityChipVariant.Regular
? ChipVariant.Highlighted
: ChipVariant.Regular
: ChipVariant.Transparent
}
leftComponent={
LeftIcon ? (
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
) : (
<Avatar
avatarUrl={pictureUrl}
colorId={entityId}
placeholder={name}
size="sm"
type={avatarType}
/>
)
}
/>
</div>
) : (
<></>
);
};

View File

@ -0,0 +1,69 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { Chip, ChipAccent, ChipSize, ChipVariant } from '../Chip';
const meta: Meta<typeof Chip> = {
title: 'UI/Chip/Chip',
component: Chip,
};
export default meta;
type Story = StoryObj<typeof Chip>;
export const Default: Story = {
args: {
label: 'Chip test',
size: ChipSize.Small,
variant: ChipVariant.Highlighted,
accent: ChipAccent.TextPrimary,
disabled: false,
clickable: true,
maxWidth: '200px',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof Chip> = {
args: { clickable: true, label: 'Hello' },
argTypes: {
size: { control: false },
variant: { control: false },
disabled: { control: false },
className: { control: false },
rightComponent: { control: false },
leftComponent: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'states',
values: ['default', 'hover', 'active', 'disabled'],
props: (state: string) =>
state === 'default' ? {} : { className: state },
},
{
name: 'variants',
values: Object.values(ChipVariant),
props: (variant: ChipVariant) => ({ variant }),
},
{
name: 'sizes',
values: Object.values(ChipSize),
props: (size: ChipSize) => ({ size }),
},
{
name: 'accents',
values: Object.values(ChipAccent),
props: (accent: ChipAccent) => ({ accent }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,21 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { EntityChip } from '../EntityChip';
const meta: Meta<typeof EntityChip> = {
title: 'UI/Chip/EntityChip',
component: EntityChip,
decorators: [ComponentWithRouterDecorator],
args: {
name: 'Entity name',
linkToEntity: '/entity-link',
avatarType: 'squared',
},
};
export default meta;
type Story = StoryObj<typeof EntityChip>;
export const Default: Story = {};

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-address-book" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M20 6v12a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2z" />
<path d="M10 16h6" />
<path d="M13 11m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 8h3" />
<path d="M4 12h3" />
<path d="M4 16h3" />
</svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@ -0,0 +1,12 @@
import { TablerIconsProps } from '@/ui/display/icon';
import { ReactComponent as IconAddressBookRaw } from '../assets/address-book.svg';
type IconAddressBookProps = TablerIconsProps;
export const IconAddressBook = (props: IconAddressBookProps): JSX.Element => {
const size = props.size ?? 24;
const stroke = props.stroke ?? 2;
return <IconAddressBookRaw height={size} width={size} strokeWidth={stroke} />;
};

View File

@ -0,0 +1,86 @@
/* eslint-disable no-restricted-imports */
export type { TablerIconsProps } from '@tabler/icons-react';
export {
IconAlertCircle,
IconAlertTriangle,
IconArchive,
IconArrowBack,
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconArrowUp,
IconArrowUpRight,
IconBell,
IconBrandGithub,
IconBrandGoogle,
IconBrandLinkedin,
IconBrandTwitter,
IconBrandX,
IconBriefcase,
IconBuildingSkyscraper,
IconCalendar,
IconCalendarEvent,
IconCheck,
IconCheckbox,
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsRight,
IconChevronUp,
IconCircleDot,
IconCirclePlus,
IconColorSwatch,
IconMessageCircle as IconComment,
IconCopy,
IconCross,
IconCurrencyDollar,
IconDotsVertical,
IconEye,
IconEyeOff,
IconFileImport,
IconFileUpload,
IconForbid,
IconFreeRights,
IconGraph,
IconGripVertical,
IconHeadphones,
IconHeart,
IconHeartOff,
IconHelpCircle,
IconHierarchy2,
IconInbox,
IconLayoutKanban,
IconLayoutSidebarLeftCollapse,
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
IconLink,
IconLinkOff,
IconList,
IconLogout,
IconLuggage,
IconMail,
IconMap,
IconMinus,
IconMoneybag,
IconNotes,
IconNumbers,
IconPencil,
IconPhone,
IconPlane,
IconPlug,
IconPlus,
IconProgressCheck,
IconSearch,
IconSettings,
IconSocial,
IconTag,
IconTarget,
IconTargetArrow,
IconTimelineEvent,
IconTrash,
IconUpload,
IconUser,
IconUserCircle,
IconUsers,
IconX,
} from '@tabler/icons-react';

View File

@ -0,0 +1,6 @@
import { FunctionComponent } from 'react';
export type IconComponent = FunctionComponent<{
size?: number;
stroke?: number;
}>;

View File

@ -0,0 +1,20 @@
import styled from '@emotion/styled';
const StyledSoonPill = styled.span`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: 50px;
color: ${({ theme }) => theme.font.color.light};
display: flex;
font-size: ${({ theme }) => theme.font.size.xs};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(4)};
justify-content: flex-end;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin-left: auto;
padding: ${({ theme }) => `0 ${theme.spacing(2)}`};
`;
export const SoonPill = () => <StyledSoonPill>Soon</StyledSoonPill>;

View File

@ -0,0 +1,16 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { SoonPill } from '../SoonPill';
const meta: Meta<typeof SoonPill> = {
title: 'UI/Accessories/SoonPill',
component: SoonPill,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof SoonPill>;
export const Default: Story = {};

View File

@ -0,0 +1,58 @@
import styled from '@emotion/styled';
import { ThemeColor } from '@/ui/theme/constants/colors';
const tagColors = [
'green',
'turquoise',
'sky',
'blue',
'purple',
'pink',
'red',
'orange',
'yellow',
'gray',
];
export type TagColor = (typeof tagColors)[number];
export const castToTagColor = (color: string): TagColor =>
tagColors.find((tagColor) => tagColor === color) ?? 'gray';
const StyledTag = styled.h3<{
color: TagColor;
}>`
align-items: center;
background: ${({ color, theme }) => theme.tag.background[color]};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ color, theme }) => theme.tag.text[color]};
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(2)};
margin: 0;
padding-bottom: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(1)};
`;
export type TagProps = {
className?: string;
color: ThemeColor;
text: string;
onClick?: () => void;
};
export const Tag = ({ className, color, text, onClick }: TagProps) => (
<StyledTag
className={className}
color={castToTagColor(color)}
onClick={onClick}
>
{text}
</StyledTag>
);

View File

@ -0,0 +1,65 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent } from '@storybook/testing-library';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { Tag, TagColor } from '../Tag';
const meta: Meta<typeof Tag> = {
title: 'UI/Tag/Tag',
component: Tag,
};
export default meta;
type Story = StoryObj<typeof Tag>;
export const Default: Story = {
args: {
text: 'Urgent',
color: 'red',
},
argTypes: { onClick: { action: 'clicked' } },
decorators: [ComponentDecorator],
play: async ({ canvasElement, args }) => {
const tag = canvasElement.querySelector('h3');
if (!tag) throw new Error('Tag not found');
await userEvent.click(tag);
await expect(args.onClick).toHaveBeenCalled();
},
};
export const Catalog: CatalogStory<Story, typeof Tag> = {
args: { text: 'Urgent' },
argTypes: {
color: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'colors',
values: [
'green',
'turquoise',
'sky',
'blue',
'purple',
'pink',
'red',
'orange',
'yellow',
'gray',
] satisfies TagColor[],
props: (color: ThemeColor) => ({ color }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,70 @@
import { PlacesType, PositionStrategy, Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { rgba } from '../../theme/constants/colors';
export enum TooltipPosition {
Top = 'top',
Left = 'left',
Right = 'right',
Bottom = 'bottom',
}
const StyledAppTooltip = styled(Tooltip)`
backdrop-filter: ${({ theme }) => theme.blur.strong};
background-color: ${({ theme }) => rgba(theme.color.gray80, 0.8)};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme }) => theme.grayScale.gray0};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
max-width: 40%;
overflow: visible;
padding: ${({ theme }) => theme.spacing(2)};
word-break: break-word;
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
export type AppTooltipProps = {
className?: string;
anchorSelect?: string;
content?: string;
delayHide?: number;
offset?: number;
noArrow?: boolean;
isOpen?: boolean;
place?: PlacesType;
positionStrategy?: PositionStrategy;
};
export const AppTooltip = ({
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
}: AppTooltipProps) => (
<StyledAppTooltip
{...{
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
}}
/>
);

View File

@ -0,0 +1,77 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { v4 as uuidV4 } from 'uuid';
import { AppTooltip } from './AppTooltip';
const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-decoration: inherit;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const OverflowingTextWithTooltip = ({
text,
}: {
text: string | null | undefined;
}) => {
const textElementId = `title-id-${uuidV4()}`;
const textRef = useRef<HTMLDivElement>(null);
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
useEffect(() => {
const isOverflowing =
(text?.length ?? 0) > 0 && textRef.current
? textRef.current?.scrollHeight > textRef.current?.clientHeight ||
textRef.current.scrollWidth > textRef.current.clientWidth
: false;
if (isTitleOverflowing !== isOverflowing) {
setIsTitleOverflowing(isOverflowing);
}
}, [isTitleOverflowing, text]);
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
};
return (
<>
<StyledOverflowingText
data-testid="tooltip"
ref={textRef}
id={textElementId}
cursorPointer={isTitleOverflowing}
>
{text}
</StyledOverflowingText>
{isTitleOverflowing &&
createPortal(
<div onClick={handleTooltipClick}>
<AppTooltip
anchorSelect={`#${textElementId}`}
content={text ?? ''}
delayHide={0}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
/>
</div>,
document.body,
)}
</>
);
};

View File

@ -0,0 +1,29 @@
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { OverflowingTextWithTooltip } from '../OverflowingTextWithTooltip';
const placeholderText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi tellus diam, rhoncus nec consequat quis, dapibus quis massa. Praesent tincidunt augue at ex bibendum, non finibus augue faucibus. In at gravida orci. Nulla facilisi. Proin ut augue ut nisi pellentesque tristique. Proin sodales libero id turpis tincidunt posuere.';
const meta: Meta<typeof OverflowingTextWithTooltip> = {
title: 'UI/Tooltip/OverflowingTextWithTooltip',
component: OverflowingTextWithTooltip,
};
export default meta;
type Story = StoryObj<typeof OverflowingTextWithTooltip>;
export const Default: Story = {
args: {
text: placeholderText,
},
decorators: [ComponentDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const tooltip = await canvas.findByTestId('tooltip');
userEvent.hover(tooltip);
},
};

View File

@ -0,0 +1,82 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { AppTooltip as Tooltip, TooltipPosition } from '../AppTooltip';
const meta: Meta<typeof Tooltip> = {
title: 'UI/Tooltip/Tooltip',
component: Tooltip,
};
export default meta;
type Story = StoryObj<typeof Tooltip>;
export const Default: Story = {
args: {
place: TooltipPosition.Bottom,
content: 'Tooltip Test',
isOpen: true,
anchorSelect: '#hover-text',
},
decorators: [ComponentDecorator],
render: ({
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
}) => (
<>
<p id="hover-text" data-testid="tooltip">
Hover me!
</p>
<Tooltip
{...{
anchorSelect,
className,
content,
delayHide,
isOpen,
noArrow,
offset,
place,
positionStrategy,
}}
/>
</>
),
};
export const Catalog: CatalogStory<Story, typeof Tooltip> = {
args: { isOpen: true, content: 'Tooltip Test' },
play: async ({ canvasElement }) => {
Object.values(TooltipPosition).forEach((position) => {
const element = canvasElement.querySelector(
`#${position}`,
) as HTMLElement;
element.style.margin = '75px';
});
},
parameters: {
catalog: {
dimensions: [
{
name: 'anchorSelect',
values: Object.values(TooltipPosition),
props: (anchorSelect: TooltipPosition) => ({
anchorSelect: `#${anchorSelect}`,
place: anchorSelect,
}),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,36 @@
import styled from '@emotion/styled';
type H1TitleProps = {
title: string;
fontColor?: H1TitleFontColor;
className?: string;
};
export enum H1TitleFontColor {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
}
const StyledTitle = styled.h2<{
fontColor: H1TitleFontColor;
}>`
color: ${({ theme, fontColor }) => theme.font.color[fontColor]};
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
margin: 0;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
export const H1Title = ({
title,
fontColor = H1TitleFontColor.Tertiary,
className,
}: H1TitleProps) => {
return (
<StyledTitle fontColor={fontColor} className={className}>
{title}
</StyledTitle>
);
};

View File

@ -0,0 +1,44 @@
import styled from '@emotion/styled';
type H2TitleProps = {
title: string;
description?: string;
addornment?: React.ReactNode;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
`;
const StyledTitle = styled.h2`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: 0;
`;
const StyledDescription = styled.h3`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin: 0;
margin-top: ${({ theme }) => theme.spacing(3)};
`;
export const H2Title = ({ title, description, addornment }: H2TitleProps) => (
<StyledContainer>
<StyledTitleContainer>
<StyledTitle>{title}</StyledTitle>
{addornment}
</StyledTitleContainer>
{description && <StyledDescription>{description}</StyledDescription>}
</StyledContainer>
);

View File

@ -0,0 +1,43 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { H1Title, H1TitleFontColor } from '../H1Title';
const meta: Meta<typeof H1Title> = {
title: 'UI/Title/H1Title',
component: H1Title,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof H1Title>;
const args = {
title: 'Title',
fontColor: H1TitleFontColor.Primary,
};
export const Default: Story = {
args,
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof H1Title> = {
args,
decorators: [CatalogDecorator],
parameters: {
catalog: {
dimensions: [
{
name: 'FontColor',
values: Object.values(H1TitleFontColor),
props: (fontColor: H1TitleFontColor) => ({ fontColor }),
},
],
},
},
};

View File

@ -0,0 +1,32 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { H2Title } from '../H2Title';
const args = {
title: 'Sub title',
description: 'Lorem ipsum dolor sit amet',
};
const meta: Meta<typeof H2Title> = {
title: 'UI/Title/H2Title',
component: H2Title,
decorators: [ComponentDecorator],
args: {
title: args.title,
},
};
export default meta;
type Story = StoryObj<typeof H2Title>;
export const Default: Story = {
decorators: [ComponentDecorator],
};
export const WithDescription: Story = {
args,
decorators: [ComponentDecorator],
};