diff --git a/bun.lockb b/bun.lockb
index 6c3130b0..4bba695c 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/cspell.config.yaml b/cspell.config.yaml
index 2a4927f6..eb4dd65e 100644
--- a/cspell.config.yaml
+++ b/cspell.config.yaml
@@ -13,20 +13,26 @@ ignorePaths:
- tsconfig.tsbuildinfo
- branch_out
words:
+ - ANTLR
- Arcjet
+ - atrule
- callees
- classname
- clsx
+ - combobox
- commitlint
- compat
+ - Draggables
+ - esbenp
+ - excalidraw
- frontmatter
+ - healthcheck
- hookform
- ianvs
- ixahmedxi
- ixahmedxii
- jiti
- lockb
- - esbenp
- lucide
- mitigations
- neondatabase
@@ -34,8 +40,11 @@ words:
- packagejson
- paralleldrive
- Posthog
+ - prismjs
+ - protobuf
- ratelimit
- Registrator
+ - Scroller
- serviceworker
- Shadcn
- Signin
@@ -45,12 +54,12 @@ words:
- superjson
- tailwindcss
- tanstack
+ - timeago
- Todos
- trpc
- tsbuildinfo
- tseslint
- typecheck
+ - udecode
- Uploadthing
- Upstash
- - healthcheck
- - timeago
diff --git a/package.json b/package.json
index 93a3b678..a945bf88 100644
--- a/package.json
+++ b/package.json
@@ -54,13 +54,14 @@
}
},
"dependencies": {
- "@clerk/nextjs": "^5.4.1",
- "@clerk/themes": "^2.1.27",
+ "@clerk/nextjs": "^5.5.2",
+ "@clerk/themes": "^2.1.29",
"@hookform/resolvers": "^3.9.0",
"@neondatabase/serverless": "^0.9.5",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-checkbox": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
@@ -70,12 +71,60 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.2",
"@react-email/components": "^0.0.25",
"@t3-oss/env-nextjs": "^0.11.1",
- "@tanstack/react-query": "^5.54.1",
+ "@tanstack/react-query": "^5.56.2",
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
+ "@udecode/cn": "^38.0.1",
+ "@udecode/plate-alignment": "^38.0.1",
+ "@udecode/plate-autoformat": "^38.0.1",
+ "@udecode/plate-basic-marks": "^38.0.1",
+ "@udecode/plate-block-quote": "^38.0.1",
+ "@udecode/plate-break": "^38.0.1",
+ "@udecode/plate-caption": "^38.0.1",
+ "@udecode/plate-code-block": "^38.0.1",
+ "@udecode/plate-combobox": "^38.0.1",
+ "@udecode/plate-comments": "^38.0.1",
+ "@udecode/plate-common": "^38.0.4",
+ "@udecode/plate-csv": "^38.0.1",
+ "@udecode/plate-cursor": "^38.0.0",
+ "@udecode/plate-date": "^38.0.1",
+ "@udecode/plate-dnd": "^38.0.0",
+ "@udecode/plate-docx": "^38.0.1",
+ "@udecode/plate-emoji": "^38.0.1",
+ "@udecode/plate-excalidraw": "^38.0.1",
+ "@udecode/plate-find-replace": "^38.0.0",
+ "@udecode/plate-floating": "^38.0.1",
+ "@udecode/plate-font": "^38.0.1",
+ "@udecode/plate-heading": "^38.0.1",
+ "@udecode/plate-highlight": "^38.0.1",
+ "@udecode/plate-horizontal-rule": "^38.0.1",
+ "@udecode/plate-html": "^38.0.1",
+ "@udecode/plate-indent": "^38.0.1",
+ "@udecode/plate-indent-list": "^38.0.1",
+ "@udecode/plate-juice": "^38.0.1",
+ "@udecode/plate-kbd": "^38.0.1",
+ "@udecode/plate-layout": "^38.0.1",
+ "@udecode/plate-line-height": "^38.0.1",
+ "@udecode/plate-link": "^38.0.1",
+ "@udecode/plate-list": "^38.0.1",
+ "@udecode/plate-markdown": "^38.0.1",
+ "@udecode/plate-media": "^38.0.1",
+ "@udecode/plate-mention": "^38.0.1",
+ "@udecode/plate-node-id": "^38.0.1",
+ "@udecode/plate-normalizers": "^38.0.1",
+ "@udecode/plate-paragraph": "^36.0.0",
+ "@udecode/plate-reset-node": "^38.0.1",
+ "@udecode/plate-resizable": "^38.0.0",
+ "@udecode/plate-select": "^38.0.1",
+ "@udecode/plate-selection": "^38.0.0",
+ "@udecode/plate-tabbable": "^38.0.1",
+ "@udecode/plate-table": "^38.0.1",
+ "@udecode/plate-toggle": "^38.0.1",
+ "@udecode/plate-trailing-block": "^38.0.1",
"@upstash/redis": "^1.34.0",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
@@ -83,15 +132,18 @@
"clsx": "^2.1.1",
"drizzle-orm": "^0.33.0",
"drizzle-zod": "^0.5.1",
- "framer-motion": "^11.5.2",
+ "framer-motion": "^11.5.4",
"geist": "^1.3.1",
"jiti": "^1.21.6",
- "lucide-react": "^0.438.0",
- "next": "14.2.8",
+ "lucide-react": "^0.441.0",
+ "next": "14.2.11",
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
+ "prismjs": "^1.29.0",
"react": "^18.3.1",
"react-animate-height": "^3.2.3",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-email": "^3.0.1",
"react-hook-form": "^7.53.0",
@@ -106,40 +158,41 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.8",
- "@commitlint/cli": "^19.4.1",
- "@commitlint/config-conventional": "^19.4.1",
- "@commitlint/cz-commitlint": "^19.4.0",
+ "@commitlint/cli": "^19.5.0",
+ "@commitlint/config-conventional": "^19.5.0",
+ "@commitlint/cz-commitlint": "^19.5.0",
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "^9.9.1",
- "@happy-dom/global-registrator": "^15.7.3",
+ "@eslint/js": "^9.10.0",
+ "@happy-dom/global-registrator": "^15.7.4",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
- "@next/eslint-plugin-next": "^14.2.8",
+ "@next/eslint-plugin-next": "^14.2.11",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.15",
"@total-typescript/ts-reset": "^0.6.1",
- "@types/bun": "^1.1.8",
+ "@types/bun": "^1.1.9",
"@types/eslint": "^9.6.1",
"@types/eslint-config-prettier": "^6.11.3",
- "@types/node": "^22.5.4",
+ "@types/node": "^22.5.5",
+ "@types/prismjs": "^1.26.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"commitizen": "^4.3.0",
"cspell": "^8.14.2",
"drizzle-kit": "^0.24.2",
- "eslint": "^9.9.1",
+ "eslint": "^9.10.0",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-jsdoc": "^50.2.2",
+ "eslint-plugin-jsdoc": "^50.2.3",
"eslint-plugin-jsx-a11y": "^6.10.0",
- "eslint-plugin-react": "^7.35.2",
+ "eslint-plugin-react": "^7.36.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-regexp": "^2.6.0",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-tailwindcss": "^3.17.4",
"globals": "^15.9.0",
- "husky": "^9.1.5",
+ "husky": "^9.1.6",
"lint-staged": "^15.2.10",
"markdownlint": "^0.35.0",
"markdownlint-cli": "^0.41.0",
@@ -147,10 +200,10 @@
"prettier": "^3.3.3",
"prettier-plugin-curly": "^0.2.2",
"prettier-plugin-packagejson": "^2.5.2",
- "tailwindcss": "^3.4.10",
+ "tailwindcss": "^3.4.11",
"tailwindcss-animate": "^1.0.7",
- "typescript": "^5.5.4",
- "typescript-eslint": "^8.4.0"
+ "typescript": "^5.6.2",
+ "typescript-eslint": "^8.5.0"
},
"trustedDependencies": [
"@clerk/shared",
diff --git a/src/app/notebook/[id]/page.tsx b/src/app/notebook/[id]/page.tsx
new file mode 100644
index 00000000..16c21f80
--- /dev/null
+++ b/src/app/notebook/[id]/page.tsx
@@ -0,0 +1,14 @@
+import dynamic from 'next/dynamic';
+
+const PlateEditor = dynamic(
+ () => import('@/editor').then((res) => res.PlateEditor),
+ { ssr: false },
+);
+
+export default function NotebookPage() {
+ return (
+
+ );
+}
diff --git a/src/editor/index.tsx b/src/editor/index.tsx
new file mode 100644
index 00000000..8c922ab9
--- /dev/null
+++ b/src/editor/index.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import {
+ createPlateEditor,
+ ParagraphPlugin,
+ Plate,
+} from '@udecode/plate-common/react';
+import { Editor } from './ui/editor';
+import { createPlateUI } from './ui/components';
+import { plugins } from './plugins';
+import { HEADING_KEYS } from '@udecode/plate-heading';
+import type { Value } from '@udecode/plate-common';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
+
+const initialValue = [
+ {
+ type: HEADING_KEYS.h1,
+ children: [{ text: '' }],
+ },
+ {
+ type: ParagraphPlugin.key,
+ children: [{ text: '' }],
+ },
+];
+
+export const PlateEditor = () => {
+ const localValue =
+ typeof window !== 'undefined' && localStorage.getItem('editorContent');
+
+ const editor = createPlateEditor({
+ value: localValue ? (JSON.parse(localValue) as Value) : initialValue,
+ plugins,
+ override: {
+ components: createPlateUI(),
+ },
+ });
+
+ return (
+
+ {
+ console.log(value);
+ localStorage.setItem('editorContent', JSON.stringify(value));
+ }}
+ >
+
+
+
+ );
+};
diff --git a/src/editor/plugins/autoformat-rules.ts b/src/editor/plugins/autoformat-rules.ts
new file mode 100644
index 00000000..4d3e59ee
--- /dev/null
+++ b/src/editor/plugins/autoformat-rules.ts
@@ -0,0 +1,341 @@
+import {
+ autoformatArrow,
+ autoformatLegal,
+ autoformatLegalHtml,
+ autoformatMath,
+ autoformatPunctuation,
+ autoformatSmartQuotes,
+} from '@udecode/plate-autoformat';
+import {
+ BoldPlugin,
+ CodePlugin,
+ ItalicPlugin,
+ StrikethroughPlugin,
+ SubscriptPlugin,
+ SuperscriptPlugin,
+ UnderlinePlugin,
+} from '@udecode/plate-basic-marks/react';
+import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
+import { insertEmptyCodeBlock } from '@udecode/plate-code-block';
+import {
+ CodeBlockPlugin,
+ CodeLinePlugin,
+} from '@udecode/plate-code-block/react';
+import {
+ getParentNode,
+ insertNodes,
+ isBlock,
+ isElement,
+ isType,
+ setNodes,
+} from '@udecode/plate-common';
+import { ParagraphPlugin } from '@udecode/plate-common/react';
+import { HEADING_KEYS } from '@udecode/plate-heading';
+import { HighlightPlugin } from '@udecode/plate-highlight/react';
+import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
+import {
+ INDENT_LIST_KEYS,
+ ListStyleType,
+ toggleIndentList,
+} from '@udecode/plate-indent-list';
+import { toggleList, unwrapList } from '@udecode/plate-list';
+import {
+ BulletedListPlugin,
+ ListItemPlugin,
+ NumberedListPlugin,
+ TodoListPlugin,
+} from '@udecode/plate-list/react';
+import { openNextToggles, TogglePlugin } from '@udecode/plate-toggle/react';
+
+import type {
+ AutoformatBlockRule,
+ AutoformatRule,
+} from '@udecode/plate-autoformat';
+import type { SlateEditor } from '@udecode/plate-common';
+import type { TTodoListItemElement } from '@udecode/plate-list';
+
+export const preFormat: AutoformatBlockRule['preFormat'] = (editor) => {
+ unwrapList(editor);
+};
+
+export const format = (editor: SlateEditor, customFormatting: () => void) => {
+ if (editor.selection) {
+ const parentEntry = getParentNode(editor, editor.selection);
+
+ if (!parentEntry) {
+ return;
+ }
+
+ const [node] = parentEntry;
+
+ if (
+ isElement(node) &&
+ !isType(editor, node, CodeBlockPlugin.key) &&
+ !isType(editor, node, CodeLinePlugin.key)
+ ) {
+ customFormatting();
+ }
+ }
+};
+
+export const formatList = (editor: SlateEditor, elementType: string) => {
+ format(editor, () =>
+ toggleList(editor, {
+ type: elementType,
+ }),
+ );
+};
+
+export const autoformatMarks: AutoformatRule[] = [
+ {
+ match: '***',
+ mode: 'mark',
+ type: [BoldPlugin.key, ItalicPlugin.key],
+ },
+ {
+ match: '__**',
+ mode: 'mark',
+ type: [UnderlinePlugin.key, BoldPlugin.key],
+ },
+ {
+ match: '__*',
+ mode: 'mark',
+ type: [UnderlinePlugin.key, ItalicPlugin.key],
+ },
+ {
+ match: '___***',
+ mode: 'mark',
+ type: [UnderlinePlugin.key, BoldPlugin.key, ItalicPlugin.key],
+ },
+ {
+ match: '**',
+ mode: 'mark',
+ type: BoldPlugin.key,
+ },
+ {
+ match: '__',
+ mode: 'mark',
+ type: UnderlinePlugin.key,
+ },
+ {
+ match: '*',
+ mode: 'mark',
+ type: ItalicPlugin.key,
+ },
+ {
+ match: '_',
+ mode: 'mark',
+ type: ItalicPlugin.key,
+ },
+ {
+ match: '~~',
+ mode: 'mark',
+ type: StrikethroughPlugin.key,
+ },
+ {
+ match: '^',
+ mode: 'mark',
+ type: SuperscriptPlugin.key,
+ },
+ {
+ match: '~',
+ mode: 'mark',
+ type: SubscriptPlugin.key,
+ },
+ {
+ match: '==',
+ mode: 'mark',
+ type: HighlightPlugin.key as string,
+ },
+ {
+ match: '≡',
+ mode: 'mark',
+ type: HighlightPlugin.key as string,
+ },
+ {
+ match: '`',
+ mode: 'mark',
+ type: CodePlugin.key,
+ },
+];
+
+export const autoformatBlocks: AutoformatRule[] = [
+ {
+ match: '# ',
+ mode: 'block',
+ preFormat,
+ type: HEADING_KEYS.h1,
+ },
+ {
+ match: '## ',
+ mode: 'block',
+ preFormat,
+ type: HEADING_KEYS.h2,
+ },
+ {
+ match: '### ',
+ mode: 'block',
+ preFormat,
+ type: HEADING_KEYS.h3,
+ },
+ {
+ match: '#### ',
+ mode: 'block',
+ preFormat,
+ type: HEADING_KEYS.h4,
+ },
+ {
+ match: '##### ',
+ mode: 'block',
+ preFormat,
+ type: HEADING_KEYS.h5,
+ },
+ {
+ match: '###### ',
+ mode: 'block',
+ preFormat,
+ type: HEADING_KEYS.h6,
+ },
+ {
+ match: '> ',
+ mode: 'block',
+ preFormat,
+ type: BlockquotePlugin.key,
+ },
+ {
+ format: (editor) => {
+ insertEmptyCodeBlock(editor, {
+ defaultType: ParagraphPlugin.key,
+ insertNodesOptions: { select: true },
+ });
+ },
+ match: '```',
+ mode: 'block',
+ preFormat,
+ triggerAtBlockStart: false,
+ type: CodeBlockPlugin.key,
+ },
+ {
+ match: '+ ',
+ mode: 'block',
+ preFormat: openNextToggles,
+ type: TogglePlugin.key,
+ },
+ {
+ format: (editor) => {
+ setNodes(editor, { type: HorizontalRulePlugin.key });
+ insertNodes(editor, {
+ children: [{ text: '' }],
+ type: ParagraphPlugin.key,
+ });
+ },
+ match: ['---', '—-', '___ '],
+ mode: 'block',
+ type: HorizontalRulePlugin.key,
+ },
+];
+
+export const autoformatLists: AutoformatRule[] = [
+ {
+ format: (editor) => {
+ formatList(editor, BulletedListPlugin.key);
+ },
+ match: ['* ', '- '],
+ mode: 'block',
+ preFormat,
+ type: ListItemPlugin.key,
+ },
+ {
+ format: (editor) => {
+ formatList(editor, NumberedListPlugin.key);
+ },
+ match: ['^\\d+\\.$ ', '^\\d+\\)$ '],
+ matchByRegex: true,
+ mode: 'block',
+ preFormat,
+ type: ListItemPlugin.key,
+ },
+ {
+ match: '[] ',
+ mode: 'block',
+ type: TodoListPlugin.key as string,
+ },
+ {
+ format: (editor) => {
+ setNodes(
+ editor,
+ { checked: true, type: TodoListPlugin.key },
+ {
+ match: (n) => isBlock(editor, n),
+ },
+ );
+ },
+ match: '[x] ',
+ mode: 'block',
+ type: TodoListPlugin.key as string,
+ },
+];
+
+export const autoformatIndentLists: AutoformatRule[] = [
+ {
+ format: (editor) => {
+ toggleIndentList(editor, {
+ listStyleType: ListStyleType.Disc,
+ });
+ },
+ match: ['* ', '- '],
+ mode: 'block',
+ type: 'list',
+ },
+ {
+ format: (editor) => {
+ toggleIndentList(editor, {
+ listStyleType: ListStyleType.Decimal,
+ });
+ },
+ match: ['^\\d+\\.$ ', '^\\d+\\)$ '],
+ matchByRegex: true,
+ mode: 'block',
+ type: 'list',
+ },
+ {
+ format: (editor) => {
+ toggleIndentList(editor, {
+ listStyleType: INDENT_LIST_KEYS.todo,
+ });
+ setNodes(editor, {
+ checked: false,
+ listStyleType: INDENT_LIST_KEYS.todo,
+ });
+ },
+ match: ['[] '],
+ mode: 'block',
+ type: 'list',
+ },
+ {
+ format: (editor) => {
+ toggleIndentList(editor, {
+ listStyleType: INDENT_LIST_KEYS.todo,
+ });
+ setNodes(editor, {
+ checked: true,
+ listStyleType: INDENT_LIST_KEYS.todo,
+ });
+ },
+ match: ['[x] '],
+ mode: 'block',
+ type: 'list',
+ },
+];
+
+export const autoformatRules: AutoformatRule[] = [
+ ...autoformatBlocks,
+ ...autoformatMarks,
+ ...autoformatSmartQuotes,
+ ...autoformatPunctuation,
+ ...autoformatLegal,
+ ...autoformatLegalHtml,
+ ...autoformatArrow,
+ ...autoformatMath,
+ ...autoformatIndentLists,
+];
diff --git a/src/editor/plugins/index.tsx b/src/editor/plugins/index.tsx
new file mode 100644
index 00000000..c447e767
--- /dev/null
+++ b/src/editor/plugins/index.tsx
@@ -0,0 +1,265 @@
+import {
+ BoldPlugin,
+ CodePlugin,
+ ItalicPlugin,
+ StrikethroughPlugin,
+ SubscriptPlugin,
+ SuperscriptPlugin,
+ UnderlinePlugin,
+} from '@udecode/plate-basic-marks/react';
+import { AlignPlugin } from '@udecode/plate-alignment/react';
+import { autoformatRules } from './autoformat-rules';
+import { SelectOnBackspacePlugin } from '@udecode/plate-select';
+import { JuicePlugin } from '@udecode/plate-juice';
+import { DocxPlugin } from '@udecode/plate-docx';
+import { MarkdownPlugin } from '@udecode/plate-markdown';
+import { TabbablePlugin } from '@udecode/plate-tabbable/react';
+import { ResetNodePlugin } from '@udecode/plate-reset-node/react';
+import { ImagePlugin } from '@udecode/plate-media/react';
+import { ExitBreakPlugin, SoftBreakPlugin } from '@udecode/plate-break/react';
+import { AutoformatPlugin } from '@udecode/plate-autoformat/react';
+import { DndPlugin } from '@udecode/plate-dnd';
+import {
+ isBlockAboveEmpty,
+ isSelectionAtBlockStart,
+ someNode,
+} from '@udecode/plate-common';
+import { ParagraphPlugin } from '@udecode/plate-common/react';
+import { NodeIdPlugin } from '@udecode/plate-node-id';
+import { BlockSelectionPlugin } from '@udecode/plate-selection/react';
+import { TrailingBlockPlugin } from '@udecode/plate-trailing-block';
+import { HEADING_KEYS, HEADING_LEVELS } from '@udecode/plate-heading';
+import { HeadingPlugin } from '@udecode/plate-heading/react';
+import { IndentPlugin } from '@udecode/plate-indent/react';
+import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
+import { NormalizeTypesPlugin } from '@udecode/plate-normalizers';
+import {
+ isCodeBlockEmpty,
+ isSelectionAtCodeBlockStart,
+ unwrapCodeBlock,
+} from '@udecode/plate-code-block';
+import {
+ CodeBlockPlugin,
+ CodeLinePlugin,
+ CodeSyntaxPlugin,
+} from '@udecode/plate-code-block/react';
+import { LineHeightPlugin } from '@udecode/plate-line-height/react';
+import { IndentListPlugin } from '@udecode/plate-indent-list/react';
+import {
+ TableCellHeaderPlugin,
+ TableCellPlugin,
+ TablePlugin,
+ TableRowPlugin,
+} from '@udecode/plate-table/react';
+import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
+import { TodoListPlugin } from '@udecode/plate-list/react';
+import Prism from 'prismjs';
+import { LinkPlugin } from '@udecode/plate-link/react';
+import { LinkFloatingToolbar } from '../ui/link-floating-toolbar';
+import { TodoLi, TodoMarker } from '../ui/elements/todo';
+import { HighlightPlugin } from '@udecode/plate-highlight/react';
+
+export const plugins = [
+ // Nodes
+ HeadingPlugin,
+ BlockquotePlugin,
+ CodeBlockPlugin.configure({
+ options: {
+ syntax: true,
+ prism: Prism,
+ },
+ }),
+ CodeLinePlugin,
+ CodeSyntaxPlugin,
+ HorizontalRulePlugin,
+ TablePlugin,
+ TableRowPlugin,
+ TableCellPlugin,
+ TableCellHeaderPlugin,
+ TodoListPlugin,
+ LinkPlugin.configure({
+ render: { afterEditable: () => },
+ }),
+
+ // Basic Marks
+ BoldPlugin,
+ ItalicPlugin,
+ UnderlinePlugin,
+ StrikethroughPlugin,
+ CodePlugin,
+ SubscriptPlugin,
+ SuperscriptPlugin,
+ HighlightPlugin,
+
+ // Block Style
+ AlignPlugin.configure({
+ inject: {
+ targetPlugins: [ParagraphPlugin.key, ...HEADING_LEVELS],
+ },
+ }),
+ IndentPlugin.configure({
+ inject: {
+ targetPlugins: [
+ ParagraphPlugin.key,
+ BlockquotePlugin.key,
+ CodeBlockPlugin.key,
+ ...HEADING_LEVELS,
+ ],
+ },
+ }),
+ IndentListPlugin.configure({
+ inject: {
+ targetPlugins: [
+ ParagraphPlugin.key,
+ BlockquotePlugin.key,
+ CodeBlockPlugin.key,
+ ...HEADING_LEVELS,
+ ],
+ },
+ options: {
+ listStyleTypes: {
+ todo: {
+ liComponent: TodoLi,
+ markerComponent: TodoMarker,
+ type: 'todo',
+ },
+ },
+ },
+ }),
+ LineHeightPlugin.configure({
+ inject: {
+ nodeProps: {
+ defaultNodeValue: 1.5,
+ validNodeValues: [1, 1.2, 1.5, 2, 3],
+ },
+ targetPlugins: [ParagraphPlugin.key, ...HEADING_LEVELS],
+ },
+ }),
+
+ // Functionality
+ AutoformatPlugin.configure({
+ options: {
+ rules: autoformatRules,
+ enableUndoOnDelete: true,
+ },
+ }),
+ BlockSelectionPlugin,
+ ExitBreakPlugin.configure({
+ options: {
+ rules: [
+ {
+ hotkey: 'mod+enter',
+ },
+ {
+ hotkey: 'mod+shift+enter',
+ before: true,
+ },
+ {
+ hotkey: 'enter',
+ query: {
+ start: true,
+ end: true,
+ allow: HEADING_LEVELS,
+ },
+ relative: true,
+ level: 1,
+ },
+ ],
+ },
+ }),
+ NodeIdPlugin,
+ DndPlugin.configure({ options: { enableScroller: true } }),
+ ResetNodePlugin.configure({
+ options: {
+ rules: [
+ {
+ types: [BlockquotePlugin.key, TodoListPlugin.key as string],
+ defaultType: ParagraphPlugin.key,
+ hotkey: 'Enter',
+ predicate: isBlockAboveEmpty,
+ },
+ {
+ types: [BlockquotePlugin.key, TodoListPlugin.key as string],
+ defaultType: ParagraphPlugin.key,
+ hotkey: 'Backspace',
+ predicate: isSelectionAtBlockStart,
+ },
+ {
+ types: [CodeBlockPlugin.key],
+ defaultType: ParagraphPlugin.key,
+ onReset: unwrapCodeBlock,
+ hotkey: 'Enter',
+ predicate: isCodeBlockEmpty,
+ },
+ {
+ types: [CodeBlockPlugin.key],
+ defaultType: ParagraphPlugin.key,
+ onReset: unwrapCodeBlock,
+ hotkey: 'Backspace',
+ predicate: isSelectionAtCodeBlockStart,
+ },
+ ],
+ },
+ }),
+ SelectOnBackspacePlugin.configure({
+ options: {
+ query: {
+ allow: [ImagePlugin.key, HorizontalRulePlugin.key],
+ },
+ },
+ }),
+ SoftBreakPlugin.configure({
+ options: {
+ rules: [
+ { hotkey: 'shift+enter' },
+ {
+ hotkey: 'enter',
+ query: {
+ allow: [
+ CodeBlockPlugin.key,
+ BlockquotePlugin.key,
+ TableCellPlugin.key,
+ TableCellHeaderPlugin.key,
+ ],
+ },
+ },
+ ],
+ },
+ }),
+ TabbablePlugin.configure(({ editor }) => ({
+ options: {
+ query: () => {
+ if (isSelectionAtBlockStart(editor)) {
+ return false;
+ }
+
+ return !someNode(editor, {
+ match: (n) => {
+ return !!(
+ n.type &&
+ ([
+ TablePlugin.key,
+ TodoListPlugin.key,
+ CodeBlockPlugin.key,
+ ].includes(n.type as string) ||
+ n['listStyleType'])
+ );
+ },
+ });
+ },
+ },
+ })),
+ TrailingBlockPlugin.configure({
+ options: { type: ParagraphPlugin.key },
+ }),
+ NormalizeTypesPlugin.configure({
+ options: {
+ rules: [{ path: [0], strictType: HEADING_KEYS.h1 }],
+ },
+ }),
+
+ // Deserialization
+ DocxPlugin,
+ MarkdownPlugin,
+ JuicePlugin,
+];
diff --git a/src/editor/ui/components.tsx b/src/editor/ui/components.tsx
new file mode 100644
index 00000000..0d7e3f80
--- /dev/null
+++ b/src/editor/ui/components.tsx
@@ -0,0 +1,102 @@
+import { withProps } from '@udecode/cn';
+import {
+ BoldPlugin,
+ CodePlugin,
+ ItalicPlugin,
+ StrikethroughPlugin,
+ SubscriptPlugin,
+ SuperscriptPlugin,
+ UnderlinePlugin,
+} from '@udecode/plate-basic-marks/react';
+import type { NodeComponent } from '@udecode/plate-common/react';
+import {
+ ParagraphPlugin,
+ PlateElement,
+ PlateLeaf,
+} from '@udecode/plate-common/react';
+import { HEADING_KEYS } from '@udecode/plate-heading';
+import { HeadingElement } from './elements/heading';
+import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
+import { BlockquoteElement } from './elements/blockquote';
+import { withPlaceholders } from './placeholder';
+import {
+ CodeBlockPlugin,
+ CodeLinePlugin,
+ CodeSyntaxPlugin,
+} from '@udecode/plate-code-block/react';
+import { CodeBlockElement } from './elements/code-block';
+import { CodeLineElement } from './elements/code-block/code-line-element';
+import { CodeSyntaxLeaf } from './elements/code-block/code-syntax-leaf';
+import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
+import { HrElement } from './elements/hr';
+import { TodoListPlugin } from '@udecode/plate-list/react';
+import { TodoListElement } from './elements/todo-list';
+import { LinkPlugin } from '@udecode/plate-link/react';
+import { LinkElement } from './elements/link';
+import { HighlightPlugin } from '@udecode/plate-highlight/react';
+import { withDraggables } from './with-draggables';
+
+export const createPlateUI = () => {
+ let components: Record = {
+ [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
+ [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
+ [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
+ [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),
+ [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),
+ [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),
+
+ [BlockquotePlugin.key]: BlockquoteElement,
+
+ [CodeBlockPlugin.key]: CodeBlockElement,
+ [CodeLinePlugin.key]: CodeLineElement,
+ [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
+
+ [HorizontalRulePlugin.key]: HrElement,
+ [TodoListPlugin.key]: TodoListElement,
+ [LinkPlugin.key]: LinkElement,
+
+ [ParagraphPlugin.key]: withProps(PlateElement, {
+ draggable: true,
+ as: 'p',
+ className: 'text-foreground-muted leading-7 m-0 px-0',
+ }),
+ [BoldPlugin.key]: withProps(PlateLeaf, {
+ as: 'strong',
+ className: '!font-semibold text-foreground',
+ }),
+ [ItalicPlugin.key]: withProps(PlateLeaf, {
+ as: 'em',
+ className: 'text-foreground',
+ }),
+ [UnderlinePlugin.key]: withProps(PlateLeaf, {
+ as: 'u',
+ className: 'text-foreground',
+ }),
+ [StrikethroughPlugin.key]: withProps(PlateLeaf, {
+ as: 's',
+ className: 'text-foreground',
+ }),
+ [SubscriptPlugin.key]: withProps(PlateLeaf, {
+ as: 'sub',
+ className: 'text-foreground',
+ }),
+ [SuperscriptPlugin.key]: withProps(PlateLeaf, {
+ as: 'sup',
+ className: 'text-foreground',
+ }),
+ [CodePlugin.key]: withProps(PlateLeaf, {
+ as: 'code',
+ className:
+ 'text-foreground bg-gray-element border rounded-md px-1 py-0.5',
+ }),
+ [HighlightPlugin.key]: withProps(PlateLeaf, {
+ as: 'mark',
+ className:
+ 'bg-yellow-500/10 text-yellow-500 [&>em]:text-yellow-500 [&>strong]:text-yellow-500 [&>u]:text-yellow-500 [&>s]:text-yellow-500 [&>sub]:text-yellow-500 [&>sup]:text-yellow-500',
+ }),
+ };
+
+ components = withPlaceholders(withDraggables(components));
+
+ return components;
+};
diff --git a/src/editor/ui/draggable.tsx b/src/editor/ui/draggable.tsx
new file mode 100644
index 00000000..10523c0e
--- /dev/null
+++ b/src/editor/ui/draggable.tsx
@@ -0,0 +1,174 @@
+'use client';
+
+import React from 'react';
+
+import type { ClassNames, TEditor } from '@udecode/plate-common';
+import type { DropTargetMonitor } from 'react-dnd';
+
+import { cn, withRef } from '@udecode/cn';
+import {
+ type PlateElementProps,
+ useEditorRef,
+} from '@udecode/plate-common/react';
+import {
+ type DragItemNode,
+ useDraggable,
+ useDraggableState,
+} from '@udecode/plate-dnd';
+import { BlockSelectionPlugin } from '@udecode/plate-selection/react';
+
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipPortal,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/primitives/tooltip';
+import { GripIcon } from 'lucide-react';
+
+export interface DraggableProps
+ extends PlateElementProps,
+ ClassNames<{
+ /** Block. */
+ block: string;
+
+ /** Block and gutter. */
+ blockAndGutter: string;
+
+ /** Block toolbar in the gutter. */
+ blockToolbar: string;
+
+ /**
+ * Block toolbar wrapper in the gutter left. It has the height of a line
+ * of the block.
+ */
+ blockToolbarWrapper: string;
+
+ blockWrapper: string;
+
+ /** Button to dnd the block, in the block toolbar. */
+ dragHandle: string;
+
+ /** Icon of the drag button, in the drag icon. */
+ dragIcon: string;
+
+ /** Show a dropline above or below the block when dragging a block. */
+ dropLine: string;
+
+ /** Gutter at the left side of the editor. It has the height of the block */
+ gutterLeft: string;
+ }> {
+ /**
+ * Intercepts the drop handling. If `false` is returned, the default drop
+ * behavior is called after. If `true` is returned, the default behavior is
+ * not called.
+ */
+ onDropHandler?: (
+ editor: TEditor,
+ props: {
+ id: string;
+ dragItem: DragItemNode;
+ monitor: DropTargetMonitor;
+ nodeRef: unknown;
+ },
+ ) => boolean;
+ key: string;
+ keys: string[];
+}
+
+const DragHandle = () => {
+ const editor = useEditorRef();
+
+ return (
+
+
+
+ {
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ onMouseDown={() => {
+ editor
+ .getApi(BlockSelectionPlugin)
+ .blockSelection.resetSelectedIds();
+ }}
+ />
+
+
+ Drag to move
+
+
+
+ );
+};
+
+export const Draggable = withRef<'div', DraggableProps>(
+ ({ className, classNames = {}, children, element, onDropHandler }, ref) => {
+ const state = useDraggableState({ element, onDropHandler });
+ const { dropLine, isDragging, isHovered } = state;
+ const {
+ droplineProps,
+ groupProps,
+ gutterLeftProps,
+ previewRef,
+ handleRef,
+ } = useDraggable(state);
+
+ return (
+
+
+
+
+ {children}
+
+ {!!dropLine && (
+
+ )}
+
+
+ );
+ },
+);
diff --git a/src/editor/ui/editor.tsx b/src/editor/ui/editor.tsx
new file mode 100644
index 00000000..b305ba8d
--- /dev/null
+++ b/src/editor/ui/editor.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { cn } from '@udecode/cn';
+import { PlateContent } from '@udecode/plate-common/react';
+import { cva } from 'class-variance-authority';
+
+import type { PlateContentProps } from '@udecode/plate-common/react';
+import type { VariantProps } from 'class-variance-authority';
+
+const editorVariants = cva(
+ cn(
+ 'relative whitespace-pre-wrap break-words',
+ 'min-h-[80px] w-full rounded-md text-sm ring-offset-background placeholder:text-foreground-muted focus-visible:outline-none',
+ '[&_[data-slate-placeholder]]:text-foreground-muted [&_[data-slate-placeholder]]:!opacity-100',
+ '[&_[data-slate-placeholder]]:top-[auto_!important]',
+ '[&_strong]:font-bold',
+ ),
+ {
+ defaultVariants: {
+ focusRing: true,
+ size: 'sm',
+ variant: 'outline',
+ },
+ variants: {
+ disabled: {
+ true: 'cursor-not-allowed opacity-50',
+ },
+ focusRing: {
+ false: '',
+ true: 'focus-visible:ring-2 focus-visible:ring-pink-solid-hover focus-visible:ring-offset-2',
+ },
+ focused: {
+ true: 'ring-2 ring-pink-solid-hover ring-offset-2',
+ },
+ size: {
+ md: 'text-base',
+ sm: 'text-sm',
+ },
+ variant: {
+ ghost: '',
+ outline: 'border',
+ },
+ },
+ },
+);
+
+export type EditorProps = PlateContentProps &
+ VariantProps;
+
+const Editor = React.forwardRef(
+ (
+ {
+ className,
+ disabled,
+ focusRing,
+ focused,
+ readOnly = false,
+ size = 'md',
+ variant,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+ );
+ },
+);
+Editor.displayName = 'Editor';
+
+export { Editor };
diff --git a/src/editor/ui/elements/blockquote.tsx b/src/editor/ui/elements/blockquote.tsx
new file mode 100644
index 00000000..8c979f90
--- /dev/null
+++ b/src/editor/ui/elements/blockquote.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import React from 'react';
+
+import { cn, withRef } from '@udecode/cn';
+import { PlateElement } from '@udecode/plate-common/react';
+
+export const BlockquoteElement = withRef(
+ ({ children, className, ...props }, ref) => {
+ return (
+
+ {children}
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/code-block/code-block-combobox.tsx b/src/editor/ui/elements/code-block/code-block-combobox.tsx
new file mode 100644
index 00000000..a6ab81df
--- /dev/null
+++ b/src/editor/ui/elements/code-block/code-block-combobox.tsx
@@ -0,0 +1,211 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+import Prism from 'prismjs';
+
+import { Icon } from '@/primitives/icon';
+
+import { Button } from '@/primitives/button';
+import { Popover, PopoverContent, PopoverTrigger } from '@/primitives/popover';
+
+import 'prismjs/components/prism-antlr4.js';
+import 'prismjs/components/prism-bash.js';
+import 'prismjs/components/prism-c.js';
+import 'prismjs/components/prism-cmake.js';
+import 'prismjs/components/prism-coffeescript.js';
+import 'prismjs/components/prism-cpp.js';
+import 'prismjs/components/prism-csharp.js';
+import 'prismjs/components/prism-css.js';
+import 'prismjs/components/prism-dart.js';
+import 'prismjs/components/prism-django.js';
+import 'prismjs/components/prism-docker.js';
+import 'prismjs/components/prism-ejs.js';
+import 'prismjs/components/prism-erlang.js';
+import 'prismjs/components/prism-git.js';
+import 'prismjs/components/prism-go.js';
+import 'prismjs/components/prism-graphql.js';
+import 'prismjs/components/prism-groovy.js';
+import 'prismjs/components/prism-java.js';
+import 'prismjs/components/prism-javascript.js';
+import 'prismjs/components/prism-json.js';
+import 'prismjs/components/prism-jsx.js';
+import 'prismjs/components/prism-kotlin.js';
+import 'prismjs/components/prism-latex.js';
+import 'prismjs/components/prism-less.js';
+import 'prismjs/components/prism-lua.js';
+import 'prismjs/components/prism-makefile.js';
+import 'prismjs/components/prism-markdown.js';
+import 'prismjs/components/prism-matlab.js';
+import 'prismjs/components/prism-mermaid.js';
+import 'prismjs/components/prism-objectivec.js';
+import 'prismjs/components/prism-perl.js';
+import 'prismjs/components/prism-php.js';
+import 'prismjs/components/prism-powershell.js';
+import 'prismjs/components/prism-properties.js';
+import 'prismjs/components/prism-protobuf.js';
+import 'prismjs/components/prism-python.js';
+import 'prismjs/components/prism-r.js';
+import 'prismjs/components/prism-ruby.js';
+import 'prismjs/components/prism-sass.js';
+import 'prismjs/components/prism-scala.js';
+import 'prismjs/components/prism-scheme.js';
+import 'prismjs/components/prism-scss.js';
+import 'prismjs/components/prism-sql.js';
+import 'prismjs/components/prism-swift.js';
+import 'prismjs/components/prism-tsx.js';
+import 'prismjs/components/prism-typescript.js';
+import 'prismjs/components/prism-wasm.js';
+import 'prismjs/components/prism-yaml.js';
+import { ScrollArea } from '@/primitives/scroll-area';
+import {
+ useCodeBlockCombobox,
+ useCodeBlockComboboxState,
+} from '@udecode/plate-code-block/react';
+import { cn } from '@/lib/utils';
+import { Input } from '@/primitives/input';
+
+export { Prism };
+
+const languages: { label: string; value: string }[] = [
+ { label: 'Plain Text', value: 'text' },
+ { label: 'Bash', value: 'bash' },
+ { label: 'CSS', value: 'css' },
+ { label: 'Git', value: 'git' },
+ { label: 'GraphQL', value: 'graphql' },
+ { label: 'HTML', value: 'html' },
+ { label: 'JavaScript', value: 'javascript' },
+ { label: 'JSON', value: 'json' },
+ { label: 'JSX', value: 'jsx' },
+ { label: 'Markdown', value: 'markdown' },
+ { label: 'SQL', value: 'sql' },
+ { label: 'SVG', value: 'svg' },
+ { label: 'TSX', value: 'tsx' },
+ { label: 'TypeScript', value: 'typescript' },
+ { label: 'WebAssembly', value: 'wasm' },
+ { label: 'ANTLR4', value: 'antlr4' },
+ { label: 'C', value: 'c' },
+ { label: 'CMake', value: 'cmake' },
+ { label: 'CoffeeScript', value: 'coffeescript' },
+ { label: 'C#', value: 'csharp' },
+ { label: 'Dart', value: 'dart' },
+ { label: 'Django', value: 'django' },
+ { label: 'Docker', value: 'docker' },
+ { label: 'EJS', value: 'ejs' },
+ { label: 'Erlang', value: 'erlang' },
+ { label: 'Go', value: 'go' },
+ { label: 'Groovy', value: 'groovy' },
+ { label: 'Java', value: 'java' },
+ { label: 'Kotlin', value: 'kotlin' },
+ { label: 'LaTeX', value: 'latex' },
+ { label: 'Less', value: 'less' },
+ { label: 'Lua', value: 'lua' },
+ { label: 'Makefile', value: 'makefile' },
+ { label: 'Markup', value: 'markup' },
+ { label: 'MATLAB', value: 'matlab' },
+ { label: 'Mermaid', value: 'mermaid' },
+ { label: 'Objective-C', value: 'objectivec' },
+ { label: 'Perl', value: 'perl' },
+ { label: 'PHP', value: 'php' },
+ { label: 'PowerShell', value: 'powershell' },
+ { label: '.properties', value: 'properties' },
+ { label: 'Protocol Buffers', value: 'protobuf' },
+ { label: 'Python', value: 'python' },
+ { label: 'R', value: 'r' },
+ { label: 'Ruby', value: 'ruby' },
+ { label: 'Sass (Sass)', value: 'sass' },
+ { label: 'Scala', value: 'scala' },
+ { label: 'Scheme', value: 'scheme' },
+ { label: 'Sass (Scss)', value: 'scss' },
+ { label: 'Shell', value: 'shell' },
+ { label: 'Swift', value: 'swift' },
+ { label: 'XML', value: 'xml' },
+ { label: 'YAML', value: 'yaml' },
+];
+
+export function CodeBlockCombobox() {
+ const state = useCodeBlockComboboxState();
+ const { commandItemProps } = useCodeBlockCombobox(state);
+
+ const [open, setOpen] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filteredLanguages, setFilteredLanguages] = useState(languages);
+
+ useEffect(() => {
+ const filtered = languages.filter((lang) =>
+ lang.label.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
+ setFilteredLanguages(filtered);
+ }, [searchTerm]);
+
+ if (state.readOnly) {
+ return null;
+ }
+
+ const handleSelect = (value: string) => {
+ commandItemProps.onSelect(value);
+ setOpen(false);
+ };
+
+ const selectedLanguage =
+ languages.find((lang) => lang.value === state.value) ?? languages[0];
+
+ return (
+
+
+
+
+
+
+ {
+ setSearchTerm(e.target.value);
+ }}
+ />
+
+
+ {filteredLanguages.length === 0 ? (
+
+ No language found.
+
+ ) : (
+ filteredLanguages.map((language) => (
+
+ ))
+ )}
+
+
+
+ );
+}
diff --git a/src/editor/ui/elements/code-block/code-block.css b/src/editor/ui/elements/code-block/code-block.css
new file mode 100644
index 00000000..0a521eb1
--- /dev/null
+++ b/src/editor/ui/elements/code-block/code-block.css
@@ -0,0 +1,276 @@
+pre[class*='language-'],
+code[class*='language-'] {
+ color: #d4d4d4;
+ font-size: 13px;
+ text-shadow: none;
+ font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono',
+ 'Courier New', monospace;
+ direction: ltr;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ line-height: 1.5;
+ -moz-tab-size: 4;
+ -o-tab-size: 4;
+ tab-size: 4;
+ -webkit-hyphens: none;
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ hyphens: none;
+}
+
+pre[class*='language-']::selection,
+code[class*='language-']::selection,
+pre[class*='language-'] *::selection,
+code[class*='language-'] *::selection {
+ text-shadow: none;
+ background: #264f78;
+}
+
+@media print {
+ pre[class*='language-'],
+ code[class*='language-'] {
+ text-shadow: none;
+ }
+}
+
+pre[class*='language-'] {
+ padding: 1em;
+ margin: 0.5em 0;
+ overflow: auto;
+ background: #1e1e1e;
+}
+
+:not(pre) > code[class*='language-'] {
+ padding: 0.1em 0.3em;
+ border-radius: 0.3em;
+ color: #db4c69;
+ background: #1e1e1e;
+}
+/*********************************************************
+* Tokens
+*/
+.namespace {
+ opacity: 0.7;
+}
+
+.token.doctype .token.doctype-tag {
+ color: #569cd6;
+}
+
+.token.doctype .token.name {
+ color: #9cdcfe;
+}
+
+.token.comment,
+.token.prolog {
+ color: #6a9955;
+}
+
+.token.punctuation,
+.language-html .language-css .token.punctuation,
+.language-html .language-javascript .token.punctuation {
+ color: #d4d4d4;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.inserted,
+.token.unit {
+ color: #b5cea8;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.deleted {
+ color: #ce9178;
+}
+
+.language-css .token.string.url {
+ text-decoration: underline;
+}
+
+.token.operator,
+.token.entity {
+ color: #d4d4d4;
+}
+
+.token.operator.arrow {
+ color: #569cd6;
+}
+
+.token.atrule {
+ color: #ce9178;
+}
+
+.token.atrule .token.rule {
+ color: #c586c0;
+}
+
+.token.atrule .token.url {
+ color: #9cdcfe;
+}
+
+.token.atrule .token.url .token.function {
+ color: #dcdcaa;
+}
+
+.token.atrule .token.url .token.punctuation {
+ color: #d4d4d4;
+}
+
+.token.keyword {
+ color: #569cd6;
+}
+
+.token.keyword.module,
+.token.keyword.control-flow {
+ color: #c586c0;
+}
+
+.token.function,
+.token.function .token.maybe-class-name {
+ color: #dcdcaa;
+}
+
+.token.regex {
+ color: #d16969;
+}
+
+.token.important {
+ color: #569cd6;
+}
+
+.token.italic {
+ font-style: italic;
+}
+
+.token.constant {
+ color: #9cdcfe;
+}
+
+.token.class-name,
+.token.maybe-class-name {
+ color: #4ec9b0;
+}
+
+.token.console {
+ color: #9cdcfe;
+}
+
+.token.parameter {
+ color: #9cdcfe;
+}
+
+.token.interpolation {
+ color: #9cdcfe;
+}
+
+.token.punctuation.interpolation-punctuation {
+ color: #569cd6;
+}
+
+.token.boolean {
+ color: #569cd6;
+}
+
+.token.property,
+.token.variable,
+.token.imports .token.maybe-class-name,
+.token.exports .token.maybe-class-name {
+ color: #9cdcfe;
+}
+
+.token.selector {
+ color: #d7ba7d;
+}
+
+.token.escape {
+ color: #d7ba7d;
+}
+
+.token.tag {
+ color: #569cd6;
+}
+
+.token.tag .token.punctuation {
+ color: #808080;
+}
+
+.token.cdata {
+ color: #808080;
+}
+
+.token.attr-name {
+ color: #9cdcfe;
+}
+
+.token.attr-value,
+.token.attr-value .token.punctuation {
+ color: #ce9178;
+}
+
+.token.attr-value .token.punctuation.attr-equals {
+ color: #d4d4d4;
+}
+
+.token.entity {
+ color: #569cd6;
+}
+
+.token.namespace {
+ color: #4ec9b0;
+}
+/*********************************************************
+* Language Specific
+*/
+
+pre[class*='language-javascript'],
+code[class*='language-javascript'],
+pre[class*='language-jsx'],
+code[class*='language-jsx'],
+pre[class*='language-typescript'],
+code[class*='language-typescript'],
+pre[class*='language-tsx'],
+code[class*='language-tsx'] {
+ color: #9cdcfe;
+}
+
+pre[class*='language-css'],
+code[class*='language-css'] {
+ color: #ce9178;
+}
+
+pre[class*='language-html'],
+code[class*='language-html'] {
+ color: #d4d4d4;
+}
+
+.language-regex .token.anchor {
+ color: #dcdcaa;
+}
+
+.language-html .token.punctuation {
+ color: #808080;
+}
+/*********************************************************
+* Line highlighting
+*/
+pre[class*='language-'] > code[class*='language-'] {
+ position: relative;
+ z-index: 1;
+}
+
+.line-highlight.line-highlight {
+ background: #f7ebc6;
+ box-shadow: inset 5px 0 0 #f7d87c;
+ z-index: 0;
+}
diff --git a/src/editor/ui/elements/code-block/code-line-element.tsx b/src/editor/ui/elements/code-block/code-line-element.tsx
new file mode 100644
index 00000000..52ecca5f
--- /dev/null
+++ b/src/editor/ui/elements/code-block/code-line-element.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+import React from 'react';
+import { withRef } from '@udecode/cn';
+import { PlateElement } from '@udecode/plate-common/react';
+
+export const CodeLineElement = withRef((props, ref) => (
+
+));
diff --git a/src/editor/ui/elements/code-block/code-syntax-leaf.tsx b/src/editor/ui/elements/code-block/code-syntax-leaf.tsx
new file mode 100644
index 00000000..ab8e4161
--- /dev/null
+++ b/src/editor/ui/elements/code-block/code-syntax-leaf.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import React from 'react';
+import { withRef } from '@udecode/cn';
+import { useCodeSyntaxLeaf } from '@udecode/plate-code-block/react';
+import { PlateLeaf } from '@udecode/plate-common/react';
+
+export const CodeSyntaxLeaf = withRef(
+ ({ children, ...props }, ref) => {
+ const { leaf } = props;
+
+ const { tokenProps } = useCodeSyntaxLeaf({ leaf });
+
+ return (
+
+ {children}
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/code-block/index.tsx b/src/editor/ui/elements/code-block/index.tsx
new file mode 100644
index 00000000..79a5988c
--- /dev/null
+++ b/src/editor/ui/elements/code-block/index.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import React from 'react';
+
+import { cn, withRef } from '@udecode/cn';
+import { useCodeBlockElementState } from '@udecode/plate-code-block/react';
+import { PlateElement } from '@udecode/plate-common/react';
+
+import { CodeBlockCombobox } from './code-block-combobox';
+
+import './code-block.css';
+
+export const CodeBlockElement = withRef(
+ ({ children, className, ...props }, ref) => {
+ const { element } = props;
+ const state = useCodeBlockElementState({ element });
+
+ return (
+
+
+ {children}
+
+
+ {state.syntax && (
+
+
+
+ )}
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/heading.tsx b/src/editor/ui/elements/heading.tsx
new file mode 100644
index 00000000..b0e6df77
--- /dev/null
+++ b/src/editor/ui/elements/heading.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+
+import { withRef, withVariants } from '@udecode/cn';
+import { PlateElement } from '@udecode/plate-common/react';
+import { cva } from 'class-variance-authority';
+
+const headingVariants = cva('inline-block', {
+ variants: {
+ isFirstBlock: {
+ false: '',
+ true: '!mt-0',
+ },
+ variant: {
+ h1: 'mb-4 mt-[1.4em] text-4xl font-semibold',
+ h2: 'mb-2 mt-[1.15em] text-3xl font-semibold tracking-tight',
+ h3: 'mb-1.5 mt-[1.15em] text-2xl font-semibold tracking-tight',
+ h4: 'mb-1 mt-[1.15em] text-xl font-semibold tracking-tight',
+ h5: 'mb-1 mt-[1.15em] text-lg font-semibold tracking-tight',
+ h6: 'mb-1 mt-[1.15em] text-base font-semibold tracking-tight',
+ },
+ },
+});
+
+const HeadingElementVariants = withVariants(PlateElement, headingVariants, [
+ 'isFirstBlock',
+ 'variant',
+]);
+
+export const HeadingElement = withRef(
+ ({ children, isFirstBlock, variant = 'h1', ...props }, ref) => {
+ const { editor, element } = props;
+
+ const Element = variant ?? 'h1';
+
+ return (
+
+ {children}
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/hr.tsx b/src/editor/ui/elements/hr.tsx
new file mode 100644
index 00000000..80478ac5
--- /dev/null
+++ b/src/editor/ui/elements/hr.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import { cn, withRef } from '@udecode/cn';
+import { PlateElement } from '@udecode/plate-common/react';
+import { useFocused, useSelected } from 'slate-react';
+
+export const HrElement = withRef(
+ ({ className, nodeProps, children, ...props }, ref) => {
+ const selected = useSelected();
+ const focused = useFocused();
+
+ return (
+
+
+
+
+ {children}
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/link.tsx b/src/editor/ui/elements/link.tsx
new file mode 100644
index 00000000..24cabc0e
--- /dev/null
+++ b/src/editor/ui/elements/link.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { cn, withRef } from '@udecode/cn';
+import { PlateElement, useElement } from '@udecode/plate-common/react';
+import { useLink } from '@udecode/plate-link/react';
+
+import type { TLinkElement } from '@udecode/plate-link';
+
+export const LinkElement = withRef(
+ ({ children, className, ...props }, ref) => {
+ const element = useElement();
+ const { props: linkProps } = useLink({ element });
+
+ return (
+ )}
+ {...props}
+ >
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ {children}
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/todo-list.tsx b/src/editor/ui/elements/todo-list.tsx
new file mode 100644
index 00000000..595a8b6e
--- /dev/null
+++ b/src/editor/ui/elements/todo-list.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { cn, withRef } from '@udecode/cn';
+import { PlateElement } from '@udecode/plate-common/react';
+import {
+ useTodoListElement,
+ useTodoListElementState,
+} from '@udecode/plate-list/react';
+
+import { Checkbox } from '@/primitives/checkbox';
+
+export const TodoListElement = withRef(
+ ({ children, className, ...props }, ref) => {
+ const { element } = props;
+ const state = useTodoListElementState({ element });
+ const { checkboxProps } = useTodoListElement(state);
+
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
+ },
+);
diff --git a/src/editor/ui/elements/todo.tsx b/src/editor/ui/elements/todo.tsx
new file mode 100644
index 00000000..e4d9f54c
--- /dev/null
+++ b/src/editor/ui/elements/todo.tsx
@@ -0,0 +1,37 @@
+import type { PlateRenderElementProps } from '@udecode/plate-common/react';
+
+import { cn } from '@udecode/cn';
+import {
+ useIndentTodoListElement,
+ useIndentTodoListElementState,
+} from '@udecode/plate-indent-list/react';
+
+import { Checkbox } from '@/primitives/checkbox';
+
+export const TodoMarker = ({
+ element,
+}: Omit) => {
+ const state = useIndentTodoListElementState({ element });
+ const { checkboxProps } = useIndentTodoListElement(state);
+
+ return (
+
+
+
+ );
+};
+
+export const TodoLi = ({ children, element }: PlateRenderElementProps) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/editor/ui/link-floating-toolbar.tsx b/src/editor/ui/link-floating-toolbar.tsx
new file mode 100644
index 00000000..93222257
--- /dev/null
+++ b/src/editor/ui/link-floating-toolbar.tsx
@@ -0,0 +1,159 @@
+'use client';
+
+import React from 'react';
+import { cn } from '@udecode/cn';
+import { useFormInputProps } from '@udecode/plate-common/react';
+import { flip, offset } from '@udecode/plate-floating';
+import {
+ FloatingLinkUrlInput,
+ LinkOpenButton,
+ useFloatingLinkEdit,
+ useFloatingLinkEditState,
+ useFloatingLinkInsert,
+ useFloatingLinkInsertState,
+} from '@udecode/plate-link/react';
+
+import { buttonVariants } from '@/primitives/button';
+import { inputVariants, Input } from '@/primitives/input';
+import { popoverVariants } from '@/primitives/popover';
+import { Separator } from '@/primitives/separator';
+
+import type { UseVirtualFloatingOptions } from '@udecode/plate-floating';
+import type { LinkFloatingToolbarState } from '@udecode/plate-link/react';
+import { ExternalLinkIcon, LinkIcon, TextIcon, UnlinkIcon } from 'lucide-react';
+
+const floatingOptions: UseVirtualFloatingOptions = {
+ middleware: [
+ offset(12),
+ flip({
+ fallbackPlacements: ['bottom-end', 'top-start', 'top-end'],
+ padding: 12,
+ }),
+ ],
+ placement: 'bottom-start',
+};
+
+export interface LinkFloatingToolbarProps {
+ state?: LinkFloatingToolbarState;
+}
+
+export function LinkFloatingToolbar({ state }: LinkFloatingToolbarProps) {
+ const insertState = useFloatingLinkInsertState({
+ ...state,
+ floatingOptions: {
+ ...floatingOptions,
+ ...state?.floatingOptions,
+ },
+ });
+ const {
+ hidden,
+ props: insertProps,
+ ref: insertRef,
+ textInputProps,
+ } = useFloatingLinkInsert(insertState);
+
+ const editState = useFloatingLinkEditState({
+ ...state,
+ floatingOptions: {
+ ...floatingOptions,
+ ...state?.floatingOptions,
+ },
+ });
+ const {
+ editButtonProps,
+ props: editProps,
+ ref: editRef,
+ unlinkButtonProps,
+ } = useFloatingLinkEdit(editState);
+ const inputProps = useFormInputProps({
+ preventDefaultOnEnterKeydown: true,
+ });
+
+ if (hidden) {
+ return null;
+ }
+
+ const input = (
+
+ );
+
+ const editContent = editState.isEditing ? (
+ input
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ return (
+ <>
+
+ {input}
+
+
+
+ {editContent}
+
+ >
+ );
+}
diff --git a/src/editor/ui/placeholder.tsx b/src/editor/ui/placeholder.tsx
new file mode 100644
index 00000000..5b010256
--- /dev/null
+++ b/src/editor/ui/placeholder.tsx
@@ -0,0 +1,70 @@
+import type { ReactElement } from 'react';
+import React from 'react';
+import { cn } from '@udecode/cn';
+import {
+ createNodeHOC,
+ createNodesHOC,
+ ParagraphPlugin,
+ usePlaceholderState,
+} from '@udecode/plate-common/react';
+import { HEADING_KEYS } from '@udecode/plate-heading';
+
+import type {
+ CreateHOCOptions,
+ NodeComponent,
+ PlaceholderProps,
+} from '@udecode/plate-common/react';
+
+interface CustomPlaceholderProps extends PlaceholderProps {
+ placeholder: string;
+ nodeProps?: Record;
+ children: ReactElement;
+}
+
+export const Placeholder = (props: CustomPlaceholderProps) => {
+ const { children, nodeProps, placeholder } = props;
+
+ const { enabled } = usePlaceholderState(props);
+
+ return React.Children.map(children, (child: ReactElement) => {
+ return React.cloneElement(child, {
+ className: (child.props as { className: string }).className,
+ nodeProps: {
+ ...nodeProps,
+ className: cn(
+ enabled &&
+ 'before:absolute before:cursor-text before:opacity-30 before:content-[attr(placeholder)]',
+ ),
+ placeholder,
+ },
+ });
+ });
+};
+
+export const withPlaceholder = createNodeHOC(Placeholder);
+
+export const withPlaceholdersPrimitive: (
+ components: Record,
+ options:
+ | CreateHOCOptions
+ | CreateHOCOptions[],
+) => Record = createNodesHOC(Placeholder);
+
+export const withPlaceholders: (
+ components: Record,
+) => Record = (components) =>
+ withPlaceholdersPrimitive(components, [
+ {
+ hideOnBlur: true,
+ key: ParagraphPlugin.key,
+ placeholder: 'Type a paragraph',
+ query: {
+ maxLevel: 1,
+ },
+ },
+ {
+ hideOnBlur: false,
+ key: HEADING_KEYS.h1,
+ placeholder: 'Untitled',
+ },
+ ]);
diff --git a/src/editor/ui/with-draggables.tsx b/src/editor/ui/with-draggables.tsx
new file mode 100644
index 00000000..c32c875c
--- /dev/null
+++ b/src/editor/ui/with-draggables.tsx
@@ -0,0 +1,108 @@
+import type { FC } from 'react';
+
+import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
+import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
+import type { NodeComponent } from '@udecode/plate-common/react';
+import {
+ ParagraphPlugin,
+ createNodesWithHOC,
+} from '@udecode/plate-common/react';
+import {
+ type WithDraggableOptions,
+ withDraggable as withDraggablePrimitive,
+} from '@udecode/plate-dnd';
+import { HEADING_KEYS } from '@udecode/plate-heading';
+import {
+ BulletedListPlugin,
+ NumberedListPlugin,
+} from '@udecode/plate-list/react';
+
+import { type DraggableProps, Draggable } from './draggable';
+
+interface MyDraggableOptions extends WithDraggableOptions {
+ keys?: string[];
+ key?: string;
+}
+
+export const withDraggable = (
+ Component: FC,
+ options?: MyDraggableOptions>,
+) =>
+ withDraggablePrimitive>(
+ Draggable,
+ Component,
+ options,
+ );
+
+export const withDraggablesPrimitive: (
+ components: Record,
+ options: MyDraggableOptions<
+ Partial>
+ >[],
+) => Record = createNodesWithHOC(withDraggable);
+
+export const withDraggables = (components: Record) => {
+ return withDraggablesPrimitive(components, [
+ {
+ keys: [
+ ParagraphPlugin.key,
+ BulletedListPlugin.key,
+ NumberedListPlugin.key,
+ ],
+ level: 0,
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-[1px]',
+ },
+ },
+ },
+ {
+ key: HEADING_KEYS.h2,
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-9',
+ },
+ },
+ },
+ {
+ key: HEADING_KEYS.h3,
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-7',
+ },
+ },
+ },
+ {
+ key: HEADING_KEYS.h4,
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-6',
+ },
+ },
+ },
+ {
+ key: HEADING_KEYS.h5,
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-[22px]',
+ },
+ },
+ },
+ {
+ key: HEADING_KEYS.h6,
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-[17px]',
+ },
+ },
+ },
+ {
+ keys: [BlockquotePlugin.key, CodeBlockPlugin.key],
+ draggableProps: {
+ classNames: {
+ gutterLeft: 'pt-[1px]',
+ },
+ },
+ },
+ ]);
+};
diff --git a/src/primitives/dialog.tsx b/src/primitives/dialog.tsx
new file mode 100644
index 00000000..988c9f64
--- /dev/null
+++ b/src/primitives/dialog.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import * as React from 'react';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogHeader.displayName = 'DialogHeader';
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogFooter.displayName = 'DialogFooter';
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/src/primitives/icon.tsx b/src/primitives/icon.tsx
index 2ebd656f..c100715d 100644
--- a/src/primitives/icon.tsx
+++ b/src/primitives/icon.tsx
@@ -5,14 +5,26 @@ interface Props {
color?: string;
size?: number;
strokeWidth?: number;
+ className?: string;
}
-export const Icon: React.FC = ({ name, color, size, strokeWidth }) => {
+export const Icon: React.FC = ({
+ name,
+ color,
+ size,
+ strokeWidth,
+ className,
+}) => {
// eslint-disable-next-line security/detect-object-injection
const LucideIcon = icons[name];
return (
-
+
);
};
diff --git a/src/primitives/input.tsx b/src/primitives/input.tsx
index e9a21f8a..a4dc5183 100644
--- a/src/primitives/input.tsx
+++ b/src/primitives/input.tsx
@@ -1,24 +1,37 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
+import type { VariantProps } from 'class-variance-authority';
+import { cva } from 'class-variance-authority';
export type InputProps = React.InputHTMLAttributes;
-const Input = React.forwardRef(
- ({ className, type, ...props }, ref) => {
- return (
-
- );
+const inputVariants = cva(
+ 'flex h-9 w-full rounded-md border-gray-subtle-border bg-gray-subtle px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-gray-solid focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ outline:
+ 'border focus-visible:ring-1 focus-visible:ring-pink-solid-hover',
+ ghost: '',
+ },
+ },
},
);
+
+const Input = React.forwardRef<
+ HTMLInputElement,
+ InputProps & VariantProps
+>(({ className, type, variant = 'outline', ...props }, ref) => {
+ return (
+
+ );
+});
Input.displayName = 'Input';
-export { Input };
+export { Input, inputVariants };
diff --git a/src/primitives/popover.tsx b/src/primitives/popover.tsx
index add93fdc..bf426782 100644
--- a/src/primitives/popover.tsx
+++ b/src/primitives/popover.tsx
@@ -5,6 +5,7 @@ import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '@/lib/utils';
+import { cva } from 'class-variance-authority';
const Popover = PopoverPrimitive.Root;
@@ -12,6 +13,10 @@ const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
+const popoverVariants = cva(
+ 'z-50 w-72 rounded-md border bg-gray-subtle text-foreground-muted shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
+);
+
const PopoverContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
@@ -21,14 +26,17 @@ const PopoverContent = React.forwardRef<
ref={ref}
align={align}
sideOffset={sideOffset}
- className={cn(
- 'z-50 w-72 rounded-md border bg-gray-subtle text-foreground-muted shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
- className,
- )}
+ className={cn(popoverVariants(), className)}
{...props}
/>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
+export {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+ PopoverAnchor,
+ popoverVariants,
+};
diff --git a/src/primitives/tooltip.tsx b/src/primitives/tooltip.tsx
new file mode 100644
index 00000000..b6e10d57
--- /dev/null
+++ b/src/primitives/tooltip.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import * as React from 'react';
+import * as TooltipPrimitive from '@radix-ui/react-tooltip';
+
+import { cn } from '@/lib/utils';
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const TooltipPortal = TooltipPrimitive.Portal;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+ TooltipProvider,
+ TooltipPortal,
+};