diff --git a/.changeset/ai.md b/.changeset/ai.md new file mode 100644 index 0000000000..81ef05139b --- /dev/null +++ b/.changeset/ai.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-ai': patch +--- + +- `CopilotPlugin`: reset on mouse down +- `CopilotPlugin`: auto trigger only on selection change diff --git a/.changeset/column.md b/.changeset/column.md new file mode 100644 index 0000000000..ded4c33130 --- /dev/null +++ b/.changeset/column.md @@ -0,0 +1,10 @@ +--- +'@udecode/plate-layout': minor +--- + +- `ColumnPlugin`: + - unwrap columns when there is only one column + - remove column group when it has no column children + - remove column when it has no children +- Add `insertColumnGroup` +- Add `toggleColumnGroup` diff --git a/.changeset/core-major.md b/.changeset/core-major.md new file mode 100644 index 0000000000..dcc4d84cc1 --- /dev/null +++ b/.changeset/core-major.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-slate-react': major +--- + +- Add `slate-dom` as a peer dependency +- Update `slate-react` peer dependency to `>=0.111.0` diff --git a/.changeset/core.md b/.changeset/core.md new file mode 100644 index 0000000000..0603b57e1b --- /dev/null +++ b/.changeset/core.md @@ -0,0 +1,19 @@ +--- +'@udecode/plate-core': patch +--- + +- `Plate` store: add `containerRef`. This is used by some plugins like `CursorOverlay`. +- Add `useEditorContainerRef` selector hook. You can pass the returned ref to your editor scroll container. +- `usePlateEditor` options: `value` can now be a callback function to get the value from the editor +- `editor.key` is now using `nanoid()` +- `editor.uid`: new property added by `Plate` to uniquely identify the editor. The difference with `editor.key` is that `uid` supports SSR hydration. This can be passed to the editor container as `id` prop. +- `Plate` now warns if multiple instances of `@udecode/plate-core` are detected. Use `suppressInstanceWarning` to suppress the warning. +- `render.aboveNodes` and `render.belowNodes` now support `useElement` +- `PlatePlugin.inject` new properties: + - `excludePlugins?: string[]` + - `excludeBelowPlugins?: string[]` + - `maxLevel?: number` + - `isLeaf?: boolean` + - `isBlock?: boolean` + - `isElement?: boolean` +- Add `getInjectMatch(editor, plugin)` to get a plugin inject match function. diff --git a/.changeset/cursor copy.md b/.changeset/cursor copy.md new file mode 100644 index 0000000000..95cbd669bf --- /dev/null +++ b/.changeset/cursor copy.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-cursor': patch +--- + +Deprecated, use `@udecode/plate-selection` instead. diff --git a/.changeset/cursor.md b/.changeset/cursor.md new file mode 100644 index 0000000000..e63bf7dba4 --- /dev/null +++ b/.changeset/cursor.md @@ -0,0 +1,9 @@ +--- +'@udecode/plate-selection': minor +--- + +- New plugin `CursorOverlayPlugin` +- `useCursorOverlay` now supports collapsed selection using `minSelectionWidth = 1` +- selectable depends now on `data-block-id` instead of `data-key` +- Fix a bug when deleting selected blocks without id +- Fix `useBlockSelected`: use `id` parameter if defined diff --git a/.changeset/dnd.md b/.changeset/dnd.md new file mode 100644 index 0000000000..ebb89406b9 --- /dev/null +++ b/.changeset/dnd.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-dnd': minor +--- + +- Dragging `dropEffect` is now `move` instead of `copy` +- Fix drag bug when dragging an element without id diff --git a/.changeset/heading.md b/.changeset/heading.md new file mode 100644 index 0000000000..87f5c50ba2 --- /dev/null +++ b/.changeset/heading.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-heading': major +'@udecode/plate-ai': major +--- + +- Remove `scrollContainerSelector` option in favor of `useEditorContainerRef` diff --git a/.changeset/html.md b/.changeset/html.md new file mode 100644 index 0000000000..f6a80fefae --- /dev/null +++ b/.changeset/html.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-html': patch +--- + +Sync with `@udecode/plate-core` diff --git a/.changeset/hyper.md b/.changeset/hyper.md new file mode 100644 index 0000000000..f9cdd00d4c --- /dev/null +++ b/.changeset/hyper.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-test-utils': patch +--- + +- Strip (potentially injected by the compiler) attributes starting with `__`. +- Remove internal functions from exports. diff --git a/.changeset/id.md b/.changeset/id.md new file mode 100644 index 0000000000..bbd5cc6534 --- /dev/null +++ b/.changeset/id.md @@ -0,0 +1,11 @@ +--- +'@udecode/plate-node-id': minor +--- + +- `idCreator` default is now `nanoid(10)` +- New option `filterInline` to filter inline elements. Default is `true`. +- `NodeIdPlugin`: + - add `normalizeInitialValue` that set node ids when missing, called before mount + - default behavior will normalize only the first and last node if missing id to avoid traversing the entire document + - you can disable it with `NodeIdPlugin.configure({ normalizeInitialValue: null })` + - you can force check all nodes with `NodeIdPlugin.configure({ options: { normalizeInitialValue: true } })` diff --git a/.changeset/layout-major.md b/.changeset/layout-major.md new file mode 100644 index 0000000000..10e7ed9166 --- /dev/null +++ b/.changeset/layout-major.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-layout': major +--- + +- Remove `toggleColumns` in favor of `toggleColumnGroup` +- Remove `insertEmptyColumn` in favor of `insertColumn` diff --git a/.changeset/md.md b/.changeset/md.md new file mode 100644 index 0000000000..e58eecd089 --- /dev/null +++ b/.changeset/md.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-markdown': minor +--- + +- `api.markdown.deserialize`: add second argument option: `processor?: (processor: Processor) => Processor`. You could use this to add other remark plugins like `remark-gfm`. +- Add `delete` text rule. This does not add support for strikethrough yet. diff --git a/.changeset/misc.md b/.changeset/misc.md new file mode 100644 index 0000000000..d813f385e8 --- /dev/null +++ b/.changeset/misc.md @@ -0,0 +1,7 @@ +--- +'@udecode/plate-line-height': patch +'@udecode/plate-alignment': patch +'@udecode/plate-indent': patch +--- + +Use `getInjectMatch(editor, plugin)` diff --git a/.changeset/table.md b/.changeset/table.md new file mode 100644 index 0000000000..b080baaea8 --- /dev/null +++ b/.changeset/table.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-table': patch +--- + +Fix: remove tables without any rows diff --git a/.changeset/utils.md b/.changeset/utils.md new file mode 100644 index 0000000000..da624c177e --- /dev/null +++ b/.changeset/utils.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-utils': patch +--- + +- `PlateElement` add `data-block-id` if `element.id` is defined, after editor mount to support SSR hydration. diff --git a/apps/www/content/docs/ai.mdx b/apps/www/content/docs/ai.mdx index 21d8766ffb..a5993b82ea 100644 --- a/apps/www/content/docs/ai.mdx +++ b/apps/www/content/docs/ai.mdx @@ -172,7 +172,6 @@ const plugins = [ ? PROMPT_TEMPLATES.userSelecting : PROMPT_TEMPLATES.userDefault; }, - scrollContainerSelector: '#scroll_container', systemTemplate: ({ isBlockSelecting, isSelecting }) => { return isBlockSelecting ? PROMPT_TEMPLATES.systemBlockSelecting @@ -183,18 +182,11 @@ const plugins = [ }, render: { afterEditable: () => }, }), - SelectionOverlayPlugin, ]; ``` - [AIMenu](/docs/components/ai-menu) -The [SelectionOverlayPlugin](https://pro.platejs.org/docs/components/cursor-overlay): - -- Maintains selection highlight when editor loses focus -- Essential for AI menu and other external input interactions -- Prevents double selection with `data-plate-prevent-overlay` attribute - ### AI SDK This plugin is depending on the [ai](https://npmjs.com/package/ai) package: @@ -278,14 +270,6 @@ Template for generating prompts. Supports placeholders: - - -CSS selector for scrollable container. - -- **Default:** `'#scroll_container'` - - - Template for system messages. Supports same placeholders as `promptTemplate`. diff --git a/apps/www/content/docs/api/core/plate-plugin.mdx b/apps/www/content/docs/api/core/plate-plugin.mdx index 617b9dbea9..b97aec51b4 100644 --- a/apps/www/content/docs/api/core/plate-plugin.mdx +++ b/apps/www/content/docs/api/core/plate-plugin.mdx @@ -63,6 +63,28 @@ Defines how the plugin injects functionality into other plugins or the editor. Properties used by Plate to inject props into any node component. + + + +An array of plugin keys to exclude from node prop injection. + + + +An array of plugin keys. Node prop injection will be excluded for any nodes that are descendants of elements with these plugin types. + + +If true, only matches block elements. Used to restrict prop injection to block-level nodes. + + + +If true, only matches element nodes. Used to restrict prop injection to element nodes. + + +If true, only matches leaf nodes. Used to restrict prop injection to leaf nodes. + + +Maximum nesting level for node prop injection. Nodes deeper than this level will not receive injected props. + Property that can be used by a plugin to allow other plugins to inject code. diff --git a/apps/www/content/docs/api/core/store.mdx b/apps/www/content/docs/api/core/store.mdx index e4cead40b0..1e00ae52e1 100644 --- a/apps/www/content/docs/api/core/store.mdx +++ b/apps/www/content/docs/api/core/store.mdx @@ -29,6 +29,10 @@ A unique ID used as a provider scope. Use it if you have multiple `Plate` in the - **Default:** random id + +A reference to the editor container element. + + Function used to decorate ranges in the editor. diff --git a/apps/www/content/docs/block-menu.mdx b/apps/www/content/docs/block-menu.mdx index 79689f632b..200ae94ed9 100644 --- a/apps/www/content/docs/block-menu.mdx +++ b/apps/www/content/docs/block-menu.mdx @@ -13,7 +13,7 @@ docs: --- - + @@ -52,10 +52,6 @@ const plugins = [ }, startThreshold: 10, }, - boundaries: '#scroll_container', - container: '#scroll_container', - selectables: '#scroll_container .slate-selectable', - selectionAreaClass: 'slate-selection-area', }, enableContextMenu: true, }, diff --git a/apps/www/content/docs/block-selection.mdx b/apps/www/content/docs/block-selection.mdx index 643ebe5a9a..478b2e6570 100644 --- a/apps/www/content/docs/block-selection.mdx +++ b/apps/www/content/docs/block-selection.mdx @@ -42,56 +42,50 @@ const plugins = [ ]; ``` -## Configuration +### Exclude blocks from selection -### Set scrollable container +You can exclude certain plugins from block selection using: -To control the scrollable container, configure the `boundaries` and `container` options within `areaOptions`. These options accept CSS selectors, such as `#selection-demo #scroll_container`, which are used with `document.querySelector()`. +```ts +BlockSelectionPlugin.configure({ + inject: { + // Exclude blocks below table rows + excludeBelowPlugins: ['tr'], + // Exclude block types + excludePlugins: ['table', 'code_line', 'column_group', 'column'], + } +}) +``` -For this to work effectively: +- `excludeBelowPlugins`: Plugin keys of non-selectable block descendants. Use this to prevent selection below specific blocks. For example, excluding 'tr' prevents selecting individual cells while still allowing table row selection. -1. Add an `id` or `className` to your scroll container.If you not sure about the container, you can add it to the `` component. -2. Use the appropriate selector in your configuration. -3. Don't forget to set `position: relative` to your scroll container. +- `excludePlugins`: Plugin keys of non-selectable blocks. -Example configuration: +### Set scrollable container -```js -{ - areaOptions: { - boundaries: '#your-scroll-container-id', - container: '#your-scroll-container-id', - selectables: '#your-scroll-container-id .slate-selectable', - selectionAreaClass: 'slate-selection-area', - } -} -``` +If you're using `EditorContainer` from [Editor](/docs/components/editor), you can skip this section. -This setup ensures that the block selection functionality is properly constrained within your designated scrollable area. +To control the scrollable container, configure the `boundaries` and `container` options within `areaOptions`. These options accept CSS selectors, such as `#selection-demo #${editor.uid}`, which are used with `document.querySelector()`. -### Set selectable element +For this to work effectively: -Add data-plate-selectable to the container or the element you want to start block selection. +1. Add an `id` or `className` to your scroll container. If you not sure about the container, you can add it to the `` component. We recommend using `id={editor.uid}`. +2. Use the appropriate selector in your configuration. +3. Don't forget to set `position: relative` to the container. -Example: -```tsx - +Default configuration: + +```js +BlockSelectionPlugin.configure({ + options: { + areaOptions: { + boundaries: `#${editor.uid}`, + container: `#${editor.uid}`, + selectables: `#${editor.uid} .slate-selectable`, + }, + }, + }, +}); ``` ### Set scroll speed @@ -102,102 +96,80 @@ The value of `1.5` is our recommended speed.Since it's same with the default spe ```ts - areaOptions: { +BlockSelectionPlugin.configure({ + options: { + areaOptions: { behaviour: { - scrolling: { - speedDivider: 1.5, - }, - }, - }, -``` - -Full configuration recommended: - -```ts -options: { - areaOptions: { - behaviour: { scrolling: { speedDivider: 1.5, }, // The distance needed to move for the selection area to appear. - // If it’s too small, it may cause the mouse click event to be blocked. 30 is a good default. - startThreshold: 30, + // If it’s too small, it may cause the mouse click event to be blocked. 10 is a good default. + startThreshold: 10, }, - boundaries: '#your-scroll-container-id', - container: '#your-scroll-container-id', - selectables: '#your-scroll-container-id .slate-selectable', - selectionAreaClass: 'slate-selection-area', }, - // if not using plate-ui context menu, set it to false - enableContextMenu: true, -}, +} ``` -## Styling - -### Selection area - -Style the selection area using `.slate-selection-area` class to your `EditorContainer` component. For example: +### Add selectable element -```js -'[&_.slate-selection-area]:border [&_.slate-selection-area]:border-primary [&_.slate-selection-area]:bg-primary/10' -``` +Add `data-plate-selectable="true"` to any element you want to start block selection. -### Selected element +### Prevent unselect -To determine if an element is selected, use the new `useBlockSelected` hook. You can render a visual indicator around selected blocks using our `BlockSelection` component or create your own: +To prevent unselecting blocks when clicking on certain elements, add the `data-plate-prevent-unselect` attribute to those components +For example: ```tsx -import React from 'react'; -import { useBlockSelected } from '@udecode/plate-selection/react'; - -export function BlockSelection() { - const isBlockSelected = useBlockSelected(); - - return ( -
- ); -} + ``` -This component should be rendered inside each block element for consistent selection feedback. Plate UI is doing it in `plate-element.tsx`. +### Full Page Selection -## Selectable and resetable +#### Making Elements Selectable -### full page selectable +You can enable block selection for elements outside the `` component, similar to the [Potion](https://potion.platejs.org/) template. Add the `data-plate-selectable` attribute to any component you want to make selectable: -Like the [potion](https://potion.platejs.org/) template, you can enable block selection outside of the `` component. +```tsx + + +``` -Simply add the `data-plate-selectable` attribute to any component outside the editor that you want to make selectable. You can even make the entire page selectable if desired. +This works for any element, even those outside the editor's DOM tree. -For example: +#### Resetting Selection + +There are two ways to handle resetting selection across the full page: +1. Direct API call: ```tsx - +editor.api.blockSelection.unselect(); ``` -### full page resetable +2. Click outside handler: +```tsx +const handleClickOutside = (event: MouseEvent) => { + if (!(event.target as HTMLElement).closest('[data-plate-selectable]')) { + editor.api.blockSelection.unselect(); + } +}; +``` -To reset the selection, you need call the `api.blockSelection.unselect();` method. +## Styling -If you want to make the full page resetable by click, this means you need to be able to access the editor outside of ``. +### Selection area -Or you can implement a click outside handler to reset the selection. +Style the selection area using `.slate-selection-area` class to your editor container component. For example: +```js +'[&_.slate-selection-area]:border [&_.slate-selection-area]:border-primary [&_.slate-selection-area]:bg-primary/10' +``` -## Prevent unselect +### Selected element -To prevent unselecting blocks when clicking on certain elements, add the `data-plate-prevent-unselect` attribute to those components +To determine if an element is selected, use `useBlockSelected` hook. You can render a visual indicator around selected blocks using our [BlockSelection](/docs/components/block-selection) component or create your own. -For example: -```tsx - -``` +This component should be rendered inside each block element for consistent selection feedback. Plate UI is doing it in [PlateElement](/docs/components/plate-element). ## Plugins @@ -209,9 +181,9 @@ Options for the selection area. Example: ```ts { - boundaries: ['#selection-demo #scroll_container'], - container: ['#selection-demo #scroll_container'], - selectables: ['#selection-demo #scroll_container .slate-selectable'], + boundaries: [`#${editor.uid}`], + container: [`#${editor.uid}`], + selectables: [`#${editor.uid} .slate-selectable`], selectionAreaClass: 'slate-selection-area', } ``` @@ -254,7 +226,6 @@ A set of IDs for the currently selected blocks. ## API - ### editor.api.blockSelection.focus Focuses the block selection shadow input. This input handles copy, delete, and paste events for selected blocks. @@ -268,9 +239,6 @@ Adds a selected row to the block selection. - - The HTML node above which to add the selection. - Whether to clear existing selections before adding the new one. - **Default:** `true` @@ -364,68 +332,34 @@ Sets text properties on the selected nodes. -## API Components +## Hooks + +### useBlockSelectable -### BlockSelectable +A hook that provides props for making a block element selectable, including context menu behavior. - - + + + Props to be spread on the block element: - - The element to render the block selectable. + + `'slate-selectable'` - Required class for selection functionality - - Whether the selection is active. + + Handles right-click context menu behavior: + - Opens context menu for selected blocks + - Opens for void elements + - Opens for elements with `data-plate-open-context-menu="true"` + - Adds block to selection with Shift key for multi-select - - -### BlockSelection - -A wrapper component that adds block selection functionality to its children. - - - - The content to be wrapped with block selection functionality. - - - -## Hooks + ### useBlockSelected Returns true if context block is selected. -### useBlockSelectableState - - - - Whether the block is active for selection. - - - The element associated with the block. - - - The path of the block in the editor. - - - A ref to the block's DOM element. - - - -### useBlockSelectable - - - - Props to be spread on the block's wrapper element. - - - -### useSelectionArea - -A hook that initializes and manages the selection area functionality. - ### useBlockSelectionNodes Returns an array of node entries for the currently selected blocks. @@ -437,3 +371,7 @@ Returns an array of nodes for the currently selected blocks. ### useBlockSelectionFragmentProp Returns fragment prop for the currently selected blocks. + +### useSelectionArea + +A hook that initializes and manages the selection area functionality. \ No newline at end of file diff --git a/apps/www/content/docs/column.mdx b/apps/www/content/docs/column.mdx index 4a0305841f..29b4f3e402 100644 --- a/apps/www/content/docs/column.mdx +++ b/apps/www/content/docs/column.mdx @@ -77,9 +77,13 @@ Insert a columnGroup with two empty columns. The editor instance. + + - `layout`: Array of column widths or number of equal-width columns (default: 2) + - Other InsertNodesOptions to control insert behavior + -### insertEmptyColumn +### insertColumn Insert an empty column. You can set the width by options.width default is "33%" @@ -125,6 +129,19 @@ If you want to set the `layout` use setNodes. +### toggleColumnGroup + +Convert a block into a column group layout. The selected block becomes the content of the first column. + + + + The editor instance. + + + - `layout`: Array of column widths or number of equal-width columns (default: 2) + + + ## API Components ### useColumnState diff --git a/apps/www/content/docs/components/changelog.mdx b/apps/www/content/docs/components/changelog.mdx index 9724dd1e57..b9a363fe5b 100644 --- a/apps/www/content/docs/components/changelog.mdx +++ b/apps/www/content/docs/components/changelog.mdx @@ -11,6 +11,63 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver ## November 2024 #16 +### November 8 #16.5 + +- Add this to your tailwind config: +```ts +// plugins +require("tailwind-scrollbar-hide") + +// theme.extend.screens +screens: { + /** + * Matches devices where the primary pointing device is capable of + * hovering conveniently. Usage: main-hover:group-hover:bg-red-500 See: + * https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover + * + * On iOS devices, buttons inside a .group element that contains + * descendent elements with `group-hover:` CSS rules require two taps to + * click. As a workaround, we disable these rules on devices that cannot + * conveniently hover using `main-hover:`. + */ + 'main-hover': { + raw: '(hover: hover)', + }, +}, +``` +- `editor`: + - `EditorContainer`: remove `ref`, use `useEditorContainerRef` instead + - add `caret-primary selection:bg-brand/25` + - add `id={editor.uid}` to `EditorContainer` so you can remove `scroll_container` or any id you may have defined. +- `draggable`: + - fix a **critical** mobile ux issue: focusing the editor required two taps because of the group hover styles. Fixed with `main-hover:group-hover:opacity-100`. + - for xs media, show only if selected +- `table-element`: fix width +- `table-row-element`, `table-cell-element`: support row selection +- Add `draggable` prop to `date-element`, `mention-element` +- Add `data-plate-focus` attribute to `link-toolbar-button`, `link-floating-toolbar`, `ai-menu` input +- `cursor-overlay`: + - support collapsed selection + - remove `DragOverCursorPlugin`, `SelectionOverlayPlugin`. Use `@udecode/plate-selection` instead. +- `ghost-text`: hide for xs media, add `pointer-events-none` +- `floating-toolbar`: add `overflow-x-auto scrollbar-hide` to allow horizontal scrolling (mobile) +- `fixed-toolbar`: add `scrollbar-hide`, update from `sticky` to `absolute` +- `emoji-picker-content`: add emoji font +- `column-element`: add `w-full` as default +- New hook: `use-is-touch-device` +- `block-context-menu`: disable on touch device +- `ai-toolbar-button`: add `onMouseDown` +- `ai-menu-items`: add undefined check +- `block-selection-plugins`: add +```ts +inject: { + excludeBelowPlugins: ['tr'], + excludePlugins: ['table', 'code_line', 'column_group', 'column'], +}, +``` +- Add `floating-toolbar-plugin`, `fixed-toolbar-plugin` +- misc: `hr-element`, `plate-element`, `transforms` + ### November 7 #16.4 - `block-context-menu`: prevent unselect when clicking on the context menu diff --git a/apps/www/content/docs/components/installation/manual.mdx b/apps/www/content/docs/components/installation/manual.mdx index da4444fe93..c921b1c584 100644 --- a/apps/www/content/docs/components/installation/manual.mdx +++ b/apps/www/content/docs/components/installation/manual.mdx @@ -16,7 +16,7 @@ Components are styled using Tailwind CSS. You need to install Tailwind CSS in yo Add the following dependencies to your project: ```bash -npm install tailwindcss-animate class-variance-authority lucide-react @udecode/cn @udecode/plate-common slate slate-react slate-history slate-hyperscript +npm install slate slate-dom slate-react slate-history slate-hyperscript @udecode/plate-common @udecode/cn class-variance-authority tailwindcss-animate tailwind-scrollbar-hide lucide-react ``` ### Configure path aliases diff --git a/apps/www/content/docs/cursor-overlay.mdx b/apps/www/content/docs/cursor-overlay.mdx new file mode 100644 index 0000000000..3f67b5c414 --- /dev/null +++ b/apps/www/content/docs/cursor-overlay.mdx @@ -0,0 +1,115 @@ +--- +title: Cursor Overlay +description: A visual overlay for cursors and selections. +docs: + - route: /docs/components/cursor-overlay + title: Cursor Overlay +--- + + + + + +The Cursor Overlay feature provides visual feedback for selections and cursor positions, particularly useful for maintaining context when the editor loses focus or during drag operations. + +## Features + +- Maintains selection highlight when another element is focused +- Dynamic selection (e.g. during AI streaming) +- Shows cursor position during drag operations +- Customizable styling for cursors and selections +- Essential for external UI interactions (e.g. link toolbar, AI combobox) + + + +## Installation + +```bash +npm install @udecode/plate-selection +``` + +## Usage + + +```tsx +import { CursorOverlayPlugin } from '@udecode/plate-selection/react'; +import { CursorOverlay } from '@/components/plate-ui/cursor-overlay'; +``` + +- [CursorOverlay](/docs/components/cursor-overlay) + +```tsx +const plugins = [ + // ...otherPlugins, + CursorOverlayPlugin.configure({ + render: { afterEditable: () => }, + }), +]; +``` + +## Plugins + +### CursorOverlayPlugin + +Plugin that manages cursor and selection overlays. + + + +Object containing cursor states. Default: `{}` + + + +## API + +### editor.api.cursorOverlay.addCursor + +Adds a cursor overlay with the specified key and state. + + + + Unique identifier for the cursor (e.g., 'blur', 'drag', 'custom'). + + + The cursor state including selection and optional styling data. + + + +### editor.api.cursorOverlay.removeCursor + +Removes a cursor overlay by its key. + + + + The key of the cursor to remove. + + + +## Hooks + +### useCursorOverlay + +A hook that manages cursor and selection overlay states, providing real-time cursor positions and selection rectangles. + + + + + + Minimum width in pixels for a selection rectangle. Useful for making cursor carets more visible. + - **Default:** `1` + + + Whether to recalculate cursor positions when the container is resized. + - **Default:** `true` + + + + + + + + Array of cursor states with their corresponding selection rectangles and styling data. + + + Function to manually trigger a recalculation of cursor positions. + + diff --git a/apps/www/content/docs/getting-started.mdx b/apps/www/content/docs/getting-started.mdx index 8a3bfbd8fa..33686bc35b 100644 --- a/apps/www/content/docs/getting-started.mdx +++ b/apps/www/content/docs/getting-started.mdx @@ -23,13 +23,13 @@ For an existing project, jump to the next step. Install the core and the plugins you need. You need at least: ```bash -npm install @udecode/plate-common slate slate-react slate-history slate-hyperscript react react-dom +npm install @udecode/plate-common slate slate-dom slate-react slate-history slate-hyperscript react react-dom ``` Alternatively you can install **`@udecode/plate`** that contains all the packages excluding the ones with heavy dependencies (e.g. **`@udecode/plate-dnd`**). ```bash -npm install @udecode/plate slate slate-react slate-history slate-hyperscript react react-dom +npm install @udecode/plate slate slate-dom slate-react slate-history slate-hyperscript react react-dom ``` ### Basic Editor diff --git a/apps/www/content/docs/toc.mdx b/apps/www/content/docs/toc.mdx index 1e98845e1a..38f6e1bef2 100644 --- a/apps/www/content/docs/toc.mdx +++ b/apps/www/content/docs/toc.mdx @@ -39,7 +39,6 @@ const plugins = [ NodeIdPlugin, TocPlugin.configure({ options: { - scrollContainerSelector: `#your-scroll-container-id`, topOffset: 80, }, }), @@ -55,27 +54,9 @@ const components = { - [TocElement](/docs/components/toc-element) -### Set scroll container +### Scroll container -The `TocPlugin` requires a scroll container selector to function properly. This selector is used to identify the scrollable area containing your content. - -```tsx -TocPlugin.configure({ - options: { - scrollContainerSelector: '#your-scroll-container-id', - }, -}) -``` - -You can use any valid CSS selector: - -- ID: `#scrollable-container` -- Class: `.content-wrapper` -- Nested selector: `body #main-content` - -Apply this selector to your `` component or a wrapper `
` around it, depending on your layout. - -For consistency, use the same container for both `TocPlugin` and `BlockSelectionPlugin`. +If not using [Editor](/docs/components/editor), don't forget to pass `useEditorContainerRef()` to your scroll container `ref` prop. ### Components @@ -112,13 +93,6 @@ The top offset to apply when scrolling to a heading. A custom function to query headings from the editor. - -The CSS selector for the scrollable container. Required to use scrolling behavior. - -- **Default:** `'#scroll_container'` - - - ## Transforms ### insertToc diff --git a/apps/www/package.json b/apps/www/package.json index fcd52c7871..861f0b3c2a 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -86,7 +86,6 @@ "@udecode/plate-common": "workspace:^", "@udecode/plate-core": "workspace:^", "@udecode/plate-csv": "workspace:^", - "@udecode/plate-cursor": "workspace:^", "@udecode/plate-date": "workspace:^", "@udecode/plate-diff": "workspace:^", "@udecode/plate-dnd": "workspace:^", @@ -159,10 +158,11 @@ "react-tweet": "^3.2.1", "react-wrap-balancer": "^1.1.1", "sass": "^1.78.0", - "slate": "0.103.0", - "slate-history": "0.109.0", + "slate": "0.110.2", + "slate-dom": "0.111.0", + "slate-history": "0.110.3", "slate-hyperscript": "0.100.0", - "slate-react": "0.110.1", + "slate-react": "0.111.0", "slate-test-utils": "1.3.2", "sonner": "^1.5.0", "swr": "2.2.6-beta.3", @@ -192,6 +192,7 @@ "remark-gfm": "^4.0.0", "rimraf": "^6.0.1", "shiki": "^1.17.5", + "tailwind-scrollbar-hide": "1.1.7", "tailwindcss": "^3.4.11", "tailwindcss-animate": "^1.0.7", "ts-node": "^10.9.2", diff --git a/apps/www/public/r/index.json b/apps/www/public/r/index.json index 03532355a2..75aafb645a 100644 --- a/apps/www/public/r/index.json +++ b/apps/www/public/r/index.json @@ -1328,7 +1328,8 @@ "registryDependencies": [ "calendar", "plate-element", - "context-menu" + "context-menu", + "use-is-touch-device" ], "type": "registry:ui" }, @@ -1544,15 +1545,14 @@ }, { "dependencies": [ - "@udecode/plate-cursor", - "@udecode/plate-dnd", "@udecode/plate-selection" ], "doc": { - "description": "A visual overlay for collaborative cursors and selections.", + "description": "A visual overlay for cursors and selections.", "docs": [ { - "route": "https://pro.platejs.org/docs/components/cursor-overlay" + "route": "/docs/cursor-overlay", + "title": "Cursor Overlay" } ], "examples": [ @@ -1764,11 +1764,11 @@ ], "files": [ { - "path": "plate-ui/fixed-toolbar-buttons-list.tsx", + "path": "plate-ui/fixed-toolbar-list-buttons.tsx", "type": "registry:ui" } ], - "name": "fixed-toolbar-buttons-list", + "name": "fixed-toolbar-list-buttons", "registryDependencies": [ "toolbar", "ai-toolbar-button", diff --git a/apps/www/public/r/styles/default/ai-demo.json b/apps/www/public/r/styles/default/ai-demo.json index 3ad63eddef..a5d7181773 100644 --- a/apps/www/public/r/styles/default/ai-demo.json +++ b/apps/www/public/r/styles/default/ai-demo.json @@ -9,7 +9,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React, { useRef } from 'react';\n\nimport type { ValueId } from '@/config/customizer-plugins';\n\nimport { cn } from '@udecode/cn';\nimport { AutoformatPlugin } from '@udecode/plate-autoformat/react';\nimport { SingleLinePlugin } from '@udecode/plate-break/react';\nimport { CommentsPlugin } from '@udecode/plate-comments/react';\nimport { Plate, usePlateEditor } from '@udecode/plate-common/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { ListPlugin, TodoListPlugin } from '@udecode/plate-list/react';\nimport { NormalizeTypesPlugin } from '@udecode/plate-normalizers';\nimport { PlaywrightPlugin } from '@udecode/plate-playwright';\nimport { TablePlugin } from '@udecode/plate-table/react';\n\nimport { CheckPlugin } from '@/components/context/check-plugin';\nimport { settingsStore } from '@/components/context/settings-store';\nimport { getAutoformatOptions } from '@/lib/plate/demo/plugins/autoformatOptions';\nimport { createPlateUI } from '@/plate/create-plate-ui';\nimport { editableProps } from '@/plate/demo/editableProps';\nimport { isEnabled } from '@/plate/demo/is-enabled';\nimport { DragOverCursorPlugin } from '@/plate/demo/plugins/DragOverCursorPlugin';\nimport { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue';\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { CommentsPopover } from '@/components/plate-ui/comments-popover';\nimport { CursorOverlay } from '@/components/plate-ui/cursor-overlay';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\nimport { FixedToolbar } from '@/components/plate-ui/fixed-toolbar';\nimport { FixedToolbarButtons } from '@/components/plate-ui/fixed-toolbar-buttons';\nimport { FixedToolbarButtonsList } from '@/components/plate-ui/fixed-toolbar-buttons-list';\nimport { FloatingToolbar } from '@/components/plate-ui/floating-toolbar';\nimport { FloatingToolbarButtons } from '@/components/plate-ui/floating-toolbar-buttons';\n\nimport { usePlaygroundEnabled } from './usePlaygroundEnabled';\n\nexport const usePlaygroundEditor = (id: any = '') => {\n const enabledPlugins = settingsStore.use.checkedPlugins();\n const overridePlugins = usePlaygroundEnabled(id);\n const autoformatOptions = getAutoformatOptions(id, enabledPlugins);\n\n const value = usePlaygroundValue(id);\n const key = settingsStore.use.version();\n const editorId = id || 'playground-' + key;\n\n return usePlateEditor(\n {\n id: editorId,\n override: {\n components: createPlateUI({\n draggable: isEnabled('dnd', id),\n placeholder: isEnabled('placeholder', id),\n }),\n plugins: overridePlugins,\n },\n plugins: [\n ...editorPlugins,\n\n AutoformatPlugin.configure({\n options: autoformatOptions,\n }),\n TablePlugin.configure({\n options: {\n enableMerging: id === 'tableMerge',\n },\n }),\n ListPlugin,\n TodoListPlugin,\n ExcalidrawPlugin,\n NormalizeTypesPlugin.configure({\n options: {\n rules: [{ path: [0], strictType: HEADING_KEYS.h1 }],\n },\n }),\n SingleLinePlugin,\n\n // Testing\n PlaywrightPlugin.configure({\n enabled: process.env.NODE_ENV !== 'production',\n }),\n ],\n value: value,\n },\n []\n );\n};\n\nexport default function PlaygroundDemo({\n id,\n className,\n}: {\n id?: ValueId;\n className?: string;\n scrollSelector?: string;\n}) {\n const containerRef = useRef(null);\n const enabled = settingsStore.use.checkedComponents();\n\n const editor = usePlaygroundEditor(id);\n\n return (\n \n \n \n \n \n {id === 'list' ? (\n \n ) : (\n \n )}\n \n \n \n\n
\n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n
\n
\n
\n );\n}\n\nconst DemoIdContext = React.createContext(undefined);\n\nexport function DemoId({\n id,\n children,\n}: {\n children: React.ReactNode;\n id?: string;\n}) {\n return {children};\n}\n\nexport function useDemoId() {\n return React.useContext(DemoIdContext);\n}\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport type { ValueId } from '@/config/customizer-plugins';\n\nimport { cn } from '@udecode/cn';\nimport { SingleLinePlugin } from '@udecode/plate-break/react';\nimport { Plate, usePlateEditor } from '@udecode/plate-common/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { ListPlugin, TodoListPlugin } from '@udecode/plate-list/react';\nimport { NormalizeTypesPlugin } from '@udecode/plate-normalizers';\nimport { PlaywrightPlugin } from '@udecode/plate-playwright';\nimport { TablePlugin } from '@udecode/plate-table/react';\n\nimport { settingsStore } from '@/components/context/settings-store';\nimport { createPlateUI } from '@/plate/create-plate-ui';\nimport { isEnabled } from '@/plate/demo/is-enabled';\nimport { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue';\nimport { autoformatPlugin as autoformatListPlugin } from '@/components/editor/plugins/autoformat-list-plugin';\nimport { autoformatPlugin } from '@/components/editor/plugins/autoformat-plugin';\nimport { copilotPlugins } from '@/components/editor/plugins/copilot-plugins';\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { FixedToolbarListPlugin } from '@/components/editor/plugins/fixed-toolbar-list-plugin';\nimport { FixedToolbarPlugin } from '@/components/editor/plugins/fixed-toolbar-plugin';\nimport { FloatingToolbarPlugin } from '@/components/editor/plugins/floating-toolbar-plugin';\nimport { tabbablePlugin } from '@/components/editor/plugins/tabbable-plugin';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nimport { usePlaygroundEnabled } from './usePlaygroundEnabled';\n\nexport const usePlaygroundEditor = (id: any = '') => {\n const overridePlugins = usePlaygroundEnabled(id);\n\n const value = usePlaygroundValue(id);\n const key = settingsStore.use.version();\n const editorId = id || 'playground-' + key;\n\n const plugins: any[] = [\n ...copilotPlugins,\n ...editorPlugins,\n id === 'list' ? FixedToolbarListPlugin : FixedToolbarPlugin,\n FloatingToolbarPlugin,\n\n id === 'list' ? autoformatListPlugin : autoformatPlugin,\n TablePlugin.configure({\n options: {\n enableMerging: id === 'tableMerge',\n },\n }),\n ListPlugin,\n TodoListPlugin,\n ExcalidrawPlugin,\n NormalizeTypesPlugin.configure({\n options: {\n rules: [{ path: [0], strictType: HEADING_KEYS.h1 }],\n },\n }),\n SingleLinePlugin,\n\n // Testing\n PlaywrightPlugin.configure({\n enabled: process.env.NODE_ENV !== 'production',\n }),\n ];\n\n if (id === 'tabbable') {\n plugins.push(tabbablePlugin);\n }\n\n return usePlateEditor(\n {\n id: editorId,\n override: {\n components: createPlateUI({\n draggable: isEnabled('dnd', id),\n placeholder: isEnabled('placeholder', id),\n }),\n plugins: overridePlugins,\n },\n plugins,\n value: value,\n },\n []\n );\n};\n\nexport default function PlaygroundDemo({\n id,\n className,\n}: {\n id?: ValueId;\n className?: string;\n scrollSelector?: string;\n}) {\n const editor = usePlaygroundEditor(id);\n\n return (\n \n \n \n \n \n \n \n );\n}\n\nconst DemoIdContext = React.createContext(undefined);\n\nexport function DemoId({\n id,\n children,\n}: {\n children: React.ReactNode;\n id?: string;\n}) {\n return {children};\n}\n\nexport function useDemoId() {\n return React.useContext(DemoIdContext);\n}\n", "path": "example/playground-demo.tsx", "target": "components/playground-demo.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/ai-menu.json b/apps/www/public/r/styles/default/ai-menu.json index f50ecb8350..35919c7148 100644 --- a/apps/www/public/r/styles/default/ai-menu.json +++ b/apps/www/public/r/styles/default/ai-menu.json @@ -27,13 +27,13 @@ }, "files": [ { - "content": "'use client';\n\nimport * as React from 'react';\n\nimport { AIChatPlugin, useEditorChat } from '@udecode/plate-ai/react';\nimport {\n type TElement,\n type TNodeEntry,\n getAncestorNode,\n getBlocks,\n isElementEmpty,\n isHotkey,\n isSelectionAtBlockEnd,\n} from '@udecode/plate-common';\nimport {\n type PlateEditor,\n toDOMNode,\n useEditorPlugin,\n useHotkeys,\n} from '@udecode/plate-common/react';\nimport {\n BlockSelectionPlugin,\n useIsSelecting,\n} from '@udecode/plate-selection/react';\nimport { Loader2Icon } from 'lucide-react';\n\nimport { useChat } from '@/components/editor/use-chat';\n\nimport { AIChatEditor } from './ai-chat-editor';\nimport { AIMenuItems } from './ai-menu-items';\nimport { Command, CommandList, InputCommand } from './command';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\n\nexport function AIMenu() {\n const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);\n const open = useOption('open');\n const mode = useOption('mode');\n const isSelecting = useIsSelecting();\n\n const aiEditorRef = React.useRef(null);\n const [value, setValue] = React.useState('');\n\n const chat = useChat();\n\n const { input, isLoading, messages, setInput } = chat;\n const [anchorElement, setAnchorElement] = React.useState(\n null\n );\n\n const setOpen = (open: boolean) => {\n if (open) {\n api.aiChat.show();\n } else {\n api.aiChat.hide();\n }\n };\n\n const show = (anchorElement: HTMLElement) => {\n setAnchorElement(anchorElement);\n setOpen(true);\n };\n\n useEditorChat({\n chat,\n onOpenBlockSelection: (blocks: TNodeEntry[]) => {\n show(toDOMNode(editor, blocks.at(-1)![0])!);\n },\n onOpenChange: (open) => {\n if (!open) {\n setAnchorElement(null);\n setInput('');\n }\n },\n onOpenCursor: () => {\n const ancestor = getAncestorNode(editor)?.[0] as TElement;\n\n if (!isSelectionAtBlockEnd(editor) && !isElementEmpty(editor, ancestor)) {\n editor\n .getApi(BlockSelectionPlugin)\n .blockSelection.addSelectedRow(ancestor.id as string);\n }\n\n show(toDOMNode(editor, ancestor)!);\n },\n onOpenSelection: () => {\n show(toDOMNode(editor, getBlocks(editor).at(-1)![0])!);\n },\n });\n\n useHotkeys(\n 'meta+j',\n () => {\n api.aiChat.show();\n },\n { enableOnContentEditable: true, enableOnFormTags: true }\n );\n\n return (\n \n \n\n {\n e.preventDefault();\n\n if (isLoading) {\n api.aiChat.stop();\n } else {\n api.aiChat.hide();\n }\n }}\n align=\"center\"\n avoidCollisions={false}\n side=\"bottom\"\n >\n \n {mode === 'chat' && isSelecting && messages.length > 0 && (\n \n )}\n\n {isLoading ? (\n
\n \n {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n
\n ) : (\n {\n if (isHotkey('backspace')(e) && input.length === 0) {\n e.preventDefault();\n api.aiChat.hide();\n }\n if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n e.preventDefault();\n void api.aiChat.submit();\n }\n }}\n onValueChange={setInput}\n placeholder=\"Ask AI anything...\"\n autoFocus\n />\n )}\n\n {!isLoading && (\n \n \n \n )}\n \n \n
\n );\n}\n", + "content": "'use client';\n\nimport * as React from 'react';\n\nimport { AIChatPlugin, useEditorChat } from '@udecode/plate-ai/react';\nimport {\n type TElement,\n type TNodeEntry,\n getAncestorNode,\n getBlocks,\n isElementEmpty,\n isHotkey,\n isSelectionAtBlockEnd,\n} from '@udecode/plate-common';\nimport {\n type PlateEditor,\n toDOMNode,\n useEditorPlugin,\n useHotkeys,\n} from '@udecode/plate-common/react';\nimport {\n BlockSelectionPlugin,\n useIsSelecting,\n} from '@udecode/plate-selection/react';\nimport { Loader2Icon } from 'lucide-react';\n\nimport { useChat } from '@/components/editor/use-chat';\n\nimport { AIChatEditor } from './ai-chat-editor';\nimport { AIMenuItems } from './ai-menu-items';\nimport { Command, CommandList, InputCommand } from './command';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\n\nexport function AIMenu() {\n const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);\n const open = useOption('open');\n const mode = useOption('mode');\n const isSelecting = useIsSelecting();\n\n const aiEditorRef = React.useRef(null);\n const [value, setValue] = React.useState('');\n\n const chat = useChat();\n\n const { input, isLoading, messages, setInput } = chat;\n const [anchorElement, setAnchorElement] = React.useState(\n null\n );\n\n const setOpen = (open: boolean) => {\n if (open) {\n api.aiChat.show();\n } else {\n api.aiChat.hide();\n }\n };\n\n const show = (anchorElement: HTMLElement) => {\n setAnchorElement(anchorElement);\n setOpen(true);\n };\n\n useEditorChat({\n chat,\n onOpenBlockSelection: (blocks: TNodeEntry[]) => {\n show(toDOMNode(editor, blocks.at(-1)![0])!);\n },\n onOpenChange: (open) => {\n if (!open) {\n setAnchorElement(null);\n setInput('');\n }\n },\n onOpenCursor: () => {\n const ancestor = getAncestorNode(editor)?.[0] as TElement;\n\n if (!isSelectionAtBlockEnd(editor) && !isElementEmpty(editor, ancestor)) {\n editor\n .getApi(BlockSelectionPlugin)\n .blockSelection.addSelectedRow(ancestor.id as string);\n }\n\n show(toDOMNode(editor, ancestor)!);\n },\n onOpenSelection: () => {\n show(toDOMNode(editor, getBlocks(editor).at(-1)![0])!);\n },\n });\n\n useHotkeys(\n 'meta+j',\n () => {\n api.aiChat.show();\n },\n { enableOnContentEditable: true, enableOnFormTags: true }\n );\n\n return (\n \n \n\n {\n e.preventDefault();\n\n if (isLoading) {\n api.aiChat.stop();\n } else {\n api.aiChat.hide();\n }\n }}\n align=\"center\"\n avoidCollisions={false}\n side=\"bottom\"\n >\n \n {mode === 'chat' && isSelecting && messages.length > 0 && (\n \n )}\n\n {isLoading ? (\n
\n \n {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n
\n ) : (\n {\n if (isHotkey('backspace')(e) && input.length === 0) {\n e.preventDefault();\n api.aiChat.hide();\n }\n if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n e.preventDefault();\n void api.aiChat.submit();\n }\n }}\n onValueChange={setInput}\n placeholder=\"Ask AI anything...\"\n data-plate-focus\n autoFocus\n />\n )}\n\n {!isLoading && (\n \n \n \n )}\n \n \n
\n );\n}\n", "path": "plate-ui/ai-menu.tsx", "target": "components/plate-ui/ai-menu.tsx", "type": "registry:ui" }, { - "content": "'use client';\n\nimport { useEffect, useMemo } from 'react';\n\nimport { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';\nimport {\n getAncestorNode,\n getEndPoint,\n getNodeString,\n} from '@udecode/plate-common';\nimport {\n type PlateEditor,\n focusEditor,\n useEditorPlugin,\n} from '@udecode/plate-common/react';\nimport { useIsSelecting } from '@udecode/plate-selection/react';\nimport {\n Album,\n BadgeHelp,\n Check,\n CornerUpLeft,\n FeatherIcon,\n ListEnd,\n ListMinus,\n ListPlus,\n PenLine,\n Wand,\n X,\n} from 'lucide-react';\n\nimport { CommandGroup, CommandItem } from './command';\n\nexport type EditorChatState =\n | 'cursorCommand'\n | 'cursorSuggestion'\n | 'selectionCommand'\n | 'selectionSuggestion';\n\nexport const aiChatItems = {\n accept: {\n icon: ,\n label: 'Accept',\n value: 'accept',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIChatPlugin).aiChat.accept();\n focusEditor(editor, getEndPoint(editor, editor.selection!));\n },\n },\n continueWrite: {\n icon: ,\n label: 'Continue writing',\n value: 'continueWrite',\n onSelect: ({ editor }) => {\n const ancestorNode = getAncestorNode(editor);\n const isEmpty = getNodeString(ancestorNode![0]).trim().length === 0;\n\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: isEmpty\n ? `\n{editor}\n\nStart writing a new paragraph AFTER ONLY ONE SENTENCE`\n : 'Continue writing AFTER ONLY ONE SENTENCE. DONT REPEAT THE TEXT.',\n });\n },\n },\n discard: {\n icon: ,\n label: 'Discard',\n shortcut: 'Escape',\n value: 'discard',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIPlugin).ai.undo();\n editor.getApi(AIChatPlugin).aiChat.hide();\n },\n },\n explain: {\n icon: ,\n label: 'Explain',\n value: 'explain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: {\n default: 'Explain {editor}',\n selecting: 'Explain',\n },\n });\n },\n },\n fixSpelling: {\n icon: ,\n label: 'Fix spelling & grammar',\n value: 'fixSpelling',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Fix spelling and grammar',\n });\n },\n },\n improveWriting: {\n icon: ,\n label: 'Improve writing',\n value: 'improveWriting',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Improve the writing',\n });\n },\n },\n insertBelow: {\n icon: ,\n label: 'Insert below',\n value: 'insertBelow',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.insertBelow(aiEditor);\n },\n },\n makeLonger: {\n icon: ,\n label: 'Make longer',\n value: 'makeLonger',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make longer',\n });\n },\n },\n makeShorter: {\n icon: ,\n label: 'Make shorter',\n value: 'makeShorter',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make shorter',\n });\n },\n },\n replace: {\n icon: ,\n label: 'Replace selection',\n value: 'replace',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.replaceSelection(aiEditor);\n },\n },\n simplifyLanguage: {\n icon: ,\n label: 'Simplify language',\n value: 'simplifyLanguage',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Simplify the language',\n });\n },\n },\n summarize: {\n icon: ,\n label: 'Add a summary',\n value: 'summarize',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: {\n default: 'Summarize {editor}',\n selecting: 'Summarize',\n },\n });\n },\n },\n tryAgain: {\n icon: ,\n label: 'Try again',\n value: 'tryAgain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.reload();\n },\n },\n} satisfies Record<\n string,\n {\n icon: React.ReactNode;\n label: string;\n value: string;\n component?: React.ComponentType<{ menuState: EditorChatState }>;\n filterItems?: boolean;\n items?: { label: string; value: string }[];\n shortcut?: string;\n onSelect?: ({\n aiEditor,\n editor,\n }: {\n aiEditor: PlateEditor;\n editor: PlateEditor;\n }) => void;\n }\n>;\n\nconst menuStateItems: Record<\n EditorChatState,\n {\n items: (typeof aiChatItems)[keyof typeof aiChatItems][];\n heading?: string;\n }[]\n> = {\n cursorCommand: [\n {\n items: [\n aiChatItems.continueWrite,\n aiChatItems.summarize,\n aiChatItems.explain,\n ],\n },\n ],\n cursorSuggestion: [\n {\n items: [aiChatItems.accept, aiChatItems.discard, aiChatItems.tryAgain],\n },\n ],\n selectionCommand: [\n {\n items: [\n aiChatItems.improveWriting,\n aiChatItems.makeLonger,\n aiChatItems.makeShorter,\n aiChatItems.fixSpelling,\n aiChatItems.simplifyLanguage,\n ],\n },\n ],\n selectionSuggestion: [\n {\n items: [\n aiChatItems.replace,\n aiChatItems.insertBelow,\n aiChatItems.discard,\n aiChatItems.tryAgain,\n ],\n },\n ],\n};\n\nexport const AIMenuItems = ({\n aiEditorRef,\n setValue,\n}: {\n aiEditorRef: React.MutableRefObject;\n setValue: (value: string) => void;\n}) => {\n const { editor, useOption } = useEditorPlugin(AIChatPlugin);\n const { messages } = useOption('chat');\n const isSelecting = useIsSelecting();\n\n const menuState = useMemo(() => {\n if (messages && messages.length > 0) {\n return isSelecting ? 'selectionSuggestion' : 'cursorSuggestion';\n }\n\n return isSelecting ? 'selectionCommand' : 'cursorCommand';\n }, [isSelecting, messages]);\n\n const menuGroups = useMemo(() => {\n const items = menuStateItems[menuState];\n\n return items;\n }, [menuState]);\n\n useEffect(() => {\n if (menuGroups.length > 0 && menuGroups[0].items.length > 0) {\n setValue(menuGroups[0].items[0].value);\n }\n }, [menuGroups, setValue]);\n\n return (\n <>\n {menuGroups.map((group, index) => (\n \n {group.items.map((menuItem) => (\n {\n menuItem.onSelect?.({\n aiEditor: aiEditorRef.current!,\n editor: editor,\n });\n }}\n >\n {menuItem.icon}\n {menuItem.label}\n \n ))}\n \n ))}\n \n );\n};\n", + "content": "'use client';\n\nimport { useEffect, useMemo } from 'react';\n\nimport { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';\nimport {\n getAncestorNode,\n getEndPoint,\n getNodeString,\n} from '@udecode/plate-common';\nimport {\n type PlateEditor,\n focusEditor,\n useEditorPlugin,\n} from '@udecode/plate-common/react';\nimport { useIsSelecting } from '@udecode/plate-selection/react';\nimport {\n Album,\n BadgeHelp,\n Check,\n CornerUpLeft,\n FeatherIcon,\n ListEnd,\n ListMinus,\n ListPlus,\n PenLine,\n Wand,\n X,\n} from 'lucide-react';\n\nimport { CommandGroup, CommandItem } from './command';\n\nexport type EditorChatState =\n | 'cursorCommand'\n | 'cursorSuggestion'\n | 'selectionCommand'\n | 'selectionSuggestion';\n\nexport const aiChatItems = {\n accept: {\n icon: ,\n label: 'Accept',\n value: 'accept',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIChatPlugin).aiChat.accept();\n focusEditor(editor, getEndPoint(editor, editor.selection!));\n },\n },\n continueWrite: {\n icon: ,\n label: 'Continue writing',\n value: 'continueWrite',\n onSelect: ({ editor }) => {\n const ancestorNode = getAncestorNode(editor);\n\n if (!ancestorNode) return;\n\n const isEmpty = getNodeString(ancestorNode[0]).trim().length === 0;\n\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: isEmpty\n ? `\n{editor}\n\nStart writing a new paragraph AFTER ONLY ONE SENTENCE`\n : 'Continue writing AFTER ONLY ONE SENTENCE. DONT REPEAT THE TEXT.',\n });\n },\n },\n discard: {\n icon: ,\n label: 'Discard',\n shortcut: 'Escape',\n value: 'discard',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIPlugin).ai.undo();\n editor.getApi(AIChatPlugin).aiChat.hide();\n },\n },\n explain: {\n icon: ,\n label: 'Explain',\n value: 'explain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: {\n default: 'Explain {editor}',\n selecting: 'Explain',\n },\n });\n },\n },\n fixSpelling: {\n icon: ,\n label: 'Fix spelling & grammar',\n value: 'fixSpelling',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Fix spelling and grammar',\n });\n },\n },\n improveWriting: {\n icon: ,\n label: 'Improve writing',\n value: 'improveWriting',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Improve the writing',\n });\n },\n },\n insertBelow: {\n icon: ,\n label: 'Insert below',\n value: 'insertBelow',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.insertBelow(aiEditor);\n },\n },\n makeLonger: {\n icon: ,\n label: 'Make longer',\n value: 'makeLonger',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make longer',\n });\n },\n },\n makeShorter: {\n icon: ,\n label: 'Make shorter',\n value: 'makeShorter',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make shorter',\n });\n },\n },\n replace: {\n icon: ,\n label: 'Replace selection',\n value: 'replace',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.replaceSelection(aiEditor);\n },\n },\n simplifyLanguage: {\n icon: ,\n label: 'Simplify language',\n value: 'simplifyLanguage',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Simplify the language',\n });\n },\n },\n summarize: {\n icon: ,\n label: 'Add a summary',\n value: 'summarize',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: {\n default: 'Summarize {editor}',\n selecting: 'Summarize',\n },\n });\n },\n },\n tryAgain: {\n icon: ,\n label: 'Try again',\n value: 'tryAgain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.reload();\n },\n },\n} satisfies Record<\n string,\n {\n icon: React.ReactNode;\n label: string;\n value: string;\n component?: React.ComponentType<{ menuState: EditorChatState }>;\n filterItems?: boolean;\n items?: { label: string; value: string }[];\n shortcut?: string;\n onSelect?: ({\n aiEditor,\n editor,\n }: {\n aiEditor: PlateEditor;\n editor: PlateEditor;\n }) => void;\n }\n>;\n\nconst menuStateItems: Record<\n EditorChatState,\n {\n items: (typeof aiChatItems)[keyof typeof aiChatItems][];\n heading?: string;\n }[]\n> = {\n cursorCommand: [\n {\n items: [\n aiChatItems.continueWrite,\n aiChatItems.summarize,\n aiChatItems.explain,\n ],\n },\n ],\n cursorSuggestion: [\n {\n items: [aiChatItems.accept, aiChatItems.discard, aiChatItems.tryAgain],\n },\n ],\n selectionCommand: [\n {\n items: [\n aiChatItems.improveWriting,\n aiChatItems.makeLonger,\n aiChatItems.makeShorter,\n aiChatItems.fixSpelling,\n aiChatItems.simplifyLanguage,\n ],\n },\n ],\n selectionSuggestion: [\n {\n items: [\n aiChatItems.replace,\n aiChatItems.insertBelow,\n aiChatItems.discard,\n aiChatItems.tryAgain,\n ],\n },\n ],\n};\n\nexport const AIMenuItems = ({\n aiEditorRef,\n setValue,\n}: {\n aiEditorRef: React.MutableRefObject;\n setValue: (value: string) => void;\n}) => {\n const { editor, useOption } = useEditorPlugin(AIChatPlugin);\n const { messages } = useOption('chat');\n const isSelecting = useIsSelecting();\n\n const menuState = useMemo(() => {\n if (messages && messages.length > 0) {\n return isSelecting ? 'selectionSuggestion' : 'cursorSuggestion';\n }\n\n return isSelecting ? 'selectionCommand' : 'cursorCommand';\n }, [isSelecting, messages]);\n\n const menuGroups = useMemo(() => {\n const items = menuStateItems[menuState];\n\n return items;\n }, [menuState]);\n\n useEffect(() => {\n if (menuGroups.length > 0 && menuGroups[0].items.length > 0) {\n setValue(menuGroups[0].items[0].value);\n }\n }, [menuGroups, setValue]);\n\n return (\n <>\n {menuGroups.map((group, index) => (\n \n {group.items.map((menuItem) => (\n {\n menuItem.onSelect?.({\n aiEditor: aiEditorRef.current!,\n editor: editor,\n });\n }}\n >\n {menuItem.icon}\n {menuItem.label}\n \n ))}\n \n ))}\n \n );\n};\n", "path": "plate-ui/ai-menu-items.tsx", "target": "components/plate-ui/ai-menu-items.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/ai-plugins.json b/apps/www/public/r/styles/default/ai-plugins.json index 8fb9b36881..18f90c7bfe 100644 --- a/apps/www/public/r/styles/default/ai-plugins.json +++ b/apps/www/public/r/styles/default/ai-plugins.json @@ -12,7 +12,7 @@ ], "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { withProps } from '@udecode/cn';\nimport { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\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 {\n ParagraphPlugin,\n PlateLeaf,\n createPlateEditor,\n} from '@udecode/plate-common/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport { MarkdownPlugin } from '@udecode/plate-markdown';\nimport { BlockSelectionPlugin } from '@udecode/plate-selection/react';\n\nimport { AIMenu } from '@/components/plate-ui/ai-menu';\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 { SelectionOverlayPlugin } from '@/components/plate-ui/cursor-overlay';\nimport { HeadingElement } from '@/components/plate-ui/heading-element';\nimport { HrElement } from '@/components/plate-ui/hr-element';\nimport { LinkElement } from '@/components/plate-ui/link-element';\nimport { ParagraphElement } from '@/components/plate-ui/paragraph-element';\n\nimport { basicNodesPlugins } from './basic-nodes-plugins';\nimport { indentListPlugins } from './indent-list-plugins';\nimport { linkPlugin } from './link-plugin';\n\nconst createAIEditor = () => {\n const editor = createPlateEditor({\n id: 'ai',\n override: {\n components: {\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 [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),\n [HorizontalRulePlugin.key]: HrElement,\n [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),\n [LinkPlugin.key]: LinkElement,\n [ParagraphPlugin.key]: ParagraphElement,\n [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),\n [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),\n },\n },\n plugins: [\n ParagraphPlugin,\n ...basicNodesPlugins,\n HorizontalRulePlugin,\n linkPlugin,\n ...indentListPlugins,\n MarkdownPlugin.configure({ options: { indentList: true } }),\n // FIXME\n BlockSelectionPlugin.configure({\n api: {},\n extendEditor: null,\n options: {},\n render: {},\n useHooks: null,\n handlers: {},\n }),\n ],\n value: [{ children: [{ text: '' }], type: 'p' }],\n });\n\n return editor;\n};\n\nconst systemCommon = `\\\nYou are an advanced AI-powered note-taking assistant, designed to enhance productivity and creativity in note management.\nRespond directly to user prompts with clear, concise, and relevant content. Maintain a neutral, helpful tone.\n\nRules:\n- is the entire note the user is working on.\n- is a reminder of how you should reply to INSTRUCTIONS. It does not apply to questions.\n- Anything else is the user prompt.\n- Your response should be tailored to the user's prompt, providing precise assistance to optimize note management.\n- For INSTRUCTIONS: Follow the exactly. Provide ONLY the content to be inserted or replaced. No explanations or comments.\n- For QUESTIONS: Provide a helpful and concise answer. You may include brief explanations if necessary.\n- CRITICAL: Distinguish between INSTRUCTIONS and QUESTIONS. Instructions typically ask you to modify or add content. Questions ask for information or clarification.\n`;\n\nconst systemDefault = `\\\n${systemCommon}\n- is the current block of text the user is working on.\n- Ensure your output can seamlessly fit into the existing structure.\n- CRITICAL: Provide only a single block of text. DO NOT create multiple paragraphs or separate blocks.\n\n{block}\n\n`;\n\nconst systemSelecting = `\\\n${systemCommon}\n- is the block of text containing the user's selection, providing context.\n- Ensure your output can seamlessly fit into the existing structure.\n- is the specific text the user has selected in the block and wants to modify or ask about.\n- Consider the context provided by , but only modify . Your response should be a direct replacement for .\n\n{block}\n\n\n{selection}\n\n`;\n\nconst systemBlockSelecting = `\\\n${systemCommon}\n- represents the full blocks of text the user has selected and wants to modify or ask about.\n- Your response should be a direct replacement for the entire .\n- Maintain the overall structure and formatting of the selected blocks, unless explicitly instructed otherwise.\n- CRITICAL: Provide only the content to replace . Do not add additional blocks or change the block structure unless specifically requested.\n\n{block}\n\n`;\n\nconst userDefault = `\nCRITICAL: DO NOT use block formatting. You can only use inline formatting.\nCRITICAL: DO NOT start new lines or paragraphs.\nNEVER write .\n\n{prompt}`;\n\nconst userSelecting = `\nIf this is a question, provide a helpful and concise answer about .\nIf this is an instruction, provide ONLY the text to replace . No explanations.\nEnsure it fits seamlessly within . If is empty, write ONE random sentence.\nNEVER write or .\n\n{prompt} about `;\n\nconst userBlockSelecting = `\nIf this is a question, provide a helpful and concise answer about .\nIf this is an instruction, provide ONLY the content to replace the entire . No explanations.\nMaintain the overall structure unless instructed otherwise.\nNEVER write or .\n\n{prompt} about `;\n\nexport const PROMPT_TEMPLATES = {\n systemBlockSelecting,\n systemDefault,\n systemSelecting,\n userBlockSelecting,\n userDefault,\n userSelecting,\n};\n\nexport const aiPlugins = [\n SelectionOverlayPlugin,\n MarkdownPlugin.configure({ options: { indentList: true } }),\n AIPlugin,\n AIChatPlugin.configure({\n options: {\n createAIEditor,\n promptTemplate: ({ isBlockSelecting, isSelecting }) => {\n return isBlockSelecting\n ? PROMPT_TEMPLATES.userBlockSelecting\n : isSelecting\n ? PROMPT_TEMPLATES.userSelecting\n : PROMPT_TEMPLATES.userDefault;\n },\n scrollContainerSelector: '#scroll_container',\n systemTemplate: ({ isBlockSelecting, isSelecting }) => {\n return isBlockSelecting\n ? PROMPT_TEMPLATES.systemBlockSelecting\n : isSelecting\n ? PROMPT_TEMPLATES.systemSelecting\n : PROMPT_TEMPLATES.systemDefault;\n },\n },\n render: { afterEditable: () => },\n }),\n] as const;\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { withProps } from '@udecode/cn';\nimport { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\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 {\n ParagraphPlugin,\n PlateLeaf,\n createPlateEditor,\n} from '@udecode/plate-common/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport { MarkdownPlugin } from '@udecode/plate-markdown';\nimport { BlockSelectionPlugin } from '@udecode/plate-selection/react';\n\nimport { AIMenu } from '@/components/plate-ui/ai-menu';\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 { HeadingElement } from '@/components/plate-ui/heading-element';\nimport { HrElement } from '@/components/plate-ui/hr-element';\nimport { LinkElement } from '@/components/plate-ui/link-element';\nimport { ParagraphElement } from '@/components/plate-ui/paragraph-element';\n\nimport { basicNodesPlugins } from './basic-nodes-plugins';\nimport { indentListPlugins } from './indent-list-plugins';\nimport { linkPlugin } from './link-plugin';\n\nconst createAIEditor = () => {\n const editor = createPlateEditor({\n id: 'ai',\n override: {\n components: {\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 [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),\n [HorizontalRulePlugin.key]: HrElement,\n [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),\n [LinkPlugin.key]: LinkElement,\n [ParagraphPlugin.key]: ParagraphElement,\n [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),\n [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),\n },\n },\n plugins: [\n ParagraphPlugin,\n ...basicNodesPlugins,\n HorizontalRulePlugin,\n linkPlugin,\n ...indentListPlugins,\n MarkdownPlugin.configure({ options: { indentList: true } }),\n // FIXME\n BlockSelectionPlugin.configure({\n api: {},\n extendEditor: null,\n options: {},\n render: {},\n useHooks: null,\n handlers: {},\n }),\n ],\n value: [{ children: [{ text: '' }], type: 'p' }],\n });\n\n return editor;\n};\n\nconst systemCommon = `\\\nYou are an advanced AI-powered note-taking assistant, designed to enhance productivity and creativity in note management.\nRespond directly to user prompts with clear, concise, and relevant content. Maintain a neutral, helpful tone.\n\nRules:\n- is the entire note the user is working on.\n- is a reminder of how you should reply to INSTRUCTIONS. It does not apply to questions.\n- Anything else is the user prompt.\n- Your response should be tailored to the user's prompt, providing precise assistance to optimize note management.\n- For INSTRUCTIONS: Follow the exactly. Provide ONLY the content to be inserted or replaced. No explanations or comments.\n- For QUESTIONS: Provide a helpful and concise answer. You may include brief explanations if necessary.\n- CRITICAL: Distinguish between INSTRUCTIONS and QUESTIONS. Instructions typically ask you to modify or add content. Questions ask for information or clarification.\n`;\n\nconst systemDefault = `\\\n${systemCommon}\n- is the current block of text the user is working on.\n- Ensure your output can seamlessly fit into the existing structure.\n- CRITICAL: Provide only a single block of text. DO NOT create multiple paragraphs or separate blocks.\n\n{block}\n\n`;\n\nconst systemSelecting = `\\\n${systemCommon}\n- is the block of text containing the user's selection, providing context.\n- Ensure your output can seamlessly fit into the existing structure.\n- is the specific text the user has selected in the block and wants to modify or ask about.\n- Consider the context provided by , but only modify . Your response should be a direct replacement for .\n\n{block}\n\n\n{selection}\n\n`;\n\nconst systemBlockSelecting = `\\\n${systemCommon}\n- represents the full blocks of text the user has selected and wants to modify or ask about.\n- Your response should be a direct replacement for the entire .\n- Maintain the overall structure and formatting of the selected blocks, unless explicitly instructed otherwise.\n- CRITICAL: Provide only the content to replace . Do not add additional blocks or change the block structure unless specifically requested.\n\n{block}\n\n`;\n\nconst userDefault = `\nCRITICAL: DO NOT use block formatting. You can only use inline formatting.\nCRITICAL: DO NOT start new lines or paragraphs.\nNEVER write .\n\n{prompt}`;\n\nconst userSelecting = `\nIf this is a question, provide a helpful and concise answer about .\nIf this is an instruction, provide ONLY the text to replace . No explanations.\nEnsure it fits seamlessly within . If is empty, write ONE random sentence.\nNEVER write or .\n\n{prompt} about `;\n\nconst userBlockSelecting = `\nIf this is a question, provide a helpful and concise answer about .\nIf this is an instruction, provide ONLY the content to replace the entire . No explanations.\nMaintain the overall structure unless instructed otherwise.\nNEVER write or .\n\n{prompt} about `;\n\nexport const PROMPT_TEMPLATES = {\n systemBlockSelecting,\n systemDefault,\n systemSelecting,\n userBlockSelecting,\n userDefault,\n userSelecting,\n};\n\nexport const aiPlugins = [\n MarkdownPlugin.configure({ options: { indentList: true } }),\n AIPlugin,\n AIChatPlugin.configure({\n options: {\n createAIEditor,\n promptTemplate: ({ isBlockSelecting, isSelecting }) => {\n return isBlockSelecting\n ? PROMPT_TEMPLATES.userBlockSelecting\n : isSelecting\n ? PROMPT_TEMPLATES.userSelecting\n : PROMPT_TEMPLATES.userDefault;\n },\n systemTemplate: ({ isBlockSelecting, isSelecting }) => {\n return isBlockSelecting\n ? PROMPT_TEMPLATES.systemBlockSelecting\n : isSelecting\n ? PROMPT_TEMPLATES.systemSelecting\n : PROMPT_TEMPLATES.systemDefault;\n },\n },\n render: { afterEditable: () => },\n }),\n] as const;\n", "path": "components/editor/plugins/ai-plugins.tsx", "target": "components/editor/plugins/ai-plugins.tsx", "type": "registry:component" diff --git a/apps/www/public/r/styles/default/ai-toolbar-button.json b/apps/www/public/r/styles/default/ai-toolbar-button.json index d988d31ed7..25beec8982 100644 --- a/apps/www/public/r/styles/default/ai-toolbar-button.json +++ b/apps/www/public/r/styles/default/ai-toolbar-button.json @@ -21,7 +21,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport { AIChatPlugin } from '@udecode/plate-ai/react';\nimport { useEditorPlugin } from '@udecode/plate-common/react';\n\nimport { ToolbarButton } from './toolbar';\n\nexport const AIToolbarButton = withRef(\n ({ children, ...rest }, ref) => {\n const { api } = useEditorPlugin(AIChatPlugin);\n\n return (\n {\n api.aiChat.show();\n }}\n >\n {children}\n \n );\n }\n);\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport { AIChatPlugin } from '@udecode/plate-ai/react';\nimport { useEditorPlugin } from '@udecode/plate-common/react';\n\nimport { ToolbarButton } from './toolbar';\n\nexport const AIToolbarButton = withRef(\n ({ children, ...rest }, ref) => {\n const { api } = useEditorPlugin(AIChatPlugin);\n\n return (\n {\n api.aiChat.show();\n }}\n onMouseDown={(e) => {\n e.preventDefault();\n }}\n >\n {children}\n \n );\n }\n);\n", "path": "plate-ui/ai-toolbar-button.tsx", "target": "components/plate-ui/ai-toolbar-button.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/align-demo.json b/apps/www/public/r/styles/default/align-demo.json index 27185ce4c2..31a1acbd3e 100644 --- a/apps/www/public/r/styles/default/align-demo.json +++ b/apps/www/public/r/styles/default/align-demo.json @@ -4,7 +4,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React, { useRef } from 'react';\n\nimport type { ValueId } from '@/config/customizer-plugins';\n\nimport { cn } from '@udecode/cn';\nimport { AutoformatPlugin } from '@udecode/plate-autoformat/react';\nimport { SingleLinePlugin } from '@udecode/plate-break/react';\nimport { CommentsPlugin } from '@udecode/plate-comments/react';\nimport { Plate, usePlateEditor } from '@udecode/plate-common/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { ListPlugin, TodoListPlugin } from '@udecode/plate-list/react';\nimport { NormalizeTypesPlugin } from '@udecode/plate-normalizers';\nimport { PlaywrightPlugin } from '@udecode/plate-playwright';\nimport { TablePlugin } from '@udecode/plate-table/react';\n\nimport { CheckPlugin } from '@/components/context/check-plugin';\nimport { settingsStore } from '@/components/context/settings-store';\nimport { getAutoformatOptions } from '@/lib/plate/demo/plugins/autoformatOptions';\nimport { createPlateUI } from '@/plate/create-plate-ui';\nimport { editableProps } from '@/plate/demo/editableProps';\nimport { isEnabled } from '@/plate/demo/is-enabled';\nimport { DragOverCursorPlugin } from '@/plate/demo/plugins/DragOverCursorPlugin';\nimport { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue';\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { CommentsPopover } from '@/components/plate-ui/comments-popover';\nimport { CursorOverlay } from '@/components/plate-ui/cursor-overlay';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\nimport { FixedToolbar } from '@/components/plate-ui/fixed-toolbar';\nimport { FixedToolbarButtons } from '@/components/plate-ui/fixed-toolbar-buttons';\nimport { FixedToolbarButtonsList } from '@/components/plate-ui/fixed-toolbar-buttons-list';\nimport { FloatingToolbar } from '@/components/plate-ui/floating-toolbar';\nimport { FloatingToolbarButtons } from '@/components/plate-ui/floating-toolbar-buttons';\n\nimport { usePlaygroundEnabled } from './usePlaygroundEnabled';\n\nexport const usePlaygroundEditor = (id: any = '') => {\n const enabledPlugins = settingsStore.use.checkedPlugins();\n const overridePlugins = usePlaygroundEnabled(id);\n const autoformatOptions = getAutoformatOptions(id, enabledPlugins);\n\n const value = usePlaygroundValue(id);\n const key = settingsStore.use.version();\n const editorId = id || 'playground-' + key;\n\n return usePlateEditor(\n {\n id: editorId,\n override: {\n components: createPlateUI({\n draggable: isEnabled('dnd', id),\n placeholder: isEnabled('placeholder', id),\n }),\n plugins: overridePlugins,\n },\n plugins: [\n ...editorPlugins,\n\n AutoformatPlugin.configure({\n options: autoformatOptions,\n }),\n TablePlugin.configure({\n options: {\n enableMerging: id === 'tableMerge',\n },\n }),\n ListPlugin,\n TodoListPlugin,\n ExcalidrawPlugin,\n NormalizeTypesPlugin.configure({\n options: {\n rules: [{ path: [0], strictType: HEADING_KEYS.h1 }],\n },\n }),\n SingleLinePlugin,\n\n // Testing\n PlaywrightPlugin.configure({\n enabled: process.env.NODE_ENV !== 'production',\n }),\n ],\n value: value,\n },\n []\n );\n};\n\nexport default function PlaygroundDemo({\n id,\n className,\n}: {\n id?: ValueId;\n className?: string;\n scrollSelector?: string;\n}) {\n const containerRef = useRef(null);\n const enabled = settingsStore.use.checkedComponents();\n\n const editor = usePlaygroundEditor(id);\n\n return (\n \n \n \n \n \n {id === 'list' ? (\n \n ) : (\n \n )}\n \n \n \n\n
\n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n
\n
\n
\n );\n}\n\nconst DemoIdContext = React.createContext(undefined);\n\nexport function DemoId({\n id,\n children,\n}: {\n children: React.ReactNode;\n id?: string;\n}) {\n return {children};\n}\n\nexport function useDemoId() {\n return React.useContext(DemoIdContext);\n}\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport type { ValueId } from '@/config/customizer-plugins';\n\nimport { cn } from '@udecode/cn';\nimport { SingleLinePlugin } from '@udecode/plate-break/react';\nimport { Plate, usePlateEditor } from '@udecode/plate-common/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { ListPlugin, TodoListPlugin } from '@udecode/plate-list/react';\nimport { NormalizeTypesPlugin } from '@udecode/plate-normalizers';\nimport { PlaywrightPlugin } from '@udecode/plate-playwright';\nimport { TablePlugin } from '@udecode/plate-table/react';\n\nimport { settingsStore } from '@/components/context/settings-store';\nimport { createPlateUI } from '@/plate/create-plate-ui';\nimport { isEnabled } from '@/plate/demo/is-enabled';\nimport { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue';\nimport { autoformatPlugin as autoformatListPlugin } from '@/components/editor/plugins/autoformat-list-plugin';\nimport { autoformatPlugin } from '@/components/editor/plugins/autoformat-plugin';\nimport { copilotPlugins } from '@/components/editor/plugins/copilot-plugins';\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { FixedToolbarListPlugin } from '@/components/editor/plugins/fixed-toolbar-list-plugin';\nimport { FixedToolbarPlugin } from '@/components/editor/plugins/fixed-toolbar-plugin';\nimport { FloatingToolbarPlugin } from '@/components/editor/plugins/floating-toolbar-plugin';\nimport { tabbablePlugin } from '@/components/editor/plugins/tabbable-plugin';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nimport { usePlaygroundEnabled } from './usePlaygroundEnabled';\n\nexport const usePlaygroundEditor = (id: any = '') => {\n const overridePlugins = usePlaygroundEnabled(id);\n\n const value = usePlaygroundValue(id);\n const key = settingsStore.use.version();\n const editorId = id || 'playground-' + key;\n\n const plugins: any[] = [\n ...copilotPlugins,\n ...editorPlugins,\n id === 'list' ? FixedToolbarListPlugin : FixedToolbarPlugin,\n FloatingToolbarPlugin,\n\n id === 'list' ? autoformatListPlugin : autoformatPlugin,\n TablePlugin.configure({\n options: {\n enableMerging: id === 'tableMerge',\n },\n }),\n ListPlugin,\n TodoListPlugin,\n ExcalidrawPlugin,\n NormalizeTypesPlugin.configure({\n options: {\n rules: [{ path: [0], strictType: HEADING_KEYS.h1 }],\n },\n }),\n SingleLinePlugin,\n\n // Testing\n PlaywrightPlugin.configure({\n enabled: process.env.NODE_ENV !== 'production',\n }),\n ];\n\n if (id === 'tabbable') {\n plugins.push(tabbablePlugin);\n }\n\n return usePlateEditor(\n {\n id: editorId,\n override: {\n components: createPlateUI({\n draggable: isEnabled('dnd', id),\n placeholder: isEnabled('placeholder', id),\n }),\n plugins: overridePlugins,\n },\n plugins,\n value: value,\n },\n []\n );\n};\n\nexport default function PlaygroundDemo({\n id,\n className,\n}: {\n id?: ValueId;\n className?: string;\n scrollSelector?: string;\n}) {\n const editor = usePlaygroundEditor(id);\n\n return (\n \n \n \n \n \n \n \n );\n}\n\nconst DemoIdContext = React.createContext(undefined);\n\nexport function DemoId({\n id,\n children,\n}: {\n children: React.ReactNode;\n id?: string;\n}) {\n return {children};\n}\n\nexport function useDemoId() {\n return React.useContext(DemoIdContext);\n}\n", "path": "example/playground-demo.tsx", "target": "components/playground-demo.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/basic-editor-handler-demo.json b/apps/www/public/r/styles/default/basic-editor-handler-demo.json index af2a8dca49..c040f0b0f3 100644 --- a/apps/www/public/r/styles/default/basic-editor-handler-demo.json +++ b/apps/www/public/r/styles/default/basic-editor-handler-demo.json @@ -1,7 +1,7 @@ { "files": [ { - "content": "'use client';\n\nimport React, { useState } from 'react';\n\nimport type { Value } from '@udecode/plate-common';\n\nimport {\n type PlateContentProps,\n Plate,\n usePlateEditor,\n} from '@udecode/plate-common/react';\n\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '@/components/ui/accordion';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nconst editableProps: PlateContentProps = {\n autoFocus: false,\n placeholder: 'Type…',\n spellCheck: false,\n};\n\nconst value = [\n {\n children: [\n {\n text: 'This is editable plain text with react and history plugins, just like a textarea!',\n },\n ],\n type: 'p',\n },\n];\n\nexport default function BasicEditorHandlerDemo() {\n const [debugValue, setDebugValue] = useState(value);\n\n const localValue =\n typeof window !== 'undefined' && localStorage.getItem('editorContent');\n\n const editor = usePlateEditor({\n value: localValue ? JSON.parse(localValue) : value,\n });\n\n return (\n {\n localStorage.setItem('editorContent', JSON.stringify(value));\n setDebugValue(value);\n }}\n editor={editor}\n >\n \n \n \n\n \n \n Debug Value\n {JSON.stringify(debugValue)}\n \n \n \n );\n}\n", + "content": "'use client';\n\nimport React, { useState } from 'react';\n\nimport type { Value } from '@udecode/plate-common';\n\nimport { Plate, usePlateEditor } from '@udecode/plate-common/react';\n\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '@/components/ui/accordion';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nconst value = [\n {\n children: [\n {\n text: 'This is editable plain text with react and history plugins, just like a textarea!',\n },\n ],\n type: 'p',\n },\n];\n\nexport default function BasicEditorHandlerDemo() {\n const [debugValue, setDebugValue] = useState(value);\n\n const localValue =\n typeof window !== 'undefined' && localStorage.getItem('editorContent');\n\n const editor = usePlateEditor({\n value: localValue ? JSON.parse(localValue) : value,\n });\n\n return (\n {\n localStorage.setItem('editorContent', JSON.stringify(value));\n setDebugValue(value);\n }}\n editor={editor}\n >\n \n \n \n\n \n \n Debug Value\n {JSON.stringify(debugValue)}\n \n \n \n );\n}\n", "path": "example/basic-editor-handler-demo.tsx", "target": "components/basic-editor-handler-demo.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/basic-editor-value-demo.json b/apps/www/public/r/styles/default/basic-editor-value-demo.json index 4e1747ca90..092ddd3690 100644 --- a/apps/www/public/r/styles/default/basic-editor-value-demo.json +++ b/apps/www/public/r/styles/default/basic-editor-value-demo.json @@ -1,7 +1,7 @@ { "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport {\n type PlateContentProps,\n Plate,\n usePlateEditor,\n} from '@udecode/plate-common/react';\n\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nconst editableProps: PlateContentProps = {\n autoFocus: false,\n placeholder: 'Type…',\n spellCheck: false,\n};\n\nconst value = [\n {\n children: [\n {\n text: 'This is editable plain text with react and history plugins, just like a