ToS Editor
This commit is contained in:
		
							
								
								
									
										1691
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1691
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -15,6 +15,10 @@
 | 
			
		||||
    "@dnd-kit/utilities": "^3.2.2",
 | 
			
		||||
    "@hookform/resolvers": "^5.1.1",
 | 
			
		||||
    "@platejs/basic-nodes": "^49.0.0",
 | 
			
		||||
    "@platejs/code-block": "^49.0.0",
 | 
			
		||||
    "@platejs/indent": "^49.0.0",
 | 
			
		||||
    "@platejs/list": "^49.0.0",
 | 
			
		||||
    "@platejs/markdown": "^49.0.17",
 | 
			
		||||
    "@prisma/client": "^6.11.1",
 | 
			
		||||
    "@radix-ui/react-checkbox": "^1.3.2",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.14",
 | 
			
		||||
@ -36,6 +40,7 @@
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "cmdk": "^1.1.1",
 | 
			
		||||
    "lowlight": "^3.3.0",
 | 
			
		||||
    "lucide-react": "^0.525.0",
 | 
			
		||||
    "next": "15.3.5",
 | 
			
		||||
    "next-auth": "^5.0.0-beta.29",
 | 
			
		||||
@ -44,6 +49,8 @@
 | 
			
		||||
    "react": "^19.0.0",
 | 
			
		||||
    "react-dom": "^19.0.0",
 | 
			
		||||
    "react-hook-form": "^7.59.0",
 | 
			
		||||
    "remark-gfm": "^4.0.1",
 | 
			
		||||
    "remark-math": "^6.0.0",
 | 
			
		||||
    "sonner": "^2.0.6",
 | 
			
		||||
    "tailwind-merge": "^3.3.1",
 | 
			
		||||
    "tailwind-scrollbar-hide": "^4.0.0",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								prisma/migrations/20250706133422_tos/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								prisma/migrations/20250706133422_tos/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
-- CreateTable
 | 
			
		||||
CREATE TABLE "TermsOfService" (
 | 
			
		||||
    "id" TEXT NOT NULL,
 | 
			
		||||
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    "updatedAt" TIMESTAMP(3) NOT NULL,
 | 
			
		||||
    "version" SERIAL NOT NULL,
 | 
			
		||||
    "markdown" TEXT NOT NULL,
 | 
			
		||||
 | 
			
		||||
    CONSTRAINT "TermsOfService_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
@ -91,3 +91,12 @@ model CommissionTypeExtra {
 | 
			
		||||
 | 
			
		||||
  @@unique([typeId, extraId])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model TermsOfService {
 | 
			
		||||
  id        String   @id @default(cuid())
 | 
			
		||||
  createdAt DateTime @default(now())
 | 
			
		||||
  updatedAt DateTime @updatedAt
 | 
			
		||||
 | 
			
		||||
  version  Int    @default(autoincrement())
 | 
			
		||||
  markdown String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								src/actions/items/commissions/tos/getTos.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/actions/items/commissions/tos/getTos.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
'use server';
 | 
			
		||||
 | 
			
		||||
import prisma from "@/lib/prisma";
 | 
			
		||||
 | 
			
		||||
export async function getLatestTos(): Promise<string | null> {
 | 
			
		||||
  const tos = await prisma.termsOfService.findFirst({
 | 
			
		||||
    orderBy: { createdAt: 'desc' },
 | 
			
		||||
  });
 | 
			
		||||
  return tos?.markdown ?? null;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/actions/items/commissions/tos/saveTosAction.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/actions/items/commissions/tos/saveTosAction.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
'use server';
 | 
			
		||||
 | 
			
		||||
import prisma from "@/lib/prisma";
 | 
			
		||||
 | 
			
		||||
export async function saveTosAction(markdown: string) {
 | 
			
		||||
  await prisma.termsOfService.create({
 | 
			
		||||
    data: {
 | 
			
		||||
      markdown,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@ -1,13 +1,16 @@
 | 
			
		||||
import { getLatestTos } from "@/actions/items/commissions/tos/getTos";
 | 
			
		||||
import TosEditor from "@/components/items/commissions/tos/TosEditor";
 | 
			
		||||
 | 
			
		||||
export default function TosPage() {
 | 
			
		||||
export default async function TosPage() {
 | 
			
		||||
  const markdown = await getLatestTos();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex gap-4 justify-between pb-8">
 | 
			
		||||
        <h1 className="text-2xl font-bold mb-4">Terms of Service</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="space-y-4 p-1 border rounded-xl bg-muted/20">
 | 
			
		||||
        <TosEditor />
 | 
			
		||||
        <TosEditor markdown={markdown} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								src/components/editor/plugins/code-block-base-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/editor/plugins/code-block-base-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import {
 | 
			
		||||
  BaseCodeBlockPlugin,
 | 
			
		||||
  BaseCodeLinePlugin,
 | 
			
		||||
  BaseCodeSyntaxPlugin,
 | 
			
		||||
} from '@platejs/code-block';
 | 
			
		||||
import { all, createLowlight } from 'lowlight';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CodeBlockElementStatic,
 | 
			
		||||
  CodeLineElementStatic,
 | 
			
		||||
  CodeSyntaxLeafStatic,
 | 
			
		||||
} from '@/components/ui/code-block-node-static';
 | 
			
		||||
 | 
			
		||||
const lowlight = createLowlight(all);
 | 
			
		||||
 | 
			
		||||
export const BaseCodeBlockKit = [
 | 
			
		||||
  BaseCodeBlockPlugin.configure({
 | 
			
		||||
    node: { component: CodeBlockElementStatic },
 | 
			
		||||
    options: { lowlight },
 | 
			
		||||
  }),
 | 
			
		||||
  BaseCodeLinePlugin.withComponent(CodeLineElementStatic),
 | 
			
		||||
  BaseCodeSyntaxPlugin.withComponent(CodeSyntaxLeafStatic),
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										26
									
								
								src/components/editor/plugins/code-block-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/components/editor/plugins/code-block-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CodeBlockPlugin,
 | 
			
		||||
  CodeLinePlugin,
 | 
			
		||||
  CodeSyntaxPlugin,
 | 
			
		||||
} from '@platejs/code-block/react';
 | 
			
		||||
import { all, createLowlight } from 'lowlight';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CodeBlockElement,
 | 
			
		||||
  CodeLineElement,
 | 
			
		||||
  CodeSyntaxLeaf,
 | 
			
		||||
} from '@/components/ui/code-block-node';
 | 
			
		||||
 | 
			
		||||
const lowlight = createLowlight(all);
 | 
			
		||||
 | 
			
		||||
export const CodeBlockKit = [
 | 
			
		||||
  CodeBlockPlugin.configure({
 | 
			
		||||
    node: { component: CodeBlockElement },
 | 
			
		||||
    options: { lowlight },
 | 
			
		||||
    shortcuts: { toggle: { keys: 'mod+alt+8' } },
 | 
			
		||||
  }),
 | 
			
		||||
  CodeLinePlugin.withComponent(CodeLineElement),
 | 
			
		||||
  CodeSyntaxPlugin.withComponent(CodeSyntaxLeaf),
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										19
									
								
								src/components/editor/plugins/indent-base-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/components/editor/plugins/indent-base-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
import { BaseIndentPlugin } from '@platejs/indent';
 | 
			
		||||
import { KEYS } from 'platejs';
 | 
			
		||||
 | 
			
		||||
export const BaseIndentKit = [
 | 
			
		||||
  BaseIndentPlugin.configure({
 | 
			
		||||
    inject: {
 | 
			
		||||
      targetPlugins: [
 | 
			
		||||
        ...KEYS.heading,
 | 
			
		||||
        KEYS.p,
 | 
			
		||||
        KEYS.blockquote,
 | 
			
		||||
        KEYS.codeBlock,
 | 
			
		||||
        KEYS.toggle,
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    options: {
 | 
			
		||||
      offset: 24,
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										21
									
								
								src/components/editor/plugins/indent-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/editor/plugins/indent-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import { IndentPlugin } from '@platejs/indent/react';
 | 
			
		||||
import { KEYS } from 'platejs';
 | 
			
		||||
 | 
			
		||||
export const IndentKit = [
 | 
			
		||||
  IndentPlugin.configure({
 | 
			
		||||
    inject: {
 | 
			
		||||
      targetPlugins: [
 | 
			
		||||
        ...KEYS.heading,
 | 
			
		||||
        KEYS.p,
 | 
			
		||||
        KEYS.blockquote,
 | 
			
		||||
        KEYS.codeBlock,
 | 
			
		||||
        KEYS.toggle,
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    options: {
 | 
			
		||||
      offset: 24,
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										23
									
								
								src/components/editor/plugins/list-base-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/editor/plugins/list-base-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import { BaseListPlugin } from '@platejs/list';
 | 
			
		||||
import { KEYS } from 'platejs';
 | 
			
		||||
 | 
			
		||||
import { BaseIndentKit } from '@/components/editor/plugins/indent-base-kit';
 | 
			
		||||
import { BlockListStatic } from '@/components/ui/block-list-static';
 | 
			
		||||
 | 
			
		||||
export const BaseListKit = [
 | 
			
		||||
  ...BaseIndentKit,
 | 
			
		||||
  BaseListPlugin.configure({
 | 
			
		||||
    inject: {
 | 
			
		||||
      targetPlugins: [
 | 
			
		||||
        ...KEYS.heading,
 | 
			
		||||
        KEYS.p,
 | 
			
		||||
        KEYS.blockquote,
 | 
			
		||||
        KEYS.codeBlock,
 | 
			
		||||
        KEYS.toggle,
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    render: {
 | 
			
		||||
      belowNodes: BlockListStatic,
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										25
									
								
								src/components/editor/plugins/list-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/components/editor/plugins/list-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import { ListPlugin } from '@platejs/list/react';
 | 
			
		||||
import { KEYS } from 'platejs';
 | 
			
		||||
 | 
			
		||||
import { IndentKit } from '@/components/editor/plugins/indent-kit';
 | 
			
		||||
import { BlockList } from '@/components/ui/block-list';
 | 
			
		||||
 | 
			
		||||
export const ListKit = [
 | 
			
		||||
  ...IndentKit,
 | 
			
		||||
  ListPlugin.configure({
 | 
			
		||||
    inject: {
 | 
			
		||||
      targetPlugins: [
 | 
			
		||||
        ...KEYS.heading,
 | 
			
		||||
        KEYS.p,
 | 
			
		||||
        KEYS.blockquote,
 | 
			
		||||
        KEYS.codeBlock,
 | 
			
		||||
        KEYS.toggle,
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    render: {
 | 
			
		||||
      belowNodes: BlockList,
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										13
									
								
								src/components/editor/plugins/markdown-kit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/components/editor/plugins/markdown-kit.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
import { MarkdownPlugin, remarkMdx, remarkMention } from '@platejs/markdown';
 | 
			
		||||
import { KEYS } from 'platejs';
 | 
			
		||||
import remarkGfm from 'remark-gfm';
 | 
			
		||||
import remarkMath from 'remark-math';
 | 
			
		||||
 | 
			
		||||
export const MarkdownKit = [
 | 
			
		||||
  MarkdownPlugin.configure({
 | 
			
		||||
    options: {
 | 
			
		||||
      disallowedNodes: [KEYS.suggestion],
 | 
			
		||||
      remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
@ -4,57 +4,107 @@ import type { Value } from 'platejs';
 | 
			
		||||
 | 
			
		||||
import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
 | 
			
		||||
import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit';
 | 
			
		||||
import { MarkdownKit } from '@/components/editor/plugins/markdown-kit';
 | 
			
		||||
import { Editor, EditorContainer } from '@/components/ui/editor';
 | 
			
		||||
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
 | 
			
		||||
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
 | 
			
		||||
import { ToolbarButton } from '@/components/ui/toolbar';
 | 
			
		||||
import {
 | 
			
		||||
  Bold,
 | 
			
		||||
  Braces,
 | 
			
		||||
  Code,
 | 
			
		||||
  Heading1,
 | 
			
		||||
  Heading2,
 | 
			
		||||
  Heading3,
 | 
			
		||||
  Italic,
 | 
			
		||||
  Quote,
 | 
			
		||||
  Save,
 | 
			
		||||
  Strikethrough,
 | 
			
		||||
  Underline
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import { Plate, usePlateEditor } from 'platejs/react';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import { saveTosAction } from '@/actions/items/commissions/tos/saveTosAction';
 | 
			
		||||
import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit';
 | 
			
		||||
import { ListKit } from '@/components/editor/plugins/list-kit';
 | 
			
		||||
import { BulletedListToolbarButton, NumberedListToolbarButton } from '@/components/ui/list-toolbar-button';
 | 
			
		||||
 | 
			
		||||
const initialValue: Value = [
 | 
			
		||||
  {
 | 
			
		||||
    children: [{ text: 'Title' }],
 | 
			
		||||
    type: 'h3',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    children: [{ text: 'This is a quote.' }],
 | 
			
		||||
    type: 'blockquote',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'p',
 | 
			
		||||
    children: [
 | 
			
		||||
      { text: 'Hello! Try out the ' },
 | 
			
		||||
      { text: 'bold', bold: true },
 | 
			
		||||
      { text: ', ' },
 | 
			
		||||
      { text: 'italic', italic: true },
 | 
			
		||||
      { text: ', and ' },
 | 
			
		||||
      { text: 'underline', underline: true },
 | 
			
		||||
      { text: ' formatting.' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default function TosEditor() {
 | 
			
		||||
 | 
			
		||||
export default function TosEditor({ markdown }: { markdown: string | null }) {
 | 
			
		||||
  // const [isSaving, setIsSaving] = useState(false);
 | 
			
		||||
  const editor = usePlateEditor({
 | 
			
		||||
    plugins: [
 | 
			
		||||
      ...BasicBlocksKit,
 | 
			
		||||
      ...CodeBlockKit,
 | 
			
		||||
      ...ListKit,
 | 
			
		||||
      ...BasicMarksKit,
 | 
			
		||||
    ], // Add the mark plugins
 | 
			
		||||
    value: initialValue,         // Set initial content
 | 
			
		||||
      ...MarkdownKit,
 | 
			
		||||
    ],
 | 
			
		||||
    value: initialValue,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (markdown && editor.api.markdown.deserialize) {
 | 
			
		||||
      const markdownValue = editor.api.markdown.deserialize(markdown);
 | 
			
		||||
      console.log(markdownValue);
 | 
			
		||||
      editor.children = markdownValue;
 | 
			
		||||
    }
 | 
			
		||||
  }, [editor, markdown]);
 | 
			
		||||
 | 
			
		||||
  const handleSave = async () => {
 | 
			
		||||
    console.log(editor);
 | 
			
		||||
    if (!editor.api.markdown.serialize) return;
 | 
			
		||||
    // setIsSaving(true);
 | 
			
		||||
    const markdown = editor.api.markdown.serialize();
 | 
			
		||||
    await saveTosAction(markdown);
 | 
			
		||||
    // setIsSaving(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Plate editor={editor}>      {/* Provides editor context */}
 | 
			
		||||
      <FixedToolbar className="justify-start rounded-t-lg">
 | 
			
		||||
        {/* Element Toolbar Buttons */}
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.h1.toggle()}>H1</ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.h2.toggle()}>H2</ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.h3.toggle()}>H3</ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.blockquote.toggle()}>Quote</ToolbarButton>
 | 
			
		||||
        {/* Blocks */}
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.h1.toggle()} tooltip="Heading 1">
 | 
			
		||||
          <Heading1 className="w-4 h-4" />
 | 
			
		||||
        </ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.h2.toggle()} tooltip="Heading 2">
 | 
			
		||||
          <Heading2 className="w-4 h-4" />
 | 
			
		||||
        </ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.h3.toggle()} tooltip="Heading 3">
 | 
			
		||||
          <Heading3 className="w-4 h-4" />
 | 
			
		||||
        </ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.blockquote.toggle()} tooltip="Blockquote">
 | 
			
		||||
          <Quote className="w-4 h-4" />
 | 
			
		||||
        </ToolbarButton>
 | 
			
		||||
        <ToolbarButton onClick={() => editor.tf.code_block.toggle()} tooltip="Code Block">
 | 
			
		||||
          <Braces className="w-4 h-4" />
 | 
			
		||||
        </ToolbarButton>
 | 
			
		||||
        <BulletedListToolbarButton />
 | 
			
		||||
        <NumberedListToolbarButton />
 | 
			
		||||
        {/* Mark Toolbar Buttons */}
 | 
			
		||||
        <MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">B</MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="italic" tooltip="Italic (⌘+I)">I</MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="underline" tooltip="Underline (⌘+U)">U</MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="bold" tooltip="Bold">
 | 
			
		||||
          <Bold className="w-4 h-4" />
 | 
			
		||||
        </MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="italic" tooltip="Italic">
 | 
			
		||||
          <Italic className="w-4 h-4" />
 | 
			
		||||
        </MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="underline" tooltip="Underline">
 | 
			
		||||
          <Underline className="w-4 h-4" />
 | 
			
		||||
        </MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="strikethrough" tooltip="Strikethrough">
 | 
			
		||||
          <Strikethrough className="w-4 h-4" />
 | 
			
		||||
        </MarkToolbarButton>
 | 
			
		||||
        <MarkToolbarButton nodeType="code" tooltip="Code">
 | 
			
		||||
          <Code className="w-4 h-4" />
 | 
			
		||||
        </MarkToolbarButton>
 | 
			
		||||
        {/* Save Button */}
 | 
			
		||||
        <ToolbarButton onClick={handleSave} tooltip="Save">
 | 
			
		||||
          <Save className="w-4 h-4" />
 | 
			
		||||
        </ToolbarButton>
 | 
			
		||||
      </FixedToolbar>
 | 
			
		||||
      <EditorContainer>         {/* Styles the editor area */}
 | 
			
		||||
        <Editor placeholder="Type your amazing content here..." />
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										83
									
								
								src/components/ui/block-list-static.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/ui/block-list-static.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  RenderStaticNodeWrapper,
 | 
			
		||||
  SlateRenderElementProps,
 | 
			
		||||
  TListElement,
 | 
			
		||||
} from 'platejs';
 | 
			
		||||
 | 
			
		||||
import { isOrderedList } from '@platejs/list';
 | 
			
		||||
import { CheckIcon } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
const config: Record<
 | 
			
		||||
  string,
 | 
			
		||||
  {
 | 
			
		||||
    Li: React.FC<SlateRenderElementProps>;
 | 
			
		||||
    Marker: React.FC<SlateRenderElementProps>;
 | 
			
		||||
  }
 | 
			
		||||
> = {
 | 
			
		||||
  todo: {
 | 
			
		||||
    Li: TodoLiStatic,
 | 
			
		||||
    Marker: TodoMarkerStatic,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const BlockListStatic: RenderStaticNodeWrapper = (props) => {
 | 
			
		||||
  if (!props.element.listStyleType) return;
 | 
			
		||||
 | 
			
		||||
  return (props) => <List {...props} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function List(props: SlateRenderElementProps) {
 | 
			
		||||
  const { listStart, listStyleType } = props.element as TListElement;
 | 
			
		||||
  const { Li, Marker } = config[listStyleType] ?? {};
 | 
			
		||||
  const List = isOrderedList(props.element) ? 'ol' : 'ul';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <List
 | 
			
		||||
      className="relative m-0 p-0"
 | 
			
		||||
      style={{ listStyleType }}
 | 
			
		||||
      start={listStart}
 | 
			
		||||
    >
 | 
			
		||||
      {Marker && <Marker {...props} />}
 | 
			
		||||
      {Li ? <Li {...props} /> : <li>{props.children}</li>}
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TodoMarkerStatic(props: SlateRenderElementProps) {
 | 
			
		||||
  const checked = props.element.checked as boolean;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div contentEditable={false}>
 | 
			
		||||
      <button
 | 
			
		||||
        className={cn(
 | 
			
		||||
          'peer pointer-events-none absolute top-1 -left-6 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
 | 
			
		||||
          props.className
 | 
			
		||||
        )}
 | 
			
		||||
        data-state={checked ? 'checked' : 'unchecked'}
 | 
			
		||||
        type="button"
 | 
			
		||||
      >
 | 
			
		||||
        <div className={cn('flex items-center justify-center text-current')}>
 | 
			
		||||
          {checked && <CheckIcon className="size-4" />}
 | 
			
		||||
        </div>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TodoLiStatic(props: SlateRenderElementProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <li
 | 
			
		||||
      className={cn(
 | 
			
		||||
        'list-none',
 | 
			
		||||
        (props.element.checked as boolean) &&
 | 
			
		||||
          'text-muted-foreground line-through'
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </li>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								src/components/ui/block-list.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/components/ui/block-list.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import type { TListElement } from 'platejs';
 | 
			
		||||
 | 
			
		||||
import { isOrderedList } from '@platejs/list';
 | 
			
		||||
import {
 | 
			
		||||
  useTodoListElement,
 | 
			
		||||
  useTodoListElementState,
 | 
			
		||||
} from '@platejs/list/react';
 | 
			
		||||
import {
 | 
			
		||||
  type PlateElementProps,
 | 
			
		||||
  type RenderNodeWrapper,
 | 
			
		||||
  useReadOnly,
 | 
			
		||||
} from 'platejs/react';
 | 
			
		||||
 | 
			
		||||
import { Checkbox } from '@/components/ui/checkbox';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
const config: Record<
 | 
			
		||||
  string,
 | 
			
		||||
  {
 | 
			
		||||
    Li: React.FC<PlateElementProps>;
 | 
			
		||||
    Marker: React.FC<PlateElementProps>;
 | 
			
		||||
  }
 | 
			
		||||
> = {
 | 
			
		||||
  todo: {
 | 
			
		||||
    Li: TodoLi,
 | 
			
		||||
    Marker: TodoMarker,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const BlockList: RenderNodeWrapper = (props) => {
 | 
			
		||||
  if (!props.element.listStyleType) return;
 | 
			
		||||
 | 
			
		||||
  return (props) => <List {...props} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function List(props: PlateElementProps) {
 | 
			
		||||
  const { listStart, listStyleType } = props.element as TListElement;
 | 
			
		||||
  const { Li, Marker } = config[listStyleType] ?? {};
 | 
			
		||||
  const List = isOrderedList(props.element) ? 'ol' : 'ul';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <List
 | 
			
		||||
      className="relative m-0 p-0"
 | 
			
		||||
      style={{ listStyleType }}
 | 
			
		||||
      start={listStart}
 | 
			
		||||
    >
 | 
			
		||||
      {Marker && <Marker {...props} />}
 | 
			
		||||
      {Li ? <Li {...props} /> : <li>{props.children}</li>}
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TodoMarker(props: PlateElementProps) {
 | 
			
		||||
  const state = useTodoListElementState({ element: props.element });
 | 
			
		||||
  const { checkboxProps } = useTodoListElement(state);
 | 
			
		||||
  const readOnly = useReadOnly();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div contentEditable={false}>
 | 
			
		||||
      <Checkbox
 | 
			
		||||
        className={cn(
 | 
			
		||||
          'absolute top-1 -left-6',
 | 
			
		||||
          readOnly && 'pointer-events-none'
 | 
			
		||||
        )}
 | 
			
		||||
        {...checkboxProps}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TodoLi(props: PlateElementProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <li
 | 
			
		||||
      className={cn(
 | 
			
		||||
        'list-none',
 | 
			
		||||
        (props.element.checked as boolean) &&
 | 
			
		||||
          'text-muted-foreground line-through'
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </li>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								src/components/ui/code-block-node-static.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/components/ui/code-block-node-static.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  type SlateElementProps,
 | 
			
		||||
  type SlateLeafProps,
 | 
			
		||||
  type TCodeBlockElement,
 | 
			
		||||
  SlateElement,
 | 
			
		||||
  SlateLeaf,
 | 
			
		||||
} from 'platejs';
 | 
			
		||||
 | 
			
		||||
export function CodeBlockElementStatic(
 | 
			
		||||
  props: SlateElementProps<TCodeBlockElement>
 | 
			
		||||
) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SlateElement
 | 
			
		||||
      className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="relative rounded-md bg-muted/50">
 | 
			
		||||
        <pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
 | 
			
		||||
          <code>{props.children}</code>
 | 
			
		||||
        </pre>
 | 
			
		||||
      </div>
 | 
			
		||||
    </SlateElement>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function CodeLineElementStatic(props: SlateElementProps) {
 | 
			
		||||
  return <SlateElement {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function CodeSyntaxLeafStatic(props: SlateLeafProps) {
 | 
			
		||||
  const tokenClassName = props.leaf.className as string;
 | 
			
		||||
 | 
			
		||||
  return <SlateLeaf className={tokenClassName} {...props} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										289
									
								
								src/components/ui/code-block-node.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								src/components/ui/code-block-node.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,289 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { formatCodeBlock, isLangSupported } from '@platejs/code-block';
 | 
			
		||||
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
 | 
			
		||||
import { type TCodeBlockElement, type TCodeSyntaxLeaf, NodeApi } from 'platejs';
 | 
			
		||||
import {
 | 
			
		||||
  type PlateElementProps,
 | 
			
		||||
  type PlateLeafProps,
 | 
			
		||||
  PlateElement,
 | 
			
		||||
  PlateLeaf,
 | 
			
		||||
} from 'platejs/react';
 | 
			
		||||
import { useEditorRef, useElement, useReadOnly } from 'platejs/react';
 | 
			
		||||
 | 
			
		||||
import { Button } from '@/components/ui/button';
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandEmpty,
 | 
			
		||||
  CommandGroup,
 | 
			
		||||
  CommandInput,
 | 
			
		||||
  CommandItem,
 | 
			
		||||
  CommandList,
 | 
			
		||||
} from '@/components/ui/command';
 | 
			
		||||
import {
 | 
			
		||||
  Popover,
 | 
			
		||||
  PopoverContent,
 | 
			
		||||
  PopoverTrigger,
 | 
			
		||||
} from '@/components/ui/popover';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
 | 
			
		||||
  const { editor, element } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <PlateElement
 | 
			
		||||
      className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="relative rounded-md bg-muted/50">
 | 
			
		||||
        <pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
 | 
			
		||||
          <code>{props.children}</code>
 | 
			
		||||
        </pre>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          className="absolute top-1 right-1 z-10 flex gap-0.5 select-none"
 | 
			
		||||
          contentEditable={false}
 | 
			
		||||
        >
 | 
			
		||||
          {isLangSupported(element.lang) && (
 | 
			
		||||
            <Button
 | 
			
		||||
              size="icon"
 | 
			
		||||
              variant="ghost"
 | 
			
		||||
              className="size-6 text-xs"
 | 
			
		||||
              onClick={() => formatCodeBlock(editor, { element })}
 | 
			
		||||
              title="Format code"
 | 
			
		||||
            >
 | 
			
		||||
              <BracesIcon className="!size-3.5 text-muted-foreground" />
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <CodeBlockCombobox />
 | 
			
		||||
 | 
			
		||||
          <CopyButton
 | 
			
		||||
            size="icon"
 | 
			
		||||
            variant="ghost"
 | 
			
		||||
            className="size-6 gap-1 text-xs text-muted-foreground"
 | 
			
		||||
            value={() => NodeApi.string(element)}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </PlateElement>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CodeBlockCombobox() {
 | 
			
		||||
  const [open, setOpen] = React.useState(false);
 | 
			
		||||
  const readOnly = useReadOnly();
 | 
			
		||||
  const editor = useEditorRef();
 | 
			
		||||
  const element = useElement<TCodeBlockElement>();
 | 
			
		||||
  const value = element.lang || 'plaintext';
 | 
			
		||||
  const [searchValue, setSearchValue] = React.useState('');
 | 
			
		||||
 | 
			
		||||
  const items = React.useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      languages.filter(
 | 
			
		||||
        (language) =>
 | 
			
		||||
          !searchValue ||
 | 
			
		||||
          language.label.toLowerCase().includes(searchValue.toLowerCase())
 | 
			
		||||
      ),
 | 
			
		||||
    [searchValue]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (readOnly) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover open={open} onOpenChange={setOpen}>
 | 
			
		||||
      <PopoverTrigger asChild>
 | 
			
		||||
        <Button
 | 
			
		||||
          size="sm"
 | 
			
		||||
          variant="ghost"
 | 
			
		||||
          className="h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none"
 | 
			
		||||
          aria-expanded={open}
 | 
			
		||||
          role="combobox"
 | 
			
		||||
        >
 | 
			
		||||
          {languages.find((language) => language.value === value)?.label ??
 | 
			
		||||
            'Plain Text'}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </PopoverTrigger>
 | 
			
		||||
      <PopoverContent
 | 
			
		||||
        className="w-[200px] p-0"
 | 
			
		||||
        onCloseAutoFocus={() => setSearchValue('')}
 | 
			
		||||
      >
 | 
			
		||||
        <Command shouldFilter={false}>
 | 
			
		||||
          <CommandInput
 | 
			
		||||
            className="h-9"
 | 
			
		||||
            value={searchValue}
 | 
			
		||||
            onValueChange={(value) => setSearchValue(value)}
 | 
			
		||||
            placeholder="Search language..."
 | 
			
		||||
          />
 | 
			
		||||
          <CommandEmpty>No language found.</CommandEmpty>
 | 
			
		||||
 | 
			
		||||
          <CommandList className="h-[344px] overflow-y-auto">
 | 
			
		||||
            <CommandGroup>
 | 
			
		||||
              {items.map((language) => (
 | 
			
		||||
                <CommandItem
 | 
			
		||||
                  key={language.label}
 | 
			
		||||
                  className="cursor-pointer"
 | 
			
		||||
                  value={language.value}
 | 
			
		||||
                  onSelect={(value) => {
 | 
			
		||||
                    editor.tf.setNodes<TCodeBlockElement>(
 | 
			
		||||
                      { lang: value },
 | 
			
		||||
                      { at: element }
 | 
			
		||||
                    );
 | 
			
		||||
                    setSearchValue(value);
 | 
			
		||||
                    setOpen(false);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Check
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                      value === language.value ? 'opacity-100' : 'opacity-0'
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
                  {language.label}
 | 
			
		||||
                </CommandItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </CommandGroup>
 | 
			
		||||
          </CommandList>
 | 
			
		||||
        </Command>
 | 
			
		||||
      </PopoverContent>
 | 
			
		||||
    </Popover>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CopyButton({
 | 
			
		||||
  value,
 | 
			
		||||
  ...props
 | 
			
		||||
}: { value: (() => string) | string } & Omit<
 | 
			
		||||
  React.ComponentProps<typeof Button>,
 | 
			
		||||
  'value'
 | 
			
		||||
>) {
 | 
			
		||||
  const [hasCopied, setHasCopied] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setHasCopied(false);
 | 
			
		||||
    }, 2000);
 | 
			
		||||
  }, [hasCopied]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        void navigator.clipboard.writeText(
 | 
			
		||||
          typeof value === 'function' ? value() : value
 | 
			
		||||
        );
 | 
			
		||||
        setHasCopied(true);
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="sr-only">Copy</span>
 | 
			
		||||
      {hasCopied ? (
 | 
			
		||||
        <CheckIcon className="!size-3" />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <CopyIcon className="!size-3" />
 | 
			
		||||
      )}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function CodeLineElement(props: PlateElementProps) {
 | 
			
		||||
  return <PlateElement {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
 | 
			
		||||
  const tokenClassName = props.leaf.className as string;
 | 
			
		||||
 | 
			
		||||
  return <PlateLeaf className={tokenClassName} {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const languages: { label: string; value: string }[] = [
 | 
			
		||||
  { label: 'Auto', value: 'auto' },
 | 
			
		||||
  { label: 'Plain Text', value: 'plaintext' },
 | 
			
		||||
  { label: 'ABAP', value: 'abap' },
 | 
			
		||||
  { label: 'Agda', value: 'agda' },
 | 
			
		||||
  { label: 'Arduino', value: 'arduino' },
 | 
			
		||||
  { label: 'ASCII Art', value: 'ascii' },
 | 
			
		||||
  { label: 'Assembly', value: 'x86asm' },
 | 
			
		||||
  { label: 'Bash', value: 'bash' },
 | 
			
		||||
  { label: 'BASIC', value: 'basic' },
 | 
			
		||||
  { label: 'BNF', value: 'bnf' },
 | 
			
		||||
  { label: 'C', value: 'c' },
 | 
			
		||||
  { label: 'C#', value: 'csharp' },
 | 
			
		||||
  { label: 'C++', value: 'cpp' },
 | 
			
		||||
  { label: 'Clojure', value: 'clojure' },
 | 
			
		||||
  { label: 'CoffeeScript', value: 'coffeescript' },
 | 
			
		||||
  { label: 'Coq', value: 'coq' },
 | 
			
		||||
  { label: 'CSS', value: 'css' },
 | 
			
		||||
  { label: 'Dart', value: 'dart' },
 | 
			
		||||
  { label: 'Dhall', value: 'dhall' },
 | 
			
		||||
  { label: 'Diff', value: 'diff' },
 | 
			
		||||
  { label: 'Docker', value: 'dockerfile' },
 | 
			
		||||
  { label: 'EBNF', value: 'ebnf' },
 | 
			
		||||
  { label: 'Elixir', value: 'elixir' },
 | 
			
		||||
  { label: 'Elm', value: 'elm' },
 | 
			
		||||
  { label: 'Erlang', value: 'erlang' },
 | 
			
		||||
  { label: 'F#', value: 'fsharp' },
 | 
			
		||||
  { label: 'Flow', value: 'flow' },
 | 
			
		||||
  { label: 'Fortran', value: 'fortran' },
 | 
			
		||||
  { label: 'Gherkin', value: 'gherkin' },
 | 
			
		||||
  { label: 'GLSL', value: 'glsl' },
 | 
			
		||||
  { label: 'Go', value: 'go' },
 | 
			
		||||
  { label: 'GraphQL', value: 'graphql' },
 | 
			
		||||
  { label: 'Groovy', value: 'groovy' },
 | 
			
		||||
  { label: 'Haskell', value: 'haskell' },
 | 
			
		||||
  { label: 'HCL', value: 'hcl' },
 | 
			
		||||
  { label: 'HTML', value: 'html' },
 | 
			
		||||
  { label: 'Idris', value: 'idris' },
 | 
			
		||||
  { label: 'Java', value: 'java' },
 | 
			
		||||
  { label: 'JavaScript', value: 'javascript' },
 | 
			
		||||
  { label: 'JSON', value: 'json' },
 | 
			
		||||
  { label: 'Julia', value: 'julia' },
 | 
			
		||||
  { label: 'Kotlin', value: 'kotlin' },
 | 
			
		||||
  { label: 'LaTeX', value: 'latex' },
 | 
			
		||||
  { label: 'Less', value: 'less' },
 | 
			
		||||
  { label: 'Lisp', value: 'lisp' },
 | 
			
		||||
  { label: 'LiveScript', value: 'livescript' },
 | 
			
		||||
  { label: 'LLVM IR', value: 'llvm' },
 | 
			
		||||
  { label: 'Lua', value: 'lua' },
 | 
			
		||||
  { label: 'Makefile', value: 'makefile' },
 | 
			
		||||
  { label: 'Markdown', value: 'markdown' },
 | 
			
		||||
  { label: 'Markup', value: 'markup' },
 | 
			
		||||
  { label: 'MATLAB', value: 'matlab' },
 | 
			
		||||
  { label: 'Mathematica', value: 'mathematica' },
 | 
			
		||||
  { label: 'Mermaid', value: 'mermaid' },
 | 
			
		||||
  { label: 'Nix', value: 'nix' },
 | 
			
		||||
  { label: 'Notion Formula', value: 'notion' },
 | 
			
		||||
  { label: 'Objective-C', value: 'objectivec' },
 | 
			
		||||
  { label: 'OCaml', value: 'ocaml' },
 | 
			
		||||
  { label: 'Pascal', value: 'pascal' },
 | 
			
		||||
  { label: 'Perl', value: 'perl' },
 | 
			
		||||
  { label: 'PHP', value: 'php' },
 | 
			
		||||
  { label: 'PowerShell', value: 'powershell' },
 | 
			
		||||
  { label: 'Prolog', value: 'prolog' },
 | 
			
		||||
  { label: 'Protocol Buffers', value: 'protobuf' },
 | 
			
		||||
  { label: 'PureScript', value: 'purescript' },
 | 
			
		||||
  { label: 'Python', value: 'python' },
 | 
			
		||||
  { label: 'R', value: 'r' },
 | 
			
		||||
  { label: 'Racket', value: 'racket' },
 | 
			
		||||
  { label: 'Reason', value: 'reasonml' },
 | 
			
		||||
  { label: 'Ruby', value: 'ruby' },
 | 
			
		||||
  { label: 'Rust', value: 'rust' },
 | 
			
		||||
  { label: 'Sass', value: 'scss' },
 | 
			
		||||
  { label: 'Scala', value: 'scala' },
 | 
			
		||||
  { label: 'Scheme', value: 'scheme' },
 | 
			
		||||
  { label: 'SCSS', value: 'scss' },
 | 
			
		||||
  { label: 'Shell', value: 'shell' },
 | 
			
		||||
  { label: 'Smalltalk', value: 'smalltalk' },
 | 
			
		||||
  { label: 'Solidity', value: 'solidity' },
 | 
			
		||||
  { label: 'SQL', value: 'sql' },
 | 
			
		||||
  { label: 'Swift', value: 'swift' },
 | 
			
		||||
  { label: 'TOML', value: 'toml' },
 | 
			
		||||
  { label: 'TypeScript', value: 'typescript' },
 | 
			
		||||
  { label: 'VB.Net', value: 'vbnet' },
 | 
			
		||||
  { label: 'Verilog', value: 'verilog' },
 | 
			
		||||
  { label: 'VHDL', value: 'vhdl' },
 | 
			
		||||
  { label: 'Visual Basic', value: 'vbnet' },
 | 
			
		||||
  { label: 'WebAssembly', value: 'wasm' },
 | 
			
		||||
  { label: 'XML', value: 'xml' },
 | 
			
		||||
  { label: 'YAML', value: 'yaml' },
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										32
									
								
								src/components/ui/indent-toolbar-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/components/ui/indent-toolbar-button.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { useIndentButton, useOutdentButton } from '@platejs/indent/react';
 | 
			
		||||
import { IndentIcon, OutdentIcon } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
import { ToolbarButton } from './toolbar';
 | 
			
		||||
 | 
			
		||||
export function IndentToolbarButton(
 | 
			
		||||
  props: React.ComponentProps<typeof ToolbarButton>
 | 
			
		||||
) {
 | 
			
		||||
  const { props: buttonProps } = useIndentButton();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolbarButton {...props} {...buttonProps} tooltip="Indent">
 | 
			
		||||
      <IndentIcon />
 | 
			
		||||
    </ToolbarButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function OutdentToolbarButton(
 | 
			
		||||
  props: React.ComponentProps<typeof ToolbarButton>
 | 
			
		||||
) {
 | 
			
		||||
  const { props: buttonProps } = useOutdentButton();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolbarButton {...props} {...buttonProps} tooltip="Outdent">
 | 
			
		||||
      <OutdentIcon />
 | 
			
		||||
    </ToolbarButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										206
									
								
								src/components/ui/list-toolbar-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								src/components/ui/list-toolbar-button.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,206 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { ListStyleType, someList, toggleList } from '@platejs/list';
 | 
			
		||||
import {
 | 
			
		||||
  useIndentTodoToolBarButton,
 | 
			
		||||
  useIndentTodoToolBarButtonState,
 | 
			
		||||
} from '@platejs/list/react';
 | 
			
		||||
import { List, ListOrdered, ListTodoIcon } from 'lucide-react';
 | 
			
		||||
import { useEditorRef, useEditorSelector } from 'platejs/react';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
  DropdownMenuGroup,
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuTrigger,
 | 
			
		||||
} from '@/components/ui/dropdown-menu';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ToolbarButton,
 | 
			
		||||
  ToolbarSplitButton,
 | 
			
		||||
  ToolbarSplitButtonPrimary,
 | 
			
		||||
  ToolbarSplitButtonSecondary,
 | 
			
		||||
} from './toolbar';
 | 
			
		||||
 | 
			
		||||
export function BulletedListToolbarButton() {
 | 
			
		||||
  const editor = useEditorRef();
 | 
			
		||||
  const [open, setOpen] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
  const pressed = useEditorSelector(
 | 
			
		||||
    (editor) =>
 | 
			
		||||
      someList(editor, [
 | 
			
		||||
        ListStyleType.Disc,
 | 
			
		||||
        ListStyleType.Circle,
 | 
			
		||||
        ListStyleType.Square,
 | 
			
		||||
      ]),
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolbarSplitButton pressed={open}>
 | 
			
		||||
      <ToolbarSplitButtonPrimary
 | 
			
		||||
        className="data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          toggleList(editor, {
 | 
			
		||||
            listStyleType: ListStyleType.Disc,
 | 
			
		||||
          });
 | 
			
		||||
        }}
 | 
			
		||||
        data-state={pressed ? 'on' : 'off'}
 | 
			
		||||
      >
 | 
			
		||||
        <List className="size-4" />
 | 
			
		||||
      </ToolbarSplitButtonPrimary>
 | 
			
		||||
 | 
			
		||||
      <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
 | 
			
		||||
        <DropdownMenuTrigger asChild>
 | 
			
		||||
          <ToolbarSplitButtonSecondary />
 | 
			
		||||
        </DropdownMenuTrigger>
 | 
			
		||||
 | 
			
		||||
        <DropdownMenuContent align="start" alignOffset={-32}>
 | 
			
		||||
          <DropdownMenuGroup>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onClick={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.Disc,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <div className="flex items-center gap-2">
 | 
			
		||||
                <div className="size-2 rounded-full border border-current bg-current" />
 | 
			
		||||
                Default
 | 
			
		||||
              </div>
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onClick={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.Circle,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <div className="flex items-center gap-2">
 | 
			
		||||
                <div className="size-2 rounded-full border border-current" />
 | 
			
		||||
                Circle
 | 
			
		||||
              </div>
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onClick={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.Square,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <div className="flex items-center gap-2">
 | 
			
		||||
                <div className="size-2 border border-current bg-current" />
 | 
			
		||||
                Square
 | 
			
		||||
              </div>
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
          </DropdownMenuGroup>
 | 
			
		||||
        </DropdownMenuContent>
 | 
			
		||||
      </DropdownMenu>
 | 
			
		||||
    </ToolbarSplitButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function NumberedListToolbarButton() {
 | 
			
		||||
  const editor = useEditorRef();
 | 
			
		||||
  const [open, setOpen] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
  const pressed = useEditorSelector(
 | 
			
		||||
    (editor) =>
 | 
			
		||||
      someList(editor, [
 | 
			
		||||
        ListStyleType.Decimal,
 | 
			
		||||
        ListStyleType.LowerAlpha,
 | 
			
		||||
        ListStyleType.UpperAlpha,
 | 
			
		||||
        ListStyleType.LowerRoman,
 | 
			
		||||
        ListStyleType.UpperRoman,
 | 
			
		||||
      ]),
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolbarSplitButton pressed={open}>
 | 
			
		||||
      <ToolbarSplitButtonPrimary
 | 
			
		||||
        className="data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"
 | 
			
		||||
        onClick={() =>
 | 
			
		||||
          toggleList(editor, {
 | 
			
		||||
            listStyleType: ListStyleType.Decimal,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        data-state={pressed ? 'on' : 'off'}
 | 
			
		||||
      >
 | 
			
		||||
        <ListOrdered className="size-4" />
 | 
			
		||||
      </ToolbarSplitButtonPrimary>
 | 
			
		||||
 | 
			
		||||
      <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
 | 
			
		||||
        <DropdownMenuTrigger asChild>
 | 
			
		||||
          <ToolbarSplitButtonSecondary />
 | 
			
		||||
        </DropdownMenuTrigger>
 | 
			
		||||
 | 
			
		||||
        <DropdownMenuContent align="start" alignOffset={-32}>
 | 
			
		||||
          <DropdownMenuGroup>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onSelect={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.Decimal,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              Decimal (1, 2, 3)
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onSelect={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.LowerAlpha,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              Lower Alpha (a, b, c)
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onSelect={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.UpperAlpha,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              Upper Alpha (A, B, C)
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onSelect={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.LowerRoman,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              Lower Roman (i, ii, iii)
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
            <DropdownMenuItem
 | 
			
		||||
              onSelect={() =>
 | 
			
		||||
                toggleList(editor, {
 | 
			
		||||
                  listStyleType: ListStyleType.UpperRoman,
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              Upper Roman (I, II, III)
 | 
			
		||||
            </DropdownMenuItem>
 | 
			
		||||
          </DropdownMenuGroup>
 | 
			
		||||
        </DropdownMenuContent>
 | 
			
		||||
      </DropdownMenu>
 | 
			
		||||
    </ToolbarSplitButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function TodoListToolbarButton(
 | 
			
		||||
  props: React.ComponentProps<typeof ToolbarButton>
 | 
			
		||||
) {
 | 
			
		||||
  const state = useIndentTodoToolBarButtonState({ nodeType: 'todo' });
 | 
			
		||||
  const { props: buttonProps } = useIndentTodoToolBarButton(state);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolbarButton {...props} {...buttonProps} tooltip="Todo">
 | 
			
		||||
      <ListTodoIcon />
 | 
			
		||||
    </ToolbarButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user