diff --git a/apps/client/package.json b/apps/client/package.json index c2a3999a6..4535451ac 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -12,6 +12,8 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@fontsource/open-sans": "^5.0.28", + "@lexical/react": "^0.16.1", + "@lexical/rich-text": "^0.16.1", "@mantine/hooks": "^7.17.2", "@sentry/react": "^8.43.0", "@table-nav/react": "^0.0.7", @@ -34,7 +36,8 @@ "react-router-dom": "^6.3.0", "react-simple-code-editor": "^0.14.1", "web-vitals": "^3.1.1", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "lexical": "^0.16.1" }, "scripts": { "addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js", diff --git a/apps/client/src/common/components/input/rich-text-editor/RichTextEditor.tsx b/apps/client/src/common/components/input/rich-text-editor/RichTextEditor.tsx new file mode 100644 index 000000000..786a5a93a --- /dev/null +++ b/apps/client/src/common/components/input/rich-text-editor/RichTextEditor.tsx @@ -0,0 +1,310 @@ +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; +import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; +import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'; +import { ListPlugin } from '@lexical/react/LexicalListPlugin'; +import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'; +import { TRANSFORMERS } from '@lexical/markdown'; +import { $getRoot, $getSelection, EditorState, FORMAT_TEXT_COMMAND, LexicalEditor, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, $createParagraphNode, $createTextNode, $patchStyleText } from 'lexical'; +import { useEffect, useState, useCallback, useRef } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { mergeRegister } from '@lexical/utils'; +import { LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { ListItemNode, ListNode } from '@lexical/list'; +import { HeadingNode, QuoteNode } from '@lexical/rich-text'; +import { CodeNode, CodeHighlightNode } from '@lexical/code'; +import { TableNode, TableCellNode, TableRowNode } from '@lexical/table'; + +// Define initial editor configuration +const editorConfig = { + namespace: 'MyEditor', + theme: { + ltr: 'ltr', + rtl: 'rtl', + placeholder: 'editor-placeholder', + paragraph: 'editor-paragraph', + quote: 'editor-quote', + heading: { + h1: 'editor-heading-h1', + h2: 'editor-heading-h2', + h3: 'editor-heading-h3', + h4: 'editor-heading-h4', + h5: 'editor-heading-h5', + }, + list: { + nested: { + listitem: 'editor-nested-listitem', + }, + ol: 'editor-list-ol', + ul: 'editor-list-ul', + listitem: 'editor-listitem', + }, + link: 'editor-link', + text: { + bold: 'editor-text-bold', + italic: 'editor-text-italic', + underline: 'editor-text-underline', + strikethrough: 'editor-text-strikethrough', + underlineStrikethrough: 'editor-text-underlineStrikethrough', + code: 'editor-text-code', + }, + code: 'editor-code', + codeHighlight: { + atrule: 'editor-tokenAttr', + attr: 'editor-tokenAttr', + boolean: 'editor-tokenProperty', + builtin: 'editor-tokenSelector', + cdata: 'editor-tokenComment', + char: 'editor-tokenSelector', + clike: 'editor-tokenComment', + comment: 'editor-tokenComment', + contentType: 'editor-tokenComment', + coord: 'editor-tokenComment', + deleted: 'editor-tokenProperty', + doctype: 'editor-tokenComment', + doi: 'editor-tokenComment', + entity: 'editor-tokenOperator', + function: 'editor-tokenFunction', + important: 'editor-tokenVariable', + inserted: 'editor-tokenSelector', + keyword: 'editor-tokenAttr', + markup: 'editor-tokenComment', + merged: 'editor-tokenComment', + namespace: 'editor-tokenVariable', + number: 'editor-tokenProperty', + operator: 'editor-tokenOperator', + prolog: 'editor-tokenComment', + property: 'editor-tokenProperty', + punctuation: 'editor-tokenPunctuation', + regex: 'editor-tokenVariable', + selector: 'editor-tokenSelector', + string: 'editor-tokenSelector', + style: 'editor-tokenComment', + symbol: 'editor-tokenProperty', + tag: 'editor-tokenProperty', + url: 'editor-tokenOperator', + variable: 'editor-tokenVariable', + }, + }, + onError(error: Error) { + throw error; + }, + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + TableNode, + TableCellNode, + TableRowNode, + LinkNode, + ], +}; + +interface RichTextEditorProps { + initialValue?: string; + onChange?: (value: string) => void; +} + +export default function RichTextEditor({ initialValue, onChange }: RichTextEditorProps) { + const [isFocused, setIsFocused] = useState(false); + const editorRef = useRef(null); // Use ref for editor instance + const editorWrapperRef = useRef(null); // Ref for the entire editor + toolbar wrapper + + const handleEditorFocus = () => { + setIsFocused(true); + }; + + // This handleBlur is for the wrapper around the editor and toolbar + const handleWrapperBlur = (event: React.FocusEvent) => { + // Check if the new focused element is still within the editor wrapper + if (editorWrapperRef.current && !editorWrapperRef.current.contains(event.relatedTarget as Node)) { + setIsFocused(false); + } + }; + + const handleChange = useCallback((editorState: EditorState) => { + if (onChange) { + editorState.read(() => { + const root = $getRoot(); + // For now, sending plain text. Could be HTML or Lexical's format. + onChange(root.getTextContent()); + }); + } + }, [onChange]); + + // Placeholder for initial value loading + useEffect(() => { + const currentEditor = editorRef.current; + if (currentEditor && initialValue && isFocused) { // only update if focused and has initial value + currentEditor.update(() => { + const root = $getRoot(); + root.clear(); + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode(initialValue)); + root.append(paragraph); + root.selectEnd(); // Move cursor to the end + }); + } + }, [initialValue, isFocused]); // Rerun when isFocused changes or initialValue changes (though initialValue should be stable) + + + if (!isFocused) { + return ( +
+ {initialValue || 'Click to edit...'} +
+ ); + } + + // Prepare initial config for when the editor mounts + const localEditorConfig = { + ...editorConfig, + editorState: initialValue && !editorRef.current // Only set initial state if editor hasn't been initialized yet with a value + ? () => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode(initialValue)); + root.append(paragraph); + } + : undefined, + }; + + return ( +
+ + + +
{/* Container for ContentEditable and Placeholder */} + } + placeholder={
Enter text...
} + ErrorBoundary={LexicalErrorBoundary} + /> +
+ + + {/* LinkPlugin provides TOGGLE_LINK_COMMAND */} + + +
+
+ ); +} + +// Helper component to get editor instance and attach it to a ref +function EditorInitializer({ editorRef }: { editorRef: React.MutableRefObject }) { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + editorRef.current = editor; + return () => { + editorRef.current = null; + }; + }, [editor, editorRef]); + return null; +} + + +function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + // Add states for other formats if needed, e.g., selectedTextColor, selectedBgColor + + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + setIsBold(selection.hasFormat('bold')); + // Update link state + const node = selection.anchor.getNode(); + const parent = node.getParent(); + setIsLink(parent?.getType() === 'link' || node.getType() === 'link'); + // Could also get current text color/bg color here if needed + // const style = selection.style; + // setSelectedTextColor(style.color); + // setSelectedBgColor(style.backgroundColor); + } + }, [editor]); // editor dependency is implicit via useLexicalComposerContext + + useEffect(() => { + return mergeRegister( + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + updateToolbar(); + return false; + }, + COMMAND_PRIORITY_LOW, + ), + // Potentially other listeners here, e.g., for text format changes + ); + }, [editor, updateToolbar]); + + + const toggleBold = () => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'); + }; + + const toggleLink = useCallback(() => { + if (isLink) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); // Remove link + } else { + const url = prompt('Enter link URL:'); + if (url) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, url); + } + } + }, [editor, isLink]); + + return ( +
e.preventDefault()} // Prevent editor blur when clicking toolbar + > + + + + + {/* Add more buttons here */} +
+ ); +} + +function applyStyleToSelection(editor: LexicalEditor, styleKey: string, styleValue: string) { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + $patchStyleText(selection, { [styleKey]: styleValue }); + } + }); +} + +function applyTextColor() { + const [editor] = useLexicalComposerContext(); + const color = prompt('Enter text color (e.g., red, #FF0000):'); + if (color) { + applyStyleToSelection(editor, 'color', color); + } +} + +function applyBackgroundColor() { + const [editor] = useLexicalComposerContext(); + const color = prompt('Enter background color (e.g., yellow, #FFFF00):'); + if (color) { + // For background color, Lexical often uses 'highlight' format or direct background-color style + // Using FORMAT_TEXT_COMMAND for 'highlight' might be one way if a plugin handles it. + // Or, apply direct style: + applyStyleToSelection(editor, 'background-color', color); + } +} diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx b/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx index 2755244cc..888fca0f0 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx @@ -5,8 +5,8 @@ import { sanitiseCue } from 'ontime-utils'; import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect'; import * as Editor from '../../../editors/editor-utils/EditorUtils'; import { type EditorUpdateFields } from '../EventEditor'; +import RichTextEditor from '../../../../common/components/input/rich-text-editor/RichTextEditor'; -import EventTextArea from './EventTextArea'; import EventTextInput from './EventTextInput'; import style from '../EventEditor.module.scss'; @@ -49,7 +49,10 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => { - +
+ Note + handleSubmit('note', newNote)} /> +
); }; diff --git a/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx b/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx deleted file mode 100644 index 7f566e148..000000000 --- a/apps/client/src/features/rundown/event-editor/composite/EventTextArea.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { type CSSProperties, useCallback, useRef } from 'react'; - -import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea'; -import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput'; -import * as Editor from '../../../editors/editor-utils/EditorUtils'; -import { EditorUpdateFields } from '../EventEditor'; - -interface CountedTextAreaProps { - className?: string; - field: EditorUpdateFields; - label: string; - initialValue: string; - style?: CSSProperties; - submitHandler: (field: EditorUpdateFields, value: string) => void; -} - -export default function EventTextArea(props: CountedTextAreaProps) { - const { className, field, label, initialValue, style: givenStyles, submitHandler } = props; - const ref = useRef(null); - const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]); - - const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, { - submitOnCtrlEnter: true, - }); - - return ( -
- - {label} - - -
- ); -} diff --git a/e2e/tests/features/213-rich-text-editor.spec.ts b/e2e/tests/features/213-rich-text-editor.spec.ts new file mode 100644 index 000000000..46836d7c0 --- /dev/null +++ b/e2e/tests/features/213-rich-text-editor.spec.ts @@ -0,0 +1,206 @@ +import { test, expect } from '../../fixtures/override'; +import { TEST_USER_ID_ADMIN } from '../../../src/common/utils/testing'; +import { SEED_EVENT_A, SEED_PROJECT_A } from '../../../prisma/seed/standardSeed'; + +test.describe('[FEAT] Rich Text Editor in Event Note', () => { + test.beforeEach(async ({ page, loginByUserId }) => { + // Login as admin user (or any user who can edit events) + await loginByUserId(TEST_USER_ID_ADMIN); + // Navigate to the cuesheet for a known project + await page.goto(`/project/${SEED_PROJECT_A.id}/cuesheet`); + // Wait for cuesheet to be ready (e.g., for events to load) + await expect(page.locator('div[data-testid^="cue-row-"]')).toHaveCountGreaterThanOrEqual(1); + }); + + test('should allow rich text editing for event notes', async ({ page }) => { + // 1. Open an existing event to edit + // Assuming SEED_EVENT_A is visible on the cuesheet + const eventRowLocator = page.locator(`div[data-testid="cue-row-${SEED_EVENT_A.id}"]`); + await eventRowLocator.click(); + + // Wait for the event editor modal to appear + // Looking for a common element in the editor, e.g., the cue input or title input. + // The title input for SEED_EVENT_A has a specific test id pattern from other tests. + await expect(page.locator(`input[data-testid="input-textfield-cue-${SEED_EVENT_A.id}"]`)).toBeVisible(); + + // 2. Activate Rich Text Editor for 'Note' Field + // The note field is initially a placeholder div. + // Its value is SEED_EVENT_A.note + // The RichTextEditor component's placeholder div has: + // - onClick/onFocus to activate + // - style: border: '1px solid #ccc', padding: '10px', minHeight: '50px', cursor: 'text' + // - role="textbox" (added in tests for RTL, might be present) + // - aria-placeholder="Click to edit..." + // Let's try to locate it by its initial text content or a combination of properties. + // The label "Note" is above it. + const noteLabel = page.locator('label:has-text("Note")'); + const notePlaceholder = noteLabel.locator('+ div[style*="cursor: text"]'); // Sibling div with cursor: text style + + await expect(notePlaceholder).toBeVisible(); + if (SEED_EVENT_A.note) { + await expect(notePlaceholder).toHaveText(SEED_EVENT_A.note); + } else { + await expect(notePlaceholder).toHaveText('Click to edit...'); + } + + await notePlaceholder.click(); // Activate the editor + + // Verify the toolbar is now visible + // Toolbar buttons: Bold, Link, Color, BG Color + const boldButton = page.locator('button:has-text("B")'); // Simple locator for now + const linkButton = page.locator('button:has-text("Link")'); + const colorButton = page.locator('button:has-text("Color")'); + const bgColorButton = page.locator('button:has-text("BG Color")'); + + await expect(boldButton).toBeVisible(); + await expect(linkButton).toBeVisible(); + await expect(colorButton).toBeVisible(); + await expect(bgColorButton).toBeVisible(); + + // Locate the ContentEditable area (it's a div with style min-height: '150px') + const editorContentArea = page.locator('div[contenteditable="true"]'); + await expect(editorContentArea).toBeVisible(); + await expect(editorContentArea).toBeFocused(); + + // Clear existing note content (if any) to type fresh + // This can be done by selecting all and deleting, or Lexical's CLEAR_EDITOR_COMMAND + // For E2E, page.keyboard.press('Control+A') then page.keyboard.press('Delete') is common. + await editorContentArea.press('Control+A'); // or 'Meta+A' on macOS + await editorContentArea.press('Delete'); + await expect(editorContentArea).toHaveText(''); + + + // 3. Test Bold Functionality + const boldText = 'This text will be bold.'; + await editorContentArea.type(boldText); + await editorContentArea.selectText(); // Select the typed text + await boldButton.click(); + // Verify bold: Lexical usually wraps bold text in a span with font-weight: bold or a tag. + // Let's assume span with style for now. + await expect(editorContentArea.locator('span[style*="font-weight: bold;"]')).toHaveText(boldText); + // Or if it uses : await expect(editorContentArea.locator('strong')).toHaveText(boldText); + + // Clear for next test + await editorContentArea.press('Control+A'); + await editorContentArea.press('Delete'); + + // 4. Test Link Functionality + const linkText = 'This is a link'; + const testUrl = 'https://example.com'; + page.on('dialog', dialog => dialog.accept(testUrl)); // Handle prompt + await editorContentArea.type(linkText); + await editorContentArea.selectText(); + await linkButton.click(); + // Verify link: Check for an tag + const linkElement = editorContentArea.locator(`a[href="${testUrl}"]`); + await expect(linkElement).toBeVisible(); + await expect(linkElement).toHaveText(linkText); + page.off('dialog', () => {}); // Remove listener if it's a one-time thing per action + + // Clear for next test + await editorContentArea.press('Control+A'); + await editorContentArea.press('Delete'); + + // 5. Test Text Color Functionality + const colorText = 'This text is red'; + const redColor = 'rgb(255, 0, 0)'; + page.on('dialog', dialog => dialog.accept(redColor)); // Handle prompt + await editorContentArea.type(colorText); + await editorContentArea.selectText(); + await colorButton.click(); + // Verify text color + await expect(editorContentArea.locator(`span[style*="color: ${redColor};"]`)).toHaveText(colorText); + page.off('dialog', () => {}); + + // Clear for next test + await editorContentArea.press('Control+A'); + await editorContentArea.press('Delete'); + + // 6. Test Background Color Functionality + const bgColorText = 'This text has yellow background'; + const yellowBgColor = 'rgb(255, 255, 0)'; + page.on('dialog', dialog => dialog.accept(yellowBgColor)); // Handle prompt + await editorContentArea.type(bgColorText); + await editorContentArea.selectText(); + await bgColorButton.click(); + // Verify background color + await expect(editorContentArea.locator(`span[style*="background-color: ${yellowBgColor};"]`)).toHaveText(bgColorText); + page.off('dialog', () => {}); + + + // 7. Save and Verify + // Combine some formatting for the save test + await editorContentArea.press('Control+A'); + await editorContentArea.press('Delete'); + + const finalNoteText = "Final Note: Bold, Link, and Red."; + await editorContentArea.type(finalNoteText); + + // Apply Bold to "Bold" + await editorContentArea.press('Home'); // Go to start + for(let i=0; i < "Final Note: ".length; i++) await page.keyboard.press('ArrowRight'); // Move cursor + await page.keyboard.down('Shift'); + for(let i=0; i < "Bold".length; i++) await page.keyboard.press('ArrowRight'); // Select "Bold" + await page.keyboard.up('Shift'); + await boldButton.click(); + + // Apply Link to "Link" (re-register dialog handler) + page.on('dialog', dialog => dialog.accept(testUrl)); + await editorContentArea.press('End'); // Go to end + for(let i=0; i < ", and Red.".length; i++) await page.keyboard.press('ArrowLeft'); + await page.keyboard.down('Shift'); + for(let i=0; i < "Link".length; i++) await page.keyboard.press('ArrowLeft'); + await page.keyboard.up('Shift'); + await linkButton.click(); + page.off('dialog', () => {}); + + // Apply Red color to "Red" (re-register dialog handler) + page.on('dialog', dialog => dialog.accept(redColor)); + await editorContentArea.press('End'); + await page.keyboard.press('ArrowLeft'); // for the dot + await page.keyboard.down('Shift'); + for(let i=0; i < "Red".length; i++) await page.keyboard.press('ArrowLeft'); + await page.keyboard.up('Shift'); + await colorButton.click(); + page.off('dialog', () => {}); + + // Click save button in the event editor + // Assuming a common save button testid or text + const saveButton = page.locator('button:has-text("Save")'); // Or a more specific selector + await saveButton.click(); + + // Wait for modal to close (e.g., the save button is no longer visible) + await expect(saveButton).not.toBeVisible(); + // Or check that the cuesheet is active again + await expect(page.locator('div[data-testid^="cue-row-"]')).toHaveCountGreaterThanOrEqual(1); + + // Re-open the same event + await eventRowLocator.click(); + await expect(page.locator(`input[data-testid="input-textfield-cue-${SEED_EVENT_A.id}"]`)).toBeVisible(); // Wait for editor + + // Activate the note editor again + const persistedNotePlaceholder = noteLabel.locator('+ div[style*="cursor: text"]'); + await persistedNotePlaceholder.click(); + + const persistedEditorContentArea = page.locator('div[contenteditable="true"]'); + await expect(persistedEditorContentArea).toBeVisible(); + + // Verify persisted content + // This is the most complex part, as the exact HTML structure needs to be known. + // Example: Check for bold part + await expect(persistedEditorContentArea.locator('span[style*="font-weight: bold;"]')).toContainText("Bold"); + // Example: Check for link part + await expect(persistedEditorContentArea.locator(`a[href="${testUrl}"]`)).toContainText("Link"); + // Example: Check for colored part + await expect(persistedEditorContentArea.locator(`span[style*="color: ${redColor};"]`)).toContainText("Red"); + + // Overall text check might be too simple if HTML is involved, but good for a basic check + // await expect(persistedEditorContentArea).toContainText("Final Note: Bold, Link, and Red."); + // A more robust check would be to get the innerHTML and parse it or use snapshot testing. + + // Close the editor (e.g., click cancel or save again) + const cancelButton = page.locator('button:has-text("Cancel")'); // Or a more specific selector + await cancelButton.click(); + await expect(cancelButton).not.toBeVisible(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 634b1c35f..7b1f08088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,12 @@ importers: '@fontsource/open-sans': specifier: ^5.0.28 version: 5.0.28 + '@lexical/react': + specifier: ^0.16.1 + version: 0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.27) + '@lexical/rich-text': + specifier: ^0.16.1 + version: 0.16.1 '@mantine/hooks': specifier: ^7.17.2 version: 7.17.2(react@18.3.1) @@ -143,6 +149,9 @@ importers: framer-motion: specifier: ^10.10.0 version: 10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lexical: + specifier: ^0.16.1 + version: 0.16.1 prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -299,7 +308,7 @@ importers: version: 2.8.5 dotenv: specifier: ^16.0.1 - version: 16.3.1 + version: 16.5.0 express: specifier: 5.1.0 version: 5.1.0 @@ -1766,6 +1775,77 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lexical/clipboard@0.16.1': + resolution: {integrity: sha512-0dWs/SwKS5KPpuf6fUVVt9vSCl6HAqcDGhSITw/okv0rrIlXTUT6WhVsMJtXfFxTyVvwMeOecJHvQH3i/jRQtA==} + + '@lexical/code@0.16.1': + resolution: {integrity: sha512-pOC28rRZ2XkmI2nIJm50DbKaCJtk5D0o7r6nORYp4i0z+lxt5Sf2m82DL9ksUHJRqKy87pwJDpoWvJ2SAI0ohw==} + + '@lexical/devtools-core@0.16.1': + resolution: {integrity: sha512-8CvGERGL7ySDVGLU+YPeq+JupIXsOFlXa3EuJ88koLKqXxYenwMleZgGqayFp6lCP78xqPKnATVeoOZUt/NabQ==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/dragon@0.16.1': + resolution: {integrity: sha512-Rvd60GIYN5kpjjBumS34EnNbBaNsoseI0AlzOdtIV302jiHPCLH0noe9kxzu9nZy+MZmjZy8Dx2zTbQT2mueRw==} + + '@lexical/hashtag@0.16.1': + resolution: {integrity: sha512-G+YOxStAKs3q1utqm9KR4D4lCkwIH52Rctm4RgaVTI+4lvTaybeDRGFV75P/pI/qlF7/FvAYHTYEzCjtC3GNMQ==} + + '@lexical/history@0.16.1': + resolution: {integrity: sha512-WQhScx0TJeKSQAnEkRpIaWdUXqirrNrom2MxbBUc/32zEUMm9FzV7nRGknvUabEFUo7vZq6xTZpOExQJqHInQA==} + + '@lexical/html@0.16.1': + resolution: {integrity: sha512-vbtAdCvQ3PaAqa5mFmtmrvbiAvjCu1iXBAJ0bsHqFXCF2Sba5LwHVe8dUAOTpfEZEMbiHfjul6b5fj4vNPGF2A==} + + '@lexical/link@0.16.1': + resolution: {integrity: sha512-zG36gEnEqbIe6tK/MhXi7wn/XMY/zdivnPcOY5WyC3derkEezeLSSIFsC1u5UNeK5pbpNMSy4LDpLhi1Ww4Y5w==} + + '@lexical/list@0.16.1': + resolution: {integrity: sha512-i9YhLAh5N6YO9dP+R1SIL9WEdCKeTiQQYVUzj84vDvX5DIBxMPUjTmMn3LXu9T+QO3h1s2L/vJusZASrl45eAw==} + + '@lexical/mark@0.16.1': + resolution: {integrity: sha512-CZRGMLcxn5D+jzf1XnH+Z+uUugmpg1mBwTbGybCPm8UWpBrKDHkrscfMgWz62iRWz0cdVjM5+0zWpNElxFTRjQ==} + + '@lexical/markdown@0.16.1': + resolution: {integrity: sha512-0sBLttMvfQO/hVaIqpHdvDowpgV2CoRuWo2CNwvRLZPPWvPVjL4Nkb73wmi8zAZsAOTbX2aw+g4m/+k5oJqNig==} + + '@lexical/offset@0.16.1': + resolution: {integrity: sha512-/i2J04lQmFeydUZIF8tKXLQTXiJDTQ6GRnkfv1OpxU4amc0rwGa7+qAz/PuF1n58rP6InpLmSHxgY5JztXa2jw==} + + '@lexical/overflow@0.16.1': + resolution: {integrity: sha512-xh5YpoxwA7K4wgMQF/Sjl8sdjaxqesLCtH5ZrcMsaPlmucDIEEs+i8xxk+kDUTEY7y+3FvRxs4lGNgX8RVWkvQ==} + + '@lexical/plain-text@0.16.1': + resolution: {integrity: sha512-GjY4ylrBZIaAVIF8IFnmW0XGyHAuRmWA6gKB8iTTlsjgFrCHFIYC74EeJSp309O0Hflg9rRBnKoX1TYruFHVwA==} + + '@lexical/react@0.16.1': + resolution: {integrity: sha512-SsGgLt9iKfrrMRy9lFb6ROVPUYOgv6b+mCn9Al+TLqs/gBReDBi3msA7m526nrtBUKYUnjHdQ1QXIJzuKgOxcg==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/rich-text@0.16.1': + resolution: {integrity: sha512-4uEVXJur7tdSbqbmsToCW4YVm0AMh4y9LK077Yq2O9hSuA5dqpI8UbTDnxZN2D7RfahNvwlqp8eZKFB1yeiJGQ==} + + '@lexical/selection@0.16.1': + resolution: {integrity: sha512-+nK3RvXtyQvQDq7AZ46JpphmM33pwuulwiRfeXR5T9iFQTtgWOEjsAi/KKX7vGm70BxACfiSxy5QCOgBWFwVJg==} + + '@lexical/table@0.16.1': + resolution: {integrity: sha512-GWb0/MM1sVXpi1p2HWWOBldZXASMQ4c6WRNYnRmq7J/aB5N66HqQgJGKp3m66Kz4k1JjhmZfPs7F018qIBhnFQ==} + + '@lexical/text@0.16.1': + resolution: {integrity: sha512-Os/nKQegORTrKKN6vL3/FMVszyzyqaotlisPynvTaHTUC+yY4uyjM2hlF93i5a2ixxyiPLF9bDroxUP96TMPXg==} + + '@lexical/utils@0.16.1': + resolution: {integrity: sha512-BVyJxDQi/rIxFTDjf2zE7rMDKSuEaeJ4dybHRa/hRERt85gavGByQawSLeQlTjLaYLVsy+x7wCcqh2fNhlLf0g==} + + '@lexical/yjs@0.16.1': + resolution: {integrity: sha512-QHw1bmzB/IypIV1tRWMH4hhwE1xX7wV+HxbzBS8oJAkoU5AYXM/kyp/sQicgqiwVfpai1Px7zatOoUDFgbyzHQ==} + peerDependencies: + yjs: '>=13.5.22' + '@malept/cross-spawn-promise@1.1.1': resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} engines: {node: '>= 10'} @@ -3108,10 +3188,6 @@ packages: dotenv-expand@5.1.0: resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} - dotenv@16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} - engines: {node: '>=12'} - dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -3981,6 +4057,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -4074,6 +4153,14 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lexical@0.16.1: + resolution: {integrity: sha512-+R05d3+N945OY8pTUjTqQrWoApjC+ctzvjnmNETtx9WmVAaiW0tQVG+AYLt5pDGY8dQXtd4RPorvnxBTECt9SA==} + + lib0@0.2.108: + resolution: {integrity: sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==} + engines: {node: '>=16'} + hasBin: true + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4657,6 +4744,12 @@ packages: peerDependencies: react: ^18.3.1 + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + react-fast-compare@3.2.1: resolution: {integrity: sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==} @@ -5646,6 +5739,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yjs@13.6.27: + resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -7169,6 +7266,151 @@ snapshots: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.5.0 + '@lexical/clipboard@0.16.1': + dependencies: + '@lexical/html': 0.16.1 + '@lexical/list': 0.16.1 + '@lexical/selection': 0.16.1 + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/code@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + prismjs: 1.29.0 + + '@lexical/devtools-core@0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@lexical/html': 0.16.1 + '@lexical/link': 0.16.1 + '@lexical/mark': 0.16.1 + '@lexical/table': 0.16.1 + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@lexical/dragon@0.16.1': + dependencies: + lexical: 0.16.1 + + '@lexical/hashtag@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/history@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/html@0.16.1': + dependencies: + '@lexical/selection': 0.16.1 + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/link@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/list@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/mark@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/markdown@0.16.1': + dependencies: + '@lexical/code': 0.16.1 + '@lexical/link': 0.16.1 + '@lexical/list': 0.16.1 + '@lexical/rich-text': 0.16.1 + '@lexical/text': 0.16.1 + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/offset@0.16.1': + dependencies: + lexical: 0.16.1 + + '@lexical/overflow@0.16.1': + dependencies: + lexical: 0.16.1 + + '@lexical/plain-text@0.16.1': + dependencies: + '@lexical/clipboard': 0.16.1 + '@lexical/selection': 0.16.1 + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/react@0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.27)': + dependencies: + '@lexical/clipboard': 0.16.1 + '@lexical/code': 0.16.1 + '@lexical/devtools-core': 0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@lexical/dragon': 0.16.1 + '@lexical/hashtag': 0.16.1 + '@lexical/history': 0.16.1 + '@lexical/link': 0.16.1 + '@lexical/list': 0.16.1 + '@lexical/mark': 0.16.1 + '@lexical/markdown': 0.16.1 + '@lexical/overflow': 0.16.1 + '@lexical/plain-text': 0.16.1 + '@lexical/rich-text': 0.16.1 + '@lexical/selection': 0.16.1 + '@lexical/table': 0.16.1 + '@lexical/text': 0.16.1 + '@lexical/utils': 0.16.1 + '@lexical/yjs': 0.16.1(yjs@13.6.27) + lexical: 0.16.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-error-boundary: 3.1.4(react@18.3.1) + transitivePeerDependencies: + - yjs + + '@lexical/rich-text@0.16.1': + dependencies: + '@lexical/clipboard': 0.16.1 + '@lexical/selection': 0.16.1 + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/selection@0.16.1': + dependencies: + lexical: 0.16.1 + + '@lexical/table@0.16.1': + dependencies: + '@lexical/utils': 0.16.1 + lexical: 0.16.1 + + '@lexical/text@0.16.1': + dependencies: + lexical: 0.16.1 + + '@lexical/utils@0.16.1': + dependencies: + '@lexical/list': 0.16.1 + '@lexical/selection': 0.16.1 + '@lexical/table': 0.16.1 + lexical: 0.16.1 + + '@lexical/yjs@0.16.1(yjs@13.6.27)': + dependencies: + '@lexical/offset': 0.16.1 + lexical: 0.16.1 + yjs: 13.6.27 + '@malept/cross-spawn-promise@1.1.1': dependencies: cross-spawn: 7.0.6 @@ -7338,7 +7580,7 @@ snapshots: '@babel/core': 7.23.6 '@sentry/babel-plugin-component-annotate': 2.16.1 '@sentry/cli': 2.23.0(encoding@0.1.13) - dotenv: 16.3.1 + dotenv: 16.5.0 find-up: 5.0.0 glob: 9.3.2 magic-string: 0.30.8 @@ -8721,8 +8963,6 @@ snapshots: dotenv-expand@5.1.0: {} - dotenv@16.3.1: {} - dotenv@16.5.0: {} dotenv@9.0.2: {} @@ -9844,6 +10084,8 @@ snapshots: isexe@2.0.0: {} + isomorphic.js@0.2.5: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -9969,6 +10211,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lexical@0.16.1: {} + + lib0@0.2.108: + dependencies: + isomorphic.js: 0.2.5 + lines-and-columns@1.2.4: {} locate-path@6.0.0: @@ -10489,6 +10737,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + react-fast-compare@3.2.1: {} react-fast-compare@3.2.2: {} @@ -11539,6 +11792,10 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yjs@13.6.27: + dependencies: + lib0: 0.2.108 + yocto-queue@0.1.0: {} zip-stream@4.1.1: