From b67376949d1e8ecfffa68d4a9fc5b57d471462c0 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Tue, 19 Nov 2024 23:57:24 +0100 Subject: [PATCH 01/15] docs --- apps/www/content/docs/caption.mdx | 40 +- apps/www/content/docs/media-placeholder.mdx | 269 ---------- apps/www/content/docs/media.mdx | 466 ++++++++++++++++-- .../(app)/docs/[[...slug]]/doc-content.tsx | 33 +- apps/www/src/config/customizer-items.ts | 2 +- apps/www/src/config/docs-icons.tsx | 1 - apps/www/src/config/docs-plugins.ts | 8 +- .../editor/plugins/delete-plugins.ts | 11 +- .../editor/plugins/media-plugins.tsx | 10 +- .../plate-ui/media-placeholder-element.tsx | 1 + apps/www/src/registry/registry-ui.ts | 2 +- apps/www/src/styles/globals.css | 2 + .../plate-playground-template/.eslintrc.js | 2 +- templates/plate-template/.eslintrc.js | 2 +- yarn.lock | 10 +- 15 files changed, 497 insertions(+), 362 deletions(-) delete mode 100644 apps/www/content/docs/media-placeholder.mdx diff --git a/apps/www/content/docs/caption.mdx b/apps/www/content/docs/caption.mdx index c6fecde124..382585b112 100644 --- a/apps/www/content/docs/caption.mdx +++ b/apps/www/content/docs/caption.mdx @@ -1,8 +1,8 @@ --- title: Caption docs: - - route: /docs/components/draggable - title: Draggable + - route: /docs/components/caption + title: Caption --- @@ -25,25 +25,35 @@ npm install @udecode/plate-caption ```tsx import { CaptionPlugin } from '@udecode/plate-caption/react'; -import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react'; +import { + AudioPlugin, + FilePlugin, + ImagePlugin, + MediaEmbedPlugin, + VideoPlugin, +} from '@udecode/plate-media/react'; +``` +```tsx const plugins = [ // ...otherPlugins, - CaptionPlugin, ImagePlugin, + VideoPlugin, + AudioPlugin, + FilePlugin, MediaEmbedPlugin, -]; - -const editor = createPlateEditor({ - plugins, - override: { - plugins: { - [CaptionPlugin.key]: { - plugins: [ImagePlugin.key, MediaEmbedPlugin.key], - }, + CaptionPlugin.configure({ + options: { + plugins: [ + ImagePlugin, + VideoPlugin, + AudioPlugin, + FilePlugin, + MediaEmbedPlugin, + ], }, - }, -}); + }), +]; ``` ## Plugins diff --git a/apps/www/content/docs/media-placeholder.mdx b/apps/www/content/docs/media-placeholder.mdx deleted file mode 100644 index c46767ce5c..0000000000 --- a/apps/www/content/docs/media-placeholder.mdx +++ /dev/null @@ -1,269 +0,0 @@ ---- -title: Media Placeholder -docs: - - route: components/media-placeholder-element - title: Media Placeholder Element - - route: components/media-upload-toast - title: Media Upload Toast ---- - - - - - -## Features - -- Supports multiple media types: image, video, audio, and file -- Transforms for inserting different types of media placeholders - - - -## Installation - -```bash -npm install @udecode/plate-media -``` - -## Usage - -```tsx -import { - AudioPlugin, - FilePlugin, - ImagePlugin, - MediaEmbedPlugin, - PlaceholderPlugin, - VideoPlugin, -} from '@udecode/plate-media/react'; -``` - -```tsx -const plugins = [ - // ...otherPlugins, - PlaceholderPlugin.configure({ - options: { - disableEmptyPlaceholder: true, - }, - render: { - afterEditable: () => , - }, - }), -]; -``` - -```tsx -const components = { - // ...otherComponents, - [PlaceholderPlugin.key]: MediaPlaceholderElement, -}; -``` - -### UploadThing Integration - -The UploadThing integration provides an easy way to handle file uploads in your editor. Follow these steps to set it up: - -1. Install the required dependencies: - - ```bash - npm install @uploadthing/react uploadthing - ``` - -2. Install the [media-placeholder-element](/docs/components/media-placeholder-element) component. - -3. Set up the UploadThing API route by copying the [example implementation](https://github.com/udecode/plate/blob/main/templates/plate-playground-template/src/app/api/uploadthing/core.ts). - -4. Get your UploadThing API key from the [dashboard](https://uploadthing.com/dashboard) and add it to your `.env` file: - ```bash - UPLOADTHING_SECRET=your_secret_key - ``` - -### Using your own backend - -To implement your own backend for file uploads: - -1. Remove the UploadThing implementation: - - ```bash - lib/uploadthing/ - api/uploadthing/ - ``` - -2. Create your own upload hook: - - ```ts - function useUploadFile() { - // Your implementation here - return { - isUploading: boolean, - progress: number, - uploadFile: (file: File) => Promise, - uploadedFile: UploadedFile | undefined, - uploadingFile: File | undefined, - }; - } - ``` - -3. The hook should match the interface expected by the media placeholder component: - ```ts - interface UploadedFile { - key: string; - url: string; - name: string; - size: number; - type: string; - } - ``` - -### Plate UI - -Refer to the preview above. - -### Plate Plus - - - -## Plugins - -### PlaceholderPlugin - -Media placeholder element plugin. - - - -Configuration for different file types: - -- You can use this option to configure upload limits for each file type, including: - - - Maximum file count (e.g. `maxFileCount: 1`) - - Maximum file size (e.g. `maxFileSize: '8MB'`) - - Minimum file count (e.g. `minFileCount: 1`) - - mediaType: Used for passing to the media-placeholder-elements file to distinguish between different file types and their progress bar styles. - - default configuration: - - ```tsx - uploadConfig: { - audio: { - maxFileCount: 1, - maxFileSize: '8MB', - mediaType: AudioPlugin.key, - minFileCount: 1, - }, - blob: { - maxFileCount: 1, - maxFileSize: '8MB', - mediaType: FilePlugin.key, - minFileCount: 1, - }, - image: { - maxFileCount: 3, - maxFileSize: '4MB', - mediaType: ImagePlugin.key, - minFileCount: 1, - }, - pdf: { - maxFileCount: 1, - maxFileSize: '4MB', - mediaType: FilePlugin.key, - minFileCount: 1, - }, - text: { - maxFileCount: 1, - maxFileSize: '64KB', - mediaType: FilePlugin.key, - minFileCount: 1, - }, - video: { - maxFileCount: 1, - maxFileSize: '16MB', - mediaType: VideoPlugin.key, - minFileCount: 1, - }, - }, - ``` - - here is all allowed file types (keys for `uploadConfig`): - - ```tsx - export const ALLOWED_FILE_TYPES = [ - 'image', - 'video', - 'audio', - 'pdf', - 'text', - 'blob', - ] as const; - ``` - - - - -Disable empty placeholder when no file is selected. - -- **Default:** `false` - - - -Whether we can undo to the placeholder after the file upload is complete. - -- **Default:** `false` - - - -Maximum number of files that can be uploaded at once. - -- **Default:** `5` - - - -## Transforms - -### editor.tf.insert.audioPlaceholder - -Inserts an audio placeholder element. - - - - Options for the insert nodes transform. - - - -### editor.tf.insert.filePlaceholder - -Inserts a file placeholder element. - - - - Options for the insert nodes transform. - - - -### editor.tf.insert.imagePlaceholder - -Inserts an image placeholder element. - - - - Options for the insert nodes transform. - - - -### editor.tf.insert.videoPlaceholder - -Inserts a video placeholder element. - - - - Options for the insert nodes transform. - - - -## Types - -### TPlaceholderElement - -```tsx -type TPlaceholderElement = TElement & { - mediaType: string; -}; -``` diff --git a/apps/www/content/docs/media.mdx b/apps/www/content/docs/media.mdx index afde41371a..26da9e4fa1 100644 --- a/apps/www/content/docs/media.mdx +++ b/apps/www/content/docs/media.mdx @@ -3,10 +3,20 @@ title: Media docs: - route: /docs/components/image-element title: Image Element + - route: /docs/components/video-element + title: Video Element + - route: /docs/components/audio-element + title: Audio Element + - route: /docs/components/file-element + title: File Element - route: /docs/components/media-embed-element title: Media Embed Element - route: /docs/components/media-popover title: Media Popover + - route: /docs/components/media-placeholder-element + title: Media Placeholder Element + - route: /docs/components/media-upload-toast + title: Media Upload Toast - route: /docs/components/media-toolbar-button title: Media Toolbar Button - route: https://pro.platejs.org/docs/components/media-toolbar @@ -19,11 +29,36 @@ docs: ## Features -- Allows insertion of embeddable media: images, videos, and tweets. -- Supports multiple media providers: video, youtube, vimeo, dailymotion, youku, coub, twitter. -- Editable captions. -- Resizable. -- Use [Plate Cloud](/docs/cloud) for easy cloud uploads and server-side image resizing. +### Media Features + +- Editable captions +- Resizable elements + +### Media Support +- **File types**: + - Image + - Video + - Audio + - Others (PDF, Word, etc.) +- **Video providers**: + - Local video files + - YouTube, Vimeo, Dailymotion, Youku, Coub +- **Embed providers**: + - Tweets + +### Upload + +- **Multiple upload methods**: + - Toolbar button with file picker + - Drag and drop from file system + - Paste from clipboard (images) + - URL embedding for external media +- **Upload experience**: + - Real-time progress tracking + - Preview during upload + - Error handling + - File size validation + - Type validation @@ -36,38 +71,201 @@ npm install @udecode/plate-media ## Usage ```tsx -import { CaptionPlugin } from '@udecode/plate-caption/react'; -import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react'; +import { + AudioPlugin, + FilePlugin, + ImagePlugin, + MediaEmbedPlugin, + PlaceholderPlugin, + VideoPlugin, +} from '@udecode/plate-media/react'; import { SelectOnBackspacePlugin } from '@udecode/plate-select'; +``` +```tsx const plugins = [ // ...otherPlugins, - CaptionPlugin.configure({ - options: { plugins: [ImagePlugin, MediaEmbedPlugin] }, - }), ImagePlugin, + VideoPlugin, + AudioPlugin, + FilePlugin, MediaEmbedPlugin, SelectOnBackspacePlugin.configure({ options: { query: { - allow: [ImagePlugin.key, MediaEmbedPlugin.key], + allow: [ImagePlugin.key, VideoPlugin.key, AudioPlugin.key, FilePlugin.key, MediaEmbedPlugin.key], }, }, }), + PlaceholderPlugin.configure({ + options: { disableEmptyPlaceholder: true }, + render: { afterEditable: MediaUploadToast }, + }), ]; ``` +```tsx +const components = { + // ...otherComponents, + [ImagePlugin.key]: ImageElement, + [VideoPlugin.key]: VideoElement, + [AudioPlugin.key]: AudioElement, + [FilePlugin.key]: FileElement, + [MediaEmbedPlugin.key]: MediaEmbedElement, + [PlaceholderPlugin.key]: MediaPlaceholderElement, +}; +``` + +### Caption + +To enable media captions, use the [Caption Plugin](/docs/caption). + +### Upload + +There are two ways to implement file uploads in your editor: + +1. Using our UploadThing implementation +2. Creating a custom implementation with your preferred upload solution + +#### UploadThing + +1. Install [MediaPlaceholderElement](/docs/components/media-placeholder-element) +2. Get your secret key from [UploadThing](https://uploadthing.com/dashboard/settings) for free +3. Add your UploadThing secret key to `.env`: + +```bash title=".env" +UPLOADTHING_SECRET=sk_live_xxx +``` + +#### Custom Implementation + +For custom implementations, you'll need to create an upload hook that matches our interface. This can work with any upload backend (AWS S3, UploadThing, Cloudinary, Firebase Storage, etc.). + +The upload hook should implement this interface: + +```ts +interface UseUploadFileProps { + onUploadComplete?: (file: UploadedFile) => void; + onUploadError?: (error: unknown) => void; + headers?: Record; + onUploadBegin?: (fileName: string) => void; + onUploadProgress?: (progress: { progress: number }) => void; + skipPolling?: boolean; +} + +interface UploadedFile { + key: string; // Unique identifier + url: string; // Public URL of the uploaded file + name: string; // Original filename + size: number; // File size in bytes + type: string; // MIME type +} +``` + +Example implementation with S3 presigned URLs: + +```ts +export function useUploadFile({ + onUploadComplete, + onUploadError, + onUploadProgress +}: UseUploadFileProps = {}) { + const [uploadedFile, setUploadedFile] = useState(); + const [uploadingFile, setUploadingFile] = useState(); + const [progress, setProgress] = useState(0); + const [isUploading, setIsUploading] = useState(false); + + async function uploadFile(file: File) { + setIsUploading(true); + setUploadingFile(file); + + try { + // Get presigned URL and final URL from your backend + const { presignedUrl, fileUrl, fileKey } = await fetch('/api/upload', { + method: 'POST', + body: JSON.stringify({ + filename: file.name, + contentType: file.type, + }), + }).then(r => r.json()); + + // Upload to S3 using presigned URL + await axios.put(presignedUrl, file, { + headers: { 'Content-Type': file.type }, + onUploadProgress: (progressEvent) => { + const progress = (progressEvent.loaded / progressEvent.total) * 100; + setProgress(progress); + onUploadProgress?.({ progress }); + }, + }); + + const uploadedFile = { + key: fileKey, + url: fileUrl, + name: file.name, + size: file.size, + type: file.type, + }; + + setUploadedFile(uploadedFile); + onUploadComplete?.(uploadedFile); + + return uploadedFile; + } catch (error) { + onUploadError?.(error); + throw error; + } finally { + setProgress(0); + setIsUploading(false); + setUploadingFile(undefined); + } + } + + return { + isUploading, + progress, + uploadFile, + uploadedFile, + uploadingFile, + }; +} +``` + + ## Plugins +### MediaPluginOptions + +Plugin options used by media plugins. + + + + A function to check whether a text string is a URL. + + + A function to transform the URL. + + + ### ImagePlugin +Plugin for void image elements. Options extends [MediaPluginOptions](#mediapluginoptions). + -Extends `MediaPluginOptions`. +Extends [MediaPluginOptions](#mediapluginoptions). -An optional method that will upload the image to a server. The method receives the base64 dataUrl of the uploaded image, and should return the URL of the uploaded image. +An optional method that will upload the image to a server. The method receives either: +- A data URL (string) from `FileReader.readAsDataURL` +- An ArrayBuffer from clipboard data + +Should return either: +- A URL string to the uploaded image +- The original data URL/ArrayBuffer if no upload is needed + +If not provided, the original data URL/ArrayBuffer will be used as the image source. @@ -78,12 +276,159 @@ Disables URL embed on data insertion if set to true. +### VideoPlugin + +Plugin for void video elements. + +### AudioPlugin + +Plugin for void audio elements. + +### FilePlugin + +Plugin for void file elements. + ### MediaEmbedPlugin -Options extends `MediaPluginOptions`. +Plugin for void media embed elements. Options extends `MediaPluginOptions`. + +### PlaceholderPlugin + +Plugin for void media placeholder elements. Handles file uploads, drag & drop, and clipboard paste events. + + + + + +Configuration for different file types. Default configuration: + +```ts +{ + audio: { + maxFileCount: 1, + maxFileSize: '8MB', + mediaType: AudioPlugin.key, + minFileCount: 1, + }, + blob: { + maxFileCount: 1, + maxFileSize: '8MB', + mediaType: FilePlugin.key, + minFileCount: 1, + }, + image: { + maxFileCount: 3, + maxFileSize: '4MB', + mediaType: ImagePlugin.key, + minFileCount: 1, + }, + pdf: { + maxFileCount: 1, + maxFileSize: '4MB', + mediaType: FilePlugin.key, + minFileCount: 1, + }, + text: { + maxFileCount: 1, + maxFileSize: '64KB', + mediaType: FilePlugin.key, + minFileCount: 1, + }, + video: { + maxFileCount: 1, + maxFileSize: '16MB', + mediaType: VideoPlugin.key, + minFileCount: 1, + }, +} +``` + +Supported file types: `'image' | 'video' | 'audio' | 'pdf' | 'text' | 'blob'` + + + + The media plugin keys that this config is for: `'audio' | 'file' | 'image' | 'video'` + + + The maximum number of files of this type that can be uploaded. + + + The maximum file size for a file of this type. Format: `${1|2|4|8|16|32|64|128|256|512|1024}${B|KB|MB|GB}` + + + The minimum number of files of this type that must be uploaded. + + + + + + +Disable empty placeholder when no file is uploading. + +- **Default:** `false` + + + +Disable drag and drop file upload functionality. + +- **Default:** `false` + + + +Maximum number of files that can be uploaded at once, if not specified by `uploadConfig`. + +- **Default:** `5` + + + +Allow multiple files of the same type to be uploaded. + +- **Default:** `true` + + + ## API Placeholder +### editor.tf.insert.media() + +Inserts media files into the editor with upload placeholders. + + + + Files to upload. Validates against configured file types and limits. + + + + + Location to insert the media. Defaults to current selection. + + + Whether to insert a new block after the media. + - **Default:** `true` + + + + + +The transform: +- Validates files against configured limits (size, count, type) +- Creates placeholder elements for each file +- Handles multiple file uploads sequentially +- Maintains upload history for undo/redo operations +- Triggers error handling if validation fails + +Error codes: +```ts +enum UploadErrorCode { + INVALID_FILE_TYPE = 400, + TOO_MANY_FILES = 402, + INVALID_FILE_SIZE = 403, + TOO_LESS_FILES = 405, + TOO_LARGE = 413, +} +``` + ### editor.tf.insert.audioPlaceholder Inserts a placeholder. Converts to an audio element when completed. @@ -94,7 +439,6 @@ Inserts a placeholder. Converts to an audio element when completed. - ### editor.tf.insert.filePlaceholder Inserts a placeholder. Converts to a file element when completed. @@ -123,41 +467,47 @@ Inserts a placeholder. Converts to a video element when completed. +### editor.api.placeholder.addUploadingFile() -## API Media +Tracks a file that is currently being uploaded. + + + + Unique identifier for the placeholder element. + + + The file being uploaded. + + -### insertMedia +### editor.api.placeholder.getUploadingFile() -Inserts media (image or media embed) into the editor. The type of media to insert is determined by the `type` parameter. +Gets a file that is currently being uploaded. - -The editor instance. - - - - -A function that returns a promise resolving to the URL of the media to -be inserted. If not provided, a prompt will be displayed to enter the -URL. - - -The type of media to insert. Defaults to the editor's image element -type. + + Unique identifier for the placeholder element. + + -- **Default:** `editor.getType(ImagePlugin)` + + + The uploading file if found, undefined otherwise. + + - - +### editor.api.placeholder.removeUploadingFile() - +Removes a file from the uploading tracking state after upload completes or fails. + + + + Unique identifier for the placeholder element to remove. + +## API Media + ### parseMediaUrl Parses a media URL and returns the data associated with it based on the configured rules of the media plugin. @@ -198,19 +548,6 @@ Submits the floating media element by setting its URL and performing necessary t -### MediaPluginOptions - -Common attributes shared by image and media embed plugins. - - - - A function to check whether a text string is a URL. - - - A function to transform the URL. - - - ### EmbedUrlData A type defining the data returned from parsing an embed URL. @@ -556,3 +893,26 @@ A behavior hook for a media toolbar button. + +## Types + +### TMediaElement + +```tsx +export interface TMediaElement extends TElement { + url: string; + id?: string; + align?: 'center' | 'left' | 'right'; + isUpload?: boolean; + name?: string; + placeholderId?: string; +} +``` + +### TPlaceholderElement + +```tsx +export interface TPlaceholderElement extends TElement { + mediaType: string; +} +``` \ No newline at end of file diff --git a/apps/www/src/app/(app)/docs/[[...slug]]/doc-content.tsx b/apps/www/src/app/(app)/docs/[[...slug]]/doc-content.tsx index fe7fc3e73c..fb397e972c 100644 --- a/apps/www/src/app/(app)/docs/[[...slug]]/doc-content.tsx +++ b/apps/www/src/app/(app)/docs/[[...slug]]/doc-content.tsx @@ -9,6 +9,7 @@ import type { RegistryEntry } from '@/registry/schema'; import { cn } from '@udecode/cn'; import { ChevronRight, ExternalLinkIcon } from 'lucide-react'; import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import { DocBreadcrumb } from '@/app/(app)/docs/[[...slug]]/doc-breadcrumb'; import { OpenInPlus } from '@/components/open-in-plus'; @@ -18,6 +19,7 @@ import { badgeVariants } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { categoryNavGroups, docSections } from '@/config/docs-utils'; import { getDocTitle, getRegistryTitle } from '@/lib/registry-utils'; +import { Button } from '@/registry/default/plate-ui/button'; // import { formatBytes, getPackageData } from '@/lib/bundlephobia'; @@ -57,11 +59,24 @@ export function DocContent({ children: React.ReactNode; toc?: TableOfContents; } & Omit, 'category'>) { + const pathname = usePathname(); + const isCategory = + pathname && + [ + '/docs/api', + '/docs/components', + '/docs/examples', + '/docs/plugins', + ].includes(pathname); const title = doc?.title ?? getRegistryTitle(file); const hasToc = doc?.toc && toc; const items = categoryNavGroups[category]; + const docSection = docSections[0].items!.find( + (item) => item.value === category + ); + return (
- + {isCategory ? ( + + ) : ( + + + + )} = { 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-icons.tsx b/apps/www/src/config/docs-icons.tsx index e7bf814bef..4ecc6747d4 100644 --- a/apps/www/src/config/docs-icons.tsx +++ b/apps/www/src/config/docs-icons.tsx @@ -210,7 +210,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, 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/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/plate-ui/media-placeholder-element.tsx b/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx index 812c9790f3..6837900460 100644 --- a/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx +++ b/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx @@ -134,6 +134,7 @@ export const MediaPlaceholderElement = withHOC( // React dev mode will call useEffect twice const isReplaced = useRef(false); + /** Paste and drop */ useEffect(() => { if (isReplaced.current) return; diff --git a/apps/www/src/registry/registry-ui.ts b/apps/www/src/registry/registry-ui.ts index cff3933725..5558cf1a9b 100644 --- a/apps/www/src/registry/registry-ui.ts +++ b/apps/www/src/registry/registry-ui.ts @@ -700,7 +700,7 @@ import { withDraggables } from './withDraggables';`, dependencies: ['@udecode/plate-media', 'sonner'], doc: { description: 'Show toast notifications for media uploads.', - docs: [{ route: '/docs/media-placeholder', title: 'Media Placeholder' }], + docs: [{ route: '/docs/media', title: 'Media Placeholder' }], examples: ['media-demo', 'upload-pro'], }, files: ['plate-ui/media-upload-toast.tsx'], diff --git a/apps/www/src/styles/globals.css b/apps/www/src/styles/globals.css index 327cc3a620..fe6d3183e3 100644 --- a/apps/www/src/styles/globals.css +++ b/apps/www/src/styles/globals.css @@ -30,6 +30,8 @@ .prose { --tw-prose-body: var(--foreground); + --tw-prose-bold: inherit; + --tw-prose-links: inherit; --tw-prose-bullets: var(--foreground); } } diff --git a/templates/plate-playground-template/.eslintrc.js b/templates/plate-playground-template/.eslintrc.js index 12e2acda04..462dd3d4a9 100644 --- a/templates/plate-playground-template/.eslintrc.js +++ b/templates/plate-playground-template/.eslintrc.js @@ -24,7 +24,7 @@ module.exports = { 'react/display-name': 'off', 'react/jsx-key': 'off', 'react/no-unescaped-entities': 'off', - 'tailwindcss/classnames-order': 'warn', + 'tailwindcss/classnames-order': 'off', 'tailwindcss/no-custom-classname': 'off', 'unused-imports/no-unused-imports': 'warn', 'unused-imports/no-unused-vars': [ diff --git a/templates/plate-template/.eslintrc.js b/templates/plate-template/.eslintrc.js index 12e2acda04..462dd3d4a9 100644 --- a/templates/plate-template/.eslintrc.js +++ b/templates/plate-template/.eslintrc.js @@ -24,7 +24,7 @@ module.exports = { 'react/display-name': 'off', 'react/jsx-key': 'off', 'react/no-unescaped-entities': 'off', - 'tailwindcss/classnames-order': 'warn', + 'tailwindcss/classnames-order': 'off', 'tailwindcss/no-custom-classname': 'off', 'unused-imports/no-unused-imports': 'warn', 'unused-imports/no-unused-vars': [ diff --git a/yarn.lock b/yarn.lock index af4870c5ba..b93bc6cf45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6593,7 +6593,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-docx@npm:40.2.1, @udecode/plate-docx@workspace:^, @udecode/plate-docx@workspace:packages/docx": +"@udecode/plate-docx@npm:40.2.3, @udecode/plate-docx@workspace:^, @udecode/plate-docx@workspace:packages/docx": version: 0.0.0-use.local resolution: "@udecode/plate-docx@workspace:packages/docx" dependencies: @@ -6601,7 +6601,7 @@ __metadata: "@udecode/plate-heading": "npm:40.0.2" "@udecode/plate-indent": "npm:40.0.0" "@udecode/plate-indent-list": "npm:40.0.0" - "@udecode/plate-media": "npm:40.2.1" + "@udecode/plate-media": "npm:40.2.3" "@udecode/plate-table": "npm:40.0.0" validator: "npm:^13.12.0" peerDependencies: @@ -6961,7 +6961,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-media@npm:40.2.1, @udecode/plate-media@workspace:^, @udecode/plate-media@workspace:packages/media": +"@udecode/plate-media@npm:40.2.3, @udecode/plate-media@workspace:^, @udecode/plate-media@workspace:packages/media": version: 0.0.0-use.local resolution: "@udecode/plate-media@workspace:packages/media" dependencies: @@ -7297,7 +7297,7 @@ __metadata: "@udecode/plate-common": "npm:40.0.3" "@udecode/plate-csv": "npm:40.0.0" "@udecode/plate-diff": "npm:40.0.0" - "@udecode/plate-docx": "npm:40.2.1" + "@udecode/plate-docx": "npm:40.2.3" "@udecode/plate-find-replace": "npm:40.0.0" "@udecode/plate-floating": "npm:40.0.0" "@udecode/plate-font": "npm:40.0.0" @@ -7313,7 +7313,7 @@ __metadata: "@udecode/plate-link": "npm:40.0.0" "@udecode/plate-list": "npm:40.0.0" "@udecode/plate-markdown": "npm:40.2.2" - "@udecode/plate-media": "npm:40.2.1" + "@udecode/plate-media": "npm:40.2.3" "@udecode/plate-mention": "npm:40.0.0" "@udecode/plate-node-id": "npm:40.0.0" "@udecode/plate-normalizers": "npm:40.0.0" From d81e11884ca4abc3ef8455d1b8604c2f268485eb Mon Sep 17 00:00:00 2001 From: zbeyens Date: Wed, 20 Nov 2024 00:36:25 +0100 Subject: [PATCH 02/15] docs --- apps/www/public/r/index.json | 2 +- .../public/r/styles/default/delete-plugins.json | 2 +- apps/www/public/r/styles/default/editor-ai.json | 6 +++++- .../default/media-placeholder-element.json | 2 +- .../public/r/styles/default/media-plugins.json | 2 +- .../r/styles/default/media-upload-toast.json | 2 +- apps/www/src/__registry__/index.tsx | 2 +- .../components/editor/use-create-editor.tsx | 17 ++++++++++++++++- apps/www/src/registry/registry-blocks.ts | 4 ++++ 9 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/www/public/r/index.json b/apps/www/public/r/index.json index 7cf01d654e..f1e8a420a7 100644 --- a/apps/www/public/r/index.json +++ b/apps/www/public/r/index.json @@ -2557,7 +2557,7 @@ "description": "Show toast notifications for media uploads.", "docs": [ { - "route": "/docs/media-placeholder", + "route": "/docs/media", "title": "Media Placeholder" } ], diff --git a/apps/www/public/r/styles/default/delete-plugins.json b/apps/www/public/r/styles/default/delete-plugins.json index 1b89d722e3..dedeb6de51 100644 --- a/apps/www/public/r/styles/default/delete-plugins.json +++ b/apps/www/public/r/styles/default/delete-plugins.json @@ -6,7 +6,7 @@ ], "files": [ { - "content": "'use client';\n\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';\nimport { DeletePlugin, SelectOnBackspacePlugin } from '@udecode/plate-select';\n\nexport const deletePlugins = [\n SelectOnBackspacePlugin.configure({\n options: {\n query: {\n allow: [\n ImagePlugin.key,\n MediaEmbedPlugin.key,\n HorizontalRulePlugin.key,\n ],\n },\n },\n }),\n DeletePlugin,\n] as const;\n", + "content": "'use client';\n\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n MediaEmbedPlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport { DeletePlugin, SelectOnBackspacePlugin } from '@udecode/plate-select';\n\nexport const deletePlugins = [\n SelectOnBackspacePlugin.configure({\n options: {\n query: {\n allow: [\n ImagePlugin.key,\n VideoPlugin.key,\n AudioPlugin.key,\n FilePlugin.key,\n MediaEmbedPlugin.key,\n HorizontalRulePlugin.key,\n ],\n },\n },\n }),\n DeletePlugin,\n] as const;\n", "path": "components/editor/plugins/delete-plugins.ts", "target": "components/editor/plugins/delete-plugins.ts", "type": "registry:component" diff --git a/apps/www/public/r/styles/default/editor-ai.json b/apps/www/public/r/styles/default/editor-ai.json index d268a91200..b06ea7a399 100644 --- a/apps/www/public/r/styles/default/editor-ai.json +++ b/apps/www/public/r/styles/default/editor-ai.json @@ -38,7 +38,7 @@ "type": "registry:component" }, { - "content": "import { withProps } from '@udecode/cn';\nimport { AIPlugin } from '@udecode/plate-ai/react';\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\n SubscriptPlugin,\n SuperscriptPlugin,\n UnderlinePlugin,\n} from '@udecode/plate-basic-marks/react';\nimport { BlockquotePlugin } from '@udecode/plate-block-quote/react';\nimport {\n CodeBlockPlugin,\n CodeLinePlugin,\n CodeSyntaxPlugin,\n} from '@udecode/plate-code-block/react';\nimport { CommentsPlugin } from '@udecode/plate-comments/react';\nimport {\n ParagraphPlugin,\n PlateLeaf,\n usePlateEditor,\n} from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { EmojiInputPlugin } from '@udecode/plate-emoji/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { TocPlugin } from '@udecode/plate-heading/react';\nimport { HighlightPlugin } from '@udecode/plate-highlight/react';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { KbdPlugin } from '@udecode/plate-kbd/react';\nimport { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';\nimport {\n MentionInputPlugin,\n MentionPlugin,\n} from '@udecode/plate-mention/react';\nimport { SlashInputPlugin } from '@udecode/plate-slash-command/react';\nimport {\n TableCellHeaderPlugin,\n TableCellPlugin,\n TablePlugin,\n TableRowPlugin,\n} from '@udecode/plate-table/react';\nimport { TogglePlugin } from '@udecode/plate-toggle/react';\n\nimport { copilotPlugins } from '@/components/editor/plugins/copilot-plugins';\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { FixedToolbarPlugin } from '@/components/editor/plugins/fixed-toolbar-plugin';\nimport { FloatingToolbarPlugin } from '@/components/editor/plugins/floating-toolbar-plugin';\nimport { AILeaf } from '@/components/plate-ui/ai-leaf';\nimport { BlockquoteElement } from '@/components/plate-ui/blockquote-element';\nimport { CodeBlockElement } from '@/components/plate-ui/code-block-element';\nimport { CodeLeaf } from '@/components/plate-ui/code-leaf';\nimport { CodeLineElement } from '@/components/plate-ui/code-line-element';\nimport { CodeSyntaxLeaf } from '@/components/plate-ui/code-syntax-leaf';\nimport { ColumnElement } from '@/components/plate-ui/column-element';\nimport { ColumnGroupElement } from '@/components/plate-ui/column-group-element';\nimport { CommentLeaf } from '@/components/plate-ui/comment-leaf';\nimport { DateElement } from '@/components/plate-ui/date-element';\nimport { EmojiInputElement } from '@/components/plate-ui/emoji-input-element';\nimport { ExcalidrawElement } from '@/components/plate-ui/excalidraw-element';\nimport { HeadingElement } from '@/components/plate-ui/heading-element';\nimport { HighlightLeaf } from '@/components/plate-ui/highlight-leaf';\nimport { HrElement } from '@/components/plate-ui/hr-element';\nimport { ImageElement } from '@/components/plate-ui/image-element';\nimport { KbdLeaf } from '@/components/plate-ui/kbd-leaf';\nimport { LinkElement } from '@/components/plate-ui/link-element';\nimport { MediaEmbedElement } from '@/components/plate-ui/media-embed-element';\nimport { MentionElement } from '@/components/plate-ui/mention-element';\nimport { MentionInputElement } from '@/components/plate-ui/mention-input-element';\nimport { ParagraphElement } from '@/components/plate-ui/paragraph-element';\nimport { withPlaceholders } from '@/components/plate-ui/placeholder';\nimport { SlashInputElement } from '@/components/plate-ui/slash-input-element';\nimport {\n TableCellElement,\n TableCellHeaderElement,\n} from '@/components/plate-ui/table-cell-element';\nimport { TableElement } from '@/components/plate-ui/table-element';\nimport { TableRowElement } from '@/components/plate-ui/table-row-element';\nimport { TocElement } from '@/components/plate-ui/toc-element';\nimport { ToggleElement } from '@/components/plate-ui/toggle-element';\nimport { withDraggables } from '@/components/plate-ui/with-draggables';\n\nexport const useCreateEditor = () => {\n return usePlateEditor({\n override: {\n components: withDraggables(\n withPlaceholders({\n [AIPlugin.key]: AILeaf,\n [BlockquotePlugin.key]: BlockquoteElement,\n [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),\n [CodeBlockPlugin.key]: CodeBlockElement,\n [CodeLinePlugin.key]: CodeLineElement,\n [CodePlugin.key]: CodeLeaf,\n [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,\n [ColumnItemPlugin.key]: ColumnElement,\n [ColumnPlugin.key]: ColumnGroupElement,\n [CommentsPlugin.key]: CommentLeaf,\n [DatePlugin.key]: DateElement,\n [EmojiInputPlugin.key]: EmojiInputElement,\n [ExcalidrawPlugin.key]: ExcalidrawElement,\n [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),\n [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),\n [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),\n [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),\n [HighlightPlugin.key]: HighlightLeaf,\n [HorizontalRulePlugin.key]: HrElement,\n [ImagePlugin.key]: ImageElement,\n [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),\n [KbdPlugin.key]: KbdLeaf,\n [LinkPlugin.key]: LinkElement,\n [MediaEmbedPlugin.key]: MediaEmbedElement,\n [MentionInputPlugin.key]: MentionInputElement,\n [MentionPlugin.key]: MentionElement,\n [ParagraphPlugin.key]: ParagraphElement,\n [SlashInputPlugin.key]: SlashInputElement,\n [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),\n [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),\n [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),\n [TableCellHeaderPlugin.key]: TableCellHeaderElement,\n [TableCellPlugin.key]: TableCellElement,\n [TablePlugin.key]: TableElement,\n [TableRowPlugin.key]: TableRowElement,\n [TocPlugin.key]: TocElement,\n [TogglePlugin.key]: ToggleElement,\n [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),\n })\n ),\n },\n plugins: [\n ...copilotPlugins,\n ...editorPlugins,\n FixedToolbarPlugin,\n FloatingToolbarPlugin,\n ],\n value: [\n {\n children: [{ text: 'Playground' }],\n type: 'h1',\n },\n {\n children: [\n { text: 'A rich-text editor with AI capabilities. Try the ' },\n { bold: true, text: 'AI commands' },\n { text: ' or use ' },\n { kbd: true, text: 'Cmd+J' },\n { text: ' to open the AI menu.' },\n ],\n type: ParagraphPlugin.key,\n },\n ],\n });\n};\n", + "content": "import { withProps } from '@udecode/cn';\nimport { AIPlugin } from '@udecode/plate-ai/react';\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\n SubscriptPlugin,\n SuperscriptPlugin,\n UnderlinePlugin,\n} from '@udecode/plate-basic-marks/react';\nimport { BlockquotePlugin } from '@udecode/plate-block-quote/react';\nimport {\n CodeBlockPlugin,\n CodeLinePlugin,\n CodeSyntaxPlugin,\n} from '@udecode/plate-code-block/react';\nimport { CommentsPlugin } from '@udecode/plate-comments/react';\nimport {\n ParagraphPlugin,\n PlateLeaf,\n usePlateEditor,\n} from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { EmojiInputPlugin } from '@udecode/plate-emoji/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { TocPlugin } from '@udecode/plate-heading/react';\nimport { HighlightPlugin } from '@udecode/plate-highlight/react';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { KbdPlugin } from '@udecode/plate-kbd/react';\nimport { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n MediaEmbedPlugin,\n PlaceholderPlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n MentionInputPlugin,\n MentionPlugin,\n} from '@udecode/plate-mention/react';\nimport { SlashInputPlugin } from '@udecode/plate-slash-command/react';\nimport {\n TableCellHeaderPlugin,\n TableCellPlugin,\n TablePlugin,\n TableRowPlugin,\n} from '@udecode/plate-table/react';\nimport { TogglePlugin } from '@udecode/plate-toggle/react';\n\nimport { copilotPlugins } from '@/components/editor/plugins/copilot-plugins';\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { FixedToolbarPlugin } from '@/components/editor/plugins/fixed-toolbar-plugin';\nimport { FloatingToolbarPlugin } from '@/components/editor/plugins/floating-toolbar-plugin';\nimport { AILeaf } from '@/components/plate-ui/ai-leaf';\nimport { BlockquoteElement } from '@/components/plate-ui/blockquote-element';\nimport { CodeBlockElement } from '@/components/plate-ui/code-block-element';\nimport { CodeLeaf } from '@/components/plate-ui/code-leaf';\nimport { CodeLineElement } from '@/components/plate-ui/code-line-element';\nimport { CodeSyntaxLeaf } from '@/components/plate-ui/code-syntax-leaf';\nimport { ColumnElement } from '@/components/plate-ui/column-element';\nimport { ColumnGroupElement } from '@/components/plate-ui/column-group-element';\nimport { CommentLeaf } from '@/components/plate-ui/comment-leaf';\nimport { DateElement } from '@/components/plate-ui/date-element';\nimport { EmojiInputElement } from '@/components/plate-ui/emoji-input-element';\nimport { ExcalidrawElement } from '@/components/plate-ui/excalidraw-element';\nimport { HeadingElement } from '@/components/plate-ui/heading-element';\nimport { HighlightLeaf } from '@/components/plate-ui/highlight-leaf';\nimport { HrElement } from '@/components/plate-ui/hr-element';\nimport { ImageElement } from '@/components/plate-ui/image-element';\nimport { KbdLeaf } from '@/components/plate-ui/kbd-leaf';\nimport { LinkElement } from '@/components/plate-ui/link-element';\nimport { MediaAudioElement } from '@/components/plate-ui/media-audio-element';\nimport { MediaEmbedElement } from '@/components/plate-ui/media-embed-element';\nimport { MediaFileElement } from '@/components/plate-ui/media-file-element';\nimport { MediaPlaceholderElement } from '@/components/plate-ui/media-placeholder-element';\nimport { MediaVideoElement } from '@/components/plate-ui/media-video-element';\nimport { MentionElement } from '@/components/plate-ui/mention-element';\nimport { MentionInputElement } from '@/components/plate-ui/mention-input-element';\nimport { ParagraphElement } from '@/components/plate-ui/paragraph-element';\nimport { withPlaceholders } from '@/components/plate-ui/placeholder';\nimport { SlashInputElement } from '@/components/plate-ui/slash-input-element';\nimport {\n TableCellElement,\n TableCellHeaderElement,\n} from '@/components/plate-ui/table-cell-element';\nimport { TableElement } from '@/components/plate-ui/table-element';\nimport { TableRowElement } from '@/components/plate-ui/table-row-element';\nimport { TocElement } from '@/components/plate-ui/toc-element';\nimport { ToggleElement } from '@/components/plate-ui/toggle-element';\nimport { withDraggables } from '@/components/plate-ui/with-draggables';\n\nexport const useCreateEditor = () => {\n return usePlateEditor({\n override: {\n components: withDraggables(\n withPlaceholders({\n [AIPlugin.key]: AILeaf,\n [AudioPlugin.key]: MediaAudioElement,\n [BlockquotePlugin.key]: BlockquoteElement,\n [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),\n [CodeBlockPlugin.key]: CodeBlockElement,\n [CodeLinePlugin.key]: CodeLineElement,\n [CodePlugin.key]: CodeLeaf,\n [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,\n [ColumnItemPlugin.key]: ColumnElement,\n [ColumnPlugin.key]: ColumnGroupElement,\n [CommentsPlugin.key]: CommentLeaf,\n [DatePlugin.key]: DateElement,\n [EmojiInputPlugin.key]: EmojiInputElement,\n [ExcalidrawPlugin.key]: ExcalidrawElement,\n [FilePlugin.key]: MediaFileElement,\n [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),\n [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),\n [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),\n [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),\n [HighlightPlugin.key]: HighlightLeaf,\n [HorizontalRulePlugin.key]: HrElement,\n [ImagePlugin.key]: ImageElement,\n [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),\n [KbdPlugin.key]: KbdLeaf,\n [LinkPlugin.key]: LinkElement,\n [MediaEmbedPlugin.key]: MediaEmbedElement,\n [MentionInputPlugin.key]: MentionInputElement,\n [MentionPlugin.key]: MentionElement,\n [ParagraphPlugin.key]: ParagraphElement,\n [PlaceholderPlugin.key]: MediaPlaceholderElement,\n [SlashInputPlugin.key]: SlashInputElement,\n [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),\n [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),\n [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),\n [TableCellHeaderPlugin.key]: TableCellHeaderElement,\n [TableCellPlugin.key]: TableCellElement,\n [TablePlugin.key]: TableElement,\n [TableRowPlugin.key]: TableRowElement,\n [TocPlugin.key]: TocElement,\n [TogglePlugin.key]: ToggleElement,\n [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),\n [VideoPlugin.key]: MediaVideoElement,\n })\n ),\n },\n plugins: [\n ...copilotPlugins,\n ...editorPlugins,\n FixedToolbarPlugin,\n FloatingToolbarPlugin,\n ],\n value: [\n {\n children: [{ text: 'Playground' }],\n type: 'h1',\n },\n {\n children: [\n { text: 'A rich-text editor with AI capabilities. Try the ' },\n { bold: true, text: 'AI commands' },\n { text: ' or use ' },\n { kbd: true, text: 'Cmd+J' },\n { text: ' to open the AI menu.' },\n ],\n type: ParagraphPlugin.key,\n },\n ],\n });\n};\n", "path": "block/editor-ai/components/editor/use-create-editor.tsx", "target": "components/editor/use-create-editor.tsx", "type": "registry:component" @@ -77,7 +77,11 @@ "image-element", "kbd-leaf", "link-element", + "media-audio-element", "media-embed-element", + "media-file-element", + "media-placeholder-element", + "media-video-element", "mention-element", "mention-input-element", "paragraph-element", diff --git a/apps/www/public/r/styles/default/media-placeholder-element.json b/apps/www/public/r/styles/default/media-placeholder-element.json index 3d32431387..f262a1548c 100644 --- a/apps/www/public/r/styles/default/media-placeholder-element.json +++ b/apps/www/public/r/styles/default/media-placeholder-element.json @@ -23,7 +23,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { TPlaceholderElement } from '@udecode/plate-media';\n\nimport { cn } from '@udecode/cn';\nimport {\n insertNodes,\n removeNodes,\n withoutSavingHistory,\n} from '@udecode/plate-common';\nimport {\n findNodePath,\n useEditorPlugin,\n withHOC,\n withRef,\n} from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n PlaceholderPlugin,\n PlaceholderProvider,\n VideoPlugin,\n updateUploadHistory,\n} from '@udecode/plate-media/react';\nimport { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport { useUploadFile } from '@/lib/uploadthing';\n\nimport { PlateElement } from './plate-element';\nimport { Spinner } from './spinner';\n\nconst CONTENT: Record<\n string,\n {\n accept: string[];\n content: ReactNode;\n icon: ReactNode;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n content: 'Add an audio file',\n icon: ,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: ,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: ,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: ,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef(\n ({ children, className, editor, nodeProps, ...props }, ref) => {\n const element = props.element as TPlaceholderElement;\n\n const { api } = useEditorPlugin(PlaceholderPlugin);\n\n const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } =\n useUploadFile('imageUploader');\n\n const loading = isUploading && uploadingFile;\n\n const currentContent = CONTENT[element.mediaType];\n\n const isImage = element.mediaType === ImagePlugin.key;\n\n const imageRef = useRef(null);\n\n const { openFilePicker } = useFilePicker({\n accept: currentContent.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n const firstFile = updatedFiles[0];\n const restFiles = updatedFiles.slice(1);\n\n replaceCurrentPlaceholder(firstFile);\n\n restFiles.length > 0 && (editor as any).tf.insert.media(restFiles);\n },\n });\n\n const replaceCurrentPlaceholder = useCallback(\n (file: File) => {\n void uploadFile(file);\n api.placeholder.addUploadingFile(element.id as string, file);\n },\n [api.placeholder, element.id, uploadFile]\n );\n\n useEffect(() => {\n if (!uploadedFile) return;\n\n const path = findNodePath(editor, element);\n\n withoutSavingHistory(editor, () => {\n removeNodes(editor, { at: path });\n\n const node = {\n children: [{ text: '' }],\n initialHeight: imageRef.current?.height,\n initialWidth: imageRef.current?.width,\n isUpload: true,\n name: element.mediaType === FilePlugin.key ? uploadedFile.name : '',\n placeholderId: element.id as string,\n type: element.mediaType!,\n url: uploadedFile.url,\n };\n\n insertNodes(editor, node, { at: path });\n\n updateUploadHistory(editor, node);\n });\n\n api.placeholder.removeUploadingFile(element.id as string);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [uploadedFile, element.id]);\n\n // React dev mode will call useEffect twice\n const isReplaced = useRef(false);\n /** Paste and drop */\n useEffect(() => {\n if (isReplaced.current) return;\n\n isReplaced.current = true;\n const currentFiles = api.placeholder.getUploadingFile(\n element.id as string\n );\n\n if (!currentFiles) return;\n\n replaceCurrentPlaceholder(currentFiles);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isReplaced]);\n\n return (\n \n {(!loading || !isImage) && (\n !loading && openFilePicker()}\n contentEditable={false}\n >\n
\n {currentContent.icon}\n
\n
\n
\n {loading ? uploadingFile?.name : currentContent.content}\n
\n\n {loading && !isImage && (\n
\n
{formatBytes(uploadingFile?.size ?? 0)}
\n
\n
\n \n {progress ?? 0}%\n
\n
\n )}\n
\n
\n )}\n\n {isImage && loading && (\n \n )}\n\n {children}\n \n );\n }\n )\n);\n\nexport function ImageProgress({\n className,\n file,\n imageRef,\n progress = 0,\n}: {\n file: File;\n className?: string;\n imageRef?: React.RefObject;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState(null);\n\n useEffect(() => {\n const url = URL.createObjectURL(file);\n setObjectUrl(url);\n\n return () => {\n URL.revokeObjectURL(url);\n };\n }, [file]);\n\n if (!objectUrl) {\n return null;\n }\n\n return (\n
\n \n {progress < 100 && (\n
\n \n \n {Math.round(progress)}%\n \n
\n )}\n
\n );\n}\n\nexport function formatBytes(\n bytes: number,\n opts: {\n decimals?: number;\n sizeType?: 'accurate' | 'normal';\n } = {}\n) {\n const { decimals = 0, sizeType = 'normal' } = opts;\n\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n if (bytes === 0) return '0 Byte';\n\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n\n return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${\n sizeType === 'accurate'\n ? (accurateSizes[i] ?? 'Bytest')\n : (sizes[i] ?? 'Bytes')\n }`;\n}\n", + "content": "'use client';\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { TPlaceholderElement } from '@udecode/plate-media';\n\nimport { cn } from '@udecode/cn';\nimport {\n insertNodes,\n removeNodes,\n withoutSavingHistory,\n} from '@udecode/plate-common';\nimport {\n findNodePath,\n useEditorPlugin,\n withHOC,\n withRef,\n} from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n PlaceholderPlugin,\n PlaceholderProvider,\n VideoPlugin,\n updateUploadHistory,\n} from '@udecode/plate-media/react';\nimport { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport { useUploadFile } from '@/lib/uploadthing';\n\nimport { PlateElement } from './plate-element';\nimport { Spinner } from './spinner';\n\nconst CONTENT: Record<\n string,\n {\n accept: string[];\n content: ReactNode;\n icon: ReactNode;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n content: 'Add an audio file',\n icon: ,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: ,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: ,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: ,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef(\n ({ children, className, editor, nodeProps, ...props }, ref) => {\n const element = props.element as TPlaceholderElement;\n\n const { api } = useEditorPlugin(PlaceholderPlugin);\n\n const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } =\n useUploadFile('imageUploader');\n\n const loading = isUploading && uploadingFile;\n\n const currentContent = CONTENT[element.mediaType];\n\n const isImage = element.mediaType === ImagePlugin.key;\n\n const imageRef = useRef(null);\n\n const { openFilePicker } = useFilePicker({\n accept: currentContent.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n const firstFile = updatedFiles[0];\n const restFiles = updatedFiles.slice(1);\n\n replaceCurrentPlaceholder(firstFile);\n\n restFiles.length > 0 && (editor as any).tf.insert.media(restFiles);\n },\n });\n\n const replaceCurrentPlaceholder = useCallback(\n (file: File) => {\n void uploadFile(file);\n api.placeholder.addUploadingFile(element.id as string, file);\n },\n [api.placeholder, element.id, uploadFile]\n );\n\n useEffect(() => {\n if (!uploadedFile) return;\n\n const path = findNodePath(editor, element);\n\n withoutSavingHistory(editor, () => {\n removeNodes(editor, { at: path });\n\n const node = {\n children: [{ text: '' }],\n initialHeight: imageRef.current?.height,\n initialWidth: imageRef.current?.width,\n isUpload: true,\n name: element.mediaType === FilePlugin.key ? uploadedFile.name : '',\n placeholderId: element.id as string,\n type: element.mediaType!,\n url: uploadedFile.url,\n };\n\n insertNodes(editor, node, { at: path });\n\n updateUploadHistory(editor, node);\n });\n\n api.placeholder.removeUploadingFile(element.id as string);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [uploadedFile, element.id]);\n\n // React dev mode will call useEffect twice\n const isReplaced = useRef(false);\n\n /** Paste and drop */\n useEffect(() => {\n if (isReplaced.current) return;\n\n isReplaced.current = true;\n const currentFiles = api.placeholder.getUploadingFile(\n element.id as string\n );\n\n if (!currentFiles) return;\n\n replaceCurrentPlaceholder(currentFiles);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isReplaced]);\n\n return (\n \n {(!loading || !isImage) && (\n !loading && openFilePicker()}\n contentEditable={false}\n >\n
\n {currentContent.icon}\n
\n
\n
\n {loading ? uploadingFile?.name : currentContent.content}\n
\n\n {loading && !isImage && (\n
\n
{formatBytes(uploadingFile?.size ?? 0)}
\n
\n
\n \n {progress ?? 0}%\n
\n
\n )}\n
\n
\n )}\n\n {isImage && loading && (\n \n )}\n\n {children}\n \n );\n }\n )\n);\n\nexport function ImageProgress({\n className,\n file,\n imageRef,\n progress = 0,\n}: {\n file: File;\n className?: string;\n imageRef?: React.RefObject;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState(null);\n\n useEffect(() => {\n const url = URL.createObjectURL(file);\n setObjectUrl(url);\n\n return () => {\n URL.revokeObjectURL(url);\n };\n }, [file]);\n\n if (!objectUrl) {\n return null;\n }\n\n return (\n
\n \n {progress < 100 && (\n
\n \n \n {Math.round(progress)}%\n \n
\n )}\n
\n );\n}\n\nexport function formatBytes(\n bytes: number,\n opts: {\n decimals?: number;\n sizeType?: 'accurate' | 'normal';\n } = {}\n) {\n const { decimals = 0, sizeType = 'normal' } = opts;\n\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n if (bytes === 0) return '0 Byte';\n\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n\n return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${\n sizeType === 'accurate'\n ? (accurateSizes[i] ?? 'Bytest')\n : (sizes[i] ?? 'Bytes')\n }`;\n}\n", "path": "plate-ui/media-placeholder-element.tsx", "target": "components/plate-ui/media-placeholder-element.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/media-plugins.json b/apps/www/public/r/styles/default/media-plugins.json index e39edd6702..675f5bab98 100644 --- a/apps/www/public/r/styles/default/media-plugins.json +++ b/apps/www/public/r/styles/default/media-plugins.json @@ -5,7 +5,7 @@ ], "files": [ { - "content": "'use client';\n\nimport { CaptionPlugin } from '@udecode/plate-caption/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n MediaEmbedPlugin,\n PlaceholderPlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\n\nimport { ImagePreview } from '@/components/plate-ui/image-preview';\nimport { MediaUploadToast } from '@/components/plate-ui/media-upload-toast';\n\nexport const mediaPlugins = [\n ImagePlugin.extend({\n options: { disableUploadInsert: true },\n render: { afterEditable: ImagePreview },\n }),\n MediaEmbedPlugin,\n VideoPlugin,\n AudioPlugin,\n FilePlugin,\n CaptionPlugin.configure({\n options: { plugins: [ImagePlugin, MediaEmbedPlugin] },\n }),\n PlaceholderPlugin.configure({\n options: { disableEmptyPlaceholder: true },\n render: { afterEditable: MediaUploadToast },\n }),\n] as const;\n", + "content": "'use client';\n\nimport { CaptionPlugin } from '@udecode/plate-caption/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n MediaEmbedPlugin,\n PlaceholderPlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\n\nimport { ImagePreview } from '@/components/plate-ui/image-preview';\nimport { MediaUploadToast } from '@/components/plate-ui/media-upload-toast';\n\nexport const mediaPlugins = [\n ImagePlugin.extend({\n options: { disableUploadInsert: true },\n render: { afterEditable: ImagePreview },\n }),\n MediaEmbedPlugin,\n VideoPlugin,\n AudioPlugin,\n FilePlugin,\n CaptionPlugin.configure({\n options: {\n plugins: [\n ImagePlugin,\n VideoPlugin,\n AudioPlugin,\n FilePlugin,\n MediaEmbedPlugin,\n ],\n },\n }),\n PlaceholderPlugin.configure({\n options: { disableEmptyPlaceholder: true },\n render: { afterEditable: MediaUploadToast },\n }),\n] as const;\n", "path": "components/editor/plugins/media-plugins.tsx", "target": "components/editor/plugins/media-plugins.tsx", "type": "registry:component" diff --git a/apps/www/public/r/styles/default/media-upload-toast.json b/apps/www/public/r/styles/default/media-upload-toast.json index b09b2a22d6..b19dfef904 100644 --- a/apps/www/public/r/styles/default/media-upload-toast.json +++ b/apps/www/public/r/styles/default/media-upload-toast.json @@ -7,7 +7,7 @@ "description": "Show toast notifications for media uploads.", "docs": [ { - "route": "/docs/media-placeholder", + "route": "/docs/media", "title": "Media Placeholder" } ], diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index 578613da20..bbdcc8ad6e 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -2349,7 +2349,7 @@ export const Index: Record = { name: "editor-ai", description: "An AI editor", type: "registry:block", - registryDependencies: ["api-ai","plate-types","editor-plugins","copilot-plugins","floating-toolbar-plugin","fixed-toolbar-plugin","ai-menu","ghost-text","comments-popover","cursor-overlay","editor","block-context-menu","ai-leaf","blockquote-element","code-block-element","code-leaf","code-line-element","code-syntax-leaf","column-element","column-group-element","comment-leaf","date-element","draggable","emoji-input-element","excalidraw-element","heading-element","highlight-leaf","hr-element","image-element","kbd-leaf","link-element","media-embed-element","mention-element","mention-input-element","paragraph-element","placeholder","slash-input-element","table-cell-element","table-element","table-row-element","toc-element","toggle-element"], + registryDependencies: ["api-ai","plate-types","editor-plugins","copilot-plugins","floating-toolbar-plugin","fixed-toolbar-plugin","ai-menu","ghost-text","comments-popover","cursor-overlay","editor","block-context-menu","ai-leaf","blockquote-element","code-block-element","code-leaf","code-line-element","code-syntax-leaf","column-element","column-group-element","comment-leaf","date-element","draggable","emoji-input-element","excalidraw-element","heading-element","highlight-leaf","hr-element","image-element","kbd-leaf","link-element","media-audio-element","media-embed-element","media-file-element","media-placeholder-element","media-video-element","mention-element","mention-input-element","paragraph-element","placeholder","slash-input-element","table-cell-element","table-element","table-row-element","toc-element","toggle-element"], files: ["registry/default/block/editor-ai/page.tsx","registry/default/block/editor-ai/components/editor/plate-editor.tsx","registry/default/block/editor-ai/components/editor/use-create-editor.tsx"], component: React.lazy(() => import("@/registry/default/block/editor-ai/page.tsx")), source: "src/__registry__/default/block/editor-ai/page.tsx", 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.tsx 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.tsx @@ -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/registry-blocks.ts b/apps/www/src/registry/registry-blocks.ts index 5cf71caa2c..45bfe750bc 100644 --- a/apps/www/src/registry/registry-blocks.ts +++ b/apps/www/src/registry/registry-blocks.ts @@ -80,7 +80,11 @@ export const blocks: Registry = [ 'image-element', 'kbd-leaf', 'link-element', + 'media-audio-element', 'media-embed-element', + 'media-file-element', + 'media-placeholder-element', + 'media-video-element', 'mention-element', 'mention-input-element', 'paragraph-element', From e98e870bc194ef002ee8638e31fb0f051e73e736 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Wed, 20 Nov 2024 01:23:21 +0100 Subject: [PATCH 03/15] docs --- apps/www/content/docs/media.mdx | 2 +- apps/www/public/r/index.json | 11 +++------- .../r/styles/default/api-uploadthing.json | 2 +- .../default/media-placeholder-element.json | 15 ++++--------- .../public/r/styles/default/uploadthing.json | 3 ++- apps/www/src/__registry__/index.tsx | 4 ++-- .../components/api/uploadthing/route.ts | 21 +++---------------- .../src/registry/default/lib/uploadthing.ts | 14 ++++++------- .../plate-ui/media-placeholder-element.tsx | 2 +- apps/www/src/registry/registry-lib.ts | 2 +- apps/www/src/registry/registry-ui.ts | 11 +++------- 11 files changed, 27 insertions(+), 60 deletions(-) diff --git a/apps/www/content/docs/media.mdx b/apps/www/content/docs/media.mdx index 26da9e4fa1..5100f0c526 100644 --- a/apps/www/content/docs/media.mdx +++ b/apps/www/content/docs/media.mdx @@ -134,7 +134,7 @@ There are two ways to implement file uploads in your editor: 3. Add your UploadThing secret key to `.env`: ```bash title=".env" -UPLOADTHING_SECRET=sk_live_xxx +UPLOADTHING_TOKEN=sk_live_xxx ``` #### Custom Implementation diff --git a/apps/www/public/r/index.json b/apps/www/public/r/index.json index f1e8a420a7..2da1042058 100644 --- a/apps/www/public/r/index.json +++ b/apps/www/public/r/index.json @@ -691,9 +691,7 @@ { "dependencies": [ "@udecode/plate-media", - "use-file-picker", - "@uploadthing/react@7.1.0", - "uploadthing@7.2.0" + "use-file-picker" ], "doc": { "description": "A placeholder for media upload progress indication.", @@ -715,16 +713,13 @@ { "path": "plate-ui/media-placeholder-element.tsx", "type": "registry:ui" - }, - { - "path": "lib/uploadthing.ts", - "type": "registry:ui" } ], "name": "media-placeholder-element", "registryDependencies": [ "plate-element", - "spinner" + "spinner", + "uploadthing" ], "type": "registry:ui" }, diff --git a/apps/www/public/r/styles/default/api-uploadthing.json b/apps/www/public/r/styles/default/api-uploadthing.json index 7e021a2c51..05e9e24372 100644 --- a/apps/www/public/r/styles/default/api-uploadthing.json +++ b/apps/www/public/r/styles/default/api-uploadthing.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "import type { FileRouter } from 'uploadthing/next';\n\nimport { createRouteHandler, createUploadthing } from 'uploadthing/next';\n\nconst f = createUploadthing();\n\n// FileRouter for your app, can contain multiple FileRoutes\nconst ourFileRouter = {\n // Define as many FileRoutes as you like, each with a unique routeSlug\n imageUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio'])\n // Set permissions and file types for this FileRoute\n // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await\n .middleware(async ({ req }) => {\n // This code runs on your server before upload\n\n // Whatever is returned here is accessible in onUploadComplete as `metadata`\n return {};\n })\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n .onUploadComplete(({ file, metadata }) => {\n // This code RUNS ON YOUR SERVER after upload\n\n // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback\n return { file };\n }),\n} satisfies FileRouter;\n\nexport type OurFileRouter = typeof ourFileRouter;\n\n// Export routes for Next App Router\nexport const { GET, POST } = createRouteHandler({\n router: ourFileRouter,\n\n // Apply an (optional) custom config:\n // config: { ... },\n});\n", + "content": "import type { FileRouter } from 'uploadthing/next';\n\nimport { createRouteHandler, createUploadthing } from 'uploadthing/next';\n\nconst f = createUploadthing();\n\nconst ourFileRouter = {\n editorUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio'])\n .middleware(() => {\n return {};\n })\n .onUploadComplete(({ file }) => {\n return { file };\n }),\n} satisfies FileRouter;\n\nexport type OurFileRouter = typeof ourFileRouter;\n\nexport const { GET, POST } = createRouteHandler({\n router: ourFileRouter,\n});\n", "path": "components/api/uploadthing/route.ts", "target": "app/api/uploadthing/route.ts", "type": "registry:page" diff --git a/apps/www/public/r/styles/default/media-placeholder-element.json b/apps/www/public/r/styles/default/media-placeholder-element.json index f262a1548c..53ecedd10e 100644 --- a/apps/www/public/r/styles/default/media-placeholder-element.json +++ b/apps/www/public/r/styles/default/media-placeholder-element.json @@ -1,9 +1,7 @@ { "dependencies": [ "@udecode/plate-media", - "use-file-picker", - "@uploadthing/react@7.1.0", - "uploadthing@7.2.0" + "use-file-picker" ], "doc": { "description": "A placeholder for media upload progress indication.", @@ -23,22 +21,17 @@ }, "files": [ { - "content": "'use client';\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { TPlaceholderElement } from '@udecode/plate-media';\n\nimport { cn } from '@udecode/cn';\nimport {\n insertNodes,\n removeNodes,\n withoutSavingHistory,\n} from '@udecode/plate-common';\nimport {\n findNodePath,\n useEditorPlugin,\n withHOC,\n withRef,\n} from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n PlaceholderPlugin,\n PlaceholderProvider,\n VideoPlugin,\n updateUploadHistory,\n} from '@udecode/plate-media/react';\nimport { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport { useUploadFile } from '@/lib/uploadthing';\n\nimport { PlateElement } from './plate-element';\nimport { Spinner } from './spinner';\n\nconst CONTENT: Record<\n string,\n {\n accept: string[];\n content: ReactNode;\n icon: ReactNode;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n content: 'Add an audio file',\n icon: ,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: ,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: ,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: ,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef(\n ({ children, className, editor, nodeProps, ...props }, ref) => {\n const element = props.element as TPlaceholderElement;\n\n const { api } = useEditorPlugin(PlaceholderPlugin);\n\n const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } =\n useUploadFile('imageUploader');\n\n const loading = isUploading && uploadingFile;\n\n const currentContent = CONTENT[element.mediaType];\n\n const isImage = element.mediaType === ImagePlugin.key;\n\n const imageRef = useRef(null);\n\n const { openFilePicker } = useFilePicker({\n accept: currentContent.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n const firstFile = updatedFiles[0];\n const restFiles = updatedFiles.slice(1);\n\n replaceCurrentPlaceholder(firstFile);\n\n restFiles.length > 0 && (editor as any).tf.insert.media(restFiles);\n },\n });\n\n const replaceCurrentPlaceholder = useCallback(\n (file: File) => {\n void uploadFile(file);\n api.placeholder.addUploadingFile(element.id as string, file);\n },\n [api.placeholder, element.id, uploadFile]\n );\n\n useEffect(() => {\n if (!uploadedFile) return;\n\n const path = findNodePath(editor, element);\n\n withoutSavingHistory(editor, () => {\n removeNodes(editor, { at: path });\n\n const node = {\n children: [{ text: '' }],\n initialHeight: imageRef.current?.height,\n initialWidth: imageRef.current?.width,\n isUpload: true,\n name: element.mediaType === FilePlugin.key ? uploadedFile.name : '',\n placeholderId: element.id as string,\n type: element.mediaType!,\n url: uploadedFile.url,\n };\n\n insertNodes(editor, node, { at: path });\n\n updateUploadHistory(editor, node);\n });\n\n api.placeholder.removeUploadingFile(element.id as string);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [uploadedFile, element.id]);\n\n // React dev mode will call useEffect twice\n const isReplaced = useRef(false);\n\n /** Paste and drop */\n useEffect(() => {\n if (isReplaced.current) return;\n\n isReplaced.current = true;\n const currentFiles = api.placeholder.getUploadingFile(\n element.id as string\n );\n\n if (!currentFiles) return;\n\n replaceCurrentPlaceholder(currentFiles);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isReplaced]);\n\n return (\n \n {(!loading || !isImage) && (\n !loading && openFilePicker()}\n contentEditable={false}\n >\n
\n {currentContent.icon}\n
\n
\n
\n {loading ? uploadingFile?.name : currentContent.content}\n
\n\n {loading && !isImage && (\n
\n
{formatBytes(uploadingFile?.size ?? 0)}
\n
\n
\n \n {progress ?? 0}%\n
\n
\n )}\n
\n \n )}\n\n {isImage && loading && (\n \n )}\n\n {children}\n \n );\n }\n )\n);\n\nexport function ImageProgress({\n className,\n file,\n imageRef,\n progress = 0,\n}: {\n file: File;\n className?: string;\n imageRef?: React.RefObject;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState(null);\n\n useEffect(() => {\n const url = URL.createObjectURL(file);\n setObjectUrl(url);\n\n return () => {\n URL.revokeObjectURL(url);\n };\n }, [file]);\n\n if (!objectUrl) {\n return null;\n }\n\n return (\n
\n \n {progress < 100 && (\n
\n \n \n {Math.round(progress)}%\n \n
\n )}\n
\n );\n}\n\nexport function formatBytes(\n bytes: number,\n opts: {\n decimals?: number;\n sizeType?: 'accurate' | 'normal';\n } = {}\n) {\n const { decimals = 0, sizeType = 'normal' } = opts;\n\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n if (bytes === 0) return '0 Byte';\n\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n\n return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${\n sizeType === 'accurate'\n ? (accurateSizes[i] ?? 'Bytest')\n : (sizes[i] ?? 'Bytes')\n }`;\n}\n", + "content": "'use client';\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { TPlaceholderElement } from '@udecode/plate-media';\n\nimport { cn } from '@udecode/cn';\nimport {\n insertNodes,\n removeNodes,\n withoutSavingHistory,\n} from '@udecode/plate-common';\nimport {\n findNodePath,\n useEditorPlugin,\n withHOC,\n withRef,\n} from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n PlaceholderPlugin,\n PlaceholderProvider,\n VideoPlugin,\n updateUploadHistory,\n} from '@udecode/plate-media/react';\nimport { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport { useUploadFile } from '@/lib/uploadthing';\n\nimport { PlateElement } from './plate-element';\nimport { Spinner } from './spinner';\n\nconst CONTENT: Record<\n string,\n {\n accept: string[];\n content: ReactNode;\n icon: ReactNode;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n content: 'Add an audio file',\n icon: ,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: ,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: ,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: ,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef(\n ({ children, className, editor, nodeProps, ...props }, ref) => {\n const element = props.element as TPlaceholderElement;\n\n const { api } = useEditorPlugin(PlaceholderPlugin);\n\n const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } =\n useUploadFile();\n\n const loading = isUploading && uploadingFile;\n\n const currentContent = CONTENT[element.mediaType];\n\n const isImage = element.mediaType === ImagePlugin.key;\n\n const imageRef = useRef(null);\n\n const { openFilePicker } = useFilePicker({\n accept: currentContent.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n const firstFile = updatedFiles[0];\n const restFiles = updatedFiles.slice(1);\n\n replaceCurrentPlaceholder(firstFile);\n\n restFiles.length > 0 && (editor as any).tf.insert.media(restFiles);\n },\n });\n\n const replaceCurrentPlaceholder = useCallback(\n (file: File) => {\n void uploadFile(file);\n api.placeholder.addUploadingFile(element.id as string, file);\n },\n [api.placeholder, element.id, uploadFile]\n );\n\n useEffect(() => {\n if (!uploadedFile) return;\n\n const path = findNodePath(editor, element);\n\n withoutSavingHistory(editor, () => {\n removeNodes(editor, { at: path });\n\n const node = {\n children: [{ text: '' }],\n initialHeight: imageRef.current?.height,\n initialWidth: imageRef.current?.width,\n isUpload: true,\n name: element.mediaType === FilePlugin.key ? uploadedFile.name : '',\n placeholderId: element.id as string,\n type: element.mediaType!,\n url: uploadedFile.url,\n };\n\n insertNodes(editor, node, { at: path });\n\n updateUploadHistory(editor, node);\n });\n\n api.placeholder.removeUploadingFile(element.id as string);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [uploadedFile, element.id]);\n\n // React dev mode will call useEffect twice\n const isReplaced = useRef(false);\n\n /** Paste and drop */\n useEffect(() => {\n if (isReplaced.current) return;\n\n isReplaced.current = true;\n const currentFiles = api.placeholder.getUploadingFile(\n element.id as string\n );\n\n if (!currentFiles) return;\n\n replaceCurrentPlaceholder(currentFiles);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isReplaced]);\n\n return (\n \n {(!loading || !isImage) && (\n !loading && openFilePicker()}\n contentEditable={false}\n >\n
\n {currentContent.icon}\n
\n
\n
\n {loading ? uploadingFile?.name : currentContent.content}\n
\n\n {loading && !isImage && (\n
\n
{formatBytes(uploadingFile?.size ?? 0)}
\n
\n
\n \n {progress ?? 0}%\n
\n
\n )}\n
\n \n )}\n\n {isImage && loading && (\n \n )}\n\n {children}\n \n );\n }\n )\n);\n\nexport function ImageProgress({\n className,\n file,\n imageRef,\n progress = 0,\n}: {\n file: File;\n className?: string;\n imageRef?: React.RefObject;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState(null);\n\n useEffect(() => {\n const url = URL.createObjectURL(file);\n setObjectUrl(url);\n\n return () => {\n URL.revokeObjectURL(url);\n };\n }, [file]);\n\n if (!objectUrl) {\n return null;\n }\n\n return (\n
\n \n {progress < 100 && (\n
\n \n \n {Math.round(progress)}%\n \n
\n )}\n
\n );\n}\n\nexport function formatBytes(\n bytes: number,\n opts: {\n decimals?: number;\n sizeType?: 'accurate' | 'normal';\n } = {}\n) {\n const { decimals = 0, sizeType = 'normal' } = opts;\n\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n if (bytes === 0) return '0 Byte';\n\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n\n return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${\n sizeType === 'accurate'\n ? (accurateSizes[i] ?? 'Bytest')\n : (sizes[i] ?? 'Bytes')\n }`;\n}\n", "path": "plate-ui/media-placeholder-element.tsx", "target": "components/plate-ui/media-placeholder-element.tsx", "type": "registry:ui" - }, - { - "content": "import * as React from 'react';\n\nimport type { OurFileRouter } from '@/components/api/uploadthing/route';\nimport type {\n ClientUploadedFileData,\n UploadFilesOptions,\n} from 'uploadthing/types';\n\nimport { generateReactHelpers } from '@uploadthing/react';\nimport { isRedirectError } from 'next/dist/client/components/redirect';\nimport { toast } from 'sonner';\nimport { z } from 'zod';\n\nexport interface UploadedFile extends ClientUploadedFileData {}\n\ninterface UseUploadFileProps\n extends Pick<\n UploadFilesOptions,\n 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling'\n > {\n onUploadComplete?: (file: UploadedFile) => void;\n onUploadError?: (error: unknown) => void;\n}\n\nexport function useUploadFile(\n endpoint: keyof OurFileRouter,\n { onUploadComplete, onUploadError, ...props }: UseUploadFileProps = {}\n) {\n const [uploadedFile, setUploadedFile] = React.useState();\n const [uploadingFile, setUploadingFile] = React.useState();\n const [progress, setProgress] = React.useState(0);\n const [isUploading, setIsUploading] = React.useState(false);\n\n async function uploadThing(file: File) {\n setIsUploading(true);\n setUploadingFile(file);\n\n try {\n const res = await uploadFiles(endpoint, {\n ...props,\n files: [file],\n onUploadProgress: ({ progress }) => {\n setProgress(Math.min(progress, 100));\n },\n });\n\n setUploadedFile(res[0]);\n\n onUploadComplete?.(res[0]);\n\n return uploadedFile;\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n const message =\n errorMessage.length > 0\n ? errorMessage\n : 'Something went wrong, please try again later.';\n\n toast.error(message);\n\n onUploadError?.(error);\n\n // Mock upload for unauthenticated users\n // toast.info('User not logged in. Mocking upload process.');\n const mockUploadedFile = {\n key: 'mock-key-0',\n appUrl: `https://mock-app-url.com/${file.name}`,\n name: file.name,\n size: file.size,\n type: file.type,\n url: URL.createObjectURL(file),\n } as UploadedFile;\n\n // Simulate upload progress\n let progress = 0;\n\n const simulateProgress = async () => {\n while (progress < 100) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n progress += 2;\n setProgress(Math.min(progress, 100));\n }\n };\n\n await simulateProgress();\n\n setUploadedFile(mockUploadedFile);\n\n return mockUploadedFile;\n } finally {\n setProgress(0);\n setIsUploading(false);\n setUploadingFile(undefined);\n }\n }\n\n return {\n isUploading,\n progress,\n uploadFile: uploadThing,\n uploadedFile,\n uploadingFile,\n };\n}\n\nexport const { uploadFiles, useUploadThing } =\n generateReactHelpers();\n\nexport function getErrorMessage(err: unknown) {\n const unknownError = 'Something went wrong, please try again later.';\n\n if (err instanceof z.ZodError) {\n const errors = err.issues.map((issue) => {\n return issue.message;\n });\n\n return errors.join('\\n');\n } else if (err instanceof Error) {\n return err.message;\n } else if (isRedirectError(err)) {\n throw err;\n } else {\n return unknownError;\n }\n}\n\nexport function showErrorToast(err: unknown) {\n const errorMessage = getErrorMessage(err);\n\n return toast.error(errorMessage);\n}\n", - "path": "lib/uploadthing.ts", - "target": "components/plate-ui/uploadthing.ts", - "type": "registry:ui" } ], "name": "media-placeholder-element", "registryDependencies": [ "plate-element", - "spinner" + "spinner", + "uploadthing" ], "type": "registry:ui" } \ No newline at end of file diff --git a/apps/www/public/r/styles/default/uploadthing.json b/apps/www/public/r/styles/default/uploadthing.json index 5685781e1b..6a0aba7f26 100644 --- a/apps/www/public/r/styles/default/uploadthing.json +++ b/apps/www/public/r/styles/default/uploadthing.json @@ -1,11 +1,12 @@ { "dependencies": [ "uploadthing@7.2.0", + "@uploadthing/react@7.1.0", "sonner" ], "files": [ { - "content": "import * as React from 'react';\n\nimport type { OurFileRouter } from '@/components/api/uploadthing/route';\nimport type {\n ClientUploadedFileData,\n UploadFilesOptions,\n} from 'uploadthing/types';\n\nimport { generateReactHelpers } from '@uploadthing/react';\nimport { isRedirectError } from 'next/dist/client/components/redirect';\nimport { toast } from 'sonner';\nimport { z } from 'zod';\n\nexport interface UploadedFile extends ClientUploadedFileData {}\n\ninterface UseUploadFileProps\n extends Pick<\n UploadFilesOptions,\n 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling'\n > {\n onUploadComplete?: (file: UploadedFile) => void;\n onUploadError?: (error: unknown) => void;\n}\n\nexport function useUploadFile(\n endpoint: keyof OurFileRouter,\n { onUploadComplete, onUploadError, ...props }: UseUploadFileProps = {}\n) {\n const [uploadedFile, setUploadedFile] = React.useState();\n const [uploadingFile, setUploadingFile] = React.useState();\n const [progress, setProgress] = React.useState(0);\n const [isUploading, setIsUploading] = React.useState(false);\n\n async function uploadThing(file: File) {\n setIsUploading(true);\n setUploadingFile(file);\n\n try {\n const res = await uploadFiles(endpoint, {\n ...props,\n files: [file],\n onUploadProgress: ({ progress }) => {\n setProgress(Math.min(progress, 100));\n },\n });\n\n setUploadedFile(res[0]);\n\n onUploadComplete?.(res[0]);\n\n return uploadedFile;\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n const message =\n errorMessage.length > 0\n ? errorMessage\n : 'Something went wrong, please try again later.';\n\n toast.error(message);\n\n onUploadError?.(error);\n\n // Mock upload for unauthenticated users\n // toast.info('User not logged in. Mocking upload process.');\n const mockUploadedFile = {\n key: 'mock-key-0',\n appUrl: `https://mock-app-url.com/${file.name}`,\n name: file.name,\n size: file.size,\n type: file.type,\n url: URL.createObjectURL(file),\n } as UploadedFile;\n\n // Simulate upload progress\n let progress = 0;\n\n const simulateProgress = async () => {\n while (progress < 100) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n progress += 2;\n setProgress(Math.min(progress, 100));\n }\n };\n\n await simulateProgress();\n\n setUploadedFile(mockUploadedFile);\n\n return mockUploadedFile;\n } finally {\n setProgress(0);\n setIsUploading(false);\n setUploadingFile(undefined);\n }\n }\n\n return {\n isUploading,\n progress,\n uploadFile: uploadThing,\n uploadedFile,\n uploadingFile,\n };\n}\n\nexport const { uploadFiles, useUploadThing } =\n generateReactHelpers();\n\nexport function getErrorMessage(err: unknown) {\n const unknownError = 'Something went wrong, please try again later.';\n\n if (err instanceof z.ZodError) {\n const errors = err.issues.map((issue) => {\n return issue.message;\n });\n\n return errors.join('\\n');\n } else if (err instanceof Error) {\n return err.message;\n } else if (isRedirectError(err)) {\n throw err;\n } else {\n return unknownError;\n }\n}\n\nexport function showErrorToast(err: unknown) {\n const errorMessage = getErrorMessage(err);\n\n return toast.error(errorMessage);\n}\n", + "content": "import * as React from 'react';\n\nimport type { OurFileRouter } from '@/components/api/uploadthing/route';\nimport type {\n ClientUploadedFileData,\n UploadFilesOptions,\n} from 'uploadthing/types';\n\nimport { generateReactHelpers } from '@uploadthing/react';\nimport { toast } from 'sonner';\nimport { z } from 'zod';\n\nexport interface UploadedFile extends ClientUploadedFileData {}\n\ninterface UseUploadFileProps\n extends Pick<\n UploadFilesOptions,\n 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling'\n > {\n onUploadComplete?: (file: UploadedFile) => void;\n onUploadError?: (error: unknown) => void;\n}\n\nexport function useUploadFile({\n onUploadComplete,\n onUploadError,\n ...props\n}: UseUploadFileProps = {}) {\n const [uploadedFile, setUploadedFile] = React.useState();\n const [uploadingFile, setUploadingFile] = React.useState();\n const [progress, setProgress] = React.useState(0);\n const [isUploading, setIsUploading] = React.useState(false);\n\n async function uploadThing(file: File) {\n setIsUploading(true);\n setUploadingFile(file);\n\n try {\n const res = await uploadFiles('editorUploader', {\n ...props,\n files: [file],\n onUploadProgress: ({ progress }) => {\n setProgress(Math.min(progress, 100));\n },\n });\n\n setUploadedFile(res[0]);\n\n onUploadComplete?.(res[0]);\n\n return uploadedFile;\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n const message =\n errorMessage.length > 0\n ? errorMessage\n : 'Something went wrong, please try again later.';\n\n toast.error(message);\n\n onUploadError?.(error);\n\n // Mock upload for unauthenticated users\n // toast.info('User not logged in. Mocking upload process.');\n const mockUploadedFile = {\n key: 'mock-key-0',\n appUrl: `https://mock-app-url.com/${file.name}`,\n name: file.name,\n size: file.size,\n type: file.type,\n url: URL.createObjectURL(file),\n } as UploadedFile;\n\n // Simulate upload progress\n let progress = 0;\n\n const simulateProgress = async () => {\n while (progress < 100) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n progress += 2;\n setProgress(Math.min(progress, 100));\n }\n };\n\n await simulateProgress();\n\n setUploadedFile(mockUploadedFile);\n\n return mockUploadedFile;\n } finally {\n setProgress(0);\n setIsUploading(false);\n setUploadingFile(undefined);\n }\n }\n\n return {\n isUploading,\n progress,\n uploadFile: uploadThing,\n uploadedFile,\n uploadingFile,\n };\n}\n\nexport const { uploadFiles, useUploadThing } =\n generateReactHelpers();\n\nexport function getErrorMessage(err: unknown) {\n const unknownError = 'Something went wrong, please try again later.';\n\n if (err instanceof z.ZodError) {\n const errors = err.issues.map((issue) => {\n return issue.message;\n });\n\n return errors.join('\\n');\n } else if (err instanceof Error) {\n return err.message;\n } else {\n return unknownError;\n }\n}\n\nexport function showErrorToast(err: unknown) {\n const errorMessage = getErrorMessage(err);\n\n return toast.error(errorMessage);\n}\n", "path": "lib/uploadthing.ts", "target": "lib/uploadthing.ts", "type": "registry:lib" diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index bbdcc8ad6e..91665574d0 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -285,8 +285,8 @@ export const Index: Record = { name: "media-placeholder-element", description: "", type: "registry:ui", - registryDependencies: ["plate-element","spinner"], - files: ["registry/default/plate-ui/media-placeholder-element.tsx","registry/default/lib/uploadthing.ts"], + registryDependencies: ["plate-element","spinner","uploadthing"], + files: ["registry/default/plate-ui/media-placeholder-element.tsx"], component: React.lazy(() => import("@/registry/default/plate-ui/media-placeholder-element.tsx")), source: "", category: "", diff --git a/apps/www/src/registry/default/components/api/uploadthing/route.ts b/apps/www/src/registry/default/components/api/uploadthing/route.ts index d19e1ed2f3..f3d0c2d5b3 100644 --- a/apps/www/src/registry/default/components/api/uploadthing/route.ts +++ b/apps/www/src/registry/default/components/api/uploadthing/route.ts @@ -4,33 +4,18 @@ 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` + editorUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio']) + .middleware(() => { 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 + .onUploadComplete(({ file }) => { 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/lib/uploadthing.ts b/apps/www/src/registry/default/lib/uploadthing.ts index 2240fd4b49..b5f1e5cb3d 100644 --- a/apps/www/src/registry/default/lib/uploadthing.ts +++ b/apps/www/src/registry/default/lib/uploadthing.ts @@ -7,7 +7,6 @@ import type { } from 'uploadthing/types'; import { generateReactHelpers } from '@uploadthing/react'; -import { isRedirectError } from 'next/dist/client/components/redirect'; import { toast } from 'sonner'; import { z } from 'zod'; @@ -22,10 +21,11 @@ interface UseUploadFileProps onUploadError?: (error: unknown) => void; } -export function useUploadFile( - endpoint: keyof OurFileRouter, - { onUploadComplete, onUploadError, ...props }: UseUploadFileProps = {} -) { +export function useUploadFile({ + onUploadComplete, + onUploadError, + ...props +}: UseUploadFileProps = {}) { const [uploadedFile, setUploadedFile] = React.useState(); const [uploadingFile, setUploadingFile] = React.useState(); const [progress, setProgress] = React.useState(0); @@ -36,7 +36,7 @@ export function useUploadFile( setUploadingFile(file); try { - const res = await uploadFiles(endpoint, { + const res = await uploadFiles('editorUploader', { ...props, files: [file], onUploadProgress: ({ progress }) => { @@ -118,8 +118,6 @@ export function getErrorMessage(err: unknown) { return errors.join('\n'); } else if (err instanceof Error) { return err.message; - } else if (isRedirectError(err)) { - throw err; } else { return unknownError; } diff --git a/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx b/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx index 6837900460..1d7afa8d96 100644 --- a/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx +++ b/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx @@ -73,7 +73,7 @@ export const MediaPlaceholderElement = withHOC( const { api } = useEditorPlugin(PlaceholderPlugin); const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } = - useUploadFile('imageUploader'); + useUploadFile(); const loading = isUploading && uploadingFile; diff --git a/apps/www/src/registry/registry-lib.ts b/apps/www/src/registry/registry-lib.ts index 216e62c2c0..3bb8117c96 100644 --- a/apps/www/src/registry/registry-lib.ts +++ b/apps/www/src/registry/registry-lib.ts @@ -13,7 +13,7 @@ export const lib: Registry = [ type: 'registry:lib', }, { - dependencies: ['uploadthing@7.2.0', 'sonner'], + dependencies: ['uploadthing@7.2.0', '@uploadthing/react@7.1.0', 'sonner'], files: [ { path: 'lib/uploadthing.ts', diff --git a/apps/www/src/registry/registry-ui.ts b/apps/www/src/registry/registry-ui.ts index 5558cf1a9b..5a87df3bf6 100644 --- a/apps/www/src/registry/registry-ui.ts +++ b/apps/www/src/registry/registry-ui.ts @@ -1247,12 +1247,7 @@ export const uiNodes: Registry = [ type: 'registry:ui', }, { - dependencies: [ - '@udecode/plate-media', - 'use-file-picker', - '@uploadthing/react@7.1.0', - 'uploadthing@7.2.0', - ], + dependencies: ['@udecode/plate-media', 'use-file-picker'], doc: { description: 'A placeholder for media upload progress indication.', docs: [ @@ -1265,9 +1260,9 @@ export const uiNodes: Registry = [ ], examples: ['media-demo', 'upload-pro'], }, - files: ['plate-ui/media-placeholder-element.tsx', 'lib/uploadthing.ts'], + files: ['plate-ui/media-placeholder-element.tsx'], name: 'media-placeholder-element', - registryDependencies: ['plate-element', 'spinner'], + registryDependencies: ['plate-element', 'spinner', 'uploadthing'], type: 'registry:ui', }, { From 11478ab42a3ff54bcf13aa436c8914e72527649a Mon Sep 17 00:00:00 2001 From: zbeyens Date: Wed, 20 Nov 2024 02:10:21 +0100 Subject: [PATCH 04/15] docs --- apps/www/content/docs/media.mdx | 1 + .../public/r/styles/default/editor-ai.json | 1 + .../public/r/styles/default/uploadthing.json | 5 +- apps/www/scripts/build-registry.mts | 2 +- apps/www/scripts/fix-import.mts | 4 +- apps/www/src/__registry__/index.tsx | 50 +++++++++---------- .../api/ai/command/route.ts | 0 .../api/ai/copilot/route.ts | 0 .../api/uploadthing/route.ts | 0 .../src/registry/default/lib/uploadthing.ts | 2 +- apps/www/src/registry/registry-app.ts | 18 +++++++ apps/www/src/registry/registry-blocks.ts | 1 + apps/www/src/registry/registry-components.ts | 31 ------------ apps/www/src/registry/registry-lib.ts | 7 ++- apps/www/src/registry/registry.ts | 2 + apps/www/src/registry/schema.ts | 1 + package.json | 3 +- scripts/add-ai.sh | 4 +- scripts/post-registry.sh | 1 + scripts/pre-registry.sh | 1 + 20 files changed, 70 insertions(+), 64 deletions(-) rename apps/www/src/registry/default/{components => app}/api/ai/command/route.ts (100%) rename apps/www/src/registry/default/{components => app}/api/ai/copilot/route.ts (100%) rename apps/www/src/registry/default/{components => app}/api/uploadthing/route.ts (100%) create mode 100644 apps/www/src/registry/registry-app.ts create mode 100644 scripts/post-registry.sh create mode 100644 scripts/pre-registry.sh diff --git a/apps/www/content/docs/media.mdx b/apps/www/content/docs/media.mdx index 5100f0c526..5838d45b16 100644 --- a/apps/www/content/docs/media.mdx +++ b/apps/www/content/docs/media.mdx @@ -659,6 +659,7 @@ The key of the media embed element. +The options for inserting nodes. diff --git a/apps/www/public/r/styles/default/editor-ai.json b/apps/www/public/r/styles/default/editor-ai.json index b06ea7a399..8957fc0e2a 100644 --- a/apps/www/public/r/styles/default/editor-ai.json +++ b/apps/www/public/r/styles/default/editor-ai.json @@ -47,6 +47,7 @@ "name": "editor-ai", "registryDependencies": [ "api-ai", + "api-uploadthing", "plate-types", "editor-plugins", "copilot-plugins", diff --git a/apps/www/public/r/styles/default/uploadthing.json b/apps/www/public/r/styles/default/uploadthing.json index 6a0aba7f26..6ab9430424 100644 --- a/apps/www/public/r/styles/default/uploadthing.json +++ b/apps/www/public/r/styles/default/uploadthing.json @@ -2,11 +2,12 @@ "dependencies": [ "uploadthing@7.2.0", "@uploadthing/react@7.1.0", - "sonner" + "sonner", + "zod" ], "files": [ { - "content": "import * as React from 'react';\n\nimport type { OurFileRouter } from '@/components/api/uploadthing/route';\nimport type {\n ClientUploadedFileData,\n UploadFilesOptions,\n} from 'uploadthing/types';\n\nimport { generateReactHelpers } from '@uploadthing/react';\nimport { toast } from 'sonner';\nimport { z } from 'zod';\n\nexport interface UploadedFile extends ClientUploadedFileData {}\n\ninterface UseUploadFileProps\n extends Pick<\n UploadFilesOptions,\n 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling'\n > {\n onUploadComplete?: (file: UploadedFile) => void;\n onUploadError?: (error: unknown) => void;\n}\n\nexport function useUploadFile({\n onUploadComplete,\n onUploadError,\n ...props\n}: UseUploadFileProps = {}) {\n const [uploadedFile, setUploadedFile] = React.useState();\n const [uploadingFile, setUploadingFile] = React.useState();\n const [progress, setProgress] = React.useState(0);\n const [isUploading, setIsUploading] = React.useState(false);\n\n async function uploadThing(file: File) {\n setIsUploading(true);\n setUploadingFile(file);\n\n try {\n const res = await uploadFiles('editorUploader', {\n ...props,\n files: [file],\n onUploadProgress: ({ progress }) => {\n setProgress(Math.min(progress, 100));\n },\n });\n\n setUploadedFile(res[0]);\n\n onUploadComplete?.(res[0]);\n\n return uploadedFile;\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n const message =\n errorMessage.length > 0\n ? errorMessage\n : 'Something went wrong, please try again later.';\n\n toast.error(message);\n\n onUploadError?.(error);\n\n // Mock upload for unauthenticated users\n // toast.info('User not logged in. Mocking upload process.');\n const mockUploadedFile = {\n key: 'mock-key-0',\n appUrl: `https://mock-app-url.com/${file.name}`,\n name: file.name,\n size: file.size,\n type: file.type,\n url: URL.createObjectURL(file),\n } as UploadedFile;\n\n // Simulate upload progress\n let progress = 0;\n\n const simulateProgress = async () => {\n while (progress < 100) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n progress += 2;\n setProgress(Math.min(progress, 100));\n }\n };\n\n await simulateProgress();\n\n setUploadedFile(mockUploadedFile);\n\n return mockUploadedFile;\n } finally {\n setProgress(0);\n setIsUploading(false);\n setUploadingFile(undefined);\n }\n }\n\n return {\n isUploading,\n progress,\n uploadFile: uploadThing,\n uploadedFile,\n uploadingFile,\n };\n}\n\nexport const { uploadFiles, useUploadThing } =\n generateReactHelpers();\n\nexport function getErrorMessage(err: unknown) {\n const unknownError = 'Something went wrong, please try again later.';\n\n if (err instanceof z.ZodError) {\n const errors = err.issues.map((issue) => {\n return issue.message;\n });\n\n return errors.join('\\n');\n } else if (err instanceof Error) {\n return err.message;\n } else {\n return unknownError;\n }\n}\n\nexport function showErrorToast(err: unknown) {\n const errorMessage = getErrorMessage(err);\n\n return toast.error(errorMessage);\n}\n", + "content": "import * as React from 'react';\n\nimport type { OurFileRouter } from '@/app/api/uploadthing/route';\nimport type {\n ClientUploadedFileData,\n UploadFilesOptions,\n} from 'uploadthing/types';\n\nimport { generateReactHelpers } from '@uploadthing/react';\nimport { toast } from 'sonner';\nimport { z } from 'zod';\n\nexport interface UploadedFile extends ClientUploadedFileData {}\n\ninterface UseUploadFileProps\n extends Pick<\n UploadFilesOptions,\n 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling'\n > {\n onUploadComplete?: (file: UploadedFile) => void;\n onUploadError?: (error: unknown) => void;\n}\n\nexport function useUploadFile({\n onUploadComplete,\n onUploadError,\n ...props\n}: UseUploadFileProps = {}) {\n const [uploadedFile, setUploadedFile] = React.useState();\n const [uploadingFile, setUploadingFile] = React.useState();\n const [progress, setProgress] = React.useState(0);\n const [isUploading, setIsUploading] = React.useState(false);\n\n async function uploadThing(file: File) {\n setIsUploading(true);\n setUploadingFile(file);\n\n try {\n const res = await uploadFiles('editorUploader', {\n ...props,\n files: [file],\n onUploadProgress: ({ progress }) => {\n setProgress(Math.min(progress, 100));\n },\n });\n\n setUploadedFile(res[0]);\n\n onUploadComplete?.(res[0]);\n\n return uploadedFile;\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n const message =\n errorMessage.length > 0\n ? errorMessage\n : 'Something went wrong, please try again later.';\n\n toast.error(message);\n\n onUploadError?.(error);\n\n // Mock upload for unauthenticated users\n // toast.info('User not logged in. Mocking upload process.');\n const mockUploadedFile = {\n key: 'mock-key-0',\n appUrl: `https://mock-app-url.com/${file.name}`,\n name: file.name,\n size: file.size,\n type: file.type,\n url: URL.createObjectURL(file),\n } as UploadedFile;\n\n // Simulate upload progress\n let progress = 0;\n\n const simulateProgress = async () => {\n while (progress < 100) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n progress += 2;\n setProgress(Math.min(progress, 100));\n }\n };\n\n await simulateProgress();\n\n setUploadedFile(mockUploadedFile);\n\n return mockUploadedFile;\n } finally {\n setProgress(0);\n setIsUploading(false);\n setUploadingFile(undefined);\n }\n }\n\n return {\n isUploading,\n progress,\n uploadFile: uploadThing,\n uploadedFile,\n uploadingFile,\n };\n}\n\nexport const { uploadFiles, useUploadThing } =\n generateReactHelpers();\n\nexport function getErrorMessage(err: unknown) {\n const unknownError = 'Something went wrong, please try again later.';\n\n if (err instanceof z.ZodError) {\n const errors = err.issues.map((issue) => {\n return issue.message;\n });\n\n return errors.join('\\n');\n } else if (err instanceof Error) {\n return err.message;\n } else {\n return unknownError;\n }\n}\n\nexport function showErrorToast(err: unknown) {\n const errorMessage = getErrorMessage(err);\n\n return toast.error(errorMessage);\n}\n", "path": "lib/uploadthing.ts", "target": "lib/uploadthing.ts", "type": "registry:lib" diff --git a/apps/www/scripts/build-registry.mts b/apps/www/scripts/build-registry.mts index 0afb77f840..81fe3e1ae9 100644 --- a/apps/www/scripts/build-registry.mts +++ b/apps/www/scripts/build-registry.mts @@ -410,7 +410,7 @@ async function buildStyles(registry: Registry) { if (!target || target === "") { const fileName = file.path.split("/").pop() - if (file.type === "registry:component") { + if (file.type === "registry:component" || file.type === "registry:app") { target = file.path } if ( diff --git a/apps/www/scripts/fix-import.mts b/apps/www/scripts/fix-import.mts index 19da908e49..976faf2778 100644 --- a/apps/www/scripts/fix-import.mts +++ b/apps/www/scripts/fix-import.mts @@ -1,5 +1,5 @@ export function fixImport(content: string) { - const regex = /@\/(.+?)\/((?:.*?\/)?(?:components|plate-ui|hooks|lib))\/([\w-]+)/g + const regex = /@\/(.+?)\/((?:.*?\/)?(?:components|plate-ui|hooks|lib|app))\/([\w-]+)/g const replacement = ( match: string, @@ -15,6 +15,8 @@ export function fixImport(content: string) { return `@/hooks/${component}` } else if (type.endsWith("lib")) { return `@/lib/${component}` + } else if (type.endsWith("app")) { + return `@/app/${component}` } return match diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index 91665574d0..c965902c9b 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -1493,30 +1493,6 @@ export const Index: Record = { subcategory: "", chunks: [] }, - "api-ai": { - name: "api-ai", - description: "", - type: "registry:component", - registryDependencies: ["use-chat"], - files: ["registry/default/components/api/ai/command/route.ts","registry/default/components/api/ai/copilot/route.ts"], - component: React.lazy(() => import("@/registry/default/components/api/ai/command/route.ts")), - source: "", - category: "", - subcategory: "", - chunks: [] - }, - "api-uploadthing": { - name: "api-uploadthing", - description: "", - type: "registry:component", - registryDependencies: ["media-placeholder-element","uploadthing"], - files: ["registry/default/components/api/uploadthing/route.ts"], - component: React.lazy(() => import("@/registry/default/components/api/uploadthing/route.ts")), - source: "", - category: "", - subcategory: "", - chunks: [] - }, "transforms": { name: "transforms", description: "", @@ -2345,11 +2321,35 @@ export const Index: Record = { subcategory: "", chunks: [] }, + "api-ai": { + name: "api-ai", + description: "", + type: "registry:app", + registryDependencies: ["use-chat"], + files: ["registry/default/app/api/ai/command/route.ts","registry/default/app/api/ai/copilot/route.ts"], + component: React.lazy(() => import("@/registry/default/app/api/ai/command/route.ts")), + source: "", + category: "", + subcategory: "", + chunks: [] + }, + "api-uploadthing": { + name: "api-uploadthing", + description: "", + type: "registry:app", + registryDependencies: [], + files: ["registry/default/app/api/uploadthing/route.ts"], + component: React.lazy(() => import("@/registry/default/app/api/uploadthing/route.ts")), + source: "", + category: "", + subcategory: "", + chunks: [] + }, "editor-ai": { name: "editor-ai", description: "An AI editor", type: "registry:block", - registryDependencies: ["api-ai","plate-types","editor-plugins","copilot-plugins","floating-toolbar-plugin","fixed-toolbar-plugin","ai-menu","ghost-text","comments-popover","cursor-overlay","editor","block-context-menu","ai-leaf","blockquote-element","code-block-element","code-leaf","code-line-element","code-syntax-leaf","column-element","column-group-element","comment-leaf","date-element","draggable","emoji-input-element","excalidraw-element","heading-element","highlight-leaf","hr-element","image-element","kbd-leaf","link-element","media-audio-element","media-embed-element","media-file-element","media-placeholder-element","media-video-element","mention-element","mention-input-element","paragraph-element","placeholder","slash-input-element","table-cell-element","table-element","table-row-element","toc-element","toggle-element"], + registryDependencies: ["api-ai","api-uploadthing","plate-types","editor-plugins","copilot-plugins","floating-toolbar-plugin","fixed-toolbar-plugin","ai-menu","ghost-text","comments-popover","cursor-overlay","editor","block-context-menu","ai-leaf","blockquote-element","code-block-element","code-leaf","code-line-element","code-syntax-leaf","column-element","column-group-element","comment-leaf","date-element","draggable","emoji-input-element","excalidraw-element","heading-element","highlight-leaf","hr-element","image-element","kbd-leaf","link-element","media-audio-element","media-embed-element","media-file-element","media-placeholder-element","media-video-element","mention-element","mention-input-element","paragraph-element","placeholder","slash-input-element","table-cell-element","table-element","table-row-element","toc-element","toggle-element"], files: ["registry/default/block/editor-ai/page.tsx","registry/default/block/editor-ai/components/editor/plate-editor.tsx","registry/default/block/editor-ai/components/editor/use-create-editor.tsx"], component: React.lazy(() => import("@/registry/default/block/editor-ai/page.tsx")), source: "src/__registry__/default/block/editor-ai/page.tsx", 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/components/api/uploadthing/route.ts b/apps/www/src/registry/default/app/api/uploadthing/route.ts similarity index 100% rename from apps/www/src/registry/default/components/api/uploadthing/route.ts rename to apps/www/src/registry/default/app/api/uploadthing/route.ts diff --git a/apps/www/src/registry/default/lib/uploadthing.ts b/apps/www/src/registry/default/lib/uploadthing.ts index b5f1e5cb3d..375e37a167 100644 --- a/apps/www/src/registry/default/lib/uploadthing.ts +++ b/apps/www/src/registry/default/lib/uploadthing.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import type { OurFileRouter } from '@/registry/default/components/api/uploadthing/route'; +import type { OurFileRouter } from '@/registry/default/app/api/uploadthing/route'; import type { ClientUploadedFileData, UploadFilesOptions, diff --git a/apps/www/src/registry/registry-app.ts b/apps/www/src/registry/registry-app.ts new file mode 100644 index 0000000000..33183485a1 --- /dev/null +++ b/apps/www/src/registry/registry-app.ts @@ -0,0 +1,18 @@ +import type { Registry } from './schema'; + +export const registryApp: Registry = [ + { + dependencies: ['@ai-sdk/openai', 'ai'], + files: ['app/api/ai/command/route.ts', 'app/api/ai/copilot/route.ts'], + name: 'api-ai', + registryDependencies: ['use-chat'], + type: 'registry:app', + }, + { + dependencies: ['uploadthing@7.2.0'], + files: ['app/api/uploadthing/route.ts'], + name: 'api-uploadthing', + registryDependencies: [], + type: 'registry:app', + }, +]; diff --git a/apps/www/src/registry/registry-blocks.ts b/apps/www/src/registry/registry-blocks.ts index 45bfe750bc..27a1133cb0 100644 --- a/apps/www/src/registry/registry-blocks.ts +++ b/apps/www/src/registry/registry-blocks.ts @@ -47,6 +47,7 @@ export const blocks: Registry = [ name: 'editor-ai', registryDependencies: [ 'api-ai', + 'api-uploadthing', 'plate-types', 'editor-plugins', diff --git a/apps/www/src/registry/registry-components.ts b/apps/www/src/registry/registry-components.ts index 4a8495c47a..11e09ea981 100644 --- a/apps/www/src/registry/registry-components.ts +++ b/apps/www/src/registry/registry-components.ts @@ -319,37 +319,6 @@ export const components: Registry = [ registryDependencies: ['button', 'dialog', 'input', 'command', 'popover'], type: 'registry:component', }, - { - dependencies: ['@ai-sdk/openai', 'ai'], - files: [ - { - path: 'components/api/ai/command/route.ts', - target: 'app/api/ai/command/route.ts', - type: 'registry:page', - }, - { - path: 'components/api/ai/copilot/route.ts', - target: 'app/api/ai/copilot/route.ts', - type: 'registry:page', - }, - ], - name: 'api-ai', - registryDependencies: ['use-chat'], - type: 'registry:component', - }, - { - dependencies: ['uploadthing@7.2.0'], - files: [ - { - path: 'components/api/uploadthing/route.ts', - target: 'app/api/uploadthing/route.ts', - type: 'registry:page', - }, - ], - name: 'api-uploadthing', - registryDependencies: ['media-placeholder-element', 'uploadthing'], - type: 'registry:component', - }, { dependencies: [ '@udecode/plate-callout', diff --git a/apps/www/src/registry/registry-lib.ts b/apps/www/src/registry/registry-lib.ts index 3bb8117c96..68cb67649b 100644 --- a/apps/www/src/registry/registry-lib.ts +++ b/apps/www/src/registry/registry-lib.ts @@ -13,7 +13,12 @@ export const lib: Registry = [ type: 'registry:lib', }, { - dependencies: ['uploadthing@7.2.0', '@uploadthing/react@7.1.0', 'sonner'], + dependencies: [ + 'uploadthing@7.2.0', + '@uploadthing/react@7.1.0', + 'sonner', + 'zod', + ], files: [ { path: 'lib/uploadthing.ts', diff --git a/apps/www/src/registry/registry.ts b/apps/www/src/registry/registry.ts index b5f0527253..e0b0c158d5 100644 --- a/apps/www/src/registry/registry.ts +++ b/apps/www/src/registry/registry.ts @@ -1,5 +1,6 @@ import type { Registry } from './schema'; +import { registryApp } from './registry-app'; import { blocks } from './registry-blocks'; import { components } from './registry-components'; import { examples } from './registry-examples'; @@ -12,6 +13,7 @@ export const registry: Registry = [ ...ui, ...components, ...examples, + ...registryApp, ...blocks, ...lib, ...hooks, diff --git a/apps/www/src/registry/schema.ts b/apps/www/src/registry/schema.ts index 60f78fd26a..6500032f1e 100644 --- a/apps/www/src/registry/schema.ts +++ b/apps/www/src/registry/schema.ts @@ -16,6 +16,7 @@ export const blockChunkSchema = z.object({ export const registryItemTypeSchema = z.enum([ 'registry:pro', + 'registry:app', 'registry:style', 'registry:lib', 'registry:example', diff --git a/package.json b/package.json index 19a5d7f923..87faa2adeb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "build:templates": "turbo --filter \"./templates/**\" build", "build:watch": "ROARR_LOG=true turbowatch ./config/turbowatch.config.ts | roarr", "check:install": "yarn dlx @yarnpkg/doctor@4.0.0-rc.10 --configFileName config/.ncurc.yml packages", - "cli:full": "yarn cli:plate && yarn cli:editor-ai", + "cli:ai": "sh ./scripts/pre-registry.sh && npx shadcx@latest add plate/editor-ai -o -c templates/plate-template && sh ./scripts/post-registry.sh", + "cli:sync": "sh ./scripts/pre-registry.sh && npx shadcx@latest add plate/editor-ai -o -c templates/plate-playground-template && sh ./scripts/post-registry.sh", "deps:check": "npx npm-check-updates@latest --configFileName config/ncurc.yml --workspaces --root --mergeConfig", "deps:update": "npx npm-check-updates@latest --configFileName config/ncurc.yml -u --workspaces --root --mergeConfig", "dev": "turbo --filter=www dev", diff --git a/scripts/add-ai.sh b/scripts/add-ai.sh index 2a160ca0dd..248ec58cdf 100644 --- a/scripts/add-ai.sh +++ b/scripts/add-ai.sh @@ -1,4 +1,6 @@ #!/bin/sh # add editor-ai -node ./packages/cli/dist/index.js add plate/editor-ai -c ./templates/plate-template +# node ./packages/cli/dist/index.js add plate/editor-ai -c ./templates/plate-template + +./pre-registry.sh && npx shadcx@latest add plate/editor-ai -o && ./post-registry.sh \ No newline at end of file diff --git a/scripts/post-registry.sh b/scripts/post-registry.sh new file mode 100644 index 0000000000..d8347f0fcc --- /dev/null +++ b/scripts/post-registry.sh @@ -0,0 +1 @@ +sed -i '' 's|"url": "http://localhost:3000/r"|"url": "https://platejs.org/r"|' templates/plate-template/components.json \ No newline at end of file diff --git a/scripts/pre-registry.sh b/scripts/pre-registry.sh new file mode 100644 index 0000000000..6e80693a17 --- /dev/null +++ b/scripts/pre-registry.sh @@ -0,0 +1 @@ +sed -i '' 's|"url": "https://platejs.org/r"|"url": "http://localhost:3000/r"|' templates/plate-template/components.json \ No newline at end of file From c461420874956b46d8d2bd943bb795eb024f815c Mon Sep 17 00:00:00 2001 From: zbeyens Date: Wed, 20 Nov 2024 02:16:56 +0100 Subject: [PATCH 05/15] docs --- package.json | 4 ++-- scripts/post-sync.sh | 1 + scripts/{post-registry.sh => post-test.sh} | 0 scripts/pre-sync.sh | 1 + scripts/{pre-registry.sh => pre-test.sh} | 0 5 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 scripts/post-sync.sh rename scripts/{post-registry.sh => post-test.sh} (100%) create mode 100644 scripts/pre-sync.sh rename scripts/{pre-registry.sh => pre-test.sh} (100%) diff --git a/package.json b/package.json index 87faa2adeb..8dbd48273a 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build:templates": "turbo --filter \"./templates/**\" build", "build:watch": "ROARR_LOG=true turbowatch ./config/turbowatch.config.ts | roarr", "check:install": "yarn dlx @yarnpkg/doctor@4.0.0-rc.10 --configFileName config/.ncurc.yml packages", - "cli:ai": "sh ./scripts/pre-registry.sh && npx shadcx@latest add plate/editor-ai -o -c templates/plate-template && sh ./scripts/post-registry.sh", - "cli:sync": "sh ./scripts/pre-registry.sh && npx shadcx@latest add plate/editor-ai -o -c templates/plate-playground-template && sh ./scripts/post-registry.sh", + "cli:sync": "sh ./scripts/pre-sync.sh && npx shadcx@latest add plate/editor-ai -o -c templates/plate-playground-template && sh ./scripts/post-sync.sh", + "cli:test": "sh ./scripts/pre-test.sh && npx shadcx@latest add plate/editor-ai -o -c templates/plate-template && sh ./scripts/post-test.sh", "deps:check": "npx npm-check-updates@latest --configFileName config/ncurc.yml --workspaces --root --mergeConfig", "deps:update": "npx npm-check-updates@latest --configFileName config/ncurc.yml -u --workspaces --root --mergeConfig", "dev": "turbo --filter=www dev", diff --git a/scripts/post-sync.sh b/scripts/post-sync.sh new file mode 100644 index 0000000000..3a530e27a6 --- /dev/null +++ b/scripts/post-sync.sh @@ -0,0 +1 @@ +sed -i '' 's|"url": "http://localhost:3000/r"|"url": "https://platejs.org/r"|' templates/plate-playground-template/components.json \ No newline at end of file diff --git a/scripts/post-registry.sh b/scripts/post-test.sh similarity index 100% rename from scripts/post-registry.sh rename to scripts/post-test.sh diff --git a/scripts/pre-sync.sh b/scripts/pre-sync.sh new file mode 100644 index 0000000000..145d9cd6b8 --- /dev/null +++ b/scripts/pre-sync.sh @@ -0,0 +1 @@ +sed -i '' 's|"url": "https://platejs.org/r"|"url": "http://localhost:3000/r"|' templates/plate-playground-template/components.json \ No newline at end of file diff --git a/scripts/pre-registry.sh b/scripts/pre-test.sh similarity index 100% rename from scripts/pre-registry.sh rename to scripts/pre-test.sh From b671a27b4fa59054c4f882c90fb5435a2d1f8512 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Wed, 20 Nov 2024 02:17:53 +0100 Subject: [PATCH 06/15] sync --- .../plate-playground-template/package.json | 16 +- .../plate-playground-template/pnpm-lock.yaml | 4 +- .../src/app/api/uploadthing/route.ts | 19 +- .../src/app/editor/page.tsx | 3 - .../src/components/editor/plate-editor.tsx | 2 +- .../components/editor/plugins/ai-plugins.tsx | 18 +- .../editor/plugins/block-selection-plugins.ts | 9 + .../editor/plugins/copilot-plugins.tsx | 60 ++++++ .../editor/plugins/cursor-overlay-plugin.tsx | 11 + .../editor/plugins/delete-plugins.ts | 11 +- .../editor/plugins/editor-plugins.tsx | 35 +-- .../editor/plugins/indent-list-plugins.ts | 5 +- .../editor/plugins/media-plugins.tsx | 22 +- .../src/components/editor/use-chat.tsx | 4 +- .../components/editor/use-create-editor.tsx | 9 +- .../src/components/plate-ui/ai-menu.tsx | 4 +- .../src/components/plate-ui/alert-dialog.tsx | 10 +- .../src/components/plate-ui/avatar.tsx | 2 +- .../components/plate-ui/block-selection.tsx | 4 +- .../src/components/plate-ui/button.tsx | 6 +- .../src/components/plate-ui/checkbox.tsx | 2 +- .../plate-ui/code-block-element.tsx | 2 +- .../src/components/plate-ui/code-leaf.tsx | 2 +- .../plate-ui/color-dropdown-menu-items.tsx | 2 +- .../plate-ui/column-group-element.tsx | 2 +- .../src/components/plate-ui/command.tsx | 14 +- .../src/components/plate-ui/comment-item.tsx | 2 +- .../src/components/plate-ui/comment-leaf.tsx | 2 +- .../plate-ui/comment-more-dropdown.tsx | 2 +- .../plate-ui/comment-resolve-button.tsx | 2 +- .../src/components/plate-ui/context-menu.tsx | 18 +- .../components/plate-ui/cursor-overlay.tsx | 2 +- .../src/components/plate-ui/date-element.tsx | 2 +- .../src/components/plate-ui/dialog.tsx | 8 +- .../src/components/plate-ui/draggable.tsx | 13 +- .../src/components/plate-ui/dropdown-menu.tsx | 16 +- .../src/components/plate-ui/editor.tsx | 7 +- .../plate-ui/emoji-picker-content.tsx | 8 +- .../plate-ui/emoji-picker-navigation.tsx | 6 +- .../plate-ui/emoji-picker-preview.tsx | 6 +- .../emoji-picker-search-and-clear.tsx | 6 +- .../plate-ui/emoji-picker-search-bar.tsx | 2 +- .../src/components/plate-ui/emoji-picker.tsx | 2 +- .../plate-ui/fixed-toolbar-buttons.tsx | 46 ++-- .../src/components/plate-ui/fixed-toolbar.tsx | 2 +- .../plate-ui/floating-toolbar-buttons.tsx | 9 +- .../components/plate-ui/floating-toolbar.tsx | 2 +- .../src/components/plate-ui/ghost-text.tsx | 2 +- .../components/plate-ui/heading-element.tsx | 8 +- .../plate-ui/history-toolbar-button.tsx | 52 +++++ .../src/components/plate-ui/hr-element.tsx | 4 +- .../src/components/plate-ui/image-element.tsx | 2 +- .../plate-ui/indent-todo-toolbar-button.tsx | 4 +- .../components/plate-ui/inline-combobox.tsx | 8 +- .../src/components/plate-ui/input.tsx | 36 +++- .../src/components/plate-ui/kbd-leaf.tsx | 2 +- .../src/components/plate-ui/link-element.tsx | 2 +- .../plate-ui/link-floating-toolbar.tsx | 4 +- .../plate-ui/media-embed-element.tsx | 6 +- .../plate-ui/media-file-element.tsx | 2 +- .../plate-ui/media-placeholder-element.tsx | 12 +- .../src/components/plate-ui/media-popover.tsx | 2 +- .../plate-ui/media-toolbar-button.tsx | 199 ++++++++++-------- .../plate-ui/media-upload-toast.tsx | 2 + .../components/plate-ui/mention-element.tsx | 4 +- .../plate-ui/mention-input-element.tsx | 2 +- .../plate-ui/more-dropdown-menu.tsx | 13 -- .../src/components/plate-ui/popover.tsx | 2 +- .../src/components/plate-ui/resizable.tsx | 2 +- .../src/components/plate-ui/separator.tsx | 2 +- .../plate-ui/slash-input-element.tsx | 2 +- .../src/components/plate-ui/spinner.tsx | 2 +- .../plate-ui/table-cell-element.tsx | 16 +- .../src/components/plate-ui/toc-element.tsx | 2 +- .../components/plate-ui/toggle-element.tsx | 2 +- .../plate-ui/toggle-toolbar-button.tsx | 4 +- .../src/components/plate-ui/toolbar.tsx | 109 ++++------ .../plate-ui/turn-into-dropdown-menu.tsx | 5 +- .../src/lib/uploadthing.ts | 130 ++++++++++++ 79 files changed, 688 insertions(+), 397 deletions(-) create mode 100644 templates/plate-playground-template/src/components/editor/plugins/copilot-plugins.tsx create mode 100644 templates/plate-playground-template/src/components/editor/plugins/cursor-overlay-plugin.tsx create mode 100644 templates/plate-playground-template/src/components/plate-ui/history-toolbar-button.tsx create mode 100644 templates/plate-playground-template/src/lib/uploadthing.ts diff --git a/templates/plate-playground-template/package.json b/templates/plate-playground-template/package.json index 4cea2810d7..bcc44251ef 100644 --- a/templates/plate-playground-template/package.json +++ b/templates/plate-playground-template/package.json @@ -16,11 +16,11 @@ "@ai-sdk/openai": "^0.0.72", "@ariakit/react": "^0.4.13", "@faker-js/faker": "^9.2.0", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-context-menu": "^2.2.2", "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-separator": "^1.1.0", @@ -74,11 +74,6 @@ "@udecode/plate-toggle": "^40.0.0", "@udecode/plate-trailing-block": "^40.0.0", "@uploadthing/react": "7.1.0", - "uploadthing": "7.2.0", - "zod": "^3.23.8", - "react-player": "^2.16.0", - "sonner": "^1.5.0", - "use-file-picker": "^2.1.2", "ai": "^3.4.33", "class-variance-authority": "0.7.0", "clsx": "^2.1.1", @@ -93,6 +88,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-lite-youtube-embed": "^2.4.0", + "react-player": "^2.16.0", "react-resizable-panels": "^2.1.6", "react-tweet": "^3.2.1", "slate": "^0.110.2", @@ -100,12 +96,15 @@ "slate-history": "^0.110.3", "slate-hyperscript": "^0.100.0", "slate-react": "^0.111.0", + "sonner": "^1.7.0", "tailwind-merge": "2.5.4", "tailwind-scrollbar-hide": "^1.1.7", - "tailwindcss-animate": "1.0.7" + "tailwindcss-animate": "1.0.7", + "uploadthing": "7.2.0", + "use-file-picker": "^2.1.2", + "zod": "^3.23.8" }, "devDependencies": { - "eslint-plugin-prettier": "^5.2.1", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -116,6 +115,7 @@ "eslint-config-next": "15.0.3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-perfectionist": "3.9.1", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.37.2", "eslint-plugin-tailwindcss": "^3.17.5", "eslint-plugin-unused-imports": "^4.1.3", diff --git a/templates/plate-playground-template/pnpm-lock.yaml b/templates/plate-playground-template/pnpm-lock.yaml index d46fd5d769..221992d26d 100644 --- a/templates/plate-playground-template/pnpm-lock.yaml +++ b/templates/plate-playground-template/pnpm-lock.yaml @@ -18,7 +18,7 @@ importers: specifier: ^9.2.0 version: 9.2.0 '@radix-ui/react-alert-dialog': - specifier: ^1.1.1 + specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: ^1.1.1 @@ -258,7 +258,7 @@ importers: specifier: ^0.111.0 version: 0.111.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate-dom@0.111.0(slate@0.110.2))(slate@0.110.2) sonner: - specifier: ^1.5.0 + specifier: ^1.7.0 version: 1.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: 2.5.4 diff --git a/templates/plate-playground-template/src/app/api/uploadthing/route.ts b/templates/plate-playground-template/src/app/api/uploadthing/route.ts index 1a20e0732b..f3d0c2d5b3 100644 --- a/templates/plate-playground-template/src/app/api/uploadthing/route.ts +++ b/templates/plate-playground-template/src/app/api/uploadthing/route.ts @@ -4,31 +4,18 @@ 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 - .middleware(async ({ req }) => { - // This code runs on your server before upload - - // Whatever is returned here is accessible in onUploadComplete as `metadata` + editorUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio']) + .middleware(() => { return {}; }) - .onUploadComplete(({ file, metadata }) => { - // This code RUNS ON YOUR SERVER after upload - - // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback + .onUploadComplete(({ file }) => { 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/templates/plate-playground-template/src/app/editor/page.tsx b/templates/plate-playground-template/src/app/editor/page.tsx index 6f6314dc11..2859d21b23 100644 --- a/templates/plate-playground-template/src/app/editor/page.tsx +++ b/templates/plate-playground-template/src/app/editor/page.tsx @@ -1,5 +1,3 @@ -import { Toaster } from 'sonner'; - import { PlateEditor } from '@/components/editor/plate-editor'; import { OpenAIProvider } from '@/components/editor/use-chat'; @@ -8,7 +6,6 @@ export default function Page() {
-
); diff --git a/templates/plate-playground-template/src/components/editor/plate-editor.tsx b/templates/plate-playground-template/src/components/editor/plate-editor.tsx index 85c96457ba..d0cd3d129a 100644 --- a/templates/plate-playground-template/src/components/editor/plate-editor.tsx +++ b/templates/plate-playground-template/src/components/editor/plate-editor.tsx @@ -6,8 +6,8 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { Plate } from '@udecode/plate-common/react'; -import { SettingsDialog } from '@/components/editor/use-chat'; import { useCreateEditor } from '@/components/editor/use-create-editor'; +import { SettingsDialog } from '@/components/editor/use-chat'; import { Editor, EditorContainer } from '@/components/plate-ui/editor'; export function PlateEditor() { diff --git a/templates/plate-playground-template/src/components/editor/plugins/ai-plugins.tsx b/templates/plate-playground-template/src/components/editor/plugins/ai-plugins.tsx index bb4f6098c6..29cb89691f 100644 --- a/templates/plate-playground-template/src/components/editor/plugins/ai-plugins.tsx +++ b/templates/plate-playground-template/src/components/editor/plugins/ai-plugins.tsx @@ -26,8 +26,8 @@ import { HEADING_KEYS } from '@udecode/plate-heading'; import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react'; import { LinkPlugin } from '@udecode/plate-link/react'; import { MarkdownPlugin } from '@udecode/plate-markdown'; -import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; +import { cursorOverlayPlugin } from '@/components/editor/plugins/cursor-overlay-plugin'; import { AIMenu } from '@/components/plate-ui/ai-menu'; import { BlockquoteElement } from '@/components/plate-ui/blockquote-element'; import { CodeBlockElement } from '@/components/plate-ui/code-block-element'; @@ -40,6 +40,7 @@ import { LinkElement } from '@/components/plate-ui/link-element'; import { ParagraphElement } from '@/components/plate-ui/paragraph-element'; import { basicNodesPlugins } from './basic-nodes-plugins'; +import { blockSelectionReadOnlyPlugin } from './block-selection-plugins'; import { indentListPlugins } from './indent-list-plugins'; import { linkPlugin } from './link-plugin'; @@ -66,23 +67,13 @@ const createAIEditor = () => { }, }, plugins: [ - ParagraphPlugin, ...basicNodesPlugins, + ...indentListPlugins, HorizontalRulePlugin, linkPlugin, - ...indentListPlugins, MarkdownPlugin.configure({ options: { indentList: true } }), - // FIXME - BlockSelectionPlugin.configure({ - api: {}, - extendEditor: null, - options: {}, - render: {}, - useHooks: null, - handlers: {}, - }), + blockSelectionReadOnlyPlugin, ], - value: [{ children: [{ text: '' }], type: 'p' }], }); return editor; @@ -170,6 +161,7 @@ export const PROMPT_TEMPLATES = { }; export const aiPlugins = [ + cursorOverlayPlugin, MarkdownPlugin.configure({ options: { indentList: true } }), AIPlugin, AIChatPlugin.configure({ diff --git a/templates/plate-playground-template/src/components/editor/plugins/block-selection-plugins.ts b/templates/plate-playground-template/src/components/editor/plugins/block-selection-plugins.ts index 35edf1fd42..246efe37b7 100644 --- a/templates/plate-playground-template/src/components/editor/plugins/block-selection-plugins.ts +++ b/templates/plate-playground-template/src/components/editor/plugins/block-selection-plugins.ts @@ -13,3 +13,12 @@ export const blockSelectionPlugins = [ }, }), ] as const; + +export const blockSelectionReadOnlyPlugin = BlockSelectionPlugin.configure({ + api: {}, + extendEditor: null, + options: {}, + render: {}, + useHooks: null, + handlers: {}, +}); diff --git a/templates/plate-playground-template/src/components/editor/plugins/copilot-plugins.tsx b/templates/plate-playground-template/src/components/editor/plugins/copilot-plugins.tsx new file mode 100644 index 0000000000..21d7c600bb --- /dev/null +++ b/templates/plate-playground-template/src/components/editor/plugins/copilot-plugins.tsx @@ -0,0 +1,60 @@ +'use client'; + +import type { TElement } from '@udecode/plate-common'; + +import { faker } from '@faker-js/faker'; +import { CopilotPlugin } from '@udecode/plate-ai/react'; +import { getAncestorNode } from '@udecode/plate-common'; +import { serializeMdNodes, stripMarkdown } from '@udecode/plate-markdown'; + +import { GhostText } from '@/components/plate-ui/ghost-text'; + +export const copilotPlugins = [ + CopilotPlugin.configure(({ api }) => ({ + options: { + completeOptions: { + api: '/api/ai/copilot', + body: { + system: `You are an advanced AI writing assistant, similar to VSCode Copilot but for general text. Your task is to predict and generate the next part of the text based on the given context. + + Rules: + - Continue the text naturally up to the next punctuation mark (., ,, ;, :, ?, or !). + - Maintain style and tone. Don't repeat given text. + - For unclear context, provide the most likely continuation. + - Handle code snippets, lists, or structured text if needed. + - Don't include """ in your response. + - CRITICAL: Always end with a punctuation mark. + - CRITICAL: Avoid starting a new block. Do not use block formatting like >, #, 1., 2., -, etc. The suggestion should continue in the same block as the context. + - If no context is provided or you can't generate a continuation, return "0" without explanation.`, + }, + onError: () => { + // Mock the API response. Remove it when you implement the route /api/ai/copilot + api.copilot.setBlockSuggestion({ + text: stripMarkdown(faker.lorem.sentence()), + }); + }, + onFinish: (_, completion) => { + if (completion === '0') return; + + api.copilot.setBlockSuggestion({ + text: stripMarkdown(completion), + }); + }, + }, + debounceDelay: 500, + getPrompt: ({ editor }) => { + const contextEntry = getAncestorNode(editor); + + if (!contextEntry) return ''; + + const prompt = serializeMdNodes([contextEntry[0] as TElement]); + + return `Continue the text up to the next punctuation mark: + """ + ${prompt} + """`; + }, + renderGhostText: GhostText, + }, + })), +] as const; diff --git a/templates/plate-playground-template/src/components/editor/plugins/cursor-overlay-plugin.tsx b/templates/plate-playground-template/src/components/editor/plugins/cursor-overlay-plugin.tsx new file mode 100644 index 0000000000..dd94e47358 --- /dev/null +++ b/templates/plate-playground-template/src/components/editor/plugins/cursor-overlay-plugin.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { CursorOverlayPlugin } from '@udecode/plate-selection/react'; + +import { CursorOverlay } from '@/components/plate-ui/cursor-overlay'; + +export const cursorOverlayPlugin = CursorOverlayPlugin.configure({ + render: { + afterEditable: () => , + }, +}); diff --git a/templates/plate-playground-template/src/components/editor/plugins/delete-plugins.ts b/templates/plate-playground-template/src/components/editor/plugins/delete-plugins.ts index 947137bc50..ff384acbcd 100644 --- a/templates/plate-playground-template/src/components/editor/plugins/delete-plugins.ts +++ b/templates/plate-playground-template/src/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/templates/plate-playground-template/src/components/editor/plugins/editor-plugins.tsx b/templates/plate-playground-template/src/components/editor/plugins/editor-plugins.tsx index adfbbddd95..7dca88d29d 100644 --- a/templates/plate-playground-template/src/components/editor/plugins/editor-plugins.tsx +++ b/templates/plate-playground-template/src/components/editor/plugins/editor-plugins.tsx @@ -20,12 +20,12 @@ import { EquationPlugin, InlineEquationPlugin, } from '@udecode/plate-math/react'; -import { CursorOverlayPlugin } from '@udecode/plate-selection/react'; import { SlashPlugin } from '@udecode/plate-slash-command/react'; import { TogglePlugin } from '@udecode/plate-toggle/react'; import { TrailingBlockPlugin } from '@udecode/plate-trailing-block'; -import { CursorOverlay } from '@/components/plate-ui/cursor-overlay'; +import { FixedToolbarPlugin } from '@/components/editor/plugins/fixed-toolbar-plugin'; +import { FloatingToolbarPlugin } from '@/components/editor/plugins/floating-toolbar-plugin'; import { aiPlugins } from './ai-plugins'; import { alignPlugin } from './align-plugin'; @@ -33,6 +33,7 @@ import { autoformatPlugin } from './autoformat-plugin'; import { basicNodesPlugins } from './basic-nodes-plugins'; import { blockMenuPlugins } from './block-menu-plugins'; import { commentsPlugin } from './comments-plugin'; +import { cursorOverlayPlugin } from './cursor-overlay-plugin'; import { deletePlugins } from './delete-plugins'; import { dndPlugins } from './dnd-plugins'; import { exitBreakPlugin } from './exit-break-plugin'; @@ -46,17 +47,12 @@ import { softBreakPlugin } from './soft-break-plugin'; import { tablePlugin } from './table-plugin'; import { tocPlugin } from './toc-plugin'; -export const editorPlugins = [ - // AI - ...aiPlugins, - - // Nodes +export const viewPlugins = [ ...basicNodesPlugins, HorizontalRulePlugin, linkPlugin, DatePlugin, mentionPlugin, - SlashPlugin, tablePlugin, TogglePlugin, tocPlugin, @@ -78,11 +74,21 @@ export const editorPlugins = [ ...indentListPlugins, lineHeightPlugin, + // Collaboration + commentsPlugin, +] as const; + +export const editorPlugins = [ + // AI + ...aiPlugins, + + // Nodes + ...viewPlugins, + // Functionality + SlashPlugin, autoformatPlugin, - CursorOverlayPlugin.configure({ - render: { afterEditable: () => }, - }), + cursorOverlayPlugin, ...blockMenuPlugins, ...dndPlugins, EmojiPlugin, @@ -92,11 +98,12 @@ export const editorPlugins = [ softBreakPlugin, TrailingBlockPlugin.configure({ options: { type: ParagraphPlugin.key } }), - // Collaboration - commentsPlugin, - // Deserialization DocxPlugin, MarkdownPlugin.configure({ options: { indentList: true } }), JuicePlugin, + + // UI + FixedToolbarPlugin, + FloatingToolbarPlugin, ]; diff --git a/templates/plate-playground-template/src/components/editor/plugins/indent-list-plugins.ts b/templates/plate-playground-template/src/components/editor/plugins/indent-list-plugins.ts index f760cfca80..4f677e544b 100644 --- a/templates/plate-playground-template/src/components/editor/plugins/indent-list-plugins.ts +++ b/templates/plate-playground-template/src/components/editor/plugins/indent-list-plugins.ts @@ -12,7 +12,10 @@ import { FireLiComponent, FireMarker, } from '@/components/plate-ui/indent-fire-marker'; -import { TodoLi, TodoMarker } from '@/components/plate-ui/indent-todo-marker'; +import { + TodoLi, + TodoMarker, +} from '@/components/plate-ui/indent-todo-marker'; export const indentListPlugins = [ IndentPlugin.extend({ diff --git a/templates/plate-playground-template/src/components/editor/plugins/media-plugins.tsx b/templates/plate-playground-template/src/components/editor/plugins/media-plugins.tsx index 2bd9e43722..430f803ab2 100644 --- a/templates/plate-playground-template/src/components/editor/plugins/media-plugins.tsx +++ b/templates/plate-playground-template/src/components/editor/plugins/media-plugins.tsx @@ -15,9 +15,7 @@ import { MediaUploadToast } from '@/components/plate-ui/media-upload-toast'; export const mediaPlugins = [ ImagePlugin.extend({ - options: { - disableUploadInsert: true, - }, + options: { disableUploadInsert: true }, render: { afterEditable: ImagePreview }, }), MediaEmbedPlugin, @@ -25,14 +23,18 @@ export const mediaPlugins = [ AudioPlugin, FilePlugin, CaptionPlugin.configure({ - options: { plugins: [ImagePlugin, MediaEmbedPlugin] }, - }), - PlaceholderPlugin.configure({ options: { - disableEmptyPlaceholder: true, - }, - render: { - afterEditable: () => , + plugins: [ + ImagePlugin, + VideoPlugin, + AudioPlugin, + FilePlugin, + MediaEmbedPlugin, + ], }, }), + PlaceholderPlugin.configure({ + options: { disableEmptyPlaceholder: true }, + render: { afterEditable: MediaUploadToast }, + }), ] as const; diff --git a/templates/plate-playground-template/src/components/editor/use-chat.tsx b/templates/plate-playground-template/src/components/editor/use-chat.tsx index d8e54488b4..93bbd9438d 100644 --- a/templates/plate-playground-template/src/components/editor/use-chat.tsx +++ b/templates/plate-playground-template/src/components/editor/use-chat.tsx @@ -217,7 +217,7 @@ export function SettingsDialog() { Enter your{' '} -

+

Not stored anywhere. Used only for current session requests.

diff --git a/templates/plate-playground-template/src/components/editor/use-create-editor.tsx b/templates/plate-playground-template/src/components/editor/use-create-editor.tsx index d9c3cf6737..eced477bea 100644 --- a/templates/plate-playground-template/src/components/editor/use-create-editor.tsx +++ b/templates/plate-playground-template/src/components/editor/use-create-editor.tsx @@ -74,7 +74,11 @@ import { HrElement } from '@/components/plate-ui/hr-element'; import { ImageElement } from '@/components/plate-ui/image-element'; import { KbdLeaf } from '@/components/plate-ui/kbd-leaf'; import { LinkElement } from '@/components/plate-ui/link-element'; +import { MediaAudioElement } from '@/components/plate-ui/media-audio-element'; import { MediaEmbedElement } from '@/components/plate-ui/media-embed-element'; +import { MediaFileElement } from '@/components/plate-ui/media-file-element'; +import { MediaPlaceholderElement } from '@/components/plate-ui/media-placeholder-element'; +import { MediaVideoElement } from '@/components/plate-ui/media-video-element'; import { MentionElement } from '@/components/plate-ui/mention-element'; import { MentionInputElement } from '@/components/plate-ui/mention-input-element'; import { ParagraphElement } from '@/components/plate-ui/paragraph-element'; @@ -90,11 +94,6 @@ import { TocElement } from '@/components/plate-ui/toc-element'; import { ToggleElement } from '@/components/plate-ui/toggle-element'; import { withDraggables } from '@/components/plate-ui/with-draggables'; -import { MediaAudioElement } from '../plate-ui/media-audio-element'; -import { MediaFileElement } from '../plate-ui/media-file-element'; -import { MediaPlaceholderElement } from '../plate-ui/media-placeholder-element'; -import { MediaVideoElement } from '../plate-ui/media-video-element'; - export const useCreateEditor = () => { return usePlateEditor({ override: { diff --git a/templates/plate-playground-template/src/components/plate-ui/ai-menu.tsx b/templates/plate-playground-template/src/components/plate-ui/ai-menu.tsx index 3fc74a21f7..7afbe5948e 100644 --- a/templates/plate-playground-template/src/components/plate-ui/ai-menu.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/ai-menu.tsx @@ -127,14 +127,14 @@ export function AIMenu() { )} {isLoading ? ( -
+
{messages.length > 1 ? 'Editing...' : 'Thinking...'}
) : ( { if (isHotkey('backspace')(e) && input.length === 0) { diff --git a/templates/plate-playground-template/src/components/plate-ui/alert-dialog.tsx b/templates/plate-playground-template/src/components/plate-ui/alert-dialog.tsx index fd3f90a273..c98b9845cc 100644 --- a/templates/plate-playground-template/src/components/plate-ui/alert-dialog.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/alert-dialog.tsx @@ -19,7 +19,7 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( (({ className, ...props }, ref) => ( )); @@ -105,7 +105,7 @@ const AlertDialogAction = React.forwardRef< >(({ className, ...props }, ref) => ( )); @@ -118,7 +118,7 @@ const AlertDialogCancel = React.forwardRef< ( ( className={cn('relative py-1', state.className, className)} {...props} > -
+        
           {children}
         
diff --git a/templates/plate-playground-template/src/components/plate-ui/code-leaf.tsx b/templates/plate-playground-template/src/components/plate-ui/code-leaf.tsx index a1b24f1ced..dd0c48a2fd 100644 --- a/templates/plate-playground-template/src/components/plate-ui/code-leaf.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/code-leaf.tsx @@ -12,7 +12,7 @@ export const CodeLeaf = withRef( ref={ref} asChild className={cn( - 'bg-muted whitespace-pre-wrap rounded-md px-[0.3em] py-[0.2em] font-mono text-sm', + 'whitespace-pre-wrap rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm', className )} {...props} diff --git a/templates/plate-playground-template/src/components/plate-ui/color-dropdown-menu-items.tsx b/templates/plate-playground-template/src/components/plate-ui/color-dropdown-menu-items.tsx index f68a0a9926..8714bdd987 100644 --- a/templates/plate-playground-template/src/components/plate-ui/color-dropdown-menu-items.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/color-dropdown-menu-items.tsx @@ -47,7 +47,7 @@ export function ColorDropdownMenuItem({ size: 'icon', variant: 'outline', }), - 'border-muted my-1 flex size-6 items-center justify-center rounded-full border border-solid p-0 transition-all hover:scale-125', + 'my-1 flex size-6 items-center justify-center rounded-full border border-solid border-muted p-0 transition-all hover:scale-125', !isBrightColor && 'border-transparent text-white hover:!text-white', className )} diff --git a/templates/plate-playground-template/src/components/plate-ui/column-group-element.tsx b/templates/plate-playground-template/src/components/plate-ui/column-group-element.tsx index 5aec9a0ec3..10d903bd74 100644 --- a/templates/plate-playground-template/src/components/plate-ui/column-group-element.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/column-group-element.tsx @@ -60,7 +60,7 @@ export function ColumnFloatingToolbar({ children }: React.PropsWithChildren) { side="top" sideOffset={10} > -
+
diff --git a/templates/plate-playground-template/src/components/plate-ui/command.tsx b/templates/plate-playground-template/src/components/plate-ui/command.tsx index 3395a99a64..0a4b9dc120 100644 --- a/templates/plate-playground-template/src/components/plate-ui/command.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/command.tsx @@ -19,7 +19,7 @@ import { inputVariants } from './input'; export const Command = withCn( CommandPrimitive, - 'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md' + 'flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground' ); export function CommandDialog({ children, ...props }: DialogProps) { @@ -27,7 +27,7 @@ export function CommandDialog({ children, ...props }: DialogProps) { Command Dialog - + {children} @@ -42,7 +42,7 @@ export const CommandInput = withRef( {user?.name} -
+
{formatDistance(comment.createdAt, Date.now())} ago
diff --git a/templates/plate-playground-template/src/components/plate-ui/comment-leaf.tsx b/templates/plate-playground-template/src/components/plate-ui/comment-leaf.tsx index 6c8c748a71..0751d01dac 100644 --- a/templates/plate-playground-template/src/components/plate-ui/comment-leaf.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/comment-leaf.tsx @@ -34,7 +34,7 @@ export function CommentLeaf({ - diff --git a/templates/plate-playground-template/src/components/plate-ui/comment-resolve-button.tsx b/templates/plate-playground-template/src/components/plate-ui/comment-resolve-button.tsx index 165a973708..239a59ac27 100644 --- a/templates/plate-playground-template/src/components/plate-ui/comment-resolve-button.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/comment-resolve-button.tsx @@ -18,7 +18,7 @@ export function CommentResolveButton() { {comment.isResolved ? ( diff --git a/templates/plate-playground-template/src/components/plate-ui/context-menu.tsx b/templates/plate-playground-template/src/components/plate-ui/context-menu.tsx index 7749b6c0a2..baf1d1f166 100644 --- a/templates/plate-playground-template/src/components/plate-ui/context-menu.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/context-menu.tsx @@ -56,7 +56,7 @@ const ContextMenuSubTrigger = React.forwardRef< (({ className, ...props }, ref) => ( )); @@ -201,7 +201,7 @@ const ContextMenuShortcut = ({ return ( diff --git a/templates/plate-playground-template/src/components/plate-ui/date-element.tsx b/templates/plate-playground-template/src/components/plate-ui/date-element.tsx index 54ceeafb50..a9b12737fb 100644 --- a/templates/plate-playground-template/src/components/plate-ui/date-element.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/date-element.tsx @@ -23,7 +23,7 @@ export const DateElement = withRef( ( @@ -26,13 +26,13 @@ export const DialogContent = withRef( {children} - + Close @@ -58,5 +58,5 @@ export const DialogTitle = withCn( export const DialogDescription = withCn( DialogPrimitive.Description, - 'text-muted-foreground text-sm' + 'text-sm text-muted-foreground' ); diff --git a/templates/plate-playground-template/src/components/plate-ui/draggable.tsx b/templates/plate-playground-template/src/components/plate-ui/draggable.tsx index 30ce35d65c..1b06eeef2b 100644 --- a/templates/plate-playground-template/src/components/plate-ui/draggable.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/draggable.tsx @@ -25,8 +25,6 @@ import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; import { GripVertical } from 'lucide-react'; import { useSelected } from 'slate-react'; -import { useMounted } from '@/hooks/use-mounted'; - import { Tooltip, TooltipContent, @@ -61,7 +59,6 @@ export const Draggable = withHOC( const state = useDraggableState({ element, onDropHandler }); const { isDragging } = state; const { previewRef, handleRef } = useDraggable(state); - const mounted = useMounted(); return (
-
+
@@ -117,7 +110,7 @@ const Gutter = React.forwardRef< ref={ref} className={cn( 'slate-gutterLeft', - 'main-hover:group-hover:opacity-100 absolute -top-px z-50 flex h-full -translate-x-full cursor-text hover:opacity-100 sm:opacity-0', + 'absolute -top-px z-50 flex h-full -translate-x-full cursor-text hover:opacity-100 sm:opacity-0 main-hover:group-hover:opacity-100', isSelectionAreaVisible && 'hidden', !selected && 'opacity-0', className @@ -138,7 +131,7 @@ const DragHandle = React.memo(() => { { event.stopPropagation(); event.preventDefault(); diff --git a/templates/plate-playground-template/src/components/plate-ui/dropdown-menu.tsx b/templates/plate-playground-template/src/components/plate-ui/dropdown-menu.tsx index 2ce862da31..2f4eb47a5f 100644 --- a/templates/plate-playground-template/src/components/plate-ui/dropdown-menu.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/dropdown-menu.tsx @@ -94,7 +94,7 @@ export const DropdownMenuSubTrigger = withRef< -
+
{i18n.categories[categoryId]}
{ return (
-
+
{i18n.searchResult}
@@ -186,8 +186,8 @@ export function EmojiPickerContent({ 'h-full min-h-[50%] overflow-y-auto overflow-x-hidden px-2', '[&::-webkit-scrollbar]:w-4', '[&::-webkit-scrollbar-button]:hidden [&::-webkit-scrollbar-button]:size-0', - '[&::-webkit-scrollbar-thumb]:bg-muted [&::-webkit-scrollbar-thumb]:hover:bg-muted-foreground/25 [&::-webkit-scrollbar-thumb]:min-h-11 [&::-webkit-scrollbar-thumb]:rounded-full', - '[&::-webkit-scrollbar-thumb]:border-popover [&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:bg-clip-padding' + '[&::-webkit-scrollbar-thumb]:min-h-11 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted [&::-webkit-scrollbar-thumb]:hover:bg-muted-foreground/25', + '[&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-popover [&::-webkit-scrollbar-thumb]:bg-clip-padding' )} data-id="scroll" > diff --git a/templates/plate-playground-template/src/components/plate-ui/emoji-picker-navigation.tsx b/templates/plate-playground-template/src/components/plate-ui/emoji-picker-navigation.tsx index 6fb89d4a3a..2549c8ca6e 100644 --- a/templates/plate-playground-template/src/components/plate-ui/emoji-picker-navigation.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/emoji-picker-navigation.tsx @@ -53,7 +53,7 @@ export function EmojiPickerNavigation({