= props => {
+ const { value, maxLimit, minLimit, setHasLimitError } = props;
+ const [count, setCount] = useState(0);
+
+ useEffect(() => {
+ const temp = setTimeout(() => {
+ const _count = calcLengthOfHTML(value.toString());
+ setCount(_count);
+ setHasLimitError &&
+ setHasLimitError(
+ maxLimit ? _count > maxLimit : _count < minLimit!,
+ );
+ }, 1000);
+ return () => {
+ clearTimeout(temp);
+ };
+ }, [maxLimit, minLimit, setHasLimitError, value]);
+
+ return (
+
+ {count} / {maxLimit || minLimit}
+
+ );
+};
+
+const CounterContainer = styled(SublineBold)`
+ position: absolute;
+ bottom: 10px;
+ right: 10px;
+ background-color: ${neutralColors.gray[300]};
+ border-radius: 64px;
+ padding: 6px 10px;
+ color: ${neutralColors.gray[700]};
+ opacity: 0.8;
+`;
+
+export default RichTextCounter;
diff --git a/src/components/rich-text-lexical/RichTextLexicalEditor.tsx b/src/components/rich-text-lexical/RichTextLexicalEditor.tsx
new file mode 100644
index 0000000000..69f47e595e
--- /dev/null
+++ b/src/components/rich-text-lexical/RichTextLexicalEditor.tsx
@@ -0,0 +1,286 @@
+import { LexicalComposer } from '@lexical/react/LexicalComposer';
+import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import {
+ $createParagraphNode,
+ $createTextNode,
+ $getRoot,
+ $isTextNode,
+ DOMConversionMap,
+ TextNode,
+ EditorState,
+ LexicalNode,
+ ParagraphNode,
+ // LexicalEditor,
+} from 'lexical';
+import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
+import { useEffect, useState } from 'react';
+import { GLink, semanticColors } from '@giveth/ui-design-system';
+import styled from 'styled-components';
+import { FieldError, Merge, FieldErrorsImpl } from 'react-hook-form';
+import { useSettings } from './context/SettingsContext';
+import Editor from './Editor';
+import { SharedHistoryContext } from './context/SharedHistoryContext';
+import { TableContext } from './plugins/TablePlugin';
+import { ToolbarContext } from './context/ToolbarContext';
+import { FlashMessageContext } from './context/FlashMessageContext';
+import PlaygroundNodes from './nodes/PlaygroundNodes';
+import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme';
+import { parseAllowedColor } from './ui/ColorPicker';
+import { parseAllowedFontSize } from './plugins/ToolbarPlugin/fontSize';
+import TypingPerfPlugin from './plugins/TypingPerfPlugin';
+import { EditorShell } from '@/components/rich-text-lexical/mainStyles';
+import RichTextCounter from '@/components/rich-text-lexical/RichTextCounter';
+
+function getExtraStyles(element: HTMLElement): string {
+ // Parse styles from pasted input, but only if they match exactly the
+ // sort of styles that would be produced by exportDOM
+ let extraStyles = '';
+ const fontSize = parseAllowedFontSize(element.style.fontSize);
+ const backgroundColor = parseAllowedColor(element.style.backgroundColor);
+ const color = parseAllowedColor(element.style.color);
+ if (fontSize !== '' && fontSize !== '15px') {
+ extraStyles += `font-size: ${fontSize};`;
+ }
+ if (backgroundColor !== '' && backgroundColor !== 'rgb(255, 255, 255)') {
+ extraStyles += `background-color: ${backgroundColor};`;
+ }
+ if (color !== '' && color !== 'rgb(0, 0, 0)') {
+ extraStyles += `color: ${color};`;
+ }
+ return extraStyles;
+}
+
+function buildImportMap(): DOMConversionMap {
+ const importMap: DOMConversionMap = {};
+
+ // Wrap all TextNode importers with a function that also imports
+ // the custom styles implemented by the playground
+ for (const [tag, fn] of Object.entries(TextNode.importDOM() || {})) {
+ importMap[tag] = importNode => {
+ const importer = fn(importNode);
+ if (!importer) {
+ return null;
+ }
+ return {
+ ...importer,
+ conversion: element => {
+ const output = importer.conversion(element);
+ if (
+ output === null ||
+ output.forChild === undefined ||
+ output.after !== undefined ||
+ output.node !== null
+ ) {
+ return output;
+ }
+ const extraStyles = getExtraStyles(element);
+ if (extraStyles) {
+ const { forChild } = output;
+ return {
+ ...output,
+ forChild: (child, parent) => {
+ const textNode = forChild(child, parent);
+ if ($isTextNode(textNode)) {
+ textNode.setStyle(
+ textNode.getStyle() + extraStyles,
+ );
+ }
+ return textNode;
+ },
+ };
+ }
+ return output;
+ },
+ };
+ };
+ }
+
+ return importMap;
+}
+
+// Component to handle onChange events
+// End here is what data is being saved
+function OnChangeHandler({ onChange }: { onChange?: (html: string) => void }) {
+ const [editor] = useLexicalComposerContext();
+
+ const handleChange = (editorState: EditorState) => {
+ if (onChange) {
+ editorState.read(() => {
+ const htmlString = $generateHtmlFromNodes(editor);
+ onChange(htmlString);
+ });
+ }
+ };
+
+ return onChange ? : null;
+}
+
+// End here is what data is being loaded
+function parseHtmlToLexicalNodes(editor: any, html: string) {
+ // Clean up problematic input
+ const cleanedHtml = html
+ .replace(/""/g, '"')
+ .replace(/]*>]*>([^<]*)<\/span><\/p>/g, '$1
')
+ // .replace(/style="[^"]*white-space:\s*pre-wrap[^"]*"/g, '')
+ // Unwrap image-uploading spans that contain images (keep the image)
+ .replace(
+ /]*class=["']image-uploading["'][^>]*>\s*(
]+>)\s*<\/span>/gi,
+ '$1
',
+ )
+ // Remove image-uploading spans with text/br content (no images)
+ .replace(
+ /]*class=["']image-uploading["'][^>]*>(?!
)*<\/span>/gi,
+ '',
+ )
+ // Unwrap other spans containing only images
+ .replace(/]*>\s*(
]+>)\s*<\/span>/gi, '$1
');
+
+ const parser = new DOMParser();
+ const dom = parser.parseFromString(cleanedHtml, 'text/html');
+ const nodes = $generateNodesFromDOM(editor, dom);
+
+ // 🛠 Fix orphan text nodes being directly at root level
+ const fixedNodes: (LexicalNode | ParagraphNode)[] = [];
+ let buffer: LexicalNode[] = [];
+
+ const flushBuffer = () => {
+ if (buffer.length > 0) {
+ const p = $createParagraphNode();
+ buffer.forEach(n => p.append(n));
+ fixedNodes.push(p);
+ buffer = [];
+ }
+ };
+
+ nodes.forEach(node => {
+ const type = node.getType?.();
+ if (type === 'text' || type === 'linebreak' || type === 'link') {
+ buffer.push(node);
+ } else {
+ flushBuffer();
+ fixedNodes.push(node);
+ }
+ });
+
+ flushBuffer();
+ return fixedNodes;
+}
+
+// Function to create initial editor state from HTML or text
+function createInitialEditorState(content?: string) {
+ return (editor: any) => {
+ const root = $getRoot();
+ if (!content || root.getFirstChild() !== null) return;
+
+ try {
+ const fixedNodes = parseHtmlToLexicalNodes(editor, content);
+
+ root.clear();
+ root.append(...fixedNodes);
+ } catch (err) {
+ console.error('Error parsing HTML content:', err);
+ // Fallback to plain text paragraph
+ const paragraph = $createParagraphNode();
+ paragraph.append($createTextNode(content || ''));
+ root.append(paragraph);
+ }
+ };
+}
+
+function EditorInitializer({ html }: { html?: string }) {
+ const [editor] = useLexicalComposerContext();
+ const [isInitialized, setIsInitialized] = useState(false);
+
+ useEffect(() => {
+ // Only run once on initial load, not on every content change
+ if (!html || isInitialized) return;
+
+ editor.update(() => {
+ const root = $getRoot();
+ root.clear();
+
+ const fixedNodes = parseHtmlToLexicalNodes(editor, html);
+
+ root.clear();
+ root.append(...fixedNodes);
+ });
+
+ setIsInitialized(true);
+ }, [editor, html, isInitialized]);
+
+ return null;
+}
+
+export default function RichTextLexicalEditor({
+ initialValue,
+ onChange,
+ projectId,
+ maxLength,
+ setHasLimitError,
+ error,
+}: {
+ initialValue?: string;
+ onChange?: (html: string) => void;
+ projectId?: string;
+ maxLength?: number;
+ setHasLimitError?: (hasLimitError: boolean) => void;
+ error?: string | FieldError | Merge>;
+} = {}) {
+ const [currentValue, setCurrentValue] = useState(initialValue || '');
+
+ const {
+ settings: { isCollab, measureTypingPerf },
+ } = useSettings();
+
+ // Wrapper to track current value for the counter
+ const handleChange = (html: string) => {
+ setCurrentValue(html);
+ onChange?.(html);
+ };
+
+ const initialConfig = {
+ editorState: isCollab ? null : createInitialEditorState(initialValue),
+ html: { import: buildImportMap() },
+ namespace: 'Playground',
+ nodes: [...PlaygroundNodes],
+ onError: (error: Error) => {
+ throw error;
+ },
+ theme: PlaygroundEditorTheme,
+ };
+
+ return (
+
+
+
+
+
+
+
+ {maxLength && (
+
+ )}
+
+ {isCollab ? (
+
+ ) : null}
+
+ {measureTypingPerf ? : null}
+ {error && {error as string}}
+
+
+
+
+
+ );
+}
+
+const Error = styled(GLink)`
+ color: ${semanticColors.punch[500]};
+ margin-top: 4px;
+`;
diff --git a/src/components/rich-text-lexical/RichTextLexicalViewer.tsx b/src/components/rich-text-lexical/RichTextLexicalViewer.tsx
new file mode 100644
index 0000000000..2d17af3917
--- /dev/null
+++ b/src/components/rich-text-lexical/RichTextLexicalViewer.tsx
@@ -0,0 +1,258 @@
+import { LexicalComposer } from '@lexical/react/LexicalComposer';
+import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
+import { ContentEditable } from '@lexical/react/LexicalContentEditable';
+import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import {
+ $createParagraphNode,
+ $createTextNode,
+ $getRoot,
+ $isTextNode,
+ DOMConversionMap,
+ TextNode,
+ LexicalNode,
+ ParagraphNode,
+ LexicalEditor,
+} from 'lexical';
+import { $generateNodesFromDOM } from '@lexical/html';
+import { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import PlaygroundNodes from './nodes/PlaygroundNodes';
+import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme';
+import { parseAllowedColor } from './ui/ColorPicker';
+import { parseAllowedFontSize } from './plugins/ToolbarPlugin/fontSize';
+import { EditorShell } from '@/components/rich-text-lexical/mainStyles';
+
+function getExtraStyles(element: HTMLElement): string {
+ let extraStyles = '';
+ const fontSize = parseAllowedFontSize(element.style.fontSize);
+ const backgroundColor = parseAllowedColor(element.style.backgroundColor);
+ const color = parseAllowedColor(element.style.color);
+ if (fontSize !== '' && fontSize !== '15px') {
+ extraStyles += `font-size: ${fontSize};`;
+ }
+ if (backgroundColor !== '' && backgroundColor !== 'rgb(255, 255, 255)') {
+ extraStyles += `background-color: ${backgroundColor};`;
+ }
+ if (color !== '' && color !== 'rgb(0, 0, 0)') {
+ extraStyles += `color: ${color};`;
+ }
+ return extraStyles;
+}
+
+function buildImportMap(): DOMConversionMap {
+ const importMap: DOMConversionMap = {};
+
+ for (const [tag, fn] of Object.entries(TextNode.importDOM() || {})) {
+ importMap[tag] = importNode => {
+ const importer = fn(importNode);
+ if (!importer) {
+ return null;
+ }
+ return {
+ ...importer,
+ conversion: element => {
+ const output = importer.conversion(element);
+ if (
+ output === null ||
+ output.forChild === undefined ||
+ output.after !== undefined ||
+ output.node !== null
+ ) {
+ return output;
+ }
+ const extraStyles = getExtraStyles(element);
+ if (extraStyles) {
+ const { forChild } = output;
+ return {
+ ...output,
+ forChild: (child, parent) => {
+ const textNode = forChild(child, parent);
+ if ($isTextNode(textNode)) {
+ textNode.setStyle(
+ textNode.getStyle() + extraStyles,
+ );
+ }
+ return textNode;
+ },
+ };
+ }
+ return output;
+ },
+ };
+ };
+ }
+
+ return importMap;
+}
+
+function parseHtmlToLexicalNodes(editor: LexicalEditor, html: string) {
+ const cleanedHtml = html
+ .replace(/""/g, '"')
+ .replace(/]*>]*>([^<]*)<\/span><\/p>/g, '$1
')
+ .replace(
+ /]*class=["']image-uploading["'][^>]*>\s*(
]+>)\s*<\/span>/gi,
+ '$1
',
+ )
+ .replace(
+ /]*class=["']image-uploading["'][^>]*>(?!
)*<\/span>/gi,
+ '',
+ )
+ .replace(/]*>\s*(
]+>)\s*<\/span>/gi, '$1
');
+
+ const parser = new DOMParser();
+ const dom = parser.parseFromString(cleanedHtml, 'text/html');
+ const nodes = $generateNodesFromDOM(editor, dom);
+
+ const fixedNodes: (LexicalNode | ParagraphNode)[] = [];
+ let buffer: LexicalNode[] = [];
+
+ const flushBuffer = () => {
+ if (buffer.length > 0) {
+ const p = $createParagraphNode();
+ buffer.forEach(n => p.append(n));
+ fixedNodes.push(p);
+ buffer = [];
+ }
+ };
+
+ nodes.forEach(node => {
+ const type = node.getType?.();
+ if (type === 'text' || type === 'linebreak' || type === 'link') {
+ buffer.push(node);
+ } else {
+ flushBuffer();
+ fixedNodes.push(node);
+ }
+ });
+
+ flushBuffer();
+ return fixedNodes;
+}
+
+function ContentInitializer({ html }: { html?: string }) {
+ const [editor] = useLexicalComposerContext();
+ const [initializedHtml, setInitializedHtml] = useState<
+ string | undefined
+ >();
+
+ useEffect(() => {
+ if (!html || initializedHtml === html) return;
+
+ editor.update(() => {
+ const root = $getRoot();
+ root.clear();
+
+ try {
+ const fixedNodes = parseHtmlToLexicalNodes(editor, html);
+ root.append(...fixedNodes);
+ } catch (err) {
+ console.error('Error parsing HTML content:', err);
+ const paragraph = $createParagraphNode();
+ paragraph.append($createTextNode(html || ''));
+ root.append(paragraph);
+ }
+ });
+
+ setInitializedHtml(html);
+ }, [editor, html, initializedHtml]);
+
+ return null;
+}
+
+export default function RichTextLexicalViewer({
+ content,
+}: {
+ content?: string;
+}) {
+ if (!content) {
+ return null;
+ }
+
+ const initialConfig = {
+ namespace: 'RichTextViewer',
+ nodes: [...PlaygroundNodes],
+ editable: false, // Read-only mode
+ onError: (error: Error) => {
+ console.error('Lexical viewer error:', error);
+ },
+ theme: PlaygroundEditorTheme,
+ html: { import: buildImportMap() },
+ };
+
+ return (
+
+
+
+
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+
+
+
+
+ );
+}
+
+const ViewerWrapper = styled.div`
+ .editor-shell {
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ }
+
+ /* Viewer-only code styling (project description, etc.) */
+ .editor-shell.viewer-mode {
+ .PlaygroundEditorTheme__textCode {
+ background-color: rgb(240, 242, 245);
+ padding: 1px 0.25rem;
+ border-radius: 4px;
+ font-family: Menlo, Consolas, Monaco, monospace;
+ font-size: 94%;
+ }
+
+ .PlaygroundEditorTheme__code {
+ background-color: rgb(240, 242, 245);
+ font-family: Menlo, Consolas, Monaco, monospace;
+ display: block;
+ padding: 8px 8px 8px 52px;
+ line-height: 1.53;
+ font-size: 13px;
+ margin: 8px 0;
+ overflow-x: auto;
+ position: relative;
+ tab-size: 2;
+ border-radius: 8px;
+ }
+
+ .PlaygroundEditorTheme__code:before {
+ content: attr(data-gutter);
+ position: absolute;
+ background-color: #eee;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ border-right: 1px solid #ccc;
+ padding: 8px;
+ color: #777;
+ white-space: pre-wrap;
+ text-align: right;
+ min-width: 25px;
+ }
+ }
+`;
+
+const StyledContentEditable = styled(ContentEditable)`
+ outline: none;
+ border: none;
+ padding: 0;
+ min-height: auto;
+
+ /* Ensure links are clickable in view mode */
+ a {
+ pointer-events: auto;
+ }
+`;
diff --git a/src/components/rich-text-lexical/appSettings.ts b/src/components/rich-text-lexical/appSettings.ts
new file mode 100644
index 0000000000..6f9cd2501c
--- /dev/null
+++ b/src/components/rich-text-lexical/appSettings.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+export const DEFAULT_SETTINGS = {
+ disableBeforeInput: false,
+ emptyEditor: false,
+ hasLinkAttributes: false,
+ isAutocomplete: false,
+ isCharLimit: false,
+ isCharLimitUtf8: false,
+ isCodeHighlighted: true,
+ isCodeShiki: false,
+ isCollab: false,
+ isMaxLength: false,
+ isRichText: true,
+ listStrictIndent: false,
+ measureTypingPerf: false,
+ selectionAlwaysOnDisplay: false,
+ shouldAllowHighlightingWithBrackets: false,
+ shouldPreserveNewLinesInMarkdown: false,
+ shouldUseLexicalContextMenu: false,
+ showNestedEditorTreeView: false,
+ showTableOfContents: false,
+ showTreeView: false,
+ tableCellBackgroundColor: true,
+ tableCellMerge: true,
+ tableHorizontalScroll: true,
+} as const;
+
+// These are mutated in setupEnv
+export const INITIAL_SETTINGS: Record = {
+ ...DEFAULT_SETTINGS,
+};
+
+export type SettingName = keyof typeof DEFAULT_SETTINGS;
+
+export type Settings = typeof INITIAL_SETTINGS;
diff --git a/src/components/rich-text-lexical/collaboration.ts b/src/components/rich-text-lexical/collaboration.ts
new file mode 100644
index 0000000000..c1ee429b86
--- /dev/null
+++ b/src/components/rich-text-lexical/collaboration.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { Provider } from '@lexical/yjs';
+import { WebsocketProvider } from 'y-websocket';
+import { Doc } from 'yjs';
+
+const url = new URL(window.location.href);
+const params = new URLSearchParams(url.search);
+const WEBSOCKET_ENDPOINT =
+ params.get('collabEndpoint') || 'ws://localhost:1234';
+const WEBSOCKET_SLUG = 'playground';
+const WEBSOCKET_ID = params.get('collabId') || '0';
+
+// parent dom -> child doc
+export function createWebsocketProvider(
+ id: string,
+ yjsDocMap: Map,
+): Provider {
+ let doc = yjsDocMap.get(id);
+
+ if (doc === undefined) {
+ doc = new Doc();
+ yjsDocMap.set(id, doc);
+ } else {
+ doc.load();
+ }
+
+ // @ts-expect-error
+ return new WebsocketProvider(
+ WEBSOCKET_ENDPOINT,
+ WEBSOCKET_SLUG + '/' + WEBSOCKET_ID + '/' + id,
+ doc,
+ {
+ connect: false,
+ },
+ );
+}
diff --git a/src/components/rich-text-lexical/commenting/index.ts b/src/components/rich-text-lexical/commenting/index.ts
new file mode 100644
index 0000000000..2f1bed0215
--- /dev/null
+++ b/src/components/rich-text-lexical/commenting/index.ts
@@ -0,0 +1,477 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { Provider, TOGGLE_CONNECT_COMMAND } from '@lexical/yjs';
+import { COMMAND_PRIORITY_LOW } from 'lexical';
+import { useEffect, useState } from 'react';
+import {
+ Array as YArray,
+ Map as YMap,
+ Transaction,
+ YArrayEvent,
+ YEvent,
+} from 'yjs';
+import type { LexicalEditor } from 'lexical';
+
+export type Comment = {
+ author: string;
+ content: string;
+ deleted: boolean;
+ id: string;
+ timeStamp: number;
+ type: 'comment';
+};
+
+export type Thread = {
+ comments: Array;
+ id: string;
+ quote: string;
+ type: 'thread';
+};
+
+export type Comments = Array;
+
+function createUID(): string {
+ return Math.random()
+ .toString(36)
+ .replace(/[^a-z]+/g, '')
+ .substring(0, 5);
+}
+
+export function createComment(
+ content: string,
+ author: string,
+ id?: string,
+ timeStamp?: number,
+ deleted?: boolean,
+): Comment {
+ return {
+ author,
+ content,
+ deleted: deleted === undefined ? false : deleted,
+ id: id === undefined ? createUID() : id,
+ timeStamp:
+ timeStamp === undefined
+ ? performance.timeOrigin + performance.now()
+ : timeStamp,
+ type: 'comment',
+ };
+}
+
+export function createThread(
+ quote: string,
+ comments: Array,
+ id?: string,
+): Thread {
+ return {
+ comments,
+ id: id === undefined ? createUID() : id,
+ quote,
+ type: 'thread',
+ };
+}
+
+function cloneThread(thread: Thread): Thread {
+ return {
+ comments: Array.from(thread.comments),
+ id: thread.id,
+ quote: thread.quote,
+ type: 'thread',
+ };
+}
+
+function markDeleted(comment: Comment): Comment {
+ return {
+ author: comment.author,
+ content: '[Deleted Comment]',
+ deleted: true,
+ id: comment.id,
+ timeStamp: comment.timeStamp,
+ type: 'comment',
+ };
+}
+
+function triggerOnChange(commentStore: CommentStore): void {
+ const listeners = commentStore._changeListeners;
+ for (const listener of listeners) {
+ listener();
+ }
+}
+
+export class CommentStore {
+ _editor: LexicalEditor;
+ _comments: Comments;
+ _changeListeners: Set<() => void>;
+ _collabProvider: null | Provider;
+
+ constructor(editor: LexicalEditor) {
+ this._comments = [];
+ this._editor = editor;
+ this._collabProvider = null;
+ this._changeListeners = new Set();
+ }
+
+ isCollaborative(): boolean {
+ return this._collabProvider !== null;
+ }
+
+ getComments(): Comments {
+ return this._comments;
+ }
+
+ addComment(
+ commentOrThread: Comment | Thread,
+ thread?: Thread,
+ offset?: number,
+ ): void {
+ const nextComments = Array.from(this._comments);
+ // The YJS types explicitly use `any` as well.
+ const sharedCommentsArray: YArray | null =
+ this._getCollabComments();
+
+ if (thread !== undefined && commentOrThread.type === 'comment') {
+ for (let i = 0; i < nextComments.length; i++) {
+ const comment = nextComments[i];
+ if (comment.type === 'thread' && comment.id === thread.id) {
+ const newThread = cloneThread(comment);
+ nextComments.splice(i, 1, newThread);
+ const insertOffset =
+ offset !== undefined
+ ? offset
+ : newThread.comments.length;
+ if (
+ this.isCollaborative() &&
+ sharedCommentsArray !== null
+ ) {
+ const parentSharedArray = sharedCommentsArray
+ .get(i)
+ .get('comments');
+ this._withRemoteTransaction(() => {
+ const sharedMap =
+ this._createCollabSharedMap(commentOrThread);
+ parentSharedArray.insert(insertOffset, [sharedMap]);
+ });
+ }
+ newThread.comments.splice(insertOffset, 0, commentOrThread);
+ break;
+ }
+ }
+ } else {
+ const insertOffset =
+ offset !== undefined ? offset : nextComments.length;
+ if (this.isCollaborative() && sharedCommentsArray !== null) {
+ this._withRemoteTransaction(() => {
+ const sharedMap =
+ this._createCollabSharedMap(commentOrThread);
+ sharedCommentsArray.insert(insertOffset, [sharedMap]);
+ });
+ }
+ nextComments.splice(insertOffset, 0, commentOrThread);
+ }
+ this._comments = nextComments;
+ triggerOnChange(this);
+ }
+
+ deleteCommentOrThread(
+ commentOrThread: Comment | Thread,
+ thread?: Thread,
+ ): { markedComment: Comment; index: number } | null {
+ const nextComments = Array.from(this._comments);
+ // The YJS types explicitly use `any` as well.
+ const sharedCommentsArray: YArray | null =
+ this._getCollabComments();
+ let commentIndex: number | null = null;
+
+ if (thread !== undefined) {
+ for (let i = 0; i < nextComments.length; i++) {
+ const nextComment = nextComments[i];
+ if (
+ nextComment.type === 'thread' &&
+ nextComment.id === thread.id
+ ) {
+ const newThread = cloneThread(nextComment);
+ nextComments.splice(i, 1, newThread);
+ const threadComments = newThread.comments;
+ commentIndex = threadComments.indexOf(
+ commentOrThread as Comment,
+ );
+ if (
+ this.isCollaborative() &&
+ sharedCommentsArray !== null
+ ) {
+ const parentSharedArray = sharedCommentsArray
+ .get(i)
+ .get('comments');
+ this._withRemoteTransaction(() => {
+ parentSharedArray.delete(commentIndex);
+ });
+ }
+ threadComments.splice(commentIndex, 1);
+ break;
+ }
+ }
+ } else {
+ commentIndex = nextComments.indexOf(commentOrThread);
+ if (this.isCollaborative() && sharedCommentsArray !== null) {
+ this._withRemoteTransaction(() => {
+ sharedCommentsArray.delete(commentIndex as number);
+ });
+ }
+ nextComments.splice(commentIndex, 1);
+ }
+ this._comments = nextComments;
+ triggerOnChange(this);
+
+ if (commentOrThread.type === 'comment') {
+ return {
+ index: commentIndex as number,
+ markedComment: markDeleted(commentOrThread),
+ };
+ }
+
+ return null;
+ }
+
+ registerOnChange(onChange: () => void): () => void {
+ const changeListeners = this._changeListeners;
+ changeListeners.add(onChange);
+ return () => {
+ changeListeners.delete(onChange);
+ };
+ }
+
+ _withRemoteTransaction(fn: () => void): void {
+ const provider = this._collabProvider;
+ if (provider !== null) {
+ // @ts-expect-error doc does exist
+ const doc = provider.doc;
+ doc.transact(fn, this);
+ }
+ }
+
+ _withLocalTransaction(fn: () => void): void {
+ const collabProvider = this._collabProvider;
+ try {
+ this._collabProvider = null;
+ fn();
+ } finally {
+ this._collabProvider = collabProvider;
+ }
+ }
+
+ _getCollabComments(): null | YArray {
+ const provider = this._collabProvider;
+ if (provider !== null) {
+ // @ts-expect-error doc does exist
+ const doc = provider.doc;
+ return doc.get('comments', YArray) as YArray;
+ }
+ return null;
+ }
+
+ _createCollabSharedMap(commentOrThread: Comment | Thread): YMap {
+ const sharedMap = new YMap();
+ const type = commentOrThread.type;
+ const id = commentOrThread.id;
+ sharedMap.set('type', type);
+ sharedMap.set('id', id);
+ if (type === 'comment') {
+ sharedMap.set('author', commentOrThread.author);
+ sharedMap.set('content', commentOrThread.content);
+ sharedMap.set('deleted', commentOrThread.deleted);
+ sharedMap.set('timeStamp', commentOrThread.timeStamp);
+ } else {
+ sharedMap.set('quote', commentOrThread.quote);
+ const commentsArray = new YArray();
+ commentOrThread.comments.forEach((comment, i) => {
+ const sharedChildComment = this._createCollabSharedMap(comment);
+ commentsArray.insert(i, [sharedChildComment]);
+ });
+ sharedMap.set('comments', commentsArray);
+ }
+ return sharedMap;
+ }
+
+ registerCollaboration(provider: Provider): () => void {
+ this._collabProvider = provider;
+ const sharedCommentsArray = this._getCollabComments();
+
+ const connect = () => {
+ provider.connect();
+ };
+
+ const disconnect = () => {
+ try {
+ provider.disconnect();
+ } catch (e) {
+ // Do nothing
+ }
+ };
+
+ const unsubscribe = this._editor.registerCommand(
+ TOGGLE_CONNECT_COMMAND,
+ payload => {
+ if (connect !== undefined && disconnect !== undefined) {
+ const shouldConnect = payload;
+
+ if (shouldConnect) {
+ // eslint-disable-next-line no-console
+ console.log('Comments connected!');
+ connect();
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('Comments disconnected!');
+ disconnect();
+ }
+ }
+
+ return false;
+ },
+ COMMAND_PRIORITY_LOW,
+ );
+
+ const onSharedCommentChanges = (
+ // The YJS types explicitly use `any` as well.
+ events: Array>,
+ transaction: Transaction,
+ ) => {
+ if (transaction.origin !== this) {
+ for (let i = 0; i < events.length; i++) {
+ const event = events[i];
+
+ if (event instanceof YArrayEvent) {
+ const target = event.target;
+ const deltas = event.delta;
+ let offset = 0;
+
+ for (let s = 0; s < deltas.length; s++) {
+ const delta = deltas[s];
+ const insert = delta.insert;
+ const retain = delta.retain;
+ const del = delta.delete;
+ const parent = target.parent;
+ const parentThread =
+ target === sharedCommentsArray
+ ? undefined
+ : parent instanceof YMap &&
+ (this._comments.find(
+ t => t.id === parent.get('id'),
+ ) as Thread | undefined);
+
+ if (Array.isArray(insert)) {
+ insert
+ .slice()
+ .reverse()
+ .forEach((map: YMap) => {
+ const id = map.get('id');
+ const type = map.get('type');
+
+ const commentOrThread =
+ type === 'thread'
+ ? createThread(
+ map.get('quote'),
+ map
+ .get('comments')
+ .toArray()
+ .map(
+ (
+ innerComment: Map<
+ string,
+ | string
+ | number
+ | boolean
+ >,
+ ) =>
+ createComment(
+ innerComment.get(
+ 'content',
+ ) as string,
+ innerComment.get(
+ 'author',
+ ) as string,
+ innerComment.get(
+ 'id',
+ ) as string,
+ innerComment.get(
+ 'timeStamp',
+ ) as number,
+ innerComment.get(
+ 'deleted',
+ ) as boolean,
+ ),
+ ),
+ id,
+ )
+ : createComment(
+ map.get('content'),
+ map.get('author'),
+ id,
+ map.get('timeStamp'),
+ map.get('deleted'),
+ );
+ this._withLocalTransaction(() => {
+ this.addComment(
+ commentOrThread,
+ parentThread as Thread,
+ offset,
+ );
+ });
+ });
+ } else if (typeof retain === 'number') {
+ offset += retain;
+ } else if (typeof del === 'number') {
+ for (let d = 0; d < del; d++) {
+ const commentOrThread =
+ parentThread === undefined ||
+ parentThread === false
+ ? this._comments[offset]
+ : parentThread.comments[offset];
+ this._withLocalTransaction(() => {
+ this.deleteCommentOrThread(
+ commentOrThread,
+ parentThread as Thread,
+ );
+ });
+ offset++;
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ if (sharedCommentsArray === null) {
+ return () => null;
+ }
+
+ sharedCommentsArray.observeDeep(onSharedCommentChanges);
+
+ connect();
+
+ return () => {
+ sharedCommentsArray.unobserveDeep(onSharedCommentChanges);
+ unsubscribe();
+ this._collabProvider = null;
+ };
+ }
+}
+
+export function useCommentStore(commentStore: CommentStore): Comments {
+ const [comments, setComments] = useState(
+ commentStore.getComments(),
+ );
+
+ useEffect(() => {
+ return commentStore.registerOnChange(() => {
+ setComments(commentStore.getComments());
+ });
+ }, [commentStore]);
+
+ return comments;
+}
diff --git a/src/components/rich-text-lexical/context/FlashMessageContext.tsx b/src/components/rich-text-lexical/context/FlashMessageContext.tsx
new file mode 100644
index 0000000000..f218254779
--- /dev/null
+++ b/src/components/rich-text-lexical/context/FlashMessageContext.tsx
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from 'react';
+
+import FlashMessage from '../ui/FlashMessage';
+import type { JSX } from 'react';
+
+export type ShowFlashMessage = (
+ message?: React.ReactNode,
+ duration?: number,
+) => void;
+
+interface FlashMessageProps {
+ message?: React.ReactNode;
+ duration?: number;
+}
+
+const Context = createContext(undefined);
+const INITIAL_STATE: FlashMessageProps = {};
+const DEFAULT_DURATION = 1000;
+
+export const FlashMessageContext = ({
+ children,
+}: {
+ children: ReactNode;
+}): JSX.Element => {
+ const [props, setProps] = useState(INITIAL_STATE);
+ const showFlashMessage = useCallback(
+ (message, duration) =>
+ setProps(message ? { duration, message } : INITIAL_STATE),
+ [],
+ );
+ useEffect(() => {
+ if (props.message) {
+ const timeoutId = setTimeout(
+ () => setProps(INITIAL_STATE),
+ props.duration ?? DEFAULT_DURATION,
+ );
+ return () => clearTimeout(timeoutId);
+ }
+ }, [props]);
+ return (
+
+ {children}
+ {props.message && {props.message}}
+
+ );
+};
+
+export const useFlashMessageContext = (): ShowFlashMessage => {
+ const ctx = useContext(Context);
+ if (!ctx) {
+ throw new Error('Missing FlashMessageContext');
+ }
+ return ctx;
+};
diff --git a/src/components/rich-text-lexical/context/SettingsContext.tsx b/src/components/rich-text-lexical/context/SettingsContext.tsx
new file mode 100644
index 0000000000..26fdb68332
--- /dev/null
+++ b/src/components/rich-text-lexical/context/SettingsContext.tsx
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import * as React from 'react';
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+} from 'react';
+
+import { DEFAULT_SETTINGS, INITIAL_SETTINGS } from '../appSettings';
+import type { JSX } from 'react';
+import type { SettingName } from '../appSettings';
+
+type SettingsContextShape = {
+ setOption: (name: SettingName, value: boolean) => void;
+ settings: Record;
+};
+
+const Context: React.Context = createContext({
+ setOption: (name: SettingName, value: boolean) => {
+ return;
+ },
+ settings: INITIAL_SETTINGS,
+});
+
+export const SettingsContext = ({
+ children,
+}: {
+ children: ReactNode;
+}): JSX.Element => {
+ const [settings, setSettings] = useState(INITIAL_SETTINGS);
+
+ const setOption = useCallback((setting: SettingName, value: boolean) => {
+ setSettings(options => ({
+ ...options,
+ [setting]: value,
+ }));
+ setURLParam(setting, value);
+ }, []);
+
+ const contextValue = useMemo(() => {
+ return { setOption, settings };
+ }, [setOption, settings]);
+
+ return {children};
+};
+
+export const useSettings = (): SettingsContextShape => {
+ return useContext(Context);
+};
+
+function setURLParam(param: SettingName, value: null | boolean) {
+ const url = new URL(window.location.href);
+ const params = new URLSearchParams(url.search);
+ if (value !== DEFAULT_SETTINGS[param]) {
+ params.set(param, String(value));
+ } else {
+ params.delete(param);
+ }
+ url.search = params.toString();
+ window.history.pushState(null, '', url.toString());
+}
diff --git a/src/components/rich-text-lexical/context/SharedHistoryContext.tsx b/src/components/rich-text-lexical/context/SharedHistoryContext.tsx
new file mode 100644
index 0000000000..3425633c92
--- /dev/null
+++ b/src/components/rich-text-lexical/context/SharedHistoryContext.tsx
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { createEmptyHistoryState } from '@lexical/react/LexicalHistoryPlugin';
+import * as React from 'react';
+import { createContext, ReactNode, useContext, useMemo } from 'react';
+import type { JSX } from 'react';
+import type { HistoryState } from '@lexical/react/LexicalHistoryPlugin';
+
+type ContextShape = {
+ historyState?: HistoryState;
+};
+
+const Context: React.Context = createContext({});
+
+export const SharedHistoryContext = ({
+ children,
+}: {
+ children: ReactNode;
+}): JSX.Element => {
+ const historyContext = useMemo(
+ () => ({ historyState: createEmptyHistoryState() }),
+ [],
+ );
+ return (
+ {children}
+ );
+};
+
+export const useSharedHistoryContext = (): ContextShape => {
+ return useContext(Context);
+};
diff --git a/src/components/rich-text-lexical/context/ToolbarContext.tsx b/src/components/rich-text-lexical/context/ToolbarContext.tsx
new file mode 100644
index 0000000000..e0815d8135
--- /dev/null
+++ b/src/components/rich-text-lexical/context/ToolbarContext.tsx
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { ElementFormatType } from 'lexical';
+import React, {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import type { JSX } from 'react';
+
+export const MIN_ALLOWED_FONT_SIZE = 8;
+export const MAX_ALLOWED_FONT_SIZE = 72;
+export const DEFAULT_FONT_SIZE = 15;
+
+const rootTypeToRootName = {
+ root: 'Root',
+ table: 'Table',
+};
+
+export const blockTypeToBlockName = {
+ bullet: 'Bulleted List',
+ check: 'Check List',
+ code: 'Code Block',
+ h1: 'Heading 1',
+ h2: 'Heading 2',
+ h3: 'Heading 3',
+ h4: 'Heading 4',
+ h5: 'Heading 5',
+ h6: 'Heading 6',
+ number: 'Numbered List',
+ paragraph: 'Normal',
+ quote: 'Quote',
+};
+
+//disable eslint sorting rule for quick reference to toolbar state
+const INITIAL_TOOLBAR_STATE = {
+ bgColor: '#fff',
+ blockType: 'paragraph' as keyof typeof blockTypeToBlockName,
+ canRedo: false,
+ canUndo: false,
+ codeLanguage: '',
+ codeTheme: '',
+ elementFormat: 'left' as ElementFormatType,
+ fontColor: '#000',
+ fontFamily: 'Arial',
+ // Current font size in px
+ fontSize: `${DEFAULT_FONT_SIZE}px`,
+ // Font size input value - for controlled input
+ fontSizeInputValue: `${DEFAULT_FONT_SIZE}`,
+ isBold: false,
+ isCode: false,
+ isHighlight: false,
+ isImageCaption: false,
+ isItalic: false,
+ isLink: false,
+ isRTL: false,
+ isStrikethrough: false,
+ isSubscript: false,
+ isSuperscript: false,
+ isUnderline: false,
+ isLowercase: false,
+ isUppercase: false,
+ isCapitalize: false,
+ rootType: 'root' as keyof typeof rootTypeToRootName,
+ listStartNumber: null as number | null,
+};
+
+type ToolbarState = typeof INITIAL_TOOLBAR_STATE;
+
+// Utility type to get keys and infer value types
+type ToolbarStateKey = keyof ToolbarState;
+type ToolbarStateValue = ToolbarState[Key];
+
+type ContextShape = {
+ toolbarState: ToolbarState;
+ updateToolbarState(
+ key: Key,
+ value: ToolbarStateValue,
+ ): void;
+};
+
+const Context = createContext(undefined);
+
+export const ToolbarContext = ({
+ children,
+}: {
+ children: ReactNode;
+}): JSX.Element => {
+ const [toolbarState, setToolbarState] = useState(INITIAL_TOOLBAR_STATE);
+ const selectionFontSize = toolbarState.fontSize;
+
+ const updateToolbarState = useCallback(
+ (
+ key: Key,
+ value: ToolbarStateValue,
+ ) => {
+ setToolbarState(prev => ({
+ ...prev,
+ [key]: value,
+ }));
+ },
+ [],
+ );
+
+ useEffect(() => {
+ updateToolbarState(
+ 'fontSizeInputValue',
+ selectionFontSize.slice(0, -2),
+ );
+ }, [selectionFontSize, updateToolbarState]);
+
+ const contextValue = useMemo(() => {
+ return {
+ toolbarState,
+ updateToolbarState,
+ };
+ }, [toolbarState, updateToolbarState]);
+
+ return {children};
+};
+
+export const useToolbarState = () => {
+ const context = useContext(Context);
+
+ if (context === undefined) {
+ throw new Error(
+ 'useToolbarState must be used within a ToolbarProvider',
+ );
+ }
+
+ return context;
+};
diff --git a/src/components/rich-text-lexical/hooks/useFlashMessage.tsx b/src/components/rich-text-lexical/hooks/useFlashMessage.tsx
new file mode 100644
index 0000000000..a8f4be87d0
--- /dev/null
+++ b/src/components/rich-text-lexical/hooks/useFlashMessage.tsx
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import {
+ type ShowFlashMessage,
+ useFlashMessageContext,
+} from '../context/FlashMessageContext';
+
+export default function useFlashMessage(): ShowFlashMessage {
+ return useFlashMessageContext();
+}
diff --git a/src/components/rich-text-lexical/hooks/useModal.tsx b/src/components/rich-text-lexical/hooks/useModal.tsx
new file mode 100644
index 0000000000..41ff7d5889
--- /dev/null
+++ b/src/components/rich-text-lexical/hooks/useModal.tsx
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useCallback, useMemo, useState } from 'react';
+import * as React from 'react';
+
+import Modal from '../ui/Modal';
+import type { JSX } from 'react';
+
+export default function useModal(): [
+ JSX.Element | null,
+ (title: string, showModal: (onClose: () => void) => JSX.Element) => void,
+] {
+ const [modalContent, setModalContent] = useState(null);
+
+ const onClose = useCallback(() => {
+ setModalContent(null);
+ }, []);
+
+ const modal = useMemo(() => {
+ if (modalContent === null) {
+ return null;
+ }
+ const { title, content, closeOnClickOutside } = modalContent;
+ return (
+
+ {content}
+
+ );
+ }, [modalContent, onClose]);
+
+ const showModal = useCallback(
+ (
+ title: string,
+ // eslint-disable-next-line no-shadow
+ getContent: (onClose: () => void) => JSX.Element,
+ closeOnClickOutside = false,
+ ) => {
+ setModalContent({
+ closeOnClickOutside,
+ content: getContent(onClose),
+ title,
+ });
+ },
+ [onClose],
+ );
+
+ return [modal, showModal];
+}
diff --git a/src/components/rich-text-lexical/hooks/useReport.ts b/src/components/rich-text-lexical/hooks/useReport.ts
new file mode 100644
index 0000000000..4a74b8e876
--- /dev/null
+++ b/src/components/rich-text-lexical/hooks/useReport.ts
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useCallback, useEffect, useRef } from 'react';
+
+const getElement = (): HTMLElement => {
+ let element = document.getElementById('report-container');
+
+ if (element === null) {
+ element = document.createElement('div');
+ element.id = 'report-container';
+ element.style.position = 'fixed';
+ element.style.top = '50%';
+ element.style.left = '50%';
+ element.style.fontSize = '32px';
+ element.style.transform = 'translate(-50%, -50px)';
+ element.style.padding = '20px';
+ element.style.background = 'rgba(240, 240, 240, 0.4)';
+ element.style.borderRadius = '20px';
+
+ if (document.body) {
+ document.body.appendChild(element);
+ }
+ }
+
+ return element;
+};
+
+export default function useReport(): (
+ arg0: string,
+) => ReturnType {
+ const timer = useRef | null>(null);
+ const cleanup = useCallback(() => {
+ if (timer.current !== null) {
+ clearTimeout(timer.current);
+ timer.current = null;
+ }
+
+ if (document.body) {
+ document.body.removeChild(getElement());
+ }
+ }, []);
+
+ useEffect(() => {
+ return cleanup;
+ }, [cleanup]);
+
+ return useCallback(
+ content => {
+ // eslint-disable-next-line no-console
+ console.log(content);
+ const element = getElement();
+ if (timer.current !== null) {
+ clearTimeout(timer.current);
+ }
+ element.textContent = content;
+ timer.current = setTimeout(cleanup, 1000);
+ return timer.current;
+ },
+ [cleanup],
+ );
+}
diff --git a/src/components/rich-text-lexical/images/cat-typing.gif b/src/components/rich-text-lexical/images/cat-typing.gif
new file mode 100644
index 0000000000..4f45feb30e
Binary files /dev/null and b/src/components/rich-text-lexical/images/cat-typing.gif differ
diff --git a/src/components/rich-text-lexical/images/emoji/1F600.png b/src/components/rich-text-lexical/images/emoji/1F600.png
new file mode 100644
index 0000000000..ba0e4feb8e
Binary files /dev/null and b/src/components/rich-text-lexical/images/emoji/1F600.png differ
diff --git a/src/components/rich-text-lexical/images/emoji/1F641.png b/src/components/rich-text-lexical/images/emoji/1F641.png
new file mode 100644
index 0000000000..0dd4e5be3f
Binary files /dev/null and b/src/components/rich-text-lexical/images/emoji/1F641.png differ
diff --git a/src/components/rich-text-lexical/images/emoji/1F642.png b/src/components/rich-text-lexical/images/emoji/1F642.png
new file mode 100644
index 0000000000..b37115fd50
Binary files /dev/null and b/src/components/rich-text-lexical/images/emoji/1F642.png differ
diff --git a/src/components/rich-text-lexical/images/emoji/2764.png b/src/components/rich-text-lexical/images/emoji/2764.png
new file mode 100644
index 0000000000..6dd2cd5684
Binary files /dev/null and b/src/components/rich-text-lexical/images/emoji/2764.png differ
diff --git a/src/components/rich-text-lexical/images/emoji/LICENSE.md b/src/components/rich-text-lexical/images/emoji/LICENSE.md
new file mode 100644
index 0000000000..87b04e9aff
--- /dev/null
+++ b/src/components/rich-text-lexical/images/emoji/LICENSE.md
@@ -0,0 +1,5 @@
+OpenMoji
+https://openmoji.org
+
+Licensed under Attribution-ShareAlike 4.0 International
+https://creativecommons.org/licenses/by-sa/4.0/
diff --git a/src/components/rich-text-lexical/images/icons/3-columns.svg b/src/components/rich-text-lexical/images/icons/3-columns.svg
new file mode 100644
index 0000000000..06496e2e0a
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/3-columns.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/rich-text-lexical/images/icons/LICENSE.md b/src/components/rich-text-lexical/images/icons/LICENSE.md
new file mode 100644
index 0000000000..ce74f6abee
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/LICENSE.md
@@ -0,0 +1,5 @@
+Bootstrap Icons
+https://icons.getbootstrap.com
+
+Licensed under MIT license
+https://github.com/twbs/icons/blob/main/LICENSE.md
diff --git a/src/components/rich-text-lexical/images/icons/add-sign.svg b/src/components/rich-text-lexical/images/icons/add-sign.svg
new file mode 100644
index 0000000000..1f0dd5b115
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/add-sign.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/rich-text-lexical/images/icons/arrow-clockwise.svg b/src/components/rich-text-lexical/images/icons/arrow-clockwise.svg
new file mode 100644
index 0000000000..80b3ad066e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/arrow-clockwise.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/arrow-counterclockwise.svg b/src/components/rich-text-lexical/images/icons/arrow-counterclockwise.svg
new file mode 100644
index 0000000000..46d3581d8e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/arrow-counterclockwise.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/bg-color.svg b/src/components/rich-text-lexical/images/icons/bg-color.svg
new file mode 100644
index 0000000000..ae08b2c1d8
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/bg-color.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/calendar.svg b/src/components/rich-text-lexical/images/icons/calendar.svg
new file mode 100644
index 0000000000..d32ebe7eab
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/calendar.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/camera.svg b/src/components/rich-text-lexical/images/icons/camera.svg
new file mode 100755
index 0000000000..968ebf4eb3
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/camera.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/card-checklist.svg b/src/components/rich-text-lexical/images/icons/card-checklist.svg
new file mode 100644
index 0000000000..f81734be43
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/card-checklist.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/caret-right-fill.svg b/src/components/rich-text-lexical/images/icons/caret-right-fill.svg
new file mode 100644
index 0000000000..04c258e6d1
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/caret-right-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/chat-left-text.svg b/src/components/rich-text-lexical/images/icons/chat-left-text.svg
new file mode 100644
index 0000000000..7c7acc2397
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/chat-left-text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/chat-right-dots.svg b/src/components/rich-text-lexical/images/icons/chat-right-dots.svg
new file mode 100644
index 0000000000..110925a122
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/chat-right-dots.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/chat-right-text.svg b/src/components/rich-text-lexical/images/icons/chat-right-text.svg
new file mode 100644
index 0000000000..08daa52bc7
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/chat-right-text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/chat-right.svg b/src/components/rich-text-lexical/images/icons/chat-right.svg
new file mode 100644
index 0000000000..d9c2b110e7
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/chat-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/chat-square-quote.svg b/src/components/rich-text-lexical/images/icons/chat-square-quote.svg
new file mode 100755
index 0000000000..5501848a57
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/chat-square-quote.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/chevron-down.svg b/src/components/rich-text-lexical/images/icons/chevron-down.svg
new file mode 100644
index 0000000000..ef1a6ba3b7
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/chevron-down.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/clipboard.svg b/src/components/rich-text-lexical/images/icons/clipboard.svg
new file mode 100755
index 0000000000..f09e1a1c9b
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/clipboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/close.svg b/src/components/rich-text-lexical/images/icons/close.svg
new file mode 100644
index 0000000000..4f5bb39382
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/code.svg b/src/components/rich-text-lexical/images/icons/code.svg
new file mode 100755
index 0000000000..c9070bf06e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/code.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/comments.svg b/src/components/rich-text-lexical/images/icons/comments.svg
new file mode 100644
index 0000000000..6a23ac5463
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/comments.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/copy.svg b/src/components/rich-text-lexical/images/icons/copy.svg
new file mode 100644
index 0000000000..e757cdfe5d
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/diagram-2.svg b/src/components/rich-text-lexical/images/icons/diagram-2.svg
new file mode 100644
index 0000000000..7b7b696d0e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/diagram-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/download.svg b/src/components/rich-text-lexical/images/icons/download.svg
new file mode 100755
index 0000000000..cd27d96c10
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/draggable-block-menu.svg b/src/components/rich-text-lexical/images/icons/draggable-block-menu.svg
new file mode 100644
index 0000000000..7086d2990a
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/draggable-block-menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/dropdown-more.svg b/src/components/rich-text-lexical/images/icons/dropdown-more.svg
new file mode 100644
index 0000000000..399ea8de5f
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/dropdown-more.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/figma.svg b/src/components/rich-text-lexical/images/icons/figma.svg
new file mode 100644
index 0000000000..fa319e12be
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/figma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/file-earmark-text.svg b/src/components/rich-text-lexical/images/icons/file-earmark-text.svg
new file mode 100644
index 0000000000..0d60c7957a
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/file-earmark-text.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/file-image.svg b/src/components/rich-text-lexical/images/icons/file-image.svg
new file mode 100644
index 0000000000..73a9ff15f1
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/file-image.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/filetype-gif.svg b/src/components/rich-text-lexical/images/icons/filetype-gif.svg
new file mode 100644
index 0000000000..12acb80f39
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/filetype-gif.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/font-color.svg b/src/components/rich-text-lexical/images/icons/font-color.svg
new file mode 100644
index 0000000000..1ac53f7ac5
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/font-color.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/font-family.svg b/src/components/rich-text-lexical/images/icons/font-family.svg
new file mode 100644
index 0000000000..a13f5ad1ef
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/font-family.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/gear.svg b/src/components/rich-text-lexical/images/icons/gear.svg
new file mode 100755
index 0000000000..ee6efa0441
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/gear.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/highlighter.svg b/src/components/rich-text-lexical/images/icons/highlighter.svg
new file mode 100644
index 0000000000..01d88ddeac
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/highlighter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/horizontal-rule.svg b/src/components/rich-text-lexical/images/icons/horizontal-rule.svg
new file mode 100644
index 0000000000..cb84970fb1
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/horizontal-rule.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/indent.svg b/src/components/rich-text-lexical/images/icons/indent.svg
new file mode 100644
index 0000000000..c9c5df7bf6
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/indent.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/journal-code.svg b/src/components/rich-text-lexical/images/icons/journal-code.svg
new file mode 100755
index 0000000000..9db6666a7c
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/journal-code.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/journal-text.svg b/src/components/rich-text-lexical/images/icons/journal-text.svg
new file mode 100755
index 0000000000..9defed2c3e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/journal-text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/justify.svg b/src/components/rich-text-lexical/images/icons/justify.svg
new file mode 100644
index 0000000000..6c5f8d0f7e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/justify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/link.svg b/src/components/rich-text-lexical/images/icons/link.svg
new file mode 100755
index 0000000000..bc38ff5d4b
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/list-ol.svg b/src/components/rich-text-lexical/images/icons/list-ol.svg
new file mode 100755
index 0000000000..ad288e8ea4
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/list-ol.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/list-ul.svg b/src/components/rich-text-lexical/images/icons/list-ul.svg
new file mode 100755
index 0000000000..6d7aae75d7
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/list-ul.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/lock-fill.svg b/src/components/rich-text-lexical/images/icons/lock-fill.svg
new file mode 100644
index 0000000000..466ca138f9
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/lock-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/lock.svg b/src/components/rich-text-lexical/images/icons/lock.svg
new file mode 100644
index 0000000000..3e19e71b51
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/markdown.svg b/src/components/rich-text-lexical/images/icons/markdown.svg
new file mode 100644
index 0000000000..310bff6d55
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/markdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/mic.svg b/src/components/rich-text-lexical/images/icons/mic.svg
new file mode 100644
index 0000000000..afdb58da9b
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/mic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/minus-sign.svg b/src/components/rich-text-lexical/images/icons/minus-sign.svg
new file mode 100644
index 0000000000..d7875aaed5
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/minus-sign.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/rich-text-lexical/images/icons/outdent.svg b/src/components/rich-text-lexical/images/icons/outdent.svg
new file mode 100644
index 0000000000..a98e0e1921
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/outdent.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/paint-bucket.svg b/src/components/rich-text-lexical/images/icons/paint-bucket.svg
new file mode 100644
index 0000000000..baa02d3b31
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/paint-bucket.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/palette.svg b/src/components/rich-text-lexical/images/icons/palette.svg
new file mode 100644
index 0000000000..338222ec61
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/palette.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/pencil-fill.svg b/src/components/rich-text-lexical/images/icons/pencil-fill.svg
new file mode 100755
index 0000000000..eb01fb2a41
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/pencil-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/plug-fill.svg b/src/components/rich-text-lexical/images/icons/plug-fill.svg
new file mode 100644
index 0000000000..3863ef8400
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/plug-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/plug.svg b/src/components/rich-text-lexical/images/icons/plug.svg
new file mode 100644
index 0000000000..de8d4c80b0
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/plug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/plus-slash-minus.svg b/src/components/rich-text-lexical/images/icons/plus-slash-minus.svg
new file mode 100644
index 0000000000..40ff781e5a
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/plus-slash-minus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/plus.svg b/src/components/rich-text-lexical/images/icons/plus.svg
new file mode 100644
index 0000000000..1a26928a16
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/prettier-error.svg b/src/components/rich-text-lexical/images/icons/prettier-error.svg
new file mode 100644
index 0000000000..8fc8450d03
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/prettier-error.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/prettier.svg b/src/components/rich-text-lexical/images/icons/prettier.svg
new file mode 100644
index 0000000000..b25a626c7f
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/prettier.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/scissors.svg b/src/components/rich-text-lexical/images/icons/scissors.svg
new file mode 100644
index 0000000000..80a76cdb57
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/scissors.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/send.svg b/src/components/rich-text-lexical/images/icons/send.svg
new file mode 100644
index 0000000000..04e9f29836
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/send.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/square-check.svg b/src/components/rich-text-lexical/images/icons/square-check.svg
new file mode 100644
index 0000000000..352ba61582
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/square-check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/sticky.svg b/src/components/rich-text-lexical/images/icons/sticky.svg
new file mode 100644
index 0000000000..2b14115cdb
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/sticky.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/success-alt.svg b/src/components/rich-text-lexical/images/icons/success-alt.svg
new file mode 100644
index 0000000000..c9d4ad9c23
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/success-alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/success.svg b/src/components/rich-text-lexical/images/icons/success.svg
new file mode 100644
index 0000000000..8e11879e07
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/success.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/table.svg b/src/components/rich-text-lexical/images/icons/table.svg
new file mode 100644
index 0000000000..e514555c7f
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/text-center.svg b/src/components/rich-text-lexical/images/icons/text-center.svg
new file mode 100644
index 0000000000..97ced49e63
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/text-center.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/text-left.svg b/src/components/rich-text-lexical/images/icons/text-left.svg
new file mode 100644
index 0000000000..5fe4cc4452
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/text-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/text-paragraph.svg b/src/components/rich-text-lexical/images/icons/text-paragraph.svg
new file mode 100755
index 0000000000..1b943ab44e
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/text-paragraph.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/text-right.svg b/src/components/rich-text-lexical/images/icons/text-right.svg
new file mode 100644
index 0000000000..de984517f9
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/text-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/trash.svg b/src/components/rich-text-lexical/images/icons/trash.svg
new file mode 100644
index 0000000000..75680bb7ad
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/trash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/trash3.svg b/src/components/rich-text-lexical/images/icons/trash3.svg
new file mode 100644
index 0000000000..5c38b387e6
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/trash3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-bold.svg b/src/components/rich-text-lexical/images/icons/type-bold.svg
new file mode 100755
index 0000000000..ec0dc2ec0d
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-bold.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-capitalize.svg b/src/components/rich-text-lexical/images/icons/type-capitalize.svg
new file mode 100644
index 0000000000..359fcd0707
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-capitalize.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/rich-text-lexical/images/icons/type-h1.svg b/src/components/rich-text-lexical/images/icons/type-h1.svg
new file mode 100755
index 0000000000..379da930d8
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-h1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-h2.svg b/src/components/rich-text-lexical/images/icons/type-h2.svg
new file mode 100755
index 0000000000..e724a0be39
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-h2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-h3.svg b/src/components/rich-text-lexical/images/icons/type-h3.svg
new file mode 100755
index 0000000000..02d4a06c5a
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-h3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-h4.svg b/src/components/rich-text-lexical/images/icons/type-h4.svg
new file mode 100755
index 0000000000..eb950c9ed4
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-h4.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-h5.svg b/src/components/rich-text-lexical/images/icons/type-h5.svg
new file mode 100755
index 0000000000..5d565639cf
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-h5.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-h6.svg b/src/components/rich-text-lexical/images/icons/type-h6.svg
new file mode 100755
index 0000000000..8274acacd5
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-h6.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-italic.svg b/src/components/rich-text-lexical/images/icons/type-italic.svg
new file mode 100755
index 0000000000..ac139f3cc9
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-italic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-lowercase.svg b/src/components/rich-text-lexical/images/icons/type-lowercase.svg
new file mode 100644
index 0000000000..5d097d7a57
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-lowercase.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-strikethrough.svg b/src/components/rich-text-lexical/images/icons/type-strikethrough.svg
new file mode 100755
index 0000000000..a0d7e17e2d
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-strikethrough.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-subscript.svg b/src/components/rich-text-lexical/images/icons/type-subscript.svg
new file mode 100644
index 0000000000..f6ebe4b6f6
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-subscript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-superscript.svg b/src/components/rich-text-lexical/images/icons/type-superscript.svg
new file mode 100644
index 0000000000..bed98f9d80
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-superscript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-underline.svg b/src/components/rich-text-lexical/images/icons/type-underline.svg
new file mode 100755
index 0000000000..d5c7046ee3
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-underline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/type-uppercase.svg b/src/components/rich-text-lexical/images/icons/type-uppercase.svg
new file mode 100644
index 0000000000..d0887b5d2f
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/type-uppercase.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/upload.svg b/src/components/rich-text-lexical/images/icons/upload.svg
new file mode 100644
index 0000000000..81328ddbca
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/upload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/user.svg b/src/components/rich-text-lexical/images/icons/user.svg
new file mode 100644
index 0000000000..823b72d1e6
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/vertical-bottom.svg b/src/components/rich-text-lexical/images/icons/vertical-bottom.svg
new file mode 100644
index 0000000000..edd2ece616
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/vertical-bottom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/vertical-middle.svg b/src/components/rich-text-lexical/images/icons/vertical-middle.svg
new file mode 100644
index 0000000000..7d06ffe606
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/vertical-middle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/vertical-top.svg b/src/components/rich-text-lexical/images/icons/vertical-top.svg
new file mode 100644
index 0000000000..1dc151d0b6
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/vertical-top.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/x.svg b/src/components/rich-text-lexical/images/icons/x.svg
new file mode 100644
index 0000000000..25e0001416
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/icons/youtube.svg b/src/components/rich-text-lexical/images/icons/youtube.svg
new file mode 100644
index 0000000000..e7fb9faabc
--- /dev/null
+++ b/src/components/rich-text-lexical/images/icons/youtube.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/image-broken.svg b/src/components/rich-text-lexical/images/image-broken.svg
new file mode 100644
index 0000000000..58e4aa9a85
--- /dev/null
+++ b/src/components/rich-text-lexical/images/image-broken.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/images/image/LICENSE.md b/src/components/rich-text-lexical/images/image/LICENSE.md
new file mode 100644
index 0000000000..f11a532cbf
--- /dev/null
+++ b/src/components/rich-text-lexical/images/image/LICENSE.md
@@ -0,0 +1,5 @@
+yellow-flower.jpg by Andrew Haimerl
+https://unsplash.com/photos/oxQHb8Yqt14
+
+Licensed under Unsplash License
+https://unsplash.com/license
diff --git a/src/components/rich-text-lexical/images/landscape.jpg b/src/components/rich-text-lexical/images/landscape.jpg
new file mode 100644
index 0000000000..a54a11a425
Binary files /dev/null and b/src/components/rich-text-lexical/images/landscape.jpg differ
diff --git a/src/components/rich-text-lexical/images/yellow-flower-small.jpg b/src/components/rich-text-lexical/images/yellow-flower-small.jpg
new file mode 100644
index 0000000000..93a6ad0e28
Binary files /dev/null and b/src/components/rich-text-lexical/images/yellow-flower-small.jpg differ
diff --git a/src/components/rich-text-lexical/images/yellow-flower.jpg b/src/components/rich-text-lexical/images/yellow-flower.jpg
new file mode 100644
index 0000000000..9f96c1e57d
Binary files /dev/null and b/src/components/rich-text-lexical/images/yellow-flower.jpg differ
diff --git a/src/components/rich-text-lexical/index.module.css b/src/components/rich-text-lexical/index.module.css
new file mode 100644
index 0000000000..23496eb7a0
--- /dev/null
+++ b/src/components/rich-text-lexical/index.module.css
@@ -0,0 +1,1591 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+@import 'https://fonts.googleapis.com/css?family=Reenie+Beanie';
+
+.editorWrapper {
+ margin: 0;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ border-width: 2px;
+ border-color: #ebecf2;
+ border-style: solid;
+ position: relative;
+}
+
+.editor-shell {
+ margin: 20px auto;
+ border-radius: 2px;
+ max-width: 1100px;
+ color: #000;
+ position: relative;
+ line-height: 1.7;
+ font-weight: 400;
+}
+
+.editor-shell .editor-container {
+ background: #fff;
+ position: relative;
+ display: block;
+ border-bottom-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+}
+
+.editor-shell .editor-container.tree-view {
+ border-radius: 0;
+}
+
+.editor-shell .editor-container.plain-text {
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+}
+
+.editor-scroller {
+ min-height: 150px;
+ max-width: 100%;
+ border: 0;
+ display: flex;
+ position: relative;
+ outline: 0;
+ z-index: 0;
+ resize: vertical;
+}
+
+.editor {
+ flex: auto;
+ max-width: 100%;
+ position: relative;
+ resize: vertical;
+ z-index: -1;
+}
+
+.test-recorder-output {
+ margin: 20px auto 20px auto;
+ width: 100%;
+}
+
+.pre {
+ line-height: 1.1;
+ background: #222;
+ color: #fff;
+ margin: 0;
+ padding: 10px;
+ font-size: 12px;
+ overflow: auto;
+ max-height: 400px;
+}
+
+.tree-view-output {
+ display: block;
+ background: #222;
+ color: #fff;
+ padding: 0;
+ font-size: 12px;
+ margin: 1px auto 10px auto;
+ position: relative;
+ overflow: hidden;
+ border-bottom-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+}
+
+.pre::-webkit-scrollbar {
+ background: transparent;
+ width: 10px;
+}
+
+.pre::-webkit-scrollbar-thumb {
+ background: #999;
+}
+
+.editor-dev-button {
+ position: relative;
+ display: block;
+ width: 40px;
+ height: 40px;
+ font-size: 12px;
+ border-radius: 20px;
+ border: none;
+ cursor: pointer;
+ outline: none;
+ box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.3);
+ background-color: #444;
+}
+
+.editor-dev-button::after {
+ content: '';
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ bottom: 10px;
+ left: 10px;
+ display: block;
+ background-size: contain;
+ filter: invert(1);
+}
+
+.editor-dev-button:hover {
+ background-color: #555;
+}
+
+.editor-dev-button.active {
+ background-color: rgb(233, 35, 35);
+}
+
+.test-recorder-toolbar {
+ display: flex;
+}
+
+.test-recorder-button {
+ position: relative;
+ display: block;
+ width: 32px;
+ height: 32px;
+ font-size: 10px;
+ padding: 6px 6px;
+ border-radius: 4px;
+ border: none;
+ cursor: pointer;
+ outline: none;
+ box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.4);
+ background-color: #222;
+ transition: box-shadow 50ms ease-out;
+}
+
+.test-recorder-button:active {
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.4);
+}
+
+.test-recorder-button+.test-recorder-button {
+ margin-left: 4px;
+}
+
+.test-recorder-button::after {
+ content: '';
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ bottom: 8px;
+ left: 8px;
+ display: block;
+ background-size: contain;
+ filter: invert(1);
+}
+
+#options-button {
+ position: fixed;
+ left: 20px;
+ bottom: 20px;
+}
+
+#test-recorder-button {
+ position: fixed;
+ left: 70px;
+ bottom: 20px;
+}
+
+#paste-log-button {
+ position: fixed;
+ left: 120px;
+ bottom: 20px;
+}
+
+#docs-button {
+ position: fixed;
+ left: 170px;
+ bottom: 20px;
+}
+
+#options-button::after {
+ background-image: url(images/icons/gear.svg);
+}
+
+#test-recorder-button::after {
+ background-image: url(images/icons/journal-code.svg);
+}
+
+#paste-log-button::after {
+ background-image: url(images/icons/clipboard.svg);
+}
+
+#docs-button::after {
+ background-image: url(images/icons/file-earmark-text.svg);
+}
+
+#test-recorder-button-snapshot {
+ margin-right: auto;
+}
+
+#test-recorder-button-snapshot::after {
+ background-image: url(images/icons/camera.svg);
+}
+
+#test-recorder-button-copy::after {
+ background-image: url(images/icons/clipboard.svg);
+}
+
+#test-recorder-button-download::after {
+ background-image: url(images/icons/download.svg);
+}
+
+.typeahead-popover {
+ background: #fff;
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ position: relative;
+}
+
+.typeahead-popover ul {
+ padding: 0;
+ list-style: none;
+ margin: 0;
+ border-radius: 8px;
+ max-height: 200px;
+ overflow-y: scroll;
+}
+
+.typeahead-popover ul::-webkit-scrollbar {
+ display: none;
+}
+
+.typeahead-popover ul {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.typeahead-popover ul li {
+ margin: 0;
+ min-width: 180px;
+ font-size: 14px;
+ outline: none;
+ cursor: pointer;
+ border-radius: 8px;
+}
+
+.typeahead-popover ul li.selected {
+ background: #eee;
+}
+
+.typeahead-popover li {
+ margin: 0 8px 0 8px;
+ padding: 8px;
+ color: #050505;
+ cursor: pointer;
+ line-height: 16px;
+ font-size: 15px;
+ display: flex;
+ align-content: center;
+ flex-direction: row;
+ flex-shrink: 0;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 0;
+}
+
+.typeahead-popover li.active {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ background-size: contain;
+}
+
+.typeahead-popover li:first-child {
+ border-radius: 8px 8px 0px 0px;
+}
+
+.typeahead-popover li:last-child {
+ border-radius: 0px 0px 8px 8px;
+}
+
+.typeahead-popover li:hover {
+ background-color: #eee;
+}
+
+.typeahead-popover li .text {
+ display: flex;
+ line-height: 20px;
+ flex-grow: 1;
+ min-width: 150px;
+}
+
+.typeahead-popover li .icon {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ user-select: none;
+ margin-right: 8px;
+ line-height: 16px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.component-picker-menu {
+ width: 200px;
+}
+
+.mentions-menu {
+ width: 250px;
+}
+
+.auto-embed-menu {
+ width: 150px;
+}
+
+.emoji-menu {
+ width: 200px;
+}
+
+i.palette {
+ background-image: url(images/icons/palette.svg);
+}
+
+i.bucket {
+ background-image: url(images/icons/paint-bucket.svg);
+}
+
+i.bold {
+ background-image: url(images/icons/type-bold.svg);
+}
+
+i.italic {
+ background-image: url(images/icons/type-italic.svg);
+}
+
+i.clear {
+ background-image: url(images/icons/trash.svg);
+}
+
+i.code {
+ background-image: url(images/icons/code.svg);
+}
+
+i.underline {
+ background-image: url(images/icons/type-underline.svg);
+}
+
+i.uppercase {
+ background-image: url(images/icons/type-uppercase.svg);
+}
+
+i.lowercase {
+ background-image: url(images/icons/type-lowercase.svg);
+}
+
+i.capitalize {
+ background-image: url(images/icons/type-capitalize.svg);
+}
+
+i.strikethrough {
+ background-image: url(images/icons/type-strikethrough.svg);
+}
+
+i.subscript {
+ background-image: url(images/icons/type-subscript.svg);
+}
+
+i.superscript {
+ background-image: url(images/icons/type-superscript.svg);
+}
+
+i.highlight {
+ background-image: url(images/icons/highlighter.svg);
+}
+
+i.link {
+ background-image: url(images/icons/link.svg);
+}
+
+i.horizontal-rule {
+ background-image: url(images/icons/horizontal-rule.svg);
+}
+
+.icon.plus {
+ background-image: url(images/icons/plus.svg);
+}
+
+.icon.caret-right {
+ background-image: url(images/icons/caret-right-fill.svg);
+}
+
+.icon.dropdown-more {
+ background-image: url(images/icons/dropdown-more.svg);
+}
+
+.icon.font-color {
+ background-image: url(images/icons/font-color.svg);
+}
+
+.icon.font-family {
+ background-image: url(images/icons/font-family.svg);
+}
+
+.icon.bg-color {
+ background-image: url(images/icons/bg-color.svg);
+}
+
+.icon.table {
+ background-color: #6c757d;
+ mask-image: url(images/icons/table.svg);
+ -webkit-mask-image: url(images/icons/table.svg);
+ mask-repeat: no-repeat;
+ -webkit-mask-repeat: no-repeat;
+ mask-size: contain;
+ -webkit-mask-size: contain;
+}
+
+i.image {
+ background-image: url(images/icons/file-image.svg);
+}
+
+i.table {
+ background-image: url(images/icons/table.svg);
+}
+
+i.close {
+ background-image: url(images/icons/close.svg);
+}
+
+i.figma {
+ background-image: url(images/icons/figma.svg);
+}
+
+i.poll {
+ background-image: url(images/icons/card-checklist.svg);
+}
+
+i.columns {
+ background-image: url(images/icons/3-columns.svg);
+}
+
+i.x {
+ background-image: url(images/icons/x.svg);
+}
+
+i.youtube {
+ background-image: url(images/icons/youtube.svg);
+}
+
+.icon.left-align,
+i.left-align {
+ background-image: url(images/icons/text-left.svg);
+}
+
+.icon.center-align,
+i.center-align {
+ background-image: url(images/icons/text-center.svg);
+}
+
+.icon.right-align,
+i.right-align {
+ background-image: url(images/icons/text-right.svg);
+}
+
+.icon.justify-align,
+i.justify-align {
+ background-image: url(images/icons/justify.svg);
+}
+
+.icon.vertical-top,
+i.left-align {
+ background-image: url(images/icons/vertical-top.svg);
+}
+
+.icon.vertical-middle,
+i.center-align {
+ background-image: url(images/icons/vertical-middle.svg);
+}
+
+.icon.vertical-bottom,
+i.right-align {
+ background-image: url(images/icons/vertical-bottom.svg);
+}
+
+i.indent {
+ background-image: url(images/icons/indent.svg);
+}
+
+i.markdown {
+ background-image: url(images/icons/markdown.svg);
+}
+
+i.outdent {
+ background-image: url(images/icons/outdent.svg);
+}
+
+i.undo {
+ background-image: url(images/icons/arrow-counterclockwise.svg);
+}
+
+i.redo {
+ background-image: url(images/icons/arrow-clockwise.svg);
+}
+
+i.sticky {
+ background-image: url(images/icons/sticky.svg);
+}
+
+i.mic {
+ background-image: url(images/icons/mic.svg);
+}
+
+i.import {
+ background-image: url(images/icons/upload.svg);
+}
+
+i.export {
+ background-image: url(images/icons/download.svg);
+}
+
+i.share {
+ background-image: url(images/icons/send.svg);
+}
+
+i.diagram-2 {
+ background-image: url(images/icons/diagram-2.svg);
+}
+
+i.user {
+ background-image: url(images/icons/user.svg);
+}
+
+i.equation {
+ background-image: url(images/icons/plus-slash-minus.svg);
+}
+
+i.calendar {
+ background-image: url(images/icons/calendar.svg);
+}
+
+i.gif {
+ background-image: url(images/icons/filetype-gif.svg);
+}
+
+i.copy {
+ background-image: url(images/icons/copy.svg);
+}
+
+i.paste {
+ background-image: url(images/icons/clipboard.svg);
+}
+
+i.success {
+ background-image: url(images/icons/success.svg);
+}
+
+i.prettier {
+ background-image: url(images/icons/prettier.svg);
+}
+
+i.prettier-error {
+ background-image: url(images/icons/prettier-error.svg);
+}
+
+i.page-break,
+.icon.page-break {
+ background-image: url(images/icons/scissors.svg);
+}
+
+.link-editor .button.active,
+.toolbar .button.active {
+ background-color: rgb(223, 232, 250);
+}
+
+.link-editor .link-input {
+ display: block;
+ width: calc(100% - 75px);
+ box-sizing: border-box;
+ margin: 12px 12px;
+ padding: 8px 12px;
+ border-radius: 15px;
+ background-color: #eee;
+ font-size: 15px;
+ color: rgb(5, 5, 5);
+ border: 0;
+ outline: 0;
+ position: relative;
+ font-family: inherit;
+}
+
+.link-editor .link-view {
+ display: block;
+ width: calc(100% - 24px);
+ margin: 8px 12px;
+ padding: 8px 12px;
+ border-radius: 15px;
+ font-size: 15px;
+ color: rgb(5, 5, 5);
+ border: 0;
+ outline: 0;
+ position: relative;
+ font-family: inherit;
+}
+
+.link-editor .link-view a {
+ display: block;
+ word-break: break-word;
+ width: calc(100% - 33px);
+}
+
+.link-editor div.link-edit {
+ background-image: url(images/icons/pencil-fill.svg);
+ background-size: 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 35px;
+ vertical-align: -0.25em;
+ position: absolute;
+ right: 30px;
+ top: 0;
+ bottom: 0;
+ cursor: pointer;
+}
+
+.link-editor div.link-trash {
+ background-image: url(images/icons/trash.svg);
+ background-size: 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 35px;
+ vertical-align: -0.25em;
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ cursor: pointer;
+}
+
+.link-editor div.link-cancel {
+ background-image: url(images/icons/close.svg);
+ background-size: 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 35px;
+ vertical-align: -0.25em;
+ margin-right: 28px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ cursor: pointer;
+}
+
+.link-editor div.link-confirm {
+ background-image: url(images/icons/success-alt.svg);
+ background-size: 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 35px;
+ vertical-align: -0.25em;
+ margin-right: 2px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ cursor: pointer;
+}
+
+.link-editor .link-input a {
+ color: rgb(33, 111, 219);
+ text-decoration: underline;
+ white-space: nowrap;
+ overflow: hidden;
+ margin-right: 30px;
+ text-overflow: ellipsis;
+}
+
+.link-editor .link-input a:hover {
+ text-decoration: underline;
+}
+
+.link-editor .font-size-wrapper,
+.link-editor .font-family-wrapper {
+ display: flex;
+ margin: 0 4px;
+}
+
+.link-editor select {
+ padding: 6px;
+ border: none;
+ background-color: rgba(0, 0, 0, 0.075);
+ border-radius: 4px;
+}
+
+.mention:focus {
+ box-shadow: rgb(180 213 255) 0px 0px 0px 2px;
+ outline: none;
+}
+
+.characters-limit {
+ color: #888;
+ font-size: 12px;
+ text-align: right;
+ display: block;
+ position: absolute;
+ left: 12px;
+ bottom: 5px;
+}
+
+.characters-limit.characters-limit-exceeded {
+ color: red;
+}
+
+.dropdown {
+ z-index: 100;
+ display: block;
+ position: fixed;
+ box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.5);
+ border-radius: 8px;
+ min-height: 40px;
+ background-color: #fff;
+}
+
+.dropdown .item {
+ margin: 0 8px 0 8px;
+ padding: 8px;
+ color: #050505;
+ cursor: pointer;
+ line-height: 16px;
+ font-size: 15px;
+ display: flex;
+ align-content: center;
+ flex-direction: row;
+ flex-shrink: 0;
+ justify-content: space-between;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 0;
+ max-width: 264px;
+ min-width: 100px;
+}
+
+.dropdown .item.wide {
+ align-items: center;
+ width: 260px;
+}
+
+.dropdown .item.wide .icon-text-container {
+ display: flex;
+
+ .text {
+ min-width: 120px;
+ }
+}
+
+.dropdown .item .shortcut {
+ color: #939393;
+ align-self: flex-end;
+}
+
+.dropdown .item .active {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ background-size: contain;
+}
+
+.dropdown .item:first-child {
+ margin-top: 8px;
+}
+
+.dropdown .item:last-child {
+ margin-bottom: 8px;
+}
+
+.dropdown .item:hover {
+ background-color: #eee;
+}
+
+.dropdown .item .text {
+ display: flex;
+ line-height: 20px;
+ flex-grow: 1;
+ min-width: 150px;
+}
+
+.dropdown .item .icon {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ user-select: none;
+ margin-right: 12px;
+ line-height: 16px;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.dropdown .divider {
+ width: auto;
+ background-color: #eee;
+ margin: 4px 8px;
+ height: 1px;
+}
+
+@media screen and (max-width: 1100px) {
+ .dropdown-button-text {
+ display: none !important;
+ }
+
+ .dialog-dropdown>.dropdown-button-text {
+ display: flex !important;
+ }
+
+ .font-size .dropdown-button-text {
+ display: flex !important;
+ }
+
+ .code-language .dropdown-button-text {
+ display: flex !important;
+ }
+}
+
+.icon.paragraph {
+ background-image: url(images/icons/text-paragraph.svg);
+}
+
+.toolbar-toolbar-item-icon.h1,
+.icon.h1 {
+ background-image: url(images/icons/type-h1.svg);
+}
+
+.toolbar-toolbar-item-icon.h2,
+.icon.h2 {
+ background-image: url(images/icons/type-h2.svg);
+}
+
+.toolbar-toolbar-item-icon.h3,
+.icon.h3 {
+ background-image: url(images/icons/type-h3.svg);
+}
+
+.toolbar-toolbar-item-icon.h4,
+.icon.h4 {
+ background-image: url(images/icons/type-h4.svg);
+}
+
+.toolbar-toolbar-item-icon.h5,
+.icon.h5 {
+ background-image: url(images/icons/type-h5.svg);
+}
+
+.toolbar-toolbar-item-icon.h6,
+.icon.h6 {
+ background-image: url(images/icons/type-h6.svg);
+}
+
+.toolbar-toolbar-item-icon.bullet-list,
+.icon.bullet-list,
+.icon.bullet {
+ background-image: url(images/icons/list-ul.svg);
+}
+
+.toolbar-toolbar-item-icon.check-list,
+.icon.check-list,
+.icon.check {
+ background-image: url(images/icons/square-check.svg);
+}
+
+.toolbar-toolbar-item-icon.numbered-list,
+.icon.numbered-list,
+.icon.number {
+ background-image: url(images/icons/list-ol.svg);
+}
+
+
+.toolbar-toolbar-item-icon.quote,
+.icon.quote {
+ background-image: url(images/icons/chat-square-quote.svg);
+}
+
+.toolbar-toolbar-item-icon.quote,
+.icon.code {
+ background-image: url(images/icons/code.svg);
+}
+
+.switches {
+ z-index: 6;
+ position: fixed;
+ left: 10px;
+ bottom: 70px;
+ animation: slide-in 0.4s ease;
+}
+
+@keyframes slide-in {
+ 0% {
+ opacity: 0;
+ transform: translateX(-200px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.switch {
+ display: block;
+ color: #444;
+ margin: 5px 0;
+ background-color: rgba(238, 238, 238, 0.7);
+ padding: 5px 10px;
+ border-radius: 10px;
+}
+
+#rich-text-switch {
+ right: 0;
+}
+
+#character-count-switch {
+ right: 130px;
+}
+
+.switch label {
+ margin-right: 5px;
+ line-height: 14px;
+ width: 100px;
+ font-size: 12px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.switch button {
+ background-color: rgb(206, 208, 212);
+ height: 24px;
+ box-sizing: border-box;
+ border-radius: 12px;
+ width: 44px;
+ display: inline-block;
+ vertical-align: middle;
+ position: relative;
+ outline: none;
+ cursor: pointer;
+ transition: background-color 0.1s;
+ border: 2px solid transparent;
+}
+
+.switch button:focus-visible {
+ border-color: blue;
+}
+
+.switch button span {
+ top: 0px;
+ left: 0px;
+ display: block;
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border-radius: 12px;
+ background-color: white;
+ transition: transform 0.2s;
+}
+
+.switch button[aria-checked='true'] {
+ background-color: rgb(24, 119, 242);
+}
+
+.switch button[aria-checked='true'] span {
+ transform: translateX(20px);
+}
+
+
+
+.emoji {
+ color: transparent;
+ caret-color: rgb(5, 5, 5);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ vertical-align: middle;
+ margin: 0 -1px;
+}
+
+.emoji-inner {
+ padding: 0 0.15em;
+}
+
+.emoji-inner::selection {
+ color: transparent;
+ background-color: rgba(150, 150, 150, 0.4);
+}
+
+.emoji-inner::moz-selection {
+ color: transparent;
+ background-color: rgba(150, 150, 150, 0.4);
+}
+
+.emoji.happysmile {
+ background-image: url(images/emoji/1F642.png);
+}
+
+.emoji.veryhappysmile {
+ background-image: url(images/emoji/1F600.png);
+}
+
+.emoji.unhappysmile {
+ background-image: url(images/emoji/1F641.png);
+}
+
+.emoji.heart {
+ background-image: url(images/emoji/2764.png);
+}
+
+.keyword {
+ color: rgb(241, 118, 94);
+ font-weight: bold;
+}
+
+.actions {
+ position: absolute;
+ text-align: right;
+ margin: 10px;
+ bottom: 0;
+ right: 0;
+}
+
+.actions.tree-view {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.actions i {
+ background-size: contain;
+ display: inline-block;
+ height: 15px;
+ width: 15px;
+ vertical-align: -0.25em;
+}
+
+.actions i.indent {
+ background-image: url(images/icons/indent.svg);
+}
+
+.actions i.outdent {
+ background-image: url(images/icons/outdent.svg);
+}
+
+.actions i.lock {
+ background-image: url(images/icons/lock-fill.svg);
+}
+
+.actions i.image {
+ background-image: url(images/icons/file-image.svg);
+}
+
+.actions i.table {
+ background-image: url(images/icons/table.svg);
+}
+
+.actions i.unlock {
+ background-image: url(images/icons/lock.svg);
+}
+
+.actions i.left-align {
+ background-image: url(images/icons/text-left.svg);
+}
+
+.actions i.center-align {
+ background-image: url(images/icons/text-center.svg);
+}
+
+.actions i.right-align {
+ background-image: url(images/icons/text-right.svg);
+}
+
+.actions i.justify-align {
+ background-image: url(images/icons/justify.svg);
+}
+
+.actions i.disconnect {
+ background-image: url(images/icons/plug.svg);
+}
+
+.actions i.connect {
+ background-image: url(images/icons/plug-fill.svg);
+}
+
+.table-cell-action-button-container {
+ position: absolute;
+ z-index: 3;
+ top: 0;
+ left: 0;
+ will-change: transform;
+}
+
+.table-cell-action-button-container.table-cell-action-button-container--active {
+ pointer-events: auto;
+ opacity: 1;
+}
+
+.table-cell-action-button-container.table-cell-action-button-container--inactive {
+ pointer-events: none;
+ opacity: 0;
+}
+
+.table-cell-action-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 0;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ border-radius: 15px;
+ color: #222;
+ display: inline-block;
+ cursor: pointer;
+}
+
+i.chevron-down {
+ background-color: transparent;
+ background-size: contain;
+ display: inline-block;
+ height: 8px;
+ width: 8px;
+ background-image: url(images/icons/chevron-down.svg);
+}
+
+.action-button {
+ background-color: #eee;
+ border: 0;
+ padding: 8px 12px;
+ position: relative;
+ margin-left: 5px;
+ border-radius: 15px;
+ color: #222;
+ display: inline-block;
+ cursor: pointer;
+}
+
+.action-button:hover {
+ background-color: #ddd;
+ color: #000;
+}
+
+.action-button-mic.active {
+ animation: mic-pulsate-color 3s infinite;
+}
+
+button.action-button:disabled {
+ opacity: 0.6;
+ background: #eee;
+ cursor: not-allowed;
+}
+
+@keyframes mic-pulsate-color {
+ 0% {
+ background-color: #ffdcdc;
+ }
+
+ 50% {
+ background-color: #ff8585;
+ }
+
+ 100% {
+ background-color: #ffdcdc;
+ }
+}
+
+.debug-timetravel-panel {
+ overflow: hidden;
+ padding: 0 0 10px 0;
+ margin: auto;
+ display: flex;
+}
+
+.debug-timetravel-panel-slider {
+ padding: 0;
+ flex: 8;
+}
+
+.debug-timetravel-panel-button {
+ padding: 0;
+ border: 0;
+ background: none;
+ flex: 1;
+ color: #fff;
+ font-size: 12px;
+}
+
+.debug-timetravel-panel-button:hover {
+ text-decoration: underline;
+}
+
+.debug-timetravel-button {
+ border: 0;
+ padding: 0;
+ font-size: 12px;
+ top: 10px;
+ right: 15px;
+ position: absolute;
+ background: none;
+ color: #fff;
+}
+
+.debug-timetravel-button:hover {
+ text-decoration: underline;
+}
+
+.debug-treetype-button {
+ border: 0;
+ padding: 0;
+ font-size: 12px;
+ top: 10px;
+ right: 85px;
+ position: absolute;
+ background: none;
+ color: #fff;
+}
+
+.debug-treetype-button:hover {
+ text-decoration: underline;
+}
+
+.connecting {
+ font-size: 15px;
+ color: #999;
+ overflow: hidden;
+ position: absolute;
+ text-overflow: ellipsis;
+ top: 10px;
+ left: 10px;
+ user-select: none;
+ white-space: nowrap;
+ display: inline-block;
+ pointer-events: none;
+}
+
+.toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 1px;
+ background: #fff;
+ padding: 4px;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ vertical-align: middle;
+ overflow: auto;
+ /* height: 36px; */
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ overflow-y: hidden;
+ /* disable vertical scroll*/
+}
+
+button.toolbar-item {
+ border: 0;
+ display: flex;
+ background: none;
+ border-radius: 10px;
+ padding: 8px;
+ cursor: pointer;
+ vertical-align: middle;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: space-between;
+}
+
+button.toolbar-item:disabled {
+ cursor: not-allowed;
+}
+
+button.toolbar-item.spaced {
+ margin-right: 2px;
+}
+
+button.toolbar-item i.format {
+ background-size: contain;
+ display: inline-block;
+ height: 18px;
+ width: 18px;
+ vertical-align: -0.25em;
+ display: flex;
+ opacity: 0.6;
+}
+
+button.toolbar-item:disabled .icon,
+button.toolbar-item:disabled .text,
+button.toolbar-item:disabled i.format,
+button.toolbar-item:disabled .chevron-down {
+ opacity: 0.2;
+}
+
+button.toolbar-item.active {
+ background-color: rgba(223, 232, 250, 0.3);
+}
+
+button.toolbar-item.active i {
+ opacity: 1;
+}
+
+.toolbar-item:hover:not([disabled]) {
+ background-color: #eee;
+}
+
+.toolbar-item.font-family .text {
+ display: block;
+ max-width: 40px;
+}
+
+.toolbar .code-language {
+ width: 150px;
+}
+
+.toolbar .toolbar-item .text {
+ display: flex;
+ line-height: 20px;
+ vertical-align: middle;
+ font-size: 14px;
+ color: #777;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ height: 20px;
+ text-align: left;
+ padding-right: 10px;
+}
+
+.toolbar .toolbar-item .icon {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ user-select: none;
+ margin-right: 8px;
+ line-height: 16px;
+ background-size: contain;
+}
+
+.toolbar i.chevron-down,
+.toolbar-item i.chevron-down {
+ margin-top: 3px;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ user-select: none;
+}
+
+.toolbar i.chevron-down.inside {
+ width: 16px;
+ height: 16px;
+ display: flex;
+ margin-left: -25px;
+ margin-top: 11px;
+ margin-right: 10px;
+ pointer-events: none;
+}
+
+.toolbar .divider {
+ width: 1px;
+ background-color: #eee;
+ margin: 0 4px;
+}
+
+.sticky-note-container {
+ position: absolute;
+ z-index: 9;
+ width: 120px;
+ display: inline-block;
+}
+
+.sticky-note {
+ line-height: 1;
+ text-align: left;
+ width: 120px;
+ margin: 25px;
+ padding: 20px 10px;
+ position: relative;
+ border: 1px solid #e8e8e8;
+ font-family: 'Reenie Beanie';
+ font-size: 24px;
+ border-bottom-right-radius: 60px 5px;
+ display: block;
+ cursor: move;
+}
+
+.sticky-note:after {
+ content: '';
+ position: absolute;
+ z-index: -1;
+ right: -0px;
+ bottom: 20px;
+ width: 120px;
+ height: 25px;
+ background: rgba(0, 0, 0, 0.2);
+ box-shadow: 2px 15px 5px rgba(0, 0, 0, 0.4);
+ transform: matrix(-1, -0.1, 0, 1, 0, 0);
+}
+
+.sticky-note.yellow {
+ border-top: 1px solid #fdfd86;
+ background: linear-gradient(135deg,
+ #ffff88 81%,
+ #ffff88 82%,
+ #ffff88 82%,
+ #ffffc6 100%);
+}
+
+.sticky-note.pink {
+ border-top: 1px solid #e7d1e4;
+ background: linear-gradient(135deg,
+ #f7cbe8 81%,
+ #f7cbe8 82%,
+ #f7cbe8 82%,
+ #e7bfe1 100%);
+}
+
+.sticky-note-container.dragging {
+ transition: none !important;
+}
+
+.sticky-note div {
+ cursor: text;
+}
+
+.sticky-note .delete {
+ border: 0;
+ background: none;
+ position: absolute;
+ top: 8px;
+ right: 10px;
+ font-size: 10px;
+ cursor: pointer;
+ opacity: 0.5;
+}
+
+.sticky-note .delete:hover {
+ font-weight: bold;
+ opacity: 1;
+}
+
+.sticky-note .color {
+ border: 0;
+ background: none;
+ position: absolute;
+ top: 8px;
+ right: 25px;
+ cursor: pointer;
+ opacity: 0.5;
+}
+
+.sticky-note .color:hover {
+ opacity: 1;
+}
+
+.sticky-note .color i {
+ display: block;
+ width: 12px;
+ height: 12px;
+ background-size: contain;
+}
+
+.excalidraw-button {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ background-color: transparent;
+}
+
+.excalidraw-button.selected {
+ outline: 2px solid rgb(60, 132, 244);
+ user-select: none;
+}
+
+.github-corner:hover .octo-arm {
+ animation: octocat-wave 560ms ease-in-out;
+}
+
+@keyframes octocat-wave {
+
+ 0%,
+ 100% {
+ transform: rotate(0);
+ }
+
+ 20%,
+ 60% {
+ transform: rotate(-25deg);
+ }
+
+ 40%,
+ 80% {
+ transform: rotate(10deg);
+ }
+}
+
+@media (max-width: 500px) {
+ .github-corner:hover .octo-arm {
+ animation: none;
+ }
+
+ .github-corner .octo-arm {
+ animation: octocat-wave 560ms ease-in-out;
+ }
+}
+
+.spacer {
+ letter-spacing: -2px;
+}
+
+.editor-equation {
+ cursor: default;
+ user-select: none;
+}
+
+.editor-equation.focused {
+ outline: 2px solid rgb(60, 132, 244);
+}
+
+button.item i {
+ opacity: 0.6;
+}
+
+button.item.dropdown-item-active {
+ background-color: rgba(223, 232, 250, 0.3);
+}
+
+button.item.dropdown-item-active i {
+ opacity: 1;
+}
+
+.TableNode__contentEditable {
+ min-height: 20px;
+ border: 0px;
+ resize: none;
+ cursor: text;
+ display: block;
+ position: relative;
+ outline: 0px;
+ padding: 0;
+ user-select: text;
+ font-size: 15px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ z-index: 3;
+}
+
+.dialog-dropdown {
+ background-color: #eee !important;
+ margin-bottom: 10px;
+ width: 100%;
+}
+
+.toolbar .block-controls {
+ display: flex;
+ align-items: center;
+}
+
+.toolbar .block-controls .dropdown-button-text {
+ width: 7em;
+ text-align: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/src/components/rich-text-lexical/katex.d.ts b/src/components/rich-text-lexical/katex.d.ts
new file mode 100644
index 0000000000..f675c4f7d0
--- /dev/null
+++ b/src/components/rich-text-lexical/katex.d.ts
@@ -0,0 +1,54 @@
+/**
+ * Type declaration for katex module
+ * Resolves TypeScript module resolution issue with katex types
+ * The katex package includes types at node_modules/katex/dist/katex.d.ts
+ */
+declare module 'katex' {
+ const katex: {
+ render: (
+ tex: string,
+ element: HTMLElement,
+ options?: {
+ displayMode?: boolean;
+ output?: 'html' | 'mathml' | 'htmlAndMathml';
+ leqno?: boolean;
+ fleqn?: boolean;
+ throwOnError?: boolean;
+ errorColor?: string;
+ macros?: Record;
+ minRuleThickness?: number;
+ colorIsTextColor?: boolean;
+ strict?: boolean | string | Function;
+ trust?: boolean | ((context: any) => boolean);
+ maxSize?: number;
+ maxExpand?: number;
+ globalGroup?: boolean;
+ },
+ ) => void;
+ renderToString: (
+ tex: string,
+ options?: {
+ displayMode?: boolean;
+ output?: 'html' | 'mathml' | 'htmlAndMathml';
+ leqno?: boolean;
+ fleqn?: boolean;
+ throwOnError?: boolean;
+ errorColor?: string;
+ macros?: Record;
+ minRuleThickness?: number;
+ colorIsTextColor?: boolean;
+ strict?: boolean | string | Function;
+ trust?: boolean | ((context: any) => boolean);
+ maxSize?: number;
+ maxExpand?: number;
+ globalGroup?: boolean;
+ },
+ ) => string;
+ __parse: (tex: string, settings: any) => any;
+ __renderToDomTree: (tex: string, settings: any) => any;
+ __renderToHTMLTree: (tex: string, settings: any) => any;
+ __setFontMetrics: (fontName: string, metrics: any) => void;
+ ParseError: any;
+ };
+ export default katex;
+}
diff --git a/src/components/rich-text-lexical/mainStyles.tsx b/src/components/rich-text-lexical/mainStyles.tsx
new file mode 100644
index 0000000000..9cc4bebbe9
--- /dev/null
+++ b/src/components/rich-text-lexical/mainStyles.tsx
@@ -0,0 +1,1251 @@
+import styled from 'styled-components';
+
+export const ToolBarHolder = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 1px;
+ background: #fff;
+ padding: 4px;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ vertical-align: middle;
+ overflow: auto;
+ /* height: 36px; */
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ overflow-y: hidden;
+ /* disable vertical scroll*/
+
+ button.toolbar-item {
+ border: 0;
+ display: flex;
+ background: none;
+ border-radius: 10px;
+ padding: 8px;
+ cursor: pointer;
+ vertical-align: middle;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ button.toolbar-item:disabled {
+ cursor: not-allowed;
+ }
+
+ button.toolbar-item.spaced {
+ margin-right: 2px;
+ }
+
+ button.toolbar-item i.format {
+ background-size: contain;
+ display: inline-block;
+ height: 18px;
+ width: 18px;
+ vertical-align: -0.25em;
+ display: flex;
+ opacity: 0.6;
+ }
+
+ button.toolbar-item:disabled .icon,
+ button.toolbar-item:disabled .text,
+ button.toolbar-item:disabled i.format,
+ button.toolbar-item:disabled .chevron-down {
+ opacity: 0.2;
+ }
+
+ button.toolbar-item.active {
+ background-color: rgba(223, 232, 250, 0.3);
+ }
+
+ button.toolbar-item.active i {
+ opacity: 1;
+ }
+
+ .toolbar-item:hover:not([disabled]) {
+ background-color: #eee;
+ }
+
+ .toolbar-item.font-family .text {
+ display: block;
+ max-width: 40px;
+ }
+
+ .code-language {
+ width: 150px;
+ }
+
+ .toolbar-item .text {
+ display: flex;
+ line-height: 20px;
+ vertical-align: middle;
+ font-size: 14px;
+ color: #777;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ height: 20px;
+ text-align: left;
+ padding-right: 10px;
+ }
+
+ .toolbar-item .icon {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ user-select: none;
+ margin-right: 8px;
+ line-height: 16px;
+ background-size: contain;
+ }
+
+ i.chevron-down,
+ .toolbar-item i.chevron-down {
+ margin-top: 3px;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ user-select: none;
+ }
+
+ i.chevron-down.inside {
+ width: 16px;
+ height: 16px;
+ display: flex;
+ margin-left: -25px;
+ margin-top: 11px;
+ margin-right: 10px;
+ pointer-events: none;
+ }
+
+ .divider {
+ width: 1px;
+ background-color: #eee;
+ margin: 0 4px;
+ }
+
+ i.palette {
+ background-image: url(/images/rich-text-lexical/icons/palette.svg);
+ }
+
+ i.bucket {
+ background-image: url(/images/rich-text-lexical/icons/paint-bucket.svg);
+ }
+
+ i.bold {
+ background-image: url(/images/rich-text-lexical/icons/type-bold.svg);
+ }
+
+ i.italic {
+ background-image: url(/images/rich-text-lexical/icons/type-italic.svg);
+ }
+
+ i.undo {
+ background-image: url(/images/rich-text-lexical/icons/arrow-counterclockwise.svg);
+ }
+
+ i.redo {
+ background-image: url(/images/rich-text-lexical/icons/arrow-clockwise.svg);
+ }
+
+ .icon.paragraph {
+ background-image: url(/images/rich-text-lexical/icons/text-paragraph.svg);
+ }
+
+ .icon.h1 {
+ background-image: url(/images/rich-text-lexical/icons/type-h1.svg);
+ }
+
+ .icon.h2 {
+ background-image: url(/images/rich-text-lexical/icons/type-h2.svg);
+ }
+
+ .icon.h3 {
+ background-image: url(/images/rich-text-lexical/icons/type-h3.svg);
+ }
+
+ .icon.h4 {
+ background-image: url(/images/rich-text-lexical/icons/type-h4.svg);
+ }
+
+ .icon.h5 {
+ background-image: url(/images/rich-text-lexical/icons/type-h5.svg);
+ }
+
+ .icon.h6 {
+ background-image: url(/images/rich-text-lexical/icons/type-h6.svg);
+ }
+
+ .icon.bullet-list {
+ background-image: url(/images/rich-text-lexical/icons/list-bullet.svg);
+ }
+
+ .icon.check-list {
+ background-image: url(/images/rich-text-lexical/icons/list-check.svg);
+ }
+
+ .icon.numbered-list {
+ background-image: url(/images/rich-text-lexical/icons/list-numbered.svg);
+ }
+
+ .icon.quote {
+ background-image: url(/images/rich-text-lexical/icons/quote.svg);
+ }
+
+ .icon.code {
+ background-image: url(/images/rich-text-lexical/icons/code.svg);
+ }
+
+ .icon.table {
+ background-image: url(/images/rich-text-lexical/icons/table.svg);
+ }
+
+ .icon.image {
+ background-image: url(/images/rich-text-lexical/icons/file-image.svg);
+ }
+
+ .icon.left-align {
+ background-image: url(/images/rich-text-lexical/icons/align-left.svg);
+ }
+
+ .icon.center-align {
+ background-image: url(/images/rich-text-lexical/icons/align-center.svg);
+ }
+
+ .icon.right-align {
+ background-image: url(/images/rich-text-lexical/icons/align-right.svg);
+ }
+
+ .icon.justify-align {
+ background-image: url(/images/rich-text-lexical/icons/align-justify.svg);
+ }
+
+ .icon.vertical-top {
+ background-image: url(/images/rich-text-lexical/icons/align-top.svg);
+ }
+
+ .icon.vertical-middle {
+ background-image: url(/images/rich-text-lexical/icons/align-middle.svg);
+ }
+
+ .icon.vertical-bottom {
+ background-image: url(/images/rich-text-lexical/icons/align-bottom.svg);
+ }
+
+ .icon.indent {
+ background-image: url(/images/rich-text-lexical/icons/indent.svg);
+ }
+
+ .icon.outdent {
+ background-image: url(/images/rich-text-lexical/icons/outdent.svg);
+ }
+
+ .icon.lock {
+ background-image: url(/images/rich-text-lexical/icons/lock.svg);
+ }
+
+ .icon.unlock {
+ background-image: url(/images/rich-text-lexical/icons/unlock.svg);
+ }
+
+ .icon.number,
+ .icon.numbered-list {
+ background-image: url(/images/rich-text-lexical/icons/list-ol.svg);
+ }
+
+ .icon.bullet,
+ .icon.bullet-list {
+ background-image: url(/images/rich-text-lexical/icons/list-ul.svg);
+ }
+
+ .icon.check-list,
+ .icon.check {
+ background-image: url(/images/rich-text-lexical/icons/square-check.svg);
+ }
+
+ .icon.quote {
+ background-image: url(/images/rich-text-lexical/icons/chat-square-quote.svg);
+ }
+
+ .icon.font-family {
+ background-image: url(/images/rich-text-lexical/icons/font-family.svg);
+ }
+
+ i.underline {
+ background-image: url(/images/rich-text-lexical/icons/type-underline.svg);
+ }
+
+ i.code {
+ background-image: url(/images/rich-text-lexical/icons/code.svg);
+ }
+
+ i.link {
+ background-image: url(/images/rich-text-lexical/icons/link.svg);
+ }
+
+ .icon.font-color {
+ background-image: url(/images/rich-text-lexical/icons/font-color.svg);
+ }
+
+ i.chevron-down {
+ background-color: transparent;
+ background-size: contain;
+ display: inline-block;
+ height: 8px;
+ width: 8px;
+ background-image: url(/images/rich-text-lexical/icons/chevron-down.svg);
+ }
+
+ .icon.bg-color {
+ background-image: url(/images/rich-text-lexical/icons/bg-color.svg);
+ }
+
+ .icon.dropdown-more {
+ background-image: url(/images/rich-text-lexical/icons/dropdown-more.svg);
+ }
+
+ .icon.plus {
+ background-image: url(/images/rich-text-lexical/icons/plus.svg);
+ }
+
+ i.poll {
+ background-image: url(/images/rich-text-lexical/icons/card-checklist.svg);
+ }
+
+ .icon.columns {
+ background-image: url(/images/rich-text-lexical/icons/3-columns.svg);
+ }
+
+ .icon.x {
+ background-image: url(/images/rich-text-lexical/icons/x.svg);
+ }
+
+ .icon.youtube {
+ background-image: url(/images/rich-text-lexical/icons/youtube.svg);
+ }
+
+ .icon.figma {
+ background-image: url(/images/rich-text-lexical/icons/figma.svg);
+ }
+
+ .icon.close {
+ background-image: url(/images/rich-text-lexical/icons/close.svg);
+ }
+
+ i.horizontal-rule {
+ background-image: url(/images/rich-text-lexical/icons/horizontal-rule.svg);
+ }
+
+ i.page-break,
+ .icon.page-break {
+ background-image: url(/images/rich-text-lexical/icons/scissors.svg);
+ }
+
+ .icon.left-align,
+ i.left-align {
+ background-image: url(/images/rich-text-lexical/icons/text-left.svg);
+ }
+
+ .icon.center-align,
+ i.center-align {
+ background-image: url(/images/rich-text-lexical/icons/text-center.svg);
+ }
+
+ .icon.right-align,
+ i.right-align {
+ background-image: url(/images/rich-text-lexical/icons/text-right.svg);
+ }
+
+ .icon.justify-align,
+ i.justify-align {
+ background-image: url(/images/rich-text-lexical/icons/justify.svg);
+ }
+
+ i.outdent {
+ background-image: url(/images/rich-text-lexical/icons/outdent.svg);
+ }
+
+ i.indent {
+ background-image: url(/images/rich-text-lexical/icons/indent.svg);
+ }
+
+ .add-icon {
+ background-image: url(/images/rich-text-lexical/icons/add-sign.svg);
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ .minus-icon {
+ background-image: url(/images/rich-text-lexical/icons/minus-sign.svg);
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+`;
+
+export const DropdownUI = styled.div`
+ z-index: 100;
+ display: block;
+ position: fixed;
+ box-shadow:
+ 0 12px 28px #0003,
+ 0 2px 4px #0000001a,
+ inset 0 0 0 1px #ffffff80;
+ border-radius: 8px;
+ min-height: 40px;
+ background-color: #fff;
+
+ .item {
+ margin: 0 8px;
+ padding: 8px;
+ color: #050505;
+ cursor: pointer;
+ line-height: 16px;
+ font-size: 15px;
+ display: flex;
+ align-content: center;
+ flex-direction: row;
+ flex-shrink: 0;
+ justify-content: space-between;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 0;
+ max-width: 264px;
+ min-width: 100px;
+ }
+
+ .item.wide {
+ align-items: center;
+ width: 260px;
+ }
+
+ .item.wide .icon-text-container {
+ display: flex;
+ }
+
+ .item.wide .icon-text-container .text {
+ min-width: 120px;
+ }
+
+ .item .shortcut {
+ color: #939393;
+ align-self: flex-end;
+ }
+
+ .item .active {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ background-size: contain;
+ }
+
+ .item:first-child {
+ margin-top: 8px;
+ }
+
+ .item:last-child {
+ margin-bottom: 8px;
+ }
+
+ .item:hover {
+ background-color: #eee;
+ }
+
+ .item .text {
+ display: flex;
+ line-height: 20px;
+ flex-grow: 1;
+ min-width: 150px;
+ }
+
+ .item .icon {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ -webkit-user-select: none;
+ user-select: none;
+ margin-right: 12px;
+ line-height: 16px;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ }
+
+ .divider {
+ width: auto;
+ background-color: #eee;
+ margin: 4px 8px;
+ height: 1px;
+ }
+
+ .icon.paragraph {
+ background-image: url(/images/rich-text-lexical/icons/text-paragraph.svg);
+ }
+
+ .icon.h1 {
+ background-image: url(/images/rich-text-lexical/icons/type-h1.svg);
+ }
+
+ .icon.h2 {
+ background-image: url(/images/rich-text-lexical/icons/type-h2.svg);
+ }
+
+ .icon.h3 {
+ background-image: url(/images/rich-text-lexical/icons/type-h3.svg);
+ }
+
+ .icon.h4 {
+ background-image: url(/images/rich-text-lexical/icons/type-h4.svg);
+ }
+
+ .icon.h5 {
+ background-image: url(/images/rich-text-lexical/icons/type-h5.svg);
+ }
+
+ .icon.h6 {
+ background-image: url(/images/rich-text-lexical/icons/type-h6.svg);
+ }
+
+ .icon.bullet-list {
+ background-image: url(/images/rich-text-lexical/icons/list-bullet.svg);
+ }
+
+ .icon.check-list {
+ background-image: url(/images/rich-text-lexical/icons/list-check.svg);
+ }
+
+ .icon.numbered-list {
+ background-image: url(/images/rich-text-lexical/icons/list-ol.svg);
+ }
+
+ .icon.quote {
+ background-image: url(/images/rich-text-lexical/icons/quote.svg);
+ }
+
+ .icon.code {
+ background-image: url(/images/rich-text-lexical/icons/code.svg);
+ }
+
+ .icon.table {
+ background-image: url(/images/rich-text-lexical/icons/table.svg);
+ }
+
+ .icon.bullet,
+ .icon.bullet-list {
+ background-image: url(/images/rich-text-lexical/icons/list-ul.svg);
+ }
+
+ .icon.check-list,
+ .icon.check {
+ background-image: url(/images/rich-text-lexical/icons/square-check.svg);
+ }
+
+ .icon.quote {
+ background-image: url(/images/rich-text-lexical/icons/chat-square-quote.svg);
+ }
+
+ i.poll {
+ background-image: url(/images/rich-text-lexical/icons/card-checklist.svg);
+ }
+
+ .icon.columns {
+ background-image: url(/images/rich-text-lexical/icons/3-columns.svg);
+ }
+
+ .icon.x {
+ background-image: url(/images/rich-text-lexical/icons/x.svg);
+ }
+
+ .icon.youtube {
+ background-image: url(/images/rich-text-lexical/icons/youtube.svg);
+ }
+
+ .icon.figma {
+ background-image: url(/images/rich-text-lexical/icons/figma.svg);
+ }
+
+ .icon.close {
+ background-image: url(/images/rich-text-lexical/icons/close.svg);
+ }
+
+ i.horizontal-rule {
+ background-image: url(/images/rich-text-lexical/icons/horizontal-rule.svg);
+ }
+
+ i.page-break,
+ .icon.page-break {
+ background-image: url(/images/rich-text-lexical/icons/scissors.svg);
+ }
+
+ .icon.image,
+ i.image {
+ background-image: url(/images/rich-text-lexical/icons/file-image.svg);
+ }
+
+ i.gif {
+ background-image: url(/images/rich-text-lexical/icons/filetype-gif.svg);
+ }
+
+ i.diagram-2 {
+ background-image: url(/images/rich-text-lexical/icons/diagram-2.svg);
+ }
+
+ i.equation {
+ background-image: url(/images/rich-text-lexical/icons/plus-slash-minus.svg);
+ }
+
+ i.sticky {
+ background-image: url(/images/rich-text-lexical/icons/sticky.svg);
+ }
+
+ .icon.caret-right {
+ background-image: url(/images/rich-text-lexical/icons/caret-right-fill.svg);
+ }
+
+ i.calendar {
+ background-image: url(/images/rich-text-lexical/icons/calendar.svg);
+ }
+
+ .icon.left-align,
+ i.left-align {
+ background-image: url(/images/rich-text-lexical/icons/text-left.svg);
+ }
+
+ .icon.center-align,
+ i.center-align {
+ background-image: url(/images/rich-text-lexical//icons/text-center.svg);
+ }
+
+ .icon.right-align,
+ i.right-align {
+ background-image: url(/images/rich-text-lexical//icons/text-right.svg);
+ }
+
+ .icon.justify-align,
+ i.justify-align {
+ background-image: url(/images/rich-text-lexical//icons/justify.svg);
+ }
+
+ i.outdent {
+ background-image: url(/images/rich-text-lexical/icons/outdent.svg);
+ }
+
+ i.indent {
+ background-image: url(/images/rich-text-lexical/icons/indent.svg);
+ }
+
+ i.lowercase {
+ background-image: url(/images/rich-text-lexical/icons/type-lowercase.svg);
+ }
+
+ i.uppercase {
+ background-image: url(/images/rich-text-lexical/icons/type-uppercase.svg);
+ }
+
+ i.capitalize {
+ background-image: url(/images/rich-text-lexical/icons/type-capitalize.svg);
+ }
+
+ i.strikethrough {
+ background-image: url(/images/rich-text-lexical/icons/type-strikethrough.svg);
+ }
+
+ i.subscript {
+ background-image: url(/images/rich-text-lexical/icons/type-subscript.svg);
+ }
+
+ i.superscript {
+ background-image: url(/images/rich-text-lexical/icons/type-superscript.svg);
+ }
+
+ i.highlight {
+ background-image: url(/images/rich-text-lexical/icons/highlighter.svg);
+ }
+
+ i.clear {
+ background-image: url(/images/rich-text-lexical/icons/trash.svg);
+ }
+}
+`;
+
+export const EditorShell = styled.div`
+ position: relative;
+
+ span.editor-image {
+ cursor: default;
+ display: inline-block;
+ position: relative;
+ user-select: none;
+ }
+
+ .editor-image img {
+ max-width: 100%;
+ cursor: default;
+ }
+
+ .editor-image img.focused {
+ outline: 2px solid rgb(60, 132, 244);
+ user-select: none;
+ }
+
+ .editor-image img.focused.draggable {
+ cursor: grab;
+ }
+
+ .editor-image img.focused.draggable:active {
+ cursor: grabbing;
+ }
+
+ .editor-image .image-caption-container .tree-view-output {
+ margin: 0;
+ border-radius: 0;
+ }
+
+ .editor-image .image-caption-container {
+ display: block;
+ position: absolute;
+ bottom: 4px;
+ left: 0;
+ right: 0;
+ padding: 0;
+ margin: 0;
+ border-top: 1px solid #fff;
+ background-color: rgba(255, 255, 255, 0.9);
+ min-width: 100px;
+ color: #000;
+ overflow: hidden;
+ }
+
+ .editor-image .image-caption-button {
+ display: block;
+ position: absolute;
+ bottom: 20px;
+ left: 0;
+ right: 0;
+ width: 30%;
+ padding: 10px;
+ margin: 0 auto;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 5px;
+ background-color: rgba(0, 0, 0, 0.5);
+ min-width: 100px;
+ color: #fff;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .editor-image .image-caption-button:hover {
+ background-color: rgba(60, 132, 244, 0.5);
+ }
+
+ .editor-image .image-edit-button {
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 5px;
+ background-image: url(/images/rich-text-lexical/icons/pencil-fill.svg);
+ background-size: 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 35px;
+ height: 35px;
+ vertical-align: -0.25em;
+ position: absolute;
+ right: 4px;
+ top: 4px;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .editor-image .image-edit-button:hover {
+ background-color: rgba(60, 132, 244, 0.1);
+ }
+
+ .editor-image .image-resizer {
+ display: block;
+ width: 7px;
+ height: 7px;
+ position: absolute;
+ background-color: rgb(60, 132, 244);
+ border: 1px solid #fff;
+ }
+
+ .editor-image .image-resizer.image-resizer-n {
+ top: -6px;
+ left: 48%;
+ cursor: n-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-ne {
+ top: -6px;
+ right: -6px;
+ cursor: ne-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-e {
+ bottom: 48%;
+ right: -6px;
+ cursor: e-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-se {
+ bottom: -2px;
+ right: -6px;
+ cursor: nwse-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-s {
+ bottom: -2px;
+ left: 48%;
+ cursor: s-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-sw {
+ bottom: -2px;
+ left: -6px;
+ cursor: sw-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-w {
+ bottom: 48%;
+ left: -6px;
+ cursor: w-resize;
+ }
+
+ .editor-image .image-resizer.image-resizer-nw {
+ top: -6px;
+ left: -6px;
+ cursor: nw-resize;
+ }
+
+ span.inline-editor-image {
+ cursor: default;
+ display: inline-block;
+ position: relative;
+ z-index: 1;
+ }
+
+ .inline-editor-image img {
+ max-width: 100%;
+ cursor: default;
+ }
+
+ .inline-editor-image img.focused {
+ outline: 2px solid rgb(60, 132, 244);
+ }
+
+ .inline-editor-image img.focused.draggable {
+ cursor: grab;
+ }
+
+ .inline-editor-image img.focused.draggable:active {
+ cursor: grabbing;
+ }
+
+ .inline-editor-image .image-caption-container .tree-view-output {
+ margin: 0;
+ border-radius: 0;
+ }
+
+ .inline-editor-image.position-full {
+ margin: 1em 0 1em 0;
+ }
+
+ .inline-editor-image.position-left {
+ float: left;
+ width: 50%;
+ margin: 1em 1em 0 0;
+ }
+
+ .inline-editor-image.position-right {
+ float: right;
+ width: 50%;
+ margin: 1em 0 0 1em;
+ }
+
+ .inline-editor-image .image-edit-button {
+ display: block;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ padding: 6px 8px;
+ margin: 0 auto;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 5px;
+ background-color: rgba(0, 0, 0, 0.5);
+ min-width: 60px;
+ color: #fff;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .inline-editor-image .image-edit-button:hover {
+ background-color: rgba(60, 132, 244, 0.5);
+ }
+
+ .inline-editor-image .image-caption-container {
+ display: block;
+ background-color: #f4f4f4;
+ min-width: 100%;
+ color: #000;
+ overflow: hidden;
+ }
+
+ .PlaygroundEditorTheme__tableScrollableWrapper {
+ overflow-x: auto;
+ margin: 0 25px 30px 0;
+ }
+
+ .PlaygroundEditorTheme__tableScrollableWrapper
+ > .PlaygroundEditorTheme__table {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ .PlaygroundEditorTheme__tableAlignmentCenter {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .PlaygroundEditorTheme__tableAlignmentRight {
+ margin-left: auto;
+ }
+
+ .PlaygroundEditorTheme__table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ overflow-y: scroll;
+ overflow-x: scroll;
+ table-layout: fixed;
+ width: fit-content;
+ margin-top: 25px;
+ margin-bottom: 30px;
+ }
+
+ .PlaygroundEditorTheme__tableScrollableWrapper.PlaygroundEditorTheme__tableFrozenRow {
+ overflow-x: clip;
+ }
+
+ .PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > td {
+ overflow: clip;
+ background-color: #fff;
+ position: sticky;
+ z-index: 2;
+ top: 44px;
+ }
+
+ .PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > th {
+ overflow: clip;
+ background-color: #f2f3f5;
+ position: sticky;
+ z-index: 2;
+ top: 44px;
+ }
+
+ .PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > th:after,
+ .PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > td:after {
+ content: '';
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ border-bottom: 1px solid #bbb;
+ }
+
+ .PlaygroundEditorTheme__tableFrozenColumn tr > td:first-child {
+ background-color: #fff;
+ position: sticky;
+ z-index: 2;
+ left: 0;
+ }
+
+ .PlaygroundEditorTheme__tableFrozenColumn tr > th:first-child {
+ background-color: #f2f3f5;
+ position: sticky;
+ z-index: 2;
+ left: 0;
+ }
+
+ .PlaygroundEditorTheme__tableFrozenColumn tr > :first-child:after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ height: 100%;
+ border-right: 1px solid #bbb;
+ }
+
+ .PlaygroundEditorTheme__tableRowStriping tr:nth-child(2n),
+ .PlaygroundEditorTheme__tableFrozenColumn
+ .PlaygroundEditorTheme__table.PlaygroundEditorTheme__tableRowStriping
+ tr:nth-child(2n)
+ > td:first-child {
+ background-color: #f2f5fb;
+ }
+
+ .PlaygroundEditorTheme__tableSelection *::selection {
+ background-color: transparent;
+ }
+
+ .PlaygroundEditorTheme__tableSelected {
+ outline: 2px solid rgb(60, 132, 244);
+ }
+
+ .PlaygroundEditorTheme__tableCell {
+ border: 1px solid #bbb;
+ width: 75px;
+ vertical-align: top;
+ text-align: start;
+ padding: 6px 8px;
+ position: relative;
+ outline: none;
+ overflow: auto;
+ }
+
+ .PlaygroundEditorTheme__tableCell > * {
+ overflow: inherit;
+ }
+
+ .PlaygroundEditorTheme__tableCellResizer {
+ position: absolute;
+ right: -4px;
+ height: 100%;
+ width: 8px;
+ cursor: ew-resize;
+ z-index: 10;
+ top: 0;
+ }
+
+ .PlaygroundEditorTheme__tableCellHeader {
+ background-color: #f2f3f5;
+ text-align: start;
+ }
+
+ .PlaygroundEditorTheme__tableCellSelected {
+ caret-color: transparent;
+ }
+
+ .PlaygroundEditorTheme__tableCellSelected:after {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ background-color: highlight;
+ mix-blend-mode: multiply;
+ content: '';
+ pointer-events: none;
+ }
+
+ .PlaygroundEditorTheme__tableAddColumns {
+ position: absolute;
+ background-color: #eee;
+ height: 100%;
+ animation: table-controls 0.2s ease;
+ border: 0;
+ cursor: pointer;
+ }
+
+ .PlaygroundEditorTheme__tableAddColumns:after {
+ background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-plus'%3e%3cpath%20d='M8%204a.5.5%200%200%201%20.5.5v3h3a.5.5%200%200%201%200%201h-3v3a.5.5%200%200%201-1%200v-3h-3a.5.5%200%200%201%200-1h3v-3A.5.5%200%200%201%208%204z'/%3e%3c/svg%3e");
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ display: block;
+ content: ' ';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0.4;
+ }
+
+ .PlaygroundEditorTheme__tableAddColumns:hover,
+ .PlaygroundEditorTheme__tableAddRows:hover {
+ background-color: #c9dbf0;
+ }
+
+ .PlaygroundEditorTheme__tableAddRows {
+ position: absolute;
+ width: calc(100% - 25px);
+ background-color: #eee;
+ animation: table-controls 0.2s ease;
+ border: 0;
+ cursor: pointer;
+ }
+
+ .PlaygroundEditorTheme__tableAddRows:after {
+ background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-plus'%3e%3cpath%20d='M8%204a.5.5%200%200%201%20.5.5v3h3a.5.5%200%200%201%200%201h-3v3a.5.5%200%200%201-1%200v-3h-3a.5.5%200%200%201%200-1h3v-3A.5.5%200%200%201%208%204z'/%3e%3c/svg%3e");
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ display: block;
+ content: ' ';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0.4;
+ }
+
+ @keyframes table-controls {
+ 0% {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+ }
+
+ .PlaygroundEditorTheme__tableCellResizeRuler {
+ display: block;
+ position: absolute;
+ width: 1px;
+ background-color: #3c84f4;
+ height: 100%;
+ top: 0;
+ }
+
+ .PlaygroundEditorTheme__tableCellActionButtonContainer {
+ display: block;
+ right: 5px;
+ top: 6px;
+ position: absolute;
+ z-index: 4;
+ width: 20px;
+ height: 20px;
+ }
+
+ .PlaygroundEditorTheme__tableCellActionButton {
+ background-color: #eee;
+ display: block;
+ border: 0;
+ border-radius: 20px;
+ width: 20px;
+ height: 20px;
+ color: #222;
+ cursor: pointer;
+ }
+
+ .PlaygroundEditorTheme__tableCellActionButton:hover {
+ background-color: #ddd;
+ }
+
+ .table-cell-action-button-container {
+ position: absolute;
+ z-index: 3;
+ top: 0;
+ left: 0;
+ will-change: transform;
+ }
+
+ .table-cell-action-button-container.table-cell-action-button-container--active {
+ pointer-events: auto;
+ opacity: 1;
+ }
+
+ .table-cell-action-button-container.table-cell-action-button-container--inactive {
+ pointer-events: none;
+ opacity: 0;
+ }
+
+ .table-cell-action-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 0;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ border-radius: 15px;
+ color: #222;
+ display: inline-block;
+ cursor: pointer;
+ }
+
+ i.chevron-down {
+ background-color: transparent;
+ background-size: contain;
+ display: inline-block;
+ height: 8px;
+ width: 8px;
+ background-image: url(/images/rich-text-lexical/icons/chevron-down.svg);
+ }
+
+ .action-button {
+ background-color: #eee;
+ border: 0;
+ padding: 8px 12px;
+ position: relative;
+ margin-left: 5px;
+ border-radius: 15px;
+ color: #222;
+ display: inline-block;
+ cursor: pointer;
+ }
+
+ .action-button:hover {
+ background-color: #ddd;
+ color: #000;
+ }
+
+ .action-button-mic.active {
+ animation: mic-pulsate-color 3s infinite;
+ }
+
+ button.action-button:disabled {
+ opacity: 0.6;
+ background: #eee;
+ cursor: not-allowed;
+ }
+
+ .PlaygroundEditorTheme__layoutContainer {
+ display: grid;
+ gap: 10px;
+ margin: 10px 0;
+ }
+
+ .PlaygroundEditorTheme__layoutItem {
+ border: 1px dashed #ddd;
+ padding: 8px 16px;
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ .PlaygroundEditorTheme__autocomplete {
+ color: #ccc;
+ }
+
+ .PlaygroundEditorTheme__textItalic {
+ font-style: italic;
+ }
+
+ .PlaygroundEditorTheme__textUnderline {
+ text-decoration: underline;
+ }
+
+ .PlaygroundEditorTheme__textStrikethrough {
+ text-decoration: line-through;
+ }
+
+ .PlaygroundEditorTheme__textUnderlineStrikethrough {
+ text-decoration: underline line-through;
+ }
+
+ .PlaygroundEditorTheme__textCode {
+ background-color: rgb(240, 242, 245);
+ padding: 1px 0.25rem;
+ font-family: Menlo, Consolas, Monaco, monospace;
+ font-size: 94%;
+ }
+
+ .PlaygroundEditorTheme__link {
+ color: rgb(33, 111, 219);
+ text-decoration: none;
+ }
+ .PlaygroundEditorTheme__link:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+`;
diff --git a/src/components/rich-text-lexical/nodes/AutocompleteNode.tsx b/src/components/rich-text-lexical/nodes/AutocompleteNode.tsx
new file mode 100644
index 0000000000..6ba2542ea3
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/AutocompleteNode.tsx
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { TextNode } from 'lexical';
+
+import { uuid as UUID } from '../plugins/AutocompletePlugin';
+import type {
+ DOMExportOutput,
+ EditorConfig,
+ LexicalEditor,
+ NodeKey,
+ SerializedTextNode,
+ Spread,
+} from 'lexical';
+
+export type SerializedAutocompleteNode = Spread<
+ {
+ uuid: string;
+ },
+ SerializedTextNode
+>;
+
+export class AutocompleteNode extends TextNode {
+ /**
+ * A unique uuid is generated for each session and assigned to the instance.
+ * This helps to:
+ * - Ensures max one Autocomplete node per session.
+ * - Ensure that when collaboration is enabled, this node is not shown in
+ * other sessions.
+ * See https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx
+ */
+ __uuid: string;
+
+ static clone(node: AutocompleteNode): AutocompleteNode {
+ return new AutocompleteNode(node.__text, node.__uuid, node.__key);
+ }
+
+ static getType(): 'autocomplete' {
+ return 'autocomplete';
+ }
+
+ static importDOM() {
+ // Never import from DOM
+ return null;
+ }
+
+ static importJSON(
+ serializedNode: SerializedAutocompleteNode,
+ ): AutocompleteNode {
+ return $createAutocompleteNode(
+ serializedNode.text,
+ serializedNode.uuid,
+ ).updateFromJSON(serializedNode);
+ }
+
+ exportJSON(): SerializedAutocompleteNode {
+ return {
+ ...super.exportJSON(),
+ uuid: this.__uuid,
+ };
+ }
+
+ constructor(text: string, uuid: string, key?: NodeKey) {
+ super(text, key);
+ this.__uuid = uuid;
+ }
+
+ updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
+ return false;
+ }
+
+ exportDOM(_: LexicalEditor): DOMExportOutput {
+ return { element: null };
+ }
+
+ excludeFromCopy() {
+ return true;
+ }
+
+ createDOM(config: EditorConfig): HTMLElement {
+ const dom = super.createDOM(config);
+ dom.classList.add(config.theme.autocomplete);
+ if (this.__uuid !== UUID) {
+ dom.style.display = 'none';
+ }
+ return dom;
+ }
+}
+
+export function $createAutocompleteNode(
+ text: string,
+ uuid: string,
+): AutocompleteNode {
+ return new AutocompleteNode(text, uuid).setMode('token');
+}
diff --git a/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeComponent.tsx b/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeComponent.tsx
new file mode 100644
index 0000000000..dac1d50418
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeComponent.tsx
@@ -0,0 +1,239 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+// eslint-disable-next-line import/order
+import type { JSX } from 'react';
+
+import 'react-day-picker/style.css';
+
+import {
+ autoUpdate,
+ flip,
+ FloatingFocusManager,
+ FloatingOverlay,
+ FloatingPortal,
+ offset,
+ shift,
+ useDismiss,
+ useFloating,
+ useInteractions,
+ useRole,
+} from '@floating-ui/react';
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
+import { setHours, setMinutes } from 'date-fns';
+import { $getNodeByKey, NodeKey } from 'lexical';
+import * as React from 'react';
+import { useEffect, useRef, useState } from 'react';
+import { DayPicker } from 'react-day-picker';
+
+import { $isDateTimeNode, type DateTimeNode } from './DateTimeNode';
+
+const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+export default function DateTimeComponent({
+ dateTime,
+ nodeKey,
+}: {
+ dateTime: Date | undefined;
+ nodeKey: NodeKey;
+}): JSX.Element {
+ const [editor] = useLexicalComposerContext();
+ const [isOpen, setIsOpen] = useState(false);
+ const ref = useRef(null);
+ const [selected, setSelected] = useState(dateTime);
+ const [includeTime, setIncludeTime] = useState(false);
+ const [timeValue, setTimeValue] = useState('00:00');
+ const [isNodeSelected, setNodeSelected, clearNodeSelection] =
+ useLexicalNodeSelection(nodeKey);
+
+ const { refs, floatingStyles, context } = useFloating({
+ elements: {
+ reference: ref.current,
+ },
+ middleware: [
+ offset(5),
+ flip({
+ fallbackPlacements: ['top-start'],
+ }),
+ shift({ padding: 10 }),
+ ],
+ onOpenChange: setIsOpen,
+ open: isOpen,
+ placement: 'bottom-start',
+ strategy: 'fixed',
+ whileElementsMounted: autoUpdate,
+ });
+
+ const role = useRole(context, { role: 'dialog' });
+ const dismiss = useDismiss(context);
+
+ const { getFloatingProps } = useInteractions([role, dismiss]);
+
+ useEffect(() => {
+ const dateTimePillRef = ref.current as HTMLElement | null;
+ function onClick(e: MouseEvent) {
+ e.preventDefault();
+ setIsOpen(true);
+ }
+
+ if (dateTimePillRef) {
+ dateTimePillRef.addEventListener('click', onClick);
+ }
+
+ return () => {
+ if (dateTimePillRef) {
+ dateTimePillRef.removeEventListener('click', onClick);
+ }
+ };
+ }, [refs, editor]);
+
+ const withDateTimeNode = (
+ cb: (node: DateTimeNode) => void,
+ onUpdate?: () => void,
+ ): void => {
+ editor.update(
+ () => {
+ const node = $getNodeByKey(nodeKey);
+ if ($isDateTimeNode(node)) {
+ cb(node);
+ }
+ },
+ { onUpdate },
+ );
+ };
+
+ const handleCheckboxChange = (e: React.ChangeEvent) => {
+ withDateTimeNode(node => {
+ if (e.target.checked) {
+ setIncludeTime(true);
+ } else {
+ if (selected) {
+ const newSelectedDate = setHours(
+ setMinutes(selected, 0),
+ 0,
+ );
+ node.setDateTime(newSelectedDate);
+ }
+ setIncludeTime(false);
+ setTimeValue('00:00');
+ }
+ });
+ };
+
+ const handleTimeChange = (e: React.ChangeEvent) => {
+ withDateTimeNode(node => {
+ const time = e.target.value;
+ if (!selected) {
+ setTimeValue(time);
+ return;
+ }
+ const [hours, minutes] = time
+ .split(':')
+ .map((str: string) => parseInt(str, 10));
+ const newSelectedDate = setHours(
+ setMinutes(selected, minutes),
+ hours,
+ );
+ setSelected(newSelectedDate);
+ node.setDateTime(newSelectedDate);
+ setTimeValue(time);
+ });
+ };
+
+ const handleDaySelect = (date: Date | undefined) => {
+ withDateTimeNode(node => {
+ if (!timeValue || !date) {
+ setSelected(date);
+ return;
+ }
+ const [hours, minutes] = timeValue
+ .split(':')
+ .map(str => parseInt(str, 10));
+ const newDate = new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ hours,
+ minutes,
+ );
+ node.setDateTime(newDate);
+ setSelected(newDate);
+ });
+ };
+
+ return (
+
+ {dateTime?.toDateString() + (includeTime ? ' ' + timeValue : '') ||
+ 'Invalid Date'}
+ {isOpen && (
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeNode.module.css b/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeNode.module.css
new file mode 100644
index 0000000000..296a7a1a81
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeNode.module.css
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ *
+ */
+
+.dateTimePill {
+ background: #ddd;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 0px 4px 0px 4px;
+}
+.dateTimePill.selected {
+ outline: 2px solid rgb(60, 132, 244);
+}
+.dateTimePill:hover {
+ background: #f2f2f2;
+}
+.dateTimePicker {
+ background: #fff;
+ border: 1px solid #ddd;
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ padding: 0px 6px 0px 10px;
+}
+/* https://daypicker.dev/docs/styling#css-variables */
+.rdp-root {
+ --rdp-accent-color: #76b6ff;
+ --rdp-accent-background-color: #f0f0f0;
+}
diff --git a/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeNode.tsx b/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeNode.tsx
new file mode 100644
index 0000000000..e3f472cd31
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/DateTimeNode/DateTimeNode.tsx
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import {
+ $getState,
+ $setState,
+ buildImportMap,
+ createState,
+ DecoratorNode,
+ DOMConversionOutput,
+ DOMExportOutput,
+ LexicalNode,
+ SerializedLexicalNode,
+ Spread,
+ StateConfigValue,
+ StateValueOrUpdater,
+} from 'lexical';
+import * as React from 'react';
+import type { JSX } from 'react';
+
+const DateTimeComponent = React.lazy(() => import('./DateTimeComponent'));
+
+const getDateTimeText = (dateTime: Date) => {
+ if (dateTime === undefined) {
+ return '';
+ }
+ const hours = dateTime?.getHours();
+ const minutes = dateTime?.getMinutes();
+ return (
+ dateTime.toDateString() +
+ (hours === 0 && minutes === 0
+ ? ''
+ : ` ${hours.toString().padStart(2, '0')}:${minutes
+ .toString()
+ .padStart(2, '0')}`)
+ );
+};
+
+export type SerializedDateTimeNode = Spread<
+ {
+ dateTime?: string;
+ },
+ SerializedLexicalNode
+>;
+
+function $convertDateTimeElement(
+ domNode: HTMLElement,
+): DOMConversionOutput | null {
+ const dateTimeValue = domNode.getAttribute('data-lexical-datetime');
+ if (dateTimeValue) {
+ const node = $createDateTimeNode(new Date(Date.parse(dateTimeValue)));
+ return { node };
+ }
+ return null;
+}
+
+const dateTimeState = createState('dateTime', {
+ parse: v => new Date(v as string),
+ unparse: v => v.toISOString(),
+});
+
+export class DateTimeNode extends DecoratorNode {
+ $config() {
+ return this.config('datetime', {
+ extends: DecoratorNode,
+ importDOM: buildImportMap({
+ span: domNode =>
+ domNode.getAttribute('data-lexical-datetime') !== null
+ ? {
+ conversion: $convertDateTimeElement,
+ priority: 2,
+ }
+ : null,
+ }),
+ stateConfigs: [{ flat: true, stateConfig: dateTimeState }],
+ });
+ }
+
+ getDateTime(): StateConfigValue {
+ return $getState(this, dateTimeState);
+ }
+
+ setDateTime(
+ valueOrUpdater: StateValueOrUpdater,
+ ): this {
+ return $setState(this, dateTimeState, valueOrUpdater);
+ }
+
+ getTextContent(): string {
+ const dateTime = this.getDateTime();
+ return getDateTimeText(dateTime);
+ }
+
+ exportDOM(): DOMExportOutput {
+ const element = document.createElement('span');
+ element.textContent = getDateTimeText(this.getDateTime());
+ element.setAttribute(
+ 'data-lexical-datetime',
+ this.getDateTime()?.toString() || '',
+ );
+ return { element };
+ }
+
+ createDOM(): HTMLElement {
+ const element = document.createElement('span');
+ element.setAttribute(
+ 'data-lexical-datetime',
+ this.getDateTime()?.toString() || '',
+ );
+ element.style.display = 'inline-block';
+ return element;
+ }
+
+ updateDOM(): false {
+ return false;
+ }
+
+ isInline(): boolean {
+ return true;
+ }
+
+ decorate(): JSX.Element {
+ return (
+
+ );
+ }
+}
+
+export function $createDateTimeNode(dateTime: Date): DateTimeNode {
+ return new DateTimeNode().setDateTime(dateTime);
+}
+
+export function $isDateTimeNode(
+ node: LexicalNode | null | undefined,
+): node is DateTimeNode {
+ return node instanceof DateTimeNode;
+}
diff --git a/src/components/rich-text-lexical/nodes/EmojiNode.tsx b/src/components/rich-text-lexical/nodes/EmojiNode.tsx
new file mode 100644
index 0000000000..4fcdd09217
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/EmojiNode.tsx
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { $applyNodeReplacement, TextNode } from 'lexical';
+import type {
+ EditorConfig,
+ LexicalNode,
+ NodeKey,
+ SerializedTextNode,
+ Spread,
+} from 'lexical';
+
+export type SerializedEmojiNode = Spread<
+ {
+ className: string;
+ },
+ SerializedTextNode
+>;
+
+export class EmojiNode extends TextNode {
+ __className: string;
+
+ static getType(): string {
+ return 'emoji';
+ }
+
+ static clone(node: EmojiNode): EmojiNode {
+ return new EmojiNode(node.__className, node.__text, node.__key);
+ }
+
+ constructor(className: string, text: string, key?: NodeKey) {
+ super(text, key);
+ this.__className = className;
+ }
+
+ createDOM(config: EditorConfig): HTMLElement {
+ const dom = document.createElement('span');
+ const inner = super.createDOM(config);
+ dom.className = this.__className;
+ inner.className = 'emoji-inner';
+ dom.appendChild(inner);
+ return dom;
+ }
+
+ updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
+ const inner = dom.firstChild;
+ if (inner === null) {
+ return true;
+ }
+ super.updateDOM(prevNode, inner as HTMLElement, config);
+ return false;
+ }
+
+ static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
+ return $createEmojiNode(
+ serializedNode.className,
+ serializedNode.text,
+ ).updateFromJSON(serializedNode);
+ }
+
+ exportJSON(): SerializedEmojiNode {
+ return {
+ ...super.exportJSON(),
+ className: this.getClassName(),
+ };
+ }
+
+ getClassName(): string {
+ const self = this.getLatest();
+ return self.__className;
+ }
+}
+
+export function $isEmojiNode(
+ node: LexicalNode | null | undefined,
+): node is EmojiNode {
+ return node instanceof EmojiNode;
+}
+
+export function $createEmojiNode(
+ className: string,
+ emojiText: string,
+): EmojiNode {
+ const node = new EmojiNode(className, emojiText).setMode('token');
+ return $applyNodeReplacement(node);
+}
diff --git a/src/components/rich-text-lexical/nodes/EquationComponent.tsx b/src/components/rich-text-lexical/nodes/EquationComponent.tsx
new file mode 100644
index 0000000000..19e8a3e039
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/EquationComponent.tsx
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
+import { mergeRegister } from '@lexical/utils';
+import {
+ $getNodeByKey,
+ $getSelection,
+ $isNodeSelection,
+ COMMAND_PRIORITY_HIGH,
+ KEY_ESCAPE_COMMAND,
+ NodeKey,
+ SELECTION_CHANGE_COMMAND,
+} from 'lexical';
+import * as React from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+import EquationEditor from '../ui/EquationEditor';
+import KatexRenderer from '../ui/KatexRenderer';
+import { $isEquationNode } from './EquationNode';
+import type { JSX } from 'react';
+
+type EquationComponentProps = {
+ equation: string;
+ inline: boolean;
+ nodeKey: NodeKey;
+};
+
+export default function EquationComponent({
+ equation,
+ inline,
+ nodeKey,
+}: EquationComponentProps): JSX.Element {
+ const [editor] = useLexicalComposerContext();
+ const isEditable = useLexicalEditable();
+ const [equationValue, setEquationValue] = useState(equation);
+ const [showEquationEditor, setShowEquationEditor] =
+ useState(false);
+ const inputRef = useRef(null);
+
+ const onHide = useCallback(
+ (restoreSelection?: boolean) => {
+ setShowEquationEditor(false);
+ editor.update(() => {
+ const node = $getNodeByKey(nodeKey);
+ if ($isEquationNode(node)) {
+ node.setEquation(equationValue);
+ if (restoreSelection) {
+ node.selectNext(0, 0);
+ }
+ }
+ });
+ },
+ [editor, equationValue, nodeKey],
+ );
+
+ useEffect(() => {
+ if (!showEquationEditor && equationValue !== equation) {
+ setEquationValue(equation);
+ }
+ }, [showEquationEditor, equation, equationValue]);
+
+ useEffect(() => {
+ if (!isEditable) {
+ return;
+ }
+ if (showEquationEditor) {
+ return mergeRegister(
+ editor.registerCommand(
+ SELECTION_CHANGE_COMMAND,
+ payload => {
+ const activeElement = document.activeElement;
+ const inputElem = inputRef.current;
+ if (inputElem !== activeElement) {
+ onHide();
+ }
+ return false;
+ },
+ COMMAND_PRIORITY_HIGH,
+ ),
+ editor.registerCommand(
+ KEY_ESCAPE_COMMAND,
+ payload => {
+ const activeElement = document.activeElement;
+ const inputElem = inputRef.current;
+ if (inputElem === activeElement) {
+ onHide(true);
+ return true;
+ }
+ return false;
+ },
+ COMMAND_PRIORITY_HIGH,
+ ),
+ );
+ } else {
+ return editor.registerUpdateListener(({ editorState }) => {
+ const isSelected = editorState.read(() => {
+ const selection = $getSelection();
+ return (
+ $isNodeSelection(selection) &&
+ selection.has(nodeKey) &&
+ selection.getNodes().length === 1
+ );
+ });
+ if (isSelected) {
+ setShowEquationEditor(true);
+ }
+ });
+ }
+ }, [editor, nodeKey, onHide, showEquationEditor, isEditable]);
+
+ return (
+ <>
+ {showEquationEditor && isEditable ? (
+
+ ) : (
+ editor._onError(e)}
+ fallback={null}
+ >
+ {
+ if (isEditable) {
+ setShowEquationEditor(true);
+ }
+ }}
+ />
+
+ )}
+ >
+ );
+}
diff --git a/src/components/rich-text-lexical/nodes/EquationNode.tsx b/src/components/rich-text-lexical/nodes/EquationNode.tsx
new file mode 100644
index 0000000000..134096acc5
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/EquationNode.tsx
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import katex from 'katex';
+import { $applyNodeReplacement, DecoratorNode, DOMExportOutput } from 'lexical';
+import * as React from 'react';
+import type { JSX } from 'react';
+import type {
+ DOMConversionMap,
+ DOMConversionOutput,
+ EditorConfig,
+ LexicalNode,
+ NodeKey,
+ SerializedLexicalNode,
+ Spread,
+} from 'lexical';
+
+const EquationComponent = React.lazy(() => import('./EquationComponent'));
+
+export type SerializedEquationNode = Spread<
+ {
+ equation: string;
+ inline: boolean;
+ },
+ SerializedLexicalNode
+>;
+
+function $convertEquationElement(
+ domNode: HTMLElement,
+): null | DOMConversionOutput {
+ let equation = domNode.getAttribute('data-lexical-equation');
+ const inline = domNode.getAttribute('data-lexical-inline') === 'true';
+ // Decode the equation from base64
+ equation = atob(equation || '');
+ if (equation) {
+ const node = $createEquationNode(equation, inline);
+ return { node };
+ }
+
+ return null;
+}
+
+export class EquationNode extends DecoratorNode {
+ __equation: string;
+ __inline: boolean;
+
+ static getType(): string {
+ return 'equation';
+ }
+
+ static clone(node: EquationNode): EquationNode {
+ return new EquationNode(node.__equation, node.__inline, node.__key);
+ }
+
+ constructor(equation: string, inline?: boolean, key?: NodeKey) {
+ super(key);
+ this.__equation = equation;
+ this.__inline = inline ?? false;
+ }
+
+ static importJSON(serializedNode: SerializedEquationNode): EquationNode {
+ return $createEquationNode(
+ serializedNode.equation,
+ serializedNode.inline,
+ ).updateFromJSON(serializedNode);
+ }
+
+ exportJSON(): SerializedEquationNode {
+ return {
+ ...super.exportJSON(),
+ equation: this.getEquation(),
+ inline: this.__inline,
+ };
+ }
+
+ createDOM(_config: EditorConfig): HTMLElement {
+ const element = document.createElement(this.__inline ? 'span' : 'div');
+ // EquationNodes should implement `user-action:none` in their CSS to avoid issues with deletion on Android.
+ element.className = 'editor-equation';
+ return element;
+ }
+
+ exportDOM(): DOMExportOutput {
+ const element = document.createElement(this.__inline ? 'span' : 'div');
+ // Encode the equation as base64 to avoid issues with special characters
+ const equation = btoa(this.__equation);
+ element.setAttribute('data-lexical-equation', equation);
+ element.setAttribute('data-lexical-inline', `${this.__inline}`);
+ katex.render(this.__equation, element, {
+ displayMode: !this.__inline, // true === block display //
+ errorColor: '#cc0000',
+ output: 'html',
+ strict: 'warn',
+ throwOnError: false,
+ trust: false,
+ });
+ return { element };
+ }
+
+ static importDOM(): DOMConversionMap | null {
+ return {
+ div: (domNode: HTMLElement) => {
+ if (!domNode.hasAttribute('data-lexical-equation')) {
+ return null;
+ }
+ return {
+ conversion: $convertEquationElement,
+ priority: 2,
+ };
+ },
+ span: (domNode: HTMLElement) => {
+ if (!domNode.hasAttribute('data-lexical-equation')) {
+ return null;
+ }
+ return {
+ conversion: $convertEquationElement,
+ priority: 1,
+ };
+ },
+ };
+ }
+
+ updateDOM(prevNode: this): boolean {
+ // If the inline property changes, replace the element
+ return this.__inline !== prevNode.__inline;
+ }
+
+ getTextContent(): string {
+ return this.__equation;
+ }
+
+ getEquation(): string {
+ return this.__equation;
+ }
+
+ setEquation(equation: string): void {
+ const writable = this.getWritable();
+ writable.__equation = equation;
+ }
+
+ decorate(): JSX.Element {
+ return (
+
+ );
+ }
+}
+
+export function $createEquationNode(
+ equation = '',
+ inline = false,
+): EquationNode {
+ const equationNode = new EquationNode(equation, inline);
+ return $applyNodeReplacement(equationNode);
+}
+
+export function $isEquationNode(
+ node: LexicalNode | null | undefined,
+): node is EquationNode {
+ return node instanceof EquationNode;
+}
diff --git a/src/components/rich-text-lexical/nodes/ExcalidrawNode/ExcalidrawComponent.tsx b/src/components/rich-text-lexical/nodes/ExcalidrawNode/ExcalidrawComponent.tsx
new file mode 100644
index 0000000000..58b2e9871d
--- /dev/null
+++ b/src/components/rich-text-lexical/nodes/ExcalidrawNode/ExcalidrawComponent.tsx
@@ -0,0 +1,239 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
+import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
+import { mergeRegister } from '@lexical/utils';
+import {
+ $getNodeByKey,
+ CLICK_COMMAND,
+ COMMAND_PRIORITY_LOW,
+ isDOMNode,
+} from 'lexical';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import * as React from 'react';
+
+import ExcalidrawModal from '../../ui/ExcalidrawModal';
+import ImageResizer from '../../ui/ImageResizer';
+import ExcalidrawImage from './ExcalidrawImage';
+import { $isExcalidrawNode } from '.';
+import type { JSX } from 'react';
+import type { NodeKey } from 'lexical';
+import type {
+ AppState,
+ BinaryFiles,
+} from '@excalidraw/excalidraw/dist/types/excalidraw/types';
+import type { ExcalidrawInitialElements } from '../../ui/ExcalidrawModal';
+
+export default function ExcalidrawComponent({
+ nodeKey,
+ data,
+ width,
+ height,
+}: {
+ data: string;
+ nodeKey: NodeKey;
+ width: 'inherit' | number;
+ height: 'inherit' | number;
+}): JSX.Element {
+ const [editor] = useLexicalComposerContext();
+ const isEditable = useLexicalEditable();
+ const [isModalOpen, setModalOpen] = useState(
+ data === '[]' && editor.isEditable(),
+ );
+ const imageContainerRef = useRef(null);
+ const buttonRef = useRef(null);
+ const captionButtonRef = useRef(null);
+ const [isSelected, setSelected, clearSelection] =
+ useLexicalNodeSelection(nodeKey);
+ const [isResizing, setIsResizing] = useState(false);
+
+ useEffect(() => {
+ if (!isEditable) {
+ if (isSelected) {
+ clearSelection();
+ }
+ return;
+ }
+ return mergeRegister(
+ editor.registerCommand(
+ CLICK_COMMAND,
+ (event: MouseEvent) => {
+ const buttonElem = buttonRef.current;
+ const eventTarget = event.target;
+
+ if (isResizing) {
+ return true;
+ }
+
+ if (
+ buttonElem !== null &&
+ isDOMNode(eventTarget) &&
+ buttonElem.contains(eventTarget)
+ ) {
+ if (!event.shiftKey) {
+ clearSelection();
+ }
+ setSelected(!isSelected);
+ if (event.detail > 1) {
+ setModalOpen(true);
+ }
+ return true;
+ }
+
+ return false;
+ },
+ COMMAND_PRIORITY_LOW,
+ ),
+ );
+ }, [
+ clearSelection,
+ editor,
+ isSelected,
+ isResizing,
+ setSelected,
+ isEditable,
+ ]);
+
+ const deleteNode = useCallback(() => {
+ setModalOpen(false);
+ return editor.update(() => {
+ const node = $getNodeByKey(nodeKey);
+ if (node) {
+ node.remove();
+ }
+ });
+ }, [editor, nodeKey]);
+
+ const setData = (
+ els: ExcalidrawInitialElements,
+ aps: Partial,
+ fls: BinaryFiles,
+ ) => {
+ return editor.update(() => {
+ const node = $getNodeByKey(nodeKey);
+ if ($isExcalidrawNode(node)) {
+ if ((els && els.length > 0) || Object.keys(fls).length > 0) {
+ node.setData(
+ JSON.stringify({
+ appState: aps,
+ elements: els,
+ files: fls,
+ }),
+ );
+ } else {
+ node.remove();
+ }
+ }
+ });
+ };
+
+ const onResizeStart = () => {
+ setIsResizing(true);
+ };
+
+ const onResizeEnd = (
+ nextWidth: 'inherit' | number,
+ nextHeight: 'inherit' | number,
+ ) => {
+ // Delay hiding the resize bars for click case
+ setTimeout(() => {
+ setIsResizing(false);
+ }, 200);
+
+ editor.update(() => {
+ const node = $getNodeByKey(nodeKey);
+
+ if ($isExcalidrawNode(node)) {
+ node.setWidth(nextWidth);
+ node.setHeight(nextHeight);
+ }
+ });
+ };
+
+ const openModal = useCallback(() => {
+ setModalOpen(true);
+ }, []);
+
+ const {
+ elements = [],
+ files = {},
+ appState = {},
+ } = useMemo(() => JSON.parse(data), [data]);
+
+ const closeModal = useCallback(() => {
+ setModalOpen(false);
+ if (elements.length === 0) {
+ editor.update(() => {
+ const node = $getNodeByKey(nodeKey);
+ if (node) {
+ node.remove();
+ }
+ });
+ }
+ }, [editor, nodeKey, elements.length]);
+
+ return (
+ <>
+ {isEditable && isModalOpen && (
+ {
+ setData(els, aps, fls);
+ setModalOpen(false);
+ }}
+ closeOnClickOutside={false}
+ />
+ )}
+ {elements.length > 0 && (
+