Skip to content

Commit

Permalink
feat(OnboardingChecklist): new component
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinsawicki committed Aug 2, 2024
1 parent 3c36812 commit 3b9d8a0
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
$base-class: 'onboarding-checklist';

.#{$base-class} {
display: flex;
flex-direction: row;
border-radius: var(--radius-4);
background-color: var(--surface-secondary-default);
width: 100%;
max-width: 1280px;

&__column {
padding: 40px;
width: 50%;
}

&__header {
display: flex;
flex-direction: column;
margin-bottom: var(--spacing-9);

&__label {
margin-bottom: 4px;
color: var(--content-basic-secondary);
}

&__title {
margin: 0;
}
}

&__checklist {
position: relative;

&::after {
position: absolute;
top: 0;
left: 11.5px;
z-index: 1;
border-left: 1px dashed var(--border-basic-primary);
width: 1px;
height: 100%;
content: '';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react';

import * as Icons from '@livechat/design-system-icons';

import { Icon } from '../Icon';

import { OnboardingChecklist } from './OnboardingChecklist';
import { ChecklistItem } from './types';

export default {
title: 'Components/OnboardingChecklist',
component: OnboardingChecklist,
};

const getItems = (handler: (id: string) => void): ChecklistItem[] => [
{
id: '1',
title: 'Run a test chat',
description: 'Make it match your brand and website',
primaryButton: {
label: 'Chat with yourself',
icon: <Icon source={Icons.OpenInNew} />,
iconPosition: 'right',
onClick: () => handler('1'),
},
},
{
id: '2',
title: 'Install LiveChat on your website',
description: 'Invite your team to help you with chats',
primaryButton: {
label: 'Chat with yourself',
icon: <Icon source={Icons.OpenInNew} />,
iconPosition: 'right',
onClick: () => handler('2'),
},
},
{
id: '3',
title: 'Invite your teammates',
description: 'Save time by using prepared answers',
primaryButton: {
label: 'Chat with yourself',
icon: <Icon source={Icons.OpenInNew} />,
iconPosition: 'right',
onClick: () => handler('3'),
},
},
{
id: '4',
title: 'Set up a chatbot',
description: 'Collect feedback from your customers',
primaryButton: {
label: 'Chat with yourself',
icon: <Icon source={Icons.OpenInNew} />,
iconPosition: 'right',
onClick: () => handler('4'),
},
},
{
id: '5',
title: 'Upload profile picture',
description: 'Automate your customer service',
primaryButton: {
label: 'Chat with yourself',
icon: <Icon source={Icons.OpenInNew} />,
iconPosition: 'right',
onClick: () => handler('5'),
},
},
];

export const Default = (): React.ReactElement => {
const [activeItem, setActiveItem] = React.useState('1');
const [checkedIds, setCheckedIds] = React.useState<string[]>([]);

const handleCheckItem = (id: string) => {
return setCheckedIds((prev) => [...prev, id]);
};

return (
<OnboardingChecklist
title="Here’s your getting started guide"
titleLabel="Hello, Tim!"
items={getItems(handleCheckItem)}
activeId={activeItem}
checkedId={checkedIds}
onActiveChange={setActiveItem}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react';

import cx from 'clsx';

import { Heading, Text } from '../Typography';

import { CheckListItem } from './components';
import { IOnboardingChecklistProps } from './types';

import styles from './OnboardingChecklist.module.scss';

const baseClass = 'onboarding-checklist';

export const OnboardingChecklist: React.FC<IOnboardingChecklistProps> = ({
activeId,
checkedId,
title,
titleLabel,
items,
onActiveChange,
}) => {
return (
<div className={styles[baseClass]}>
<div className={styles[`${baseClass}__column`]}>
<div className={styles[`${baseClass}__header`]}>
{titleLabel && (
<Text size="lg" className={styles[`${baseClass}__header__label`]}>
{titleLabel}
</Text>
)}
<Heading size="lg" className={styles[`${baseClass}__header__title`]}>
{title}
</Heading>
</div>
<div className={styles[`${baseClass}__checklist`]}>
{items.map((item, index) => (
<CheckListItem
key={index}
isActive={item.id === activeId}
isChecked={checkedId.includes(item.id)}
onClick={onActiveChange}
{...item}
/>
))}
</div>
</div>
<div
className={cx(
styles[`${baseClass}__column`],
styles[`${baseClass}__column--right`]
)}
>
placeholder
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
$base-class: 'checklist-item';

.#{$base-class} {
display: flex;
gap: 20px;
transition: all var(--transition-duration-moderate-1);
margin-top: 0;
height: 72px;

&--active {
height: 100%;
max-height: 9999px;
}

&__check-mark {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
transition: inherit;
z-index: 2;
margin-top: 24px;
border: 2px solid var(--border-basic-primary);
border-radius: 24px;
background-color: var(--surface-primary-default);
width: 24px;
height: 24px;
color: #fff;

&--checked {
border-color: var(--surface-accent-emphasis-high-positive);
background-color: var(--surface-accent-emphasis-high-positive);
}
}

&__content {
transition: inherit;
border-radius: var(--radius-4);
box-shadow: 0;
padding: 24px;
width: 100%;
min-height: 24px;

&:hover {
background-color: var(--surface-secondary-hover);
cursor: pointer;
}

&--open {
z-index: 1;
margin-top: 0;
margin-bottom: 0;
box-shadow: 0 2px 6px 0 rgb(19 19 23 / 20%);
background-color: var(--content-locked-white);

&:hover {
background-color: var(--content-locked-white);
}
}

&__label {
transition: inherit;
margin: 0;

&--open {
margin-bottom: 4px;
}
}

&__inner {
transition: inherit;
height: 100%;
overflow: hidden;

&__description {
margin: 0;
padding: 0;
color: var(--content-basic-secondary);
}

&__cta {
padding-top: var(--spacing-3);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react';

import { Check } from '@livechat/design-system-icons';
import cx from 'clsx';

import { Button } from '../../Button';
import { Icon } from '../../Icon';
import { Heading, Text } from '../../Typography';
import { ICheckListItem } from '../types';

import styles from './CheckListItem.module.scss';

const baseClass = 'checklist-item';

export const CheckListItem: React.FC<ICheckListItem> = ({
id,
isActive,
isChecked,
title,
description,
onClick,
primaryButton,
}) => {
const descriptionRef = React.useRef<HTMLDivElement>(null);
const [size, setSize] = React.useState(0);

React.useEffect(() => {
if (
descriptionRef.current &&
size !== descriptionRef.current.offsetHeight
) {
setSize(descriptionRef.current.offsetHeight);
}
}, [descriptionRef]);

return (
<div
className={cx(styles[baseClass], {
[styles[`${baseClass}--active`]]: isActive,
})}
>
<span
className={cx(styles[`${baseClass}__check-mark`], {
[styles[`${baseClass}__check-mark--checked`]]: isChecked,
})}
>
{isChecked && <Icon size="small" source={Check} />}
</span>
<div
onClick={() => onClick(id)}
className={cx(styles[`${baseClass}__content`], {
[styles[`${baseClass}__content--open`]]: isActive,
})}
>
<Heading
size="xs"
className={cx(styles[`${baseClass}__content__label`], {
[styles[`${baseClass}__content__label--open`]]: isActive,
})}
>
{title}
</Heading>
<div
className={styles[`${baseClass}__content__inner`]}
style={{ maxHeight: isActive ? size : 0 }}
>
<span ref={descriptionRef}>
<Text
size="lg"
className={styles[`${baseClass}__content__inner__description`]}
>
{description}
</Text>
<div className={styles[`${baseClass}__content__inner__cta`]}>
{primaryButton && (
<Button
kind="high-contrast"
onClick={primaryButton.onClick}
icon={primaryButton.icon}
iconPosition={primaryButton.iconPosition}
>
{primaryButton.label}
</Button>
)}
</div>
</span>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CheckListItem } from './CheckListItem';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { OnboardingChecklist } from './OnboardingChecklist';
Loading

0 comments on commit 3b9d8a0

Please sign in to comment.