style: update the ui of the slash command menu

This commit is contained in:
Aaryan Khandelwal 2023-12-09 17:26:47 +05:30
parent d16a6402cd
commit 842048b2f2
5 changed files with 175 additions and 155 deletions

View File

@ -15,12 +15,13 @@ export const DocumentEditorExtensions = (
isSubmitting: "submitting" | "submitted" | "saved", isSubmitting: "submitting" | "submitted" | "saved",
) => void, ) => void,
) => { ) => {
const additonalOptions: ISlashCommandItem[] = [ const additionalOptions: ISlashCommandItem[] = [
{ {
title: "Issue Embed", key: "issue_embed",
description: "Embed an issue from the project", title: "Issue embed",
searchTerms: ["Issue", "Iss"], description: "Embed an issue from the project.",
icon: <LayersIcon height={"20px"} width={"20px"} />, searchTerms: ["issue", "link", "embed"],
icon: <LayersIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor editor
.chain() .chain()
@ -35,7 +36,7 @@ export const DocumentEditorExtensions = (
]; ];
return [ return [
SlashCommand(uploadFile, setIsSubmitting, additonalOptions), SlashCommand(uploadFile, setIsSubmitting, additionalOptions),
DragAndDrop, DragAndDrop,
Placeholder.configure({ Placeholder.configure({
placeholder: ({ node }) => { placeholder: ({ node }) => {

View File

@ -35,7 +35,7 @@
"@plane/editor-core": "*", "@plane/editor-core": "*",
"eslint": "8.36.0", "eslint": "8.36.0",
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"lucide-react": "^0.244.0", "lucide-react": "^0.249.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"@tiptap/pm": "^2.1.7" "@tiptap/pm": "^2.1.7"
}, },

View File

@ -10,19 +10,23 @@ import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion"; import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react"; import { ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js"; import tippy from "tippy.js";
import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types"; import type {
UploadImage,
ISlashCommandItem,
CommandProps,
} from "@plane/editor-types";
import { import {
CaseSensitive,
Code2,
Heading1, Heading1,
Heading2, Heading2,
Heading3, Heading3,
ImageIcon,
List, List,
ListOrdered, ListOrdered,
Text, ListTodo,
TextQuote,
Code,
MinusSquare, MinusSquare,
CheckSquare, Quote,
ImageIcon,
Table, Table,
} from "lucide-react"; } from "lucide-react";
import { import {
@ -39,6 +43,7 @@ import {
} from "@plane/editor-core"; } from "@plane/editor-core";
interface CommandItemProps { interface CommandItemProps {
key: string;
title: string; title: string;
description: string; description: string;
icon: ReactNode; icon: ReactNode;
@ -83,15 +88,16 @@ const getSuggestionItems =
setIsSubmitting?: ( setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved", isSubmitting: "submitting" | "submitted" | "saved",
) => void, ) => void,
additonalOptions?: Array<ISlashCommandItem> additionalOptions?: Array<ISlashCommandItem>,
) => ) =>
({ query }: { query: string }) => { ({ query }: { query: string }) => {
let slashCommands: ISlashCommandItem[] = [ let slashCommands: ISlashCommandItem[] = [
{ {
key: "text",
title: "Text", title: "Text",
description: "Just start typing with plain text.", description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"], searchTerms: ["p", "paragraph"],
icon: <Text size={18} />, icon: <CaseSensitive className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
editor editor
.chain() .chain()
@ -102,110 +108,121 @@ const getSuggestionItems =
}, },
}, },
{ {
key: "heading_1",
title: "Heading 1", title: "Heading 1",
description: "Big section heading.", description: "Big section heading.",
searchTerms: ["title", "big", "large"], searchTerms: ["title", "big", "large"],
icon: <Heading1 size={18} />, icon: <Heading1 className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
toggleHeadingOne(editor, range); toggleHeadingOne(editor, range);
}, },
}, },
{ {
key: "heading_2",
title: "Heading 2", title: "Heading 2",
description: "Medium section heading.", description: "Medium section heading.",
searchTerms: ["subtitle", "medium"], searchTerms: ["subtitle", "medium"],
icon: <Heading2 size={18} />, icon: <Heading2 className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
toggleHeadingTwo(editor, range); toggleHeadingTwo(editor, range);
}, },
}, },
{ {
key: "heading_3",
title: "Heading 3", title: "Heading 3",
description: "Small section heading.", description: "Small section heading.",
searchTerms: ["subtitle", "small"], searchTerms: ["subtitle", "small"],
icon: <Heading3 size={18} />, icon: <Heading3 className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
toggleHeadingThree(editor, range); toggleHeadingThree(editor, range);
}, },
}, },
{ {
title: "To-do List", key: "todo_list",
title: "To do",
description: "Track tasks with a to-do list.", description: "Track tasks with a to-do list.",
searchTerms: ["todo", "task", "list", "check", "checkbox"], searchTerms: ["todo", "task", "list", "check", "checkbox"],
icon: <CheckSquare size={18} />, icon: <ListTodo className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
toggleTaskList(editor, range); toggleTaskList(editor, range);
}, },
}, },
{ {
title: "Bullet List", key: "bullet_list",
title: "Bullet list",
description: "Create a simple bullet list.", description: "Create a simple bullet list.",
searchTerms: ["unordered", "point"], searchTerms: ["unordered", "point"],
icon: <List size={18} />, icon: <List className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
toggleBulletList(editor, range); toggleBulletList(editor, range);
}, },
}, },
{ {
title: "Divider", key: "numbered_list",
description: "Visually divide blocks", title: "Numbered list",
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
icon: <MinusSquare size={18} />,
command: ({ editor, range }: CommandProps) => {
// @ts-expect-error I have to move this to the core
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
},
{
title: "Table",
description: "Create a Table",
searchTerms: ["table", "cell", "db", "data", "tabular"],
icon: <Table size={18} />,
command: ({ editor, range }: CommandProps) => {
insertTableCommand(editor, range);
},
},
{
title: "Numbered List",
description: "Create a list with numbering.", description: "Create a list with numbering.",
searchTerms: ["ordered"], searchTerms: ["ordered"],
icon: <ListOrdered size={18} />, icon: <ListOrdered className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
toggleOrderedList(editor, range); toggleOrderedList(editor, range);
}, },
}, },
{ {
key: "table",
title: "Table",
description: "Create a table",
searchTerms: ["table", "cell", "db", "data", "tabular"],
icon: <Table className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
insertTableCommand(editor, range);
},
},
{
key: "quote_block",
title: "Quote", title: "Quote",
description: "Capture a quote.", description: "Capture a quote.",
searchTerms: ["blockquote"], searchTerms: ["blockquote"],
icon: <TextQuote size={18} />, icon: <Quote className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => command: ({ editor, range }: CommandProps) =>
toggleBlockquote(editor, range), toggleBlockquote(editor, range),
}, },
{ {
key: "code_block",
title: "Code", title: "Code",
description: "Capture a code snippet.", description: "Capture a code snippet.",
searchTerms: ["codeblock"], searchTerms: ["codeblock"],
icon: <Code size={18} />, icon: <Code2 className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => command: ({ editor, range }: CommandProps) =>
// @ts-expect-error I have to move this to the core // @ts-expect-error I have to move this to the core
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
}, },
{ {
key: "image",
title: "Image", title: "Image",
description: "Upload an image from your computer.", description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"], searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />, icon: <ImageIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range); insertImageCommand(editor, uploadFile, setIsSubmitting, range);
}, },
}, },
] {
key: "divider",
title: "Divider",
description: "Visually divide blocks.",
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
icon: <MinusSquare className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
// @ts-expect-error I have to move this to the core
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
},
];
if (additonalOptions) { if (additionalOptions) {
additonalOptions.map(item => { additionalOptions.map((item) => {
slashCommands.push(item) slashCommands.push(item);
}) });
} }
slashCommands = slashCommands.filter((item) => { slashCommands = slashCommands.filter((item) => {
@ -219,9 +236,9 @@ const getSuggestionItems =
); );
} }
return true; return true;
}) });
return slashCommands return slashCommands;
}; };
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
@ -303,27 +320,23 @@ const CommandList = ({
<div <div
id="slash-command" id="slash-command"
ref={commandListContainer} ref={commandListContainer}
className="z-50 fixed h-auto max-h-[330px] w-72 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all" className="z-50 fixed h-auto max-h-[330px] w-52 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
> >
{items.map((item: CommandItemProps, index: number) => ( {items.map((item, index) => (
<button <button
key={item.key}
className={cn( className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`, `w-full flex items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-custom-text-100 hover:bg-custom-primary-100/5`,
{ {
"bg-custom-primary-100/5 text-custom-text-100": "bg-custom-primary-100/5": index === selectedIndex,
index === selectedIndex,
}, },
)} )}
key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-custom-border-300 bg-custom-background-100"> <div className="flex-shrink-0 grid place-items-center">
{item.icon} {item.icon}
</div> </div>
<div> <p>{item.title}</p>
<p className="font-medium">{item.title}</p>
<p className="text-xs text-custom-text-300">{item.description}</p>
</div>
</button> </button>
))} ))}
</div> </div>
@ -383,11 +396,11 @@ export const SlashCommand = (
setIsSubmitting?: ( setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved", isSubmitting: "submitting" | "submitted" | "saved",
) => void, ) => void,
additonalOptions?: Array<ISlashCommandItem>, additionalOptions?: Array<ISlashCommandItem>,
) => ) =>
Command.configure({ Command.configure({
suggestion: { suggestion: {
items: getSuggestionItems(uploadFile, setIsSubmitting, additonalOptions), items: getSuggestionItems(uploadFile, setIsSubmitting, additionalOptions),
render: renderItems, render: renderItems,
}, },
}); });

View File

@ -1,15 +1,16 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Editor, Range } from "@tiptap/core" import { Editor, Range } from "@tiptap/core";
export type CommandProps = { export type CommandProps = {
editor: Editor; editor: Editor;
range: Range; range: Range;
} };
export type ISlashCommandItem = { export type ISlashCommandItem = {
key: string;
title: string; title: string;
description: string; description: string;
searchTerms: string[]; searchTerms: string[];
icon: ReactNode; icon: ReactNode;
command: ({ editor, range }: CommandProps) => void; command: ({ editor, range }: CommandProps) => void;
} };

View File

@ -6078,6 +6078,11 @@ lucide-react@^0.244.0:
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.244.0.tgz#9626f44881830280012dad23afda7ddbcffff24b" resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.244.0.tgz#9626f44881830280012dad23afda7ddbcffff24b"
integrity sha512-PeDVbx5PlIRrVvdxiuSxPfBo7sK5qrL3LbvvRoGVNiHYRAkBm/48lKqoioxcmp0bgsyJs9lMw7CdtGFvnMJbVg== integrity sha512-PeDVbx5PlIRrVvdxiuSxPfBo7sK5qrL3LbvvRoGVNiHYRAkBm/48lKqoioxcmp0bgsyJs9lMw7CdtGFvnMJbVg==
lucide-react@^0.249.0:
version "0.249.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.249.0.tgz#6914c70220d9bdd69015d7b2009fb358a716c909"
integrity sha512-vBvk6aEz7c/v3moYejn3CbIZ9jtn8aDhtT+N9nao71MycQqZB9QtQ0U0WEnB58856A5boKtSUEkjF9NwkM04gA==
lucide-react@^0.293.0: lucide-react@^0.293.0:
version "0.293.0" version "0.293.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.293.0.tgz#02703dbcc56bb38779f4e576cc03be8cc0046fcc" resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.293.0.tgz#02703dbcc56bb38779f4e576cc03be8cc0046fcc"