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:
@ -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 } },
|
||||
};
|
||||
145
front/src/modules/ui/display/chip/components/Chip.tsx
Normal file
145
front/src/modules/ui/display/chip/components/Chip.tsx
Normal 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>
|
||||
);
|
||||
76
front/src/modules/ui/display/chip/components/EntityChip.tsx
Normal file
76
front/src/modules/ui/display/chip/components/EntityChip.tsx
Normal 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>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@ -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 = {};
|
||||
11
front/src/modules/ui/display/icon/assets/address-book.svg
Normal file
11
front/src/modules/ui/display/icon/assets/address-book.svg
Normal 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 |
@ -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} />;
|
||||
};
|
||||
86
front/src/modules/ui/display/icon/index.ts
Normal file
86
front/src/modules/ui/display/icon/index.ts
Normal 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';
|
||||
6
front/src/modules/ui/display/icon/types/IconComponent.ts
Normal file
6
front/src/modules/ui/display/icon/types/IconComponent.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
export type IconComponent = FunctionComponent<{
|
||||
size?: number;
|
||||
stroke?: number;
|
||||
}>;
|
||||
20
front/src/modules/ui/display/pill/components/SoonPill.tsx
Normal file
20
front/src/modules/ui/display/pill/components/SoonPill.tsx
Normal 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>;
|
||||
@ -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 = {};
|
||||
58
front/src/modules/ui/display/tag/components/Tag.tsx
Normal file
58
front/src/modules/ui/display/tag/components/Tag.tsx
Normal 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>
|
||||
);
|
||||
@ -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],
|
||||
};
|
||||
70
front/src/modules/ui/display/tooltip/AppTooltip.tsx
Normal file
70
front/src/modules/ui/display/tooltip/AppTooltip.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -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