Add phosphor icons and improve menus UI
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -23,6 +23,8 @@ import { useDebounceCallback } from "usehooks-ts";
|
|||||||
import type { DocumentDescriptionPage_updateContentMutation } from "#/__generated__/core/DocumentDescriptionPage_updateContentMutation.graphql";
|
import type { DocumentDescriptionPage_updateContentMutation } from "#/__generated__/core/DocumentDescriptionPage_updateContentMutation.graphql";
|
||||||
import type { DocumentDescriptionPageQuery } from "#/__generated__/core/DocumentDescriptionPageQuery.graphql";
|
import type { DocumentDescriptionPageQuery } from "#/__generated__/core/DocumentDescriptionPageQuery.graphql";
|
||||||
|
|
||||||
|
const autoSaveIntervalMs = 1000;
|
||||||
|
|
||||||
export const documentDescriptionPageQuery = graphql`
|
export const documentDescriptionPageQuery = graphql`
|
||||||
query DocumentDescriptionPageQuery($documentId: ID! $versionId: ID! $versionSpecified: Boolean!) {
|
query DocumentDescriptionPageQuery($documentId: ID! $versionId: ID! $versionSpecified: Boolean!) {
|
||||||
# We use this on /documents/:documentId/versions/:versionId/description
|
# We use this on /documents/:documentId/versions/:versionId/description
|
||||||
@@ -113,7 +115,7 @@ export function DocumentDescriptionPage(props: { queryRef: PreloadedQuery<Docume
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [currentVersion.id, updateContent, toast, __]),
|
}, [currentVersion.id, updateContent, toast, __]),
|
||||||
500,
|
autoSaveIntervalMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -3196,6 +3196,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@phosphor-icons/react": {
|
||||||
|
"version": "2.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
|
||||||
|
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8",
|
||||||
|
"react-dom": ">= 16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.58.2",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
@@ -19702,6 +19715,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ariakit/react": "^0.4.17",
|
"@ariakit/react": "^0.4.17",
|
||||||
"@floating-ui/react": "^0.27.19",
|
"@floating-ui/react": "^0.27.19",
|
||||||
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@probo/helpers": "1.0.0",
|
"@probo/helpers": "1.0.0",
|
||||||
"@probo/i18n": "1.0.0",
|
"@probo/i18n": "1.0.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ariakit/react": "^0.4.17",
|
"@ariakit/react": "^0.4.17",
|
||||||
"@floating-ui/react": "^0.27.19",
|
"@floating-ui/react": "^0.27.19",
|
||||||
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@probo/helpers": "1.0.0",
|
"@probo/helpers": "1.0.0",
|
||||||
"@probo/i18n": "1.0.0",
|
"@probo/i18n": "1.0.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
|||||||
@@ -9,21 +9,20 @@ import {
|
|||||||
useFloatingRootContext,
|
useFloatingRootContext,
|
||||||
useInteractions,
|
useInteractions,
|
||||||
} from "@floating-ui/react";
|
} from "@floating-ui/react";
|
||||||
|
import { CodeBlockIcon, ListBulletsIcon, ListNumbersIcon, MinusIcon, PlusIcon, QuotesIcon, TextHOneIcon, TextHThreeIcon, TextHTwoIcon, TextTIcon } from "@phosphor-icons/react";
|
||||||
import { type useEditor } from "@tiptap/react";
|
import { type useEditor } from "@tiptap/react";
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
import { IconPlusSmall } from "../Atoms/Icons";
|
|
||||||
|
|
||||||
import { MenuButton } from "./MenuButton";
|
import { MenuButton } from "./MenuButton";
|
||||||
|
|
||||||
const blockMenuVariants = tv({
|
const blockMenuVariants = tv({
|
||||||
slots: {
|
slots: {
|
||||||
trigger: [
|
trigger: [
|
||||||
"z-50 flex size-6 items-center justify-center",
|
"z-10 flex size-6 items-center justify-center",
|
||||||
"rounded text-txt-tertiary hover:bg-subtle hover:text-txt-primary text-xl font-light cursor-pointer",
|
"rounded text-txt-tertiary hover:bg-subtle hover:text-txt-primary text-xl font-light cursor-pointer",
|
||||||
],
|
],
|
||||||
menu: ["flex items-center gap-1 rounded-lg border border-border-mid bg-level-0 p-1 shadow-md z-50"],
|
menu: ["rounded-lg border border-border-mid bg-level-0 p-1 shadow-md z-20"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,7 +169,7 @@ export function BlockMenu({ editor }: BlockMenuProps) {
|
|||||||
}}
|
}}
|
||||||
className={trigger()}
|
className={trigger()}
|
||||||
>
|
>
|
||||||
<IconPlusSmall size={16} />
|
<PlusIcon size={16} weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -183,50 +182,69 @@ export function BlockMenu({ editor }: BlockMenuProps) {
|
|||||||
onMouseDown={e => e.preventDefault()}
|
onMouseDown={e => e.preventDefault()}
|
||||||
className={menu()}
|
className={menu()}
|
||||||
>
|
>
|
||||||
|
<div className="p-1 font-semibold text-sm">Style</div>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="H1"
|
active={false}
|
||||||
active={editor.isActive("heading", { level: 1 })}
|
onClick={() => handleAction(chain => chain.setParagraph())}
|
||||||
|
>
|
||||||
|
<TextTIcon size={16} weight="bold" />
|
||||||
|
Text
|
||||||
|
</MenuButton>
|
||||||
|
<MenuButton
|
||||||
|
active={false}
|
||||||
onClick={() => handleAction(chain => chain.toggleHeading({ level: 1 }))}
|
onClick={() => handleAction(chain => chain.toggleHeading({ level: 1 }))}
|
||||||
/>
|
>
|
||||||
|
<TextHOneIcon size={16} weight="bold" />
|
||||||
|
Heading 1
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="H2"
|
active={false}
|
||||||
active={editor.isActive("heading", { level: 2 })}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleHeading({ level: 2 }))}
|
onClick={() => handleAction(chain => chain.toggleHeading({ level: 2 }))}
|
||||||
/>
|
>
|
||||||
|
<TextHTwoIcon size={16} weight="bold" />
|
||||||
|
Heading 2
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="H3"
|
active={false}
|
||||||
active={editor.isActive("heading", { level: 3 })}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleHeading({ level: 3 }))}
|
onClick={() => handleAction(chain => chain.toggleHeading({ level: 3 }))}
|
||||||
/>
|
>
|
||||||
|
<TextHThreeIcon size={16} weight="bold" />
|
||||||
|
Heading 3
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Bullet List"
|
active={false}
|
||||||
active={editor.isActive("bulletList")}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleBulletList())}
|
onClick={() => handleAction(chain => chain.toggleBulletList())}
|
||||||
/>
|
>
|
||||||
|
<ListBulletsIcon size={16} weight="bold" />
|
||||||
|
Bullet List
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Ordered List"
|
active={false}
|
||||||
active={editor.isActive("orderedList")}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleOrderedList())}
|
onClick={() => handleAction(chain => chain.toggleOrderedList())}
|
||||||
/>
|
>
|
||||||
|
<ListNumbersIcon size={16} weight="bold" />
|
||||||
|
Ordered List
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Code"
|
active={false}
|
||||||
active={editor.isActive("code")}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleCode())}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Code Block"
|
|
||||||
active={editor.isActive("codeBlock")}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleCodeBlock())}
|
onClick={() => handleAction(chain => chain.toggleCodeBlock())}
|
||||||
/>
|
>
|
||||||
|
<CodeBlockIcon size={16} weight="bold" />
|
||||||
|
Code Block
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Blockquote"
|
active={false}
|
||||||
active={editor.isActive("blockquote")}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleBlockquote())}
|
onClick={() => handleAction(chain => chain.toggleBlockquote())}
|
||||||
/>
|
>
|
||||||
|
<QuotesIcon size={16} weight="bold" />
|
||||||
|
Blockquote
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Divider"
|
|
||||||
onClick={() => handleAction(chain => chain.setHorizontalRule())}
|
onClick={() => handleAction(chain => chain.setHorizontalRule())}
|
||||||
/>
|
>
|
||||||
|
<MinusIcon size={16} weight="bold" />
|
||||||
|
Divider
|
||||||
|
</MenuButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CodeIcon, LinkIcon, TextBIcon, TextItalicIcon, TextStrikethroughIcon, TextUnderlineIcon } from "@phosphor-icons/react";
|
||||||
import type { useEditor } from "@tiptap/react";
|
import type { useEditor } from "@tiptap/react";
|
||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
@@ -21,32 +22,36 @@ export function BubbleMenu(props: BubbleMenuProps) {
|
|||||||
className={bubbleMenuVariants()}
|
className={bubbleMenuVariants()}
|
||||||
>
|
>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Bold"
|
|
||||||
active={editor.isActive("bold")}
|
active={editor.isActive("bold")}
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
/>
|
>
|
||||||
|
<TextBIcon size={16} weight="bold" />
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Italic"
|
|
||||||
active={editor.isActive("italic")}
|
active={editor.isActive("italic")}
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
/>
|
>
|
||||||
|
<TextItalicIcon size={16} weight="bold" />
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Underline"
|
|
||||||
active={editor.isActive("underline")}
|
active={editor.isActive("underline")}
|
||||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
/>
|
>
|
||||||
|
<TextUnderlineIcon size={16} weight="bold" />
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Strike"
|
|
||||||
active={editor.isActive("strike")}
|
active={editor.isActive("strike")}
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
/>
|
>
|
||||||
|
<TextStrikethroughIcon size={16} weight="bold" />
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Code"
|
|
||||||
active={editor.isActive("code")}
|
active={editor.isActive("code")}
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
/>
|
>
|
||||||
|
<CodeIcon size={16} weight="bold" />
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Link"
|
|
||||||
active={editor.isActive("link")}
|
active={editor.isActive("link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (editor.isActive("link")) {
|
if (editor.isActive("link")) {
|
||||||
@@ -58,7 +63,9 @@ export function BubbleMenu(props: BubbleMenuProps) {
|
|||||||
editor.chain().focus().setLink({ href: url }).run();
|
editor.chain().focus().setLink({ href: url }).run();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<LinkIcon size={16} weight="bold" />
|
||||||
|
</MenuButton>
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import type { PropsWithChildren } from "react";
|
||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
const menuButtonVariants = tv({
|
const menuButtonVariants = tv({
|
||||||
base: ["px-2 py-1 text-sm rounded-sm font-semibold bg-level-0 hover:bg-subtle"],
|
base: [
|
||||||
|
"flex items-center gap-2 w-full",
|
||||||
|
"px-2 py-1.5 text-sm rounded-sm bg-level-0 hover:bg-subtle cursor-pointer",
|
||||||
|
],
|
||||||
variants: {
|
variants: {
|
||||||
active: {
|
active: {
|
||||||
true: ["bg-active"],
|
true: ["bg-active"],
|
||||||
@@ -10,19 +14,18 @@ const menuButtonVariants = tv({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type MenuButtonProps = {
|
type MenuButtonProps = {
|
||||||
label: string;
|
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MenuButton({ label, active, onClick }: MenuButtonProps) {
|
export function MenuButton({ children, active, onClick }: PropsWithChildren<MenuButtonProps>) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={menuButtonVariants({ active })}
|
className={menuButtonVariants({ active })}
|
||||||
>
|
>
|
||||||
{label}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,22 @@ import {
|
|||||||
useFloatingRootContext,
|
useFloatingRootContext,
|
||||||
useInteractions,
|
useInteractions,
|
||||||
} from "@floating-ui/react";
|
} from "@floating-ui/react";
|
||||||
|
import { CodeBlockIcon, DotsSixVerticalIcon, ListBulletsIcon, ListNumbersIcon, QuotesIcon, TextHOneIcon, TextHThreeIcon, TextHTwoIcon, TextTIcon } from "@phosphor-icons/react";
|
||||||
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
|
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
|
||||||
import type { EditorView } from "@tiptap/pm/view";
|
import type { EditorView } from "@tiptap/pm/view";
|
||||||
import { type useEditor } from "@tiptap/react";
|
import { type useEditor } from "@tiptap/react";
|
||||||
import { type DragEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { type DragEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
import { IconChevronGrabberVertical } from "../Atoms/Icons";
|
|
||||||
|
|
||||||
import { MenuButton } from "./MenuButton";
|
import { MenuButton } from "./MenuButton";
|
||||||
|
|
||||||
const optionsMenuVariants = tv({
|
const optionsMenuVariants = tv({
|
||||||
slots: {
|
slots: {
|
||||||
trigger: [
|
trigger: [
|
||||||
"z-50 flex size-6 items-center justify-center",
|
"z-10 flex size-6 items-center justify-center",
|
||||||
"rounded text-txt-tertiary hover:bg-subtle hover:text-txt-primary cursor-grab",
|
"rounded text-txt-tertiary hover:bg-subtle hover:text-txt-primary cursor-grab",
|
||||||
],
|
],
|
||||||
menu: ["flex items-center gap-1 rounded-lg border border-border-mid bg-level-0 p-1 shadow-md z-50"],
|
menu: ["rounded-lg border border-border-mid bg-level-0 p-1 shadow-md z-20"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,7 +264,7 @@ export function OptionsMenu({ editor }: OptionsMenuProps) {
|
|||||||
}}
|
}}
|
||||||
className={trigger()}
|
className={trigger()}
|
||||||
>
|
>
|
||||||
<IconChevronGrabberVertical size={16} />
|
<DotsSixVerticalIcon size={20} weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -278,46 +277,63 @@ export function OptionsMenu({ editor }: OptionsMenuProps) {
|
|||||||
onMouseDown={e => e.preventDefault()}
|
onMouseDown={e => e.preventDefault()}
|
||||||
className={menu()}
|
className={menu()}
|
||||||
>
|
>
|
||||||
|
<div className="p-1 font-semibold text-sm">Turn into</div>
|
||||||
|
<MenuButton
|
||||||
|
active={isNodeType("paragraph")}
|
||||||
|
onClick={() => handleAction(chain => chain.setParagraph())}
|
||||||
|
>
|
||||||
|
<TextTIcon size={16} weight="bold" />
|
||||||
|
Text
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="H1"
|
|
||||||
active={isNodeType("heading", { level: 1 })}
|
active={isNodeType("heading", { level: 1 })}
|
||||||
onClick={() => handleAction(chain => chain.toggleHeading({ level: 1 }))}
|
onClick={() => handleAction(chain => chain.toggleHeading({ level: 1 }))}
|
||||||
/>
|
>
|
||||||
|
<TextHOneIcon size={16} weight="bold" />
|
||||||
|
Heading 1
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="H2"
|
|
||||||
active={isNodeType("heading", { level: 2 })}
|
active={isNodeType("heading", { level: 2 })}
|
||||||
onClick={() => handleAction(chain => chain.toggleHeading({ level: 2 }))}
|
onClick={() => handleAction(chain => chain.toggleHeading({ level: 2 }))}
|
||||||
/>
|
>
|
||||||
|
<TextHTwoIcon size={16} weight="bold" />
|
||||||
|
Heading 2
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="H3"
|
|
||||||
active={isNodeType("heading", { level: 3 })}
|
active={isNodeType("heading", { level: 3 })}
|
||||||
onClick={() => handleAction(chain => chain.toggleHeading({ level: 3 }))}
|
onClick={() => handleAction(chain => chain.toggleHeading({ level: 3 }))}
|
||||||
/>
|
>
|
||||||
|
<TextHThreeIcon size={16} weight="bold" />
|
||||||
|
Heading 3
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Bullet List"
|
|
||||||
active={isNodeType("bulletList")}
|
active={isNodeType("bulletList")}
|
||||||
onClick={() => handleAction(chain => chain.toggleBulletList())}
|
onClick={() => handleAction(chain => chain.toggleBulletList())}
|
||||||
/>
|
>
|
||||||
|
<ListBulletsIcon size={16} weight="bold" />
|
||||||
|
Bullet List
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Ordered List"
|
|
||||||
active={isNodeType("orderedList")}
|
active={isNodeType("orderedList")}
|
||||||
onClick={() => handleAction(chain => chain.toggleOrderedList())}
|
onClick={() => handleAction(chain => chain.toggleOrderedList())}
|
||||||
/>
|
>
|
||||||
|
<ListNumbersIcon size={16} weight="bold" />
|
||||||
|
Ordered List
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Code"
|
|
||||||
active={isNodeType("code")}
|
|
||||||
onClick={() => handleAction(chain => chain.toggleCode())}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Code Block"
|
|
||||||
active={isNodeType("codeBlock")}
|
active={isNodeType("codeBlock")}
|
||||||
onClick={() => handleAction(chain => chain.toggleCodeBlock())}
|
onClick={() => handleAction(chain => chain.toggleCodeBlock())}
|
||||||
/>
|
>
|
||||||
|
<CodeBlockIcon size={16} weight="bold" />
|
||||||
|
Code Block
|
||||||
|
</MenuButton>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="Blockquote"
|
|
||||||
active={isNodeType("blockquote")}
|
active={isNodeType("blockquote")}
|
||||||
onClick={() => handleAction(chain => chain.toggleBlockquote())}
|
onClick={() => handleAction(chain => chain.toggleBlockquote())}
|
||||||
/>
|
>
|
||||||
|
<QuotesIcon size={16} weight="bold" />
|
||||||
|
Blockquote
|
||||||
|
</MenuButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ import { Text } from "@tiptap/extension-text";
|
|||||||
import { Underline } from "@tiptap/extension-underline";
|
import { Underline } from "@tiptap/extension-underline";
|
||||||
import { Dropcursor, Gapcursor, UndoRedo } from "@tiptap/extensions";
|
import { Dropcursor, Gapcursor, UndoRedo } from "@tiptap/extensions";
|
||||||
import { type Content, EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
import { type Content, EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
||||||
import { BubbleMenu } from "@tiptap/react/menus";
|
|
||||||
import { type ComponentProps, useEffect, useRef } from "react";
|
import { type ComponentProps, useEffect, useRef } from "react";
|
||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
import { BlockMenu } from "./BlockMenu";
|
import { BlockMenu } from "./BlockMenu";
|
||||||
|
import { BubbleMenu } from "./BubbleMenu";
|
||||||
import { LinkExtension } from "./LinkExtension";
|
import { LinkExtension } from "./LinkExtension";
|
||||||
import { MenuButton } from "./MenuButton";
|
|
||||||
import { OptionsMenu } from "./OptionsMenu";
|
import { OptionsMenu } from "./OptionsMenu";
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
@@ -49,14 +48,9 @@ const extensions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const richEditorVariants = tv({
|
const richEditorVariants = tv({
|
||||||
slots: {
|
base: ["h-full px-12"],
|
||||||
bubbleMenu: ["flex items-center gap-1 rounded-lg border border-border-mid bg-level-0 p-1 shadow-md"],
|
|
||||||
editor: ["h-full px-12"],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { bubbleMenu, editor: editorVariants } = richEditorVariants();
|
|
||||||
|
|
||||||
type RichEditorProps = ComponentProps<"div"> & {
|
type RichEditorProps = ComponentProps<"div"> & {
|
||||||
content: string;
|
content: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -94,55 +88,10 @@ export function RichEditor(props: RichEditorProps) {
|
|||||||
}, [content, watchedContent, onChangeContent]);
|
}, [content, watchedContent, onChangeContent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={editorVariants({ className })}>
|
<div className={richEditorVariants({ className })}>
|
||||||
<BubbleMenu
|
<BubbleMenu editor={editor} />
|
||||||
editor={editor}
|
|
||||||
className={bubbleMenu()}
|
|
||||||
>
|
|
||||||
<MenuButton
|
|
||||||
label="Bold"
|
|
||||||
active={editor.isActive("bold")}
|
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Italic"
|
|
||||||
active={editor.isActive("italic")}
|
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Underline"
|
|
||||||
active={editor.isActive("underline")}
|
|
||||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Strike"
|
|
||||||
active={editor.isActive("strike")}
|
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Code"
|
|
||||||
active={editor.isActive("code")}
|
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
||||||
/>
|
|
||||||
<MenuButton
|
|
||||||
label="Link"
|
|
||||||
active={editor.isActive("link")}
|
|
||||||
onClick={() => {
|
|
||||||
if (editor.isActive("link")) {
|
|
||||||
editor.chain().focus().unsetLink().run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = window.prompt("URL");
|
|
||||||
if (url) {
|
|
||||||
editor.chain().focus().setLink({ href: url }).run();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</BubbleMenu>
|
|
||||||
|
|
||||||
<BlockMenu editor={editor} />
|
<BlockMenu editor={editor} />
|
||||||
<OptionsMenu editor={editor} />
|
<OptionsMenu editor={editor} />
|
||||||
|
|
||||||
<EditorContent className="h-full" editor={editor} />
|
<EditorContent className="h-full" editor={editor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user