{
return { getDisplayMedia, pause, play, stopTrack, toBlob };
};
+const getRichtextEditor = (container: HTMLElement) => {
+ const editor = container.querySelector('[contenteditable="true"]');
+ expect(editor).toBeInstanceOf(HTMLElement);
+ return editor as HTMLElement;
+};
+
describe("promptInput", () => {
it("renders form", () => {
setupPromptInputTests();
@@ -759,6 +768,468 @@ describe("promptInputTextarea", () => {
});
});
+describe("promptInputRichtext", () => {
+ it("renders a contenteditable input with placeholder", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const { container } = render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText("Ask anything")).toBeInTheDocument();
+ expect(getRichtextEditor(container)).toHaveAttribute(
+ "aria-placeholder",
+ "Ask anything"
+ );
+ });
+
+ it("submits richtext on Enter without provider and clears the editor", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("Hello from Lexical");
+
+ expect(editor).toHaveTextContent("Hello from Lexical");
+
+ await user.keyboard("{Enter}");
+
+ await vi.waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledOnce();
+ });
+
+ const [[message]] = onSubmit.mock.calls;
+ expect(message).toHaveProperty("text", "Hello from Lexical");
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("");
+ });
+ });
+
+ it("submits richtext on Enter with provider and clears the editor", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("Hello with provider");
+ await user.keyboard("{Enter}");
+
+ await vi.waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledOnce();
+ });
+
+ const [[message]] = onSubmit.mock.calls;
+ expect(message).toHaveProperty("text", "Hello with provider");
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("");
+ });
+ });
+
+ it("does not submit on Shift+Enter", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("Line 1");
+ await user.keyboard("{Shift>}{Enter}{/Shift}");
+
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
+ it("does not submit on Enter during IME composition", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+
+ const { container } = render(
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ editor.focus();
+
+ const enterKeyDuringComposition = new KeyboardEvent("keydown", {
+ bubbles: true,
+ cancelable: true,
+ key: "Enter",
+ });
+
+ Object.defineProperty(enterKeyDuringComposition, "isComposing", {
+ value: true,
+ writable: false,
+ });
+
+ editor.dispatchEvent(enterKeyDuringComposition);
+
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
+ it("renders bold formatting after markdown shortcut resolves", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("**bold** ");
+
+ await vi.waitFor(() => {
+ const boldNode = editor.querySelector("strong, b, span.font-semibold");
+ expect(boldNode).toHaveTextContent("bold");
+ expect(editor).toHaveTextContent("bold");
+ expect(editor.textContent).not.toContain("**");
+ });
+ });
+
+ it("renders italic formatting after underscore markdown shortcut resolves", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("_italic_ ");
+
+ await vi.waitFor(() => {
+ const italicNode = editor.querySelector("em, i, span.italic");
+ expect(italicNode).toHaveTextContent("italic");
+ expect(editor).toHaveTextContent("italic");
+ expect(editor.textContent).not.toContain("_italic_");
+ });
+ });
+
+ it("renders italic formatting after asterisk markdown shortcut resolves", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("*italic* ");
+
+ await vi.waitFor(() => {
+ const italicNode = editor.querySelector("em, i, span.italic");
+ expect(italicNode).toHaveTextContent("italic");
+ expect(editor).toHaveTextContent("italic");
+ expect(editor.textContent).not.toContain("*italic*");
+ });
+ });
+
+ it("syncs initialInput into the richtext editor", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+
+ const { container } = render(
+
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("Initial provider value");
+ });
+ });
+
+ it("renders provider markdown initialInput as styled text instead of literal markers", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+
+ const { container } = render(
+
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+
+ await vi.waitFor(() => {
+ const boldNode = editor.querySelector("strong, b, span.font-semibold");
+ expect(boldNode).toHaveTextContent("hello");
+ expect(editor).toHaveTextContent("hello");
+ expect(editor.textContent).not.toContain("**hello**");
+ });
+ });
+
+ it("syncs provider state in both directions", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const ProviderProbe = () => {
+ const controller = usePromptInputController();
+ return (
+ <>
+
+ {controller.textInput.value}
+ >
+ );
+ };
+
+ const { container } = render(
+
+
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+
+ await user.click(screen.getByRole("button", { name: "Set Input" }));
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("Updated externally");
+ });
+
+ await user.click(editor);
+ await user.keyboard("!");
+
+ await vi.waitFor(() => {
+ expect(screen.getByTestId("provider-value")).toHaveTextContent(
+ "Updated externally!"
+ );
+ });
+ });
+
+ it("preserves richtext content when async submit rejects", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn(() => Promise.reject(new Error("submit failed")));
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("Keep this content");
+ await user.keyboard("{Enter}");
+
+ await vi.waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledOnce();
+ });
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("Keep this content");
+ });
+ });
+
+ it("submits typed URLs as markdown links", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const { container } = render(
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ await user.click(editor);
+ await user.keyboard("https://example.com ");
+
+ await vi.waitFor(() => {
+ const linkNode = editor.querySelector("a");
+ expect(linkNode).toHaveTextContent("https://example.com");
+ });
+
+ await user.keyboard("{Enter}");
+
+ await vi.waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledOnce();
+ });
+
+ const [[message]] = onSubmit.mock.calls;
+ expect(message.text).toContain("[https://example.com](https://example.com)");
+ });
+
+ it("clears richtext editor when provider textInput.clear() is called", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const ProviderProbe = () => {
+ const controller = usePromptInputController();
+ return (
+ <>
+
+ {controller.textInput.value}
+ >
+ );
+ };
+
+ const { container } = render(
+
+
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("Initial text");
+ });
+
+ await user.click(screen.getByRole("button", { name: "Clear Input" }));
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("");
+ });
+
+ await vi.waitFor(() => {
+ expect(screen.getByTestId("provider-value")).toHaveTextContent("");
+ });
+ });
+
+ it("clears richtext editor via external setInput to empty string", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ const user = userEvent.setup();
+
+ const ProviderProbe = () => {
+ const controller = usePromptInputController();
+ return (
+ <>
+
+ >
+ );
+ };
+
+ const { container } = render(
+
+
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("Some content");
+ });
+
+ await user.click(screen.getByRole("button", { name: "Clear Input" }));
+
+ await vi.waitFor(() => {
+ expect(editor).toHaveTextContent("");
+ });
+ });
+});
+
describe("promptInputTools", () => {
it("renders tools", () => {
setupPromptInputTests();
@@ -1997,6 +2468,7 @@ describe("paste functionality", () => {
// Mock clipboardData items
pasteEvent.clipboardData = {
+ getData: () => "",
items: [
{
getAsFile: () => file,
@@ -2014,6 +2486,53 @@ describe("paste functionality", () => {
});
});
+ it("adds files from clipboard in richtext editor", async () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+
+ const AttachmentConsumer = () => {
+ const attachments = usePromptInputAttachments();
+ return {attachments.files.length}
;
+ };
+
+ const { container } = render(
+
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ editor.focus();
+
+ const file = new File(["image"], "test.png", { type: "image/png" });
+ const pasteEvent = new Event("paste", {
+ bubbles: true,
+ cancelable: true,
+ // oxlint-disable-next-line typescript-eslint(no-explicit-any)
+ }) as any;
+
+ pasteEvent.clipboardData = {
+ getData: () => "",
+ items: [
+ {
+ getAsFile: () => file,
+ kind: "file",
+ },
+ ],
+ };
+
+ await act(() => {
+ editor.dispatchEvent(pasteEvent);
+ });
+
+ await vi.waitFor(() => {
+ expect(screen.getByTestId("count")).toHaveTextContent("1");
+ });
+ });
+
it("handles paste with no files", () => {
setupPromptInputTests();
const onSubmit = vi.fn();
@@ -2036,11 +2555,36 @@ describe("paste functionality", () => {
cancelable: true,
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
}) as any;
- pasteEvent.clipboardData = { items: [] };
+ pasteEvent.clipboardData = { getData: () => "", items: [] };
// Should not throw
expect(() => textarea.dispatchEvent(pasteEvent)).not.toThrow();
});
+
+ it("handles richtext paste with no files", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+
+ const { container } = render(
+
+
+
+
+
+ );
+
+ const editor = getRichtextEditor(container);
+ editor.focus();
+
+ const pasteEvent = new Event("paste", {
+ bubbles: true,
+ cancelable: true,
+ // oxlint-disable-next-line typescript-eslint(no-explicit-any)
+ }) as any;
+ pasteEvent.clipboardData = { getData: () => "", items: [] };
+
+ expect(() => editor.dispatchEvent(pasteEvent)).not.toThrow();
+ });
});
describe("promptInputAttachment", () => {
@@ -3155,7 +3699,9 @@ describe("promptInputSelect components", () => {
// Mock hasPointerCapture and releasePointerCapture for select
vi.spyOn(Element.prototype, "hasPointerCapture").mockReturnValue(false);
- vi.spyOn(Element.prototype, "releasePointerCapture").mockImplementation();
+ vi
+ .spyOn(Element.prototype, "releasePointerCapture")
+ .mockImplementation((_pointerId: number) => {});
render(
diff --git a/packages/elements/package.json b/packages/elements/package.json
index 0ac0705f..39201241 100644
--- a/packages/elements/package.json
+++ b/packages/elements/package.json
@@ -12,6 +12,9 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
+ "@lexical/link": "^0.42.0",
+ "@lexical/markdown": "^0.42.0",
+ "@lexical/react": "^0.42.0",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@repo/shadcn-ui": "workspace:*",
"@rive-app/react-webgl2": "^4.26.1",
@@ -24,6 +27,7 @@
"ansi-to-react": "^6.2.6",
"class-variance-authority": "^0.7.1",
"katex": "^0.16.28",
+ "lexical": "^0.42.0",
"lucide-react": "^0.577.0",
"media-chrome": "^4.17.2",
"motion": "^12.26.2",
diff --git a/packages/elements/src/prompt-input.tsx b/packages/elements/src/prompt-input.tsx
index 412c846d..88d28418 100644
--- a/packages/elements/src/prompt-input.tsx
+++ b/packages/elements/src/prompt-input.tsx
@@ -50,6 +50,36 @@ import {
XIcon,
} from "lucide-react";
import { nanoid } from "nanoid";
+import { LexicalComposer } from "@lexical/react/LexicalComposer";
+import type { InitialConfigType } from "@lexical/react/LexicalComposer";
+import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
+import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
+import { ContentEditable } from "@lexical/react/LexicalContentEditable";
+import type { ContentEditableProps } from "@lexical/react/LexicalContentEditable";
+import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
+import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
+import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
+import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
+import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
+import { EditorRefPlugin } from "@lexical/react/LexicalEditorRefPlugin";
+import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import {
+ $convertFromMarkdownString,
+ $convertToMarkdownString,
+ LINK,
+ TEXT_FORMAT_TRANSFORMERS,
+ TEXT_MATCH_TRANSFORMERS,
+} from "@lexical/markdown";
+import { $isLinkNode, AutoLinkNode, LinkNode } from "@lexical/link";
+import {
+ $getRoot,
+ COMMAND_PRIORITY_LOW,
+ KEY_BACKSPACE_COMMAND,
+ KEY_ENTER_COMMAND,
+ TextNode,
+} from "lexical";
+import type { EditorState, LexicalEditor, LexicalNode } from "lexical";
import type {
ChangeEvent,
ChangeEventHandler,
@@ -174,6 +204,70 @@ const captureScreenshot = async (): Promise => {
}
};
+const syncEditorTextContent = (editor: LexicalEditor | null, value: string) => {
+ if (!editor) {
+ return;
+ }
+ editor.update(() => {
+ const root = $getRoot();
+ const currentValue = $convertToMarkdownString(PROMPT_INPUT_TRANSFORMERS);
+
+ if (value === currentValue) {
+ return;
+ }
+
+ root.clear();
+
+ if (value) {
+ $convertFromMarkdownString(value, PROMPT_INPUT_TRANSFORMERS);
+ }
+
+ root.selectEnd();
+ });
+};
+
+const URL_MATCHER =
+ /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
+
+const AUTO_LINK_MATCHERS = [
+ (text: string) => {
+ const match = URL_MATCHER.exec(text);
+ if (match === null) {
+ return null;
+ }
+ const [fullMatch] = match;
+ return {
+ index: match.index,
+ length: fullMatch.length,
+ text: fullMatch,
+ url: fullMatch.startsWith("http") ? fullMatch : `https://${fullMatch}`,
+ // attributes: { rel: 'noreferrer', target: '_blank' }, // Optional link attributes
+ };
+ },
+];
+
+const LINK_TRANSFORMER = {
+ ...LINK,
+ export: (node: LexicalNode, exportChildren: (node: LinkNode) => string) => {
+ if (!$isLinkNode(node)) {
+ return null;
+ }
+
+ const textContent = exportChildren(node);
+ const title = node.getTitle();
+
+ return title
+ ? `[${textContent}](${node.getURL()} "${title.replaceAll(/([\\"])/g, "\\$1")}")`
+ : `[${textContent}](${node.getURL()})`;
+ },
+};
+
+const PROMPT_INPUT_TRANSFORMERS = [
+ LINK_TRANSFORMER,
+ ...TEXT_FORMAT_TRANSFORMERS,
+ ...TEXT_MATCH_TRANSFORMERS,
+];
+
// ============================================================================
// Provider Context & Types
// ============================================================================
@@ -193,28 +287,41 @@ export interface TextInputContext {
clear: () => void;
}
+interface PromptInputSubmitAdapter {
+ getText: () => string;
+ clear: () => void;
+ setText?: (value: string) => void;
+}
+
export interface PromptInputControllerProps {
textInput: TextInputContext;
attachments: AttachmentsContext;
/** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
__registerFileInput: (
ref: RefObject,
- open: () => void
+ open: () => void,
) => void;
}
+interface PromptInputSubmitAdapterValue {
+ registerSubmitAdapter: (adapter: PromptInputSubmitAdapter | null) => void;
+}
+
+const PromptInputSubmitAdapterContext =
+ createContext(null);
+
const PromptInputController = createContext(
- null
+ null,
);
const ProviderAttachmentsContext = createContext(
- null
+ null,
);
export const usePromptInputController = () => {
const ctx = useContext(PromptInputController);
if (!ctx) {
throw new Error(
- "Wrap your component inside to use usePromptInputController()."
+ "Wrap your component inside to use usePromptInputController().",
);
}
return ctx;
@@ -228,7 +335,7 @@ export const useProviderAttachments = () => {
const ctx = useContext(ProviderAttachmentsContext);
if (!ctx) {
throw new Error(
- "Wrap your component inside to use useProviderAttachments()."
+ "Wrap your component inside to use useProviderAttachments().",
);
}
return ctx;
@@ -316,7 +423,7 @@ export const PromptInputProvider = ({
}
}
},
- []
+ [],
);
const openFileDialog = useCallback(() => {
@@ -332,7 +439,7 @@ export const PromptInputProvider = ({
openFileDialog,
remove,
}),
- [attachmentFiles, add, remove, clear, openFileDialog]
+ [attachmentFiles, add, remove, clear, openFileDialog],
);
const __registerFileInput = useCallback(
@@ -340,7 +447,7 @@ export const PromptInputProvider = ({
fileInputRef.current = ref.current;
openRef.current = open;
},
- []
+ [],
);
const controller = useMemo(
@@ -353,7 +460,7 @@ export const PromptInputProvider = ({
value: textInput,
},
}),
- [textInput, clearInput, attachments, __registerFileInput]
+ [textInput, clearInput, attachments, __registerFileInput],
);
return (
@@ -378,7 +485,7 @@ export const usePromptInputAttachments = () => {
const context = local ?? provider;
if (!context) {
throw new Error(
- "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider"
+ "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider",
);
}
return context;
@@ -402,7 +509,7 @@ export const usePromptInputReferencedSources = () => {
const ctx = useContext(LocalReferencedSourcesContext);
if (!ctx) {
throw new Error(
- "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider"
+ "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider",
);
}
return ctx;
@@ -425,7 +532,7 @@ export const PromptInputActionAddAttachments = ({
e.preventDefault();
attachments.openFileDialog();
},
- [attachments]
+ [attachments],
);
return (
@@ -470,7 +577,7 @@ export const PromptInputActionAddScreenshot = ({
throw error;
}
},
- [onSelect, attachments]
+ [onSelect, attachments],
);
return (
@@ -507,7 +614,7 @@ export type PromptInputProps = Omit<
}) => void;
onSubmit: (
message: PromptInputMessage,
- event: FormEvent
+ event: FormEvent,
) => void | Promise;
};
@@ -572,7 +679,7 @@ export const PromptInput = ({
return f.type === pattern;
});
},
- [accept]
+ [accept],
);
const addLocal = useCallback(
@@ -623,7 +730,7 @@ export const PromptInput = ({
return [...prev, ...next];
});
},
- [matchesAccept, maxFiles, maxFileSize, onError]
+ [matchesAccept, maxFiles, maxFileSize, onError],
);
const removeLocal = useCallback(
@@ -635,7 +742,7 @@ export const PromptInput = ({
}
return prev.filter((file) => file.id !== id);
}),
- []
+ [],
);
// Wrapper that validates files before calling provider's add
@@ -679,7 +786,7 @@ export const PromptInput = ({
controller?.attachments.add(capped);
}
},
- [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller]
+ [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller],
);
const clearAttachments = useCallback(
@@ -694,12 +801,12 @@ export const PromptInput = ({
}
return [];
}),
- [usingProvider, controller]
+ [usingProvider, controller],
);
const clearReferencedSources = useCallback(
() => setReferencedSources([]),
- []
+ [],
);
const add = usingProvider ? addWithProviderValidation : addLocal;
@@ -797,7 +904,7 @@ export const PromptInput = ({
}
}
},
- [usingProvider]
+ [usingProvider],
);
const handleChange: ChangeEventHandler = useCallback(
@@ -808,7 +915,7 @@ export const PromptInput = ({
// Reset input value to allow selecting files that were previously removed
event.currentTarget.value = "";
},
- [add]
+ [add],
);
const attachmentsCtx = useMemo(
@@ -820,7 +927,7 @@ export const PromptInput = ({
openFileDialog,
remove,
}),
- [files, add, remove, clearAttachments, openFileDialog]
+ [files, add, remove, clearAttachments, openFileDialog],
);
const refsCtx = useMemo(
@@ -838,7 +945,16 @@ export const PromptInput = ({
},
sources: referencedSources,
}),
- [referencedSources, clearReferencedSources]
+ [referencedSources, clearReferencedSources],
+ );
+
+ const promptSubmitAdapterRef = useRef(null);
+
+ const registerSubmitAdapter = useCallback(
+ (adapter: PromptInputSubmitAdapter | null) => {
+ promptSubmitAdapterRef.current = adapter;
+ },
+ [],
);
const handleSubmit: FormEventHandler = useCallback(
@@ -846,13 +962,18 @@ export const PromptInput = ({
event.preventDefault();
const form = event.currentTarget;
- const text = usingProvider
- ? controller.textInput.value
- : (() => {
- const formData = new FormData(form);
- return (formData.get("message") as string) || "";
- })();
-
+ const adapter = promptSubmitAdapterRef.current;
+ let text = "";
+ if (adapter) {
+ text = adapter.getText();
+ } else if (usingProvider) {
+ text = controller.textInput.value;
+ } else {
+ text = (() => {
+ const formData = new FormData(form);
+ return (formData.get("message") as string) || "";
+ })();
+ }
// Reset form immediately after capturing text to avoid race condition
// where user input during async blob conversion would be lost
if (!usingProvider) {
@@ -872,7 +993,7 @@ export const PromptInput = ({
};
}
return item;
- })
+ }),
);
const result = onSubmit({ files: convertedFiles, text }, event);
@@ -882,24 +1003,32 @@ export const PromptInput = ({
try {
await result;
clear();
- if (usingProvider) {
+ adapter?.clear();
+ if (!adapter && usingProvider) {
controller.textInput.clear();
}
+ if (!adapter && !usingProvider) {
+ form.reset();
+ }
} catch {
// Don't clear on error - user may want to retry
}
} else {
// Sync function completed without throwing, clear inputs
clear();
- if (usingProvider) {
+ adapter?.clear();
+ if (!adapter && usingProvider) {
controller.textInput.clear();
}
+ if (!adapter && !usingProvider) {
+ form.reset();
+ }
}
} catch {
// Don't clear on error - user may want to retry
}
},
- [usingProvider, controller, files, onSubmit, clear]
+ [usingProvider, controller, files, onSubmit, clear],
);
// Render with or without local provider
@@ -934,9 +1063,11 @@ export const PromptInput = ({
// Always provide LocalAttachmentsContext so children get validated add function
return (
-
- {withReferencedSources}
-
+
+
+ {withReferencedSources}
+
+
);
};
@@ -986,7 +1117,7 @@ export const PromptInputTextarea = ({
// Check if the submit button is disabled before submitting
const { form } = e.currentTarget;
const submitButton = form?.querySelector(
- 'button[type="submit"]'
+ 'button[type="submit"]',
) as HTMLButtonElement | null;
if (submitButton?.disabled) {
return;
@@ -1008,7 +1139,7 @@ export const PromptInputTextarea = ({
}
}
},
- [onKeyDown, isComposing, attachments]
+ [onKeyDown, isComposing, attachments],
);
const handlePaste: ClipboardEventHandler = useCallback(
@@ -1035,7 +1166,7 @@ export const PromptInputTextarea = ({
attachments.add(files);
}
},
- [attachments]
+ [attachments],
);
const handleCompositionEnd = useCallback(() => setIsComposing(false), []);
@@ -1068,6 +1199,284 @@ export const PromptInputTextarea = ({
);
};
+const PromptInputKeyBindingsPlugin = ({
+ onKeyDown,
+}: {
+ onKeyDown?: (event: KeyboardEvent) => void;
+}) => {
+ const [editor] = useLexicalComposerContext();
+ const attachments = usePromptInputAttachments();
+
+ useEffect(() => {
+ const unregisterEnter = editor.registerCommand(
+ KEY_ENTER_COMMAND,
+ (event: KeyboardEvent | null) => {
+ if (editor.isComposing() || event?.isComposing || event?.shiftKey) {
+ return false;
+ }
+
+ // Check if the submit button is disabled before submitting
+ const form = editor.getRootElement()?.closest("form");
+ const submitButton = form?.querySelector(
+ 'button[type="submit"]',
+ ) as HTMLButtonElement | null;
+
+ if (submitButton?.disabled) {
+ event?.preventDefault();
+ return true;
+ }
+
+ event?.preventDefault();
+ form?.requestSubmit();
+ return true;
+ },
+ COMMAND_PRIORITY_LOW,
+ );
+
+ const unregisterBackspace = editor.registerCommand(
+ KEY_BACKSPACE_COMMAND,
+ (event: KeyboardEvent | null) => {
+ // Remove last attachment when Backspace is pressed and textarea is empty
+ if (
+ $getRoot().getTextContent() !== "" ||
+ attachments.files.length === 0
+ ) {
+ return false;
+ }
+ if (
+ $getRoot().getTextContent() === "" &&
+ attachments.files.length > 0
+ ) {
+ event?.preventDefault();
+ const lastAttachment = attachments.files.at(-1);
+ if (lastAttachment) {
+ attachments.remove(lastAttachment.id);
+ return true;
+ }
+ }
+ return false;
+ },
+ COMMAND_PRIORITY_LOW,
+ );
+
+ const unregisters = [unregisterEnter, unregisterBackspace];
+
+ if (onKeyDown) {
+ unregisters.push(
+ editor.registerRootListener((rootElement, prevRootElement) => {
+ if (prevRootElement) {
+ prevRootElement.removeEventListener("keydown", onKeyDown);
+ }
+ if (rootElement) {
+ rootElement.addEventListener("keydown", onKeyDown);
+ }
+ }),
+ );
+ }
+
+ return () => {
+ for (const unregister of unregisters) {
+ unregister();
+ }
+ };
+ }, [editor, attachments, onKeyDown]);
+
+ return null;
+};
+
+export type PromptInputRichtextProps = Omit<
+ ContentEditableProps,
+ "placeholder"
+> & {
+ className?: string;
+ editorConfig?: InitialConfigType;
+ autoFocus?: boolean;
+ onChange?: (editorState: EditorState) => void;
+ onKeyDown?: (event: KeyboardEvent) => void;
+ placeholder?: string;
+ placeholderClassName?: string;
+};
+
+export const PromptInputRichtext = ({
+ className,
+ editorConfig,
+ autoFocus = true,
+ onChange,
+ onKeyDown,
+ placeholder = "What would you like to know?",
+ placeholderClassName,
+ children,
+ ...props
+}: PromptInputRichtextProps) => {
+ const editorRef = useRef(null);
+ const attachments = usePromptInputAttachments();
+ const submitAdapterContext = useContext(PromptInputSubmitAdapterContext);
+ const controller = useOptionalPromptInputController();
+ const controllerValue = controller?.textInput.value;
+
+ // register adapter into context using editor ref
+ useEffect(() => {
+ if (!submitAdapterContext) {
+ return;
+ }
+
+ const adapter: PromptInputSubmitAdapter = {
+ clear: () => {
+ editorRef.current?.update(() => {
+ const root = $getRoot();
+ root.clear();
+ root.selectEnd();
+ });
+ },
+ getText: () =>
+ editorRef.current
+ ?.getEditorState()
+ .read(() => $convertToMarkdownString(PROMPT_INPUT_TRANSFORMERS)) ??
+ "",
+ setText: (value) => {
+ syncEditorTextContent(editorRef.current, value);
+ },
+ };
+
+ submitAdapterContext.registerSubmitAdapter(adapter);
+
+ return () => {
+ submitAdapterContext.registerSubmitAdapter(null);
+ };
+ }, [submitAdapterContext]);
+
+ // sync editor text content in provider mode
+ useEffect(() => {
+ if (controllerValue === undefined) {
+ return;
+ }
+ syncEditorTextContent(editorRef.current, controllerValue);
+ }, [controllerValue]);
+
+ const handlePaste: ClipboardEventHandler = useCallback(
+ (event) => {
+ const items = event.clipboardData?.items;
+
+ if (!items) {
+ return;
+ }
+
+ const files: File[] = [];
+
+ for (const item of items) {
+ if (item.kind === "file") {
+ const file = item.getAsFile();
+ if (file) {
+ files.push(file);
+ }
+ }
+ }
+
+ if (files.length > 0) {
+ event.preventDefault();
+ attachments.add(files);
+ }
+ },
+ [attachments],
+ );
+
+ const onChangeHandler = useCallback(
+ (editorState: EditorState) => {
+ editorState.read(() => {
+ const markdown = $convertToMarkdownString(PROMPT_INPUT_TRANSFORMERS);
+ controller?.textInput.setInput(markdown);
+ });
+ onChange?.(editorState);
+ },
+ [onChange, controller?.textInput],
+ );
+
+ const initialConfig: InitialConfigType = {
+ namespace: "Editor",
+ nodes: [TextNode, LinkNode, AutoLinkNode],
+ onError: (error: Error) => {
+ console.error(error);
+ },
+ theme: {
+ link: "rounded-md bg-blue-100 px-1 py-0.5 font-medium text-blue-600 dark:bg-blue-950 dark:text-blue-300",
+ paragraph: "mb-0",
+ text: {
+ bold: "font-semibold",
+ italic: "italic",
+ underline: "underline",
+ },
+ },
+ };
+
+ return (
+
+
+
+ {placeholder}
+
+ }
+ aria-placeholder={placeholder}
+ onPaste={handlePaste}
+ {...props}
+ />
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+ {/*required plugins*/}
+
+
+
+
+ {/*good default plugins*/}
+
+
+
+ {/*optional plugins*/}
+ {autoFocus && }
+ {children}
+
+
+ );
+};
+
+export const PromptInputRichtextPlaceholder = ({
+ placeholder = "What would you like to know?",
+ className,
+ ...props
+}: ComponentProps<"span"> & {
+ placeholder?: string;
+}) => (
+
+ {placeholder}
+
+);
+
export type PromptInputHeaderProps = Omit<
ComponentProps,
"align"
@@ -1244,7 +1653,7 @@ export const PromptInputSubmit = ({
}
onClick?.(e);
},
- [isGenerating, onStop, onClick]
+ [isGenerating, onStop, onClick],
);
return (
@@ -1280,7 +1689,7 @@ export const PromptInputSelectTrigger = ({
className={cn(
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
- className
+ className,
)}
{...props}
/>
@@ -1330,7 +1739,7 @@ export type PromptInputHoverCardTriggerProps = ComponentProps<
>;
export const PromptInputHoverCardTrigger = (
- props: PromptInputHoverCardTriggerProps
+ props: PromptInputHoverCardTriggerProps,
) => ;
export type PromptInputHoverCardContentProps = ComponentProps<
@@ -1369,7 +1778,7 @@ export const PromptInputTabLabel = ({
@@ -1393,7 +1802,7 @@ export const PromptInputTabItem = ({
diff --git a/packages/elements/vitest.config.mts b/packages/elements/vitest.config.mts
index 8e90c9b9..a5febec3 100644
--- a/packages/elements/vitest.config.mts
+++ b/packages/elements/vitest.config.mts
@@ -7,6 +7,23 @@ import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";
export default defineConfig({
+ optimizeDeps: {
+ include: [
+ "lexical",
+ "@lexical/link",
+ "@lexical/markdown",
+ "@lexical/react/LexicalAutoLinkPlugin",
+ "@lexical/react/LexicalComposer",
+ "@lexical/react/LexicalContentEditable",
+ "@lexical/react/LexicalEditorRefPlugin",
+ "@lexical/react/LexicalErrorBoundary",
+ "@lexical/react/LexicalHistoryPlugin",
+ "@lexical/react/LexicalLinkPlugin",
+ "@lexical/react/LexicalMarkdownShortcutPlugin",
+ "@lexical/react/LexicalOnChangePlugin",
+ "@lexical/react/LexicalRichTextPlugin",
+ ],
+ },
plugins: [react()],
resolve: {
alias: {
diff --git a/packages/examples/src/prompt-input-richtext.tsx b/packages/examples/src/prompt-input-richtext.tsx
new file mode 100644
index 00000000..833f83e6
--- /dev/null
+++ b/packages/examples/src/prompt-input-richtext.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import {
+ PromptInput,
+ PromptInputBody,
+ PromptInputButton,
+ PromptInputFooter,
+ PromptInputMessage,
+ PromptInputProvider,
+ PromptInputRichtext,
+ PromptInputSubmit,
+ PromptInputTextarea,
+ PromptInputTools,
+} from "@repo/elements/prompt-input";
+import { GlobeIcon, MicIcon, PaperclipIcon } from "lucide-react";
+
+const handleSubmit = (message: PromptInputMessage) => {
+ const hasText = Boolean(message.text);
+ const hasAttachments = Boolean(message.files?.length);
+
+ if (!(hasText || hasAttachments)) {
+ return;
+ }
+
+ // eslint-disable-next-line no-console
+ console.log("Submitting message:", message);
+};
+
+const Example = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default Example;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d82cfd3a..76b27dab 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -200,6 +200,15 @@ importers:
packages/elements:
dependencies:
+ '@lexical/link':
+ specifier: ^0.42.0
+ version: 0.42.0
+ '@lexical/markdown':
+ specifier: ^0.42.0
+ version: 0.42.0
+ '@lexical/react':
+ specifier: ^0.42.0
+ version: 0.42.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(yjs@13.6.30)
'@radix-ui/react-use-controllable-state':
specifier: ^1.2.2
version: 1.2.2(@types/react@19.2.8)(react@19.2.3)
@@ -236,6 +245,9 @@ importers:
katex:
specifier: ^0.16.28
version: 0.16.28
+ lexical:
+ specifier: ^0.42.0
+ version: 0.42.0
lucide-react:
specifier: ^0.577.0
version: 0.577.0(react@19.2.3)
@@ -1130,18 +1142,39 @@ packages:
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
'@floating-ui/dom@1.7.3':
resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
'@floating-ui/react-dom@2.1.5':
resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
+ '@floating-ui/react-dom@2.1.8':
+ resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/react@0.27.19':
+ resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==}
+ peerDependencies:
+ react: '>=17.0.0'
+ react-dom: '>=17.0.0'
+
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
'@formatjs/intl-localematcher@0.6.2':
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
@@ -1372,6 +1405,80 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@lexical/clipboard@0.42.0':
+ resolution: {integrity: sha512-D3K2ID0zew/+CKpwxnUTTh/N46yU4IK8bFWV9Htz+g1vFhgUF9UnDOQCmqpJbdP7z+9U1F8rk3fzf9OmP2Fm2w==}
+
+ '@lexical/code-core@0.42.0':
+ resolution: {integrity: sha512-vrZTUPWDJkHjAAvuV2+Qte4vYE80s7hIO7wxipiJmWojGx6lcmQjO+UqJ8AIrqI4Wjy8kXrK74kisApWmwxuCw==}
+
+ '@lexical/devtools-core@0.42.0':
+ resolution: {integrity: sha512-8nP8eE9i8JImgSrvInkWFfMCmXVKp3w3VaOvbJysdlK/Zal6xd8EWJEi6elj0mUW5T/oycfipPs2Sfl7Z+n14A==}
+ peerDependencies:
+ react: '>=17.x'
+ react-dom: '>=17.x'
+
+ '@lexical/dragon@0.42.0':
+ resolution: {integrity: sha512-/TQzP+7PLJMqq9+MlgQWiJsxS9GOOa8Gp0svCD8vNIOciYmXfd28TR1Go+ZnBWwr7k/2W++3XUYVQU2KUcQsDQ==}
+
+ '@lexical/extension@0.42.0':
+ resolution: {integrity: sha512-rkZq/h8d1BenKRqU4t/zQUVfY/RinMX1Tz7t+Ee3ss0sk+kzP4W+URXNAxpn7r39Vn6wrFBqmCziah3dLAIqPw==}
+
+ '@lexical/hashtag@0.42.0':
+ resolution: {integrity: sha512-WOg5nFOfhabNBXzEIutdWDj+TUHtJEezj6w8jyYDGqZ31gu0cgrXSeV8UIynz/1oj+rpzEeEB7P6ODnwgjt7qA==}
+
+ '@lexical/history@0.42.0':
+ resolution: {integrity: sha512-YfCZ1ICUt6BCg2ncJWFMuS4yftnB7FEHFRf3qqTSTf6oGZ4IZfzabMNEy47xybUuf7FXBbdaCKJrc/zOM+wGxw==}
+
+ '@lexical/html@0.42.0':
+ resolution: {integrity: sha512-KgBUDLXehufCsXW3w0XsuoI2xecIhouOishnaNOH4zIA7dAtnNAfdPN/kWrWs0s83gz44OrnqccP+Bprw3UDEQ==}
+
+ '@lexical/link@0.42.0':
+ resolution: {integrity: sha512-cdeM/+f+kn7aGwW/3FIi6USjl1gBNdEEwg0/ZS+KlYcsy8gxx2e4cyVjsomBu/WU17Qxa0NC0paSr7qEJ/1Fig==}
+
+ '@lexical/list@0.42.0':
+ resolution: {integrity: sha512-TIezILnmIVuvfqEEbcMnsT4xQRlswI6ysHISqsvKL6l5EBhs1gqmNYjHa/Yrfzaq5y52TM1PAtxbFts+G7N6kg==}
+
+ '@lexical/mark@0.42.0':
+ resolution: {integrity: sha512-H1aGjbMEcL4B8GT7bm/ePHm7j3Wema+wIRNPmxMtXGMz5gpVN3gZlvg2UcUHHJb00SrBA95OUVT5I2nu/KP06w==}
+
+ '@lexical/markdown@0.42.0':
+ resolution: {integrity: sha512-+mOxgBiumlgVX8Acna+9HjJfSOw1jywufGcAQq3/8S11wZ4gE0u13AaR8LMmU8ydVeOQg09y8PNzGNQ/avZJbg==}
+
+ '@lexical/offset@0.42.0':
+ resolution: {integrity: sha512-V+4af1KmTOnBZrR+kU3e6eD33W/g3QqMPPp3cpFwyXk/dKRc4K8HfyDsSDrjop1mPd9pl3lKSiEmX6uQG8K9XQ==}
+
+ '@lexical/overflow@0.42.0':
+ resolution: {integrity: sha512-wlrHaM27rODJP5m+CTgfZGLg3qWlQ0ptGodcqoGdq6HSbV8nGFY6TvcLMaMtYQ1lm4v9G7Xe9LwjooR6xS3Gug==}
+
+ '@lexical/plain-text@0.42.0':
+ resolution: {integrity: sha512-YWvBwIxLltrIaZDcv0rK4s44P6Yt17yhOb0E+g3+tjF8GGPrrocox+Pglu0m2RHR+G7zULN3isolmWIm/HhWiw==}
+
+ '@lexical/react@0.42.0':
+ resolution: {integrity: sha512-ujWJXhvlFVVTpwDcnSgEYWRuqUbreZaMB+4bjIDT5r7hkAplUHQndlkeuFHKFiJBasSAreleV7zhXrLL5xa9eA==}
+ peerDependencies:
+ react: '>=17.x'
+ react-dom: '>=17.x'
+
+ '@lexical/rich-text@0.42.0':
+ resolution: {integrity: sha512-v4YgiM3oK3FZcRrfB+LetvLbQ5aee9MRO9tHf0EFweXg19XnSjHV0cfPAW7TyPxRELzB69+K0Q3AybRlTMjG4Q==}
+
+ '@lexical/selection@0.42.0':
+ resolution: {integrity: sha512-iWTjLA5BSEuUnvWe9Xwu9FSdZFl3Yi0NqalabXKI+7KgCIlIVXE74y4NvWPUSLkSCB/Z1RPKiHmZqZ1vyu/yGQ==}
+
+ '@lexical/table@0.42.0':
+ resolution: {integrity: sha512-GKiZyjQsHDXRckq5VBrOowyvds51WoVRECfDgcl8pqLMnKyEdCa58E7fkSJrr5LS80Scod+Cjn6SBRzOcdsrKg==}
+
+ '@lexical/text@0.42.0':
+ resolution: {integrity: sha512-hT3EYVtBmONXyXe4TFVgtFcG1tf6JhLEuAf95+cOjgFGFSgvkZ/64BPbKLNTj2/9n6cU7EGPUNNwVigCSECJ2g==}
+
+ '@lexical/utils@0.42.0':
+ resolution: {integrity: sha512-wGNdCW3QWEyVdFiSTLZfFPtiASPyYLcekIiYYZmoRVxVimT/jY+QPfnkO4JYgkO7Z70g/dsg9OhqyQSChQfvkQ==}
+
+ '@lexical/yjs@0.42.0':
+ resolution: {integrity: sha512-DplzWnYhfFceGPR+UyDFpZdB287wF/vNOHFuDsBF/nGDdTezvr0Gf60opzyBEF3oXym6p3xTmGygxvO97LZ+vw==}
+ peerDependencies:
+ yjs: '>=13.5.22'
+
'@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
@@ -1709,6 +1816,9 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+ '@preact/signals-core@1.14.0':
+ resolution: {integrity: sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==}
+
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -4490,6 +4600,9 @@ packages:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
+ isomorphic.js@0.2.5:
+ resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
@@ -4617,6 +4730,14 @@ packages:
layout-base@2.0.1:
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
+ lexical@0.42.0:
+ resolution: {integrity: sha512-GY9Lg3YEIU7nSFaiUlLspZ1fm4NfIcfABaxy9nT+fRVDkX7iV005T5Swil83gXUmxFUNKGal3j+hUxHOUDr+Aw==}
+
+ lib0@0.2.117:
+ resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==}
+ engines: {node: '>=16'}
+ hasBin: true
+
lie@3.1.1:
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
@@ -5467,6 +5588,11 @@ packages:
peerDependencies:
react: ^19.2.3
+ react-error-boundary@6.1.1:
+ resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0
+
react-hook-form@7.71.1:
resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==}
engines: {node: '>=18.0.0'}
@@ -5976,6 +6102,9 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+ tabbable@6.4.0:
+ resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
+
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
@@ -6519,6 +6648,10 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
+ yjs@13.6.30:
+ resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==}
+ engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+
yoctocolors-cjs@2.1.3:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
@@ -7278,19 +7411,44 @@ snapshots:
dependencies:
'@floating-ui/utils': 0.2.10
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
'@floating-ui/dom@1.7.3':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
'@floating-ui/react-dom@2.1.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@floating-ui/dom': 1.7.3
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
+ '@floating-ui/react-dom@2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ '@floating-ui/react@0.27.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@floating-ui/utils': 0.2.11
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ tabbable: 6.4.0
+
'@floating-ui/utils@0.2.10': {}
+ '@floating-ui/utils@0.2.11': {}
+
'@formatjs/intl-localematcher@0.6.2':
dependencies:
tslib: 2.8.1
@@ -7480,6 +7638,163 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@lexical/clipboard@0.42.0':
+ dependencies:
+ '@lexical/html': 0.42.0
+ '@lexical/list': 0.42.0
+ '@lexical/selection': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/code-core@0.42.0':
+ dependencies:
+ lexical: 0.42.0
+
+ '@lexical/devtools-core@0.42.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@lexical/html': 0.42.0
+ '@lexical/link': 0.42.0
+ '@lexical/mark': 0.42.0
+ '@lexical/table': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ '@lexical/dragon@0.42.0':
+ dependencies:
+ '@lexical/extension': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/extension@0.42.0':
+ dependencies:
+ '@lexical/utils': 0.42.0
+ '@preact/signals-core': 1.14.0
+ lexical: 0.42.0
+
+ '@lexical/hashtag@0.42.0':
+ dependencies:
+ '@lexical/text': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/history@0.42.0':
+ dependencies:
+ '@lexical/extension': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/html@0.42.0':
+ dependencies:
+ '@lexical/selection': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/link@0.42.0':
+ dependencies:
+ '@lexical/extension': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/list@0.42.0':
+ dependencies:
+ '@lexical/extension': 0.42.0
+ '@lexical/selection': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/mark@0.42.0':
+ dependencies:
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/markdown@0.42.0':
+ dependencies:
+ '@lexical/code-core': 0.42.0
+ '@lexical/link': 0.42.0
+ '@lexical/list': 0.42.0
+ '@lexical/rich-text': 0.42.0
+ '@lexical/text': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/offset@0.42.0':
+ dependencies:
+ lexical: 0.42.0
+
+ '@lexical/overflow@0.42.0':
+ dependencies:
+ lexical: 0.42.0
+
+ '@lexical/plain-text@0.42.0':
+ dependencies:
+ '@lexical/clipboard': 0.42.0
+ '@lexical/dragon': 0.42.0
+ '@lexical/selection': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/react@0.42.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(yjs@13.6.30)':
+ dependencies:
+ '@floating-ui/react': 0.27.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@lexical/devtools-core': 0.42.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@lexical/dragon': 0.42.0
+ '@lexical/extension': 0.42.0
+ '@lexical/hashtag': 0.42.0
+ '@lexical/history': 0.42.0
+ '@lexical/link': 0.42.0
+ '@lexical/list': 0.42.0
+ '@lexical/mark': 0.42.0
+ '@lexical/markdown': 0.42.0
+ '@lexical/overflow': 0.42.0
+ '@lexical/plain-text': 0.42.0
+ '@lexical/rich-text': 0.42.0
+ '@lexical/table': 0.42.0
+ '@lexical/text': 0.42.0
+ '@lexical/utils': 0.42.0
+ '@lexical/yjs': 0.42.0(yjs@13.6.30)
+ lexical: 0.42.0
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-error-boundary: 6.1.1(react@19.2.3)
+ transitivePeerDependencies:
+ - yjs
+
+ '@lexical/rich-text@0.42.0':
+ dependencies:
+ '@lexical/clipboard': 0.42.0
+ '@lexical/dragon': 0.42.0
+ '@lexical/selection': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/selection@0.42.0':
+ dependencies:
+ lexical: 0.42.0
+
+ '@lexical/table@0.42.0':
+ dependencies:
+ '@lexical/clipboard': 0.42.0
+ '@lexical/extension': 0.42.0
+ '@lexical/utils': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/text@0.42.0':
+ dependencies:
+ lexical: 0.42.0
+
+ '@lexical/utils@0.42.0':
+ dependencies:
+ '@lexical/selection': 0.42.0
+ lexical: 0.42.0
+
+ '@lexical/yjs@0.42.0(yjs@13.6.30)':
+ dependencies:
+ '@lexical/offset': 0.42.0
+ '@lexical/selection': 0.42.0
+ lexical: 0.42.0
+ yjs: 13.6.30
+
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.28.6
@@ -7858,6 +8173,8 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
+ '@preact/signals-core@1.14.0': {}
+
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
@@ -10890,6 +11207,8 @@ snapshots:
isexe@3.1.1: {}
+ isomorphic.js@0.2.5: {}
+
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
@@ -11010,6 +11329,12 @@ snapshots:
layout-base@2.0.1: {}
+ lexical@0.42.0: {}
+
+ lib0@0.2.117:
+ dependencies:
+ isomorphic.js: 0.2.5
+
lie@3.1.1:
dependencies:
immediate: 3.0.6
@@ -12204,6 +12529,10 @@ snapshots:
react: 19.2.3
scheduler: 0.27.0
+ react-error-boundary@6.1.1(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+
react-hook-form@7.71.1(react@19.2.3):
dependencies:
react: 19.2.3
@@ -12925,6 +13254,8 @@ snapshots:
symbol-tree@3.2.4:
optional: true
+ tabbable@6.4.0: {}
+
tagged-tag@1.0.0: {}
tailwind-merge@3.4.0: {}
@@ -13428,6 +13759,10 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
+ yjs@13.6.30:
+ dependencies:
+ lib0: 0.2.117
+
yoctocolors-cjs@2.1.3: {}
yoctocolors@2.1.2: {}