summaryrefslogtreecommitdiffstats
path: root/src/components/molecules/modals
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/modals')
-rw-r--r--src/components/molecules/modals/modal.module.scss38
-rw-r--r--src/components/molecules/modals/modal.stories.tsx69
-rw-r--r--src/components/molecules/modals/modal.test.tsx9
-rw-r--r--src/components/molecules/modals/modal.tsx77
-rw-r--r--src/components/molecules/modals/tooltip.module.scss46
-rw-r--r--src/components/molecules/modals/tooltip.stories.tsx64
-rw-r--r--src/components/molecules/modals/tooltip.test.tsx24
-rw-r--r--src/components/molecules/modals/tooltip.tsx62
8 files changed, 389 insertions, 0 deletions
diff --git a/src/components/molecules/modals/modal.module.scss b/src/components/molecules/modals/modal.module.scss
new file mode 100644
index 0000000..8866834
--- /dev/null
+++ b/src/components/molecules/modals/modal.module.scss
@@ -0,0 +1,38 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ padding: var(--spacing-md);
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(4) solid;
+ border-image: radial-gradient(
+ ellipse at top,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
+ box-shadow: fun.convert-px(2) fun.convert-px(-2) fun.convert-px(3)
+ fun.convert-px(-1) var(--color-shadow-dark);
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "sm") {
+ padding: var(--spacing-xs);
+ border-left: none;
+ border-right: none;
+
+ .title {
+ margin-bottom: var(--spacing-2xs);
+ }
+ }
+
+ @include mix.dimensions("sm") {
+ max-width: 35ch;
+ }
+ }
+}
+
+.icon {
+ --icon-size: #{fun.convert-px(30)};
+
+ margin-right: var(--spacing-2xs);
+}
diff --git a/src/components/molecules/modals/modal.stories.tsx b/src/components/molecules/modals/modal.stories.tsx
new file mode 100644
index 0000000..bd7d9f4
--- /dev/null
+++ b/src/components/molecules/modals/modal.stories.tsx
@@ -0,0 +1,69 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ModalComponent from './modal';
+
+export default {
+ title: 'Molecules/Modals',
+ component: ModalComponent,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The modal body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ icon: {
+ control: {
+ type: 'select',
+ },
+ description: 'The title icon.',
+ options: ['', 'cogs', 'search'],
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The modal title.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ModalComponent>;
+
+const Template: ComponentStory<typeof ModalComponent> = (args) => (
+ <ModalComponent {...args} />
+);
+
+export const Modal = Template.bind({});
+Modal.args = {
+ children:
+ 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.',
+};
diff --git a/src/components/molecules/modals/modal.test.tsx b/src/components/molecules/modals/modal.test.tsx
new file mode 100644
index 0000000..14fb224
--- /dev/null
+++ b/src/components/molecules/modals/modal.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import Modal from './modal';
+
+describe('Modal', () => {
+ it('renders a title', () => {
+ render(<Modal title="A custom title" />);
+ expect(screen.getByText('A custom title')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx
new file mode 100644
index 0000000..52ada57
--- /dev/null
+++ b/src/components/molecules/modals/modal.tsx
@@ -0,0 +1,77 @@
+import Heading from '@components/atoms/headings/heading';
+import { CogProps } from '@components/atoms/icons/cog';
+import { MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
+import dynamic from 'next/dynamic';
+import { FC } from 'react';
+import styles from './modal.module.scss';
+
+export type Icons = 'cogs' | 'search';
+
+export type ModalProps = {
+ /**
+ * Set additional classnames.
+ */
+ className?: string;
+ /**
+ * Set additional classnames to the heading.
+ */
+ headingClassName?: string;
+ /**
+ * A icon to illustrate the modal.
+ */
+ icon?: Icons;
+ /**
+ * The modal title.
+ */
+ title?: string;
+};
+
+const CogIcon = dynamic<CogProps>(() => import('@components/atoms/icons/cog'), {
+ ssr: false,
+});
+const SearchIcon = dynamic<MagnifyingGlassProps>(
+ () => import('@components/atoms/icons/magnifying-glass'),
+ { ssr: false }
+);
+
+/**
+ * Modal component
+ *
+ * Render a modal component with an optional title and icon.
+ */
+const Modal: FC<ModalProps> = ({
+ children,
+ className = '',
+ headingClassName = '',
+ icon,
+ title,
+}) => {
+ const getIcon = (id: Icons) => {
+ switch (id) {
+ case 'cogs':
+ return <CogIcon />;
+ case 'search':
+ return <SearchIcon />;
+ default:
+ return <></>;
+ }
+ };
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ {title && (
+ <Heading
+ isFake={true}
+ level={3}
+ className={`${styles.title} ${headingClassName}`}
+ >
+ {icon && <span className={styles.icon}>{getIcon(icon)}</span>}
+ {title}
+ </Heading>
+ )}
+ {children}
+ </div>
+ );
+};
+
+export default Modal;
diff --git a/src/components/molecules/modals/tooltip.module.scss b/src/components/molecules/modals/tooltip.module.scss
new file mode 100644
index 0000000..94aa3dd
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.module.scss
@@ -0,0 +1,46 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ --title-height: #{fun.convert-px(40)};
+
+ margin-top: calc(var(--title-height) / 2);
+ padding: calc((var(--title-height) / 2) + var(--spacing-sm)) var(--spacing-sm)
+ var(--spacing-sm);
+ position: relative;
+ background: var(--color-bg);
+ border: fun.convert-px(2) solid var(--color-primary-dark);
+ border-radius: fun.convert-px(3);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow),
+ fun.convert-px(2) fun.convert-px(2) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light);
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ height: var(--title-height);
+ padding-right: var(--spacing-xs);
+ position: absolute;
+ top: calc(var(--title-height) / -2);
+ left: var(--spacing-xs);
+ background: var(--color-bg);
+ border: fun.convert-px(1) solid var(--color-primary-dark);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-sm);
+ font-variant: small-caps;
+ font-weight: 500;
+}
+
+.icon {
+ display: flex;
+ align-items: center;
+ height: var(--title-height);
+ margin-right: var(--spacing-xs);
+ padding: 0 var(--spacing-2xs);
+ background: var(--color-primary-dark);
+ border: fun.convert-px(1) solid var(--color-primary-dark);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
+ color: var(--color-fg-inverted);
+ font-weight: 600;
+}
diff --git a/src/components/molecules/modals/tooltip.stories.tsx b/src/components/molecules/modals/tooltip.stories.tsx
new file mode 100644
index 0000000..63fc71d
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.stories.tsx
@@ -0,0 +1,64 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import TooltipComponent from './tooltip';
+
+export default {
+ title: 'Molecules/Modals',
+ component: TooltipComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ content: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ icon: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip icon.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip title',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof TooltipComponent>;
+
+const Template: ComponentStory<typeof TooltipComponent> = (args) => (
+ <TooltipComponent {...args} />
+);
+
+export const Tooltip = Template.bind({});
+Tooltip.args = {
+ content:
+ 'Minima tempora fuga omnis ratione doloribus ut. Totam ea vitae consequatur. Fuga hic ipsum. In non debitis ex assumenda ut dicta. Sit ut maxime eligendi est.',
+ icon: '?',
+ title: 'Laborum enim vero',
+};
diff --git a/src/components/molecules/modals/tooltip.test.tsx b/src/components/molecules/modals/tooltip.test.tsx
new file mode 100644
index 0000000..24f20d8
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.test.tsx
@@ -0,0 +1,24 @@
+import { render, screen } from '@test-utils';
+import Tooltip from './tooltip';
+
+const title = 'Illum eum at';
+const content =
+ 'Non accusantium ad. Est et impedit iste animi voluptas cum accusamus accusantium. Repellat ut sint pariatur cumque cupiditate. Animi occaecati odio ut debitis ipsam similique. Repudiandae aut earum occaecati consequatur laborum ut nobis iusto. Adipisci laboriosam id.';
+const icon = '?';
+
+describe('Tooltip', () => {
+ it('renders a title', () => {
+ render(<Tooltip title={title} content={content} icon={icon} />);
+ expect(screen.getByText(title)).toBeInTheDocument();
+ });
+
+ it('renders an explanation', () => {
+ render(<Tooltip title={title} content={content} icon={icon} />);
+ expect(screen.getByText(content)).toBeInTheDocument();
+ });
+
+ it('renders an icon', () => {
+ render(<Tooltip title={title} content={content} icon={icon} />);
+ expect(screen.getByText(icon)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx
new file mode 100644
index 0000000..73f36e7
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.tsx
@@ -0,0 +1,62 @@
+import List, { type ListItem } from '@components/atoms/lists/list';
+import { ReactNode, VFC } from 'react';
+import styles from './tooltip.module.scss';
+
+export type TooltipProps = {
+ /**
+ * Set additional classnames to the tooltip wrapper.
+ */
+ className?: string;
+ /**
+ * The tooltip body.
+ */
+ content: string | string[];
+ /**
+ * An icon to illustrate tooltip content.
+ */
+ icon: ReactNode;
+ /**
+ * The tooltip title.
+ */
+ title: string;
+};
+
+/**
+ * Tooltip component
+ *
+ * Render a tooltip modal.
+ */
+const Tooltip: VFC<TooltipProps> = ({
+ className = '',
+ content,
+ icon,
+ title,
+}) => {
+ /**
+ * Format an array of strings to an array of object with id and value.
+ *
+ * @param {string[]} array - An array of strings.
+ * @returns {ListItem[]} The array formatted to be used as list items.
+ */
+ const getListItems = (array: string[]): ListItem[] => {
+ return array.map((string, index) => {
+ return { id: `item-${index}`, value: string };
+ });
+ };
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ <div className={styles.title}>
+ <span className={styles.icon}>{icon}</span>
+ {title}
+ </div>
+ {Array.isArray(content) ? (
+ <List items={getListItems(content)} />
+ ) : (
+ content
+ )}
+ </div>
+ );
+};
+
+export default Tooltip;