This commit is contained in:
2025-12-27 11:14:07 +01:00
parent e9176ff73e
commit 13685d100c
51 changed files with 2685 additions and 1 deletions

View File

@ -0,0 +1,35 @@
import {
BaseBlockquotePlugin,
BaseH1Plugin,
BaseH2Plugin,
BaseH3Plugin,
BaseH4Plugin,
BaseH5Plugin,
BaseH6Plugin,
BaseHorizontalRulePlugin,
} from '@platejs/basic-nodes';
import { BaseParagraphPlugin } from 'platejs';
import { BlockquoteElementStatic } from '@/components/ui/blockquote-node-static';
import {
H1ElementStatic,
H2ElementStatic,
H3ElementStatic,
H4ElementStatic,
H5ElementStatic,
H6ElementStatic,
} from '@/components/ui/heading-node-static';
import { HrElementStatic } from '@/components/ui/hr-node-static';
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
export const BaseBasicBlocksKit = [
BaseParagraphPlugin.withComponent(ParagraphElementStatic),
BaseH1Plugin.withComponent(H1ElementStatic),
BaseH2Plugin.withComponent(H2ElementStatic),
BaseH3Plugin.withComponent(H3ElementStatic),
BaseH4Plugin.withComponent(H4ElementStatic),
BaseH5Plugin.withComponent(H5ElementStatic),
BaseH6Plugin.withComponent(H6ElementStatic),
BaseBlockquotePlugin.withComponent(BlockquoteElementStatic),
BaseHorizontalRulePlugin.withComponent(HrElementStatic),
];

View File

@ -0,0 +1,88 @@
'use client';
import {
BlockquotePlugin,
H1Plugin,
H2Plugin,
H3Plugin,
H4Plugin,
H5Plugin,
H6Plugin,
HorizontalRulePlugin,
} from '@platejs/basic-nodes/react';
import { ParagraphPlugin } from 'platejs/react';
import { BlockquoteElement } from '@/components/ui/blockquote-node';
import {
H1Element,
H2Element,
H3Element,
H4Element,
H5Element,
H6Element,
} from '@/components/ui/heading-node';
import { HrElement } from '@/components/ui/hr-node';
import { ParagraphElement } from '@/components/ui/paragraph-node';
export const BasicBlocksKit = [
ParagraphPlugin.withComponent(ParagraphElement),
H1Plugin.configure({
node: {
component: H1Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+1' } },
}),
H2Plugin.configure({
node: {
component: H2Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+2' } },
}),
H3Plugin.configure({
node: {
component: H3Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+3' } },
}),
H4Plugin.configure({
node: {
component: H4Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+4' } },
}),
H5Plugin.configure({
node: {
component: H5Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+5' } },
}),
H6Plugin.configure({
node: {
component: H6Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+6' } },
}),
BlockquotePlugin.configure({
node: { component: BlockquoteElement },
shortcuts: { toggle: { keys: 'mod+shift+period' } },
}),
HorizontalRulePlugin.withComponent(HrElement),
];

View File

@ -0,0 +1,27 @@
import {
BaseBoldPlugin,
BaseCodePlugin,
BaseHighlightPlugin,
BaseItalicPlugin,
BaseKbdPlugin,
BaseStrikethroughPlugin,
BaseSubscriptPlugin,
BaseSuperscriptPlugin,
BaseUnderlinePlugin,
} from '@platejs/basic-nodes';
import { CodeLeafStatic } from '@/components/ui/code-node-static';
import { HighlightLeafStatic } from '@/components/ui/highlight-node-static';
import { KbdLeafStatic } from '@/components/ui/kbd-node-static';
export const BaseBasicMarksKit = [
BaseBoldPlugin,
BaseItalicPlugin,
BaseUnderlinePlugin,
BaseCodePlugin.withComponent(CodeLeafStatic),
BaseStrikethroughPlugin,
BaseSubscriptPlugin,
BaseSuperscriptPlugin,
BaseHighlightPlugin.withComponent(HighlightLeafStatic),
BaseKbdPlugin.withComponent(KbdLeafStatic),
];

View File

@ -0,0 +1,41 @@
'use client';
import {
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
KbdPlugin,
StrikethroughPlugin,
SubscriptPlugin,
SuperscriptPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import { CodeLeaf } from '@/components/ui/code-node';
import { HighlightLeaf } from '@/components/ui/highlight-node';
import { KbdLeaf } from '@/components/ui/kbd-node';
export const BasicMarksKit = [
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin.configure({
node: { component: CodeLeaf },
shortcuts: { toggle: { keys: 'mod+e' } },
}),
StrikethroughPlugin.configure({
shortcuts: { toggle: { keys: 'mod+shift+x' } },
}),
SubscriptPlugin.configure({
shortcuts: { toggle: { keys: 'mod+comma' } },
}),
SuperscriptPlugin.configure({
shortcuts: { toggle: { keys: 'mod+period' } },
}),
HighlightPlugin.configure({
node: { component: HighlightLeaf },
shortcuts: { toggle: { keys: 'mod+shift+h' } },
}),
KbdPlugin.withComponent(KbdLeaf),
];

View File

@ -0,0 +1,6 @@
'use client';
import { BasicBlocksKit } from './basic-blocks-kit';
import { BasicMarksKit } from './basic-marks-kit';
export const BasicNodesKit = [...BasicBlocksKit, ...BasicMarksKit];

View 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),
];

View 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),
];

View 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,
},
}),
];

View File

@ -0,0 +1,22 @@
'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,
KEYS.img,
],
},
options: {
offset: 24,
},
}),
];

View 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,
},
}),
];

View File

@ -0,0 +1,26 @@
'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,
KEYS.img,
],
},
render: {
belowNodes: BlockList,
},
}),
];

View 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: {
plainMarks: [KEYS.suggestion, KEYS.comment],
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
},
}),
];

View File

@ -133,6 +133,12 @@ export default function TopNav() {
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/tos">Terms of Service</Link>
</NavigationMenuLink>
</NavigationMenuItem>
{/* <NavigationMenuItem>
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
<NavigationMenuContent>

View File

@ -0,0 +1,113 @@
"use client"
import type { Value } from 'platejs';
import { saveTosAction } from '@/actions/tos/saveTos';
import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit';
import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit';
import { ListKit } from '@/components/editor/plugins/list-kit';
import { MarkdownKit } from '@/components/editor/plugins/markdown-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
import { BulletedListToolbarButton, NumberedListToolbarButton } from '@/components/ui/list-toolbar-button';
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';
const initialValue: Value = [
];
export default function TosEditor({ markdown }: { markdown: string | null }) {
// const [isSaving, setIsSaving] = useState(false);
const editor = usePlateEditor({
plugins: [
...BasicBlocksKit,
...CodeBlockKit,
...ListKit,
...BasicMarksKit,
...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">
{/* 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">
<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..." />
</EditorContainer>
</Plate>
);
}

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import type { RenderStaticNodeWrapper, TListElement } from 'platejs';
import type { SlateRenderElementProps } from 'platejs/static';
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 -left-6 pointer-events-none absolute top-1 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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>
);
}

View 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(
'-left-6 absolute top-1',
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>
);
}

View File

@ -0,0 +1,13 @@
import * as React from 'react';
import { type SlateElementProps, SlateElement } from 'platejs/static';
export function BlockquoteElementStatic(props: SlateElementProps) {
return (
<SlateElement
as="blockquote"
className="my-1 border-l-2 pl-6 italic"
{...props}
/>
);
}

View File

@ -0,0 +1,13 @@
'use client';
import { type PlateElementProps, PlateElement } from 'platejs/react';
export function BlockquoteElement(props: PlateElementProps) {
return (
<PlateElement
as="blockquote"
className="my-1 border-l-2 pl-6 italic"
{...props}
/>
);
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,37 @@
import * as React from 'react';
import type { TCodeBlockElement } from 'platejs';
import {
type SlateElementProps,
type SlateLeafProps,
SlateElement,
SlateLeaf,
} from 'platejs/static';
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} />;
}

View 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 select-none gap-0.5"
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-muted-foreground text-xs"
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 select-none justify-between gap-1 px-2 text-muted-foreground text-xs"
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' },
];

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import type { SlateLeafProps } from 'platejs/static';
import { SlateLeaf } from 'platejs/static';
export function CodeLeafStatic(props: SlateLeafProps) {
return (
<SlateLeaf
{...props}
as="code"
className="whitespace-pre-wrap rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm"
>
{props.children}
</SlateLeaf>
);
}

View File

@ -0,0 +1,19 @@
'use client';
import * as React from 'react';
import type { PlateLeafProps } from 'platejs/react';
import { PlateLeaf } from 'platejs/react';
export function CodeLeaf(props: PlateLeafProps) {
return (
<PlateLeaf
{...props}
as="code"
className="whitespace-pre-wrap rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm"
>
{props.children}
</PlateLeaf>
);
}

View File

@ -0,0 +1,55 @@
import * as React from 'react';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { type PlateStaticProps, PlateStatic } from 'platejs/static';
import { cn } from '@/lib/utils';
export const editorVariants = cva(
cn(
'group/editor',
'relative w-full cursor-text select-text overflow-x-hidden whitespace-pre-wrap break-words',
'rounded-md ring-offset-background focus-visible:outline-none',
'placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
'[&_strong]:font-bold'
),
{
defaultVariants: {
variant: 'none',
},
variants: {
disabled: {
true: 'cursor-not-allowed opacity-50',
},
focused: {
true: 'ring-2 ring-ring ring-offset-2',
},
variant: {
ai: 'w-full px-0 text-base md:text-sm',
aiChat:
'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm',
default:
'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24',
none: '',
select: 'px-3 py-2 text-base data-readonly:w-fit',
},
},
}
);
export function EditorStatic({
className,
variant,
...props
}: PlateStaticProps & VariantProps<typeof editorVariants>) {
return (
<PlateStatic
className={cn(editorVariants({ variant }), className)}
{...props}
/>
);
}

View File

@ -0,0 +1,132 @@
'use client';
import * as React from 'react';
import type { VariantProps } from 'class-variance-authority';
import type { PlateContentProps, PlateViewProps } from 'platejs/react';
import { cva } from 'class-variance-authority';
import { PlateContainer, PlateContent, PlateView } from 'platejs/react';
import { cn } from '@/lib/utils';
const editorContainerVariants = cva(
'relative w-full cursor-text select-text overflow-y-auto caret-primary selection:bg-brand/25 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
{
defaultVariants: {
variant: 'default',
},
variants: {
variant: {
comment: cn(
'flex flex-wrap justify-between gap-1 px-1 py-0.5 text-sm',
'rounded-md border-[1.5px] border-transparent bg-transparent',
'has-[[data-slate-editor]:focus]:border-brand/50 has-[[data-slate-editor]:focus]:ring-2 has-[[data-slate-editor]:focus]:ring-brand/30',
'has-aria-disabled:border-input has-aria-disabled:bg-muted'
),
default: 'h-full',
demo: 'h-[650px]',
select: cn(
'group rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
'has-data-readonly:w-fit has-data-readonly:cursor-default has-data-readonly:border-transparent has-data-readonly:focus-within:[box-shadow:none]'
),
},
},
}
);
export function EditorContainer({
className,
variant,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof editorContainerVariants>) {
return (
<PlateContainer
className={cn(
'ignore-click-outside/toolbar',
editorContainerVariants({ variant }),
className
)}
{...props}
/>
);
}
const editorVariants = cva(
cn(
'group/editor',
'relative w-full cursor-text select-text overflow-x-hidden whitespace-pre-wrap break-words',
'rounded-md ring-offset-background focus-visible:outline-none',
'**:data-slate-placeholder:!top-1/2 **:data-slate-placeholder:-translate-y-1/2 placeholder:text-muted-foreground/80 **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
'[&_strong]:font-bold'
),
{
defaultVariants: {
variant: 'default',
},
variants: {
disabled: {
true: 'cursor-not-allowed opacity-50',
},
focused: {
true: 'ring-2 ring-ring ring-offset-2',
},
variant: {
ai: 'w-full px-0 text-base md:text-sm',
aiChat:
'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm',
comment: cn('rounded-none border-none bg-transparent text-sm'),
default:
'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24',
none: '',
select: 'px-3 py-2 text-base data-readonly:w-fit',
},
},
}
);
export type EditorProps = PlateContentProps &
VariantProps<typeof editorVariants>;
export const Editor = ({
className,
disabled,
focused,
variant,
ref,
...props
}: EditorProps & { ref?: React.RefObject<HTMLDivElement | null> }) => (
<PlateContent
ref={ref}
className={cn(
editorVariants({
disabled,
focused,
variant,
}),
className
)}
disabled={disabled}
disableDefaultStyles
{...props}
/>
);
Editor.displayName = 'Editor';
export function EditorView({
className,
variant,
...props
}: PlateViewProps & VariantProps<typeof editorVariants>) {
return (
<PlateView
{...props}
className={cn(editorVariants({ variant }), className)}
/>
);
}
EditorView.displayName = 'EditorView';

View File

@ -0,0 +1,17 @@
'use client';
import { cn } from '@/lib/utils';
import { Toolbar } from './toolbar';
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
className={cn(
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
props.className
)}
/>
);
}

View File

@ -0,0 +1,68 @@
import * as React from 'react';
import type { SlateElementProps } from 'platejs/static';
import { type VariantProps, cva } from 'class-variance-authority';
import { SlateElement } from 'platejs/static';
const headingVariants = cva('relative mb-1', {
variants: {
variant: {
h1: 'mt-[1.6em] pb-1 font-bold font-heading text-4xl',
h2: 'mt-[1.4em] pb-px font-heading font-semibold text-2xl tracking-tight',
h3: 'mt-[1em] pb-px font-heading font-semibold text-xl tracking-tight',
h4: 'mt-[0.75em] font-heading font-semibold text-lg tracking-tight',
h5: 'mt-[0.75em] font-semibold text-lg tracking-tight',
h6: 'mt-[0.75em] font-semibold text-base tracking-tight',
},
},
});
export function HeadingElementStatic({
variant = 'h1',
...props
}: SlateElementProps & VariantProps<typeof headingVariants>) {
return (
<SlateElement
as={variant!}
className={headingVariants({ variant })}
{...props}
>
{props.children}
</SlateElement>
);
}
export function H1ElementStatic(props: SlateElementProps) {
return <HeadingElementStatic variant="h1" {...props} />;
}
export function H2ElementStatic(
props: React.ComponentProps<typeof HeadingElementStatic>
) {
return <HeadingElementStatic variant="h2" {...props} />;
}
export function H3ElementStatic(
props: React.ComponentProps<typeof HeadingElementStatic>
) {
return <HeadingElementStatic variant="h3" {...props} />;
}
export function H4ElementStatic(
props: React.ComponentProps<typeof HeadingElementStatic>
) {
return <HeadingElementStatic variant="h4" {...props} />;
}
export function H5ElementStatic(
props: React.ComponentProps<typeof HeadingElementStatic>
) {
return <HeadingElementStatic variant="h5" {...props} />;
}
export function H6ElementStatic(
props: React.ComponentProps<typeof HeadingElementStatic>
) {
return <HeadingElementStatic variant="h6" {...props} />;
}

View File

@ -0,0 +1,60 @@
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import { type VariantProps, cva } from 'class-variance-authority';
import { PlateElement } from 'platejs/react';
const headingVariants = cva('relative mb-1', {
variants: {
variant: {
h1: 'mt-[1.6em] pb-1 font-bold font-heading text-4xl',
h2: 'mt-[1.4em] pb-px font-heading font-semibold text-2xl tracking-tight',
h3: 'mt-[1em] pb-px font-heading font-semibold text-xl tracking-tight',
h4: 'mt-[0.75em] font-heading font-semibold text-lg tracking-tight',
h5: 'mt-[0.75em] font-semibold text-lg tracking-tight',
h6: 'mt-[0.75em] font-semibold text-base tracking-tight',
},
},
});
export function HeadingElement({
variant = 'h1',
...props
}: PlateElementProps & VariantProps<typeof headingVariants>) {
return (
<PlateElement
as={variant!}
className={headingVariants({ variant })}
{...props}
>
{props.children}
</PlateElement>
);
}
export function H1Element(props: PlateElementProps) {
return <HeadingElement variant="h1" {...props} />;
}
export function H2Element(props: PlateElementProps) {
return <HeadingElement variant="h2" {...props} />;
}
export function H3Element(props: PlateElementProps) {
return <HeadingElement variant="h3" {...props} />;
}
export function H4Element(props: PlateElementProps) {
return <HeadingElement variant="h4" {...props} />;
}
export function H5Element(props: PlateElementProps) {
return <HeadingElement variant="h5" {...props} />;
}
export function H6Element(props: PlateElementProps) {
return <HeadingElement variant="h6" {...props} />;
}

View File

@ -0,0 +1,13 @@
import * as React from 'react';
import type { SlateLeafProps } from 'platejs/static';
import { SlateLeaf } from 'platejs/static';
export function HighlightLeafStatic(props: SlateLeafProps) {
return (
<SlateLeaf {...props} as="mark" className="bg-highlight/30 text-inherit">
{props.children}
</SlateLeaf>
);
}

View File

@ -0,0 +1,15 @@
'use client';
import * as React from 'react';
import type { PlateLeafProps } from 'platejs/react';
import { PlateLeaf } from 'platejs/react';
export function HighlightLeaf(props: PlateLeafProps) {
return (
<PlateLeaf {...props} as="mark" className="bg-highlight/30 text-inherit">
{props.children}
</PlateLeaf>
);
}

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import type { SlateElementProps } from 'platejs/static';
import { SlateElement } from 'platejs/static';
import { cn } from '@/lib/utils';
export function HrElementStatic(props: SlateElementProps) {
return (
<SlateElement {...props}>
<div className="cursor-text py-6" contentEditable={false}>
<hr
className={cn(
'h-0.5 rounded-sm border-none bg-muted bg-clip-content'
)}
/>
</div>
{props.children}
</SlateElement>
);
}

View File

@ -0,0 +1,35 @@
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import {
PlateElement,
useFocused,
useReadOnly,
useSelected,
} from 'platejs/react';
import { cn } from '@/lib/utils';
export function HrElement(props: PlateElementProps) {
const readOnly = useReadOnly();
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement {...props}>
<div className="py-6" contentEditable={false}>
<hr
className={cn(
'h-0.5 rounded-sm border-none bg-muted bg-clip-content',
selected && focused && 'ring-2 ring-ring ring-offset-2',
!readOnly && 'cursor-pointer'
)}
/>
</div>
{props.children}
</PlateElement>
);
}

View 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>
);
}

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import type { SlateLeafProps } from 'platejs/static';
import { SlateLeaf } from 'platejs/static';
export function KbdLeafStatic(props: SlateLeafProps) {
return (
<SlateLeaf
{...props}
as="kbd"
className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-sm shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(248,_249,_250)_0px_1px_5px_0px_inset,_rgb(193,_200,_205)_0px_0px_0px_0.5px,_rgb(193,_200,_205)_0px_2px_1px_-1px,_rgb(193,_200,_205)_0px_1px_0px_0px] dark:shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(26,_29,_30)_0px_1px_5px_0px_inset,_rgb(76,_81,_85)_0px_0px_0px_0.5px,_rgb(76,_81,_85)_0px_2px_1px_-1px,_rgb(76,_81,_85)_0px_1px_0px_0px]"
>
{props.children}
</SlateLeaf>
);
}

View File

@ -0,0 +1,19 @@
'use client';
import * as React from 'react';
import type { PlateLeafProps } from 'platejs/react';
import { PlateLeaf } from 'platejs/react';
export function KbdLeaf(props: PlateLeafProps) {
return (
<PlateLeaf
{...props}
as="kbd"
className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-sm shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(248,_249,_250)_0px_1px_5px_0px_inset,_rgb(193,_200,_205)_0px_0px_0px_0.5px,_rgb(193,_200,_205)_0px_2px_1px_-1px,_rgb(193,_200,_205)_0px_1px_0px_0px] dark:shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(26,_29,_30)_0px_1px_5px_0px_inset,_rgb(76,_81,_85)_0px_0px_0px_0.5px,_rgb(76,_81,_85)_0px_2px_1px_-1px,_rgb(76,_81,_85)_0px_1px_0px_0px]"
>
{props.children}
</PlateLeaf>
);
}

View 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>
);
}

View File

@ -0,0 +1,21 @@
'use client';
import * as React from 'react';
import { useMarkToolbarButton, useMarkToolbarButtonState } from 'platejs/react';
import { ToolbarButton } from './toolbar';
export function MarkToolbarButton({
clear,
nodeType,
...props
}: React.ComponentProps<typeof ToolbarButton> & {
nodeType: string;
clear?: string[] | string;
}) {
const state = useMarkToolbarButtonState({ clear, nodeType });
const { props: buttonProps } = useMarkToolbarButton(state);
return <ToolbarButton {...props} {...buttonProps} />;
}

View File

@ -0,0 +1,15 @@
import * as React from 'react';
import type { SlateElementProps } from 'platejs/static';
import { SlateElement } from 'platejs/static';
import { cn } from '@/lib/utils';
export function ParagraphElementStatic(props: SlateElementProps) {
return (
<SlateElement {...props} className={cn('m-0 px-0 py-1')}>
{props.children}
</SlateElement>
);
}

View File

@ -0,0 +1,17 @@
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import { PlateElement } from 'platejs/react';
import { cn } from '@/lib/utils';
export function ParagraphElement(props: PlateElementProps) {
return (
<PlateElement {...props} className={cn('m-0 px-0 py-1')}>
{props.children}
</PlateElement>
);
}

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@ -0,0 +1,389 @@
'use client';
import * as React from 'react';
import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { type VariantProps, cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
import {
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
export function Toolbar({
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.Root>) {
return (
<ToolbarPrimitive.Root
className={cn('relative flex select-none items-center', className)}
{...props}
/>
);
}
export function ToolbarToggleGroup({
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.ToolbarToggleGroup>) {
return (
<ToolbarPrimitive.ToolbarToggleGroup
className={cn('flex items-center', className)}
{...props}
/>
);
}
export function ToolbarLink({
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.Link>) {
return (
<ToolbarPrimitive.Link
className={cn('font-medium underline underline-offset-4', className)}
{...props}
/>
);
}
export function ToolbarSeparator({
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.Separator>) {
return (
<ToolbarPrimitive.Separator
className={cn('mx-2 my-1 w-px shrink-0 bg-border', className)}
{...props}
/>
);
}
// From toggleVariants
const toolbarButtonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-checked:bg-accent aria-checked:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
defaultVariants: {
size: 'default',
variant: 'default',
},
variants: {
size: {
default: 'h-9 min-w-9 px-2',
lg: 'h-10 min-w-10 px-2.5',
sm: 'h-8 min-w-8 px-1.5',
},
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
},
},
}
);
const dropdownArrowVariants = cva(
cn(
'inline-flex items-center justify-center rounded-r-md font-medium text-foreground text-sm transition-colors disabled:pointer-events-none disabled:opacity-50'
),
{
defaultVariants: {
size: 'sm',
variant: 'default',
},
variants: {
size: {
default: 'h-9 w-6',
lg: 'h-10 w-8',
sm: 'h-8 w-4',
},
variant: {
default:
'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',
outline:
'border border-input border-l-0 bg-transparent hover:bg-accent hover:text-accent-foreground',
},
},
}
);
type ToolbarButtonProps = {
isDropdown?: boolean;
pressed?: boolean;
} & Omit<
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
'asChild' | 'value'
> &
VariantProps<typeof toolbarButtonVariants>;
export const ToolbarButton = withTooltip(function ToolbarButton({
children,
className,
isDropdown,
pressed,
size = 'sm',
variant,
...props
}: ToolbarButtonProps) {
return typeof pressed === 'boolean' ? (
<ToolbarToggleGroup disabled={props.disabled} value="single" type="single">
<ToolbarToggleItem
className={cn(
toolbarButtonVariants({
size,
variant,
}),
isDropdown && 'justify-between gap-1 pr-1',
className
)}
value={pressed ? 'single' : ''}
{...props}
>
{isDropdown ? (
<>
<div className="flex flex-1 items-center gap-2 whitespace-nowrap">
{children}
</div>
<div>
<ChevronDown
className="size-3.5 text-muted-foreground"
data-icon
/>
</div>
</>
) : (
children
)}
</ToolbarToggleItem>
</ToolbarToggleGroup>
) : (
<ToolbarPrimitive.Button
className={cn(
toolbarButtonVariants({
size,
variant,
}),
isDropdown && 'pr-1',
className
)}
{...props}
>
{children}
</ToolbarPrimitive.Button>
);
});
export function ToolbarSplitButton({
className,
...props
}: React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
return (
<ToolbarButton
className={cn('group flex gap-0 px-0 hover:bg-transparent', className)}
{...props}
/>
);
}
type ToolbarSplitButtonPrimaryProps = Omit<
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
'value'
> &
VariantProps<typeof toolbarButtonVariants>;
export function ToolbarSplitButtonPrimary({
children,
className,
size = 'sm',
variant,
...props
}: ToolbarSplitButtonPrimaryProps) {
return (
<span
className={cn(
toolbarButtonVariants({
size,
variant,
}),
'rounded-r-none',
'group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground',
className
)}
{...props}
>
{children}
</span>
);
}
export function ToolbarSplitButtonSecondary({
className,
size,
variant,
...props
}: React.ComponentPropsWithoutRef<'span'> &
VariantProps<typeof dropdownArrowVariants>) {
return (
<span
className={cn(
dropdownArrowVariants({
size,
variant,
}),
'group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground',
className
)}
onClick={(e) => e.stopPropagation()}
role="button"
{...props}
>
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
</span>
);
}
export function ToolbarToggleItem({
className,
size = 'sm',
variant,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.ToggleItem> &
VariantProps<typeof toolbarButtonVariants>) {
return (
<ToolbarPrimitive.ToggleItem
className={cn(toolbarButtonVariants({ size, variant }), className)}
{...props}
/>
);
}
export function ToolbarGroup({
children,
className,
}: React.ComponentProps<'div'>) {
return (
<div
className={cn(
'group/toolbar-group',
'relative hidden has-[button]:flex',
className
)}
>
<div className="flex items-center">{children}</div>
<div className="group-last/toolbar-group:hidden! mx-1.5 py-0.5">
<Separator orientation="vertical" />
</div>
</div>
);
}
type TooltipProps<T extends React.ElementType> = {
tooltip?: React.ReactNode;
tooltipContentProps?: Omit<
React.ComponentPropsWithoutRef<typeof TooltipContent>,
'children'
>;
tooltipProps?: Omit<
React.ComponentPropsWithoutRef<typeof Tooltip>,
'children'
>;
tooltipTriggerProps?: React.ComponentPropsWithoutRef<typeof TooltipTrigger>;
} & React.ComponentProps<T>;
function withTooltip<T extends React.ElementType>(Component: T) {
return function ExtendComponent({
tooltip,
tooltipContentProps,
tooltipProps,
tooltipTriggerProps,
...props
}: TooltipProps<T>) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
const component = <Component {...(props as React.ComponentProps<T>)} />;
if (tooltip && mounted) {
return (
<Tooltip {...tooltipProps}>
<TooltipTrigger asChild {...tooltipTriggerProps}>
{component}
</TooltipTrigger>
<TooltipContent {...tooltipContentProps}>{tooltip}</TooltipContent>
</Tooltip>
);
}
return component;
};
}
function TooltipContent({
children,
className,
// CHANGE
sideOffset = 4,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
className={cn(
'z-50 w-fit origin-(--radix-tooltip-content-transform-origin) text-balance rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-xs',
className
)}
data-slot="tooltip-content"
sideOffset={sideOffset}
{...props}
>
{children}
{/* CHANGE */}
{/* <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" /> */}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export function ToolbarMenuGroup({
children,
className,
label,
...props
}: React.ComponentProps<typeof DropdownMenuRadioGroup> & { label?: string }) {
return (
<>
<DropdownMenuSeparator
className={cn(
'hidden',
'mb-0 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block'
)}
/>
<DropdownMenuRadioGroup
{...props}
className={cn(
'hidden',
'peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemradio]]:block has-[[role=option]]:block',
className
)}
>
{label && (
<DropdownMenuLabel className="select-none font-semibold text-muted-foreground text-xs">
{label}
</DropdownMenuLabel>
)}
{children}
</DropdownMenuRadioGroup>
</>
);
}

View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }