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",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@platejs/basic-nodes": "^49.0.0",
|
"@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",
|
"@prisma/client": "^6.11.1",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
@ -36,6 +40,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
@ -44,6 +49,8 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.59.0",
|
"react-hook-form": "^7.59.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-scrollbar-hide": "^4.0.0",
|
"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])
|
@@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";
|
import TosEditor from "@/components/items/commissions/tos/TosEditor";
|
||||||
|
|
||||||
export default function TosPage() {
|
export default async function TosPage() {
|
||||||
|
const markdown = await getLatestTos();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-4 justify-between pb-8">
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">Terms of Service</h1>
|
<h1 className="text-2xl font-bold mb-4">Terms of Service</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 p-1 border rounded-xl bg-muted/20">
|
<div className="space-y-4 p-1 border rounded-xl bg-muted/20">
|
||||||
<TosEditor />
|
<TosEditor markdown={markdown} />
|
||||||
</div>
|
</div>
|
||||||
</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 { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
|
||||||
import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-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 { Editor, EditorContainer } from '@/components/ui/editor';
|
||||||
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
|
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
|
||||||
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
|
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
|
||||||
import { ToolbarButton } from '@/components/ui/toolbar';
|
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 { 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 = [
|
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({
|
const editor = usePlateEditor({
|
||||||
plugins: [
|
plugins: [
|
||||||
...BasicBlocksKit,
|
...BasicBlocksKit,
|
||||||
|
...CodeBlockKit,
|
||||||
|
...ListKit,
|
||||||
...BasicMarksKit,
|
...BasicMarksKit,
|
||||||
], // Add the mark plugins
|
...MarkdownKit,
|
||||||
value: initialValue, // Set initial content
|
],
|
||||||
|
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 (
|
return (
|
||||||
<Plate editor={editor}> {/* Provides editor context */}
|
<Plate editor={editor}> {/* Provides editor context */}
|
||||||
<FixedToolbar className="justify-start rounded-t-lg">
|
<FixedToolbar className="justify-start rounded-t-lg">
|
||||||
{/* Element Toolbar Buttons */}
|
{/* Blocks */}
|
||||||
<ToolbarButton onClick={() => editor.tf.h1.toggle()}>H1</ToolbarButton>
|
<ToolbarButton onClick={() => editor.tf.h1.toggle()} tooltip="Heading 1">
|
||||||
<ToolbarButton onClick={() => editor.tf.h2.toggle()}>H2</ToolbarButton>
|
<Heading1 className="w-4 h-4" />
|
||||||
<ToolbarButton onClick={() => editor.tf.h3.toggle()}>H3</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onClick={() => editor.tf.blockquote.toggle()}>Quote</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 */}
|
{/* Mark Toolbar Buttons */}
|
||||||
<MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">B</MarkToolbarButton>
|
<MarkToolbarButton nodeType="bold" tooltip="Bold">
|
||||||
<MarkToolbarButton nodeType="italic" tooltip="Italic (⌘+I)">I</MarkToolbarButton>
|
<Bold className="w-4 h-4" />
|
||||||
<MarkToolbarButton nodeType="underline" tooltip="Underline (⌘+U)">U</MarkToolbarButton>
|
</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>
|
</FixedToolbar>
|
||||||
<EditorContainer> {/* Styles the editor area */}
|
<EditorContainer> {/* Styles the editor area */}
|
||||||
<Editor placeholder="Type your amazing content here..." />
|
<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