switch to nx monorepo

This commit is contained in:
Philipinho
2024-01-09 18:58:26 +01:00
parent e1bb2632b8
commit 093e634c0b
273 changed files with 11419 additions and 31 deletions

View File

@ -0,0 +1,25 @@
.bubbleMenu {
display: flex;
width: fit-content;
border-radius: 2px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-8));
.active {
color: var(--mantine-color-blue-8);
}
.colorButton {
border: none;
}
.colorButton::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-8));
}
}

View File

@ -0,0 +1,149 @@
import { BubbleMenu, BubbleMenuProps, isNodeSelection } from '@tiptap/react';
import { FC, useState } from 'react';
import { IconBold, IconCode, IconItalic, IconStrikethrough, IconUnderline, IconMessage } from '@tabler/icons-react';
import clsx from 'clsx';
import classes from './bubble-menu.module.css';
import { ActionIcon, rem, Tooltip } from '@mantine/core';
import { ColorSelector } from './color-selector';
import { NodeSelector } from './node-selector';
import { draftCommentIdAtom, showCommentPopupAtom } from '@/features/comment/atoms/comment-atom';
import { useAtom } from 'jotai';
import { v4 as uuidv4 } from 'uuid';
export interface BubbleMenuItem {
name: string;
isActive: () => boolean;
command: () => void;
icon: typeof IconBold;
}
type EditorBubbleMenuProps = Omit<BubbleMenuProps, 'children'>;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
const items: BubbleMenuItem[] = [
{
name: 'bold',
isActive: () => props.editor.isActive('bold'),
command: () => props.editor.chain().focus().toggleBold().run(),
icon: IconBold,
},
{
name: 'italic',
isActive: () => props.editor.isActive('italic'),
command: () => props.editor.chain().focus().toggleItalic().run(),
icon: IconItalic,
},
{
name: 'underline',
isActive: () => props.editor.isActive('underline'),
command: () => props.editor.chain().focus().toggleUnderline().run(),
icon: IconUnderline,
},
{
name: 'strike',
isActive: () => props.editor.isActive('strike'),
command: () => props.editor.chain().focus().toggleStrike().run(),
icon: IconStrikethrough,
},
{
name: 'code',
isActive: () => props.editor.isActive('code'),
command: () => props.editor.chain().focus().toggleCode().run(),
icon: IconCode,
},
];
const commentItem: BubbleMenuItem = {
name: 'comment',
isActive: () => props.editor.isActive('comment'),
command: () => {
const commentId = uuidv4();
props.editor.chain().focus().setCommentDecoration().run();
setDraftCommentId(commentId);
setShowCommentPopup(true);
},
icon: IconMessage,
};
const bubbleMenuProps: EditorBubbleMenuProps = {
...props,
shouldShow: ({ state, editor }) => {
const { selection } = state;
const { empty } = selection;
if (editor.isActive('image') || empty || isNodeSelection(selection)) {
return false;
}
return true;
},
tippyOptions: {
moveTransition: 'transform 0.15s ease-out',
onHidden: () => {
setIsNodeSelectorOpen(false);
setIsColorSelectorOpen(false);
setIsLinkSelectorOpen(false);
},
},
};
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
return (
<BubbleMenu
{...bubbleMenuProps}
className={classes.bubbleMenu}
>
<NodeSelector
editor={props.editor}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
setIsColorSelectorOpen(false);
setIsLinkSelectorOpen(false);
}}
/>
<ActionIcon.Group>
{items.map((item, index) => (
<Tooltip key={index} label={item.name} withArrow>
<ActionIcon key={index} variant="default" size="lg" radius="0" aria-label={item.name}
className={clsx({ [classes.active]: item.isActive() })}
style={{ border: 'none' }}
onClick={item.command}>
<item.icon style={{ width: rem(16) }} stroke={2} />
</ActionIcon>
</Tooltip>
))}
</ActionIcon.Group>
<ColorSelector
editor={props.editor}
isOpen={isColorSelectorOpen}
setIsOpen={() => {
setIsColorSelectorOpen(!isColorSelectorOpen);
setIsNodeSelectorOpen(false);
setIsLinkSelectorOpen(false);
}}
/>
<Tooltip label={commentItem.name} withArrow>
<ActionIcon variant="default" size="lg" radius="0" aria-label={commentItem.name}
style={{ border: 'none' }}
onClick={commentItem.command}>
<IconMessage style={{ width: rem(16) }} stroke={2} />
</ActionIcon>
</Tooltip>
</BubbleMenu>
);
};

View File

@ -0,0 +1,189 @@
import { Editor } from '@tiptap/core';
import { Dispatch, FC, SetStateAction } from 'react';
import { IconCheck, IconChevronDown } from '@tabler/icons-react';
import { Button, Popover, rem, ScrollArea, Text } from '@mantine/core';
import classes from './bubble-menu.module.css';
export interface BubbleColorMenuItem {
name: string;
color: string;
}
interface ColorSelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
const TEXT_COLORS: BubbleColorMenuItem[] = [
{
name: 'Default',
color: '',
},
{
name: 'Blue',
color: '#2563EB',
},
{
name: 'Green',
color: '#008A00',
},
{
name: 'Purple',
color: '#9333EA',
},
{
name: 'Red',
color: '#E00000',
},
{
name: 'Yellow',
color: '#EAB308',
},
{
name: 'Orange',
color: '#FFA500',
},
{
name: 'Pink',
color: '#BA4081',
},
{
name: 'Gray',
color: '#A8A29E',
},
];
// TODO: handle dark mode
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
{
name: 'Default',
color: '',
},
{
name: 'Blue',
color: '#c1ecf9',
},
{
name: 'Green',
color: '#acf79f',
},
{
name: 'Purple',
color: '#f6f3f8',
},
{
name: 'Red',
color: '#fdebeb',
},
{
name: 'Yellow',
color: '#fbf4a2',
},
{
name: 'Orange',
color: '#faebdd',
},
{
name: 'Pink',
color: '#faf1f5',
},
{
name: 'Gray',
color: '#f1f1ef',
},
];
export const ColorSelector: FC<ColorSelectorProps> =
({ editor, isOpen, setIsOpen }) => {
const activeColorItem = TEXT_COLORS.find(({ color }) =>
editor.isActive('textStyle', { color }),
);
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
editor.isActive('highlight', { color }),
);
return (
<Popover width={200} opened={isOpen} withArrow>
<Popover.Target>
<Button variant="default" radius="0"
leftSection="A"
rightSection={<IconChevronDown size={16} />}
className={classes.colorButton}
style={{
color: activeColorItem?.color,
}}
onClick={() => setIsOpen(!isOpen)}
/>
</Popover.Target>
<Popover.Dropdown>
{/* make mah responsive */}
<ScrollArea.Autosize type="scroll" mah='400'>
<Text span c="dimmed" inherit>COLOR</Text>
<Button.Group orientation="vertical">
{TEXT_COLORS.map(({ name, color }, index) => (
<Button
key={index}
variant="default"
leftSection={<span style={{ color }}>A</span>}
justify="left"
fullWidth
rightSection={editor.isActive('textStyle', { color })
&& (<IconCheck style={{ width: rem(16) }} />)}
onClick={() => {
editor.commands.unsetColor();
name !== 'Default' &&
editor
.chain()
.focus()
.setColor(color || '')
.run();
setIsOpen(false);
}}
style={{ border: 'none' }}
>
{name}
</Button>
))}
</Button.Group>
<Text span c="dimmed" inherit>BACKGROUND</Text>
<Button.Group orientation="vertical">
{HIGHLIGHT_COLORS.map(({ name, color }, index) => (
<Button
key={index}
variant="default"
leftSection={<span style={{ padding: '4px', background: color }}>A</span>}
justify="left"
fullWidth
rightSection={editor.isActive('highlight', { color })
&& (<IconCheck style={{ width: rem(16) }} />)}
onClick={() => {
editor.commands.unsetHighlight();
name !== 'Default' &&
editor
.commands
.setHighlight({ color });
setIsOpen(false);
}}
style={{ border: 'none' }}
>
{name}
</Button>
))}
</Button.Group>
</ScrollArea.Autosize>
</Popover.Dropdown>
</Popover>
);
};

View File

@ -0,0 +1,148 @@
import { Editor } from '@tiptap/core';
import React, { Dispatch, FC, SetStateAction } from 'react';
import {
IconBlockquote,
IconCheck, IconCheckbox, IconChevronDown, IconCode,
IconH1,
IconH2,
IconH3,
IconList,
IconListNumbers,
IconTypography,
} from '@tabler/icons-react';
import { Popover, Button, rem, ScrollArea } from '@mantine/core';
import classes from '@/features/editor/components/bubble-menu/bubble-menu.module.css';
interface NodeSelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
export interface BubbleMenuItem {
name: string;
icon: React.ElementType;
command: () => void;
isActive: () => boolean;
}
export const NodeSelector: FC<NodeSelectorProps> =
({ editor, isOpen, setIsOpen }) => {
const items: BubbleMenuItem[] = [
{
name: 'Text',
icon: IconTypography,
command: () =>
editor.chain().focus().toggleNode('paragraph', 'paragraph').run(),
isActive: () =>
editor.isActive('paragraph') &&
!editor.isActive('bulletList') &&
!editor.isActive('orderedList'),
},
{
name: 'Heading 1',
icon: IconH1,
command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => editor.isActive('heading', { level: 1 }),
},
{
name: 'Heading 2',
icon: IconH2,
command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => editor.isActive('heading', { level: 2 }),
},
{
name: 'Heading 3',
icon: IconH3,
command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => editor.isActive('heading', { level: 3 }),
},
{
name: 'To-do List',
icon: IconCheckbox,
command: () => editor.chain().focus().toggleTaskList().run(),
isActive: () => editor.isActive('taskItem'),
},
{
name: 'Bullet List',
icon: IconList,
command: () => editor.chain().focus().toggleBulletList().run(),
isActive: () => editor.isActive('bulletList'),
},
{
name: 'Numbered List',
icon: IconListNumbers,
command: () => editor.chain().focus().toggleOrderedList().run(),
isActive: () => editor.isActive('orderedList'),
},
{
name: 'Blockquote',
icon: IconBlockquote,
command: () =>
editor
.chain()
.focus()
.toggleNode('paragraph', 'paragraph')
.toggleBlockquote()
.run(),
isActive: () => editor.isActive('blockquote'),
},
{
name: 'Code',
icon: IconCode,
command: () => editor.chain().focus().toggleCodeBlock().run(),
isActive: () => editor.isActive('codeBlock'),
},
];
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
name: 'Multiple',
};
return (
<Popover opened={isOpen} withArrow>
<Popover.Target>
<Button variant="default" radius="0"
rightSection={<IconChevronDown size={16} />}
className={classes.colorButton}
onClick={() => setIsOpen(!isOpen)}
>
{activeItem?.name}
</Button>
</Popover.Target>
<Popover.Dropdown>
<ScrollArea.Autosize type="scroll" mah={400}>
<Button.Group orientation="vertical">
{items.map((item, index) => (
<Button
key={index}
variant="default"
leftSection={<item.icon size={16} />}
rightSection={activeItem.name === item.name
&& (<IconCheck size={16} />)}
justify="left"
fullWidth
onClick={() => {
item.command();
setIsOpen(false);
}}
style={{ border: 'none' }}
>
{item.name}
</Button>
))}
</Button.Group>
</ScrollArea.Autosize>
</Popover.Dropdown>
</Popover>
);
};