;
84 |
--------------------------------------------------------------------------------
/src/components/sidebar-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { SidebarNavItem } from "@/types/nav";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 |
7 | import type { DocsConfig } from "@/config/docs";
8 | import { cn } from "@/lib/utils";
9 |
10 | export interface DocsSidebarNavProps {
11 | config: DocsConfig;
12 | }
13 |
14 | export function DocsSidebarNav({ config }: DocsSidebarNavProps) {
15 | const pathname = usePathname();
16 |
17 | const items = config.sidebarNav;
18 |
19 | return items.length ? (
20 |
21 | {items.map((item, index) => (
22 |
23 |
24 | {item.title}
25 |
26 | {item?.items?.length && (
27 |
28 | )}
29 |
30 | ))}
31 |
32 | ) : null;
33 | }
34 |
35 | interface DocsSidebarNavItemsProps {
36 | items: SidebarNavItem[];
37 | pathname: string | null;
38 | }
39 |
40 | export function DocsSidebarNavItems({
41 | items,
42 | pathname,
43 | }: DocsSidebarNavItemsProps) {
44 | return items?.length ? (
45 |
46 | {items.map((item, index) =>
47 | item.href && !item.disabled ? (
48 |
61 | {item.title}
62 | {item.label && (
63 |
64 | {item.label}
65 |
66 | )}
67 |
68 | ) : (
69 |
76 | {item.title}
77 | {item.label && (
78 |
79 | {item.label}
80 |
81 | )}
82 |
83 | ),
84 | )}
85 |
86 | ) : null;
87 | }
88 |
--------------------------------------------------------------------------------
/src/registry/examples/color-highlight-example.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ColorHighlightToolbar } from "@/registry/toolbars/color-and-highlight";
4 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider";
5 | import Color from "@tiptap/extension-color";
6 | import Highlight from "@tiptap/extension-highlight";
7 | import TextAlign from "@tiptap/extension-text-align";
8 | import TextStyle from "@tiptap/extension-text-style";
9 | import { EditorContent, useEditor } from "@tiptap/react";
10 | import StarterKit from "@tiptap/starter-kit";
11 |
12 | const extensions = [
13 | StarterKit.configure({
14 | orderedList: {
15 | HTMLAttributes: {
16 | class: "list-decimal",
17 | },
18 | },
19 | heading: {
20 | levels: [1, 2, 3, 4],
21 | HTMLAttributes: {
22 | class: "tiptap-heading",
23 | },
24 | },
25 | blockquote: {
26 | HTMLAttributes: {
27 | class: "text-accent-foreground p-2",
28 | },
29 | },
30 | bulletList: {
31 | HTMLAttributes: {
32 | class: "list-disc",
33 | },
34 | },
35 | }),
36 | TextAlign.configure({
37 | types: ["heading", "paragraph"],
38 | }),
39 | TextStyle,
40 | Color,
41 | Highlight.configure({
42 | multicolor: true,
43 | }),
44 | ];
45 |
46 | const content = `
47 | Exploring Text Colors and Highlights 🎨
48 | Welcome to our color and highlight demonstration!
49 | Here's some red text to catch your eye.
50 | And now, let's try a yellow highlight for emphasis.
51 | We can also combine blue text with an orange background.
52 | Feel free to experiment with the color toolbar above to create your own colorful content!
53 | Remember, judicious use of color can enhance readability and highlight key points in your document.
54 | `;
55 |
56 | const ColorHighlightExample = () => {
57 | const editor = useEditor({
58 | extensions,
59 | content,
60 | immediatelyRender: false,
61 | });
62 |
63 | if (!editor) {
64 | return null;
65 | }
66 | return (
67 |
68 |
69 |
70 |
71 |
72 |
73 |
{
75 | editor?.chain().focus().run();
76 | }}
77 | className="cursor-text min-h-[18rem] bg-background"
78 | >
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default ColorHighlightExample;
86 |
--------------------------------------------------------------------------------
/src/registry/toolbar-examples/starter-kit-toolbar-example.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Separator } from "@/components/ui/separator";
4 | import { BlockquoteToolbar } from "@/registry/toolbars/blockquote";
5 | import { BoldToolbar } from "@/registry/toolbars/bold";
6 | import { BulletListToolbar } from "@/registry/toolbars/bullet-list";
7 | import { CodeToolbar } from "@/registry/toolbars/code";
8 | import { CodeBlockToolbar } from "@/registry/toolbars/code-block";
9 | import { HardBreakToolbar } from "@/registry/toolbars/hard-break";
10 | import { HorizontalRuleToolbar } from "@/registry/toolbars/horizontal-rule";
11 | import { ItalicToolbar } from "@/registry/toolbars/italic";
12 | import { OrderedListToolbar } from "@/registry/toolbars/ordered-list";
13 | import { RedoToolbar } from "@/registry/toolbars/redo";
14 | import { StrikeThroughToolbar } from "@/registry/toolbars/strikethrough";
15 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider";
16 | import { UndoToolbar } from "@/registry/toolbars/undo";
17 | import { type Extension, useEditor } from "@tiptap/react";
18 | import StarterKit from "@tiptap/starter-kit";
19 |
20 | const extensions = [
21 | StarterKit.configure({
22 | orderedList: {
23 | HTMLAttributes: {
24 | class: "list-decimal",
25 | },
26 | },
27 | bulletList: {
28 | HTMLAttributes: {
29 | class: "list-disc",
30 | },
31 | },
32 | code: {
33 | HTMLAttributes: {
34 | class: "bg-accent rounded-md p-1",
35 | },
36 | },
37 | horizontalRule: {
38 | HTMLAttributes: {
39 | class: "my-2",
40 | },
41 | },
42 | codeBlock: {
43 | HTMLAttributes: {
44 | class: "bg-primary text-primary-foreground p-2 text-sm rounded-md p-1",
45 | },
46 | },
47 | heading: {
48 | levels: [1, 2, 3, 4],
49 | HTMLAttributes: {
50 | class: "tiptap-heading",
51 | },
52 | },
53 | }),
54 | ];
55 |
56 | const StarterKitToolbarExample = () => {
57 | const editor = useEditor({
58 | extensions: extensions as Extension[],
59 | immediatelyRender: false,
60 | });
61 |
62 | if (!editor) {
63 | return null;
64 | }
65 | return (
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default StarterKitToolbarExample;
89 |
--------------------------------------------------------------------------------
/src/components/toc.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | "use client";
3 |
4 | import * as React from "react";
5 |
6 | import { useMounted } from "@/hooks/use-mounted";
7 | import type { TableOfContents } from "@/lib/toc";
8 | import { cn } from "@/lib/utils";
9 |
10 | interface TocProps {
11 | toc: TableOfContents;
12 | }
13 |
14 | export function DashboardTableOfContents({ toc }: TocProps) {
15 | const itemIds = React.useMemo(
16 | () =>
17 | toc.items
18 | ? toc.items
19 | .flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
20 | .flat()
21 | .filter(Boolean)
22 | .map((id) => id?.split("#")[1])
23 | : [],
24 | [toc],
25 | );
26 | const activeHeading = useActiveItem(itemIds);
27 | const _ = useMounted();
28 |
29 | if (!toc?.items?.length) {
30 | return null;
31 | }
32 |
33 | return (
34 |
35 |
On This Page
36 |
37 |
38 | );
39 | }
40 |
41 | function useActiveItem(itemIds: string[]) {
42 | const [activeId, setActiveId] = React.useState(null);
43 |
44 | React.useEffect(() => {
45 | const observer = new IntersectionObserver(
46 | (entries) => {
47 | entries.map((entry) => {
48 | if (entry.isIntersecting) {
49 | setActiveId(entry.target.id);
50 | }
51 | });
52 | },
53 | { rootMargin: `0% 0% -80% 0%` },
54 | );
55 |
56 | itemIds?.map((id) => {
57 | const element = document.getElementById(id);
58 | if (element) {
59 | observer.observe(element);
60 | }
61 | });
62 |
63 | return () => {
64 | itemIds?.map((id) => {
65 | const element = document.getElementById(id);
66 | if (element) {
67 | observer.unobserve(element);
68 | }
69 | });
70 | };
71 | }, [itemIds]);
72 |
73 | return activeId;
74 | }
75 |
76 | interface TreeProps {
77 | tree: TableOfContents;
78 | level?: number;
79 | activeItem?: string;
80 | }
81 |
82 | function Tree({ tree, level = 1, activeItem }: TreeProps) {
83 | return tree?.items?.length && level < 3 ? (
84 |
85 | {tree.items.map((item, index) => {
86 | return (
87 | -
88 |
97 | {item.title}
98 |
99 | {item.items?.length ? (
100 |
101 | ) : null}
102 |
103 | );
104 | })}
105 |
106 | ) : null;
107 | }
108 |
--------------------------------------------------------------------------------
/src/app/_components/hero-section.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import TiptapEditor from "@/components/editor";
4 | import { Button } from "@/components/ui/button";
5 | import { GitHubLogoIcon } from "@radix-ui/react-icons";
6 | import { motion } from "framer-motion";
7 | import { Coffee } from "lucide-react";
8 | import Link from "next/link";
9 |
10 | export const HeroSection = ({ starCount: _ }: { starCount: number }) => {
11 | const staggeredAnimation = {
12 | hidden: { opacity: 0, y: -20, filter: "blur(4px)" },
13 | visible: {
14 | opacity: 1,
15 | y: 0,
16 | filter: "blur(0px)",
17 | transition: {
18 | when: "beforeChildren",
19 | staggerChildren: 0.1,
20 | },
21 | },
22 | };
23 |
24 | const childAnimation = {
25 | hidden: { opacity: 0, y: -20, filter: "blur(4px)" },
26 | visible: {
27 | opacity: 1,
28 | y: 0,
29 | filter: "blur(0px)",
30 | transition: {
31 | type: "spring",
32 | bounce: 0,
33 | },
34 | },
35 | };
36 |
37 | return (
38 |
44 |
45 |
46 | Shadcn Tiptap
47 |
48 |
52 | Collection of custom extensions and toolbars for Tiptap editor.
{" "}
53 | Ready to copy & paste and use with shadcn/ui components
54 |
55 |
59 |
62 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
91 |
92 | );
93 | };
94 |
--------------------------------------------------------------------------------
/public/r/color-highlight-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "color-highlight-example",
3 | "type": "registry:example",
4 | "files": [
5 | {
6 | "path": "examples/color-highlight-example.tsx",
7 | "type": "registry:example",
8 | "content": "\"use client\";\n\nimport { ColorHighlightToolbar } from \"@/components/toolbars/color-and-highlight\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport Color from \"@tiptap/extension-color\";\nimport Highlight from \"@tiptap/extension-highlight\";\nimport TextAlign from \"@tiptap/extension-text-align\";\nimport TextStyle from \"@tiptap/extension-text-style\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"text-accent-foreground p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t}),\n\tTextAlign.configure({\n\t\ttypes: [\"heading\", \"paragraph\"],\n\t}),\n\tTextStyle,\n\tColor,\n\tHighlight.configure({\n\t\tmulticolor: true,\n\t}),\n];\n\nconst content = `\nExploring Text Colors and Highlights 🎨
\nWelcome to our color and highlight demonstration!
\nHere's some red text to catch your eye.
\nAnd now, let's try a yellow highlight for emphasis.
\nWe can also combine blue text with an orange background.
\nFeel free to experiment with the color toolbar above to create your own colorful content!
\nRemember, judicious use of color can enhance readability and highlight key points in your document.
\n`;\n\nconst ColorHighlightExample = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\tcontent,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t
{\n\t\t\t\t\teditor?.chain().focus().run();\n\t\t\t\t}}\n\t\t\t\tclassName=\"cursor-text min-h-[18rem] bg-background\"\n\t\t\t>\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default ColorHighlightExample;\n",
9 | "target": "components/color-highlight-example.tsx"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/public/r/starter-kit-toolbar-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starter-kit-toolbar-example",
3 | "type": "registry:example",
4 | "files": [
5 | {
6 | "path": "toolbar-examples/starter-kit-toolbar-example.tsx",
7 | "type": "registry:example",
8 | "content": "\"use client\";\n\nimport { Separator } from \"@/components/ui/separator\";\nimport { BlockquoteToolbar } from \"@/components/toolbars/blockquote\";\nimport { BoldToolbar } from \"@/components/toolbars/bold\";\nimport { BulletListToolbar } from \"@/components/toolbars/bullet-list\";\nimport { CodeToolbar } from \"@/components/toolbars/code\";\nimport { CodeBlockToolbar } from \"@/components/toolbars/code-block\";\nimport { HardBreakToolbar } from \"@/components/toolbars/hard-break\";\nimport { HorizontalRuleToolbar } from \"@/components/toolbars/horizontal-rule\";\nimport { ItalicToolbar } from \"@/components/toolbars/italic\";\nimport { OrderedListToolbar } from \"@/components/toolbars/ordered-list\";\nimport { RedoToolbar } from \"@/components/toolbars/redo\";\nimport { StrikeThroughToolbar } from \"@/components/toolbars/strikethrough\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { UndoToolbar } from \"@/components/toolbars/undo\";\nimport { type Extension, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\tcode: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-accent rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\thorizontalRule: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"my-2\",\n\t\t\t},\n\t\t},\n\t\tcodeBlock: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-primary text-primary-foreground p-2 text-sm rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n];\n\nconst StarterKitToolbarExample = () => {\n\tconst editor = useEditor({\n\t\textensions: extensions as Extension[],\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\tbutton]:shrink-0 items-center gap-2\">\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t
\n\t);\n};\n\nexport default StarterKitToolbarExample;\n",
9 | "target": "components/starter-kit-toolbar-example.tsx"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/src/content/docs/installation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | description: Installation guide for shadcn tiptap.
4 | ---
5 |
6 |
7 |
8 |
9 | Install core packages
10 |
11 | ```bash
12 | npm install @tiptap/react @tiptap/core @tiptap/pm
13 | ```
14 |
15 | Install `ToolbarProvider` component
16 |
17 | `ToolbarProvider` accepts editor as a prop and serves it to its children using react context.
18 | Think of this as a provider for your toolbar components.
19 |
20 | **Install with `shadcn` cli:**
21 |
22 | ```bash
23 | npx shadcn add https://tiptap.niazmorshed.dev/r/toolbar-provider.json
24 | ```
25 |
26 | **Or manually copy & paste**, the following code under `components/toolbars/toolbar-provider.tsx`.
27 |
28 | ```jsx
29 | "use client";
30 |
31 | import type { Editor } from "@tiptap/react";
32 | import React from "react";
33 |
34 | export interface ToolbarContextProps {
35 | editor: Editor;
36 | }
37 |
38 | export const ToolbarContext = React.createContext(
39 | null,
40 | );
41 |
42 | interface ToolbarProviderProps {
43 | editor: Editor;
44 | children: React.ReactNode;
45 | }
46 |
47 | export const ToolbarProvider = ({ editor, children }: ToolbarProviderProps) => {
48 | return (
49 |
50 | {children}
51 |
52 | );
53 | };
54 |
55 | export const useToolbar = () => {
56 | const context = React.useContext(ToolbarContext);
57 |
58 | if (!context) {
59 | throw new Error("useToolbar must be used within a ToolbarProvider");
60 | }
61 |
62 | return context;
63 | };
64 |
65 | ```
66 |
67 | Copy & paste the following code under `src/app/globals.css`.
68 |
69 |
70 | It's important to be careful about what class names you use for the editor.
71 |
72 |
73 | ```css
74 | .ProseMirror {
75 | @apply px-4 pt-2;
76 | outline: none !important;
77 | }
78 |
79 | h1.tiptap-heading {
80 | @apply mb-6 mt-8 text-4xl font-bold;
81 | }
82 |
83 | h2.tiptap-heading {
84 | @apply mb-4 mt-6 text-3xl font-bold;
85 | }
86 |
87 | h3.tiptap-heading {
88 | @apply mb-3 mt-4 text-xl font-bold;
89 | }
90 |
91 | h1.tiptap-heading:first-child,
92 | h2.tiptap-heading:first-child,
93 | h3.tiptap-heading:first-child {
94 | margin-top: 0;
95 | }
96 |
97 | h1.tiptap-heading + h2.tiptap-heading,
98 | h1.tiptap-heading + h3.tiptap-heading,
99 | h2.tiptap-heading + h1.tiptap-heading,
100 | h2.tiptap-heading + h3.tiptap-heading,
101 | h3.tiptap-heading + h1.tiptap-heading,
102 | h3.tiptap-heading + h2.tiptap-heading {
103 | margin-top: 0;
104 | }
105 |
106 | .tiptap p.is-editor-empty:first-child::before {
107 | @apply pointer-events-none float-left h-0 text-accent-foreground;
108 | content: attr(data-placeholder);
109 | }
110 |
111 | .tiptap ul,
112 | .tiptap ol {
113 | padding: 0 1rem;
114 | }
115 |
116 | .tiptap blockquote {
117 | border-left: 3px solid gray;
118 | margin: 1.5rem 0;
119 | padding-left: 1rem;
120 | }
121 | ```
122 |
123 | That's it! Now go ahead and install any extensions and toolbars you want.
124 |
125 |
126 |
--------------------------------------------------------------------------------
/src/registry/examples/starter-kit-example.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Separator } from "@/components/ui/separator";
4 | import { BlockquoteToolbar } from "@/registry/toolbars/blockquote";
5 | import { BoldToolbar } from "@/registry/toolbars/bold";
6 | import { BulletListToolbar } from "@/registry/toolbars/bullet-list";
7 | import { CodeToolbar } from "@/registry/toolbars/code";
8 | import { CodeBlockToolbar } from "@/registry/toolbars/code-block";
9 | import { HardBreakToolbar } from "@/registry/toolbars/hard-break";
10 | import { HorizontalRuleToolbar } from "@/registry/toolbars/horizontal-rule";
11 | import { ItalicToolbar } from "@/registry/toolbars/italic";
12 | import { OrderedListToolbar } from "@/registry/toolbars/ordered-list";
13 | import { RedoToolbar } from "@/registry/toolbars/redo";
14 | import { StrikeThroughToolbar } from "@/registry/toolbars/strikethrough";
15 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider";
16 | import { UndoToolbar } from "@/registry/toolbars/undo";
17 | import { EditorContent, type Extension, useEditor } from "@tiptap/react";
18 | import StarterKit from "@tiptap/starter-kit";
19 |
20 | const extensions = [
21 | StarterKit.configure({
22 | orderedList: {
23 | HTMLAttributes: {
24 | class: "list-decimal",
25 | },
26 | },
27 | bulletList: {
28 | HTMLAttributes: {
29 | class: "list-disc",
30 | },
31 | },
32 | code: {
33 | HTMLAttributes: {
34 | class: "bg-accent rounded-md p-1",
35 | },
36 | },
37 | horizontalRule: {
38 | HTMLAttributes: {
39 | class: "my-2",
40 | },
41 | },
42 | codeBlock: {
43 | HTMLAttributes: {
44 | class: "bg-primary text-primary-foreground p-2 text-sm rounded-md p-1",
45 | },
46 | },
47 | heading: {
48 | levels: [1, 2, 3, 4],
49 | HTMLAttributes: {
50 | class: "tiptap-heading",
51 | },
52 | },
53 | }),
54 | ];
55 |
56 | const content = `
57 | Hello world 🌍
58 | `;
59 |
60 | const StarterKitExample = () => {
61 | const editor = useEditor({
62 | extensions: extensions as Extension[],
63 | content,
64 | immediatelyRender: false,
65 | });
66 |
67 | if (!editor) {
68 | return null;
69 | }
70 | return (
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
{
93 | editor?.chain().focus().run();
94 | }}
95 | className="cursor-text min-h-[18rem] bg-background"
96 | >
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default StarterKitExample;
104 |
--------------------------------------------------------------------------------
/src/registry/toolbars/alignment.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | AlignCenter,
5 | AlignJustify,
6 | AlignLeft,
7 | AlignRight,
8 | Check,
9 | ChevronDown,
10 | } from "lucide-react";
11 |
12 | import { Button } from "@/components/ui/button";
13 | import {
14 | DropdownMenu,
15 | DropdownMenuContent,
16 | DropdownMenuGroup,
17 | DropdownMenuItem,
18 | DropdownMenuTrigger,
19 | } from "@/components/ui/dropdown-menu";
20 | import {
21 | Tooltip,
22 | TooltipContent,
23 | TooltipTrigger,
24 | } from "@/components/ui/tooltip";
25 | import { useToolbar } from "@/registry/toolbars/toolbar-provider";
26 |
27 | export const AlignmentTooolbar = () => {
28 | const { editor } = useToolbar();
29 | const handleAlign = (value: string) => {
30 | editor?.chain().focus().setTextAlign(value).run();
31 | };
32 |
33 | const isDisabled =
34 | (editor?.isActive("image") || editor?.isActive("video") || !editor) ??
35 | false;
36 |
37 | const currentTextAlign = () => {
38 | if (editor?.isActive({ textAlign: "left" })) {
39 | return "left";
40 | }
41 | if (editor?.isActive({ textAlign: "center" })) {
42 | return "center";
43 | }
44 | if (editor?.isActive({ textAlign: "right" })) {
45 | return "right";
46 | }
47 | if (editor?.isActive({ textAlign: "justify" })) {
48 | return "justify";
49 | }
50 |
51 | return "left";
52 | };
53 |
54 | const alignmentOptions = [
55 | {
56 | name: "Left Align",
57 | value: "left",
58 | icon: ,
59 | },
60 | {
61 | name: "Center Align",
62 | value: "center",
63 | icon: ,
64 | },
65 | {
66 | name: "Right Align",
67 | value: "right",
68 | icon: ,
69 | },
70 | {
71 | name: "Justify Align",
72 | value: "justify",
73 | icon: ,
74 | },
75 | ];
76 |
77 | const findIndex = (value: string) => {
78 | return alignmentOptions.findIndex((option) => option.value === value);
79 | };
80 |
81 | return (
82 |
83 |
84 |
85 |
86 |
93 |
94 |
95 | Text Alignment
96 |
97 | {
100 | e.preventDefault();
101 | }}
102 | >
103 |
104 | {alignmentOptions.map((option, index) => (
105 | {
107 | handleAlign(option.value);
108 | }}
109 | key={index}
110 | >
111 | {option.icon}
112 | {option.name}
113 |
114 | {option.value === currentTextAlign() && (
115 |
116 | )}
117 |
118 | ))}
119 |
120 |
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/public/r/starter-kit-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starter-kit-example",
3 | "type": "registry:example",
4 | "files": [
5 | {
6 | "path": "examples/starter-kit-example.tsx",
7 | "type": "registry:example",
8 | "content": "\"use client\";\n\nimport { Separator } from \"@/components/ui/separator\";\nimport { BlockquoteToolbar } from \"@/components/toolbars/blockquote\";\nimport { BoldToolbar } from \"@/components/toolbars/bold\";\nimport { BulletListToolbar } from \"@/components/toolbars/bullet-list\";\nimport { CodeToolbar } from \"@/components/toolbars/code\";\nimport { CodeBlockToolbar } from \"@/components/toolbars/code-block\";\nimport { HardBreakToolbar } from \"@/components/toolbars/hard-break\";\nimport { HorizontalRuleToolbar } from \"@/components/toolbars/horizontal-rule\";\nimport { ItalicToolbar } from \"@/components/toolbars/italic\";\nimport { OrderedListToolbar } from \"@/components/toolbars/ordered-list\";\nimport { RedoToolbar } from \"@/components/toolbars/redo\";\nimport { StrikeThroughToolbar } from \"@/components/toolbars/strikethrough\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { UndoToolbar } from \"@/components/toolbars/undo\";\nimport { EditorContent, type Extension, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\tcode: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-accent rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\thorizontalRule: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"my-2\",\n\t\t\t},\n\t\t},\n\t\tcodeBlock: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-primary text-primary-foreground p-2 text-sm rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n];\n\nconst content = `\nHello world 🌍
\n`;\n\nconst StarterKitExample = () => {\n\tconst editor = useEditor({\n\t\textensions: extensions as Extension[],\n\t\tcontent,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t\t
{\n\t\t\t\t\teditor?.chain().focus().run();\n\t\t\t\t}}\n\t\t\t\tclassName=\"cursor-text min-h-[18rem] bg-background\"\n\t\t\t>\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default StarterKitExample;\n",
9 | "target": "components/starter-kit-example.tsx"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/src/components/component-example.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { CopyButton, CopyWithClassNames } from "@/components/copy-button";
7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8 |
9 | interface ComponentExampleProps extends React.HTMLAttributes {
10 | extractClassname?: boolean;
11 | extractedClassNames?: string;
12 | align?: "center" | "start" | "end";
13 | src?: string;
14 | }
15 |
16 | export function ComponentExample({
17 | children,
18 | className,
19 | extractClassname,
20 | extractedClassNames,
21 | align = "center",
22 | src: _,
23 | ...props
24 | }: ComponentExampleProps) {
25 | const [Example, Code, ...Children] = React.Children.toArray(
26 | children,
27 | ) as React.ReactElement[];
28 |
29 | const codeString = React.useMemo(() => {
30 | if (
31 | typeof Code?.props["data-rehype-pretty-code-fragment"] !== "undefined"
32 | ) {
33 | const [, Button] = React.Children.toArray(
34 | Code.props.children,
35 | ) as React.ReactElement[];
36 | return Button?.props?.value || Button?.props?.__rawString__ || null;
37 | }
38 | }, [Code]);
39 |
40 | return (
41 |
45 |
46 |
47 |
48 |
52 | Preview
53 |
54 |
58 | Code
59 |
60 |
61 | {extractedClassNames ? (
62 |
67 | ) : (
68 | codeString && (
69 |
73 | )
74 | )}
75 |
76 |
77 |
84 | {Example}
85 |
86 |
87 |
88 |
89 |
90 | {Code}
91 |
92 | {Children?.length ? (
93 |
94 | {Children}
95 |
96 | ) : null}
97 |
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/registry/toolbars/link.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PopoverClose } from "@radix-ui/react-popover";
4 | import { Trash2, X } from "lucide-react";
5 |
6 | import React, { type FormEvent } from "react";
7 |
8 | import { Button, type ButtonProps } from "@/components/ui/button";
9 | import { Input } from "@/components/ui/input";
10 | import { Label } from "@/components/ui/label";
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipTrigger,
15 | } from "@/components/ui/tooltip";
16 | import { cn, getUrlFromString } from "@/lib/utils";
17 | import { useToolbar } from "@/registry/toolbars/toolbar-provider";
18 | import {
19 | Popover,
20 | PopoverContent,
21 | PopoverTrigger,
22 | } from "../../components/ui/popover";
23 |
24 | const LinkToolbar = React.forwardRef(
25 | ({ className, ...props }, ref) => {
26 | const { editor } = useToolbar();
27 | const [link, setLink] = React.useState("");
28 |
29 | const handleSubmit = (e: FormEvent) => {
30 | e.preventDefault();
31 | const url = getUrlFromString(link);
32 | url && editor?.chain().focus().setLink({ href: url }).run();
33 | };
34 |
35 | React.useEffect(() => {
36 | setLink(editor?.getAttributes("link").href || "");
37 | }, [editor]);
38 |
39 | return (
40 |
41 |
42 |
43 |
47 |
63 |
64 |
65 |
66 | Link
67 |
68 |
69 |
70 | {
72 | e.preventDefault();
73 | }}
74 | asChild
75 | className="relative px-3 py-2.5"
76 | >
77 |
78 |
79 |
80 |
81 |
117 |
118 |
119 |
120 | );
121 | },
122 | );
123 |
124 | LinkToolbar.displayName = "LinkToolbar";
125 |
126 | export { LinkToolbar };
127 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadcn-tiptap",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "contentlayer2 build && pnpm build:registry && next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "build:registry": "npx tsx --tsconfig ./tsconfig.scripts.json ./src/scripts/build-registry.mts",
11 | "build:docs": "contentlayer2 build"
12 | },
13 | "dependencies": {
14 | "@commitlint/cli": "^19.3.0",
15 | "@commitlint/config-conventional": "^19.2.2",
16 | "@emotion/is-prop-valid": "^1.3.1",
17 | "@evilmartians/lefthook": "^1.7.1",
18 | "@opentelemetry/api": "^1.8.0",
19 | "@radix-ui/react-accordion": "^1.2.1",
20 | "@radix-ui/react-aspect-ratio": "^1.1.0",
21 | "@radix-ui/react-checkbox": "^1.1.1",
22 | "@radix-ui/react-collapsible": "^1.1.1",
23 | "@radix-ui/react-dialog": "^1.1.2",
24 | "@radix-ui/react-dropdown-menu": "^2.1.1",
25 | "@radix-ui/react-icons": "^1.3.0",
26 | "@radix-ui/react-label": "^2.1.0",
27 | "@radix-ui/react-popover": "^1.1.1",
28 | "@radix-ui/react-scroll-area": "^1.2.0",
29 | "@radix-ui/react-separator": "^1.1.0",
30 | "@radix-ui/react-slot": "^1.1.0",
31 | "@radix-ui/react-tabs": "^1.1.1",
32 | "@radix-ui/react-tooltip": "^1.1.2",
33 | "@shikijs/compat": "^1.22.1",
34 | "@shikijs/core": "^1.22.0",
35 | "@tiptap/core": "^2.9.1",
36 | "@tiptap/extension-blockquote": "^2.9.1",
37 | "@tiptap/extension-color": "^2.9.1",
38 | "@tiptap/extension-heading": "^2.9.1",
39 | "@tiptap/extension-highlight": "^2.9.1",
40 | "@tiptap/extension-image": "^2.9.1",
41 | "@tiptap/extension-link": "^2.9.1",
42 | "@tiptap/extension-subscript": "^2.9.1",
43 | "@tiptap/extension-superscript": "^2.9.1",
44 | "@tiptap/extension-text-align": "^2.9.1",
45 | "@tiptap/extension-text-style": "^2.9.1",
46 | "@tiptap/extension-underline": "^2.9.1",
47 | "@tiptap/pm": "^2.9.1",
48 | "@tiptap/react": "^2.9.1",
49 | "@tiptap/starter-kit": "^2.9.1",
50 | "aria-hidden": "^1.2.4",
51 | "class-variance-authority": "^0.7.0",
52 | "clsx": "^2.1.1",
53 | "contentlayer2": "^0.5.1",
54 | "enforce-branch-name": "^1.1.2",
55 | "framer-motion": "^11.11.9",
56 | "linkifyjs": "^4.1.3",
57 | "lodash": "^4.17.21",
58 | "lodash.template": "^4.5.0",
59 | "lucide-react": "^0.407.0",
60 | "mdast-util-toc": "^7.1.0",
61 | "next": "14.2.4",
62 | "next-contentlayer2": "^0.5.1",
63 | "next-themes": "^0.3.0",
64 | "prosemirror-commands": "^1.6.2",
65 | "prosemirror-dropcursor": "^1.8.1",
66 | "prosemirror-gapcursor": "^1.3.2",
67 | "prosemirror-history": "^1.4.1",
68 | "prosemirror-keymap": "^1.2.2",
69 | "prosemirror-model": "^1.23.0",
70 | "prosemirror-schema-list": "^1.4.1",
71 | "prosemirror-state": "^1.4.3",
72 | "prosemirror-transform": "^1.10.2",
73 | "prosemirror-view": "^1.34.3",
74 | "react": "^18",
75 | "react-dom": "^18",
76 | "react-dropzone": "^14.2.10",
77 | "react-remove-scroll": "^2.6.0",
78 | "react-wrap-balancer": "^1.1.1",
79 | "rehype-autolink-headings": "^7.1.0",
80 | "rehype-pretty-code": "^0.14.0",
81 | "rehype-slug": "^6.0.0",
82 | "remark": "^15.0.1",
83 | "remark-code-import": "^1.2.0",
84 | "remark-gfm": "^4.0.0",
85 | "rimraf": "^6.0.1",
86 | "shiki": "^1.22.0",
87 | "tailwind-merge": "^2.4.0",
88 | "tailwindcss-animate": "^1.0.7",
89 | "ts-morph": "^24.0.0",
90 | "unist-builder": "^4.0.0",
91 | "unist-util-visit": "^5.0.0",
92 | "zod": "^3.23.8"
93 | },
94 | "devDependencies": {
95 | "@biomejs/biome": "^1.8.3",
96 | "@tailwindcss/typography": "^0.5.15",
97 | "@types/node": "^20",
98 | "@types/react": "^18",
99 | "@types/react-dom": "^18",
100 | "postcss": "^8",
101 | "tailwindcss": "^3.4.1",
102 | "typescript": "^5"
103 | },
104 | "peerDependencies": {
105 | "react": "^18",
106 | "react-dom": "^18"
107 | },
108 | "pnpm": {
109 | "overrides": {
110 | "@opentelemetry/api": "1.8.0",
111 | "@opentelemetry/core": "1.8.0",
112 | "@opentelemetry/resources": "1.8.0",
113 | "@opentelemetry/sdk-trace-base": "1.8.0",
114 | "@opentelemetry/sdk-metrics": "1.8.0",
115 | "@opentelemetry/sdk-logs": "0.39.1"
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link, { type LinkProps } from "next/link";
4 | import { useRouter } from "next/navigation";
5 | import * as React from "react";
6 |
7 | import { Icons } from "@/components/icons";
8 | import { Button } from "@/components/ui/button";
9 | import { ScrollArea } from "@/components/ui/scroll-area";
10 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
11 | import { docsConfig } from "@/config/docs";
12 | import { siteConfig } from "@/config/site";
13 | import { cn } from "@/lib/utils";
14 |
15 | export function MobileNav() {
16 | const [open, setOpen] = React.useState(false);
17 |
18 | return (
19 |
20 |
21 |
56 |
57 |
58 |
63 |
64 | {siteConfig.name}
65 |
66 |
67 |
68 | {docsConfig.mainNav?.map(
69 | (item) =>
70 | item.href && (
71 |
76 | {item.title}
77 |
78 | ),
79 | )}
80 |
81 |
82 | {docsConfig.sidebarNav.map((item, index) => (
83 |
84 |
{item.title}
85 | {item?.items?.length &&
86 | item.items.map((item) => (
87 |
88 | {!item.disabled &&
89 | (item.href ? (
90 |
95 | {item.title}
96 | {item.label && (
97 |
98 | {item.label}
99 |
100 | )}
101 |
102 | ) : (
103 | item.title
104 | ))}
105 |
106 | ))}
107 |
108 | ))}
109 |
110 |
111 |
112 |
113 | );
114 | }
115 |
116 | interface MobileLinkProps extends LinkProps {
117 | onOpenChange?: (open: boolean) => void;
118 | children: React.ReactNode;
119 | className?: string;
120 | }
121 |
122 | function MobileLink({
123 | href,
124 | onOpenChange,
125 | className,
126 | children,
127 | ...props
128 | }: MobileLinkProps) {
129 | const router = useRouter();
130 | return (
131 | {
134 | router.push(href.toString());
135 | onOpenChange?.(false);
136 | }}
137 | className={cn(className)}
138 | {...props}
139 | >
140 | {children}
141 |
142 | );
143 | }
144 |
--------------------------------------------------------------------------------
/src/app/docs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { allDocs } from "contentlayer/generated";
2 | import { notFound } from "next/navigation";
3 |
4 | import "@/styles/mdx.css";
5 | import { ChevronRightIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
6 | import type { Metadata } from "next";
7 | import Link from "next/link";
8 | import Balancer from "react-wrap-balancer";
9 |
10 | import { Mdx } from "@/components/mdx-components";
11 | import { DocsPager } from "@/components/pager";
12 | import { DashboardTableOfContents } from "@/components/toc";
13 | import { badgeVariants } from "@/components/ui/badge";
14 | import { ScrollArea } from "@/components/ui/scroll-area";
15 | import { siteConfig } from "@/config/site";
16 | import { getTableOfContents } from "@/lib/toc";
17 | import { absoluteUrl, cn } from "@/lib/utils";
18 |
19 | interface DocPageProps {
20 | params: {
21 | slug: string[];
22 | };
23 | }
24 |
25 | async function getDocFromParams({ params }: DocPageProps) {
26 | const slug = params.slug?.join("/") || "";
27 | const doc = allDocs.find((doc) => doc.slugAsParams === slug);
28 |
29 | if (!doc) {
30 | return null;
31 | }
32 |
33 | return doc;
34 | }
35 |
36 | export async function generateMetadata({
37 | params,
38 | }: DocPageProps): Promise {
39 | const doc = await getDocFromParams({ params });
40 |
41 | if (!doc) {
42 | return {};
43 | }
44 |
45 | return {
46 | title: doc.title,
47 | description: doc.description,
48 | openGraph: {
49 | title: doc.title,
50 | description: doc.description,
51 | type: "article",
52 | url: absoluteUrl(doc.slug),
53 | images: [
54 | {
55 | url: siteConfig.ogImage,
56 | width: 1200,
57 | height: 630,
58 | alt: siteConfig.name,
59 | },
60 | ],
61 | },
62 | twitter: {
63 | card: "summary_large_image",
64 | title: doc.title,
65 | description: doc.description,
66 | images: [siteConfig.ogImage],
67 | creator: "@shadcn",
68 | },
69 | };
70 | }
71 |
72 | export async function generateStaticParams(): Promise<
73 | DocPageProps["params"][]
74 | > {
75 | return allDocs.map((doc) => ({
76 | slug: doc.slugAsParams.split("/"),
77 | }));
78 | }
79 |
80 | export default async function DocPage({ params }: DocPageProps) {
81 | const doc = await getDocFromParams({ params });
82 |
83 | if (!doc) {
84 | notFound();
85 | }
86 |
87 | const toc = await getTableOfContents(doc.body.raw);
88 |
89 | return (
90 |
91 |
92 |
93 |
Docs
94 |
95 |
{doc.title}
96 |
97 |
98 |
99 | {doc.title}
100 |
101 | {doc.description && (
102 |
103 | {doc.description}
104 |
105 | )}
106 |
107 | {doc.links ? (
108 |
109 | {doc.links?.doc && (
110 |
116 | Docs
117 |
118 |
119 | )}
120 | {doc.links?.api && (
121 |
127 | API Reference
128 |
129 |
130 | )}
131 |
132 | ) : null}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | {doc.toc && }
142 |
143 |
144 |
145 |
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/contentlayer.config.js:
--------------------------------------------------------------------------------
1 | import { getHighlighter } from "@shikijs/compat";
2 | import {
3 | defineDocumentType,
4 | defineNestedType,
5 | makeSource,
6 | } from "contentlayer2/source-files";
7 | import rehypeAutolinkHeadings from "rehype-autolink-headings";
8 | import rehypePrettyCode from "rehype-pretty-code";
9 | import rehypeSlug from "rehype-slug";
10 | import { codeImport } from "remark-code-import";
11 | import remarkGfm from "remark-gfm";
12 | import { visit } from "unist-util-visit";
13 |
14 | import { rehypeComponent } from "./src/lib/rehype-component";
15 | import { rehypeNpmCommand } from "./src/lib/rehype-npm-command";
16 |
17 | /** @type {import('contentlayer/source-files').ComputedFields} */
18 | const computedFields = {
19 | slug: {
20 | type: "string",
21 | resolve: (doc) => `/${doc._raw.flattenedPath}`,
22 | },
23 | slugAsParams: {
24 | type: "string",
25 | resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"),
26 | },
27 | };
28 |
29 | const LinksProperties = defineNestedType(() => ({
30 | name: "LinksProperties",
31 | fields: {
32 | doc: {
33 | type: "string",
34 | },
35 | api: {
36 | type: "string",
37 | },
38 | },
39 | }));
40 |
41 | export const Doc = defineDocumentType(() => ({
42 | name: "Doc",
43 | filePathPattern: `docs/**/*.mdx`,
44 | contentType: "mdx",
45 |
46 | fields: {
47 | title: {
48 | type: "string",
49 | required: true,
50 | },
51 | description: {
52 | type: "string",
53 | required: true,
54 | },
55 | published: {
56 | type: "boolean",
57 | default: true,
58 | },
59 | links: {
60 | type: "nested",
61 | of: LinksProperties,
62 | },
63 | featured: {
64 | type: "boolean",
65 | default: false,
66 | required: false,
67 | },
68 | component: {
69 | type: "boolean",
70 | default: false,
71 | required: false,
72 | },
73 | toc: {
74 | type: "boolean",
75 | default: true,
76 | required: false,
77 | },
78 | },
79 | computedFields,
80 | }));
81 |
82 | export default makeSource({
83 | contentDirPath: "./src/content",
84 | documentTypes: [Doc],
85 | mdx: {
86 | remarkPlugins: [remarkGfm, codeImport],
87 | rehypePlugins: [
88 | rehypeSlug,
89 | rehypeComponent,
90 | () => (tree) => {
91 | visit(tree, (node) => {
92 | if (node?.type === "element" && node?.tagName === "pre") {
93 | const [codeEl] = node.children;
94 | if (codeEl.tagName !== "code") {
95 | return;
96 | }
97 |
98 | if (codeEl.data?.meta) {
99 | // Extract event from meta and pass it down the tree.
100 | const regex = /event="([^"]*)"/;
101 | const match = codeEl.data?.meta.match(regex);
102 | if (match) {
103 | node.__event__ = match ? match[1] : null;
104 | codeEl.data.meta = codeEl.data.meta.replace(regex, "");
105 | }
106 | }
107 |
108 | node.__rawString__ = codeEl.children?.[0].value;
109 | node.__src__ = node.properties?.__src__;
110 | }
111 | });
112 | },
113 | [
114 | rehypePrettyCode,
115 | {
116 | theme: "github-dark",
117 | getHighlighter,
118 | onVisitLine(node) {
119 | // Prevent lines from collapsing in `display: grid` mode, and allow empty
120 | // lines to be copy/pasted
121 | if (node.children.length === 0) {
122 | node.children = [{ type: "text", value: " " }];
123 | }
124 | },
125 | onVisitHighlightedLine(node) {
126 | node.properties.className.push("line--highlighted");
127 | },
128 | onVisitHighlightedWord(node) {
129 | node.properties.className = ["word--highlighted"];
130 | },
131 | },
132 | ],
133 | () => (tree) => {
134 | visit(tree, (node) => {
135 | if (node?.type === "element") {
136 | const preElement = node.children.at(-1);
137 | if (preElement.tagName !== "pre") {
138 | return;
139 | }
140 |
141 | preElement.properties["__withMeta__"] =
142 | node.children.at(0).tagName === "div";
143 | preElement.properties["__rawString__"] = node.__rawString__;
144 |
145 | if (node.__src__) {
146 | preElement.properties["__src__"] = node.__src__;
147 | }
148 | }
149 | });
150 | },
151 | rehypeNpmCommand,
152 | [
153 | rehypeAutolinkHeadings,
154 | {
155 | properties: {
156 | className: ["subheading-anchor"],
157 | ariaLabel: "Link to section",
158 | },
159 | },
160 | ],
161 | ],
162 | },
163 | });
164 |
--------------------------------------------------------------------------------
/src/components/component-preview.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Index } from "@/__registry__";
4 | import * as React from "react";
5 |
6 | import { CopyButton } from "@/components/copy-button";
7 | import { Icons } from "@/components/icons";
8 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9 | import { cn } from "@/lib/utils";
10 |
11 | interface ComponentPreviewProps extends React.HTMLAttributes {
12 | name: string;
13 | extractClassname?: boolean;
14 | extractedClassNames?: string;
15 | align?: "center" | "start" | "end";
16 | description?: string;
17 | hideCode?: boolean;
18 | type?: "block" | "component" | "example";
19 | }
20 |
21 | export function ComponentPreview({
22 | name,
23 | type,
24 | children,
25 | className,
26 | extractClassname,
27 | extractedClassNames,
28 | align = "center",
29 | description,
30 | hideCode = false,
31 | ...props
32 | }: ComponentPreviewProps) {
33 | const Codes = React.Children.toArray(children) as React.ReactElement[];
34 | const Code = Codes[0];
35 |
36 | const Preview = React.useMemo(() => {
37 | const Component = Index[name]?.component;
38 |
39 | if (!Component) {
40 | return (
41 |
42 | Component{" "}
43 |
44 | {name}
45 | {" "}
46 | not found in registry.
47 |
48 | );
49 | }
50 |
51 | return ;
52 | }, [name]);
53 |
54 | const codeString = React.useMemo(() => {
55 | if (
56 | typeof Code?.props["data-rehype-pretty-code-fragment"] !== "undefined"
57 | ) {
58 | const [Button] = React.Children.toArray(
59 | Code.props.children,
60 | ) as React.ReactElement[];
61 | return Button?.props?.value || Button?.props?.__rawString__ || null;
62 | }
63 | }, [Code]);
64 |
65 | return (
66 |
70 |
71 |
72 | {!hideCode && (
73 |
74 |
78 | Preview
79 |
80 |
84 | Code
85 |
86 |
87 | )}
88 |
89 |
90 |
91 |
96 |
97 |
107 |
110 |
111 | Loading...
112 |
113 | }
114 | >
115 | {Preview}
116 |
117 |
118 |
119 |
120 |
121 |
122 | {Code}
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 10% 3.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 |
33 | --text-default: #000000;
34 | --text-gray: #6b7280;
35 | --text-brown: #92400e;
36 | --text-orange: #ea580c;
37 | --text-yellow: #ca8a04;
38 | --text-green: #16a34a;
39 | --text-blue: #2563eb;
40 | --text-purple: #9333ea;
41 | --text-pink: #db2777;
42 | --text-red: #dc2626;
43 |
44 | --highlight-default: #ffffff;
45 | --highlight-gray: #f3f4f6;
46 | --highlight-brown: #fef3c7;
47 | --highlight-orange: #ffedd5;
48 | --highlight-yellow: #fef9c3;
49 | --highlight-green: #dcfce7;
50 | --highlight-blue: #dbeafe;
51 | --highlight-purple: #f3e8ff;
52 | --highlight-pink: #fce7f3;
53 | --highlight-red: #fee2e2;
54 | }
55 |
56 | .dark {
57 | --background: 240 10% 3.9%;
58 | --foreground: 0 0% 98%;
59 | --card: 240 10% 3.9%;
60 | --card-foreground: 0 0% 98%;
61 | --popover: 240 10% 3.9%;
62 | --popover-foreground: 0 0% 98%;
63 | --primary: 0 0% 98%;
64 | --primary-foreground: 240 5.9% 10%;
65 | --secondary: 240 3.7% 15.9%;
66 | --secondary-foreground: 0 0% 98%;
67 | --muted: 240 3.7% 15.9%;
68 | --muted-foreground: 240 5% 64.9%;
69 | --accent: 240 3.7% 15.9%;
70 | --accent-foreground: 0 0% 98%;
71 | --destructive: 0 62.8% 30.6%;
72 | --destructive-foreground: 0 0% 98%;
73 | --border: 240 3.7% 15.9%;
74 | --input: 240 3.7% 15.9%;
75 | --ring: 240 4.9% 83.9%;
76 | --chart-1: 220 70% 50%;
77 | --chart-2: 160 60% 45%;
78 | --chart-3: 30 80% 55%;
79 | --chart-4: 280 65% 60%;
80 | --chart-5: 340 75% 55%;
81 |
82 | --text-default: #ffffff;
83 | --text-gray: #9ca3af;
84 | --text-brown: #d97706;
85 | --text-orange: #f97316;
86 | --text-yellow: #eab308;
87 | --text-green: #22c55e;
88 | --text-blue: #3b82f6;
89 | --text-purple: #a855f7;
90 | --text-pink: #ec4899;
91 | --text-red: #ef4444;
92 |
93 | --highlight-default: #1f2937;
94 | --highlight-gray: #374151;
95 | --highlight-brown: #78350f;
96 | --highlight-orange: #9a3412;
97 | --highlight-yellow: #854d0e;
98 | --highlight-green: #166534;
99 | --highlight-blue: #1e40af;
100 | --highlight-purple: #6b21a8;
101 | --highlight-pink: #9d174d;
102 | --highlight-red: #991b1b;
103 | }
104 | }
105 |
106 | @layer base {
107 | * {
108 | @apply border-border;
109 | }
110 | body {
111 | @apply bg-background text-foreground;
112 | }
113 | }
114 |
115 | @layer utilities {
116 | .step {
117 | counter-increment: step;
118 | }
119 |
120 | .step:before {
121 | @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background;
122 | @apply ml-[-50px] mt-[-4px];
123 | content: counter(step);
124 | }
125 |
126 | .chunk-container {
127 | @apply shadow-none;
128 | }
129 |
130 | .chunk-container::after {
131 | content: "";
132 | @apply absolute -inset-4 shadow-xl rounded-xl border;
133 | }
134 | }
135 |
136 | .ProseMirror {
137 | @apply px-4 pt-2;
138 | outline: none !important;
139 | }
140 |
141 | h1.tiptap-heading {
142 | @apply mb-6 mt-8 text-4xl font-bold;
143 | }
144 |
145 | h2.tiptap-heading {
146 | @apply mb-4 mt-6 text-3xl font-bold;
147 | }
148 |
149 | h3.tiptap-heading {
150 | @apply mb-3 mt-4 text-xl font-bold;
151 | }
152 |
153 | h1.tiptap-heading:first-child,
154 | h2.tiptap-heading:first-child,
155 | h3.tiptap-heading:first-child {
156 | margin-top: 0;
157 | }
158 |
159 | h1.tiptap-heading + h2.tiptap-heading,
160 | h1.tiptap-heading + h3.tiptap-heading,
161 | h2.tiptap-heading + h1.tiptap-heading,
162 | h2.tiptap-heading + h3.tiptap-heading,
163 | h3.tiptap-heading + h1.tiptap-heading,
164 | h3.tiptap-heading + h2.tiptap-heading {
165 | margin-top: 0;
166 | }
167 |
168 | .tiptap p.is-editor-empty:first-child::before {
169 | @apply pointer-events-none float-left h-0 text-accent-foreground;
170 | content: attr(data-placeholder);
171 | }
172 |
173 | .tiptap ul,
174 | .tiptap ol {
175 | padding: 0 1rem;
176 | }
177 |
178 | .tiptap blockquote {
179 | border-left: 3px solid gray;
180 | margin: 1.5rem 0;
181 | padding-left: 1rem;
182 | }
183 |
--------------------------------------------------------------------------------
/src/components/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { NpmCommands } from "@/types/unist";
4 | import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu";
5 | import { CheckIcon, ClipboardIcon } from "lucide-react";
6 | import * as React from "react";
7 |
8 | import { Button, type ButtonProps } from "@/components/ui/button";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuTrigger,
14 | } from "@/components/ui/dropdown-menu";
15 | import { cn } from "@/lib/utils";
16 |
17 | interface CopyButtonProps extends ButtonProps {
18 | value: string;
19 | src?: string;
20 | }
21 |
22 | export async function copyToClipboardWithMeta(value: string) {
23 | navigator.clipboard.writeText(value);
24 | }
25 |
26 | export function CopyButton({
27 | value,
28 | className,
29 | src,
30 | variant = "ghost",
31 | ...props
32 | }: CopyButtonProps) {
33 | const [hasCopied, setHasCopied] = React.useState(false);
34 |
35 | React.useEffect(() => {
36 | setTimeout(() => {
37 | setHasCopied(false);
38 | }, 2000);
39 | }, []);
40 |
41 | return (
42 |
58 | );
59 | }
60 |
61 | interface CopyWithClassNamesProps extends DropdownMenuTriggerProps {
62 | value: string;
63 | classNames: string;
64 | className?: string;
65 | }
66 |
67 | export function CopyWithClassNames({
68 | value,
69 | classNames,
70 | className,
71 | ...props
72 | }: CopyWithClassNamesProps) {
73 | const [hasCopied, setHasCopied] = React.useState(false);
74 |
75 | // biome-ignore lint/correctness/useExhaustiveDependencies:
76 | React.useEffect(() => {
77 | setTimeout(() => {
78 | setHasCopied(false);
79 | }, 2000);
80 | }, [hasCopied]);
81 |
82 | const copyToClipboard = React.useCallback((value: string) => {
83 | copyToClipboardWithMeta(value);
84 | setHasCopied(true);
85 | }, []);
86 |
87 | return (
88 |
89 |
90 |
106 |
107 |
108 | copyToClipboard(value)}>
109 | Component
110 |
111 | copyToClipboard(classNames)}>
112 | Classname
113 |
114 |
115 |
116 | );
117 | }
118 |
119 | interface CopyNpmCommandButtonProps extends DropdownMenuTriggerProps {
120 | commands: Required;
121 | }
122 |
123 | export function CopyNpmCommandButton({
124 | commands,
125 | className,
126 | ...props
127 | }: CopyNpmCommandButtonProps) {
128 | const [hasCopied, setHasCopied] = React.useState(false);
129 |
130 | // biome-ignore lint/correctness/useExhaustiveDependencies:
131 | React.useEffect(() => {
132 | setTimeout(() => {
133 | setHasCopied(false);
134 | }, 2000);
135 | }, [hasCopied]);
136 |
137 | const copyCommand = React.useCallback((value: string) => {
138 | copyToClipboardWithMeta(value);
139 | setHasCopied(true);
140 | }, []);
141 |
142 | return (
143 |
144 |
145 |
161 |
162 |
163 | copyCommand(commands.__npmCommand__)}>
164 | npm
165 |
166 | copyCommand(commands.__yarnCommand__)}>
167 | yarn
168 |
169 | copyCommand(commands.__pnpmCommand__)}>
170 | pnpm
171 |
172 | copyCommand(commands.__bunCommand__)}>
173 | bun
174 |
175 |
176 |
177 | );
178 | }
179 |
--------------------------------------------------------------------------------
/src/content/docs/extensions/color-and-highlight.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Color and Highlight
3 | description: Color and highlight extensions for Tiptap
4 | links:
5 | ---
6 |
7 |
8 |
9 |
10 | ## Installation
11 |
12 |
13 |
14 |
15 |
16 | CLI
17 | Manual
18 |
19 |
20 |
21 |
22 |
23 | Run the following command to install associated toolbar component.
24 | ```bash
25 | npx shadcn add https://tiptap.niazmorshed.dev/r/color-and-highlight.json
26 | ```
27 |
28 | Add the following colors to your CSS file
29 |
30 | The command above should install the colors for you. If not, copy and paste the following in your CSS file.
31 |
32 | ```css title="app/globals.css"
33 | @layer base {
34 | :root {
35 | --text-default: #000000;
36 | --text-gray: #6b7280;
37 | --text-brown: #92400e;
38 | --text-orange: #ea580c;
39 | --text-yellow: #ca8a04;
40 | --text-green: #16a34a;
41 | --text-blue: #2563eb;
42 | --text-purple: #9333ea;
43 | --text-pink: #db2777;
44 | --text-red: #dc2626;
45 |
46 | --highlight-default: #ffffff;
47 | --highlight-gray: #f3f4f6;
48 | --highlight-brown: #fef3c7;
49 | --highlight-orange: #ffedd5;
50 | --highlight-yellow: #fef9c3;
51 | --highlight-green: #dcfce7;
52 | --highlight-blue: #dbeafe;
53 | --highlight-purple: #f3e8ff;
54 | --highlight-pink: #fce7f3;
55 | --highlight-red: #fee2e2;
56 | }
57 |
58 | .dark {
59 | --text-default: #ffffff;
60 | --text-gray: #9ca3af;
61 | --text-brown: #d97706;
62 | --text-orange: #f97316;
63 | --text-yellow: #eab308;
64 | --text-green: #22c55e;
65 | --text-blue: #3b82f6;
66 | --text-purple: #a855f7;
67 | --text-pink: #ec4899;
68 | --text-red: #ef4444;
69 |
70 | --highlight-default: #1f2937;
71 | --highlight-gray: #374151;
72 | --highlight-brown: #78350f;
73 | --highlight-orange: #9a3412;
74 | --highlight-yellow: #854d0e;
75 | --highlight-green: #166534;
76 | --highlight-blue: #1e40af;
77 | --highlight-purple: #6b21a8;
78 | --highlight-pink: #9d174d;
79 | --highlight-red: #991b1b;
80 | }
81 | }
82 | ```
83 |
84 | Enable `multicolor` for Highlight extension when using it.
85 | ```jsx
86 | extensions: [
87 | Highlight.configure({
88 | multicolor: true,
89 | }),
90 | ]
91 | ```
92 |
93 |
94 |
95 |
96 |
97 | Install the following dependencies.
98 |
99 | ```bash
100 | npm install @tiptap/extension-text-style @tiptap/extension-highlight @tiptap/extension-color
101 | ```
102 |
103 | Copy & paste the css variables.
104 |
105 | ```css title="app/globals.css"
106 | @layer base {
107 | :root {
108 | --text-default: #000000;
109 | --text-gray: #6b7280;
110 | --text-brown: #92400e;
111 | --text-orange: #ea580c;
112 | --text-yellow: #ca8a04;
113 | --text-green: #16a34a;
114 | --text-blue: #2563eb;
115 | --text-purple: #9333ea;
116 | --text-pink: #db2777;
117 | --text-red: #dc2626;
118 |
119 | --highlight-default: #ffffff;
120 | --highlight-gray: #f3f4f6;
121 | --highlight-brown: #fef3c7;
122 | --highlight-orange: #ffedd5;
123 | --highlight-yellow: #fef9c3;
124 | --highlight-green: #dcfce7;
125 | --highlight-blue: #dbeafe;
126 | --highlight-purple: #f3e8ff;
127 | --highlight-pink: #fce7f3;
128 | --highlight-red: #fee2e2;
129 | }
130 |
131 | .dark {
132 | --text-default: #ffffff;
133 | --text-gray: #9ca3af;
134 | --text-brown: #d97706;
135 | --text-orange: #f97316;
136 | --text-yellow: #eab308;
137 | --text-green: #22c55e;
138 | --text-blue: #3b82f6;
139 | --text-purple: #a855f7;
140 | --text-pink: #ec4899;
141 | --text-red: #ef4444;
142 |
143 | --highlight-default: #1f2937;
144 | --highlight-gray: #374151;
145 | --highlight-brown: #78350f;
146 | --highlight-orange: #9a3412;
147 | --highlight-yellow: #854d0e;
148 | --highlight-green: #166534;
149 | --highlight-blue: #1e40af;
150 | --highlight-purple: #6b21a8;
151 | --highlight-pink: #9d174d;
152 | --highlight-red: #991b1b;
153 | }
154 | }
155 | ```
156 |
157 | Add the [Color & Highlight Toolbar](/docs/toolbars/color-and-highlight) to your editor.
158 |
159 | Enable `multicolor` for Highlight extension when using it.
160 | ```jsx
161 | extensions: [
162 | Highlight.configure({
163 | multicolor: true,
164 | }),
165 | ]
166 | ```
167 |
168 |
169 |
170 |
171 | ## Usage
172 |
173 | ```jsx
174 | const extensions = [
175 | StarterKit.configure({
176 | // ...
177 | }),
178 | TextStyle,
179 | Color,
180 | Highlight.configure({
181 | multicolor: true,
182 | }),
183 | ];
184 |
185 | const editor = useEditor({
186 | extensions,
187 | });
188 | ```
189 |
190 |
191 | ## API Reference
192 |
193 | - [TextStyle](https://tiptap.dev/docs/editor/extensions/marks/text-style#page-title)
194 | - [Highlight](https://tiptap.dev/docs/editor/extensions/marks/highlight#page-title)
195 | - [Color](https://tiptap.dev/docs/editor/extensions/functionality/color#page-title)
196 |
--------------------------------------------------------------------------------
/src/registry/toolbars/color-and-highlight.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Popover,
6 | PopoverContent,
7 | PopoverTrigger,
8 | } from "@/components/ui/popover";
9 | import { ScrollArea } from "@/components/ui/scroll-area";
10 | import { Separator } from "@/components/ui/separator";
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipTrigger,
15 | } from "@/components/ui/tooltip";
16 | import { cn } from "@/lib/utils";
17 | import { useToolbar } from "@/registry/toolbars/toolbar-provider";
18 | import type { Extension } from "@tiptap/core";
19 | import type { ColorOptions } from "@tiptap/extension-color";
20 | import type { HighlightOptions } from "@tiptap/extension-highlight";
21 | import { Check, ChevronDown } from "lucide-react";
22 |
23 | type TextStylingExtensions =
24 | | Extension
25 | | Extension;
26 |
27 | const TEXT_COLORS = [
28 | { name: "Default", color: "var(--text-default)" },
29 | { name: "Gray", color: "var(--text-gray)" },
30 | { name: "Brown", color: "var(--text-brown)" },
31 | { name: "Orange", color: "var(--text-orange)" },
32 | { name: "Yellow", color: "var(--text-yellow)" },
33 | { name: "Green", color: "var(--text-green)" },
34 | { name: "Blue", color: "var(--text-blue)" },
35 | { name: "Purple", color: "var(--text-purple)" },
36 | { name: "Pink", color: "var(--text-pink)" },
37 | { name: "Red", color: "var(--text-red)" },
38 | ];
39 |
40 | const HIGHLIGHT_COLORS = [
41 | { name: "Default", color: "var(--highlight-default)" },
42 | { name: "Gray", color: "var(--highlight-gray)" },
43 | { name: "Brown", color: "var(--highlight-brown)" },
44 | { name: "Orange", color: "var(--highlight-orange)" },
45 | { name: "Yellow", color: "var(--highlight-yellow)" },
46 | { name: "Green", color: "var(--highlight-green)" },
47 | { name: "Blue", color: "var(--highlight-blue)" },
48 | { name: "Purple", color: "var(--highlight-purple)" },
49 | { name: "Pink", color: "var(--highlight-pink)" },
50 | { name: "Red", color: "var(--highlight-red)" },
51 | ];
52 |
53 | interface ColorHighlightButtonProps {
54 | name: string;
55 | color: string;
56 | isActive: boolean;
57 | onClick: () => void;
58 | isHighlight?: boolean;
59 | }
60 |
61 | const ColorHighlightButton = ({
62 | name,
63 | color,
64 | isActive,
65 | onClick,
66 | isHighlight,
67 | }: ColorHighlightButtonProps) => (
68 |
84 | );
85 |
86 | export const ColorHighlightToolbar = () => {
87 | const { editor } = useToolbar();
88 |
89 | const currentColor = editor?.getAttributes("textStyle").color;
90 | const currentHighlight = editor?.getAttributes("highlight").color;
91 |
92 | const handleSetColor = (color: string) => {
93 | editor
94 | ?.chain()
95 | .focus()
96 | .setColor(color === currentColor ? "" : color)
97 | .run();
98 | };
99 |
100 | const handleSetHighlight = (color: string) => {
101 | editor
102 | ?.chain()
103 | .focus()
104 | .setHighlight(color === currentHighlight ? { color: "" } : { color })
105 | .run();
106 | };
107 |
108 | const isDisabled =
109 | !editor?.can().chain().setHighlight().run() ||
110 | !editor?.can().chain().setColor("").run();
111 |
112 | return (
113 |
114 |
115 |
116 |
117 |
118 |
129 |
130 |
131 | Text Color & Highlight
132 |
133 |
134 |
135 |
136 | Color
137 | {TEXT_COLORS.map(({ name, color }) => (
138 | handleSetColor(color)}
144 | />
145 | ))}
146 |
147 |
148 |
149 |
150 | Background
151 |
152 | {HIGHLIGHT_COLORS.map(({ name, color }) => (
153 | handleSetHighlight(color)}
159 | isHighlight
160 | />
161 | ))}
162 |
163 |
164 |
165 |
166 | );
167 | };
168 |
--------------------------------------------------------------------------------