diff options
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx | 43 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx | 67 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/TextCard.tsx | 11 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/TextContentSection.tsx | 7 | ||||
| -rw-r--r-- | apps/web/components/ui/markdown/markdown-editor.tsx | 124 | ||||
| -rw-r--r-- | apps/web/components/ui/markdown/markdown-readonly.tsx (renamed from apps/web/components/ui/markdown-component.tsx) | 118 | ||||
| -rw-r--r-- | apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx | 290 | ||||
| -rw-r--r-- | apps/web/components/ui/markdown/theme.ts | 35 | ||||
| -rw-r--r-- | apps/web/components/ui/switch.tsx | 28 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/de/translation.json | 15 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 50 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/fr/translation.json | 15 | ||||
| -rw-r--r-- | apps/web/package.json | 7 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 496 |
14 files changed, 1177 insertions, 129 deletions
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx b/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx new file mode 100644 index 00000000..74eb0868 --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx @@ -0,0 +1,43 @@ +import MarkdownEditor from "@/components/ui/markdown/markdown-editor";
+import { MarkdownReadonly } from "@/components/ui/markdown/markdown-readonly";
+import { toast } from "@/components/ui/use-toast";
+
+import type { ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks";
+import { useUpdateBookmarkText } from "@hoarder/shared-react/hooks/bookmarks";
+
+export function BookmarkMarkdownComponent({
+ children: bookmark,
+ readOnly = true,
+}: {
+ children: ZBookmarkTypeText;
+ readOnly?: boolean;
+}) {
+ const { mutate: updateBookmarkMutator, isPending } = useUpdateBookmarkText({
+ onSuccess: () => {
+ toast({
+ description: "Note updated!",
+ });
+ },
+ onError: () => {
+ toast({ description: "Something went wrong", variant: "destructive" });
+ },
+ });
+
+ const onSave = (text: string) => {
+ updateBookmarkMutator({
+ bookmarkId: bookmark.id,
+ text,
+ });
+ };
+ return (
+ <div className="h-full overflow-hidden">
+ {readOnly ? (
+ <MarkdownReadonly>{bookmark.content.text}</MarkdownReadonly>
+ ) : (
+ <MarkdownEditor onSave={onSave} isSaving={isPending}>
+ {bookmark.content.text}
+ </MarkdownEditor>
+ )}
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx index e0434943..b2c27c7e 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx @@ -1,20 +1,12 @@ -import { useState } from "react"; -import { ActionButton } from "@/components/ui/action-button"; -import { Button } from "@/components/ui/button"; +import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent"; import { Dialog, - DialogClose, DialogContent, - DialogDescription, - DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Textarea } from "@/components/ui/textarea"; -import { toast } from "@/components/ui/use-toast"; -import { useUpdateBookmarkText } from "@hoarder/shared-react/hooks/bookmarks"; -import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { ZBookmark, ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks"; export function BookmarkedTextEditor({ bookmark, @@ -26,55 +18,20 @@ export function BookmarkedTextEditor({ setOpen: (open: boolean) => void; }) { const isNewBookmark = bookmark === undefined; - const [noteText, setNoteText] = useState( - bookmark && bookmark.content.type == BookmarkTypes.TEXT - ? bookmark.content.text - : "", - ); - - const { mutate: updateBookmarkMutator, isPending } = useUpdateBookmarkText({ - onSuccess: () => { - toast({ - description: "Note updated!", - }); - setOpen(false); - }, - onError: () => { - toast({ description: "Something went wrong", variant: "destructive" }); - }, - }); - - const onSave = () => { - updateBookmarkMutator({ - bookmarkId: bookmark.id, - text: noteText, - }); - }; return ( <Dialog open={open} onOpenChange={setOpen}> - <DialogContent> - <DialogHeader> - <DialogTitle>{isNewBookmark ? "New Note" : "Edit Note"}</DialogTitle> - <DialogDescription> - Write your note with markdown support - </DialogDescription> + <DialogContent className="max-w-[80%]"> + <DialogHeader className="flex"> + <DialogTitle className="w-fit"> + {isNewBookmark ? "New Note" : "Edit Note"} + </DialogTitle> </DialogHeader> - <Textarea - value={noteText} - onChange={(e) => setNoteText(e.target.value)} - className="h-52 grow" - /> - <DialogFooter className="flex-shrink gap-1 sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton type="button" loading={isPending} onClick={onSave}> - Save - </ActionButton> - </DialogFooter> + <div className="h-[80vh]"> + <BookmarkMarkdownComponent readOnly={false}> + {bookmark as ZBookmarkTypeText} + </BookmarkMarkdownComponent> + </div> </DialogContent> </Dialog> ); diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx index 14a4f905..9d168910 100644 --- a/apps/web/components/dashboard/bookmarks/TextCard.tsx +++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; -import { MarkdownComponent } from "@/components/ui/markdown-component"; +import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent"; import { bookmarkLayoutSwitch } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; @@ -20,15 +20,16 @@ export default function TextCard({ bookmark: ZBookmarkTypeText; className?: string; }) { - const bookmarkedText = bookmark.content; - const banner = bookmark.assets.find((a) => a.assetType == "bannerImage"); - return ( <> <BookmarkLayoutAdaptingCard title={bookmark.title} - content={<MarkdownComponent>{bookmarkedText.text}</MarkdownComponent>} + content={ + <BookmarkMarkdownComponent readOnly={true}> + {bookmark} + </BookmarkMarkdownComponent> + } footer={ getSourceUrl(bookmark) && ( <FooterLinkURL url={getSourceUrl(bookmark)} /> diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx index 327436c6..a58bc717 100644 --- a/apps/web/components/dashboard/preview/TextContentSection.tsx +++ b/apps/web/components/dashboard/preview/TextContentSection.tsx @@ -1,7 +1,8 @@ import Image from "next/image"; -import { MarkdownComponent } from "@/components/ui/markdown-component"; +import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent"; import { ScrollArea } from "@radix-ui/react-scroll-area"; +import type { ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks"; import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils"; import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; @@ -27,7 +28,9 @@ export function TextContentSection({ bookmark }: { bookmark: ZBookmark }) { /> </div> )} - <MarkdownComponent>{bookmark.content.text}</MarkdownComponent> + <BookmarkMarkdownComponent> + {bookmark as ZBookmarkTypeText} + </BookmarkMarkdownComponent> </ScrollArea> ); } diff --git a/apps/web/components/ui/markdown/markdown-editor.tsx b/apps/web/components/ui/markdown/markdown-editor.tsx new file mode 100644 index 00000000..85f0c878 --- /dev/null +++ b/apps/web/components/ui/markdown/markdown-editor.tsx @@ -0,0 +1,124 @@ +import { memo, useMemo, useState } from "react"; +import ToolbarPlugin from "@/components/ui/markdown/plugins/toolbar-plugin"; +import { MarkdownEditorTheme } from "@/components/ui/markdown/theme"; +import { + CodeHighlightNode, + CodeNode, + registerCodeHighlighting, +} from "@lexical/code"; +import { LinkNode } from "@lexical/link"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { + $convertFromMarkdownString, + $convertToMarkdownString, + TRANSFORMERS, +} from "@lexical/markdown"; +import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; +import { + InitialConfigType, + LexicalComposer, +} from "@lexical/react/LexicalComposer"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; +import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; +import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; +import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin"; +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { $getRoot, EditorState, LexicalEditor } from "lexical"; + +function onError(error: Error) { + console.error(error); +} + +const EDITOR_NODES = [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + LinkNode, + CodeNode, + HorizontalRuleNode, + CodeHighlightNode, +]; + +interface MarkdownEditorProps { + children: string; + onSave?: (markdown: string) => void; + isSaving?: boolean; +} + +const MarkdownEditor = memo( + ({ children: initialMarkdown, onSave, isSaving }: MarkdownEditorProps) => { + const [isRawMarkdownMode, setIsRawMarkdownMode] = useState(false); + const [rawMarkdown, setRawMarkdown] = useState(initialMarkdown); + + const initialConfig: InitialConfigType = useMemo( + () => ({ + namespace: "editor", + onError, + theme: MarkdownEditorTheme, + nodes: EDITOR_NODES, + editorState: (editor: LexicalEditor) => { + registerCodeHighlighting(editor); + $convertFromMarkdownString(initialMarkdown, TRANSFORMERS); + }, + }), + [initialMarkdown], + ); + + const handleOnChange = (editorState: EditorState) => { + editorState.read(() => { + let markdownString; + if (isRawMarkdownMode) { + markdownString = $getRoot()?.getFirstChild()?.getTextContent() ?? ""; + } else { + markdownString = $convertToMarkdownString(TRANSFORMERS); + } + setRawMarkdown(markdownString); + }); + }; + + return ( + <LexicalComposer initialConfig={initialConfig}> + <div className="flex h-full flex-col justify-stretch"> + <ToolbarPlugin + isRawMarkdownMode={isRawMarkdownMode} + setIsRawMarkdownMode={setIsRawMarkdownMode} + onSave={onSave && (() => onSave(rawMarkdown))} + isSaving={!!isSaving} + /> + {isRawMarkdownMode ? ( + <PlainTextPlugin + contentEditable={ + <ContentEditable className="h-full w-full min-w-full overflow-auto p-2" /> + } + ErrorBoundary={LexicalErrorBoundary} + /> + ) : ( + <RichTextPlugin + contentEditable={ + <ContentEditable className="prose h-full w-full min-w-full overflow-auto p-2 dark:prose-invert prose-p:m-0" /> + } + ErrorBoundary={LexicalErrorBoundary} + /> + )} + </div> + <HistoryPlugin /> + <AutoFocusPlugin /> + <TabIndentationPlugin /> + <MarkdownShortcutPlugin transformers={TRANSFORMERS} /> + <OnChangePlugin onChange={handleOnChange} /> + <ListPlugin /> + </LexicalComposer> + ); + }, +); +// needed for linter because of memo +MarkdownEditor.displayName = "MarkdownEditor"; + +export default MarkdownEditor; diff --git a/apps/web/components/ui/markdown-component.tsx b/apps/web/components/ui/markdown/markdown-readonly.tsx index d3c832ac..29077480 100644 --- a/apps/web/components/ui/markdown-component.tsx +++ b/apps/web/components/ui/markdown/markdown-readonly.tsx @@ -1,61 +1,57 @@ -import React from "react";
-import CopyBtn from "@/components/ui/copy-button";
-import { cn } from "@/lib/utils";
-import Markdown from "react-markdown";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
-import remarkBreaks from "remark-breaks";
-import remarkGfm from "remark-gfm";
-
-function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) {
- const ref = React.useRef<HTMLPreElement>(null);
- return (
- <span className="group relative">
- <CopyBtn
- className="absolute right-1 top-1 m-1 hidden text-white group-hover:block"
- getStringToCopy={() => {
- return ref.current?.textContent ?? "";
- }}
- />
- <pre ref={ref} className={cn(className, "")} {...props} />
- </span>
- );
-}
-
-export function MarkdownComponent({
- children: markdown,
-}: {
- children: string;
-}) {
- return (
- <Markdown
- remarkPlugins={[remarkGfm, remarkBreaks]}
- className="prose dark:prose-invert"
- components={{
- pre({ ...props }) {
- return <PreWithCopyBtn {...props} />;
- },
- code({ className, children, ...props }) {
- const match = /language-(\w+)/.exec(className ?? "");
- return match ? (
- // @ts-expect-error -- Refs are not compatible for some reason
- <SyntaxHighlighter
- PreTag="div"
- language={match[1]}
- {...props}
- style={dracula}
- >
- {String(children).replace(/\n$/, "")}
- </SyntaxHighlighter>
- ) : (
- <code className={className} {...props}>
- {children}
- </code>
- );
- },
- }}
- >
- {markdown}
- </Markdown>
- );
-}
+import React from "react"; +import CopyBtn from "@/components/ui/copy-button"; +import { cn } from "@/lib/utils"; +import Markdown from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import remarkBreaks from "remark-breaks"; +import remarkGfm from "remark-gfm"; + +function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) { + const ref = React.useRef<HTMLPreElement>(null); + return ( + <span className="group relative"> + <CopyBtn + className="absolute right-1 top-1 m-1 hidden text-white group-hover:block" + getStringToCopy={() => { + return ref.current?.textContent ?? ""; + }} + /> + <pre ref={ref} className={cn(className, "")} {...props} /> + </span> + ); +} + +export function MarkdownReadonly({ children: markdown }: { children: string }) { + return ( + <Markdown + remarkPlugins={[remarkGfm, remarkBreaks]} + className="prose dark:prose-invert" + components={{ + pre({ ...props }) { + return <PreWithCopyBtn {...props} />; + }, + code({ className, children, ...props }) { + const match = /language-(\w+)/.exec(className ?? ""); + return match ? ( + // @ts-expect-error -- Refs are not compatible for some reason + <SyntaxHighlighter + PreTag="div" + language={match[1]} + {...props} + style={dracula} + > + {String(children).replace(/\n$/, "")} + </SyntaxHighlighter> + ) : ( + <code className={className} {...props}> + {children} + </code> + ); + }, + }} + > + {markdown} + </Markdown> + ); +} diff --git a/apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx b/apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx new file mode 100644 index 00000000..28e265e2 --- /dev/null +++ b/apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx @@ -0,0 +1,290 @@ +import { useCallback, useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useTranslation } from "@/lib/i18n/client"; +import { + $convertFromMarkdownString, + $convertToMarkdownString, + TRANSFORMERS, +} from "@lexical/markdown"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { mergeRegister } from "@lexical/utils"; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $getSelection, + $isRangeSelection, + FORMAT_TEXT_COMMAND, + LexicalCommand, + SELECTION_CHANGE_COMMAND, + TextFormatType, +} from "lexical"; +import { + Bold, + Code, + Highlighter, + Italic, + LucideIcon, + Save, + Strikethrough, +} from "lucide-react"; + +import { ActionButton } from "../../action-button"; +import InfoTooltip from "../../info-tooltip"; +import { Label } from "../../label"; +import { Switch } from "../../switch"; + +const LowPriority = 1; + +function MarkdownToolTip() { + const { t } = useTranslation(); + return ( + <InfoTooltip size={15}> + <table className="w-full table-auto text-left text-sm"> + <thead> + <tr> + <th>{t("editor.text_toolbar.markdown_shortcuts.label")}</th> + </tr> + </thead> + <tbody> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.heading.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.heading.example")} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.bold.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.bold.example")} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.italic.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.italic.example")} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.blockquote.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.blockquote.example")} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.ordered_list.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.ordered_list.example")} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.unordered_list.label")} + </td> + <td className="py-2"> + {t( + "editor.text_toolbar.markdown_shortcuts.unordered_list.example", + )} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.inline_code.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.inline_code.example")} + </td> + </tr> + <tr className="border-b"> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.block_code.label")} + </td> + <td className="py-2"> + {t("editor.text_toolbar.markdown_shortcuts.block_code.example")} + </td> + </tr> + </tbody> + </table> + </InfoTooltip> + ); +} + +export default function ToolbarPlugin({ + isRawMarkdownMode = false, + setIsRawMarkdownMode, + onSave, + isSaving, +}: { + isRawMarkdownMode: boolean; + setIsRawMarkdownMode: (value: boolean) => void; + onSave?: () => void; + isSaving: boolean; +}) { + const { t } = useTranslation(); + const [editor] = useLexicalComposerContext(); + const [editorToolbarState, setEditorToolbarState] = useState<{ + isBold: boolean; + isItalic: boolean; + isStrikethrough: boolean; + isHighlight: boolean; + isCode: boolean; + }>({ + isBold: false, + isItalic: false, + isStrikethrough: false, + isHighlight: false, + isCode: false, + }); + + const $updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + setEditorToolbarState({ + isBold: selection.hasFormat("bold"), + isItalic: selection.hasFormat("italic"), + isStrikethrough: selection.hasFormat("strikethrough"), + isHighlight: selection.hasFormat("highlight"), + isCode: selection.hasFormat("code"), + }); + } + }, []); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + $updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, _newEditor) => { + $updateToolbar(); + return false; + }, + LowPriority, + ), + ); + }, [editor, $updateToolbar]); + + const formatButtons: { + command: LexicalCommand<TextFormatType>; + format: TextFormatType; + isActive?: boolean; + icon: LucideIcon; + label: string; + }[] = [ + { + command: FORMAT_TEXT_COMMAND, + format: "bold", + icon: Bold, + isActive: editorToolbarState.isBold, + label: t("editor.text_toolbar.bold"), + }, + { + command: FORMAT_TEXT_COMMAND, + format: "italic", + icon: Italic, + isActive: editorToolbarState.isItalic, + label: t("editor.text_toolbar.italic"), + }, + { + command: FORMAT_TEXT_COMMAND, + format: "strikethrough", + icon: Strikethrough, + isActive: editorToolbarState.isStrikethrough, + label: t("editor.text_toolbar.strikethrough"), + }, + { + command: FORMAT_TEXT_COMMAND, + format: "code", + icon: Code, + isActive: editorToolbarState.isCode, + label: t("editor.text_toolbar.code"), + }, + { + command: FORMAT_TEXT_COMMAND, + format: "highlight", + icon: Highlighter, + isActive: editorToolbarState.isHighlight, + label: t("editor.text_toolbar.highlight"), + }, + ]; + + const handleRawMarkdownToggle = useCallback(() => { + editor.update(() => { + console.log(isRawMarkdownMode); + const root = $getRoot(); + const firstChild = root.getFirstChild(); + if (isRawMarkdownMode) { + if (firstChild) { + $convertFromMarkdownString(firstChild.getTextContent(), TRANSFORMERS); + } + setIsRawMarkdownMode(false); + } else { + const markdown = $convertToMarkdownString(TRANSFORMERS); + const pNode = $createParagraphNode(); + pNode.append($createTextNode(markdown)); + root.clear().append(pNode); + setIsRawMarkdownMode(true); + } + }); + }, [editor, isRawMarkdownMode]); + + return ( + <div className="mb-1 flex items-center justify-between rounded-t-lg p-1"> + <div className="flex"> + {formatButtons.map( + ({ command, format, icon: Icon, isActive, label }) => ( + <Button + key={format} + disabled={isRawMarkdownMode} + size={"sm"} + onClick={() => { + editor.dispatchCommand(command, format); + }} + variant={isActive ? "default" : "ghost"} + aria-label={label} + > + <Icon className="h-4 w-4" /> + </Button> + ), + )} + </div> + <div className="flex items-center gap-2"> + <div className="flex items-center space-x-2"> + <Switch + id="editor-raw-markdown" + onCheckedChange={handleRawMarkdownToggle} + checked={isRawMarkdownMode} + /> + <Label htmlFor="editor-raw-markdown">Raw Markdown</Label> + </div> + {onSave && ( + <ActionButton + loading={isSaving} + className="flex items-center gap-2" + size={"sm"} + onClick={() => { + onSave?.(); + }} + > + <Save className="size-4" /> + Save + </ActionButton> + )} + <MarkdownToolTip /> + </div> + </div> + ); +} diff --git a/apps/web/components/ui/markdown/theme.ts b/apps/web/components/ui/markdown/theme.ts new file mode 100644 index 00000000..ff088e32 --- /dev/null +++ b/apps/web/components/ui/markdown/theme.ts @@ -0,0 +1,35 @@ +export const MarkdownEditorTheme = { + code: "bg-[#282A36] text-[#F8F8F2] font-mono block px-4 py-2 my-2 text-sm overflow-x-auto relative rounded-md shadow-sm", + codeHighlight: { + atrule: "text-[#8BE9FD]", + attr: "text-[#8BE9FD]", + boolean: "text-[#FF79C6]", + builtin: "text-[#50FA7B]", + cdata: "text-[#6272A4]", + char: "text-[#50FA7B]", + class: "text-[#FF79C6]", + "class-name": "text-[#FF79C6]", + comment: "text-[#6272A4]", + constant: "text-[#FF79C6]", + deleted: "text-[#FF5555]", + doctype: "text-[#6272A4]", + entity: "text-[#FFB86C]", + function: "text-[#50FA7B]", + important: "text-[#F1FA8C]", + inserted: "text-[#50FA7B]", + keyword: "text-[#FF79C6]", + namespace: "text-[#F1FA8C]", + number: "text-[#BD93F9]", + operator: "text-[#FFB86C]", + prolog: "text-[#6272A4]", + property: "text-[#FFB86C]", + punctuation: "text-[#F8F8F2]", + regex: "text-[#FF5555]", + selector: "text-[#50FA7B]", + string: "text-[#F1FA8C]", + symbol: "text-[#FF79C6]", + tag: "text-[#FF79C6]", + url: "text-[#8BE9FD]", + variable: "text-[#F1FA8C]", + }, +}; diff --git a/apps/web/components/ui/switch.tsx b/apps/web/components/ui/switch.tsx new file mode 100644 index 00000000..2438dc86 --- /dev/null +++ b/apps/web/components/ui/switch.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +const Switch = React.forwardRef< + React.ElementRef<typeof SwitchPrimitives.Root>, + React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> +>(({ className, ...props }, ref) => ( + <SwitchPrimitives.Root + className={cn( + "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", + className, + )} + {...props} + ref={ref} + > + <SwitchPrimitives.Thumb + className={cn( + "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0", + )} + /> + </SwitchPrimitives.Root> +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/apps/web/lib/i18n/locales/de/translation.json b/apps/web/lib/i18n/locales/de/translation.json index 40a167ad..d744dc97 100644 --- a/apps/web/lib/i18n/locales/de/translation.json +++ b/apps/web/lib/i18n/locales/de/translation.json @@ -180,7 +180,20 @@ "import_as_separate_bookmarks": "Als separate Lesezeichen importieren", "placeholder": "Fügen Sie einen Link oder ein Bild ein, schreiben Sie eine Notiz oder ziehen Sie ein Bild hierher ...", "new_item": "NEUER EINTRAG", - "disabled_submissions": "Einsendungen sind deaktiviert" + "disabled_submissions": "Einsendungen sind deaktiviert", + "text_toolbar": { + "undo": "Rückgängig", + "redo": "Wiederholen", + "bold": "Fett", + "italic": "Kursiv", + "underline": "Unterstrichen", + "strikethrough": "Durchgestrichen", + "code": "Code", + "highlight": "Hervorheben", + "align_left": "Linksbündig", + "align_center": "Zentriert", + "align_right": "Rechtsbündig" + } }, "toasts": { "bookmarks": { diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 9f12487f..a871489a 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -187,7 +187,55 @@ "import_as_separate_bookmarks": "Import as separate Bookmarks", "placeholder": "Paste a link or an image, write a note or drag and drop an image in here ...", "new_item": "NEW ITEM", - "disabled_submissions": "Submissions are disabled" + "disabled_submissions": "Submissions are disabled", + "text_toolbar": { + "undo": "Undo", + "redo": "Redo", + "bold": "Bold", + "italic": "Italic", + "underline": "Underline", + "strikethrough": "Strikethrough", + "code": "Code", + "highlight": "Highlight", + "align_left": "Left Align", + "align_center": "Center Align", + "align_right": "Right Align", + "markdown_shortcuts": { + "label": "Markdown shortcuts", + "heading": { + "label": "Heading", + "example": "# H1, ## H2, ### H3" + }, + "bold": { + "label": "Bold", + "example": "**text** or CTRL+b" + }, + "italic": { + "label": "Italic", + "example": "*Italic* or _Italic_ or CTRL+i" + }, + "blockquote": { + "label": "Blockquote", + "example": "> Blockquote" + }, + "ordered_list": { + "label": "Ordered List", + "example": "1. List item" + }, + "unordered_list": { + "label": "Unordered List", + "example": "- List item" + }, + "inline_code": { + "label": "Inline Code", + "example": "`Code`" + }, + "block_code": { + "label": "Block Code", + "example": "``` + space" + } + } + } }, "toasts": { "bookmarks": { diff --git a/apps/web/lib/i18n/locales/fr/translation.json b/apps/web/lib/i18n/locales/fr/translation.json index d369ad44..142e9148 100644 --- a/apps/web/lib/i18n/locales/fr/translation.json +++ b/apps/web/lib/i18n/locales/fr/translation.json @@ -180,7 +180,20 @@ "import_as_separate_bookmarks": "Importer comme favoris séparés", "placeholder": "Collez un lien ou une image, écrivez une note ou glissez-déposez une image ici ...", "new_item": "NOUVEL ÉLÉMENT", - "disabled_submissions": "Les soumissions sont désactivées" + "disabled_submissions": "Les soumissions sont désactivées", + "text_toolbar": { + "undo": "Annuler", + "redo": "Rétablir", + "bold": "Gras", + "italic": "Italique", + "underline": "Souligné", + "strikethrough": "Barré", + "code": "Code", + "highlight": "Surligner", + "align_left": "Aligner à gauche", + "align_center": "Aligner au centre", + "align_right": "Aligner à droite" + } }, "toasts": { "bookmarks": { diff --git a/apps/web/package.json b/apps/web/package.json index 3affe516..7edae7a8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,11 @@ "@hoarder/shared-react": "workspace:^0.1.0", "@hoarder/trpc": "workspace:^0.1.0", "@hookform/resolvers": "^3.3.4", + "@lexical/list": "^0.20.2", + "@lexical/markdown": "^0.20.2", + "@lexical/plain-text": "^0.20.2", + "@lexical/react": "^0.20.2", + "@lexical/rich-text": "^0.20.2", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -33,6 +38,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toggle": "^1.0.3", @@ -53,6 +59,7 @@ "fastest-levenshtein": "^1.0.16", "i18next": "^23.16.5", "i18next-resources-to-backend": "^1.2.1", + "lexical": "^0.20.2", "lucide-react": "^0.330.0", "next": "14.2.13", "next-auth": "^4.24.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c440d82b..959c9871 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -478,6 +478,21 @@ importers: '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.50.1(react@18.3.1)) + '@lexical/list': + specifier: ^0.20.2 + version: 0.20.2 + '@lexical/markdown': + specifier: ^0.20.2 + version: 0.20.2 + '@lexical/plain-text': + specifier: ^0.20.2 + version: 0.20.2 + '@lexical/react': + specifier: ^0.20.2 + version: 0.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.20) + '@lexical/rich-text': + specifier: ^0.20.2 + version: 0.20.2 '@radix-ui/react-collapsible': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -508,6 +523,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.58)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -568,6 +586,9 @@ importers: i18next-resources-to-backend: specifier: ^1.2.1 version: 1.2.1 + lexical: + specifier: ^0.20.2 + version: 0.20.2 lucide-react: specifier: ^0.330.0 version: 0.330.0(react@18.3.1) @@ -3178,6 +3199,77 @@ packages: '@leichtgewicht/ip-codec@2.0.4': resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} + '@lexical/clipboard@0.20.2': + resolution: {integrity: sha512-pdgSmrUhOKo23kYBzzkybKipRebaEglHBlJr0E5B8cDr2f3pWVwr0/eRYxgmm5zzg+ZSNpGIdGvpFxFhKhkc4w==} + + '@lexical/code@0.20.2': + resolution: {integrity: sha512-+sU6+5MXbwGqdHxKCncFSGpFFgR6iIx86SUEmw+sBOjnFfCXeHJo7FUQKKPI04TvoT3+7eCAwdV2rTKlj2lfsg==} + + '@lexical/devtools-core@0.20.2': + resolution: {integrity: sha512-jz7+ohju3gcs24dXIYryLD2Ekr/2U8qOhiAIGuWuC1OZgRI2X/x/jGhCxh3cCUBl0JfEHnbOiaCQpWK2wPLa0A==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/dragon@0.20.2': + resolution: {integrity: sha512-Ql+VXmNdh9fAqxYfPpsOsVHcs81EtUqFabgBQC8/xJdbA9Y6mfrj7g7JfYt0M05qCdsK2noI1VgPSWH+gRRDNg==} + + '@lexical/hashtag@0.20.2': + resolution: {integrity: sha512-iHCHtCBmlGOqyRhfzrF5wi9E2QMv5bJcMfgjR4GQKD+TPbajyM4z/SXPoS7R8ii1d7pp1YTyufdCcRcw4bIx5g==} + + '@lexical/history@0.20.2': + resolution: {integrity: sha512-cinRrW0+hlfnr6+Lv4bIDe0SmNyjqiKj6vbLHYymNcdgq3RjtoJ/fERgZHsDY+rxSC8fJ8q/Eq0ZKDgp1QLTcQ==} + + '@lexical/html@0.20.2': + resolution: {integrity: sha512-6+fdqu973wdjsWr64UP/o9X/ON3vOSBupikP3D706kpvbpTjpmUGtucSB9gEXrBas1IfJCCZ6f5g1OSmKFn4wQ==} + + '@lexical/link@0.20.2': + resolution: {integrity: sha512-tcaVIIbkJujx3SEBGFu/ibRxnRc2kAk9C7oCwrRLrOycASngXWS3jCpIRICsW6jQ8ZlT475GZmcXD0Atf9BqXQ==} + + '@lexical/list@0.20.2': + resolution: {integrity: sha512-laQaGIsWIMwmwv35OB8D87AtwrguGDFiEh6Ist3N9yI8KA+R2WlJlmSZvu4MYjG3VM5jlfl0U8ADurjZ1NpNzw==} + + '@lexical/mark@0.20.2': + resolution: {integrity: sha512-T2IQwbbt3f2BgYR3F5kO5sN0dWgXQCBdn3iv1+EXphog1U3tV1EU1N697A74nZB5f6cIEdvnYjNEx8pYaNthpw==} + + '@lexical/markdown@0.20.2': + resolution: {integrity: sha512-jk6tAvjLsXL1nv+ZBMhYkBuhT6QlxX46PyzTG4lIUOdkvLZNQUzQD+aMKmw0IGynz0bvY/aTH3RBniWZc6fzLQ==} + + '@lexical/offset@0.20.2': + resolution: {integrity: sha512-z89cr8jJHTH7UsI71ojBs66ef+RFHX4RXCN5bXs2caKg4P4rnVx8gxRI9vg8JVFVIsXQViJeJTx3KzkZAzTf0A==} + + '@lexical/overflow@0.20.2': + resolution: {integrity: sha512-mwVSHQyIm4tpqs8/ZWrb7YSdjFALNWhp6O5Zi4Zh3QPKguQ8I9A+zKPqp6YfpR5eluiWNqCndrXXspbfAVj/0Q==} + + '@lexical/plain-text@0.20.2': + resolution: {integrity: sha512-mDIj1J3ZDrV8b88aW7ttsGnJkM3yfvIsJpRZD1aQI5sjngoJHNxKjiN9MOTg7hlgTubZlQ/98AfUbyb/EPOhoA==} + + '@lexical/react@0.20.2': + resolution: {integrity: sha512-j01mrUVbqzPs+/FaL7ny8tdT9iQ/P0iaEZ0d5qZbC7Z2Z/bGWmjkXuQOeirLkFJsPRsYqnQwoIoUN3cMzc3EGw==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/rich-text@0.20.2': + resolution: {integrity: sha512-YtfPFbG3j9ySPMGDHpVYfHaHlmmNx/hmhEupq1PPkKPcwwGWKDJUsdx3TLrSUSHau1mm06hR7Aacw1FMwnGHGg==} + + '@lexical/selection@0.20.2': + resolution: {integrity: sha512-Ta3fiu9NyY5p2eVwHZboyTTPqm8D9GassNm3inc0xBn4qC+XD3MQLFAoWSzgGaw06bLlHsDGDV/9BbWve8uSZg==} + + '@lexical/table@0.20.2': + resolution: {integrity: sha512-m1DxAg4WUJ5FqnlLQtrZeRtigldaFQlutLtIU2+KthHo/4Ah3fBcmM3ZwPEz7W0ALQmLsaUKjwGNkvUy+5qCdw==} + + '@lexical/text@0.20.2': + resolution: {integrity: sha512-Zw6WagBUp/OmANloFstSC9qdIBY22CB68iKngmbrGeoq8UDJRqyPKNdGL4cpj3xLncw+YC5mbuT40HtfgM9yiQ==} + + '@lexical/utils@0.20.2': + resolution: {integrity: sha512-xH6eVNQ9ugKp2gPHwXIXoBzorZiXrxq55ORZCvkVJi8BeUoGI0si/4Rq7MyvPoIbunuPdWiVfMYia0RrL+8HRQ==} + + '@lexical/yjs@0.20.2': + resolution: {integrity: sha512-k5jaOyDa0c/B3FwYDYLW7s/11hZYXjqlLy1C/a+ftr7b25MQhAMxDIe/I7aLi4ky2ul+TguozTrlqxz/JXIf4w==} + peerDependencies: + yjs: '>=13.5.22' + '@mapbox/node-pre-gyp@1.0.11': resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -3335,6 +3427,9 @@ packages: '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-arrow@1.0.3': resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -3397,6 +3492,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.0.1': resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: @@ -3415,6 +3519,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.0.5': resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} peerDependencies: @@ -3598,6 +3711,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-progress@1.1.0': resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} peerDependencies: @@ -3686,6 +3812,28 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.1.2': + resolution: {integrity: sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.0.4': resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: @@ -3747,6 +3895,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.0.1': resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: @@ -3756,6 +3913,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.0.3': resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: @@ -3774,6 +3940,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-previous@1.0.1': resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} peerDependencies: @@ -3783,6 +3958,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.0.1': resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -3801,6 +3985,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-visually-hidden@1.0.3': resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} peerDependencies: @@ -4902,6 +5095,7 @@ packages: acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -5716,6 +5910,10 @@ packages: resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} engines: {node: '>=6'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cmdk@1.0.0: resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} peerDependencies: @@ -8357,6 +8555,9 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + isostring@0.0.1: resolution: {integrity: sha512-wRcdJtXCe2LGtXnD14fXMkduWVdbeGkzBIKg8WcKeEOi6SIc+hRjYYw76WNx3v5FebhUWZrBTWB0NOl3/sagdQ==} @@ -8623,6 +8824,14 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lexical@0.20.2: + resolution: {integrity: sha512-hWWuRcLt99s9B5uuf6RnGWV+zFEJ4mKTlloK4SxSvgESLhyOW9KW1FDnFFCDSmztm5jcPG8Aj6fmY3bo4lTy7w==} + + lib0@0.2.98: + resolution: {integrity: sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==} + engines: {node: '>=16'} + hasBin: true + lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} @@ -10778,6 +10987,12 @@ packages: peerDependencies: react: '>= 16.8 || 18.0.0' + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + react-error-overlay@6.0.11: resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} @@ -13045,6 +13260,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yjs@13.6.20: + resolution: {integrity: sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -15201,7 +15420,7 @@ snapshots: '@docusaurus/utils-common': 3.5.2(@docusaurus/types@3.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@docusaurus/utils-validation': 3.5.2(@docusaurus/types@3.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.3.3) '@mdx-js/react': 3.0.1(@types/react@18.3.12)(react@18.3.1) - clsx: 2.1.0 + clsx: 2.1.1 copy-text-to-clipboard: 3.2.0 infima: 0.2.0-alpha.44 lodash: 4.17.21 @@ -15245,7 +15464,7 @@ snapshots: '@types/history': 4.7.11 '@types/react': 18.3.12 '@types/react-router-config': 5.0.11 - clsx: 2.1.0 + clsx: 2.1.1 parse-numeric-range: 1.3.0 prism-react-renderer: 2.3.1(react@18.3.1) react: 18.3.1 @@ -15274,7 +15493,7 @@ snapshots: '@docusaurus/utils-validation': 3.5.2(@docusaurus/types@3.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.3.3) algoliasearch: 4.22.1 algoliasearch-helper: 3.16.3(algoliasearch@4.22.1) - clsx: 2.1.0 + clsx: 2.1.1 eta: 2.2.0 fs-extra: 11.2.0 lodash: 4.17.21 @@ -16317,6 +16536,174 @@ snapshots: '@leichtgewicht/ip-codec@2.0.4': dev: false + '@lexical/clipboard@0.20.2': + dependencies: + '@lexical/html': 0.20.2 + '@lexical/list': 0.20.2 + '@lexical/selection': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/code@0.20.2': + dependencies: + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + prismjs: 1.29.0 + dev: false + + '@lexical/devtools-core@0.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@lexical/html': 0.20.2 + '@lexical/link': 0.20.2 + '@lexical/mark': 0.20.2 + '@lexical/table': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + '@lexical/dragon@0.20.2': + dependencies: + lexical: 0.20.2 + dev: false + + '@lexical/hashtag@0.20.2': + dependencies: + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/history@0.20.2': + dependencies: + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/html@0.20.2': + dependencies: + '@lexical/selection': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/link@0.20.2': + dependencies: + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/list@0.20.2': + dependencies: + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/mark@0.20.2': + dependencies: + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/markdown@0.20.2': + dependencies: + '@lexical/code': 0.20.2 + '@lexical/link': 0.20.2 + '@lexical/list': 0.20.2 + '@lexical/rich-text': 0.20.2 + '@lexical/text': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/offset@0.20.2': + dependencies: + lexical: 0.20.2 + dev: false + + '@lexical/overflow@0.20.2': + dependencies: + lexical: 0.20.2 + dev: false + + '@lexical/plain-text@0.20.2': + dependencies: + '@lexical/clipboard': 0.20.2 + '@lexical/selection': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/react@0.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.20)': + dependencies: + '@lexical/clipboard': 0.20.2 + '@lexical/code': 0.20.2 + '@lexical/devtools-core': 0.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@lexical/dragon': 0.20.2 + '@lexical/hashtag': 0.20.2 + '@lexical/history': 0.20.2 + '@lexical/link': 0.20.2 + '@lexical/list': 0.20.2 + '@lexical/mark': 0.20.2 + '@lexical/markdown': 0.20.2 + '@lexical/overflow': 0.20.2 + '@lexical/plain-text': 0.20.2 + '@lexical/rich-text': 0.20.2 + '@lexical/selection': 0.20.2 + '@lexical/table': 0.20.2 + '@lexical/text': 0.20.2 + '@lexical/utils': 0.20.2 + '@lexical/yjs': 0.20.2(yjs@13.6.20) + lexical: 0.20.2 + 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 + dev: false + + '@lexical/rich-text@0.20.2': + dependencies: + '@lexical/clipboard': 0.20.2 + '@lexical/selection': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/selection@0.20.2': + dependencies: + lexical: 0.20.2 + dev: false + + '@lexical/table@0.20.2': + dependencies: + '@lexical/clipboard': 0.20.2 + '@lexical/utils': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/text@0.20.2': + dependencies: + lexical: 0.20.2 + dev: false + + '@lexical/utils@0.20.2': + dependencies: + '@lexical/list': 0.20.2 + '@lexical/selection': 0.20.2 + '@lexical/table': 0.20.2 + lexical: 0.20.2 + dev: false + + '@lexical/yjs@0.20.2(yjs@13.6.20)': + dependencies: + '@lexical/offset': 0.20.2 + '@lexical/selection': 0.20.2 + lexical: 0.20.2 + yjs: 13.6.20 + dev: false + '@mapbox/node-pre-gyp@1.0.11': dependencies: detect-libc: 2.0.3 @@ -16549,6 +16936,9 @@ snapshots: '@babel/runtime': 7.26.0 dev: false + '@radix-ui/primitive@1.1.1': + dev: false + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -16608,6 +16998,12 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-context@1.0.1(@types/react@18.2.58)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -16621,6 +17017,12 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-context@1.1.1(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.23.9 @@ -16828,6 +17230,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) dev: false + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.58)(react@18.3.1) + '@types/react': 18.2.58 + '@types/react-dom': 18.2.19 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + '@radix-ui/react-progress@1.1.0(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.0(@types/react@18.2.58)(react@18.3.1) @@ -16936,6 +17347,28 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-slot@1.1.1(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.58)(react@18.3.1) + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + + '@radix-ui/react-switch@1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.58)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.58)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.58)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.58)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.58)(react@18.3.1) + '@types/react': 18.2.58 + '@types/react-dom': 18.2.19 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + '@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.23.9 @@ -17014,6 +17447,12 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.58)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -17022,6 +17461,13 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.58)(react@18.3.1) + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.58)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -17037,6 +17483,12 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-use-previous@1.0.1(@types/react@18.2.58)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -17044,6 +17496,12 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-use-previous@1.1.0(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-use-rect@1.0.1(@types/react@18.2.58)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -17060,6 +17518,13 @@ snapshots: react: 18.3.1 dev: false + '@radix-ui/react-use-size@1.1.0(@types/react@18.2.58)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.58)(react@18.3.1) + '@types/react': 18.2.58 + react: 18.3.1 + dev: false + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 @@ -19713,6 +20178,9 @@ snapshots: clsx@2.1.0: dev: false + clsx@2.1.1: + dev: false + cmdk@1.0.0(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -23217,6 +23685,9 @@ snapshots: isobject@3.0.1: {} + isomorphic.js@0.2.5: + dev: false + isostring@0.0.1: dev: false @@ -23627,6 +24098,14 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lexical@0.20.2: + dev: false + + lib0@0.2.98: + dependencies: + isomorphic.js: 0.2.5 + dev: false + lighthouse-logger@1.4.2: dependencies: debug: 2.6.9 @@ -26691,6 +27170,12 @@ snapshots: react: 18.3.1 dev: false + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + dev: false + react-error-overlay@6.0.11: dev: false @@ -29853,6 +30338,11 @@ snapshots: fd-slicer: 1.1.0 dev: false + yjs@13.6.20: + dependencies: + lib0: 0.2.98 + dev: false + yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} |
