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:
co-authored by
Nimra Ahmed
Nimra Ahmed
parent
a35ea5e8f9
commit
258685467b
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 } },
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@@ -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],
|
||||
};
|
||||
@@ -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 = {};
|
||||
@@ -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 |
@@ -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} />;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
export type IconComponent = FunctionComponent<{
|
||||
size?: number;
|
||||
stroke?: number;
|
||||
}>;
|
||||
@@ -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>;
|
||||
@@ -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 = {};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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],
|
||||
};
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -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,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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],
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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],
|
||||
};
|
||||
Reference in New Issue
Block a user