-
+ {category === 'guide' ? (
+
+ ) : (
+
+
+
+ )}
diff --git a/apps/www/src/app/(app)/docs/examples/server/page.tsx b/apps/www/src/app/(app)/docs/examples/server-side/page.tsx
similarity index 97%
rename from apps/www/src/app/(app)/docs/examples/server/page.tsx
rename to apps/www/src/app/(app)/docs/examples/server-side/page.tsx
index 61fd6b2b09..6fc335392e 100644
--- a/apps/www/src/app/(app)/docs/examples/server/page.tsx
+++ b/apps/www/src/app/(app)/docs/examples/server-side/page.tsx
@@ -60,16 +60,11 @@ import { Code } from '@/components/code';
import { Link } from '@/components/link';
import { Markdown } from '@/components/markdown';
import { H2, H3, P } from '@/components/typography';
+import { exampleNavMap } from '@/config/docs-examples';
import { basicElementsValue } from '@/registry/default/example/values/basic-elements-value';
import { basicMarksValue } from '@/registry/default/example/values/basic-marks-value';
export default function RSCPage() {
- const mockDoc = {
- description: 'Server-side rendering.',
- title: 'Server-Side',
- // ... other necessary properties
- };
-
const editor = createSlateEditor({
plugins: [
BaseHeadingPlugin,
@@ -238,7 +233,11 @@ export default function RSCPage() {
});
return (
-
+
Using Plate in a Server Environment
Plate can be utilized in server-side environments, enabling operations
diff --git a/apps/www/src/components/component-installation.tsx b/apps/www/src/components/component-installation.tsx
index e3f2e8f098..e68fc1209d 100644
--- a/apps/www/src/components/component-installation.tsx
+++ b/apps/www/src/components/component-installation.tsx
@@ -22,6 +22,7 @@ interface ComponentInstallationProps {
dependencies?: string[];
examples?: RegistryEntry[];
files?: any[];
+ inline?: boolean;
name?: string;
usage?: string[];
}
@@ -33,6 +34,7 @@ export function ComponentInstallation({
__previewFiles__ = '[]',
codeTabs,
examples,
+ inline,
name,
usage,
...props
@@ -112,7 +114,7 @@ export function ComponentInstallation({
return (
-
Installation
+ {!inline &&
Installation
}
diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts
index 07980e4539..9b21c1cc78 100644
--- a/apps/www/src/config/customizer-items.ts
+++ b/apps/www/src/config/customizer-items.ts
@@ -1257,7 +1257,7 @@ export const customizerItems: Record = {
npmPackage: '@udecode/plate-placeholder',
pluginFactory: 'PlaceholderPlugin',
reactImport: true,
- route: getPluginNavItem('media-placeholder').href,
+ route: getPluginNavItem('media').href,
},
};
diff --git a/apps/www/src/config/docs-examples.ts b/apps/www/src/config/docs-examples.ts
index cf10d45138..805d8a966b 100644
--- a/apps/www/src/config/docs-examples.ts
+++ b/apps/www/src/config/docs-examples.ts
@@ -39,7 +39,7 @@ export const docsExamples: SidebarNavItem[] = [
},
{
description: 'Server-side rendering.',
- href: '/docs/examples/server',
+ href: '/docs/examples/server-side',
title: 'Server-Side',
},
...registryToNav(
diff --git a/apps/www/src/config/docs-icons.tsx b/apps/www/src/config/docs-icons.tsx
index e7bf814bef..61887db04b 100644
--- a/apps/www/src/config/docs-icons.tsx
+++ b/apps/www/src/config/docs-icons.tsx
@@ -21,7 +21,6 @@ import {
Columns3Icon,
ColumnsIcon,
CommandIcon,
- CopyIcon,
CornerDownLeftIcon,
DockIcon,
Edit3Icon,
@@ -141,7 +140,6 @@ export const DocIcons = {
comments: MessageSquareTextIcon,
'comments-popover': MessageSquareTextIcon,
'context-menu': MousePointerClickIcon,
- controlled: CopyIcon,
copilot: ArrowRightToLineIcon,
csv: FileSpreadsheetIcon,
'cursor-overlay': TextCursorInputIcon,
@@ -210,7 +208,6 @@ export const DocIcons = {
'media-audio-element': AudioLinesIcon,
'media-embed-element': DockIcon,
'media-file-element': FileIcon,
- 'media-placeholder': SquareDashedIcon,
'media-placeholder-element': SquareDashedIcon,
'media-popover': ImageIcon,
'media-toolbar-button': ImageIcon,
@@ -233,7 +230,7 @@ export const DocIcons = {
resizable: ProportionsIcon,
'search-highlight-leaf': HighlighterIcon,
separator: SeparatorHorizontalIcon,
- server: ServerIcon,
+ 'server-side': ServerIcon,
'single-line': RectangleHorizontalIcon,
'slash-command': SlashIcon,
'slash-input-element': SlashIcon,
@@ -259,6 +256,8 @@ export const DocIcons = {
};
export const getDocIcon = (item: SidebarNavItem, category?: string) => {
+ if (category === 'guide') return null;
+
const icon = item.icon ?? item.href?.split('/').pop();
return (DocIcons as any)[icon!] ?? (category === 'api' ? CodeXmlIcon : null);
diff --git a/apps/www/src/config/docs-plugins.ts b/apps/www/src/config/docs-plugins.ts
index 70a348bc4d..2b566430c9 100644
--- a/apps/www/src/config/docs-plugins.ts
+++ b/apps/www/src/config/docs-plugins.ts
@@ -35,12 +35,6 @@ export const pluginsNavItems: SidebarNavItem[] = [
label: 'New',
title: 'Equation',
},
- {
- description: 'A placeholder for media upload with progress indication.',
- href: '/docs/media-placeholder',
- label: 'New',
- title: 'Media Placeholder',
- },
{
description:
'Slash command menu for quick insertion of various content types.',
@@ -197,7 +191,7 @@ export const pluginsNavItems: SidebarNavItem[] = [
{
description: 'Embed medias like videos or tweets into your document.',
href: '/docs/media',
- label: 'Element',
+ label: ['Element', 'New'],
title: 'Media',
},
{
diff --git a/apps/www/src/registry/default/components/api/ai/command/route.ts b/apps/www/src/registry/default/app/api/ai/command/route.ts
similarity index 100%
rename from apps/www/src/registry/default/components/api/ai/command/route.ts
rename to apps/www/src/registry/default/app/api/ai/command/route.ts
diff --git a/apps/www/src/registry/default/components/api/ai/copilot/route.ts b/apps/www/src/registry/default/app/api/ai/copilot/route.ts
similarity index 100%
rename from apps/www/src/registry/default/components/api/ai/copilot/route.ts
rename to apps/www/src/registry/default/app/api/ai/copilot/route.ts
diff --git a/apps/www/src/registry/default/app/api/uploadthing/route.ts b/apps/www/src/registry/default/app/api/uploadthing/route.ts
new file mode 100644
index 0000000000..f3d0c2d5b3
--- /dev/null
+++ b/apps/www/src/registry/default/app/api/uploadthing/route.ts
@@ -0,0 +1,21 @@
+import type { FileRouter } from 'uploadthing/next';
+
+import { createRouteHandler, createUploadthing } from 'uploadthing/next';
+
+const f = createUploadthing();
+
+const ourFileRouter = {
+ editorUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio'])
+ .middleware(() => {
+ return {};
+ })
+ .onUploadComplete(({ file }) => {
+ return { file };
+ }),
+} satisfies FileRouter;
+
+export type OurFileRouter = typeof ourFileRouter;
+
+export const { GET, POST } = createRouteHandler({
+ router: ourFileRouter,
+});
diff --git a/apps/www/src/registry/default/block/editor-ai/components/editor/plate-editor.tsx b/apps/www/src/registry/default/block/editor-ai/components/editor/plate-editor.tsx
index d6399d884b..d07f3e4f79 100644
--- a/apps/www/src/registry/default/block/editor-ai/components/editor/plate-editor.tsx
+++ b/apps/www/src/registry/default/block/editor-ai/components/editor/plate-editor.tsx
@@ -7,7 +7,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { Plate } from '@udecode/plate-common/react';
import { useCreateEditor } from '@/registry/default/block/editor-ai/components/editor/use-create-editor';
-import { SettingsDialog } from '@/registry/default/components/editor/use-chat';
+import { SettingsDialog } from '@/registry/default/components/editor/settings';
import { Editor, EditorContainer } from '@/registry/default/plate-ui/editor';
export function PlateEditor() {
diff --git a/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor-list.tsx b/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor-list.ts
similarity index 100%
rename from apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor-list.tsx
rename to apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor-list.ts
diff --git a/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.tsx b/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts
similarity index 91%
rename from apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.tsx
rename to apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts
index ca3a8a3a21..95cf5f8276 100644
--- a/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.tsx
+++ b/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts
@@ -31,7 +31,14 @@ import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import { KbdPlugin } from '@udecode/plate-kbd/react';
import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';
import { LinkPlugin } from '@udecode/plate-link/react';
-import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
+import {
+ AudioPlugin,
+ FilePlugin,
+ ImagePlugin,
+ MediaEmbedPlugin,
+ PlaceholderPlugin,
+ VideoPlugin,
+} from '@udecode/plate-media/react';
import {
MentionInputPlugin,
MentionPlugin,
@@ -67,7 +74,11 @@ import { HrElement } from '@/registry/default/plate-ui/hr-element';
import { ImageElement } from '@/registry/default/plate-ui/image-element';
import { KbdLeaf } from '@/registry/default/plate-ui/kbd-leaf';
import { LinkElement } from '@/registry/default/plate-ui/link-element';
+import { MediaAudioElement } from '@/registry/default/plate-ui/media-audio-element';
import { MediaEmbedElement } from '@/registry/default/plate-ui/media-embed-element';
+import { MediaFileElement } from '@/registry/default/plate-ui/media-file-element';
+import { MediaPlaceholderElement } from '@/registry/default/plate-ui/media-placeholder-element';
+import { MediaVideoElement } from '@/registry/default/plate-ui/media-video-element';
import { MentionElement } from '@/registry/default/plate-ui/mention-element';
import { MentionInputElement } from '@/registry/default/plate-ui/mention-input-element';
import { ParagraphElement } from '@/registry/default/plate-ui/paragraph-element';
@@ -89,6 +100,7 @@ export const useCreateEditor = () => {
components: withDraggables(
withPlaceholders({
[AIPlugin.key]: AILeaf,
+ [AudioPlugin.key]: MediaAudioElement,
[BlockquotePlugin.key]: BlockquoteElement,
[BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
[CodeBlockPlugin.key]: CodeBlockElement,
@@ -101,6 +113,7 @@ export const useCreateEditor = () => {
[DatePlugin.key]: DateElement,
[EmojiInputPlugin.key]: EmojiInputElement,
[ExcalidrawPlugin.key]: ExcalidrawElement,
+ [FilePlugin.key]: MediaFileElement,
[HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
[HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
[HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
@@ -117,6 +130,7 @@ export const useCreateEditor = () => {
[MentionInputPlugin.key]: MentionInputElement,
[MentionPlugin.key]: MentionElement,
[ParagraphPlugin.key]: ParagraphElement,
+ [PlaceholderPlugin.key]: MediaPlaceholderElement,
[SlashInputPlugin.key]: SlashInputElement,
[StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),
[SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),
@@ -128,6 +142,7 @@ export const useCreateEditor = () => {
[TocPlugin.key]: TocElement,
[TogglePlugin.key]: ToggleElement,
[UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),
+ [VideoPlugin.key]: MediaVideoElement,
})
),
},
diff --git a/apps/www/src/registry/default/block/editor-ai/page.tsx b/apps/www/src/registry/default/block/editor-ai/page.tsx
index dac2f3d526..7b88f3790f 100644
--- a/apps/www/src/registry/default/block/editor-ai/page.tsx
+++ b/apps/www/src/registry/default/block/editor-ai/page.tsx
@@ -1,5 +1,7 @@
+import { Toaster } from 'sonner';
+
import { PlateEditor } from '@/registry/default/block/editor-ai/components/editor/plate-editor';
-import { OpenAIProvider } from '@/registry/default/components/editor/use-chat';
+import { SettingsProvider } from '@/registry/default/components/editor/settings';
export const description = 'An AI editor';
@@ -10,9 +12,11 @@ export const containerClassName = 'w-full h-full';
export default function Page() {
return (
);
}
diff --git a/apps/www/src/registry/default/block/editor-basic/components/editor/use-create-editor.tsx b/apps/www/src/registry/default/block/editor-basic/components/editor/use-create-editor.ts
similarity index 100%
rename from apps/www/src/registry/default/block/editor-basic/components/editor/use-create-editor.tsx
rename to apps/www/src/registry/default/block/editor-basic/components/editor/use-create-editor.ts
diff --git a/apps/www/src/registry/default/components/api/uploadthing/route.ts b/apps/www/src/registry/default/components/api/uploadthing/route.ts
deleted file mode 100644
index d19e1ed2f3..0000000000
--- a/apps/www/src/registry/default/components/api/uploadthing/route.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { FileRouter } from 'uploadthing/next';
-
-import { createRouteHandler, createUploadthing } from 'uploadthing/next';
-
-const f = createUploadthing();
-
-// FileRouter for your app, can contain multiple FileRoutes
-const ourFileRouter = {
- // Define as many FileRoutes as you like, each with a unique routeSlug
- imageUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio'])
- // Set permissions and file types for this FileRoute
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await
- .middleware(async ({ req }) => {
- // This code runs on your server before upload
-
- // Whatever is returned here is accessible in onUploadComplete as `metadata`
- return {};
- })
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- .onUploadComplete(({ file, metadata }) => {
- // This code RUNS ON YOUR SERVER after upload
-
- // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
- return { file };
- }),
-} satisfies FileRouter;
-
-export type OurFileRouter = typeof ourFileRouter;
-
-// Export routes for Next App Router
-export const { GET, POST } = createRouteHandler({
- router: ourFileRouter,
-
- // Apply an (optional) custom config:
- // config: { ... },
-});
diff --git a/apps/www/src/registry/default/components/editor/plugins/delete-plugins.ts b/apps/www/src/registry/default/components/editor/plugins/delete-plugins.ts
index 947137bc50..ff384acbcd 100644
--- a/apps/www/src/registry/default/components/editor/plugins/delete-plugins.ts
+++ b/apps/www/src/registry/default/components/editor/plugins/delete-plugins.ts
@@ -1,7 +1,13 @@
'use client';
import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
+import {
+ AudioPlugin,
+ FilePlugin,
+ ImagePlugin,
+ MediaEmbedPlugin,
+ VideoPlugin,
+} from '@udecode/plate-media/react';
import { DeletePlugin, SelectOnBackspacePlugin } from '@udecode/plate-select';
export const deletePlugins = [
@@ -10,6 +16,9 @@ export const deletePlugins = [
query: {
allow: [
ImagePlugin.key,
+ VideoPlugin.key,
+ AudioPlugin.key,
+ FilePlugin.key,
MediaEmbedPlugin.key,
HorizontalRulePlugin.key,
],
diff --git a/apps/www/src/registry/default/components/editor/plugins/media-plugins.tsx b/apps/www/src/registry/default/components/editor/plugins/media-plugins.tsx
index c7dc1469b4..66d57045d2 100644
--- a/apps/www/src/registry/default/components/editor/plugins/media-plugins.tsx
+++ b/apps/www/src/registry/default/components/editor/plugins/media-plugins.tsx
@@ -23,7 +23,15 @@ export const mediaPlugins = [
AudioPlugin,
FilePlugin,
CaptionPlugin.configure({
- options: { plugins: [ImagePlugin, MediaEmbedPlugin] },
+ options: {
+ plugins: [
+ ImagePlugin,
+ VideoPlugin,
+ AudioPlugin,
+ FilePlugin,
+ MediaEmbedPlugin,
+ ],
+ },
}),
PlaceholderPlugin.configure({
options: { disableEmptyPlaceholder: true },
diff --git a/apps/www/src/registry/default/components/editor/settings.tsx b/apps/www/src/registry/default/components/editor/settings.tsx
new file mode 100644
index 0000000000..6442c1c4d3
--- /dev/null
+++ b/apps/www/src/registry/default/components/editor/settings.tsx
@@ -0,0 +1,322 @@
+'use client';
+
+import { type ReactNode, createContext, useContext, useState } from 'react';
+
+import { cn } from '@udecode/cn';
+import { CopilotPlugin } from '@udecode/plate-ai/react';
+import { useEditorPlugin } from '@udecode/plate-common/react';
+import {
+ Check,
+ ChevronsUpDown,
+ ExternalLinkIcon,
+ Eye,
+ EyeOff,
+ Settings,
+ Wand2Icon,
+} from 'lucide-react';
+
+import { Button } from '@/registry/default/plate-ui/button';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/registry/default/plate-ui/command';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/registry/default/plate-ui/dialog';
+import { Input } from '@/registry/default/plate-ui/input';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/registry/default/plate-ui/popover';
+
+interface Model {
+ label: string;
+ value: string;
+}
+
+interface SettingsContextType {
+ keys: Record;
+ model: Model;
+ setKey: (service: string, key: string) => void;
+ setModel: (model: Model) => void;
+}
+
+export const models: Model[] = [
+ { label: 'gpt-4o-mini', value: 'gpt-4o-mini' },
+ { label: 'gpt-4o', value: 'gpt-4o' },
+ { label: 'gpt-4-turbo', value: 'gpt-4-turbo' },
+ { label: 'gpt-4', value: 'gpt-4' },
+ { label: 'gpt-3.5-turbo', value: 'gpt-3.5-turbo' },
+ { label: 'gpt-3.5-turbo-instruct', value: 'gpt-3.5-turbo-instruct' },
+];
+
+const SettingsContext = createContext(
+ undefined
+);
+
+export function SettingsProvider({ children }: { children: ReactNode }) {
+ const [keys, setKeys] = useState({
+ openai: '',
+ uploadthing: '',
+ });
+ const [model, setModel] = useState(models[0]);
+
+ const setKey = (service: string, key: string) => {
+ setKeys((prev) => ({ ...prev, [service]: key }));
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSettings() {
+ const context = useContext(SettingsContext);
+
+ return (
+ context ?? {
+ keys: {
+ openai: '',
+ uploadthing: '',
+ },
+ model: models[0],
+ setKey: () => {},
+ setModel: () => {},
+ }
+ );
+}
+
+export function SettingsDialog() {
+ const { keys, model, setKey, setModel } = useSettings();
+ const [tempKeys, setTempKeys] = useState(keys);
+ const [showKey, setShowKey] = useState>({});
+ const [open, setOpen] = useState(false);
+ const [openModel, setOpenModel] = useState(false);
+
+ const { getOptions, setOption } = useEditorPlugin(CopilotPlugin);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ Object.entries(tempKeys).forEach(([service, key]) => {
+ setKey(service, key);
+ });
+ setOpen(false);
+
+ // Update AI options if needed
+ const completeOptions = getOptions().completeOptions ?? {};
+ setOption('completeOptions', {
+ ...completeOptions,
+ body: {
+ ...completeOptions.body,
+ apiKey: tempKeys.openai,
+ model: model.value,
+ },
+ });
+ };
+
+ const toggleKeyVisibility = (key: string) => {
+ setShowKey((prev) => ({ ...prev, [key]: !prev[key] }));
+ };
+
+ const renderApiKeyInput = (service: string, label: string) => (
+
+
+
+
+ setTempKeys((prev) => ({ ...prev, [service]: e.target.value }))
+ }
+ placeholder=""
+ data-1p-ignore
+ type={showKey[service] ? 'text' : 'password'}
+ />
+
+
+ );
+
+ return (
+