Add ToS
This commit is contained in:
10
src/actions/tos/getTos.ts
Normal file
10
src/actions/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/tos/saveTos.ts
Normal file
11
src/actions/tos/saveTos.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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -44,6 +44,8 @@
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
--color-brand: var(--brand);
|
||||
--color-highlight: var(--highlight);
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
17
src/app/tos/page.tsx
Normal file
17
src/app/tos/page.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { getLatestTos } from "@/actions/tos/getTos";
|
||||
import TosEditor from "@/components/tos/Editor";
|
||||
|
||||
export default async function TosPage() {
|
||||
const markdown = await getLatestTos();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Terms of Service</h1>
|
||||
</div>
|
||||
<div className="space-y-4 p-1 border rounded-xl bg-muted/20">
|
||||
<TosEditor markdown={markdown} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/components/editor/plugins/basic-blocks-base-kit.tsx
Normal file
35
src/components/editor/plugins/basic-blocks-base-kit.tsx
Normal 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),
|
||||
];
|
||||
88
src/components/editor/plugins/basic-blocks-kit.tsx
Normal file
88
src/components/editor/plugins/basic-blocks-kit.tsx
Normal 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),
|
||||
];
|
||||
27
src/components/editor/plugins/basic-marks-base-kit.tsx
Normal file
27
src/components/editor/plugins/basic-marks-base-kit.tsx
Normal 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),
|
||||
];
|
||||
41
src/components/editor/plugins/basic-marks-kit.tsx
Normal file
41
src/components/editor/plugins/basic-marks-kit.tsx
Normal 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),
|
||||
];
|
||||
6
src/components/editor/plugins/basic-nodes-kit.tsx
Normal file
6
src/components/editor/plugins/basic-nodes-kit.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { BasicBlocksKit } from './basic-blocks-kit';
|
||||
import { BasicMarksKit } from './basic-marks-kit';
|
||||
|
||||
export const BasicNodesKit = [...BasicBlocksKit, ...BasicMarksKit];
|
||||
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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
22
src/components/editor/plugins/indent-kit.tsx
Normal file
22
src/components/editor/plugins/indent-kit.tsx
Normal 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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
26
src/components/editor/plugins/list-kit.tsx
Normal file
26
src/components/editor/plugins/list-kit.tsx
Normal 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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
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: {
|
||||
plainMarks: [KEYS.suggestion, KEYS.comment],
|
||||
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
|
||||
},
|
||||
}),
|
||||
];
|
||||
@ -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>
|
||||
|
||||
113
src/components/tos/Editor.tsx
Normal file
113
src/components/tos/Editor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
80
src/components/ui/block-list-static.tsx
Normal file
80
src/components/ui/block-list-static.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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(
|
||||
'-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>
|
||||
);
|
||||
}
|
||||
13
src/components/ui/blockquote-node-static.tsx
Normal file
13
src/components/ui/blockquote-node-static.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
src/components/ui/blockquote-node.tsx
Normal file
13
src/components/ui/blockquote-node.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal 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 }
|
||||
37
src/components/ui/code-block-node-static.tsx
Normal file
37
src/components/ui/code-block-node-static.tsx
Normal 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} />;
|
||||
}
|
||||
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 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' },
|
||||
];
|
||||
17
src/components/ui/code-node-static.tsx
Normal file
17
src/components/ui/code-node-static.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
src/components/ui/code-node.tsx
Normal file
19
src/components/ui/code-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
src/components/ui/editor-static.tsx
Normal file
55
src/components/ui/editor-static.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
132
src/components/ui/editor.tsx
Normal file
132
src/components/ui/editor.tsx
Normal 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';
|
||||
17
src/components/ui/fixed-toolbar.tsx
Normal file
17
src/components/ui/fixed-toolbar.tsx
Normal 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
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
68
src/components/ui/heading-node-static.tsx
Normal file
68
src/components/ui/heading-node-static.tsx
Normal 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} />;
|
||||
}
|
||||
60
src/components/ui/heading-node.tsx
Normal file
60
src/components/ui/heading-node.tsx
Normal 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} />;
|
||||
}
|
||||
13
src/components/ui/highlight-node-static.tsx
Normal file
13
src/components/ui/highlight-node-static.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/ui/highlight-node.tsx
Normal file
15
src/components/ui/highlight-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/components/ui/hr-node-static.tsx
Normal file
22
src/components/ui/hr-node-static.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
src/components/ui/hr-node.tsx
Normal file
35
src/components/ui/hr-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
17
src/components/ui/kbd-node-static.tsx
Normal file
17
src/components/ui/kbd-node-static.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
src/components/ui/kbd-node.tsx
Normal file
19
src/components/ui/kbd-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
21
src/components/ui/mark-toolbar-button.tsx
Normal file
21
src/components/ui/mark-toolbar-button.tsx
Normal 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} />;
|
||||
}
|
||||
15
src/components/ui/paragraph-node-static.tsx
Normal file
15
src/components/ui/paragraph-node-static.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
src/components/ui/paragraph-node.tsx
Normal file
17
src/components/ui/paragraph-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal 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 }
|
||||
389
src/components/ui/toolbar.tsx
Normal file
389
src/components/ui/toolbar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
61
src/components/ui/tooltip.tsx
Normal file
61
src/components/ui/tooltip.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user