From df648c93f1b93e58239022b19e02b335b92345cb Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:46:52 +0000 Subject: [PATCH 01/16] Initial Tiptap Editor --- frontends/main/next.config.js | 61 +- frontends/main/package.json | 27 +- .../app-pages/ArticlePage/NewArticlePage.tsx | 34 ++ frontends/main/src/app/article/new/page.tsx | 15 + frontends/ol-components/package.json | 20 + .../components/TiptapEditor/TiptapEditor.tsx | 174 ++++++ .../tiptap-icons/align-center-icon.tsx | 38 ++ .../tiptap-icons/align-justify-icon.tsx | 38 ++ .../tiptap-icons/align-left-icon.tsx | 38 ++ .../tiptap-icons/align-right-icon.tsx | 38 ++ .../tiptap-icons/arrow-left-icon.tsx | 24 + .../components/tiptap-icons/ban-icon.tsx | 26 + .../tiptap-icons/blockquote-icon.tsx | 44 ++ .../components/tiptap-icons/bold-icon.tsx | 26 + .../tiptap-icons/chevron-down-icon.tsx | 26 + .../components/tiptap-icons/close-icon.tsx | 24 + .../tiptap-icons/code-block-icon.tsx | 38 ++ .../components/tiptap-icons/code2-icon.tsx | 32 + .../tiptap-icons/corner-down-left-icon.tsx | 26 + .../tiptap-icons/external-link-icon.tsx | 28 + .../tiptap-icons/heading-five-icon.tsx | 28 + .../tiptap-icons/heading-four-icon.tsx | 28 + .../components/tiptap-icons/heading-icon.tsx | 24 + .../tiptap-icons/heading-one-icon.tsx | 28 + .../tiptap-icons/heading-six-icon.tsx | 30 + .../tiptap-icons/heading-three-icon.tsx | 36 ++ .../tiptap-icons/heading-two-icon.tsx | 28 + .../tiptap-icons/highlighter-icon.tsx | 26 + .../tiptap-icons/image-plus-icon.tsx | 26 + .../components/tiptap-icons/italic-icon.tsx | 24 + .../components/tiptap-icons/link-icon.tsx | 28 + .../components/tiptap-icons/list-icon.tsx | 56 ++ .../tiptap-icons/list-ordered-icon.tsx | 56 ++ .../tiptap-icons/list-todo-icon.tsx | 50 ++ .../tiptap-icons/moon-star-icon.tsx | 30 + .../components/tiptap-icons/redo2-icon.tsx | 26 + .../components/tiptap-icons/strike-icon.tsx | 28 + .../tiptap-icons/subscript-icon.tsx | 38 ++ .../components/tiptap-icons/sun-icon.tsx | 58 ++ .../tiptap-icons/superscript-icon.tsx | 38 ++ .../components/tiptap-icons/trash-icon.tsx | 26 + .../tiptap-icons/underline-icon.tsx | 26 + .../components/tiptap-icons/undo2-icon.tsx | 26 + .../blockquote-node/blockquote-node.scss | 37 ++ .../code-block-node/code-block-node.scss | 54 ++ .../heading-node/heading-node.scss | 39 ++ .../horizontal-rule-node-extension.ts | 14 + .../horizontal-rule-node.scss | 25 + .../tiptap-node/image-node/image-node.scss | 31 + .../image-upload-node-extension.ts | 162 +++++ .../image-upload-node/image-upload-node.scss | 249 ++++++++ .../image-upload-node/image-upload-node.tsx | 554 ++++++++++++++++++ .../tiptap-node/image-upload-node/index.tsx | 1 + .../tiptap-node/list-node/list-node.scss | 160 +++++ .../paragraph-node/paragraph-node.scss | 273 +++++++++ .../tiptap-templates/simple/data/content.json | 477 +++++++++++++++ .../simple/simple-editor.scss | 82 +++ .../tiptap-templates/simple/simple-editor.tsx | 290 +++++++++ .../tiptap-templates/simple/theme-toggle.tsx | 44 ++ .../badge/badge-colors.scss | 395 +++++++++++++ .../badge/badge-group.scss | 16 + .../tiptap-ui-primitive/badge/badge.scss | 99 ++++ .../tiptap-ui-primitive/badge/badge.tsx | 44 ++ .../tiptap-ui-primitive/badge/index.tsx | 1 + .../button/button-colors.scss | 429 ++++++++++++++ .../button/button-group.scss | 22 + .../tiptap-ui-primitive/button/button.scss | 314 ++++++++++ .../tiptap-ui-primitive/button/button.tsx | 114 ++++ .../tiptap-ui-primitive/button/index.tsx | 1 + .../tiptap-ui-primitive/card/card.scss | 77 +++ .../tiptap-ui-primitive/card/card.tsx | 79 +++ .../tiptap-ui-primitive/card/index.tsx | 1 + .../dropdown-menu/dropdown-menu.scss | 63 ++ .../dropdown-menu/dropdown-menu.tsx | 96 +++ .../dropdown-menu/index.tsx | 1 + .../tiptap-ui-primitive/input/index.tsx | 1 + .../tiptap-ui-primitive/input/input.scss | 45 ++ .../tiptap-ui-primitive/input/input.tsx | 22 + .../tiptap-ui-primitive/popover/index.tsx | 1 + .../tiptap-ui-primitive/popover/popover.scss | 63 ++ .../tiptap-ui-primitive/popover/popover.tsx | 35 ++ .../tiptap-ui-primitive/separator/index.tsx | 1 + .../separator/separator.scss | 23 + .../separator/separator.tsx | 31 + .../tiptap-ui-primitive/spacer/index.tsx | 1 + .../tiptap-ui-primitive/spacer/spacer.tsx | 26 + .../tiptap-ui-primitive/toolbar/index.tsx | 1 + .../tiptap-ui-primitive/toolbar/toolbar.scss | 98 ++++ .../tiptap-ui-primitive/toolbar/toolbar.tsx | 121 ++++ .../tiptap-ui-primitive/tooltip/index.tsx | 1 + .../tiptap-ui-primitive/tooltip/tooltip.scss | 43 ++ .../tiptap-ui-primitive/tooltip/tooltip.tsx | 237 ++++++++ .../blockquote-button/blockquote-button.tsx | 123 ++++ .../tiptap-ui/blockquote-button/index.tsx | 2 + .../blockquote-button/use-blockquote.ts | 246 ++++++++ .../code-block-button/code-block-button.tsx | 123 ++++ .../tiptap-ui/code-block-button/index.tsx | 2 + .../code-block-button/use-code-block.ts | 256 ++++++++ .../color-highlight-button.scss | 49 ++ .../color-highlight-button.tsx | 169 ++++++ .../color-highlight-button/index.tsx | 2 + .../use-color-highlight.ts | 339 +++++++++++ .../color-highlight-popover.tsx | 209 +++++++ .../color-highlight-popover/index.tsx | 1 + .../heading-button/heading-button.tsx | 125 ++++ .../tiptap-ui/heading-button/index.tsx | 2 + .../tiptap-ui/heading-button/use-heading.ts | 321 ++++++++++ .../heading-dropdown-menu.tsx | 127 ++++ .../tiptap-ui/heading-dropdown-menu/index.tsx | 2 + .../use-heading-dropdown-menu.ts | 132 +++++ .../image-upload-button.tsx | 131 +++++ .../tiptap-ui/image-upload-button/index.tsx | 2 + .../image-upload-button/use-image-upload.ts | 192 ++++++ .../tiptap-ui/link-popover/index.tsx | 2 + .../tiptap-ui/link-popover/link-popover.tsx | 307 ++++++++++ .../link-popover/use-link-popover.ts | 284 +++++++++ .../tiptap-ui/list-button/index.tsx | 2 + .../tiptap-ui/list-button/list-button.tsx | 121 ++++ .../tiptap-ui/list-button/use-list.ts | 326 +++++++++++ .../tiptap-ui/list-dropdown-menu/index.tsx | 1 + .../list-dropdown-menu/list-dropdown-menu.tsx | 123 ++++ .../use-list-dropdown-menu.ts | 216 +++++++ .../tiptap-ui/mark-button/index.tsx | 2 + .../tiptap-ui/mark-button/mark-button.tsx | 123 ++++ .../tiptap-ui/mark-button/use-mark.ts | 212 +++++++ .../tiptap-ui/text-align-button/index.tsx | 2 + .../text-align-button/text-align-button.tsx | 145 +++++ .../text-align-button/use-text-align.ts | 222 +++++++ .../tiptap-ui/undo-redo-button/index.tsx | 2 + .../undo-redo-button/undo-redo-button.tsx | 126 ++++ .../undo-redo-button/use-undo-redo.ts | 182 ++++++ .../TiptapEditor/hooks/use-composed-ref.ts | 47 ++ .../hooks/use-cursor-visibility.ts | 69 +++ .../TiptapEditor/hooks/use-element-rect.ts | 166 ++++++ .../TiptapEditor/hooks/use-menu-navigation.ts | 194 ++++++ .../TiptapEditor/hooks/use-mobile.ts | 17 + .../TiptapEditor/hooks/use-scrolling.ts | 75 +++ .../hooks/use-throttled-callback.ts | 48 ++ .../TiptapEditor/hooks/use-tiptap-editor.ts | 47 ++ .../TiptapEditor/hooks/use-unmount.ts | 21 + .../TiptapEditor/hooks/use-window-size.ts | 93 +++ .../TiptapEditor/lib/tiptap-utils.ts | 554 ++++++++++++++++++ .../styles/_keyframe-animations.scss | 91 +++ .../TiptapEditor/styles/_variables.scss | 296 ++++++++++ .../src/components/TiptapEditor/tsconfig.json | 16 + frontends/ol-components/src/index.ts | 2 + frontends/ol-components/tsconfig.json | 3 +- 147 files changed, 13222 insertions(+), 5 deletions(-) create mode 100644 frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx create mode 100644 frontends/main/src/app/article/new/page.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/blockquote-node/blockquote-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/code-block-node/code-block-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/heading-node/heading-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-node/image-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/list-node/list-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/data/content.json create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-group.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-group.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/index.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx create mode 100644 frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-mobile.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts create mode 100644 frontends/ol-components/src/components/TiptapEditor/styles/_keyframe-animations.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss create mode 100644 frontends/ol-components/src/components/TiptapEditor/tsconfig.json diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js index dac2a8467d..530b353028 100644 --- a/frontends/main/next.config.js +++ b/frontends/main/next.config.js @@ -128,7 +128,66 @@ const nextConfig = { allowCollectingMemory: true, }) } - // Important: return the modified config + + // Custom resolver for TiptapEditor's @/ imports + // Only applies special aliases when importing from within TiptapEditor directory + // Tiptap's Simple Editor ejects files to the project, which we'd prefer not to edit + // to update import paths. + const path = require("path") + const tiptapEditorPath = path.resolve( + __dirname, + "../ol-components/src/components/TiptapEditor", + ) + + config.resolve.plugins = config.resolve.plugins || [] + config.resolve.plugins.push({ + apply(resolver) { + const target = resolver.ensureHook("resolve") + + resolver + .getHook("described-resolve") + .tapAsync("TiptapEditorAliasPlugin", (request, resolveContext, callback) => { + const issuer = request.context?.issuer + + // Only apply custom aliases if the request is coming from TiptapEditor + if (!issuer || !issuer.includes("TiptapEditor")) { + return callback() + } + + // Check if this is a @/components, @/lib, or @/hooks import + const originalRequest = request.request + let newRequestPath = null + + if (originalRequest?.startsWith("@/components/")) { + const importPath = originalRequest.substring("@/components/".length) + newRequestPath = path.join(tiptapEditorPath, "components", importPath) + } else if (originalRequest?.startsWith("@/lib/")) { + const importPath = originalRequest.substring("@/lib/".length) + newRequestPath = path.join(tiptapEditorPath, "lib", importPath) + } else if (originalRequest?.startsWith("@/hooks/")) { + const importPath = originalRequest.substring("@/hooks/".length) + newRequestPath = path.join(tiptapEditorPath, "hooks", importPath) + } + + if (newRequestPath) { + const newRequest = { + ...request, + request: newRequestPath, + } + return resolver.doResolve( + target, + newRequest, + "aliased with TiptapEditor prefix", + resolveContext, + callback, + ) + } + + callback() + }) + }, + }) + return config }, } diff --git a/frontends/main/package.json b/frontends/main/package.json index cfc7859b40..7021ed7412 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -13,22 +13,40 @@ "@ebay/nice-modal-react": "^1.2.13", "@emotion/cache": "^11.13.1", "@emotion/styled": "^11.11.0", + "@floating-ui/react": "^0.27.16", "@mitodl/course-search-utils": "^3.5.0", - "@mitodl/mitxonline-api-axios": "^2025.11.5", + "@mitodl/mitxonline-api-axios": "^2025.10.21", "@mitodl/smoot-design": "^6.17.1", "@next/bundle-analyzer": "^14.2.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", "@react-pdf/renderer": "^4.3.0", - "@remixicon/react": "^4.2.0", + "@remixicon/react": "^4.7.0", "@sentry/nextjs": "^10.0.0", "@tanstack/react-query": "^5.66", + "@tiptap/extension-document": "^3.10.2", + "@tiptap/extension-highlight": "^3.10.1", + "@tiptap/extension-horizontal-rule": "^3.10.1", + "@tiptap/extension-image": "^3.10.1", + "@tiptap/extension-list": "^3.10.1", + "@tiptap/extension-subscript": "^3.10.1", + "@tiptap/extension-superscript": "^3.10.1", + "@tiptap/extension-text-align": "^3.10.1", + "@tiptap/extension-typography": "^3.10.1", + "@tiptap/extensions": "^3.10.1", + "@tiptap/markdown": "^3.10.1", + "@tiptap/pm": "^3.10.1", + "@tiptap/react": "^3.10.1", + "@tiptap/starter-kit": "^3.10.1", + "@tiptap/suggestion": "^3.10.2", "api": "workspace:*", - "async_hooks": "^1.0.0", "classnames": "^2.5.1", "formik": "^2.4.6", "iso-639-1": "^3.1.4", "isomorphic-dompurify": "^2.27.0", "jsdom": "^27", "lodash": "^4.17.21", + "lodash.throttle": "^4.1.1", "moment": "^2.30.1", "next": "^15.5.2", "next-nprogress-bar": "^2.4.2", @@ -37,6 +55,7 @@ "posthog-js": "^1.157.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hotkeys-hook": "^5.2.1", "react-slick": "^0.30.2", "sharp": "0.34.4", "slick-carousel": "^1.8.1", @@ -50,6 +69,7 @@ "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.7", + "@types/lodash.throttle": "^4.1.9", "@types/node": "^22.0.0", "@types/react": "^19", "@types/react-dom": "^19", @@ -63,6 +83,7 @@ "jest-next-dynamic-ts": "^0.1.1", "next-router-mock": "^1.0.2", "ol-test-utilities": "0.0.0", + "sass": "^1.93.3", "ts-jest": "^29.2.4", "type-fest": "^5.0.1", "typescript": "^5" diff --git a/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx b/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx new file mode 100644 index 0000000000..18b2685553 --- /dev/null +++ b/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx @@ -0,0 +1,34 @@ +"use client" + +import React from "react" +import { theme, styled, HEADER_HEIGHT } from "ol-components" + +import { TiptapEditor } from "ol-components" + +const PageContainer = styled.div({ + color: theme.custom.colors.darkGray2, + display: "flex", + height: `calc(100vh - ${HEADER_HEIGHT}px - 132px)`, +}) + +const EditorContainer = styled.div({ + minHeight: 0, +}) + +const StyledTiptapEditor = styled(TiptapEditor)({ + width: "70vw", + height: `calc(100% - ${HEADER_HEIGHT}px - 132px)`, + overscrollBehavior: "contain", +}) + +const NewArticlePage: React.FC = () => { + return ( + + + + + + ) +} + +export { NewArticlePage } diff --git a/frontends/main/src/app/article/new/page.tsx b/frontends/main/src/app/article/new/page.tsx new file mode 100644 index 0000000000..d0694149d1 --- /dev/null +++ b/frontends/main/src/app/article/new/page.tsx @@ -0,0 +1,15 @@ +import React from "react" +import { Metadata } from "next" +import { standardizeMetadata } from "@/common/metadata" +import { NewArticlePage } from "@/app-pages/ArticlePage/NewArticlePage" + +export const metadata: Metadata = standardizeMetadata({ + title: "New Article", + robots: "noindex, nofollow", +}) + +const Page: React.FC> = () => { + return +} + +export default Page diff --git a/frontends/ol-components/package.json b/frontends/ol-components/package.json index b075dae491..93e48f0b0f 100644 --- a/frontends/ol-components/package.json +++ b/frontends/ol-components/package.json @@ -19,13 +19,28 @@ "@dnd-kit/utilities": "^3.2.1", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@floating-ui/react": "^0.27.16", "@mui/base": "5.0.0-beta.70", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "^6.4.5", "@mui/material-nextjs": "^6.4.3", "@mui/system": "^6.4.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", "@remixicon/react": "^4.2.0", "@testing-library/dom": "^10.4.0", + "@tiptap/extension-highlight": "^3.10.5", + "@tiptap/extension-horizontal-rule": "^3.10.5", + "@tiptap/extension-image": "^3.10.5", + "@tiptap/extension-list": "^3.10.5", + "@tiptap/extension-subscript": "^3.10.5", + "@tiptap/extension-superscript": "^3.10.5", + "@tiptap/extension-text-align": "^3.10.5", + "@tiptap/extension-typography": "^3.10.5", + "@tiptap/extensions": "^3.10.5", + "@tiptap/pm": "^3.10.5", + "@tiptap/react": "^3.10.5", + "@tiptap/starter-kit": "^3.10.5", "@types/react-dom": "^19", "@types/tinycolor2": "^1.4.6", "api": "workspace:*", @@ -33,12 +48,14 @@ "embla-carousel-react": "^8.6.0", "embla-carousel-wheel-gestures": "^8.0.2", "lodash": "^4.17.21", + "lodash.throttle": "^4.1.1", "material-ui-popup-state": "^5.1.0", "next": "^15.5.2", "ol-test-utilities": "0.0.0", "ol-utilities": "0.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hotkeys-hook": "^5.2.1", "react-select": "^5.7.7", "react-share": "^5.0.3", "react-slick": "^0.30.2", @@ -65,10 +82,13 @@ "@storybook/types": "^8.2.9", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.5.2", + "@types/lodash.throttle": "^4.1.9", "@types/react-slick": "^0", "dotenv": "^17.0.0", "lodash": "^4.17.21", "prop-types": "^15.8.1", + "sass": "^1.93.3", + "sass-embedded": "^1.93.3", "storybook": "^8.2.9", "typescript": "^5.5.4", "webpack": "^5.94.0" diff --git a/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx b/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx new file mode 100644 index 0000000000..762dafb93b --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx @@ -0,0 +1,174 @@ +"use client" + +// Based on ./components/tiptap-templates/simple/simple-editor.tsx + +import React, { useRef } from "react" +import { EditorContent, EditorContext, useEditor } from "@tiptap/react" + +// --- Tiptap Core Extensions --- +import { StarterKit } from "@tiptap/starter-kit" +import { TaskItem, TaskList } from "@tiptap/extension-list" +import { TextAlign } from "@tiptap/extension-text-align" +import { Typography } from "@tiptap/extension-typography" +import { Highlight } from "@tiptap/extension-highlight" +import { Subscript } from "@tiptap/extension-subscript" +import { Superscript } from "@tiptap/extension-superscript" +import { Selection } from "@tiptap/extensions" + +// --- UI Primitives --- +import { Spacer } from "@/components/tiptap-ui-primitive/spacer" +import { + Toolbar, + ToolbarGroup, + ToolbarSeparator, +} from "@/components/tiptap-ui-primitive/toolbar" + +// --- Tiptap Node --- +import { ImageUploadNode } from "@/components/tiptap-node/image-upload-node/image-upload-node-extension" +import { HorizontalRule } from "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension" +import "@/components/tiptap-node/blockquote-node/blockquote-node.scss" +import "@/components/tiptap-node/code-block-node/code-block-node.scss" +import "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss" +import "@/components/tiptap-node/list-node/list-node.scss" +import "@/components/tiptap-node/image-node/image-node.scss" +import "@/components/tiptap-node/heading-node/heading-node.scss" +import "@/components/tiptap-node/paragraph-node/paragraph-node.scss" + +// --- Tiptap UI --- +import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" +import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" +import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button" +import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button" +import { ColorHighlightPopover } from "@/components/tiptap-ui/color-highlight-popover" +import { LinkPopover } from "@/components/tiptap-ui/link-popover" +import { MarkButton } from "@/components/tiptap-ui/mark-button" +import { TextAlignButton } from "@/components/tiptap-ui/text-align-button" +import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button" + +// --- Lib --- +import { handleImageUpload, MAX_FILE_SIZE } from "@/lib/tiptap-utils" + +// --- Styles --- +import "./styles/_keyframe-animations.scss" +import "./styles/_variables.scss" +import "./components/tiptap-templates/simple/simple-editor.scss" + +const MainToolbarContent = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default function SimpleEditor() { + const toolbarRef = useRef(null) + + const editor = useEditor({ + immediatelyRender: false, + shouldRerenderOnTransaction: false, + editorProps: { + attributes: { + autocomplete: "off", + autocorrect: "off", + autocapitalize: "off", + "aria-label": "Main content area, start typing to enter text.", + class: "simple-editor", + }, + }, + extensions: [ + StarterKit.configure({ + horizontalRule: false, + link: { + openOnClick: false, + enableClickSelection: true, + }, + }), + HorizontalRule, + TextAlign.configure({ types: ["heading", "paragraph"] }), + TaskList, + TaskItem.configure({ nested: true }), + Highlight.configure({ multicolor: true }), + Typography, + Superscript, + Subscript, + Selection, + ImageUploadNode.configure({ + accept: "image/*", + maxSize: MAX_FILE_SIZE, + limit: 3, + upload: handleImageUpload, + onError: (error) => console.error("Upload failed:", error), + }), + ], + content: { + type: "doc", + content: [ + { + type: "paragraph", + content: [], + }, + ], + }, + }) + + return ( +
+ + + + + + + +
+ ) +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx new file mode 100644 index 0000000000..bb720609ff --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const AlignCenterIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +AlignCenterIcon.displayName = "AlignCenterIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx new file mode 100644 index 0000000000..61cc6def89 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const AlignJustifyIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +AlignJustifyIcon.displayName = "AlignJustifyIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx new file mode 100644 index 0000000000..2972bdba54 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const AlignLeftIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +AlignLeftIcon.displayName = "AlignLeftIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx new file mode 100644 index 0000000000..c93fc0599a --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const AlignRightIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +AlignRightIcon.displayName = "AlignRightIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx new file mode 100644 index 0000000000..7cf04d28cd --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx @@ -0,0 +1,24 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ArrowLeftIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +ArrowLeftIcon.displayName = "ArrowLeftIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx new file mode 100644 index 0000000000..1995de03f6 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const BanIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +BanIcon.displayName = "BanIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx new file mode 100644 index 0000000000..50e665f8e0 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx @@ -0,0 +1,44 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const BlockquoteIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + + ) +}) + +BlockquoteIcon.displayName = "BlockquoteIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx new file mode 100644 index 0000000000..a61cca4e8e --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const BoldIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +BoldIcon.displayName = "BoldIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx new file mode 100644 index 0000000000..8f7844d937 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ChevronDownIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +ChevronDownIcon.displayName = "ChevronDownIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx new file mode 100644 index 0000000000..8b506a9356 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx @@ -0,0 +1,24 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const CloseIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +CloseIcon.displayName = "CloseIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx new file mode 100644 index 0000000000..9d42238262 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const CodeBlockIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +CodeBlockIcon.displayName = "CodeBlockIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx new file mode 100644 index 0000000000..e8d70d842d --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx @@ -0,0 +1,32 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const Code2Icon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +Code2Icon.displayName = "Code2Icon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx new file mode 100644 index 0000000000..0dea6393c1 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const CornerDownLeftIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +CornerDownLeftIcon.displayName = "CornerDownLeftIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx new file mode 100644 index 0000000000..a4afe67e07 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ExternalLinkIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +ExternalLinkIcon.displayName = "ExternalLinkIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx new file mode 100644 index 0000000000..e1450cfce8 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingFiveIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +HeadingFiveIcon.displayName = "HeadingFiveIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx new file mode 100644 index 0000000000..3f35a8b4af --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingFourIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +HeadingFourIcon.displayName = "HeadingFourIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx new file mode 100644 index 0000000000..ba9aa15138 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx @@ -0,0 +1,24 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +HeadingIcon.displayName = "HeadingIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx new file mode 100644 index 0000000000..d35093d521 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingOneIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +HeadingOneIcon.displayName = "HeadingOneIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx new file mode 100644 index 0000000000..9cce22b849 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx @@ -0,0 +1,30 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingSixIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +HeadingSixIcon.displayName = "HeadingSixIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx new file mode 100644 index 0000000000..f2448366ff --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx @@ -0,0 +1,36 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingThreeIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +HeadingThreeIcon.displayName = "HeadingThreeIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx new file mode 100644 index 0000000000..24417acbd2 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HeadingTwoIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +HeadingTwoIcon.displayName = "HeadingTwoIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx new file mode 100644 index 0000000000..b6374feaff --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const HighlighterIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +HighlighterIcon.displayName = "HighlighterIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx new file mode 100644 index 0000000000..b1fd8c2b09 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ImagePlusIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +ImagePlusIcon.displayName = "ImagePlusIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx new file mode 100644 index 0000000000..7c69b63317 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx @@ -0,0 +1,24 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ItalicIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +ItalicIcon.displayName = "ItalicIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx new file mode 100644 index 0000000000..27e1574363 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const LinkIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +LinkIcon.displayName = "LinkIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx new file mode 100644 index 0000000000..a3183d212b --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx @@ -0,0 +1,56 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ListIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + + + + ) +}) + +ListIcon.displayName = "ListIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx new file mode 100644 index 0000000000..119da7b8a0 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx @@ -0,0 +1,56 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ListOrderedIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + + + + ) +}) + +ListOrderedIcon.displayName = "ListOrderedIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx new file mode 100644 index 0000000000..9b899c189f --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx @@ -0,0 +1,50 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const ListTodoIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + + + ) +}) + +ListTodoIcon.displayName = "ListTodoIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx new file mode 100644 index 0000000000..e2d4422825 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx @@ -0,0 +1,30 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const MoonStarIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +MoonStarIcon.displayName = "MoonStarIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx new file mode 100644 index 0000000000..0302a16878 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const Redo2Icon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +Redo2Icon.displayName = "Redo2Icon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx new file mode 100644 index 0000000000..df4b8c8100 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const StrikeIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + ) +}) + +StrikeIcon.displayName = "StrikeIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx new file mode 100644 index 0000000000..671030dfef --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const SubscriptIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +SubscriptIcon.displayName = "SubscriptIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx new file mode 100644 index 0000000000..607fd650fe --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx @@ -0,0 +1,58 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const SunIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + + + + + + + ) +}) + +SunIcon.displayName = "SunIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx new file mode 100644 index 0000000000..6af567b2da --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx @@ -0,0 +1,38 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const SuperscriptIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + + + ) +}) + +SuperscriptIcon.displayName = "SuperscriptIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx new file mode 100644 index 0000000000..ac470bbe24 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const TrashIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +TrashIcon.displayName = "TrashIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx new file mode 100644 index 0000000000..9765a3bd57 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const UnderlineIcon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +UnderlineIcon.displayName = "UnderlineIcon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx new file mode 100644 index 0000000000..bfb643f3cd --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx @@ -0,0 +1,26 @@ +import { memo } from "react" + +type SvgProps = React.ComponentPropsWithoutRef<"svg"> + +export const Undo2Icon = memo(({ className, ...props }: SvgProps) => { + return ( + + + + ) +}) + +Undo2Icon.displayName = "Undo2Icon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/blockquote-node/blockquote-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/blockquote-node/blockquote-node.scss new file mode 100644 index 0000000000..b49c5e11e4 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/blockquote-node/blockquote-node.scss @@ -0,0 +1,37 @@ +.tiptap.ProseMirror { + --blockquote-bg-color: var(--tt-gray-light-900); + + .dark & { + --blockquote-bg-color: var(--tt-gray-dark-900); + } +} + +/* ===================== + BLOCKQUOTE + ===================== */ +.tiptap.ProseMirror { + blockquote { + position: relative; + padding-left: 1em; + padding-top: 0.375em; + padding-bottom: 0.375em; + margin: 1.5rem 0; + + p { + margin-top: 0; + } + + &::before, + &.is-empty::before { + position: absolute; + bottom: 0; + left: 0; + top: 0; + height: 100%; + width: 0.25em; + background-color: var(--blockquote-bg-color); + content: ""; + border-radius: 0; + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/code-block-node/code-block-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/code-block-node/code-block-node.scss new file mode 100644 index 0000000000..d31b312f6d --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/code-block-node/code-block-node.scss @@ -0,0 +1,54 @@ +.tiptap.ProseMirror { + --tt-inline-code-bg-color: var(--tt-gray-light-a-100); + --tt-inline-code-text-color: var(--tt-gray-light-a-700); + --tt-inline-code-border-color: var(--tt-gray-light-a-200); + --tt-codeblock-bg: var(--tt-gray-light-a-50); + --tt-codeblock-text: var(--tt-gray-light-a-800); + --tt-codeblock-border: var(--tt-gray-light-a-200); + + .dark & { + --tt-inline-code-bg-color: var(--tt-gray-dark-a-100); + --tt-inline-code-text-color: var(--tt-gray-dark-a-700); + --tt-inline-code-border-color: var(--tt-gray-dark-a-200); + --tt-codeblock-bg: var(--tt-gray-dark-a-50); + --tt-codeblock-text: var(--tt-gray-dark-a-800); + --tt-codeblock-border: var(--tt-gray-dark-a-200); + } +} + +/* ===================== + CODE FORMATTING + ===================== */ +.tiptap.ProseMirror { + // Inline code + code { + background-color: var(--tt-inline-code-bg-color); + color: var(--tt-inline-code-text-color); + border: 1px solid var(--tt-inline-code-border-color); + font-family: "JetBrains Mono NL", monospace; + font-size: 0.875em; + line-height: 1.4; + border-radius: 6px/0.375rem; + padding: 0.1em 0.2em; + } + + // Code blocks + pre { + background-color: var(--tt-codeblock-bg); + color: var(--tt-codeblock-text); + border: 1px solid var(--tt-codeblock-border); + margin-top: 1.5em; + margin-bottom: 1.5em; + padding: 1em; + font-size: 1rem; + border-radius: 6px/0.375rem; + + code { + background-color: transparent; + border: none; + border-radius: 0; + -webkit-text-fill-color: inherit; + color: inherit; + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/heading-node/heading-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/heading-node/heading-node.scss new file mode 100644 index 0000000000..882dda2d3a --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/heading-node/heading-node.scss @@ -0,0 +1,39 @@ +.tiptap.ProseMirror { + h1, + h2, + h3, + h4 { + position: relative; + color: inherit; + font-style: inherit; + + &:first-child, + &:first-of-type { + margin-top: 0; + } + } + + h1 { + font-size: 1.5em; + font-weight: 700; + margin-top: 3em; + } + + h2 { + font-size: 1.25em; + font-weight: 700; + margin-top: 2.5em; + } + + h3 { + font-size: 1.125em; + font-weight: 600; + margin-top: 2em; + } + + h4 { + font-size: 1em; + font-weight: 600; + margin-top: 2em; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts new file mode 100644 index 0000000000..de28208616 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts @@ -0,0 +1,14 @@ +import { mergeAttributes } from "@tiptap/react" +import TiptapHorizontalRule from "@tiptap/extension-horizontal-rule" + +export const HorizontalRule = TiptapHorizontalRule.extend({ + renderHTML() { + return [ + "div", + mergeAttributes(this.options.HTMLAttributes, { "data-type": this.name }), + ["hr"], + ] + }, +}) + +export default HorizontalRule diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss new file mode 100644 index 0000000000..4626e65889 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss @@ -0,0 +1,25 @@ +.tiptap.ProseMirror { + --horizontal-rule-color: var(--tt-gray-light-a-200); + + .dark & { + --horizontal-rule-color: var(--tt-gray-dark-a-200); + } +} + +/* ===================== + HORIZONTAL RULE + ===================== */ +.tiptap.ProseMirror { + hr { + border: none; + height: 1px; + background-color: var(--horizontal-rule-color); + } + + [data-type="horizontalRule"] { + margin-top: 2.25em; + margin-bottom: 2.25em; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-node/image-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-node/image-node.scss new file mode 100644 index 0000000000..10d4231cac --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-node/image-node.scss @@ -0,0 +1,31 @@ +.tiptap.ProseMirror { + img { + max-width: 100%; + height: auto; + display: block; + } + + > img:not([data-type="emoji"] img) { + margin: 2rem 0; + outline: 0.125rem solid transparent; + border-radius: var(--tt-radius-xs, 0.25rem); + } + + img:not([data-type="emoji"] img).ProseMirror-selectednode { + outline-color: var(--tt-brand-color-500); + } + + // Thread image handling + .tiptap-thread:has(> img) { + margin: 2rem 0; + + img { + outline: 0.125rem solid transparent; + border-radius: var(--tt-radius-xs, 0.25rem); + } + } + + .tiptap-thread img { + margin: 0; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts new file mode 100644 index 0000000000..c282b1b35c --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts @@ -0,0 +1,162 @@ +import { mergeAttributes, Node } from "@tiptap/react" +import { ReactNodeViewRenderer } from "@tiptap/react" +import { ImageUploadNode as ImageUploadNodeComponent } from "@/components/tiptap-node/image-upload-node/image-upload-node" +import type { NodeType } from "@tiptap/pm/model" + +export type UploadFunction = ( + file: File, + onProgress?: (event: { progress: number }) => void, + abortSignal?: AbortSignal +) => Promise + +export interface ImageUploadNodeOptions { + /** + * The type of the node. + * @default 'image' + */ + type?: string | NodeType | undefined + /** + * Acceptable file types for upload. + * @default 'image/*' + */ + accept?: string + /** + * Maximum number of files that can be uploaded. + * @default 1 + */ + limit?: number + /** + * Maximum file size in bytes (0 for unlimited). + * @default 0 + */ + maxSize?: number + /** + * Function to handle the upload process. + */ + upload?: UploadFunction + /** + * Callback for upload errors. + */ + onError?: (error: Error) => void + /** + * Callback for successful uploads. + */ + onSuccess?: (url: string) => void + /** + * HTML attributes to add to the image element. + * @default {} + * @example { class: 'foo' } + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + HTMLAttributes: Record +} + +declare module "@tiptap/react" { + interface Commands { + imageUpload: { + setImageUploadNode: (options?: ImageUploadNodeOptions) => ReturnType + } + } +} + +/** + * A Tiptap node extension that creates an image upload component. + * @see registry/tiptap-node/image-upload-node/image-upload-node + */ +export const ImageUploadNode = Node.create({ + name: "imageUpload", + + group: "block", + + draggable: true, + + selectable: true, + + atom: true, + + addOptions() { + return { + type: "image", + accept: "image/*", + limit: 1, + maxSize: 0, + upload: undefined, + onError: undefined, + onSuccess: undefined, + HTMLAttributes: {}, + } + }, + + addAttributes() { + return { + accept: { + default: this.options.accept, + }, + limit: { + default: this.options.limit, + }, + maxSize: { + default: this.options.maxSize, + }, + } + }, + + parseHTML() { + return [{ tag: 'div[data-type="image-upload"]' }] + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes({ "data-type": "image-upload" }, HTMLAttributes), + ] + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageUploadNodeComponent) + }, + + addCommands() { + return { + setImageUploadNode: + (options) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }) + }, + } + }, + + /** + * Adds Enter key handler to trigger the upload component when it's selected. + */ + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + const { selection } = editor.state + const { nodeAfter } = selection.$from + + if ( + nodeAfter && + nodeAfter.type.name === "imageUpload" && + editor.isActive("imageUpload") + ) { + const nodeEl = editor.view.nodeDOM(selection.$from.pos) + if (nodeEl && nodeEl instanceof HTMLElement) { + // Since NodeViewWrapper is wrapped with a div, we need to click the first child + const firstChild = nodeEl.firstChild + if (firstChild && firstChild instanceof HTMLElement) { + firstChild.click() + return true + } + } + } + return false + }, + } + }, +}) + +export default ImageUploadNode diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.scss new file mode 100644 index 0000000000..b85e1e33f1 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.scss @@ -0,0 +1,249 @@ +:root { + --tiptap-image-upload-active: var(--tt-brand-color-500); + --tiptap-image-upload-progress-bg: var(--tt-brand-color-50); + --tiptap-image-upload-icon-bg: var(--tt-brand-color-500); + + --tiptap-image-upload-text-color: var(--tt-gray-light-a-700); + --tiptap-image-upload-subtext-color: var(--tt-gray-light-a-400); + --tiptap-image-upload-border: var(--tt-gray-light-a-300); + --tiptap-image-upload-border-hover: var(--tt-gray-light-a-400); + --tiptap-image-upload-border-active: var(--tt-brand-color-500); + + --tiptap-image-upload-icon-doc-bg: var(--tt-gray-light-a-200); + --tiptap-image-upload-icon-doc-border: var(--tt-gray-light-300); + --tiptap-image-upload-icon-color: var(--white); +} + +.dark { + --tiptap-image-upload-active: var(--tt-brand-color-400); + --tiptap-image-upload-progress-bg: var(--tt-brand-color-900); + --tiptap-image-upload-icon-bg: var(--tt-brand-color-400); + + --tiptap-image-upload-text-color: var(--tt-gray-dark-a-700); + --tiptap-image-upload-subtext-color: var(--tt-gray-dark-a-400); + --tiptap-image-upload-border: var(--tt-gray-dark-a-300); + --tiptap-image-upload-border-hover: var(--tt-gray-dark-a-400); + --tiptap-image-upload-border-active: var(--tt-brand-color-400); + + --tiptap-image-upload-icon-doc-bg: var(--tt-gray-dark-a-200); + --tiptap-image-upload-icon-doc-border: var(--tt-gray-dark-300); + --tiptap-image-upload-icon-color: var(--black); +} + +.tiptap-image-upload { + margin: 2rem 0; + + input[type="file"] { + display: none; + } + + .tiptap-image-upload-dropzone { + position: relative; + width: 3.125rem; + height: 3.75rem; + display: inline-flex; + align-items: flex-start; + justify-content: center; + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; + } + + .tiptap-image-upload-icon-container { + position: absolute; + width: 1.75rem; + height: 1.75rem; + bottom: 0; + right: 0; + background-color: var(--tiptap-image-upload-icon-bg); + border-radius: var(--tt-radius-lg, 0.75rem); + display: flex; + align-items: center; + justify-content: center; + } + + .tiptap-image-upload-icon { + width: 0.875rem; + height: 0.875rem; + color: var(--tiptap-image-upload-icon-color); + } + + .tiptap-image-upload-dropzone-rect-primary { + color: var(--tiptap-image-upload-icon-doc-bg); + position: absolute; + } + + .tiptap-image-upload-dropzone-rect-secondary { + position: absolute; + top: 0; + right: 0.25rem; + bottom: 0; + color: var(--tiptap-image-upload-icon-doc-border); + } + + .tiptap-image-upload-text { + color: var(--tiptap-image-upload-text-color); + font-weight: 500; + font-size: 0.875rem; + line-height: normal; + + em { + font-style: normal; + text-decoration: underline; + } + } + + .tiptap-image-upload-subtext { + color: var(--tiptap-image-upload-subtext-color); + font-weight: 600; + line-height: normal; + font-size: 0.75rem; + } + + .tiptap-image-upload-drag-area { + padding: 2rem 1.5rem; + border: 1.5px dashed var(--tiptap-image-upload-border); + border-radius: var(--tt-radius-md, 0.5rem); + text-align: center; + cursor: pointer; + position: relative; + overflow: hidden; + transition: all 0.2s ease; + + &:hover { + border-color: var(--tiptap-image-upload-border-hover); + } + + &.drag-active { + border-color: var(--tiptap-image-upload-border-active); + background-color: rgba( + var(--tiptap-image-upload-active-rgb, 0, 123, 255), + 0.05 + ); + } + + &.drag-over { + border-color: var(--tiptap-image-upload-border-active); + background-color: rgba( + var(--tiptap-image-upload-active-rgb, 0, 123, 255), + 0.1 + ); + } + } + + .tiptap-image-upload-content { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 0.25rem; + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; + } + + .tiptap-image-upload-previews { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .tiptap-image-upload-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid var(--tiptap-image-upload-border); + margin-bottom: 0.5rem; + + span { + font-size: 0.875rem; + font-weight: 500; + color: var(--tiptap-image-upload-text-color); + } + } + + // === Individual File Preview Styles === + .tiptap-image-upload-preview { + position: relative; + border-radius: var(--tt-radius-md, 0.5rem); + overflow: hidden; + + .tiptap-image-upload-progress { + position: absolute; + inset: 0; + background-color: var(--tiptap-image-upload-progress-bg); + transition: all 300ms ease-out; + } + + .tiptap-image-upload-preview-content { + position: relative; + border: 1px solid var(--tiptap-image-upload-border); + border-radius: var(--tt-radius-md, 0.5rem); + padding: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + } + + .tiptap-image-upload-file-info { + display: flex; + align-items: center; + gap: 0.75rem; + height: 2rem; + + .tiptap-image-upload-file-icon { + padding: 0.5rem; + background-color: var(--tiptap-image-upload-icon-bg); + border-radius: var(--tt-radius-lg, 0.75rem); + + svg { + width: 0.875rem; + height: 0.875rem; + color: var(--tiptap-image-upload-icon-color); + } + } + } + + .tiptap-image-upload-details { + display: flex; + flex-direction: column; + } + + .tiptap-image-upload-actions { + display: flex; + align-items: center; + gap: 0.5rem; + + .tiptap-image-upload-progress-text { + font-size: 0.75rem; + color: var(--tiptap-image-upload-border-active); + font-weight: 600; + } + } + } +} + +.tiptap.ProseMirror.ProseMirror-focused { + .ProseMirror-selectednode .tiptap-image-upload-drag-area { + border-color: var(--tiptap-image-upload-active); + } +} + +@media (max-width: 480px) { + .tiptap-image-upload { + .tiptap-image-upload-drag-area { + padding: 1.5rem 1rem; + } + + .tiptap-image-upload-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .tiptap-image-upload-preview-content { + padding: 0.75rem; + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx new file mode 100644 index 0000000000..4b56f1cb60 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx @@ -0,0 +1,554 @@ +"use client" + +import { useRef, useState } from "react" +import type { NodeViewProps } from "@tiptap/react" +import { NodeViewWrapper } from "@tiptap/react" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { CloseIcon } from "@/components/tiptap-icons/close-icon" +import "@/components/tiptap-node/image-upload-node/image-upload-node.scss" +import { focusNextNode, isValidPosition } from "@/lib/tiptap-utils" + +export interface FileItem { + /** + * Unique identifier for the file item + */ + id: string + /** + * The actual File object being uploaded + */ + file: File + /** + * Current upload progress as a percentage (0-100) + */ + progress: number + /** + * Current status of the file upload process + * @default "uploading" + */ + status: "uploading" | "success" | "error" + + /** + * URL to the uploaded file, available after successful upload + * @optional + */ + url?: string + /** + * Controller that can be used to abort the upload process + * @optional + */ + abortController?: AbortController +} + +export interface UploadOptions { + /** + * Maximum allowed file size in bytes + */ + maxSize: number + /** + * Maximum number of files that can be uploaded + */ + limit: number + /** + * String specifying acceptable file types (MIME types or extensions) + * @example ".jpg,.png,image/jpeg" or "image/*" + */ + accept: string + /** + * Function that handles the actual file upload process + * @param {File} file - The file to be uploaded + * @param {Function} onProgress - Callback function to report upload progress + * @param {AbortSignal} signal - Signal that can be used to abort the upload + * @returns {Promise} Promise resolving to the URL of the uploaded file + */ + upload: ( + file: File, + onProgress: (event: { progress: number }) => void, + signal: AbortSignal + ) => Promise + /** + * Callback triggered when a file is uploaded successfully + * @param {string} url - URL of the successfully uploaded file + * @optional + */ + onSuccess?: (url: string) => void + /** + * Callback triggered when an error occurs during upload + * @param {Error} error - The error that occurred + * @optional + */ + onError?: (error: Error) => void +} + +/** + * Custom hook for managing multiple file uploads with progress tracking and cancellation + */ +function useFileUpload(options: UploadOptions) { + const [fileItems, setFileItems] = useState([]) + + const uploadFile = async (file: File): Promise => { + if (file.size > options.maxSize) { + const error = new Error( + `File size exceeds maximum allowed (${options.maxSize / 1024 / 1024}MB)` + ) + options.onError?.(error) + return null + } + + const abortController = new AbortController() + const fileId = crypto.randomUUID() + + const newFileItem: FileItem = { + id: fileId, + file, + progress: 0, + status: "uploading", + abortController, + } + + setFileItems((prev) => [...prev, newFileItem]) + + try { + if (!options.upload) { + throw new Error("Upload function is not defined") + } + + const url = await options.upload( + file, + (event: { progress: number }) => { + setFileItems((prev) => + prev.map((item) => + item.id === fileId ? { ...item, progress: event.progress } : item + ) + ) + }, + abortController.signal + ) + + if (!url) throw new Error("Upload failed: No URL returned") + + if (!abortController.signal.aborted) { + setFileItems((prev) => + prev.map((item) => + item.id === fileId + ? { ...item, status: "success", url, progress: 100 } + : item + ) + ) + options.onSuccess?.(url) + return url + } + + return null + } catch (error) { + if (!abortController.signal.aborted) { + setFileItems((prev) => + prev.map((item) => + item.id === fileId + ? { ...item, status: "error", progress: 0 } + : item + ) + ) + options.onError?.( + error instanceof Error ? error : new Error("Upload failed") + ) + } + return null + } + } + + const uploadFiles = async (files: File[]): Promise => { + if (!files || files.length === 0) { + options.onError?.(new Error("No files to upload")) + return [] + } + + if (options.limit && files.length > options.limit) { + options.onError?.( + new Error( + `Maximum ${options.limit} file${options.limit === 1 ? "" : "s"} allowed` + ) + ) + return [] + } + + // Upload all files concurrently + const uploadPromises = files.map((file) => uploadFile(file)) + const results = await Promise.all(uploadPromises) + + // Filter out null results (failed uploads) + return results.filter((url): url is string => url !== null) + } + + const removeFileItem = (fileId: string) => { + setFileItems((prev) => { + const fileToRemove = prev.find((item) => item.id === fileId) + if (fileToRemove?.abortController) { + fileToRemove.abortController.abort() + } + if (fileToRemove?.url) { + URL.revokeObjectURL(fileToRemove.url) + } + return prev.filter((item) => item.id !== fileId) + }) + } + + const clearAllFiles = () => { + fileItems.forEach((item) => { + if (item.abortController) { + item.abortController.abort() + } + if (item.url) { + URL.revokeObjectURL(item.url) + } + }) + setFileItems([]) + } + + return { + fileItems, + uploadFiles, + removeFileItem, + clearAllFiles, + } +} + +const CloudUploadIcon: React.FC = () => ( + + + + +) + +const FileIcon: React.FC = () => ( + + + +) + +const FileCornerIcon: React.FC = () => ( + + + +) + +interface ImageUploadDragAreaProps { + /** + * Callback function triggered when files are dropped or selected + * @param {File[]} files - Array of File objects that were dropped or selected + */ + onFile: (files: File[]) => void + /** + * Optional child elements to render inside the drag area + * @optional + * @default undefined + */ + children?: React.ReactNode +} + +/** + * A component that creates a drag-and-drop area for image uploads + */ +const ImageUploadDragArea: React.FC = ({ + onFile, + children, +}) => { + const [isDragOver, setIsDragOver] = useState(false) + const [isDragActive, setIsDragActive] = useState(false) + + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setIsDragActive(false) + setIsDragOver(false) + } + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragOver(true) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + setIsDragOver(false) + + const files = Array.from(e.dataTransfer.files) + if (files.length > 0) { + onFile(files) + } + } + + return ( +
+ {children} +
+ ) +} + +interface ImageUploadPreviewProps { + /** + * The file item to preview + */ + fileItem: FileItem + /** + * Callback to remove this file from upload queue + */ + onRemove: () => void +} + +/** + * Component that displays a preview of an uploading file with progress + */ +const ImageUploadPreview: React.FC = ({ + fileItem, + onRemove, +}) => { + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}` + } + + return ( +
+ {fileItem.status === "uploading" && ( +
+ )} + +
+
+
+ +
+
+ + {fileItem.file.name} + + + {formatFileSize(fileItem.file.size)} + +
+
+
+ {fileItem.status === "uploading" && ( + + {fileItem.progress}% + + )} + +
+
+
+ ) +} + +const DropZoneContent: React.FC<{ maxSize: number; limit: number }> = ({ + maxSize, + limit, +}) => ( + <> +
+ + +
+ +
+
+ +
+ + Click to upload or drag and drop + + + Maximum {limit} file{limit === 1 ? "" : "s"}, {maxSize / 1024 / 1024}MB + each. + +
+ +) + +export const ImageUploadNode: React.FC = (props) => { + const { accept, limit, maxSize } = props.node.attrs + const inputRef = useRef(null) + const extension = props.extension + + const uploadOptions: UploadOptions = { + maxSize, + limit, + accept, + upload: extension.options.upload, + onSuccess: extension.options.onSuccess, + onError: extension.options.onError, + } + + const { fileItems, uploadFiles, removeFileItem, clearAllFiles } = + useFileUpload(uploadOptions) + + const handleUpload = async (files: File[]) => { + const urls = await uploadFiles(files) + + if (urls.length > 0) { + const pos = props.getPos() + + if (isValidPosition(pos)) { + const imageNodes = urls.map((url, index) => { + const filename = + files[index]?.name.replace(/\.[^/.]+$/, "") || "unknown" + return { + type: extension.options.type, + attrs: { + ...extension.options, + src: url, + alt: filename, + title: filename, + }, + } + }) + + props.editor + .chain() + .focus() + .deleteRange({ from: pos, to: pos + props.node.nodeSize }) + .insertContentAt(pos, imageNodes) + .run() + + focusNextNode(props.editor) + } + } + } + + const handleChange = (e: React.ChangeEvent) => { + const files = e.target.files + if (!files || files.length === 0) { + extension.options.onError?.(new Error("No file selected")) + return + } + handleUpload(Array.from(files)) + } + + const handleClick = () => { + if (inputRef.current && fileItems.length === 0) { + inputRef.current.value = "" + inputRef.current.click() + } + } + + const hasFiles = fileItems.length > 0 + + return ( + + {!hasFiles && ( + + + + )} + + {hasFiles && ( +
+ {fileItems.length > 1 && ( +
+ Uploading {fileItems.length} files + +
+ )} + {fileItems.map((fileItem) => ( + removeFileItem(fileItem.id)} + /> + ))} +
+ )} + + 1} + onChange={handleChange} + onClick={(e: React.MouseEvent) => e.stopPropagation()} + /> +
+ ) +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/index.tsx new file mode 100644 index 0000000000..2510a62fae --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/index.tsx @@ -0,0 +1 @@ +export * from "./image-upload-node-extension" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/list-node/list-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/list-node/list-node.scss new file mode 100644 index 0000000000..d0fe5c8f25 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/list-node/list-node.scss @@ -0,0 +1,160 @@ +.tiptap.ProseMirror { + --tt-checklist-bg-color: var(--tt-gray-light-a-100); + --tt-checklist-bg-active-color: var(--tt-gray-light-a-900); + --tt-checklist-border-color: var(--tt-gray-light-a-200); + --tt-checklist-border-active-color: var(--tt-gray-light-a-900); + --tt-checklist-check-icon-color: var(--white); + --tt-checklist-text-active: var(--tt-gray-light-a-500); + + .dark & { + --tt-checklist-bg-color: var(--tt-gray-dark-a-100); + --tt-checklist-bg-active-color: var(--tt-gray-dark-a-900); + --tt-checklist-border-color: var(--tt-gray-dark-a-200); + --tt-checklist-border-active-color: var(--tt-gray-dark-a-900); + --tt-checklist-check-icon-color: var(--black); + --tt-checklist-text-active: var(--tt-gray-dark-a-500); + } +} + +/* ===================== + LISTS + ===================== */ +.tiptap.ProseMirror { + // Common list styles + ol, + ul { + margin-top: 1.5em; + margin-bottom: 1.5em; + padding-left: 1.5em; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + ol, + ul { + margin-top: 0; + margin-bottom: 0; + } + } + + li { + p { + margin-top: 0; + line-height: 1.6; + } + } + + // Ordered lists + ol { + list-style: decimal; + + ol { + list-style: lower-alpha; + + ol { + list-style: lower-roman; + } + } + } + + // Unordered lists + ul:not([data-type="taskList"]) { + list-style: disc; + + ul { + list-style: circle; + + ul { + list-style: square; + } + } + } + + // Task lists + ul[data-type="taskList"] { + padding-left: 0.25em; + + li { + display: flex; + flex-direction: row; + align-items: flex-start; + + &:not(:has(> p:first-child)) { + list-style-type: none; + } + + &[data-checked="true"] { + > div > p { + opacity: 0.5; + text-decoration: line-through; + } + + > div > p span { + text-decoration: line-through; + } + } + + label { + position: relative; + padding-top: 0.375rem; + padding-right: 0.5rem; + + input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; + } + + span { + display: block; + width: 1em; + height: 1em; + border: 1px solid var(--tt-checklist-border-color); + border-radius: var(--tt-radius-xs, 0.25rem); + position: relative; + cursor: pointer; + background-color: var(--tt-checklist-bg-color); + transition: + background-color 80ms ease-out, + border-color 80ms ease-out; + + &::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 0.75em; + height: 0.75em; + background-color: var(--tt-checklist-check-icon-color); + opacity: 0; + -webkit-mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.4142%204.58579C22.1953%205.36683%2022.1953%206.63317%2021.4142%207.41421L10.4142%2018.4142C9.63317%2019.1953%208.36684%2019.1953%207.58579%2018.4142L2.58579%2013.4142C1.80474%2012.6332%201.80474%2011.3668%202.58579%2010.5858C3.36683%209.80474%204.63317%209.80474%205.41421%2010.5858L9%2014.1716L18.5858%204.58579C19.3668%203.80474%2020.6332%203.80474%2021.4142%204.58579Z%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") + center/contain no-repeat; + mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.4142%204.58579C22.1953%205.36683%2022.1953%206.63317%2021.4142%207.41421L10.4142%2018.4142C9.63317%2019.1953%208.36684%2019.1953%207.58579%2018.4142L2.58579%2013.4142C1.80474%2012.6332%201.80474%2011.3668%202.58579%2010.5858C3.36683%209.80474%204.63317%209.80474%205.41421%2010.5858L9%2014.1716L18.5858%204.58579C19.3668%203.80474%2020.6332%203.80474%2021.4142%204.58579Z%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") + center/contain no-repeat; + } + } + + input[type="checkbox"]:checked + span { + background: var(--tt-checklist-bg-active-color); + border-color: var(--tt-checklist-border-active-color); + + &::before { + opacity: 1; + } + } + } + + div { + flex: 1 1 0%; + min-width: 0; + } + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss new file mode 100644 index 0000000000..7d4145a5ca --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss @@ -0,0 +1,273 @@ +.tiptap.ProseMirror { + --tt-collaboration-carets-label: var(--tt-gray-light-900); + --link-text-color: var(--tt-brand-color-500); + --thread-text: var(--tt-gray-light-900); + --placeholder-color: var(--tt-gray-light-a-400); + --thread-bg-color: var(--tt-color-yellow-inc-2); + + // ai + --tiptap-ai-insertion-color: var(--tt-brand-color-600); + + .dark & { + --tt-collaboration-carets-label: var(--tt-gray-dark-100); + --link-text-color: var(--tt-brand-color-400); + --thread-text: var(--tt-gray-dark-900); + --placeholder-color: var(--tt-gray-dark-a-400); + --thread-bg-color: var(--tt-color-yellow-dec-2); + + --tiptap-ai-insertion-color: var(--tt-brand-color-400); + } +} + +/* Ensure each top-level node has relative positioning + so absolutely positioned placeholders work correctly */ +.tiptap.ProseMirror > * { + position: relative; +} + +/* ===================== + CORE EDITOR STYLES + ===================== */ +.tiptap.ProseMirror { + white-space: pre-wrap; + outline: none; + caret-color: var(--tt-cursor-color); + + // Paragraph spacing + p:not(:first-child):not(td p):not(th p) { + font-size: 1rem; + line-height: 1.6; + font-weight: normal; + margin-top: 20px; + } + + // Selection styles + &:not(.readonly):not(.ProseMirror-hideselection) { + ::selection { + background-color: var(--tt-selection-color); + } + + .selection::selection { + background: transparent; + } + } + + .selection { + display: inline; + background-color: var(--tt-selection-color); + } + + // Selected node styles + .ProseMirror-selectednode:not(img):not(pre):not(.react-renderer) { + border-radius: var(--tt-radius-md); + background-color: var(--tt-selection-color); + } + + .ProseMirror-hideselection { + caret-color: transparent; + } + + // Resize cursor + &.resize-cursor { + cursor: ew-resize; + cursor: col-resize; + } +} + +/* ===================== + TEXT DECORATION + ===================== */ +.tiptap.ProseMirror { + // Text decoration inheritance for spans + a span { + text-decoration: underline; + } + + s span { + text-decoration: line-through; + } + + u span { + text-decoration: underline; + } + + .tiptap-ai-insertion { + color: var(--tiptap-ai-insertion-color); + } +} + +/* ===================== + COLLABORATION + ===================== */ +.tiptap.ProseMirror { + .collaboration-carets { + &__caret { + border-right: 1px solid transparent; + border-left: 1px solid transparent; + pointer-events: none; + margin-left: -1px; + margin-right: -1px; + position: relative; + word-break: normal; + } + + &__label { + color: var(--tt-collaboration-carets-label); + border-radius: 0.25rem; + border-bottom-left-radius: 0; + font-size: 0.75rem; + font-weight: 600; + left: -1px; + line-height: 1; + padding: 0.125rem 0.375rem; + position: absolute; + top: -1.3em; + user-select: none; + white-space: nowrap; + } + } +} + +/* ===================== + EMOJI + ===================== */ +.tiptap.ProseMirror [data-type="emoji"] img { + display: inline-block; + width: 1.25em; + height: 1.25em; + cursor: text; +} + +/* ===================== + LINKS + ===================== */ +.tiptap.ProseMirror { + a { + color: var(--link-text-color); + text-decoration: underline; + } +} + +/* ===================== + MENTION + ===================== */ +.tiptap.ProseMirror { + [data-type="mention"] { + display: inline-block; + color: var(--tt-brand-color-500); + } +} + +/* ===================== + THREADS + ===================== */ +.tiptap.ProseMirror { + // Base styles for inline threads + .tiptap-thread.tiptap-thread--unresolved.tiptap-thread--inline { + transition: + color 0.2s ease-in-out, + background-color 0.2s ease-in-out; + color: var(--thread-text); + border-bottom: 2px dashed var(--tt-color-yellow-base); + font-weight: 600; + + &.tiptap-thread--selected, + &.tiptap-thread--hovered { + background-color: var(--thread-bg-color); + border-bottom-color: transparent; + } + } + + // Block thread styles with images + .tiptap-thread.tiptap-thread--unresolved.tiptap-thread--block { + &:has(img) { + outline: 0.125rem solid var(--tt-color-yellow-base); + border-radius: var(--tt-radius-xs, 0.25rem); + overflow: hidden; + width: fit-content; + + &.tiptap-thread--selected { + outline-width: 0.25rem; + outline-color: var(--tt-color-yellow-base); + } + + &.tiptap-thread--hovered { + outline-width: 0.25rem; + } + } + + // Block thread styles without images + &:not(:has(img)) { + border-radius: 0.25rem; + border-bottom: 0.125rem dashed var(--tt-color-yellow-base); + border-top: 0.125rem dashed var(--tt-color-yellow-base); + // padding-bottom: 0.5rem; + outline: 0.25rem solid transparent; + + &.tiptap-thread--hovered, + &.tiptap-thread--selected { + background-color: var(--tt-color-yellow-base); + outline-color: var(--tt-color-yellow-base); + } + } + } + + // Resolved thread styles + .tiptap-thread.tiptap-thread--resolved.tiptap-thread--inline.tiptap-thread--selected { + background-color: var(--tt-color-yellow-base); + border-color: transparent; + opacity: 0.5; + } + + // React renderer specific styles + .tiptap-thread.tiptap-thread--block:has(.react-renderer) { + margin-top: 3rem; + margin-bottom: 3rem; + } +} + +/* ===================== + PLACEHOLDER + ===================== */ +.is-empty:not(.with-slash)[data-placeholder]:has( + > .ProseMirror-trailingBreak:only-child + )::before { + content: attr(data-placeholder); +} + +.is-empty.with-slash[data-placeholder]:has( + > .ProseMirror-trailingBreak:only-child + )::before { + content: "Write, type '/' for commands…"; + font-style: italic; +} + +.is-empty[data-placeholder]:has( + > .ProseMirror-trailingBreak:only-child + ):before { + pointer-events: none; + height: 0; + position: absolute; + width: 100%; + text-align: inherit; + left: 0; + right: 0; +} + +.is-empty[data-placeholder]:has(> .ProseMirror-trailingBreak):before { + color: var(--placeholder-color); +} + +/* ===================== + DROPCURSOR + ===================== */ +.prosemirror-dropcursor-block, +.prosemirror-dropcursor-inline { + background: var(--tt-brand-color-400) !important; + border-radius: 0.25rem; + margin-left: -1px; + margin-right: -1px; + width: 100%; + height: 0.188rem; + cursor: grabbing; +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/data/content.json b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/data/content.json new file mode 100644 index 0000000000..4a3c0e8617 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/data/content.json @@ -0,0 +1,477 @@ +{ + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "textAlign": null, + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Getting started" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "text": "Welcome to the " + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + }, + { + "type": "highlight", + "attrs": { + "color": "var(--tt-color-highlight-yellow)" + } + } + ], + "text": "Simple Editor" + }, + { + "type": "text", + "text": " template! This template integrates " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "open source" + }, + { + "type": "text", + "text": " UI components and Tiptap extensions licensed under " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "MIT" + }, + { + "type": "text", + "text": "." + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "text": "Integrate it by following the " + }, + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Tiptap UI Components docs" + }, + { + "type": "text", + "text": " or using our CLI tool." + } + ] + }, + { + "type": "codeBlock", + "attrs": { + "language": null + }, + "content": [ + { + "type": "text", + "text": "npx @tiptap/cli init" + } + ] + }, + { + "type": "heading", + "attrs": { + "textAlign": null, + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Features" + } + ] + }, + { + "type": "blockquote", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "A fully responsive rich text editor with built-in support for common formatting and layout tools. Type markdown " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "**" + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": " or use keyboard shortcuts " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "⌘+B" + }, + { + "type": "text", + "text": " for " + }, + { + "type": "text", + "marks": [ + { + "type": "strike" + } + ], + "text": "most" + }, + { + "type": "text", + "text": " all common markdown marks. 🪄" + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Add images, customize alignment, and apply " + }, + { + "type": "text", + "marks": [ + { + "type": "highlight", + "attrs": { + "color": "var(--tt-color-highlight-blue)" + } + } + ], + "text": "advanced formatting" + }, + { + "type": "text", + "text": " to make your writing more engaging and professional." + } + ] + }, + { + "type": "image", + "attrs": { + "src": "/images/tiptap-ui-placeholder-image.jpg", + "alt": "placeholder-image", + "title": "placeholder-image" + } + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Superscript" + }, + { + "type": "text", + "text": " (x" + }, + { + "type": "text", + "marks": [ + { + "type": "superscript" + } + ], + "text": "2" + }, + { + "type": "text", + "text": ") and " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Subscript" + }, + { + "type": "text", + "text": " (H" + }, + { + "type": "text", + "marks": [ + { + "type": "subscript" + } + ], + "text": "2" + }, + { + "type": "text", + "text": "O) for precision." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Typographic conversion" + }, + { + "type": "text", + "text": ": automatically convert to " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "->" + }, + { + "type": "text", + "text": " an arrow " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "→" + }, + { + "type": "text", + "text": "." + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "→ " + }, + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor#features", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Learn more" + } + ] + }, + { + "type": "horizontalRule" + }, + { + "type": "heading", + "attrs": { + "textAlign": "left", + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Make it your own" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Switch between light and dark modes, and tailor the editor's appearance with customizable CSS to match your style." + } + ] + }, + { + "type": "taskList", + "content": [ + { + "type": "taskItem", + "attrs": { + "checked": true + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Test template" + } + ] + } + ] + }, + { + "type": "taskItem", + "attrs": { + "checked": false + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Integrate the free template" + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + } + } + ] +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss new file mode 100644 index 0000000000..8faf836440 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.scss @@ -0,0 +1,82 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +body { + --tt-toolbar-height: 44px; + --tt-theme-text: var(--tt-gray-light-900); + + .dark & { + --tt-theme-text: var(--tt-gray-dark-900); + } +} + +body { + font-family: "Inter", sans-serif; + color: var(--tt-theme-text); + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + padding: 0; + overscroll-behavior-y: none; +} + +html, +body { + overscroll-behavior-x: none; +} + +html, +body, +#root, +#app { + height: 100%; + background-color: var(--tt-bg-color); +} + +::-webkit-scrollbar { + width: 0.25rem; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--tt-scrollbar-color) transparent; +} + +::-webkit-scrollbar-thumb { + background-color: var(--tt-scrollbar-color); + border-radius: 9999px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +.tiptap.ProseMirror { + font-family: "DM Sans", sans-serif; +} + +.simple-editor-wrapper { + width: 100vw; + height: 100vh; + overflow: auto; +} + +.simple-editor-content { + max-width: 648px; + width: 100%; + margin: 0 auto; + height: 100%; + display: flex; + flex-direction: column; + flex: 1; +} + +.simple-editor-content .tiptap.ProseMirror.simple-editor { + flex: 1; + padding: 3rem 3rem 30vh; +} + +@media screen and (max-width: 480px) { + .simple-editor-content .tiptap.ProseMirror.simple-editor { + padding: 1rem 1.5rem 30vh; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx new file mode 100644 index 0000000000..b7c268349a --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx @@ -0,0 +1,290 @@ +// File not in use. This is the base Simple Editor file, https://tiptap.dev/docs/ui-components/templates/simple-editor. +// Selectively used in ol-components/src/components/TiptapEditor/TiptapEditor.tsx. Left in project for reference. + +"use client" + +import React, { useEffect, useRef, useState } from "react" +import { EditorContent, EditorContext, useEditor } from "@tiptap/react" + +// --- Tiptap Core Extensions --- +import { StarterKit } from "@tiptap/starter-kit" +import { Image } from "@tiptap/extension-image" +import { TaskItem, TaskList } from "@tiptap/extension-list" +import { TextAlign } from "@tiptap/extension-text-align" +import { Typography } from "@tiptap/extension-typography" +import { Highlight } from "@tiptap/extension-highlight" +import { Subscript } from "@tiptap/extension-subscript" +import { Superscript } from "@tiptap/extension-superscript" +import { Selection } from "@tiptap/extensions" + +// --- UI Primitives --- +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Spacer } from "@/components/tiptap-ui-primitive/spacer" +import { + Toolbar, + ToolbarGroup, + ToolbarSeparator, +} from "@/components/tiptap-ui-primitive/toolbar" + +// --- Tiptap Node --- +import { ImageUploadNode } from "@/components/tiptap-node/image-upload-node/image-upload-node-extension" +import { HorizontalRule } from "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension" +import "@/components/tiptap-node/blockquote-node/blockquote-node.scss" +import "@/components/tiptap-node/code-block-node/code-block-node.scss" +import "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss" +import "@/components/tiptap-node/list-node/list-node.scss" +import "@/components/tiptap-node/image-node/image-node.scss" +import "@/components/tiptap-node/heading-node/heading-node.scss" +import "@/components/tiptap-node/paragraph-node/paragraph-node.scss" + +// --- Tiptap UI --- +import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" +import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button" +import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" +import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button" +import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button" +import { + ColorHighlightPopover, + ColorHighlightPopoverContent, + ColorHighlightPopoverButton, +} from "@/components/tiptap-ui/color-highlight-popover" +import { + LinkPopover, + LinkContent, + LinkButton, +} from "@/components/tiptap-ui/link-popover" +import { MarkButton } from "@/components/tiptap-ui/mark-button" +import { TextAlignButton } from "@/components/tiptap-ui/text-align-button" +import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button" + +// --- Icons --- +import { ArrowLeftIcon } from "@/components/tiptap-icons/arrow-left-icon" +import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" +import { LinkIcon } from "@/components/tiptap-icons/link-icon" + +// --- Hooks --- +import { useIsMobile } from "@/hooks/use-mobile" +import { useWindowSize } from "@/hooks/use-window-size" +import { useCursorVisibility } from "@/hooks/use-cursor-visibility" + +// --- Components --- +import { ThemeToggle } from "@/components/tiptap-templates/simple/theme-toggle" + +// --- Lib --- +import { handleImageUpload, MAX_FILE_SIZE } from "@/lib/tiptap-utils" + +// --- Styles --- +import "@/components/tiptap-templates/simple/simple-editor.scss" + +const MainToolbarContent = ({ + onHighlighterClick, + onLinkClick, + isMobile, +}: { + onHighlighterClick: () => void + onLinkClick: () => void + isMobile: boolean +}) => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + {!isMobile ? ( + + ) : ( + + )} + {!isMobile ? : } + + + + + + + + + + + + + + + + + + + + + + + + + + + {isMobile && } + + + + + + ) +} + +const MobileToolbarContent = ({ + type, + onBack, +}: { + type: "highlighter" | "link" + onBack: () => void +}) => ( + <> + + + + + + + {type === "highlighter" ? ( + + ) : ( + + )} + +) + +export function SimpleEditor() { + const isMobile = useIsMobile() + const { height } = useWindowSize() + const [mobileView, setMobileView] = useState<"main" | "highlighter" | "link">( + "main", + ) + const toolbarRef = useRef(null) + + const editor = useEditor({ + immediatelyRender: false, + shouldRerenderOnTransaction: false, + editorProps: { + attributes: { + autocomplete: "off", + autocorrect: "off", + autocapitalize: "off", + "aria-label": "Main content area, start typing to enter text.", + class: "simple-editor", + }, + }, + extensions: [ + StarterKit.configure({ + horizontalRule: false, + link: { + openOnClick: false, + enableClickSelection: true, + }, + }), + HorizontalRule, + TextAlign.configure({ types: ["heading", "paragraph"] }), + TaskList, + TaskItem.configure({ nested: true }), + Highlight.configure({ multicolor: true }), + Image, + Typography, + Superscript, + Subscript, + Selection, + ImageUploadNode.configure({ + accept: "image/*", + maxSize: MAX_FILE_SIZE, + limit: 3, + upload: handleImageUpload, + onError: (error) => console.error("Upload failed:", error), + }), + ], + content: { + type: "doc", + content: [ + { + type: "paragraph", + content: [], + }, + ], + }, + }) + + const rect = useCursorVisibility({ + editor, + overlayHeight: toolbarRef.current?.getBoundingClientRect().height ?? 0, + }) + + useEffect(() => { + if (!isMobile && mobileView !== "main") { + setMobileView("main") + } + }, [isMobile, mobileView]) + + return ( +
+ + + {mobileView === "main" ? ( + setMobileView("highlighter")} + onLinkClick={() => setMobileView("link")} + isMobile={isMobile} + /> + ) : ( + setMobileView("main")} + /> + )} + + + + +
+ ) +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx new file mode 100644 index 0000000000..18645e8d4f --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx @@ -0,0 +1,44 @@ +import { Button } from "@/components/tiptap-ui-primitive/button" + +// --- Icons --- +import { MoonStarIcon } from "@/components/tiptap-icons/moon-star-icon" +import { SunIcon } from "@/components/tiptap-icons/sun-icon" +import { useEffect, useState } from "react" + +export function ThemeToggle() { + const [isDarkMode, setIsDarkMode] = useState(false) + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + const handleChange = () => setIsDarkMode(mediaQuery.matches) + mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) + }, []) + + useEffect(() => { + const initialDarkMode = + !!document.querySelector('meta[name="color-scheme"][content="dark"]') || + window.matchMedia("(prefers-color-scheme: dark)").matches + setIsDarkMode(initialDarkMode) + }, []) + + useEffect(() => { + document.documentElement.classList.toggle("dark", isDarkMode) + }, [isDarkMode]) + + const toggleDarkMode = () => setIsDarkMode((isDark) => !isDark) + + return ( + + ) +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss new file mode 100644 index 0000000000..8f8a988fbb --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss @@ -0,0 +1,395 @@ +.tiptap-badge { + /************************************************** + Default + **************************************************/ + + /* Light mode */ + --tt-badge-border-color: var(--tt-gray-light-a-200); + --tt-badge-border-color-subdued: var(--tt-gray-light-a-200); + --tt-badge-border-color-emphasized: var(--tt-gray-light-a-600); + --tt-badge-text-color: var(--tt-gray-light-a-500); + --tt-badge-text-color-subdued: var( + --tt-gray-light-a-400 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-gray-light-a-600 + ); //more important badge + --tt-badge-bg-color: var(--white); + --tt-badge-bg-color-subdued: var(--white); //less important badge + --tt-badge-bg-color-emphasized: var(--white); //more important badge + --tt-badge-icon-color: var(--tt-gray-light-a-500); + --tt-badge-icon-color-subdued: var( + --tt-gray-light-a-400 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-brand-color-600 + ); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-gray-dark-a-200); + --tt-badge-border-color-subdued: var(--tt-gray-dark-a-200); + --tt-badge-border-color-emphasized: var(--tt-gray-dark-a-500); + --tt-badge-text-color: var(--tt-gray-dark-a-500); + --tt-badge-text-color-subdued: var( + --tt-gray-dark-a-400 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-gray-dark-a-600 + ); //more important badge + --tt-badge-bg-color: var(--black); + --tt-badge-bg-color-subdued: var(--black); //less important badge + --tt-badge-bg-color-emphasized: var(--black); //more important badge + --tt-badge-icon-color: var(--tt-gray-dark-a-500); + --tt-badge-icon-color-subdued: var( + --tt-gray-dark-a-400 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-brand-color-400 + ); //more important badge + } + + /************************************************** + Ghost + **************************************************/ + + &[data-style="ghost"] { + /* Light mode */ + --tt-badge-border-color: var(--tt-gray-light-a-200); + --tt-badge-border-color-subdued: var(--tt-gray-light-a-200); + --tt-badge-border-color-emphasized: var(--tt-gray-light-a-600); + --tt-badge-text-color: var(--tt-gray-light-a-500); + --tt-badge-text-color-subdued: var( + --tt-gray-light-a-400 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-gray-light-a-600 + ); //more important badge + --tt-badge-bg-color: var(--transparent); + --tt-badge-bg-color-subdued: var(--transparent); //less important badge + --tt-badge-bg-color-emphasized: var(--transparent); //more important badge + --tt-badge-icon-color: var(--tt-gray-light-a-500); + --tt-badge-icon-color-subdued: var( + --tt-gray-light-a-400 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-brand-color-600 + ); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-gray-dark-a-200); + --tt-badge-border-color-subdued: var(--tt-gray-dark-a-200); + --tt-badge-border-color-emphasized: var(--tt-gray-dark-a-500); + --tt-badge-text-color: var(--tt-gray-dark-a-500); + --tt-badge-text-color-subdued: var( + --tt-gray-dark-a-400 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-gray-dark-a-600 + ); //more important badge + --tt-badge-bg-color: var(--transparent); + --tt-badge-bg-color-subdued: var(--transparent); //less important badge + --tt-badge-bg-color-emphasized: var(--transparent); //more important badge + --tt-badge-icon-color: var(--tt-gray-dark-a-500); + --tt-badge-icon-color-subdued: var( + --tt-gray-dark-a-400 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-brand-color-400 + ); //more important badge + } + } + + /************************************************** + Gray + **************************************************/ + + &[data-style="gray"] { + /* Light mode */ + --tt-badge-border-color: var(--tt-gray-light-a-200); + --tt-badge-border-color-subdued: var(--tt-gray-light-a-200); + --tt-badge-border-color-emphasized: var(--tt-gray-light-a-500); + --tt-badge-text-color: var(--tt-gray-light-a-500); + --tt-badge-text-color-subdued: var( + --tt-gray-light-a-400 + ); //less important badge + --tt-badge-text-color-emphasized: var(--white); //more important badge + --tt-badge-bg-color: var(--tt-gray-light-a-100); + --tt-badge-bg-color-subdued: var( + --tt-gray-light-a-50 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-gray-light-a-700 + ); //more important badge + --tt-badge-icon-color: var(--tt-gray-light-a-500); + --tt-badge-icon-color-subdued: var( + --tt-gray-light-a-400 + ); //less important badge + --tt-badge-icon-color-emphasized: var(--white); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-gray-dark-a-200); + --tt-badge-border-color-subdued: var(--tt-gray-dark-a-200); + --tt-badge-border-color-emphasized: var(--tt-gray-dark-a-500); + --tt-badge-text-color: var(--tt-gray-dark-a-500); + --tt-badge-text-color-subdued: var( + --tt-gray-dark-a-400 + ); //less important badge + --tt-badge-text-color-emphasized: var(--black); //more important badge + --tt-badge-bg-color: var(--tt-gray-dark-a-100); + --tt-badge-bg-color-subdued: var( + --tt-gray-dark-a-50 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-gray-dark-a-800 + ); //more important badge + --tt-badge-icon-color: var(--tt-gray-dark-a-500); + --tt-badge-icon-color-subdued: var( + --tt-gray-dark-a-400 + ); //less important badge + --tt-badge-icon-color-emphasized: var(--black); //more important badge + } + } + + /************************************************** + Green + **************************************************/ + + &[data-style="green"] { + /* Light mode */ + --tt-badge-border-color: var(--tt-color-green-inc-2); + --tt-badge-border-color-subdued: var(--tt-color-green-inc-3); + --tt-badge-border-color-emphasized: var(--tt-color-green-dec-2); + --tt-badge-text-color: var(--tt-color-green-dec-3); + --tt-badge-text-color-subdued: var( + --tt-color-green-dec-2 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-color-green-inc-5 + ); //more important badge + --tt-badge-bg-color: var(--tt-color-green-inc-4); + --tt-badge-bg-color-subdued: var( + --tt-color-green-inc-5 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-color-green-dec-1 + ); //more important badge + --tt-badge-icon-color: var(--tt-color-green-dec-3); + --tt-badge-icon-color-subdued: var( + --tt-color-green-dec-2 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-color-green-inc-5 + ); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-color-green-dec-2); + --tt-badge-border-color-subdued: var(--tt-color-green-dec-3); + --tt-badge-border-color-emphasized: var(--tt-color-green-base); + --tt-badge-text-color: var(--tt-color-green-inc-3); + --tt-badge-text-color-subdued: var( + --tt-color-green-inc-2 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-color-green-dec-5 + ); //more important badge + --tt-badge-bg-color: var(--tt-color-green-dec-4); + --tt-badge-bg-color-subdued: var( + --tt-color-green-dec-5 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-color-green-inc-1 + ); //more important badge + --tt-badge-icon-color: var(--tt-color-green-inc-3); + --tt-badge-icon-color-subdued: var( + --tt-color-green-inc-2 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-color-green-dec-5 + ); //more important badge + } + } + + /************************************************** + Yellow + **************************************************/ + + &[data-style="yellow"] { + /* Light mode */ + --tt-badge-border-color: var(--tt-color-yellow-inc-2); + --tt-badge-border-color-subdued: var(--tt-color-yellow-inc-3); + --tt-badge-border-color-emphasized: var(--tt-color-yellow-dec-1); + --tt-badge-text-color: var(--tt-color-yellow-dec-3); + --tt-badge-text-color-subdued: var( + --tt-color-yellow-dec-2 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-color-yellow-dec-3 + ); //more important badge + --tt-badge-bg-color: var(--tt-color-yellow-inc-4); + --tt-badge-bg-color-subdued: var( + --tt-color-yellow-inc-5 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-color-yellow-base + ); //more important badge + --tt-badge-icon-color: var(--tt-color-yellow-dec-3); + --tt-badge-icon-color-subdued: var( + --tt-color-yellow-dec-2 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-color-yellow-dec-3 + ); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-color-yellow-dec-2); + --tt-badge-border-color-subdued: var(--tt-color-yellow-dec-3); + --tt-badge-border-color-emphasized: var(--tt-color-yellow-inc-1); + --tt-badge-text-color: var(--tt-color-yellow-inc-3); + --tt-badge-text-color-subdued: var( + --tt-color-yellow-inc-2 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-color-yellow-dec-3 + ); //more important badge + --tt-badge-bg-color: var(--tt-color-yellow-dec-4); + --tt-badge-bg-color-subdued: var( + --tt-color-yellow-dec-5 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-color-yellow-base + ); //more important badge + --tt-badge-icon-color: var(--tt-color-yellow-inc-3); + --tt-badge-icon-color-subdued: var( + --tt-color-yellow-inc-2 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-color-yellow-dec-3 + ); //more important badge + } + } + + /************************************************** + Red + **************************************************/ + + &[data-style="red"] { + /* Light mode */ + --tt-badge-border-color: var(--tt-color-red-inc-2); + --tt-badge-border-color-subdued: var(--tt-color-red-inc-3); + --tt-badge-border-color-emphasized: var(--tt-color-red-dec-2); + --tt-badge-text-color: var(--tt-color-red-dec-3); + --tt-badge-text-color-subdued: var( + --tt-color-red-dec-2 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-color-red-inc-5 + ); //more important badge + --tt-badge-bg-color: var(--tt-color-red-inc-4); + --tt-badge-bg-color-subdued: var( + --tt-color-red-inc-5 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-color-red-dec-1 + ); //more important badge + --tt-badge-icon-color: var(--tt-color-red-dec-3); + --tt-badge-icon-color-subdued: var( + --tt-color-red-dec-2 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-color-red-inc-5 + ); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-color-red-dec-2); + --tt-badge-border-color-subdued: var(--tt-color-red-dec-3); + --tt-badge-border-color-emphasized: var(--tt-color-red-base); + --tt-badge-text-color: var(--tt-color-red-inc-3); + --tt-badge-text-color-subdued: var( + --tt-color-red-inc-2 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-color-red-dec-5 + ); //more important badge + --tt-badge-bg-color: var(--tt-color-red-dec-4); + --tt-badge-bg-color-subdued: var( + --tt-color-red-dec-5 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-color-red-inc-1 + ); //more important badge + --tt-badge-icon-color: var(--tt-color-red-inc-3); + --tt-badge-icon-color-subdued: var( + --tt-color-red-inc-2 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-color-red-dec-5 + ); //more important badge + } + } + + /************************************************** + Brand + **************************************************/ + + &[data-style="brand"] { + /* Light mode */ + --tt-badge-border-color: var(--tt-brand-color-300); + --tt-badge-border-color-subdued: var(--tt-brand-color-200); + --tt-badge-border-color-emphasized: var(--tt-brand-color-600); + --tt-badge-text-color: var(--tt-brand-color-800); + --tt-badge-text-color-subdued: var( + --tt-brand-color-700 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-brand-color-50 + ); //more important badge + --tt-badge-bg-color: var(--tt-brand-color-100); + --tt-badge-bg-color-subdued: var( + --tt-brand-color-50 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-brand-color-600 + ); //more important badge + --tt-badge-icon-color: var(--tt-brand-color-800); + --tt-badge-icon-color-subdued: var( + --tt-brand-color-700 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-brand-color-100 + ); //more important badge + + /* Dark mode */ + .dark & { + --tt-badge-border-color: var(--tt-brand-color-700); + --tt-badge-border-color-subdued: var(--tt-brand-color-800); + --tt-badge-border-color-emphasized: var(--tt-brand-color-400); + --tt-badge-text-color: var(--tt-brand-color-200); + --tt-badge-text-color-subdued: var( + --tt-brand-color-300 + ); //less important badge + --tt-badge-text-color-emphasized: var( + --tt-brand-color-950 + ); //more important badge + --tt-badge-bg-color: var(--tt-brand-color-900); + --tt-badge-bg-color-subdued: var( + --tt-brand-color-950 + ); //less important badge + --tt-badge-bg-color-emphasized: var( + --tt-brand-color-400 + ); //more important badge + --tt-badge-icon-color: var(--tt-brand-color-200); + --tt-badge-icon-color-subdued: var( + --tt-brand-color-300 + ); //less important badge + --tt-badge-icon-color-emphasized: var( + --tt-brand-color-900 + ); //more important badge + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-group.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-group.scss new file mode 100644 index 0000000000..91bd45b10e --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-group.scss @@ -0,0 +1,16 @@ +.tiptap-badge-group { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.tiptap-badge-group { + [data-orientation="vertical"] { + flex-direction: column; + } + + [data-orientation="horizontal"] { + flex-direction: row; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.scss new file mode 100644 index 0000000000..b2ca9a8829 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.scss @@ -0,0 +1,99 @@ +.tiptap-badge { + font-size: 0.625rem; + font-weight: 700; + font-feature-settings: + "salt" on, + "cv01" on; + line-height: 1.15; + height: 1.25rem; + min-width: 1.25rem; + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + border: solid 1px; + border-radius: var(--tt-radius-sm, 0.375rem); + transition-property: background, color, opacity; + transition-duration: var(--tt-transition-duration-default); + transition-timing-function: var(--tt-transition-easing-default); + + /* button size large */ + &[data-size="large"] { + font-size: 0.75rem; + height: 1.5rem; + min-width: 1.5rem; + padding: 0.375rem; + border-radius: var(--tt-radius-md, 0.375rem); + } + + /* button size small */ + &[data-size="small"] { + height: 1rem; + min-width: 1rem; + padding: 0.125rem; + border-radius: var(--tt-radius-xs, 0.25rem); + } + + /* trim / expand text of the button */ + .tiptap-badge-text { + padding: 0 0.125rem; + flex-grow: 1; + text-align: left; + } + + &[data-text-trim="on"] { + .tiptap-badge-text { + text-overflow: ellipsis; + overflow: hidden; + } + } + + /* standard icon, what is used */ + .tiptap-badge-icon { + pointer-events: none; + flex-shrink: 0; + width: 0.625rem; + height: 0.625rem; + } + + &[data-size="large"] .tiptap-badge-icon { + width: 0.75rem; + height: 0.75rem; + } +} + +/* -------------------------------------------- +----------- BADGE COLOR SETTINGS ------------- +-------------------------------------------- */ + +.tiptap-badge { + background-color: var(--tt-badge-bg-color); + border-color: var(--tt-badge-border-color); + color: var(--tt-badge-text-color); + + .tiptap-badge-icon { + color: var(--tt-badge-icon-color); + } + + /* Emphasized */ + &[data-appearance="emphasized"] { + background-color: var(--tt-badge-bg-color-emphasized); + border-color: var(--tt-badge-border-color-emphasized); + color: var(--tt-badge-text-color-emphasized); + + .tiptap-badge-icon { + color: var(--tt-badge-icon-color-emphasized); + } + } + + /* Subdued */ + &[data-appearance="subdued"] { + background-color: var(--tt-badge-bg-color-subdued); + border-color: var(--tt-badge-border-color-subdued); + color: var(--tt-badge-text-color-subdued); + + .tiptap-badge-icon { + color: var(--tt-badge-icon-color-subdued); + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx new file mode 100644 index 0000000000..56ecb5948f --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx @@ -0,0 +1,44 @@ +import { forwardRef } from "react" +import "@/components/tiptap-ui-primitive/badge/badge-colors.scss" +import "@/components/tiptap-ui-primitive/badge/badge-group.scss" +import "@/components/tiptap-ui-primitive/badge/badge.scss" + +export interface BadgeProps extends React.HTMLAttributes { + variant?: "ghost" | "white" | "gray" | "green" | "default" + size?: "default" | "small" + appearance?: "default" | "subdued" | "emphasized" + trimText?: boolean +} + +export const Badge = forwardRef( + ( + { + variant, + size = "default", + appearance = "default", + trimText = false, + className, + children, + ...props + }, + ref + ) => { + return ( +
+ {children} +
+ ) + } +) + +Badge.displayName = "Badge" + +export default Badge diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/index.tsx new file mode 100644 index 0000000000..051fa6ea23 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/index.tsx @@ -0,0 +1 @@ +export * from "./badge" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss new file mode 100644 index 0000000000..fc0dd35e44 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss @@ -0,0 +1,429 @@ +.tiptap-button { + /************************************************** + Default button background color + **************************************************/ + + /* Light mode */ + --tt-button-default-bg-color: var(--tt-gray-light-a-100); + --tt-button-hover-bg-color: var(--tt-gray-light-200); + --tt-button-active-bg-color: var(--tt-gray-light-a-200); + --tt-button-active-bg-color-emphasized: var( + --tt-brand-color-100 + ); //more important active state + --tt-button-active-bg-color-subdued: var( + --tt-gray-light-a-200 + ); //less important active state + --tt-button-active-hover-bg-color: var(--tt-gray-light-300); + --tt-button-active-hover-bg-color-emphasized: var( + --tt-brand-color-200 + ); //more important active state hover + --tt-button-active-hover-bg-color-subdued: var( + --tt-gray-light-a-300 + ); //less important active state hover + --tt-button-disabled-bg-color: var(--tt-gray-light-a-50); + + /* Dark mode */ + .dark & { + --tt-button-default-bg-color: var(--tt-gray-dark-a-100); + --tt-button-hover-bg-color: var(--tt-gray-dark-200); + --tt-button-active-bg-color: var(--tt-gray-dark-a-200); + --tt-button-active-bg-color-emphasized: var( + --tt-brand-color-900 + ); //more important active state + --tt-button-active-bg-color-subdued: var( + --tt-gray-dark-a-200 + ); //less important active state + --tt-button-active-hover-bg-color: var(--tt-gray-dark-300); + --tt-button-active-hover-bg-color-emphasized: var( + --tt-brand-color-800 + ); //more important active state hover + --tt-button-active-hover-bg-color-subdued: var( + --tt-gray-dark-a-300 + ); //less important active state hover + --tt-button-disabled-bg-color: var(--tt-gray-dark-a-50); + } + + /************************************************** + Default button text color + **************************************************/ + + /* Light mode */ + --tt-button-default-text-color: var(--tt-gray-light-a-600); + --tt-button-hover-text-color: var(--tt-gray-light-a-900); + --tt-button-active-text-color: var(--tt-gray-light-a-900); + --tt-button-active-text-color-emphasized: var(--tt-gray-light-a-900); + --tt-button-active-text-color-subdued: var(--tt-gray-light-a-900); + --tt-button-disabled-text-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-text-color: var(--tt-gray-dark-a-600); + --tt-button-hover-text-color: var(--tt-gray-dark-a-900); + --tt-button-active-text-color: var(--tt-gray-dark-a-900); + --tt-button-active-text-color-emphasized: var(--tt-gray-dark-a-900); + --tt-button-active-text-color-subdued: var(--tt-gray-dark-a-900); + --tt-button-disabled-text-color: var(--tt-gray-dark-a-300); + } + + /************************************************** + Default button icon color + **************************************************/ + + /* Light mode */ + --tt-button-default-icon-color: var(--tt-gray-light-a-600); + --tt-button-hover-icon-color: var(--tt-gray-light-a-900); + --tt-button-active-icon-color: var(--tt-brand-color-500); + --tt-button-active-icon-color-emphasized: var(--tt-brand-color-600); + --tt-button-active-icon-color-subdued: var(--tt-gray-light-a-900); + --tt-button-disabled-icon-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-icon-color: var(--tt-gray-dark-a-600); + --tt-button-hover-icon-color: var(--tt-gray-dark-a-900); + --tt-button-active-icon-color: var(--tt-brand-color-400); + --tt-button-active-icon-color-emphasized: var(--tt-brand-color-400); + --tt-button-active-icon-color-subdued: var(--tt-gray-dark-a-900); + --tt-button-disabled-icon-color: var(--tt-gray-dark-a-400); + } + + /************************************************** + Default button subicon color + **************************************************/ + + /* Light mode */ + --tt-button-default-icon-sub-color: var(--tt-gray-light-a-400); + --tt-button-hover-icon-sub-color: var(--tt-gray-light-a-500); + --tt-button-active-icon-sub-color: var(--tt-gray-light-a-400); + --tt-button-active-icon-sub-color-emphasized: var(--tt-gray-light-a-500); + --tt-button-active-icon-sub-color-subdued: var(--tt-gray-light-a-400); + --tt-button-disabled-icon-sub-color: var(--tt-gray-light-a-100); + + /* Dark mode */ + .dark & { + --tt-button-default-icon-sub-color: var(--tt-gray-dark-a-300); + --tt-button-hover-icon-sub-color: var(--tt-gray-dark-a-400); + --tt-button-active-icon-sub-color: var(--tt-gray-dark-a-300); + --tt-button-active-icon-sub-color-emphasized: var(--tt-gray-dark-a-400); + --tt-button-active-icon-sub-color-subdued: var(--tt-gray-dark-a-300); + --tt-button-disabled-icon-sub-color: var(--tt-gray-dark-a-100); + } + + /************************************************** + Default button dropdown / arrows color + **************************************************/ + + /* Light mode */ + --tt-button-default-dropdown-arrows-color: var(--tt-gray-light-a-600); + --tt-button-hover-dropdown-arrows-color: var(--tt-gray-light-a-700); + --tt-button-active-dropdown-arrows-color: var(--tt-gray-light-a-600); + --tt-button-active-dropdown-arrows-color-emphasized: var( + --tt-gray-light-a-700 + ); + --tt-button-active-dropdown-arrows-color-subdued: var(--tt-gray-light-a-600); + --tt-button-disabled-dropdown-arrows-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-dropdown-arrows-color: var(--tt-gray-dark-a-600); + --tt-button-hover-dropdown-arrows-color: var(--tt-gray-dark-a-700); + --tt-button-active-dropdown-arrows-color: var(--tt-gray-dark-a-600); + --tt-button-active-dropdown-arrows-color-emphasized: var( + --tt-gray-dark-a-700 + ); + --tt-button-active-dropdown-arrows-color-subdued: var(--tt-gray-dark-a-600); + --tt-button-disabled-dropdown-arrows-color: var(--tt-gray-dark-a-400); + } + + /* ---------------------------------------------------------------- + --------------------------- GHOST BUTTON -------------------------- + ---------------------------------------------------------------- */ + + &[data-style="ghost"] { + /************************************************** + Ghost button background color + **************************************************/ + + /* Light mode */ + --tt-button-default-bg-color: var(--transparent); + --tt-button-hover-bg-color: var(--tt-gray-light-200); + --tt-button-active-bg-color: var(--tt-gray-light-a-100); + --tt-button-active-bg-color-emphasized: var( + --tt-brand-color-100 + ); //more important active state + --tt-button-active-bg-color-subdued: var( + --tt-gray-light-a-100 + ); //less important active state + --tt-button-active-hover-bg-color: var(--tt-gray-light-200); + --tt-button-active-hover-bg-color-emphasized: var( + --tt-brand-color-200 + ); //more important active state hover + --tt-button-active-hover-bg-color-subdued: var( + --tt-gray-light-a-200 + ); //less important active state hover + --tt-button-disabled-bg-color: var(--transparent); + + /* Dark mode */ + .dark & { + --tt-button-default-bg-color: var(--transparent); + --tt-button-hover-bg-color: var(--tt-gray-dark-200); + --tt-button-active-bg-color: var(--tt-gray-dark-a-100); + --tt-button-active-bg-color-emphasized: var( + --tt-brand-color-900 + ); //more important active state + --tt-button-active-bg-color-subdued: var( + --tt-gray-dark-a-100 + ); //less important active state + --tt-button-active-hover-bg-color: var(--tt-gray-dark-200); + --tt-button-active-hover-bg-color-emphasized: var( + --tt-brand-color-800 + ); //more important active state hover + --tt-button-active-hover-bg-color-subdued: var( + --tt-gray-dark-a-200 + ); //less important active state hover + --tt-button-disabled-bg-color: var(--transparent); + } + + /************************************************** + Ghost button text color + **************************************************/ + + /* Light mode */ + --tt-button-default-text-color: var(--tt-gray-light-a-600); + --tt-button-hover-text-color: var(--tt-gray-light-a-900); + --tt-button-active-text-color: var(--tt-gray-light-a-900); + --tt-button-active-text-color-emphasized: var(--tt-gray-light-a-900); + --tt-button-active-text-color-subdued: var(--tt-gray-light-a-900); + --tt-button-disabled-text-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-text-color: var(--tt-gray-dark-a-600); + --tt-button-hover-text-color: var(--tt-gray-dark-a-900); + --tt-button-active-text-color: var(--tt-gray-dark-a-900); + --tt-button-active-text-color-emphasized: var(--tt-gray-dark-a-900); + --tt-button-active-text-color-subdued: var(--tt-gray-dark-a-900); + --tt-button-disabled-text-color: var(--tt-gray-dark-a-300); + } + + /************************************************** + Ghost button icon color + **************************************************/ + + /* Light mode */ + --tt-button-default-icon-color: var(--tt-gray-light-a-600); + --tt-button-hover-icon-color: var(--tt-gray-light-a-900); + --tt-button-active-icon-color: var(--tt-brand-color-500); + --tt-button-active-icon-color-emphasized: var(--tt-brand-color-600); + --tt-button-active-icon-color-subdued: var(--tt-gray-light-a-900); + --tt-button-disabled-icon-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-icon-color: var(--tt-gray-dark-a-600); + --tt-button-hover-icon-color: var(--tt-gray-dark-a-900); + --tt-button-active-icon-color: var(--tt-brand-color-400); + --tt-button-active-icon-color-emphasized: var(--tt-brand-color-300); + --tt-button-active-icon-color-subdued: var(--tt-gray-dark-a-900); + --tt-button-disabled-icon-color: var(--tt-gray-dark-a-400); + } + + /************************************************** + Ghost button subicon color + **************************************************/ + + /* Light mode */ + --tt-button-default-icon-sub-color: var(--tt-gray-light-a-400); + --tt-button-hover-icon-sub-color: var(--tt-gray-light-a-500); + --tt-button-active-icon-sub-color: var(--tt-gray-light-a-400); + --tt-button-active-icon-sub-color-emphasized: var(--tt-gray-light-a-500); + --tt-button-active-icon-sub-color-subdued: var(--tt-gray-light-a-400); + --tt-button-disabled-icon-sub-color: var(--tt-gray-light-a-100); + + /* Dark mode */ + .dark & { + --tt-button-default-icon-sub-color: var(--tt-gray-dark-a-300); + --tt-button-hover-icon-sub-color: var(--tt-gray-dark-a-400); + --tt-button-active-icon-sub-color: var(--tt-gray-dark-a-300); + --tt-button-active-icon-sub-color-emphasized: var(--tt-gray-dark-a-400); + --tt-button-active-icon-sub-color-subdued: var(--tt-gray-dark-a-300); + --tt-button-disabled-icon-sub-color: var(--tt-gray-dark-a-100); + } + + /************************************************** + Ghost button dropdown / arrows color + **************************************************/ + + /* Light mode */ + --tt-button-default-dropdown-arrows-color: var(--tt-gray-light-a-600); + --tt-button-hover-dropdown-arrows-color: var(--tt-gray-light-a-700); + --tt-button-active-dropdown-arrows-color: var(--tt-gray-light-a-600); + --tt-button-active-dropdown-arrows-color-emphasized: var( + --tt-gray-light-a-700 + ); + --tt-button-active-dropdown-arrows-color-subdued: var( + --tt-gray-light-a-600 + ); + --tt-button-disabled-dropdown-arrows-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-dropdown-arrows-color: var(--tt-gray-dark-a-600); + --tt-button-hover-dropdown-arrows-color: var(--tt-gray-dark-a-700); + --tt-button-active-dropdown-arrows-color: var(--tt-gray-dark-a-600); + --tt-button-active-dropdown-arrows-color-emphasized: var( + --tt-gray-dark-a-700 + ); + --tt-button-active-dropdown-arrows-color-subdued: var( + --tt-gray-dark-a-600 + ); + --tt-button-disabled-dropdown-arrows-color: var(--tt-gray-dark-a-400); + } + } + + /* ---------------------------------------------------------------- + -------------------------- PRIMARY BUTTON ------------------------- + ---------------------------------------------------------------- */ + + &[data-style="primary"] { + /************************************************** + Primary button background color + **************************************************/ + + /* Light mode */ + --tt-button-default-bg-color: var(--tt-brand-color-500); + --tt-button-hover-bg-color: var(--tt-brand-color-600); + --tt-button-active-bg-color: var(--tt-brand-color-100); + --tt-button-active-bg-color-emphasized: var( + --tt-brand-color-100 + ); //more important active state + --tt-button-active-bg-color-subdued: var( + --tt-brand-color-100 + ); //less important active state + --tt-button-active-hover-bg-color: var(--tt-brand-color-200); + --tt-button-active-hover-bg-color-emphasized: var( + --tt-brand-color-200 + ); //more important active state hover + --tt-button-active-hover-bg-color-subdued: var( + --tt-brand-color-200 + ); //less important active state hover + --tt-button-disabled-bg-color: var(--tt-gray-light-a-100); + + /* Dark mode */ + .dark & { + --tt-button-default-bg-color: var(--tt-brand-color-500); + --tt-button-hover-bg-color: var(--tt-brand-color-600); + --tt-button-active-bg-color: var(--tt-brand-color-900); + --tt-button-active-bg-color-emphasized: var( + --tt-brand-color-900 + ); //more important active state + --tt-button-active-bg-color-subdued: var( + --tt-brand-color-900 + ); //less important active state + --tt-button-active-hover-bg-color: var(--tt-brand-color-800); + --tt-button-active-hover-bg-color-emphasized: var( + --tt-brand-color-800 + ); //more important active state hover + --tt-button-active-hover-bg-color-subdued: var( + --tt-brand-color-800 + ); //less important active state hover + --tt-button-disabled-bg-color: var(--tt-gray-dark-a-100); + } + + /************************************************** + Primary button text color + **************************************************/ + + /* Light mode */ + --tt-button-default-text-color: var(--white); + --tt-button-hover-text-color: var(--white); + --tt-button-active-text-color: var(--tt-gray-light-a-900); + --tt-button-active-text-color-emphasized: var(--tt-gray-light-a-900); + --tt-button-active-text-color-subdued: var(--tt-gray-light-a-900); + --tt-button-disabled-text-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-text-color: var(--white); + --tt-button-hover-text-color: var(--white); + --tt-button-active-text-color: var(--tt-gray-dark-a-900); + --tt-button-active-text-color-emphasized: var(--tt-gray-dark-a-900); + --tt-button-active-text-color-subdued: var(--tt-gray-dark-a-900); + --tt-button-disabled-text-color: var(--tt-gray-dark-a-300); + } + + /************************************************** + Primary button icon color + **************************************************/ + + /* Light mode */ + --tt-button-default-icon-color: var(--white); + --tt-button-hover-icon-color: var(--white); + --tt-button-active-icon-color: var(--tt-brand-color-600); + --tt-button-active-icon-color-emphasized: var(--tt-brand-color-600); + --tt-button-active-icon-color-subdued: var(--tt-brand-color-600); + --tt-button-disabled-icon-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-icon-color: var(--white); + --tt-button-hover-icon-color: var(--white); + --tt-button-active-icon-color: var(--tt-brand-color-400); + --tt-button-active-icon-color-emphasized: var(--tt-brand-color-400); + --tt-button-active-icon-color-subdued: var(--tt-brand-color-400); + --tt-button-disabled-icon-color: var(--tt-gray-dark-a-300); + } + + /************************************************** + Primary button subicon color + **************************************************/ + + /* Light mode */ + --tt-button-default-icon-sub-color: var(--tt-gray-dark-a-500); + --tt-button-hover-icon-sub-color: var(--tt-gray-dark-a-500); + --tt-button-active-icon-sub-color: var(--tt-gray-light-a-500); + --tt-button-active-icon-sub-color-emphasized: var(--tt-gray-light-a-500); + --tt-button-active-icon-sub-color-subdued: var(--tt-gray-light-a-500); + --tt-button-disabled-icon-sub-color: var(--tt-gray-light-a-100); + + /* Dark mode */ + .dark & { + --tt-button-default-icon-sub-color: var(--tt-gray-dark-a-400); + --tt-button-hover-icon-sub-color: var(--tt-gray-dark-a-500); + --tt-button-active-icon-sub-color: var(--tt-gray-dark-a-300); + --tt-button-active-icon-sub-color-emphasized: var(--tt-gray-dark-a-400); + --tt-button-active-icon-sub-color-subdued: var(--tt-gray-dark-a-300); + --tt-button-disabled-icon-sub-color: var(--tt-gray-dark-a-100); + } + + /************************************************** + Primary button dropdown / arrows color + **************************************************/ + + /* Light mode */ + --tt-button-default-dropdown-arrows-color: var(--white); + --tt-button-hover-dropdown-arrows-color: var(--white); + --tt-button-active-dropdown-arrows-color: var(--tt-gray-light-a-700); + --tt-button-active-dropdown-arrows-color-emphasized: var( + --tt-gray-light-a-700 + ); + --tt-button-active-dropdown-arrows-color-subdued: var( + --tt-gray-light-a-700 + ); + --tt-button-disabled-dropdown-arrows-color: var(--tt-gray-light-a-400); + + /* Dark mode */ + .dark & { + --tt-button-default-dropdown-arrows-color: var(--white); + --tt-button-hover-dropdown-arrows-color: var(--white); + --tt-button-active-dropdown-arrows-color: var(--tt-gray-dark-a-600); + --tt-button-active-dropdown-arrows-color-emphasized: var( + --tt-gray-dark-a-600 + ); + --tt-button-active-dropdown-arrows-color-subdued: var( + --tt-gray-dark-a-600 + ); + --tt-button-disabled-dropdown-arrows-color: var(--tt-gray-dark-a-400); + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-group.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-group.scss new file mode 100644 index 0000000000..59fd2561df --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-group.scss @@ -0,0 +1,22 @@ +.tiptap-button-group { + position: relative; + display: flex; + vertical-align: middle; + + &[data-orientation="vertical"] { + flex-direction: column; + align-items: flex-start; + justify-content: center; + min-width: max-content; + + > .tiptap-button { + width: 100%; + } + } + + &[data-orientation="horizontal"] { + gap: 0.125rem; + flex-direction: row; + align-items: center; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.scss new file mode 100644 index 0000000000..32d1499b3c --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.scss @@ -0,0 +1,314 @@ +.tiptap-button { + font-size: 0.875rem; + font-weight: 500; + font-feature-settings: + "salt" on, + "cv01" on; + line-height: 1.15; + height: 2rem; + min-width: 2rem; + border: none; + padding: 0.5rem; + gap: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--tt-radius-lg, 0.75rem); + transition-property: background, color, opacity; + transition-duration: var(--tt-transition-duration-default); + transition-timing-function: var(--tt-transition-easing-default); + + // focus-visible + &:focus-visible { + outline: none; + } + + &[data-highlighted="true"], + &[data-focus-visible="true"] { + background-color: var(--tt-button-hover-bg-color); + color: var(--tt-button-hover-text-color); + // outline: 2px solid var(--tt-button-active-icon-color); + } + + &[data-weight="small"] { + width: 1.5rem; + min-width: 1.5rem; + padding-right: 0; + padding-left: 0; + } + + /* button size large */ + &[data-size="large"] { + font-size: 0.9375rem; + height: 2.375rem; + min-width: 2.375rem; + padding: 0.625rem; + } + + /* button size small */ + &[data-size="small"] { + font-size: 0.75rem; + line-height: 1.2; + height: 1.5rem; + min-width: 1.5rem; + padding: 0.3125rem; + border-radius: var(--tt-radius-md, 0.5rem); + } + + /* trim / expand text of the button */ + .tiptap-button-text { + padding: 0 0.125rem; + flex-grow: 1; + text-align: left; + line-height: 1.5rem; + } + + &[data-text-trim="on"] { + .tiptap-button-text { + text-overflow: ellipsis; + overflow: hidden; + } + } + + /* global icon settings */ + .tiptap-button-icon, + .tiptap-button-icon-sub, + .tiptap-button-dropdown-arrows, + .tiptap-button-dropdown-small { + flex-shrink: 0; + } + + /* standard icon, what is used */ + .tiptap-button-icon { + width: 1rem; + height: 1rem; + } + + &[data-size="large"] .tiptap-button-icon { + width: 1.125rem; + height: 1.125rem; + } + + &[data-size="small"] .tiptap-button-icon { + width: 0.875rem; + height: 0.875rem; + } + + /* if 2 icons are used and this icon should be more subtle */ + .tiptap-button-icon-sub { + width: 1rem; + height: 1rem; + } + + &[data-size="large"] .tiptap-button-icon-sub { + width: 1.125rem; + height: 1.125rem; + } + + &[data-size="small"] .tiptap-button-icon-sub { + width: 0.875rem; + height: 0.875rem; + } + + /* dropdown menus or arrows that are slightly smaller */ + .tiptap-button-dropdown-arrows { + width: 0.75rem; + height: 0.75rem; + } + + &[data-size="large"] .tiptap-button-dropdown-arrows { + width: 0.875rem; + height: 0.875rem; + } + + &[data-size="small"] .tiptap-button-dropdown-arrows { + width: 0.625rem; + height: 0.625rem; + } + + /* dropdown menu for icon buttons only */ + .tiptap-button-dropdown-small { + width: 0.625rem; + height: 0.625rem; + } + + &[data-size="large"] .tiptap-button-dropdown-small { + width: 0.75rem; + height: 0.75rem; + } + + &[data-size="small"] .tiptap-button-dropdown-small { + width: 0.5rem; + height: 0.5rem; + } + + /* button only has icons */ + &:has(> svg):not(:has(> :not(svg))) { + gap: 0.125rem; + + &[data-size="large"], + &[data-size="small"] { + gap: 0.125rem; + } + } + + /* button only has 2 icons and one of them is dropdown small */ + &:has(> svg:nth-of-type(2)):has(> .tiptap-button-dropdown-small):not( + :has(> svg:nth-of-type(3)) + ):not(:has(> .tiptap-button-text)) { + gap: 0; + padding-right: 0.25rem; + + &[data-size="large"] { + padding-right: 0.375rem; + } + + &[data-size="small"] { + padding-right: 0.25rem; + } + } + + /* Emoji is used in a button */ + .tiptap-button-emoji { + width: 1rem; + display: flex; + justify-content: center; + } + + &[data-size="large"] .tiptap-button-emoji { + width: 1.125rem; + } + + &[data-size="small"] .tiptap-button-emoji { + width: 0.875rem; + } +} + +/* -------------------------------------------- +----------- BUTTON COLOR SETTINGS ------------- +-------------------------------------------- */ + +.tiptap-button { + background-color: var(--tt-button-default-bg-color); + color: var(--tt-button-default-text-color); + + .tiptap-button-icon { + color: var(--tt-button-default-icon-color); + } + + .tiptap-button-icon-sub { + color: var(--tt-button-default-icon-sub-color); + } + + .tiptap-button-dropdown-arrows { + color: var(--tt-button-default-dropdown-arrows-color); + } + + .tiptap-button-dropdown-small { + color: var(--tt-button-default-dropdown-arrows-color); + } + + /* hover state of a button */ + &:hover:not([data-active-item="true"]):not([disabled]), + &[data-active-item="true"]:not([disabled]), + &[data-highlighted]:not([disabled]):not([data-highlighted="false"]) { + background-color: var(--tt-button-hover-bg-color); + color: var(--tt-button-hover-text-color); + + .tiptap-button-icon { + color: var(--tt-button-hover-icon-color); + } + + .tiptap-button-icon-sub { + color: var(--tt-button-hover-icon-sub-color); + } + + .tiptap-button-dropdown-arrows, + .tiptap-button-dropdown-small { + color: var(--tt-button-hover-dropdown-arrows-color); + } + } + + /* Active state of a button */ + &[data-active-state="on"]:not([disabled]), + &[data-state="open"]:not([disabled]) { + background-color: var(--tt-button-active-bg-color); + color: var(--tt-button-active-text-color); + + .tiptap-button-icon { + color: var(--tt-button-active-icon-color); + } + + .tiptap-button-icon-sub { + color: var(--tt-button-active-icon-sub-color); + } + + .tiptap-button-dropdown-arrows, + .tiptap-button-dropdown-small { + color: var(--tt-button-active-dropdown-arrows-color); + } + + &:hover { + background-color: var(--tt-button-active-hover-bg-color); + } + + /* Emphasized */ + &[data-appearance="emphasized"] { + background-color: var(--tt-button-active-bg-color-emphasized); + color: var(--tt-button-active-text-color-emphasized); + + .tiptap-button-icon { + color: var(--tt-button-active-icon-color-emphasized); + } + + .tiptap-button-icon-sub { + color: var(--tt-button-active-icon-sub-color-emphasized); + } + + .tiptap-button-dropdown-arrows, + .tiptap-button-dropdown-small { + color: var(--tt-button-active-dropdown-arrows-color-emphasized); + } + + &:hover { + background-color: var(--tt-button-active-hover-bg-color-emphasized); + } + } + + /* Subdued */ + &[data-appearance="subdued"] { + background-color: var(--tt-button-active-bg-color-subdued); + color: var(--tt-button-active-text-color-subdued); + + .tiptap-button-icon { + color: var(--tt-button-active-icon-color-subdued); + } + + .tiptap-button-icon-sub { + color: var(--tt-button-active-icon-sub-color-subdued); + } + + .tiptap-button-dropdown-arrows, + .tiptap-button-dropdown-small { + color: var(--tt-button-active-dropdown-arrows-color-subdued); + } + + &:hover { + background-color: var(--tt-button-active-hover-bg-color-subdued); + + .tiptap-button-icon { + color: var(--tt-button-active-icon-color-subdued); + } + } + } + } + + &:disabled { + background-color: var(--tt-button-disabled-bg-color); + color: var(--tt-button-disabled-text-color); + + .tiptap-button-icon { + color: var(--tt-button-disabled-icon-color); + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx new file mode 100644 index 0000000000..9dc09a7b76 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx @@ -0,0 +1,114 @@ +import { forwardRef, Fragment, useMemo } from "react" + +// --- Tiptap UI Primitive --- +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/tiptap-ui-primitive/tooltip" + +// --- Lib --- +import { cn, parseShortcutKeys } from "@/lib/tiptap-utils" + +import "@/components/tiptap-ui-primitive/button/button-colors.scss" +import "@/components/tiptap-ui-primitive/button/button-group.scss" +import "@/components/tiptap-ui-primitive/button/button.scss" + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + className?: string + showTooltip?: boolean + tooltip?: React.ReactNode + shortcutKeys?: string +} + +export const ShortcutDisplay: React.FC<{ shortcuts: string[] }> = ({ + shortcuts, +}) => { + if (shortcuts.length === 0) return null + + return ( +
+ {shortcuts.map((key, index) => ( + + {index > 0 && +} + {key} + + ))} +
+ ) +} + +export const Button = forwardRef( + ( + { + className, + children, + tooltip, + showTooltip = true, + shortcutKeys, + "aria-label": ariaLabel, + ...props + }, + ref + ) => { + const shortcuts = useMemo( + () => parseShortcutKeys({ shortcutKeys }), + [shortcutKeys] + ) + + if (!tooltip || !showTooltip) { + return ( + + ) + } + + return ( + + + {children} + + + {tooltip} + + + + ) + } +) + +Button.displayName = "Button" + +export const ButtonGroup = forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + orientation?: "horizontal" | "vertical" + } +>(({ className, children, orientation = "vertical", ...props }, ref) => { + return ( +
+ {children} +
+ ) +}) +ButtonGroup.displayName = "ButtonGroup" + +export default Button diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/index.tsx new file mode 100644 index 0000000000..e93d26f6b0 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/index.tsx @@ -0,0 +1 @@ +export * from "./button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.scss new file mode 100644 index 0000000000..97b757e045 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.scss @@ -0,0 +1,77 @@ +:root { + --tiptap-card-bg-color: var(--white); + --tiptap-card-border-color: var(--tt-gray-light-a-100); + --tiptap-card-group-label-color: var(--tt-gray-light-a-800); +} + +.dark { + --tiptap-card-bg-color: var(--tt-gray-dark-50); + --tiptap-card-border-color: var(--tt-gray-dark-a-100); + --tiptap-card-group-label-color: var(--tt-gray-dark-a-800); +} + +.tiptap-card { + --padding: 0.375rem; + --border-width: 1px; + + border-radius: calc(var(--padding) + var(--tt-radius-lg)); + box-shadow: var(--tt-shadow-elevated-md); + background-color: var(--tiptap-card-bg-color); + border: 1px solid var(--tiptap-card-border-color); + display: flex; + flex-direction: column; + outline: none; + align-items: center; + + position: relative; + min-width: 0; + word-wrap: break-word; + background-clip: border-box; +} + +.tiptap-card-header { + padding: 0.375rem; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + border-bottom: var(--border-width) solid var(--tiptap-card-border-color); +} + +.tiptap-card-body { + padding: 0.375rem; + flex: 1 1 auto; + overflow-y: auto; +} + +.tiptap-card-item-group { + position: relative; + display: flex; + vertical-align: middle; + min-width: max-content; + + &[data-orientation="vertical"] { + flex-direction: column; + justify-content: center; + } + + &[data-orientation="horizontal"] { + gap: 0.25rem; + flex-direction: row; + align-items: center; + } +} + +.tiptap-card-group-label { + padding-top: 0.75rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + padding-bottom: 0.25rem; + line-height: normal; + font-size: 0.75rem; + font-weight: 600; + line-height: normal; + text-transform: capitalize; + color: var(--tiptap-card-group-label-color); +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx new file mode 100644 index 0000000000..1cd8d32441 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx @@ -0,0 +1,79 @@ +"use client" + +import { forwardRef } from "react" +import { cn } from "@/lib/tiptap-utils" +import "@/components/tiptap-ui-primitive/card/card.scss" + +const Card = forwardRef>( + ({ className, ...props }, ref) => { + return
+ } +) +Card.displayName = "Card" + +const CardHeader = forwardRef>( + ({ className, ...props }, ref) => { + return ( +
+ ) + } +) +CardHeader.displayName = "CardHeader" + +const CardBody = forwardRef>( + ({ className, ...props }, ref) => { + return ( +
+ ) + } +) +CardBody.displayName = "CardBody" + +const CardItemGroup = forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + orientation?: "horizontal" | "vertical" + } +>(({ className, orientation = "vertical", ...props }, ref) => { + return ( +
+ ) +}) +CardItemGroup.displayName = "CardItemGroup" + +const CardGroupLabel = forwardRef>( + ({ className, ...props }, ref) => { + return ( +
+ ) + } +) +CardGroupLabel.displayName = "CardGroupLabel" + +const CardFooter = forwardRef>( + ({ className, ...props }, ref) => { + return ( +
+ ) + } +) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardBody, CardItemGroup, CardGroupLabel } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/index.tsx new file mode 100644 index 0000000000..288c75f729 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/index.tsx @@ -0,0 +1 @@ +export * from "./card" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss new file mode 100644 index 0000000000..03b47e8631 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss @@ -0,0 +1,63 @@ +.tiptap-dropdown-menu { + --tt-dropdown-menu-bg-color: var(--white); + --tt-dropdown-menu-border-color: var(--tt-gray-light-a-100); + --tt-dropdown-menu-text-color: var(--tt-gray-light-a-600); + + .dark & { + --tt-dropdown-menu-border-color: var(--tt-gray-dark-a-50); + --tt-dropdown-menu-bg-color: var(--tt-gray-dark-50); + --tt-dropdown-menu-text-color: var(--tt-gray-dark-a-600); + } +} + +/* -------------------------------------------- + --------- DROPDOWN MENU STYLING SETTINGS ----------- + -------------------------------------------- */ +.tiptap-dropdown-menu { + z-index: 50; + outline: none; + transform-origin: var(--radix-dropdown-menu-content-transform-origin); + max-height: var(--radix-dropdown-menu-content-available-height); + + > * { + max-height: var(--radix-dropdown-menu-content-available-height); + } + + /* Animation states */ + &[data-state="open"] { + animation: + fadeIn 150ms cubic-bezier(0.16, 1, 0.3, 1), + zoomIn 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-state="closed"] { + animation: + fadeOut 150ms cubic-bezier(0.16, 1, 0.3, 1), + zoomOut 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + /* Position-based animations */ + &[data-side="top"], + &[data-side="top-start"], + &[data-side="top-end"] { + animation: slideFromBottom 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-side="right"], + &[data-side="right-start"], + &[data-side="right-end"] { + animation: slideFromLeft 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-side="bottom"], + &[data-side="bottom-start"], + &[data-side="bottom-end"] { + animation: slideFromTop 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-side="left"], + &[data-side="left-start"], + &[data-side="left-end"] { + animation: slideFromRight 150ms cubic-bezier(0.16, 1, 0.3, 1); + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx new file mode 100644 index 0000000000..0a980605c5 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx @@ -0,0 +1,96 @@ +import { forwardRef } from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { cn } from "@/lib/tiptap-utils" +import "@/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return +} + +const DropdownMenuTrigger = forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => ) +DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuItem = DropdownMenuPrimitive.Item + +const DropdownMenuSubTrigger = DropdownMenuPrimitive.SubTrigger + +const DropdownMenuSubContent = forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef & { + portal?: boolean | React.ComponentProps + } +>(({ className, portal = true, ...props }, ref) => { + const content = ( + + ) + + return portal ? ( + + {content} + + ) : ( + content + ) +}) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef & { + portal?: boolean + } +>(({ className, sideOffset = 4, portal = false, ...props }, ref) => { + const content = ( + e.preventDefault()} + className={cn("tiptap-dropdown-menu", className)} + {...props} + /> + ) + + return portal ? ( + + {content} + + ) : ( + content + ) +}) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuGroup, + DropdownMenuSub, + DropdownMenuPortal, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/index.tsx new file mode 100644 index 0000000000..c4adeceeee --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/index.tsx @@ -0,0 +1 @@ +export * from "./dropdown-menu" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/index.tsx new file mode 100644 index 0000000000..be91c8ec4b --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/index.tsx @@ -0,0 +1 @@ +export * from "./input" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.scss new file mode 100644 index 0000000000..b9f777cffe --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.scss @@ -0,0 +1,45 @@ +:root { + --tiptap-input-placeholder: var(--tt-gray-light-a-400); +} + +.dark { + --tiptap-input-placeholder: var(--tt-gray-dark-a-400); +} + +.tiptap-input { + display: block; + width: 100%; + height: 2rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + padding: 0.375rem 0.5rem; + border-radius: 0.375rem; + background: none; + appearance: none; + outline: none; + + &::placeholder { + color: var(--tiptap-input-placeholder); + } +} + +.tiptap-input-clamp { + min-width: 12rem; + padding-right: 0; + + text-overflow: ellipsis; + white-space: nowrap; + + &:focus { + text-overflow: clip; + overflow: visible; + } +} + +.tiptap-input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx new file mode 100644 index 0000000000..39203ffc5c --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx @@ -0,0 +1,22 @@ +import { cn } from "@/lib/tiptap-utils" +import "@/components/tiptap-ui-primitive/input/input.scss" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +function InputGroup({ + className, + children, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ {children} +
+ ) +} + +export { Input, InputGroup } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/index.tsx new file mode 100644 index 0000000000..137ef5d362 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/index.tsx @@ -0,0 +1 @@ +export * from "./popover" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.scss new file mode 100644 index 0000000000..07fb0e57bd --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.scss @@ -0,0 +1,63 @@ +.tiptap-popover { + --tt-popover-bg-color: var(--white); + --tt-popover-border-color: var(--tt-gray-light-a-100); + --tt-popover-text-color: var(--tt-gray-light-a-600); + + .dark & { + --tt-popover-border-color: var(--tt-gray-dark-a-50); + --tt-popover-bg-color: var(--tt-gray-dark-50); + --tt-popover-text-color: var(--tt-gray-dark-a-600); + } +} + +/* -------------------------------------------- + --------- POPOVER STYLING SETTINGS ----------- + -------------------------------------------- */ +.tiptap-popover { + z-index: 50; + outline: none; + transform-origin: var(--radix-popover-content-transform-origin); + max-height: var(--radix-popover-content-available-height); + + > * { + max-height: var(--radix-popover-content-available-height); + } + + /* Animation states */ + &[data-state="open"] { + animation: + fadeIn 150ms cubic-bezier(0.16, 1, 0.3, 1), + zoomIn 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-state="closed"] { + animation: + fadeOut 150ms cubic-bezier(0.16, 1, 0.3, 1), + zoomOut 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + /* Position-based animations */ + &[data-side="top"], + &[data-side="top-start"], + &[data-side="top-end"] { + animation: slideFromBottom 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-side="right"], + &[data-side="right-start"], + &[data-side="right-end"] { + animation: slideFromLeft 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-side="bottom"], + &[data-side="bottom-start"], + &[data-side="bottom-end"] { + animation: slideFromTop 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &[data-side="left"], + &[data-side="left-start"], + &[data-side="left-end"] { + animation: slideFromRight 150ms cubic-bezier(0.16, 1, 0.3, 1); + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx new file mode 100644 index 0000000000..9bd52f141d --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx @@ -0,0 +1,35 @@ +import * as PopoverPrimitive from "@radix-ui/react-popover" +import { cn } from "@/lib/tiptap-utils" +import "@/components/tiptap-ui-primitive/popover/popover.scss" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/index.tsx new file mode 100644 index 0000000000..068cfa8369 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/index.tsx @@ -0,0 +1 @@ +export * from "./separator" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.scss new file mode 100644 index 0000000000..78ec9ac6c4 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.scss @@ -0,0 +1,23 @@ +.tiptap-separator { + --tt-link-border-color: var(--tt-gray-light-a-200); + + .dark & { + --tt-link-border-color: var(--tt-gray-dark-a-200); + } +} + +.tiptap-separator { + flex-shrink: 0; + background-color: var(--tt-link-border-color); + + &[data-orientation="horizontal"] { + height: 1px; + width: 100%; + margin: 0.5rem 0; + } + + &[data-orientation="vertical"] { + height: 1.5rem; + width: 1px; + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx new file mode 100644 index 0000000000..e2ce9cd242 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx @@ -0,0 +1,31 @@ +import { forwardRef } from "react" +import "@/components/tiptap-ui-primitive/separator/separator.scss" +import { cn } from "@/lib/tiptap-utils" + +export type Orientation = "horizontal" | "vertical" + +export interface SeparatorProps extends React.HTMLAttributes { + orientation?: Orientation + decorative?: boolean +} + +export const Separator = forwardRef( + ({ decorative, orientation = "vertical", className, ...divProps }, ref) => { + const ariaOrientation = orientation === "vertical" ? orientation : undefined + const semanticProps = decorative + ? { role: "none" } + : { "aria-orientation": ariaOrientation, role: "separator" } + + return ( +
+ ) + } +) + +Separator.displayName = "Separator" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/index.tsx new file mode 100644 index 0000000000..b0789bf135 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/index.tsx @@ -0,0 +1 @@ +export * from "./spacer" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx new file mode 100644 index 0000000000..95e379c21d --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx @@ -0,0 +1,26 @@ +"use client" + +export type SpacerOrientation = "horizontal" | "vertical" + +export interface SpacerProps extends React.HTMLAttributes { + orientation?: SpacerOrientation + size?: string | number +} + +export function Spacer({ + orientation = "horizontal", + size, + style = {}, + ...props +}: SpacerProps) { + const computedStyle = { + ...style, + ...(orientation === "horizontal" && !size && { flex: 1 }), + ...(size && { + width: orientation === "vertical" ? "1px" : size, + height: orientation === "horizontal" ? "1px" : size, + }), + } + + return
+} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/index.tsx new file mode 100644 index 0000000000..94b181962f --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/index.tsx @@ -0,0 +1 @@ +export * from "./toolbar" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.scss new file mode 100644 index 0000000000..3ce1862beb --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.scss @@ -0,0 +1,98 @@ +:root { + --tt-toolbar-height: 2.75rem; + --tt-safe-area-bottom: env(safe-area-inset-bottom, 0px); + --tt-toolbar-bg-color: var(--white); + --tt-toolbar-border-color: var(--tt-gray-light-a-100); +} + +.dark { + --tt-toolbar-bg-color: var(--black); + --tt-toolbar-border-color: var(--tt-gray-dark-a-50); +} + +.tiptap-toolbar { + display: flex; + align-items: center; + gap: 0.25rem; + + &-group { + display: flex; + align-items: center; + gap: 0.125rem; + + &:empty { + display: none; + } + + &:empty + .tiptap-separator, + .tiptap-separator + &:empty { + display: none; + } + } + + &[data-variant="fixed"] { + position: sticky; + top: 0; + z-index: 10; + width: 100%; + min-height: var(--tt-toolbar-height); + background: var(--tt-toolbar-bg-color); + border-bottom: 1px solid var(--tt-toolbar-border-color); + padding: 0 0.5rem; + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + @media (max-width: 480px) { + position: absolute; + top: auto; + height: calc(var(--tt-toolbar-height) + var(--tt-safe-area-bottom)); + border-top: 1px solid var(--tt-toolbar-border-color); + border-bottom: none; + padding: 0 0.5rem var(--tt-safe-area-bottom); + flex-wrap: nowrap; + justify-content: flex-start; + + .tiptap-toolbar-group { + flex: 0 0 auto; + } + } + } + + &[data-variant="floating"] { + --tt-toolbar-padding: 0.125rem; + --tt-toolbar-border-width: 1px; + + padding: 0.188rem; + border-radius: calc( + var(--tt-toolbar-padding) + var(--tt-radius-lg) + + var(--tt-toolbar-border-width) + ); + border: var(--tt-toolbar-border-width) solid var(--tt-toolbar-border-color); + background-color: var(--tt-toolbar-bg-color); + box-shadow: var(--tt-shadow-elevated-md); + outline: none; + overflow: hidden; + + &[data-plain="true"] { + padding: 0; + border-radius: 0; + border: none; + box-shadow: none; + background-color: transparent; + } + + @media screen and (max-width: 480px) { + width: 100%; + border-radius: 0; + border: none; + box-shadow: none; + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx new file mode 100644 index 0000000000..b6de1f46f6 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx @@ -0,0 +1,121 @@ +import { forwardRef, useCallback, useEffect, useRef, useState } from "react" +import { Separator } from "@/components/tiptap-ui-primitive/separator" +import "@/components/tiptap-ui-primitive/toolbar/toolbar.scss" +import { cn } from "@/lib/tiptap-utils" +import { useMenuNavigation } from "@/hooks/use-menu-navigation" +import { useComposedRef } from "@/hooks/use-composed-ref" + +type BaseProps = React.HTMLAttributes + +interface ToolbarProps extends BaseProps { + variant?: "floating" | "fixed" +} + +const useToolbarNavigation = ( + toolbarRef: React.RefObject +) => { + const [items, setItems] = useState([]) + + const collectItems = useCallback(() => { + if (!toolbarRef.current) return [] + return Array.from( + toolbarRef.current.querySelectorAll( + 'button:not([disabled]), [role="button"]:not([disabled]), [tabindex="0"]:not([disabled])' + ) + ) + }, [toolbarRef]) + + useEffect(() => { + const toolbar = toolbarRef.current + if (!toolbar) return + + const updateItems = () => setItems(collectItems()) + + updateItems() + const observer = new MutationObserver(updateItems) + observer.observe(toolbar, { childList: true, subtree: true }) + + return () => observer.disconnect() + }, [collectItems, toolbarRef]) + + const { selectedIndex } = useMenuNavigation({ + containerRef: toolbarRef, + items, + orientation: "horizontal", + onSelect: (el) => el.click(), + autoSelectFirstItem: false, + }) + + useEffect(() => { + const toolbar = toolbarRef.current + if (!toolbar) return + + const handleFocus = (e: FocusEvent) => { + const target = e.target as HTMLElement + if (toolbar.contains(target)) + target.setAttribute("data-focus-visible", "true") + } + + const handleBlur = (e: FocusEvent) => { + const target = e.target as HTMLElement + if (toolbar.contains(target)) target.removeAttribute("data-focus-visible") + } + + toolbar.addEventListener("focus", handleFocus, true) + toolbar.addEventListener("blur", handleBlur, true) + + return () => { + toolbar.removeEventListener("focus", handleFocus, true) + toolbar.removeEventListener("blur", handleBlur, true) + } + }, [toolbarRef]) + + useEffect(() => { + if (selectedIndex !== undefined && items[selectedIndex]) { + items[selectedIndex].focus() + } + }, [selectedIndex, items]) +} + +export const Toolbar = forwardRef( + ({ children, className, variant = "fixed", ...props }, ref) => { + const toolbarRef = useRef(null) + const composedRef = useComposedRef(toolbarRef, ref) + useToolbarNavigation(toolbarRef) + + return ( +
+ {children} +
+ ) + } +) +Toolbar.displayName = "Toolbar" + +export const ToolbarGroup = forwardRef( + ({ children, className, ...props }, ref) => ( +
+ {children} +
+ ) +) +ToolbarGroup.displayName = "ToolbarGroup" + +export const ToolbarSeparator = forwardRef( + ({ ...props }, ref) => ( + + ) +) +ToolbarSeparator.displayName = "ToolbarSeparator" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/index.tsx new file mode 100644 index 0000000000..e12712a782 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/index.tsx @@ -0,0 +1 @@ +export * from "./tooltip" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.scss new file mode 100644 index 0000000000..d717757fa0 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.scss @@ -0,0 +1,43 @@ +.tiptap-tooltip { + --tt-tooltip-bg: var(--tt-gray-light-900); + --tt-tooltip-text: var(--white); + --tt-kbd: var(--tt-gray-dark-a-400); + + .dark & { + --tt-tooltip-bg: var(--white); + --tt-tooltip-text: var(--tt-gray-light-600); + --tt-kbd: var(--tt-gray-light-a-400); + } +} + +.tiptap-tooltip { + z-index: 200; + overflow: hidden; + border-radius: var(--tt-radius-md, 0.375rem); + background-color: var(--tt-tooltip-bg); + padding: 0.375rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + color: var(--tt-tooltip-text); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); + text-align: center; + + kbd { + display: inline-block; + text-align: center; + vertical-align: baseline; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + sans-serif; + text-transform: capitalize; + color: var(--tt-kbd); + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx new file mode 100644 index 0000000000..59ecc44a80 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx @@ -0,0 +1,237 @@ +"use client" + +import { + cloneElement, + createContext, + forwardRef, + isValidElement, + useContext, + useMemo, + useState, + version, +} from "react" +import { + useFloating, + autoUpdate, + offset, + flip, + shift, + useHover, + useFocus, + useDismiss, + useRole, + useInteractions, + useMergeRefs, + FloatingPortal, + type Placement, + type UseFloatingReturn, + type ReferenceType, + FloatingDelayGroup, +} from "@floating-ui/react" +import "@/components/tiptap-ui-primitive/tooltip/tooltip.scss" + +interface TooltipProviderProps { + children: React.ReactNode + initialOpen?: boolean + placement?: Placement + open?: boolean + onOpenChange?: (open: boolean) => void + delay?: number + closeDelay?: number + timeout?: number + useDelayGroup?: boolean +} + +interface TooltipTriggerProps + extends Omit, "ref"> { + asChild?: boolean + children: React.ReactNode +} + +interface TooltipContentProps + extends Omit, "ref"> { + children?: React.ReactNode + portal?: boolean + portalProps?: Omit, "children"> +} + +interface TooltipContextValue extends UseFloatingReturn { + open: boolean + setOpen: (open: boolean) => void + getReferenceProps: ( + userProps?: React.HTMLProps + ) => Record + getFloatingProps: ( + userProps?: React.HTMLProps + ) => Record +} + +function useTooltip({ + initialOpen = false, + placement = "top", + open: controlledOpen, + onOpenChange: setControlledOpen, + delay = 600, + closeDelay = 0, +}: Omit = {}) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen) + + const open = controlledOpen ?? uncontrolledOpen + const setOpen = setControlledOpen ?? setUncontrolledOpen + + const data = useFloating({ + placement, + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware: [ + offset(4), + flip({ + crossAxis: placement.includes("-"), + fallbackAxisSideDirection: "start", + padding: 4, + }), + shift({ padding: 4 }), + ], + }) + + const context = data.context + + const hover = useHover(context, { + mouseOnly: true, + move: false, + restMs: delay, + enabled: controlledOpen == null, + delay: { + close: closeDelay, + }, + }) + const focus = useFocus(context, { + enabled: controlledOpen == null, + }) + const dismiss = useDismiss(context) + const role = useRole(context, { role: "tooltip" }) + + const interactions = useInteractions([hover, focus, dismiss, role]) + + return useMemo( + () => ({ + open, + setOpen, + ...interactions, + ...data, + }), + [open, setOpen, interactions, data] + ) +} + +const TooltipContext = createContext(null) + +function useTooltipContext() { + const context = useContext(TooltipContext) + + if (context == null) { + throw new Error("Tooltip components must be wrapped in ") + } + + return context +} + +export function Tooltip({ children, ...props }: TooltipProviderProps) { + const tooltip = useTooltip(props) + + if (!props.useDelayGroup) { + return ( + + {children} + + ) + } + + return ( + + + {children} + + + ) +} + +export const TooltipTrigger = forwardRef( + function TooltipTrigger({ children, asChild = false, ...props }, propRef) { + const context = useTooltipContext() + const childrenRef = isValidElement(children) + ? parseInt(version, 10) >= 19 + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + (children as { props: { ref?: React.Ref } }).props.ref + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + (children as any).ref + : undefined + const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]) + + if (asChild && isValidElement(children)) { + const dataAttributes = { + "data-tooltip-state": context.open ? "open" : "closed", + } + + return cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...(typeof children.props === "object" ? children.props : {}), + ...dataAttributes, + }) + ) + } + + return ( + + ) + } +) + +export const TooltipContent = forwardRef( + function TooltipContent( + { style, children, portal = true, portalProps = {}, ...props }, + propRef + ) { + const context = useTooltipContext() + const ref = useMergeRefs([context.refs.setFloating, propRef]) + + if (!context.open) return null + + const content = ( +
+ {children} +
+ ) + + if (portal) { + return {content} + } + + return content + } +) + +Tooltip.displayName = "Tooltip" +TooltipTrigger.displayName = "TooltipTrigger" +TooltipContent.displayName = "TooltipContent" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx new file mode 100644 index 0000000000..3052381055 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx @@ -0,0 +1,123 @@ +import { forwardRef, useCallback } from "react" + +// --- Tiptap UI --- +import type { UseBlockquoteConfig } from "@/components/tiptap-ui/blockquote-button" +import { + BLOCKQUOTE_SHORTCUT_KEY, + useBlockquote, +} from "@/components/tiptap-ui/blockquote-button" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +export interface BlockquoteButtonProps + extends Omit, + UseBlockquoteConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function BlockquoteShortcutBadge({ + shortcutKeys = BLOCKQUOTE_SHORTCUT_KEY, +}: { + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for toggling blockquote in a Tiptap editor. + * + * For custom button implementations, use the `useBlockquote` hook instead. + */ +export const BlockquoteButton = forwardRef< + HTMLButtonElement, + BlockquoteButtonProps +>( + ( + { + editor: providedEditor, + text, + hideWhenUnavailable = false, + onToggled, + showShortcut = false, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + canToggle, + isActive, + handleToggle, + label, + shortcutKeys, + Icon, + } = useBlockquote({ + editor, + hideWhenUnavailable, + onToggled, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleToggle() + }, + [handleToggle, onClick] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +BlockquoteButton.displayName = "BlockquoteButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/index.tsx new file mode 100644 index 0000000000..0b46edfc32 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./blockquote-button" +export * from "./use-blockquote" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts new file mode 100644 index 0000000000..79d7a9294c --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts @@ -0,0 +1,246 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import type { Editor } from "@tiptap/react" +import { NodeSelection, TextSelection } from "@tiptap/pm/state" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { BlockquoteIcon } from "@/components/tiptap-icons/blockquote-icon" + +// --- UI Utils --- +import { + findNodePosition, + isNodeInSchema, + isNodeTypeSelected, + isValidPosition, + selectionWithinConvertibleTypes, +} from "@/lib/tiptap-utils" + +export const BLOCKQUOTE_SHORTCUT_KEY = "mod+shift+b" + +/** + * Configuration for the blockquote functionality + */ +export interface UseBlockquoteConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * Whether the button should hide when blockquote is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful toggle. + */ + onToggled?: () => void +} + +/** + * Checks if blockquote can be toggled in the current editor state + */ +export function canToggleBlockquote( + editor: Editor | null, + turnInto: boolean = true +): boolean { + if (!editor || !editor.isEditable) return false + if ( + !isNodeInSchema("blockquote", editor) || + isNodeTypeSelected(editor, ["image"]) + ) + return false + + if (!turnInto) { + return editor.can().toggleWrap("blockquote") + } + + // Ensure selection is in nodes we're allowed to convert + if ( + !selectionWithinConvertibleTypes(editor, [ + "paragraph", + "heading", + "bulletList", + "orderedList", + "taskList", + "blockquote", + "codeBlock", + ]) + ) + return false + + // Either we can wrap in blockquote directly on the selection, + // or we can clear formatting/nodes to arrive at a blockquote. + return editor.can().toggleWrap("blockquote") || editor.can().clearNodes() +} + +/** + * Toggles blockquote formatting for a specific node or the current selection + */ +export function toggleBlockquote(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + if (!canToggleBlockquote(editor)) return false + + try { + const view = editor.view + let state = view.state + let tr = state.tr + + // No selection, find the the cursor position + if (state.selection.empty || state.selection instanceof TextSelection) { + const pos = findNodePosition({ + editor, + node: state.selection.$anchor.node(1), + })?.pos + if (!isValidPosition(pos)) return false + + tr = tr.setSelection(NodeSelection.create(state.doc, pos)) + view.dispatch(tr) + state = view.state + } + + const selection = state.selection + + let chain = editor.chain().focus() + + // Handle NodeSelection + if (selection instanceof NodeSelection) { + const firstChild = selection.node.firstChild?.firstChild + const lastChild = selection.node.lastChild?.lastChild + + const from = firstChild + ? selection.from + firstChild.nodeSize + : selection.from + 1 + + const to = lastChild + ? selection.to - lastChild.nodeSize + : selection.to - 1 + + const resolvedFrom = state.doc.resolve(from) + const resolvedTo = state.doc.resolve(to) + + chain = chain + .setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)) + .clearNodes() + } + + const toggle = editor.isActive("blockquote") + ? chain.lift("blockquote") + : chain.wrapIn("blockquote") + + toggle.run() + + editor.chain().focus().selectTextblockEnd().run() + + return true + } catch { + return false + } +} + +/** + * Determines if the blockquote button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean +}): boolean { + const { editor, hideWhenUnavailable } = props + + if (!editor || !editor.isEditable) return false + if (!isNodeInSchema("blockquote", editor)) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canToggleBlockquote(editor) + } + + return true +} + +/** + * Custom hook that provides blockquote functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage - no params needed + * function MySimpleBlockquoteButton() { + * const { isVisible, handleToggle, isActive } = useBlockquote() + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedBlockquoteButton() { + * const { isVisible, handleToggle, label, isActive } = useBlockquote({ + * editor: myEditor, + * hideWhenUnavailable: true, + * onToggled: () => console.log('Blockquote toggled!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Toggle Blockquote + * + * ) + * } + * ``` + */ +export function useBlockquote(config?: UseBlockquoteConfig) { + const { + editor: providedEditor, + hideWhenUnavailable = false, + onToggled, + } = config || {} + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canToggle = canToggleBlockquote(editor) + const isActive = editor?.isActive("blockquote") || false + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable]) + + const handleToggle = useCallback(() => { + if (!editor) return false + + const success = toggleBlockquote(editor) + if (success) { + onToggled?.() + } + return success + }, [editor, onToggled]) + + return { + isVisible, + isActive, + handleToggle, + canToggle, + label: "Blockquote", + shortcutKeys: BLOCKQUOTE_SHORTCUT_KEY, + Icon: BlockquoteIcon, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx new file mode 100644 index 0000000000..3e4cea9684 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx @@ -0,0 +1,123 @@ +import { forwardRef, useCallback } from "react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Tiptap UI --- +import type { UseCodeBlockConfig } from "@/components/tiptap-ui/code-block-button" +import { + CODE_BLOCK_SHORTCUT_KEY, + useCodeBlock, +} from "@/components/tiptap-ui/code-block-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +export interface CodeBlockButtonProps + extends Omit, + UseCodeBlockConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function CodeBlockShortcutBadge({ + shortcutKeys = CODE_BLOCK_SHORTCUT_KEY, +}: { + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for toggling code block in a Tiptap editor. + * + * For custom button implementations, use the `useCodeBlock` hook instead. + */ +export const CodeBlockButton = forwardRef< + HTMLButtonElement, + CodeBlockButtonProps +>( + ( + { + editor: providedEditor, + text, + hideWhenUnavailable = false, + onToggled, + showShortcut = false, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + canToggle, + isActive, + handleToggle, + label, + shortcutKeys, + Icon, + } = useCodeBlock({ + editor, + hideWhenUnavailable, + onToggled, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleToggle() + }, + [handleToggle, onClick] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +CodeBlockButton.displayName = "CodeBlockButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/index.tsx new file mode 100644 index 0000000000..77d541f9c4 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./code-block-button" +export * from "./use-code-block" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts new file mode 100644 index 0000000000..0475eb72bb --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts @@ -0,0 +1,256 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { type Editor } from "@tiptap/react" +import { NodeSelection, TextSelection } from "@tiptap/pm/state" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { + findNodePosition, + isNodeInSchema, + isNodeTypeSelected, + isValidPosition, + selectionWithinConvertibleTypes, +} from "@/lib/tiptap-utils" + +// --- Icons --- +import { CodeBlockIcon } from "@/components/tiptap-icons/code-block-icon" + +export const CODE_BLOCK_SHORTCUT_KEY = "mod+alt+c" + +/** + * Configuration for the code block functionality + */ +export interface UseCodeBlockConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * Whether the button should hide when code block is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful code block toggle. + */ + onToggled?: () => void +} + +/** + * Checks if code block can be toggled in the current editor state + */ +export function canToggle( + editor: Editor | null, + turnInto: boolean = true +): boolean { + if (!editor || !editor.isEditable) return false + if ( + !isNodeInSchema("codeBlock", editor) || + isNodeTypeSelected(editor, ["image"]) + ) + return false + + if (!turnInto) { + return editor.can().toggleNode("codeBlock", "paragraph") + } + + // Ensure selection is in nodes we're allowed to convert + if ( + !selectionWithinConvertibleTypes(editor, [ + "paragraph", + "heading", + "bulletList", + "orderedList", + "taskList", + "blockquote", + "codeBlock", + ]) + ) + return false + + // Either we can toggle code block directly on the selection, + // or we can clear formatting/nodes to arrive at a code block. + return ( + editor.can().toggleNode("codeBlock", "paragraph") || + editor.can().clearNodes() + ) +} + +/** + * Toggles code block in the editor + */ +export function toggleCodeBlock(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + if (!canToggle(editor)) return false + + try { + const view = editor.view + let state = view.state + let tr = state.tr + + // No selection, find the the cursor position + if (state.selection.empty || state.selection instanceof TextSelection) { + const pos = findNodePosition({ + editor, + node: state.selection.$anchor.node(1), + })?.pos + if (!isValidPosition(pos)) return false + + tr = tr.setSelection(NodeSelection.create(state.doc, pos)) + view.dispatch(tr) + state = view.state + } + + const selection = state.selection + + let chain = editor.chain().focus() + + // Handle NodeSelection + if (selection instanceof NodeSelection) { + const firstChild = selection.node.firstChild?.firstChild + const lastChild = selection.node.lastChild?.lastChild + + const from = firstChild + ? selection.from + firstChild.nodeSize + : selection.from + 1 + + const to = lastChild + ? selection.to - lastChild.nodeSize + : selection.to - 1 + + const resolvedFrom = state.doc.resolve(from) + const resolvedTo = state.doc.resolve(to) + + chain = chain + .setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)) + .clearNodes() + } + + const toggle = editor.isActive("codeBlock") + ? chain.setNode("paragraph") + : chain.toggleNode("codeBlock", "paragraph") + + toggle.run() + + editor.chain().focus().selectTextblockEnd().run() + + return true + } catch { + return false + } +} + +/** + * Determines if the code block button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean +}): boolean { + const { editor, hideWhenUnavailable } = props + + if (!editor || !editor.isEditable) return false + if (!isNodeInSchema("codeBlock", editor)) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canToggle(editor) + } + + return true +} + +/** + * Custom hook that provides code block functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage - no params needed + * function MySimpleCodeBlockButton() { + * const { isVisible, isActive, handleToggle } = useCodeBlock() + * + * if (!isVisible) return null + * + * return ( + * + * ) + * } + * + * // Advanced usage with configuration + * function MyAdvancedCodeBlockButton() { + * const { isVisible, isActive, handleToggle, label } = useCodeBlock({ + * editor: myEditor, + * hideWhenUnavailable: true, + * onToggled: (isActive) => console.log('Code block toggled:', isActive) + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Toggle Code Block + * + * ) + * } + * ``` + */ +export function useCodeBlock(config?: UseCodeBlockConfig) { + const { + editor: providedEditor, + hideWhenUnavailable = false, + onToggled, + } = config || {} + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canToggleState = canToggle(editor) + const isActive = editor?.isActive("codeBlock") || false + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable]) + + const handleToggle = useCallback(() => { + if (!editor) return false + + const success = toggleCodeBlock(editor) + if (success) { + onToggled?.() + } + return success + }, [editor, onToggled]) + + return { + isVisible, + isActive, + handleToggle, + canToggle: canToggleState, + label: "Code Block", + shortcutKeys: CODE_BLOCK_SHORTCUT_KEY, + Icon: CodeBlockIcon, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.scss new file mode 100644 index 0000000000..2c6f387732 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.scss @@ -0,0 +1,49 @@ +.tiptap-button-highlight { + position: relative; + width: 1.25rem; + height: 1.25rem; + margin: 0 -0.175rem; + border-radius: var(--tt-radius-xl); + background-color: var(--highlight-color); + transition: transform 0.2s ease; + + &::after { + content: ""; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + box-sizing: border-box; + border: 1px solid var(--highlight-color); + filter: brightness(95%); + mix-blend-mode: multiply; + + .dark & { + filter: brightness(140%); + mix-blend-mode: lighten; + } + } +} + +.tiptap-button { + &[data-active-state="on"] { + .tiptap-button-highlight { + &::after { + filter: brightness(80%); + } + } + } + + .dark & { + &[data-active-state="on"] { + .tiptap-button-highlight { + &::after { + // Andere Eigenschaft für .dark Kontext + filter: brightness(180%); + } + } + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx new file mode 100644 index 0000000000..8fe957cbc2 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx @@ -0,0 +1,169 @@ +import { forwardRef, useCallback, useMemo } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Tiptap UI --- +import type { UseColorHighlightConfig } from "@/components/tiptap-ui/color-highlight-button" +import { + COLOR_HIGHLIGHT_SHORTCUT_KEY, + useColorHighlight, +} from "@/components/tiptap-ui/color-highlight-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +// --- Styles --- +import "@/components/tiptap-ui/color-highlight-button/color-highlight-button.scss" + +export interface ColorHighlightButtonProps + extends Omit, + UseColorHighlightConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function ColorHighlightShortcutBadge({ + shortcutKeys = COLOR_HIGHLIGHT_SHORTCUT_KEY, +}: { + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for applying color highlights in a Tiptap editor. + * + * Supports two highlighting modes: + * - "mark": Uses the highlight mark extension (default) + * - "node": Uses the node background extension + * + * For custom button implementations, use the `useColorHighlight` hook instead. + * + * @example + * ```tsx + * // Mark-based highlighting (default) + * + * + * // Node-based background coloring + * + * + * // With custom callback + * console.log(`Applied ${color} in ${mode} mode`)} + * /> + * ``` + */ +export const ColorHighlightButton = forwardRef< + HTMLButtonElement, + ColorHighlightButtonProps +>( + ( + { + editor: providedEditor, + highlightColor, + text, + hideWhenUnavailable = false, + mode = "mark", + onApplied, + showShortcut = false, + onClick, + children, + style, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + canColorHighlight, + isActive, + handleColorHighlight, + label, + shortcutKeys, + } = useColorHighlight({ + editor, + highlightColor, + label: text || `Toggle highlight (${highlightColor})`, + hideWhenUnavailable, + mode, + onApplied, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleColorHighlight() + }, + [handleColorHighlight, onClick] + ) + + const buttonStyle = useMemo( + () => + ({ + ...style, + "--highlight-color": highlightColor, + }) as React.CSSProperties, + [highlightColor, style] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +ColorHighlightButton.displayName = "ColorHighlightButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/index.tsx new file mode 100644 index 0000000000..c517648273 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./color-highlight-button" +export * from "./use-color-highlight" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts new file mode 100644 index 0000000000..b71215e0b8 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts @@ -0,0 +1,339 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { type Editor } from "@tiptap/react" +import { useHotkeys } from "react-hotkeys-hook" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useIsMobile } from "@/hooks/use-mobile" + +// --- Lib --- +import { + isMarkInSchema, + isNodeTypeSelected, + isExtensionAvailable, +} from "@/lib/tiptap-utils" + +// --- Icons --- +import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" + +export const COLOR_HIGHLIGHT_SHORTCUT_KEY = "mod+shift+h" +export const HIGHLIGHT_COLORS = [ + { + label: "Default background", + value: "var(--tt-bg-color)", + border: "var(--tt-bg-color-contrast)", + }, + { + label: "Gray background", + value: "var(--tt-color-highlight-gray)", + border: "var(--tt-color-highlight-gray-contrast)", + }, + { + label: "Brown background", + value: "var(--tt-color-highlight-brown)", + border: "var(--tt-color-highlight-brown-contrast)", + }, + { + label: "Orange background", + value: "var(--tt-color-highlight-orange)", + border: "var(--tt-color-highlight-orange-contrast)", + }, + { + label: "Yellow background", + value: "var(--tt-color-highlight-yellow)", + border: "var(--tt-color-highlight-yellow-contrast)", + }, + { + label: "Green background", + value: "var(--tt-color-highlight-green)", + border: "var(--tt-color-highlight-green-contrast)", + }, + { + label: "Blue background", + value: "var(--tt-color-highlight-blue)", + border: "var(--tt-color-highlight-blue-contrast)", + }, + { + label: "Purple background", + value: "var(--tt-color-highlight-purple)", + border: "var(--tt-color-highlight-purple-contrast)", + }, + { + label: "Pink background", + value: "var(--tt-color-highlight-pink)", + border: "var(--tt-color-highlight-pink-contrast)", + }, + { + label: "Red background", + value: "var(--tt-color-highlight-red)", + border: "var(--tt-color-highlight-red-contrast)", + }, +] +export type HighlightColor = (typeof HIGHLIGHT_COLORS)[number] + +export type HighlightMode = "mark" | "node" + +/** + * Configuration for the color highlight functionality + */ +export interface UseColorHighlightConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The color to apply when toggling the highlight. + */ + highlightColor?: string + /** + * Optional label to display alongside the icon. + */ + label?: string + /** + * Whether the button should hide when the mark is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * The highlighting mode to use. + * - "mark": Uses the highlight mark extension (default) + * - "node": Uses the node background extension + * @default "mark" + */ + mode?: HighlightMode + /** + * Called when the highlight is applied. + */ + onApplied?: ({ + color, + label, + mode, + }: { + color: string + label: string + mode: HighlightMode + }) => void +} + +export function pickHighlightColorsByValue(values: string[]) { + const colorMap = new Map( + HIGHLIGHT_COLORS.map((color) => [color.value, color]) + ) + return values + .map((value) => colorMap.get(value)) + .filter((color): color is (typeof HIGHLIGHT_COLORS)[number] => !!color) +} + +/** + * Checks if highlight can be applied based on the mode and current editor state + */ +export function canColorHighlight( + editor: Editor | null, + mode: HighlightMode = "mark" +): boolean { + if (!editor || !editor.isEditable) return false + + if (mode === "mark") { + if ( + !isMarkInSchema("highlight", editor) || + isNodeTypeSelected(editor, ["image"]) + ) + return false + + return editor.can().setMark("highlight") + } else { + if (!isExtensionAvailable(editor, ["nodeBackground"])) return false + + try { + return editor.can().toggleNodeBackgroundColor("test") + } catch { + return false + } + } +} + +/** + * Checks if highlight is currently active + */ +export function isColorHighlightActive( + editor: Editor | null, + highlightColor?: string, + mode: HighlightMode = "mark" +): boolean { + if (!editor || !editor.isEditable) return false + + if (mode === "mark") { + return highlightColor + ? editor.isActive("highlight", { color: highlightColor }) + : editor.isActive("highlight") + } else { + if (!highlightColor) return false + + try { + const { state } = editor + const { selection } = state + + const $pos = selection.$anchor + for (let depth = $pos.depth; depth >= 0; depth--) { + const node = $pos.node(depth) + if (node && node.attrs?.backgroundColor === highlightColor) { + return true + } + } + return false + } catch { + return false + } + } +} + +/** + * Removes highlight based on the mode + */ +export function removeHighlight( + editor: Editor | null, + mode: HighlightMode = "mark" +): boolean { + if (!editor || !editor.isEditable) return false + if (!canColorHighlight(editor, mode)) return false + + if (mode === "mark") { + return editor.chain().focus().unsetMark("highlight").run() + } else { + return editor.chain().focus().unsetNodeBackgroundColor().run() + } +} + +/** + * Determines if the highlight button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean + mode: HighlightMode +}): boolean { + const { editor, hideWhenUnavailable, mode } = props + + if (!editor || !editor.isEditable) return false + + if (mode === "mark") { + if (!isMarkInSchema("highlight", editor)) return false + } else { + if (!isExtensionAvailable(editor, ["nodeBackground"])) return false + } + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canColorHighlight(editor, mode) + } + + return true +} + +export function useColorHighlight(config: UseColorHighlightConfig) { + const { + editor: providedEditor, + label, + highlightColor, + hideWhenUnavailable = false, + mode = "mark", + onApplied, + } = config + + const { editor } = useTiptapEditor(providedEditor) + const isMobile = useIsMobile() + const [isVisible, setIsVisible] = useState(true) + const canColorHighlightState = canColorHighlight(editor, mode) + const isActive = isColorHighlightActive(editor, highlightColor, mode) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, mode })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable, mode]) + + const handleColorHighlight = useCallback(() => { + if (!editor || !canColorHighlightState || !highlightColor || !label) + return false + + if (mode === "mark") { + if (editor.state.storedMarks) { + const highlightMarkType = editor.schema.marks.highlight + if (highlightMarkType) { + editor.view.dispatch( + editor.state.tr.removeStoredMark(highlightMarkType) + ) + } + } + + setTimeout(() => { + const success = editor + .chain() + .focus() + .toggleMark("highlight", { color: highlightColor }) + .run() + if (success) { + onApplied?.({ color: highlightColor, label, mode }) + } + return success + }, 0) + + return true + } else { + const success = editor + .chain() + .focus() + .toggleNodeBackgroundColor(highlightColor) + .run() + + if (success) { + onApplied?.({ color: highlightColor, label, mode }) + } + return success + } + }, [canColorHighlightState, highlightColor, editor, label, onApplied, mode]) + + const handleRemoveHighlight = useCallback(() => { + const success = removeHighlight(editor, mode) + if (success) { + onApplied?.({ color: "", label: "Remove highlight", mode }) + } + return success + }, [editor, onApplied, mode]) + + useHotkeys( + COLOR_HIGHLIGHT_SHORTCUT_KEY, + (event) => { + event.preventDefault() + handleColorHighlight() + }, + { + enabled: isVisible && canColorHighlightState, + enableOnContentEditable: !isMobile, + enableOnFormTags: true, + } + ) + + return { + isVisible, + isActive, + handleColorHighlight, + handleRemoveHighlight, + canColorHighlight: canColorHighlightState, + label: label || `Highlight`, + shortcutKeys: COLOR_HIGHLIGHT_SHORTCUT_KEY, + Icon: HighlighterIcon, + mode, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx new file mode 100644 index 0000000000..3ede63f606 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx @@ -0,0 +1,209 @@ +import { forwardRef, useMemo, useRef, useState } from "react" +import { type Editor } from "@tiptap/react" + +// --- Hooks --- +import { useMenuNavigation } from "@/hooks/use-menu-navigation" +import { useIsMobile } from "@/hooks/use-mobile" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { BanIcon } from "@/components/tiptap-icons/ban-icon" +import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/tiptap-ui-primitive/popover" +import { Separator } from "@/components/tiptap-ui-primitive/separator" +import { + Card, + CardBody, + CardItemGroup, +} from "@/components/tiptap-ui-primitive/card" + +// --- Tiptap UI --- +import type { + HighlightColor, + UseColorHighlightConfig, +} from "@/components/tiptap-ui/color-highlight-button" +import { + ColorHighlightButton, + pickHighlightColorsByValue, + useColorHighlight, +} from "@/components/tiptap-ui/color-highlight-button" + +export interface ColorHighlightPopoverContentProps { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * Optional colors to use in the highlight popover. + * If not provided, defaults to a predefined set of colors. + */ + colors?: HighlightColor[] +} + +export interface ColorHighlightPopoverProps + extends Omit, + Pick< + UseColorHighlightConfig, + "editor" | "hideWhenUnavailable" | "onApplied" + > { + /** + * Optional colors to use in the highlight popover. + * If not provided, defaults to a predefined set of colors. + */ + colors?: HighlightColor[] +} + +export const ColorHighlightPopoverButton = forwardRef< + HTMLButtonElement, + ButtonProps +>(({ className, children, ...props }, ref) => ( + +)) + +ColorHighlightPopoverButton.displayName = "ColorHighlightPopoverButton" + +export function ColorHighlightPopoverContent({ + editor, + colors = pickHighlightColorsByValue([ + "var(--tt-color-highlight-green)", + "var(--tt-color-highlight-blue)", + "var(--tt-color-highlight-red)", + "var(--tt-color-highlight-purple)", + "var(--tt-color-highlight-yellow)", + ]), +}: ColorHighlightPopoverContentProps) { + const { handleRemoveHighlight } = useColorHighlight({ editor }) + const isMobile = useIsMobile() + const containerRef = useRef(null) + + const menuItems = useMemo( + () => [...colors, { label: "Remove highlight", value: "none" }], + [colors] + ) + + const { selectedIndex } = useMenuNavigation({ + containerRef, + items: menuItems, + orientation: "both", + onSelect: (item) => { + if (!containerRef.current) return false + const highlightedElement = containerRef.current.querySelector( + '[data-highlighted="true"]' + ) as HTMLElement + if (highlightedElement) highlightedElement.click() + if (item.value === "none") handleRemoveHighlight() + return true + }, + autoSelectFirstItem: false, + }) + + return ( + + + + + {colors.map((color, index) => ( + + ))} + + + + + + + + + ) +} + +export function ColorHighlightPopover({ + editor: providedEditor, + colors = pickHighlightColorsByValue([ + "var(--tt-color-highlight-green)", + "var(--tt-color-highlight-blue)", + "var(--tt-color-highlight-red)", + "var(--tt-color-highlight-purple)", + "var(--tt-color-highlight-yellow)", + ]), + hideWhenUnavailable = false, + onApplied, + ...props +}: ColorHighlightPopoverProps) { + const { editor } = useTiptapEditor(providedEditor) + const [isOpen, setIsOpen] = useState(false) + const { isVisible, canColorHighlight, isActive, label, Icon } = + useColorHighlight({ + editor, + hideWhenUnavailable, + onApplied, + }) + + if (!isVisible) return null + + return ( + + + + + + + + + + + ) +} + +export default ColorHighlightPopover diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/index.tsx new file mode 100644 index 0000000000..626b81f6e7 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/index.tsx @@ -0,0 +1 @@ +export * from "./color-highlight-popover" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx new file mode 100644 index 0000000000..4d32634d69 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx @@ -0,0 +1,125 @@ +import { forwardRef, useCallback } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Tiptap UI --- +import type { + Level, + UseHeadingConfig, +} from "@/components/tiptap-ui/heading-button" +import { + HEADING_SHORTCUT_KEYS, + useHeading, +} from "@/components/tiptap-ui/heading-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +export interface HeadingButtonProps + extends Omit, + UseHeadingConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function HeadingShortcutBadge({ + level, + shortcutKeys = HEADING_SHORTCUT_KEYS[level], +}: { + level: Level + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for toggling heading in a Tiptap editor. + * + * For custom button implementations, use the `useHeading` hook instead. + */ +export const HeadingButton = forwardRef( + ( + { + editor: providedEditor, + level, + text, + hideWhenUnavailable = false, + onToggled, + showShortcut = false, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + canToggle, + isActive, + handleToggle, + label, + Icon, + shortcutKeys, + } = useHeading({ + editor, + level, + hideWhenUnavailable, + onToggled, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleToggle() + }, + [handleToggle, onClick] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +HeadingButton.displayName = "HeadingButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/index.tsx new file mode 100644 index 0000000000..009a7005b6 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./heading-button" +export * from "./use-heading" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts new file mode 100644 index 0000000000..0157f988e7 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts @@ -0,0 +1,321 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { type Editor } from "@tiptap/react" +import { NodeSelection, TextSelection } from "@tiptap/pm/state" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { + findNodePosition, + isNodeInSchema, + isNodeTypeSelected, + isValidPosition, + selectionWithinConvertibleTypes, +} from "@/lib/tiptap-utils" + +// --- Icons --- +import { HeadingOneIcon } from "@/components/tiptap-icons/heading-one-icon" +import { HeadingTwoIcon } from "@/components/tiptap-icons/heading-two-icon" +import { HeadingThreeIcon } from "@/components/tiptap-icons/heading-three-icon" +import { HeadingFourIcon } from "@/components/tiptap-icons/heading-four-icon" +import { HeadingFiveIcon } from "@/components/tiptap-icons/heading-five-icon" +import { HeadingSixIcon } from "@/components/tiptap-icons/heading-six-icon" + +export type Level = 1 | 2 | 3 | 4 | 5 | 6 + +/** + * Configuration for the heading functionality + */ +export interface UseHeadingConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The heading level. + */ + level: Level + /** + * Whether the button should hide when heading is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful heading toggle. + */ + onToggled?: () => void +} + +export const headingIcons = { + 1: HeadingOneIcon, + 2: HeadingTwoIcon, + 3: HeadingThreeIcon, + 4: HeadingFourIcon, + 5: HeadingFiveIcon, + 6: HeadingSixIcon, +} + +export const HEADING_SHORTCUT_KEYS: Record = { + 1: "ctrl+alt+1", + 2: "ctrl+alt+2", + 3: "ctrl+alt+3", + 4: "ctrl+alt+4", + 5: "ctrl+alt+5", + 6: "ctrl+alt+6", +} + +/** + * Checks if heading can be toggled in the current editor state + */ +export function canToggle( + editor: Editor | null, + level?: Level, + turnInto: boolean = true +): boolean { + if (!editor || !editor.isEditable) return false + if ( + !isNodeInSchema("heading", editor) || + isNodeTypeSelected(editor, ["image"]) + ) + return false + + if (!turnInto) { + return level + ? editor.can().setNode("heading", { level }) + : editor.can().setNode("heading") + } + + // Ensure selection is in nodes we're allowed to convert + if ( + !selectionWithinConvertibleTypes(editor, [ + "paragraph", + "heading", + "bulletList", + "orderedList", + "taskList", + "blockquote", + "codeBlock", + ]) + ) + return false + + // Either we can set heading directly on the selection, + // or we can clear formatting/nodes to arrive at a heading. + return level + ? editor.can().setNode("heading", { level }) || editor.can().clearNodes() + : editor.can().setNode("heading") || editor.can().clearNodes() +} + +/** + * Checks if heading is currently active + */ +export function isHeadingActive( + editor: Editor | null, + level?: Level | Level[] +): boolean { + if (!editor || !editor.isEditable) return false + + if (Array.isArray(level)) { + return level.some((l) => editor.isActive("heading", { level: l })) + } + + return level + ? editor.isActive("heading", { level }) + : editor.isActive("heading") +} + +/** + * Toggles heading in the editor + */ +export function toggleHeading( + editor: Editor | null, + level: Level | Level[] +): boolean { + if (!editor || !editor.isEditable) return false + + const levels = Array.isArray(level) ? level : [level] + const toggleLevel = levels.find((l) => canToggle(editor, l)) + + if (!toggleLevel) return false + + try { + const view = editor.view + let state = view.state + let tr = state.tr + + // No selection, find the cursor position + if (state.selection.empty || state.selection instanceof TextSelection) { + const pos = findNodePosition({ + editor, + node: state.selection.$anchor.node(1), + })?.pos + if (!isValidPosition(pos)) return false + + tr = tr.setSelection(NodeSelection.create(state.doc, pos)) + view.dispatch(tr) + state = view.state + } + + const selection = state.selection + let chain = editor.chain().focus() + + // Handle NodeSelection + if (selection instanceof NodeSelection) { + const firstChild = selection.node.firstChild?.firstChild + const lastChild = selection.node.lastChild?.lastChild + + const from = firstChild + ? selection.from + firstChild.nodeSize + : selection.from + 1 + + const to = lastChild + ? selection.to - lastChild.nodeSize + : selection.to - 1 + + const resolvedFrom = state.doc.resolve(from) + const resolvedTo = state.doc.resolve(to) + + chain = chain + .setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)) + .clearNodes() + } + + const isActive = levels.some((l) => + editor.isActive("heading", { level: l }) + ) + + const toggle = isActive + ? chain.setNode("paragraph") + : chain.setNode("heading", { level: toggleLevel }) + + toggle.run() + + editor.chain().focus().selectTextblockEnd().run() + + return true + } catch { + return false + } +} + +/** + * Determines if the heading button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + level?: Level | Level[] + hideWhenUnavailable: boolean +}): boolean { + const { editor, level, hideWhenUnavailable } = props + + if (!editor || !editor.isEditable) return false + if (!isNodeInSchema("heading", editor)) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + if (Array.isArray(level)) { + return level.some((l) => canToggle(editor, l)) + } + return canToggle(editor, level) + } + + return true +} + +/** + * Custom hook that provides heading functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MySimpleHeadingButton() { + * const { isVisible, isActive, handleToggle, Icon } = useHeading({ level: 1 }) + * + * if (!isVisible) return null + * + * return ( + * + * ) + * } + * + * // Advanced usage with configuration + * function MyAdvancedHeadingButton() { + * const { isVisible, isActive, handleToggle, label, Icon } = useHeading({ + * level: 2, + * editor: myEditor, + * hideWhenUnavailable: true, + * onToggled: (isActive) => console.log('Heading toggled:', isActive) + * }) + * + * if (!isVisible) return null + * + * return ( + * + * + * Toggle Heading 2 + * + * ) + * } + * ``` + */ +export function useHeading(config: UseHeadingConfig) { + const { + editor: providedEditor, + level, + hideWhenUnavailable = false, + onToggled, + } = config + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canToggleState = canToggle(editor, level) + const isActive = isHeadingActive(editor, level) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, level, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, level, hideWhenUnavailable]) + + const handleToggle = useCallback(() => { + if (!editor) return false + + const success = toggleHeading(editor, level) + if (success) { + onToggled?.() + } + return success + }, [editor, level, onToggled]) + + return { + isVisible, + isActive, + handleToggle, + canToggle: canToggleState, + label: `Heading ${level}`, + shortcutKeys: HEADING_SHORTCUT_KEYS[level], + Icon: headingIcons[level], + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx new file mode 100644 index 0000000000..762bd55d02 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx @@ -0,0 +1,127 @@ +import { forwardRef, useCallback, useState } from "react" + +// --- Icons --- +import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Tiptap UI --- +import { HeadingButton } from "@/components/tiptap-ui/heading-button" +import type { UseHeadingDropdownMenuConfig } from "@/components/tiptap-ui/heading-dropdown-menu" +import { useHeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/tiptap-ui-primitive/dropdown-menu" +import { Card, CardBody } from "@/components/tiptap-ui-primitive/card" + +export interface HeadingDropdownMenuProps + extends Omit, + UseHeadingDropdownMenuConfig { + /** + * Whether to render the dropdown menu in a portal + * @default false + */ + portal?: boolean + /** + * Callback for when the dropdown opens or closes + */ + onOpenChange?: (isOpen: boolean) => void +} + +/** + * Dropdown menu component for selecting heading levels in a Tiptap editor. + * + * For custom dropdown implementations, use the `useHeadingDropdownMenu` hook instead. + */ +export const HeadingDropdownMenu = forwardRef< + HTMLButtonElement, + HeadingDropdownMenuProps +>( + ( + { + editor: providedEditor, + levels = [1, 2, 3, 4, 5, 6], + hideWhenUnavailable = false, + portal = false, + onOpenChange, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const [isOpen, setIsOpen] = useState(false) + const { isVisible, isActive, canToggle, Icon } = useHeadingDropdownMenu({ + editor, + levels, + hideWhenUnavailable, + }) + + const handleOpenChange = useCallback( + (open: boolean) => { + if (!editor || !canToggle) return + setIsOpen(open) + onOpenChange?.(open) + }, + [canToggle, editor, onOpenChange] + ) + + if (!isVisible) { + return null + } + + return ( + + + + + + + + + + {levels.map((level) => ( + + + + ))} + + + + + + ) + } +) + +HeadingDropdownMenu.displayName = "HeadingDropdownMenu" + +export default HeadingDropdownMenu diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/index.tsx new file mode 100644 index 0000000000..33b9679900 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/index.tsx @@ -0,0 +1,2 @@ +export * from "./heading-dropdown-menu" +export * from "./use-heading-dropdown-menu" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts new file mode 100644 index 0000000000..b25ce63785 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts @@ -0,0 +1,132 @@ +"use client" + +import { useEffect, useState } from "react" +import type { Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { HeadingIcon } from "@/components/tiptap-icons/heading-icon" + +// --- Tiptap UI --- +import { + headingIcons, + type Level, + isHeadingActive, + canToggle, + shouldShowButton, +} from "@/components/tiptap-ui/heading-button" + +/** + * Configuration for the heading dropdown menu functionality + */ +export interface UseHeadingDropdownMenuConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * Available heading levels to show in the dropdown + * @default [1, 2, 3, 4, 5, 6] + */ + levels?: Level[] + /** + * Whether the dropdown should hide when headings are not available. + * @default false + */ + hideWhenUnavailable?: boolean +} + +/** + * Gets the currently active heading level from the available levels + */ +export function getActiveHeadingLevel( + editor: Editor | null, + levels: Level[] = [1, 2, 3, 4, 5, 6] +): Level | undefined { + if (!editor || !editor.isEditable) return undefined + return levels.find((level) => isHeadingActive(editor, level)) +} + +/** + * Custom hook that provides heading dropdown menu functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MyHeadingDropdown() { + * const { + * isVisible, + * activeLevel, + * isAnyHeadingActive, + * canToggle, + * levels, + * } = useHeadingDropdownMenu() + * + * if (!isVisible) return null + * + * return ( + * + * // dropdown content + * + * ) + * } + * + * // Advanced usage with configuration + * function MyAdvancedHeadingDropdown() { + * const { + * isVisible, + * activeLevel, + * } = useHeadingDropdownMenu({ + * editor: myEditor, + * levels: [1, 2, 3], + * hideWhenUnavailable: true, + * }) + * + * // component implementation + * } + * ``` + */ +export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) { + const { + editor: providedEditor, + levels = [1, 2, 3, 4, 5, 6], + hideWhenUnavailable = false, + } = config || {} + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + + const activeLevel = getActiveHeadingLevel(editor, levels) + const isActive = isHeadingActive(editor) + const canToggleState = canToggle(editor) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible( + shouldShowButton({ editor, hideWhenUnavailable, level: levels }) + ) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable, levels]) + + return { + isVisible, + activeLevel, + isActive, + canToggle: canToggleState, + levels, + label: "Heading", + Icon: activeLevel ? headingIcons[activeLevel] : HeadingIcon, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx new file mode 100644 index 0000000000..6ec8dd75e3 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx @@ -0,0 +1,131 @@ +import { forwardRef, useCallback } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Tiptap UI --- +import type { UseImageUploadConfig } from "@/components/tiptap-ui/image-upload-button" +import { + IMAGE_UPLOAD_SHORTCUT_KEY, + useImageUpload, +} from "@/components/tiptap-ui/image-upload-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +type IconProps = React.SVGProps +type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement + +export interface ImageUploadButtonProps + extends Omit, + UseImageUploadConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean + /** + * Optional custom icon component to render instead of the default. + */ + icon?: React.MemoExoticComponent | React.FC +} + +export function ImageShortcutBadge({ + shortcutKeys = IMAGE_UPLOAD_SHORTCUT_KEY, +}: { + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for uploading/inserting images in a Tiptap editor. + * + * For custom button implementations, use the `useImage` hook instead. + */ +export const ImageUploadButton = forwardRef< + HTMLButtonElement, + ImageUploadButtonProps +>( + ( + { + editor: providedEditor, + text, + hideWhenUnavailable = false, + onInserted, + showShortcut = false, + onClick, + icon: CustomIcon, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + canInsert, + handleImage, + label, + isActive, + shortcutKeys, + Icon, + } = useImageUpload({ + editor, + hideWhenUnavailable, + onInserted, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleImage() + }, + [handleImage, onClick] + ) + + if (!isVisible) { + return null + } + + const RenderIcon = CustomIcon ?? Icon + + return ( + + ) + } +) + +ImageUploadButton.displayName = "ImageUploadButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/index.tsx new file mode 100644 index 0000000000..815d5bb5ef --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./image-upload-button" +export * from "./use-image-upload" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts new file mode 100644 index 0000000000..7d91811ff3 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts @@ -0,0 +1,192 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { useHotkeys } from "react-hotkeys-hook" +import { type Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useIsMobile } from "@/hooks/use-mobile" + +// --- Lib --- +import { isExtensionAvailable } from "@/lib/tiptap-utils" + +// --- Icons --- +import { ImagePlusIcon } from "@/components/tiptap-icons/image-plus-icon" + +export const IMAGE_UPLOAD_SHORTCUT_KEY = "mod+shift+i" + +/** + * Configuration for the image upload functionality + */ +export interface UseImageUploadConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * Whether the button should hide when insertion is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful image insertion. + */ + onInserted?: () => void +} + +/** + * Checks if image can be inserted in the current editor state + */ +export function canInsertImage(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + if (!isExtensionAvailable(editor, "imageUpload")) return false + + return editor.can().insertContent({ type: "imageUpload" }) +} + +/** + * Checks if image is currently active + */ +export function isImageActive(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + return editor.isActive("imageUpload") +} + +/** + * Inserts an image in the editor + */ +export function insertImage(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + if (!canInsertImage(editor)) return false + + try { + return editor + .chain() + .focus() + .insertContent({ + type: "imageUpload", + }) + .run() + } catch { + return false + } +} + +/** + * Determines if the image button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean +}): boolean { + const { editor, hideWhenUnavailable } = props + + if (!editor || !editor.isEditable) return false + if (!isExtensionAvailable(editor, "imageUpload")) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canInsertImage(editor) + } + + return true +} + +/** + * Custom hook that provides image functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage - no params needed + * function MySimpleImageButton() { + * const { isVisible, handleImage } = useImage() + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedImageButton() { + * const { isVisible, handleImage, label, isActive } = useImage({ + * editor: myEditor, + * hideWhenUnavailable: true, + * onInserted: () => console.log('Image inserted!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Add Image + * + * ) + * } + * ``` + */ +export function useImageUpload(config?: UseImageUploadConfig) { + const { + editor: providedEditor, + hideWhenUnavailable = false, + onInserted, + } = config || {} + + const { editor } = useTiptapEditor(providedEditor) + const isMobile = useIsMobile() + const [isVisible, setIsVisible] = useState(true) + const canInsert = canInsertImage(editor) + const isActive = isImageActive(editor) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable]) + + const handleImage = useCallback(() => { + if (!editor) return false + + const success = insertImage(editor) + if (success) { + onInserted?.() + } + return success + }, [editor, onInserted]) + + useHotkeys( + IMAGE_UPLOAD_SHORTCUT_KEY, + (event) => { + event.preventDefault() + handleImage() + }, + { + enabled: isVisible && canInsert, + enableOnContentEditable: !isMobile, + enableOnFormTags: true, + } + ) + + return { + isVisible, + isActive, + handleImage, + canInsert, + label: "Add image", + shortcutKeys: IMAGE_UPLOAD_SHORTCUT_KEY, + Icon: ImagePlusIcon, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/index.tsx new file mode 100644 index 0000000000..e725ea83ae --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/index.tsx @@ -0,0 +1,2 @@ +export * from "./link-popover" +export * from "./use-link-popover" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx new file mode 100644 index 0000000000..4f6151db66 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx @@ -0,0 +1,307 @@ +"use client" + +import { forwardRef, useCallback, useEffect, useState } from "react" +import type { Editor } from "@tiptap/react" + +// --- Hooks --- +import { useIsMobile } from "@/hooks/use-mobile" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { CornerDownLeftIcon } from "@/components/tiptap-icons/corner-down-left-icon" +import { ExternalLinkIcon } from "@/components/tiptap-icons/external-link-icon" +import { LinkIcon } from "@/components/tiptap-icons/link-icon" +import { TrashIcon } from "@/components/tiptap-icons/trash-icon" + +// --- Tiptap UI --- +import type { UseLinkPopoverConfig } from "@/components/tiptap-ui/link-popover" +import { useLinkPopover } from "@/components/tiptap-ui/link-popover" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/tiptap-ui-primitive/popover" +import { Separator } from "@/components/tiptap-ui-primitive/separator" +import { + Card, + CardBody, + CardItemGroup, +} from "@/components/tiptap-ui-primitive/card" +import { Input, InputGroup } from "@/components/tiptap-ui-primitive/input" + +export interface LinkMainProps { + /** + * The URL to set for the link. + */ + url: string + /** + * Function to update the URL state. + */ + setUrl: React.Dispatch> + /** + * Function to set the link in the editor. + */ + setLink: () => void + /** + * Function to remove the link from the editor. + */ + removeLink: () => void + /** + * Function to open the link. + */ + openLink: () => void + /** + * Whether the link is currently active in the editor. + */ + isActive: boolean +} + +export interface LinkPopoverProps + extends Omit, + UseLinkPopoverConfig { + /** + * Callback for when the popover opens or closes. + */ + onOpenChange?: (isOpen: boolean) => void + /** + * Whether to automatically open the popover when a link is active. + * @default true + */ + autoOpenOnLinkActive?: boolean +} + +/** + * Link button component for triggering the link popover + */ +export const LinkButton = forwardRef( + ({ className, children, ...props }, ref) => { + return ( + + ) + } +) + +LinkButton.displayName = "LinkButton" + +/** + * Main content component for the link popover + */ +const LinkMain: React.FC = ({ + url, + setUrl, + setLink, + removeLink, + openLink, + isActive, +}) => { + const isMobile = useIsMobile() + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault() + setLink() + } + } + + return ( + + + + + setUrl(e.target.value)} + onKeyDown={handleKeyDown} + autoFocus + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + /> + + + + + + + + + + + + + + + + + ) +} + +/** + * Link content component for standalone use + */ +export const LinkContent: React.FC<{ + editor?: Editor | null +}> = ({ editor }) => { + const linkPopover = useLinkPopover({ + editor, + }) + + return +} + +/** + * Link popover component for Tiptap editors. + * + * For custom popover implementations, use the `useLinkPopover` hook instead. + */ +export const LinkPopover = forwardRef( + ( + { + editor: providedEditor, + hideWhenUnavailable = false, + onSetLink, + onOpenChange, + autoOpenOnLinkActive = true, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const [isOpen, setIsOpen] = useState(false) + + const { + isVisible, + canSet, + isActive, + url, + setUrl, + setLink, + removeLink, + openLink, + label, + Icon, + } = useLinkPopover({ + editor, + hideWhenUnavailable, + onSetLink, + }) + + const handleOnOpenChange = useCallback( + (nextIsOpen: boolean) => { + setIsOpen(nextIsOpen) + onOpenChange?.(nextIsOpen) + }, + [onOpenChange] + ) + + const handleSetLink = useCallback(() => { + setLink() + setIsOpen(false) + }, [setLink]) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + setIsOpen(!isOpen) + }, + [onClick, isOpen] + ) + + useEffect(() => { + if (autoOpenOnLinkActive && isActive) { + setIsOpen(true) + } + }, [autoOpenOnLinkActive, isActive]) + + if (!isVisible) { + return null + } + + return ( + + + + {children ?? } + + + + + + + + ) + } +) + +LinkPopover.displayName = "LinkPopover" + +export default LinkPopover diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts new file mode 100644 index 0000000000..ac0b779e06 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts @@ -0,0 +1,284 @@ +import { useCallback, useEffect, useState } from "react" +import type { Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { LinkIcon } from "@/components/tiptap-icons/link-icon" + +// --- Lib --- +import { + isMarkInSchema, + isNodeTypeSelected, + sanitizeUrl, +} from "@/lib/tiptap-utils" + +/** + * Configuration for the link popover functionality + */ +export interface UseLinkPopoverConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * Whether to hide the link popover when not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called when the link is set. + */ + onSetLink?: () => void +} + +/** + * Configuration for the link handler functionality + */ +export interface LinkHandlerProps { + /** + * The Tiptap editor instance. + */ + editor: Editor | null + /** + * Callback function called when the link is set. + */ + onSetLink?: () => void +} + +/** + * Checks if a link can be set in the current editor state + */ +export function canSetLink(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + + // The third argument 'true' checks whether the current selection is inside an image caption, and prevents setting a link there + // If the selection is inside an image caption, we can't set a link + if (isNodeTypeSelected(editor, ["image"], true)) return false + return editor.can().setMark("link") +} + +/** + * Checks if a link is currently active in the editor + */ +export function isLinkActive(editor: Editor | null): boolean { + if (!editor || !editor.isEditable) return false + return editor.isActive("link") +} + +/** + * Determines if the link button should be shown + */ +export function shouldShowLinkButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean +}): boolean { + const { editor, hideWhenUnavailable } = props + + const linkInSchema = isMarkInSchema("link", editor) + + if (!linkInSchema || !editor) { + return false + } + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canSetLink(editor) + } + + return true +} + +/** + * Custom hook for handling link operations in a Tiptap editor + */ +export function useLinkHandler(props: LinkHandlerProps) { + const { editor, onSetLink } = props + const [url, setUrl] = useState(null) + + useEffect(() => { + if (!editor) return + + // Get URL immediately on mount + const { href } = editor.getAttributes("link") + + if (isLinkActive(editor) && url === null) { + setUrl(href || "") + } + }, [editor, url]) + + useEffect(() => { + if (!editor) return + + const updateLinkState = () => { + const { href } = editor.getAttributes("link") + setUrl(href || "") + } + + editor.on("selectionUpdate", updateLinkState) + return () => { + editor.off("selectionUpdate", updateLinkState) + } + }, [editor]) + + const setLink = useCallback(() => { + if (!url || !editor) return + + const { selection } = editor.state + const isEmpty = selection.empty + + let chain = editor.chain().focus() + + chain = chain.extendMarkRange("link").setLink({ href: url }) + + if (isEmpty) { + chain = chain.insertContent({ type: "text", text: url }) + } + + chain.run() + + setUrl(null) + + onSetLink?.() + }, [editor, onSetLink, url]) + + const removeLink = useCallback(() => { + if (!editor) return + editor + .chain() + .focus() + .extendMarkRange("link") + .unsetLink() + .setMeta("preventAutolink", true) + .run() + setUrl("") + }, [editor]) + + const openLink = useCallback( + (target: string = "_blank", features: string = "noopener,noreferrer") => { + if (!url) return + + const safeUrl = sanitizeUrl(url, window.location.href) + if (safeUrl !== "#") { + window.open(safeUrl, target, features) + } + }, + [url] + ) + + return { + url: url || "", + setUrl, + setLink, + removeLink, + openLink, + } +} + +/** + * Custom hook for link popover state management + */ +export function useLinkState(props: { + editor: Editor | null + hideWhenUnavailable: boolean +}) { + const { editor, hideWhenUnavailable = false } = props + + const canSet = canSetLink(editor) + const isActive = isLinkActive(editor) + + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible( + shouldShowLinkButton({ + editor, + hideWhenUnavailable, + }) + ) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable]) + + return { + isVisible, + canSet, + isActive, + } +} + +/** + * Main hook that provides link popover functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MyLinkButton() { + * const { isVisible, canSet, isActive, Icon, label } = useLinkPopover() + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedLinkButton() { + * const { isVisible, canSet, isActive, Icon, label } = useLinkPopover({ + * editor: myEditor, + * hideWhenUnavailable: true, + * onSetLink: () => console.log('Link set!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * + * {label} + * + * ) + * } + * ``` + */ +export function useLinkPopover(config?: UseLinkPopoverConfig) { + const { + editor: providedEditor, + hideWhenUnavailable = false, + onSetLink, + } = config || {} + + const { editor } = useTiptapEditor(providedEditor) + + const { isVisible, canSet, isActive } = useLinkState({ + editor, + hideWhenUnavailable, + }) + + const linkHandler = useLinkHandler({ + editor, + onSetLink, + }) + + return { + isVisible, + canSet, + isActive, + label: "Link", + Icon: LinkIcon, + ...linkHandler, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/index.tsx new file mode 100644 index 0000000000..9f3d066656 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./list-button" +export * from "./use-list" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx new file mode 100644 index 0000000000..bbe0f6a436 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx @@ -0,0 +1,121 @@ +import { forwardRef, useCallback } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +// --- Tiptap UI --- +import type { ListType, UseListConfig } from "@/components/tiptap-ui/list-button" +import { LIST_SHORTCUT_KEYS, useList } from "@/components/tiptap-ui/list-button" + +export interface ListButtonProps + extends Omit, + UseListConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function ListShortcutBadge({ + type, + shortcutKeys = LIST_SHORTCUT_KEYS[type], +}: { + type: ListType + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for toggling lists in a Tiptap editor. + * + * For custom button implementations, use the `useList` hook instead. + */ +export const ListButton = forwardRef( + ( + { + editor: providedEditor, + type, + text, + hideWhenUnavailable = false, + onToggled, + showShortcut = false, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + canToggle, + isActive, + handleToggle, + label, + shortcutKeys, + Icon, + } = useList({ + editor, + type, + hideWhenUnavailable, + onToggled, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleToggle() + }, + [handleToggle, onClick] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +ListButton.displayName = "ListButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts new file mode 100644 index 0000000000..55bbbff6bc --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts @@ -0,0 +1,326 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { type Editor } from "@tiptap/react" +import { NodeSelection, TextSelection } from "@tiptap/pm/state" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { ListIcon } from "@/components/tiptap-icons/list-icon" +import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon" +import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon" + +// --- Lib --- +import { + findNodePosition, + isNodeInSchema, + isNodeTypeSelected, + isValidPosition, + selectionWithinConvertibleTypes, +} from "@/lib/tiptap-utils" + +export type ListType = "bulletList" | "orderedList" | "taskList" + +/** + * Configuration for the list functionality + */ +export interface UseListConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The type of list to toggle. + */ + type: ListType + /** + * Whether the button should hide when list is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful toggle. + */ + onToggled?: () => void +} + +export const listIcons = { + bulletList: ListIcon, + orderedList: ListOrderedIcon, + taskList: ListTodoIcon, +} + +export const listLabels: Record = { + bulletList: "Bullet List", + orderedList: "Ordered List", + taskList: "Task List", +} + +export const LIST_SHORTCUT_KEYS: Record = { + bulletList: "mod+shift+8", + orderedList: "mod+shift+7", + taskList: "mod+shift+9", +} + +/** + * Checks if a list can be toggled in the current editor state + */ +export function canToggleList( + editor: Editor | null, + type: ListType, + turnInto: boolean = true +): boolean { + if (!editor || !editor.isEditable) return false + if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) + return false + + if (!turnInto) { + switch (type) { + case "bulletList": + return editor.can().toggleBulletList() + case "orderedList": + return editor.can().toggleOrderedList() + case "taskList": + return editor.can().toggleList("taskList", "taskItem") + default: + return false + } + } + + // Ensure selection is in nodes we're allowed to convert + if ( + !selectionWithinConvertibleTypes(editor, [ + "paragraph", + "heading", + "bulletList", + "orderedList", + "taskList", + "blockquote", + "codeBlock", + ]) + ) + return false + + // Either we can set list directly on the selection, + // or we can clear formatting/nodes to arrive at a list. + switch (type) { + case "bulletList": + return editor.can().toggleBulletList() || editor.can().clearNodes() + case "orderedList": + return editor.can().toggleOrderedList() || editor.can().clearNodes() + case "taskList": + return ( + editor.can().toggleList("taskList", "taskItem") || + editor.can().clearNodes() + ) + default: + return false + } +} + +/** + * Checks if list is currently active + */ +export function isListActive(editor: Editor | null, type: ListType): boolean { + if (!editor || !editor.isEditable) return false + + switch (type) { + case "bulletList": + return editor.isActive("bulletList") + case "orderedList": + return editor.isActive("orderedList") + case "taskList": + return editor.isActive("taskList") + default: + return false + } +} + +/** + * Toggles list in the editor + */ +export function toggleList(editor: Editor | null, type: ListType): boolean { + if (!editor || !editor.isEditable) return false + if (!canToggleList(editor, type)) return false + + try { + const view = editor.view + let state = view.state + let tr = state.tr + + // No selection, find the the cursor position + if (state.selection.empty || state.selection instanceof TextSelection) { + const pos = findNodePosition({ + editor, + node: state.selection.$anchor.node(1), + })?.pos + if (!isValidPosition(pos)) return false + + tr = tr.setSelection(NodeSelection.create(state.doc, pos)) + view.dispatch(tr) + state = view.state + } + + const selection = state.selection + + let chain = editor.chain().focus() + + // Handle NodeSelection + if (selection instanceof NodeSelection) { + const firstChild = selection.node.firstChild?.firstChild + const lastChild = selection.node.lastChild?.lastChild + + const from = firstChild + ? selection.from + firstChild.nodeSize + : selection.from + 1 + + const to = lastChild + ? selection.to - lastChild.nodeSize + : selection.to - 1 + + const resolvedFrom = state.doc.resolve(from) + const resolvedTo = state.doc.resolve(to) + + chain = chain + .setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)) + .clearNodes() + } + + if (editor.isActive(type)) { + // Unwrap list + chain + .liftListItem("listItem") + .lift("bulletList") + .lift("orderedList") + .lift("taskList") + .run() + } else { + // Wrap in specific list type + const toggleMap: Record typeof chain> = { + bulletList: () => chain.toggleBulletList(), + orderedList: () => chain.toggleOrderedList(), + taskList: () => chain.toggleList("taskList", "taskItem"), + } + + const toggle = toggleMap[type] + if (!toggle) return false + + toggle().run() + } + + editor.chain().focus().selectTextblockEnd().run() + + return true + } catch { + return false + } +} + +/** + * Determines if the list button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + type: ListType + hideWhenUnavailable: boolean +}): boolean { + const { editor, type, hideWhenUnavailable } = props + + if (!editor || !editor.isEditable) return false + if (!isNodeInSchema(type, editor)) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canToggleList(editor, type) + } + + return true +} + +/** + * Custom hook that provides list functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MySimpleListButton() { + * const { isVisible, handleToggle, isActive } = useList({ type: "bulletList" }) + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedListButton() { + * const { isVisible, handleToggle, label, isActive } = useList({ + * type: "orderedList", + * editor: myEditor, + * hideWhenUnavailable: true, + * onToggled: () => console.log('List toggled!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Toggle List + * + * ) + * } + * ``` + */ +export function useList(config: UseListConfig) { + const { + editor: providedEditor, + type, + hideWhenUnavailable = false, + onToggled, + } = config + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canToggle = canToggleList(editor, type) + const isActive = isListActive(editor, type) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, type, hideWhenUnavailable]) + + const handleToggle = useCallback(() => { + if (!editor) return false + + const success = toggleList(editor, type) + if (success) { + onToggled?.() + } + return success + }, [editor, type, onToggled]) + + return { + isVisible, + isActive, + handleToggle, + canToggle, + label: listLabels[type], + shortcutKeys: LIST_SHORTCUT_KEYS[type], + Icon: listIcons[type], + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/index.tsx new file mode 100644 index 0000000000..9a215b8016 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/index.tsx @@ -0,0 +1 @@ +export * from "./list-dropdown-menu" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx new file mode 100644 index 0000000000..9aa0669762 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx @@ -0,0 +1,123 @@ +import { useCallback, useState } from "react" +import { type Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon" + +// --- Tiptap UI --- +import { ListButton, type ListType } from "@/components/tiptap-ui/list-button" + +import { useListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/tiptap-ui-primitive/dropdown-menu" +import { Card, CardBody } from "@/components/tiptap-ui-primitive/card" + +export interface ListDropdownMenuProps extends Omit { + /** + * The Tiptap editor instance. + */ + editor?: Editor + /** + * The list types to display in the dropdown. + */ + types?: ListType[] + /** + * Whether the dropdown should be hidden when no list types are available + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback for when the dropdown opens or closes + */ + onOpenChange?: (isOpen: boolean) => void + /** + * Whether to render the dropdown menu in a portal + * @default false + */ + portal?: boolean +} + +export function ListDropdownMenu({ + editor: providedEditor, + types = ["bulletList", "orderedList", "taskList"], + hideWhenUnavailable = false, + onOpenChange, + portal = false, + ...props +}: ListDropdownMenuProps) { + const { editor } = useTiptapEditor(providedEditor) + const [isOpen, setIsOpen] = useState(false) + + const { filteredLists, canToggle, isActive, isVisible, Icon } = + useListDropdownMenu({ + editor, + types, + hideWhenUnavailable, + }) + + const handleOnOpenChange = useCallback( + (open: boolean) => { + setIsOpen(open) + onOpenChange?.(open) + }, + [onOpenChange] + ) + + if (!isVisible || !editor || !editor.isEditable) { + return null + } + + return ( + + + + + + + + + + {filteredLists.map((option) => ( + + + + ))} + + + + + + ) +} + +export default ListDropdownMenu diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts new file mode 100644 index 0000000000..ed7b5c1bff --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts @@ -0,0 +1,216 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import type { Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Icons --- +import { ListIcon } from "@/components/tiptap-icons/list-icon" +import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon" +import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon" + +// --- Lib --- +import { isNodeInSchema } from "@/lib/tiptap-utils" + +// --- Tiptap UI --- +import { + canToggleList, + isListActive, + listIcons, + type ListType, +} from "@/components/tiptap-ui/list-button" + +/** + * Configuration for the list dropdown menu functionality + */ +export interface UseListDropdownMenuConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The list types to display in the dropdown. + * @default ["bulletList", "orderedList", "taskList"] + */ + types?: ListType[] + /** + * Whether the dropdown should be hidden when no list types are available + * @default false + */ + hideWhenUnavailable?: boolean +} + +export interface ListOption { + label: string + type: ListType + icon: React.ElementType +} + +export const listOptions: ListOption[] = [ + { + label: "Bullet List", + type: "bulletList", + icon: ListIcon, + }, + { + label: "Ordered List", + type: "orderedList", + icon: ListOrderedIcon, + }, + { + label: "Task List", + type: "taskList", + icon: ListTodoIcon, + }, +] + +export function canToggleAnyList( + editor: Editor | null, + listTypes: ListType[] +): boolean { + if (!editor || !editor.isEditable) return false + return listTypes.some((type) => canToggleList(editor, type)) +} + +export function isAnyListActive( + editor: Editor | null, + listTypes: ListType[] +): boolean { + if (!editor || !editor.isEditable) return false + return listTypes.some((type) => isListActive(editor, type)) +} + +export function getFilteredListOptions( + availableTypes: ListType[] +): typeof listOptions { + return listOptions.filter( + (option) => !option.type || availableTypes.includes(option.type) + ) +} + +export function shouldShowListDropdown(params: { + editor: Editor | null + listTypes: ListType[] + hideWhenUnavailable: boolean + listInSchema: boolean + canToggleAny: boolean +}): boolean { + const { editor, hideWhenUnavailable, listInSchema, canToggleAny } = params + + if (!listInSchema || !editor) { + return false + } + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canToggleAny + } + + return true +} + +/** + * Gets the currently active list type from the available types + */ +export function getActiveListType( + editor: Editor | null, + availableTypes: ListType[] +): ListType | undefined { + if (!editor || !editor.isEditable) return undefined + return availableTypes.find((type) => isListActive(editor, type)) +} + +/** + * Custom hook that provides list dropdown menu functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MyListDropdown() { + * const { + * isVisible, + * activeType, + * isAnyActive, + * canToggleAny, + * filteredLists, + * } = useListDropdownMenu() + * + * if (!isVisible) return null + * + * return ( + * + * // dropdown content + * + * ) + * } + * + * // Advanced usage with configuration + * function MyAdvancedListDropdown() { + * const { + * isVisible, + * activeType, + * } = useListDropdownMenu({ + * editor: myEditor, + * types: ["bulletList", "orderedList"], + * hideWhenUnavailable: true, + * }) + * + * // component implementation + * } + * ``` + */ +export function useListDropdownMenu(config?: UseListDropdownMenuConfig) { + const { + editor: providedEditor, + types = ["bulletList", "orderedList", "taskList"], + hideWhenUnavailable = false, + } = config || {} + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(false) + + const listInSchema = types.some((type) => isNodeInSchema(type, editor)) + + const filteredLists = useMemo(() => getFilteredListOptions(types), [types]) + + const canToggleAny = canToggleAnyList(editor, types) + const isAnyActive = isAnyListActive(editor, types) + const activeType = getActiveListType(editor, types) + const activeList = filteredLists.find((option) => option.type === activeType) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible( + shouldShowListDropdown({ + editor, + listTypes: types, + hideWhenUnavailable, + listInSchema, + canToggleAny, + }) + ) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [canToggleAny, editor, hideWhenUnavailable, listInSchema, types]) + + return { + isVisible, + activeType, + isActive: isAnyActive, + canToggle: canToggleAny, + types, + filteredLists, + label: "List", + Icon: activeList ? listIcons[activeList.type] : ListIcon, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/index.tsx new file mode 100644 index 0000000000..32e85b9c7b --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./mark-button" +export * from "./use-mark" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx new file mode 100644 index 0000000000..338bae71c0 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx @@ -0,0 +1,123 @@ +"use client" + +import { forwardRef, useCallback } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Tiptap UI --- +import type { Mark, UseMarkConfig } from "@/components/tiptap-ui/mark-button" +import { MARK_SHORTCUT_KEYS, useMark } from "@/components/tiptap-ui/mark-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +export interface MarkButtonProps + extends Omit, + UseMarkConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function MarkShortcutBadge({ + type, + shortcutKeys = MARK_SHORTCUT_KEYS[type], +}: { + type: Mark + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for toggling marks in a Tiptap editor. + * + * For custom button implementations, use the `useMark` hook instead. + */ +export const MarkButton = forwardRef( + ( + { + editor: providedEditor, + type, + text, + hideWhenUnavailable = false, + onToggled, + showShortcut = false, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + handleMark, + label, + canToggle, + isActive, + Icon, + shortcutKeys, + } = useMark({ + editor, + type, + hideWhenUnavailable, + onToggled, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleMark() + }, + [handleMark, onClick] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +MarkButton.displayName = "MarkButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts new file mode 100644 index 0000000000..59b70759d4 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts @@ -0,0 +1,212 @@ +import { useCallback, useEffect, useState } from "react" +import type { Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { isMarkInSchema, isNodeTypeSelected } from "@/lib/tiptap-utils" + +// --- Icons --- +import { BoldIcon } from "@/components/tiptap-icons/bold-icon" +import { Code2Icon } from "@/components/tiptap-icons/code2-icon" +import { ItalicIcon } from "@/components/tiptap-icons/italic-icon" +import { StrikeIcon } from "@/components/tiptap-icons/strike-icon" +import { SubscriptIcon } from "@/components/tiptap-icons/subscript-icon" +import { SuperscriptIcon } from "@/components/tiptap-icons/superscript-icon" +import { UnderlineIcon } from "@/components/tiptap-icons/underline-icon" + +export type Mark = + | "bold" + | "italic" + | "strike" + | "code" + | "underline" + | "superscript" + | "subscript" + +/** + * Configuration for the mark functionality + */ +export interface UseMarkConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The type of mark to toggle + */ + type: Mark + /** + * Whether the button should hide when mark is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful mark toggle. + */ + onToggled?: () => void +} + +export const markIcons = { + bold: BoldIcon, + italic: ItalicIcon, + underline: UnderlineIcon, + strike: StrikeIcon, + code: Code2Icon, + superscript: SuperscriptIcon, + subscript: SubscriptIcon, +} + +export const MARK_SHORTCUT_KEYS: Record = { + bold: "mod+b", + italic: "mod+i", + underline: "mod+u", + strike: "mod+shift+s", + code: "mod+e", + superscript: "mod+.", + subscript: "mod+,", +} + +/** + * Checks if a mark can be toggled in the current editor state + */ +export function canToggleMark(editor: Editor | null, type: Mark): boolean { + if (!editor || !editor.isEditable) return false + if (!isMarkInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) + return false + + return editor.can().toggleMark(type) +} + +/** + * Checks if a mark is currently active + */ +export function isMarkActive(editor: Editor | null, type: Mark): boolean { + if (!editor || !editor.isEditable) return false + return editor.isActive(type) +} + +/** + * Toggles a mark in the editor + */ +export function toggleMark(editor: Editor | null, type: Mark): boolean { + if (!editor || !editor.isEditable) return false + if (!canToggleMark(editor, type)) return false + + return editor.chain().focus().toggleMark(type).run() +} + +/** + * Determines if the mark button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + type: Mark + hideWhenUnavailable: boolean +}): boolean { + const { editor, type, hideWhenUnavailable } = props + + if (!editor || !editor.isEditable) return false + if (!isMarkInSchema(type, editor)) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canToggleMark(editor, type) + } + + return true +} + +/** + * Gets the formatted mark name + */ +export function getFormattedMarkName(type: Mark): string { + return type.charAt(0).toUpperCase() + type.slice(1) +} + +/** + * Custom hook that provides mark functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MySimpleBoldButton() { + * const { isVisible, handleMark } = useMark({ type: "bold" }) + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedItalicButton() { + * const { isVisible, handleMark, label, isActive } = useMark({ + * editor: myEditor, + * type: "italic", + * hideWhenUnavailable: true, + * onToggled: () => console.log('Mark toggled!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Italic + * + * ) + * } + * ``` + */ +export function useMark(config: UseMarkConfig) { + const { + editor: providedEditor, + type, + hideWhenUnavailable = false, + onToggled, + } = config + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canToggle = canToggleMark(editor, type) + const isActive = isMarkActive(editor, type) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, type, hideWhenUnavailable]) + + const handleMark = useCallback(() => { + if (!editor) return false + + const success = toggleMark(editor, type) + if (success) { + onToggled?.() + } + return success + }, [editor, type, onToggled]) + + return { + isVisible, + isActive, + handleMark, + canToggle, + label: getFormattedMarkName(type), + shortcutKeys: MARK_SHORTCUT_KEYS[type], + Icon: markIcons[type], + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/index.tsx new file mode 100644 index 0000000000..d19f95cf02 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./text-align-button" +export * from "./use-text-align" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx new file mode 100644 index 0000000000..b62598831b --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx @@ -0,0 +1,145 @@ +"use client" + +import { forwardRef, useCallback } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Tiptap UI --- +import type { + TextAlign, + UseTextAlignConfig, +} from "@/components/tiptap-ui/text-align-button" +import { + TEXT_ALIGN_SHORTCUT_KEYS, + useTextAlign, +} from "@/components/tiptap-ui/text-align-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +type IconProps = React.SVGProps +type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement + +export interface TextAlignButtonProps + extends Omit, + UseTextAlignConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean + /** + * Optional custom icon component to render instead of the default. + */ + icon?: React.MemoExoticComponent | React.FC +} + +export function TextAlignShortcutBadge({ + align, + shortcutKeys = TEXT_ALIGN_SHORTCUT_KEYS[align], +}: { + align: TextAlign + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for setting text alignment in a Tiptap editor. + * + * For custom button implementations, use the `useTextAlign` hook instead. + */ +export const TextAlignButton = forwardRef< + HTMLButtonElement, + TextAlignButtonProps +>( + ( + { + editor: providedEditor, + align, + text, + hideWhenUnavailable = false, + onAligned, + showShortcut = false, + onClick, + icon: CustomIcon, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { + isVisible, + handleTextAlign, + label, + canAlign, + isActive, + Icon, + shortcutKeys, + } = useTextAlign({ + editor, + align, + hideWhenUnavailable, + onAligned, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleTextAlign() + }, + [handleTextAlign, onClick] + ) + + if (!isVisible) { + return null + } + + const RenderIcon = CustomIcon ?? Icon + + return ( + + ) + } +) + +TextAlignButton.displayName = "TextAlignButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts new file mode 100644 index 0000000000..0db3a10a32 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts @@ -0,0 +1,222 @@ +import { useCallback, useEffect, useState } from "react" +import type { ChainedCommands } from "@tiptap/react" +import { type Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { + isExtensionAvailable, + isNodeTypeSelected, +} from "@/lib/tiptap-utils" + +// --- Icons --- +import { AlignCenterIcon } from "@/components/tiptap-icons/align-center-icon" +import { AlignJustifyIcon } from "@/components/tiptap-icons/align-justify-icon" +import { AlignLeftIcon } from "@/components/tiptap-icons/align-left-icon" +import { AlignRightIcon } from "@/components/tiptap-icons/align-right-icon" + +export type TextAlign = "left" | "center" | "right" | "justify" + +/** + * Configuration for the text align functionality + */ +export interface UseTextAlignConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The text alignment to apply. + */ + align: TextAlign + /** + * Whether the button should hide when alignment is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful alignment change. + */ + onAligned?: () => void +} + +export const TEXT_ALIGN_SHORTCUT_KEYS: Record = { + left: "mod+shift+l", + center: "mod+shift+e", + right: "mod+shift+r", + justify: "mod+shift+j", +} + +export const textAlignIcons = { + left: AlignLeftIcon, + center: AlignCenterIcon, + right: AlignRightIcon, + justify: AlignJustifyIcon, +} + +export const textAlignLabels: Record = { + left: "Align left", + center: "Align center", + right: "Align right", + justify: "Align justify", +} + +/** + * Checks if text alignment can be performed in the current editor state + */ +export function canSetTextAlign( + editor: Editor | null, + align: TextAlign +): boolean { + if (!editor || !editor.isEditable) return false + if ( + !isExtensionAvailable(editor, "textAlign") || + isNodeTypeSelected(editor, ["image", "horizontalRule"]) + ) + return false + + return editor.can().setTextAlign(align) +} + +export function hasSetTextAlign( + commands: ChainedCommands +): commands is ChainedCommands & { + setTextAlign: (align: TextAlign) => ChainedCommands +} { + return "setTextAlign" in commands +} + +/** + * Checks if the text alignment is currently active + */ +export function isTextAlignActive( + editor: Editor | null, + align: TextAlign +): boolean { + if (!editor || !editor.isEditable) return false + return editor.isActive({ textAlign: align }) +} + +/** + * Sets text alignment in the editor + */ +export function setTextAlign(editor: Editor | null, align: TextAlign): boolean { + if (!editor || !editor.isEditable) return false + if (!canSetTextAlign(editor, align)) return false + + const chain = editor.chain().focus() + if (hasSetTextAlign(chain)) { + return chain.setTextAlign(align).run() + } + + return false +} + +/** + * Determines if the text align button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean + align: TextAlign +}): boolean { + const { editor, hideWhenUnavailable, align } = props + + if (!editor || !editor.isEditable) return false + if (!isExtensionAvailable(editor, "textAlign")) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canSetTextAlign(editor, align) + } + + return true +} + +/** + * Custom hook that provides text align functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MySimpleAlignButton() { + * const { isVisible, handleTextAlign } = useTextAlign({ align: "center" }) + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedAlignButton() { + * const { isVisible, handleTextAlign, label, isActive } = useTextAlign({ + * editor: myEditor, + * align: "right", + * hideWhenUnavailable: true, + * onAligned: () => console.log('Text aligned!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Align Right + * + * ) + * } + * ``` + */ +export function useTextAlign(config: UseTextAlignConfig) { + const { + editor: providedEditor, + align, + hideWhenUnavailable = false, + onAligned, + } = config + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canAlign = canSetTextAlign(editor, align) + const isActive = isTextAlignActive(editor, align) + + useEffect(() => { + if (!editor) return + + const handleSelectionUpdate = () => { + setIsVisible(shouldShowButton({ editor, align, hideWhenUnavailable })) + } + + handleSelectionUpdate() + + editor.on("selectionUpdate", handleSelectionUpdate) + + return () => { + editor.off("selectionUpdate", handleSelectionUpdate) + } + }, [editor, hideWhenUnavailable, align]) + + const handleTextAlign = useCallback(() => { + if (!editor) return false + + const success = setTextAlign(editor, align) + if (success) { + onAligned?.() + } + return success + }, [editor, align, onAligned]) + + return { + isVisible, + isActive, + handleTextAlign, + canAlign, + label: textAlignLabels[align], + shortcutKeys: TEXT_ALIGN_SHORTCUT_KEYS[align], + Icon: textAlignIcons[align], + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/index.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/index.tsx new file mode 100644 index 0000000000..fa0fdbeb08 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/index.tsx @@ -0,0 +1,2 @@ +export * from "./undo-redo-button" +export * from "./use-undo-redo" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx new file mode 100644 index 0000000000..d990b037ef --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx @@ -0,0 +1,126 @@ +"use client" + +import { forwardRef, useCallback } from "react" + +// --- Lib --- +import { parseShortcutKeys } from "@/lib/tiptap-utils" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Tiptap UI --- +import type { + UndoRedoAction, + UseUndoRedoConfig, +} from "@/components/tiptap-ui/undo-redo-button" +import { + UNDO_REDO_SHORTCUT_KEYS, + useUndoRedo, +} from "@/components/tiptap-ui/undo-redo-button" + +// --- UI Primitives --- +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" + +export interface UndoRedoButtonProps + extends Omit, + UseUndoRedoConfig { + /** + * Optional text to display alongside the icon. + */ + text?: string + /** + * Optional show shortcut keys in the button. + * @default false + */ + showShortcut?: boolean +} + +export function HistoryShortcutBadge({ + action, + shortcutKeys = UNDO_REDO_SHORTCUT_KEYS[action], +}: { + action: UndoRedoAction + shortcutKeys?: string +}) { + return {parseShortcutKeys({ shortcutKeys })} +} + +/** + * Button component for triggering undo/redo actions in a Tiptap editor. + * + * For custom button implementations, use the `useHistory` hook instead. + */ +export const UndoRedoButton = forwardRef< + HTMLButtonElement, + UndoRedoButtonProps +>( + ( + { + editor: providedEditor, + action, + text, + hideWhenUnavailable = false, + onExecuted, + showShortcut = false, + onClick, + children, + ...buttonProps + }, + ref + ) => { + const { editor } = useTiptapEditor(providedEditor) + const { isVisible, handleAction, label, canExecute, Icon, shortcutKeys } = + useUndoRedo({ + editor, + action, + hideWhenUnavailable, + onExecuted, + }) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.(event) + if (event.defaultPrevented) return + handleAction() + }, + [handleAction, onClick] + ) + + if (!isVisible) { + return null + } + + return ( + + ) + } +) + +UndoRedoButton.displayName = "UndoRedoButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts new file mode 100644 index 0000000000..ea4d8455a2 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts @@ -0,0 +1,182 @@ +import { useCallback, useEffect, useState } from "react" +import { type Editor } from "@tiptap/react" + +// --- Hooks --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" + +// --- Lib --- +import { isNodeTypeSelected } from "@/lib/tiptap-utils" + +// --- Icons --- +import { Redo2Icon } from "@/components/tiptap-icons/redo2-icon" +import { Undo2Icon } from "@/components/tiptap-icons/undo2-icon" + +export type UndoRedoAction = "undo" | "redo" + +/** + * Configuration for the history functionality + */ +export interface UseUndoRedoConfig { + /** + * The Tiptap editor instance. + */ + editor?: Editor | null + /** + * The history action to perform (undo or redo). + */ + action: UndoRedoAction + /** + * Whether the button should hide when action is not available. + * @default false + */ + hideWhenUnavailable?: boolean + /** + * Callback function called after a successful action execution. + */ + onExecuted?: () => void +} + +export const UNDO_REDO_SHORTCUT_KEYS: Record = { + undo: "mod+z", + redo: "mod+shift+z", +} + +export const historyActionLabels: Record = { + undo: "Undo", + redo: "Redo", +} + +export const historyIcons = { + undo: Undo2Icon, + redo: Redo2Icon, +} + +/** + * Checks if a history action can be executed + */ +export function canExecuteUndoRedoAction( + editor: Editor | null, + action: UndoRedoAction +): boolean { + if (!editor || !editor.isEditable) return false + if (isNodeTypeSelected(editor, ["image"])) return false + + return action === "undo" ? editor.can().undo() : editor.can().redo() +} + +/** + * Executes a history action on the editor + */ +export function executeUndoRedoAction( + editor: Editor | null, + action: UndoRedoAction +): boolean { + if (!editor || !editor.isEditable) return false + if (!canExecuteUndoRedoAction(editor, action)) return false + + const chain = editor.chain().focus() + return action === "undo" ? chain.undo().run() : chain.redo().run() +} + +/** + * Determines if the history button should be shown + */ +export function shouldShowButton(props: { + editor: Editor | null + hideWhenUnavailable: boolean + action: UndoRedoAction +}): boolean { + const { editor, hideWhenUnavailable, action } = props + + if (!editor || !editor.isEditable) return false + + if (hideWhenUnavailable && !editor.isActive("code")) { + return canExecuteUndoRedoAction(editor, action) + } + + return true +} + +/** + * Custom hook that provides history functionality for Tiptap editor + * + * @example + * ```tsx + * // Simple usage + * function MySimpleUndoButton() { + * const { isVisible, handleAction } = useHistory({ action: "undo" }) + * + * if (!isVisible) return null + * + * return + * } + * + * // Advanced usage with configuration + * function MyAdvancedRedoButton() { + * const { isVisible, handleAction, label } = useHistory({ + * editor: myEditor, + * action: "redo", + * hideWhenUnavailable: true, + * onExecuted: () => console.log('Action executed!') + * }) + * + * if (!isVisible) return null + * + * return ( + * + * Redo + * + * ) + * } + * ``` + */ +export function useUndoRedo(config: UseUndoRedoConfig) { + const { + editor: providedEditor, + action, + hideWhenUnavailable = false, + onExecuted, + } = config + + const { editor } = useTiptapEditor(providedEditor) + const [isVisible, setIsVisible] = useState(true) + const canExecute = canExecuteUndoRedoAction(editor, action) + + useEffect(() => { + if (!editor) return + + const handleUpdate = () => { + setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, action })) + } + + handleUpdate() + + editor.on("transaction", handleUpdate) + + return () => { + editor.off("transaction", handleUpdate) + } + }, [editor, hideWhenUnavailable, action]) + + const handleAction = useCallback(() => { + if (!editor) return false + + const success = executeUndoRedoAction(editor, action) + if (success) { + onExecuted?.() + } + return success + }, [editor, action, onExecuted]) + + return { + isVisible, + handleAction, + canExecute, + label: historyActionLabels[action], + shortcutKeys: UNDO_REDO_SHORTCUT_KEYS[action], + Icon: historyIcons[action], + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts new file mode 100644 index 0000000000..30745b8f98 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts @@ -0,0 +1,47 @@ +"use client" + +import { useCallback, useRef } from "react" + +// basically Exclude["ref"], string> +type UserRef = + | ((instance: T | null) => void) + | React.RefObject + | null + | undefined + +const updateRef = (ref: NonNullable>, value: T | null) => { + if (typeof ref === "function") { + ref(value) + } else if (ref && typeof ref === "object" && "current" in ref) { + // Safe assignment without MutableRefObject + ;(ref as { current: T | null }).current = value + } +} + +export const useComposedRef = ( + libRef: React.RefObject, + userRef: UserRef +) => { + const prevUserRef = useRef>(null) + + return useCallback( + (instance: T | null) => { + if (libRef && "current" in libRef) { + ;(libRef as { current: T | null }).current = instance + } + + if (prevUserRef.current) { + updateRef(prevUserRef.current, null) + } + + prevUserRef.current = userRef + + if (userRef) { + updateRef(userRef, instance) + } + }, + [libRef, userRef] + ) +} + +export default useComposedRef diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts new file mode 100644 index 0000000000..19668d8b2f --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts @@ -0,0 +1,69 @@ +import type { Editor } from "@tiptap/react" +import { useWindowSize } from "@/hooks/use-window-size" +import { useBodyRect } from "@/hooks/use-element-rect" +import { useEffect } from "react" + +export interface CursorVisibilityOptions { + /** + * The Tiptap editor instance + */ + editor?: Editor | null + /** + * Reference to the toolbar element that may obscure the cursor + */ + overlayHeight?: number +} + +/** + * Custom hook that ensures the cursor remains visible when typing in a Tiptap editor. + * Automatically scrolls the window when the cursor would be hidden by the toolbar. + * + * @param options.editor The Tiptap editor instance + * @param options.overlayHeight Toolbar height to account for + * @returns The bounding rect of the body + */ +export function useCursorVisibility({ + editor, + overlayHeight = 0, +}: CursorVisibilityOptions) { + const { height: windowHeight } = useWindowSize() + const rect = useBodyRect({ + enabled: true, + throttleMs: 100, + useResizeObserver: true, + }) + + useEffect(() => { + const ensureCursorVisibility = () => { + if (!editor) return + + const { state, view } = editor + if (!view.hasFocus()) return + + // Get current cursor position coordinates + const { from } = state.selection + const cursorCoords = view.coordsAtPos(from) + + if (windowHeight < rect.height && cursorCoords) { + const availableSpace = windowHeight - cursorCoords.top + + // If the cursor is hidden behind the overlay or offscreen, scroll it into view + if (availableSpace < overlayHeight) { + const targetCursorY = Math.max(windowHeight / 2, overlayHeight) + const currentScrollY = window.scrollY + const cursorAbsoluteY = cursorCoords.top + currentScrollY + const newScrollY = cursorAbsoluteY - targetCursorY + + window.scrollTo({ + top: Math.max(0, newScrollY), + behavior: "smooth", + }) + } + } + } + + ensureCursorVisibility() + }, [editor, overlayHeight, windowHeight, rect.height]) + + return rect +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts new file mode 100644 index 0000000000..55ba9bd1c9 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts @@ -0,0 +1,166 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { useThrottledCallback } from "@/hooks/use-throttled-callback" + +export type RectState = Omit + +export interface ElementRectOptions { + /** + * The element to track. Can be an Element, ref, or selector string. + * Defaults to document.body if not provided. + */ + element?: Element | React.RefObject | string | null + /** + * Whether to enable rect tracking + */ + enabled?: boolean + /** + * Throttle delay in milliseconds for rect updates + */ + throttleMs?: number + /** + * Whether to use ResizeObserver for more accurate tracking + */ + useResizeObserver?: boolean +} + +const initialRect: RectState = { + x: 0, + y: 0, + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, +} + +const isSSR = typeof window === "undefined" +const hasResizeObserver = !isSSR && typeof ResizeObserver !== "undefined" + +/** + * Helper function to check if code is running on client side + */ +const isClientSide = (): boolean => !isSSR + +/** + * Custom hook that tracks an element's bounding rectangle and updates on resize, scroll, etc. + * + * @param options Configuration options for element rect tracking + * @returns The current bounding rectangle of the element + */ +export function useElementRect({ + element, + enabled = true, + throttleMs = 100, + useResizeObserver = true, +}: ElementRectOptions = {}): RectState { + const [rect, setRect] = useState(initialRect) + + const getTargetElement = useCallback((): Element | null => { + if (!enabled || !isClientSide()) return null + + if (!element) { + return document.body + } + + if (typeof element === "string") { + return document.querySelector(element) + } + + if ("current" in element) { + return element.current + } + + return element + }, [element, enabled]) + + const updateRect = useThrottledCallback( + () => { + if (!enabled || !isClientSide()) return + + const targetElement = getTargetElement() + if (!targetElement) { + setRect(initialRect) + return + } + + const newRect = targetElement.getBoundingClientRect() + setRect({ + x: newRect.x, + y: newRect.y, + width: newRect.width, + height: newRect.height, + top: newRect.top, + right: newRect.right, + bottom: newRect.bottom, + left: newRect.left, + }) + }, + throttleMs, + [enabled, getTargetElement], + { leading: true, trailing: true } + ) + + useEffect(() => { + if (!enabled || !isClientSide()) { + setRect(initialRect) + return + } + + const targetElement = getTargetElement() + if (!targetElement) return + + updateRect() + + const cleanup: (() => void)[] = [] + + if (useResizeObserver && hasResizeObserver) { + const resizeObserver = new ResizeObserver(() => { + window.requestAnimationFrame(updateRect) + }) + resizeObserver.observe(targetElement) + cleanup.push(() => resizeObserver.disconnect()) + } + + const handleUpdate = () => updateRect() + + window.addEventListener("scroll", handleUpdate, true) + window.addEventListener("resize", handleUpdate, true) + + cleanup.push(() => { + window.removeEventListener("scroll", handleUpdate) + window.removeEventListener("resize", handleUpdate) + }) + + return () => { + cleanup.forEach((fn) => fn()) + setRect(initialRect) + } + }, [enabled, getTargetElement, updateRect, useResizeObserver]) + + return rect +} + +/** + * Convenience hook for tracking document.body rect + */ +export function useBodyRect( + options: Omit = {} +): RectState { + return useElementRect({ + ...options, + element: isClientSide() ? document.body : null, + }) +} + +/** + * Convenience hook for tracking a ref element's rect + */ +export function useRefRect( + ref: React.RefObject, + options: Omit = {} +): RectState { + return useElementRect({ ...options, element: ref }) +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts new file mode 100644 index 0000000000..58dfd4b3f6 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts @@ -0,0 +1,194 @@ +import type { Editor } from "@tiptap/react" +import { useEffect, useState } from "react" + +type Orientation = "horizontal" | "vertical" | "both" + +interface MenuNavigationOptions { + /** + * The Tiptap editor instance, if using with a Tiptap editor. + */ + editor?: Editor | null + /** + * Reference to the container element for handling keyboard events. + */ + containerRef?: React.RefObject + /** + * Search query that affects the selected item. + */ + query?: string + /** + * Array of items to navigate through. + */ + items: T[] + /** + * Callback fired when an item is selected. + */ + onSelect?: (item: T) => void + /** + * Callback fired when the menu should close. + */ + onClose?: () => void + /** + * The navigation orientation of the menu. + * @default "vertical" + */ + orientation?: Orientation + /** + * Whether to automatically select the first item when the menu opens. + * @default true + */ + autoSelectFirstItem?: boolean +} + +/** + * Hook that implements keyboard navigation for dropdown menus and command palettes. + * + * Handles arrow keys, tab, home/end, enter for selection, and escape to close. + * Works with both Tiptap editors and regular DOM elements. + * + * @param options - Configuration options for the menu navigation + * @returns Object containing the selected index and a setter function + */ +export function useMenuNavigation({ + editor, + containerRef, + query, + items, + onSelect, + onClose, + orientation = "vertical", + autoSelectFirstItem = true, +}: MenuNavigationOptions) { + const [selectedIndex, setSelectedIndex] = useState( + autoSelectFirstItem ? 0 : -1 + ) + + useEffect(() => { + const handleKeyboardNavigation = (event: KeyboardEvent) => { + if (!items.length) return false + + const moveNext = () => + setSelectedIndex((currentIndex) => { + if (currentIndex === -1) return 0 + return (currentIndex + 1) % items.length + }) + + const movePrev = () => + setSelectedIndex((currentIndex) => { + if (currentIndex === -1) return items.length - 1 + return (currentIndex - 1 + items.length) % items.length + }) + + switch (event.key) { + case "ArrowUp": { + if (orientation === "horizontal") return false + event.preventDefault() + movePrev() + return true + } + + case "ArrowDown": { + if (orientation === "horizontal") return false + event.preventDefault() + moveNext() + return true + } + + case "ArrowLeft": { + if (orientation === "vertical") return false + event.preventDefault() + movePrev() + return true + } + + case "ArrowRight": { + if (orientation === "vertical") return false + event.preventDefault() + moveNext() + return true + } + + case "Tab": { + event.preventDefault() + if (event.shiftKey) { + movePrev() + } else { + moveNext() + } + return true + } + + case "Home": { + event.preventDefault() + setSelectedIndex(0) + return true + } + + case "End": { + event.preventDefault() + setSelectedIndex(items.length - 1) + return true + } + + case "Enter": { + if (event.isComposing) return false + event.preventDefault() + if (selectedIndex !== -1 && items[selectedIndex]) { + onSelect?.(items[selectedIndex]) + } + return true + } + + case "Escape": { + event.preventDefault() + onClose?.() + return true + } + + default: + return false + } + } + + let targetElement: HTMLElement | null = null + + if (editor) { + targetElement = editor.view.dom + } else if (containerRef?.current) { + targetElement = containerRef.current + } + + if (targetElement) { + targetElement.addEventListener("keydown", handleKeyboardNavigation, true) + + return () => { + targetElement?.removeEventListener( + "keydown", + handleKeyboardNavigation, + true + ) + } + } + + return undefined + }, [ + editor, + containerRef, + items, + selectedIndex, + onSelect, + onClose, + orientation, + ]) + + useEffect(() => { + if (query) { + setSelectedIndex(autoSelectFirstItem ? 0 : -1) + } + }, [query, autoSelectFirstItem]) + + return { + selectedIndex: items.length ? selectedIndex : undefined, + setSelectedIndex, + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-mobile.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-mobile.ts new file mode 100644 index 0000000000..8088e59bb9 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-mobile.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react" + +export function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(undefined) + + useEffect(() => { + const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < breakpoint) + } + mql.addEventListener("change", onChange) + setIsMobile(window.innerWidth < breakpoint) + return () => mql.removeEventListener("change", onChange) + }, [breakpoint]) + + return !!isMobile +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts new file mode 100644 index 0000000000..8a5fb35db1 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts @@ -0,0 +1,75 @@ +import type { RefObject } from "react" +import { useEffect, useState } from "react" + +type ScrollTarget = RefObject | Window | null | undefined +type EventTargetWithScroll = Window | HTMLElement | Document + +interface UseScrollingOptions { + debounce?: number + fallbackToDocument?: boolean +} + +export function useScrolling( + target?: ScrollTarget, + options: UseScrollingOptions = {} +): boolean { + const { debounce = 150, fallbackToDocument = true } = options + const [isScrolling, setIsScrolling] = useState(false) + + useEffect(() => { + // Resolve element or window + const element: EventTargetWithScroll = + target && typeof Window !== "undefined" && target instanceof Window + ? target + : ((target as RefObject)?.current ?? window) + + // Mobile: fallback to document when using window + const eventTarget: EventTargetWithScroll = + fallbackToDocument && + element === window && + typeof document !== "undefined" + ? document + : element + + const on = ( + el: EventTargetWithScroll, + event: string, + handler: EventListener + ) => el.addEventListener(event, handler, true) + + const off = ( + el: EventTargetWithScroll, + event: string, + handler: EventListener + ) => el.removeEventListener(event, handler) + + let timeout: ReturnType + const supportsScrollEnd = element === window && "onscrollend" in window + + const handleScroll: EventListener = () => { + if (!isScrolling) setIsScrolling(true) + + if (!supportsScrollEnd) { + clearTimeout(timeout) + timeout = setTimeout(() => setIsScrolling(false), debounce) + } + } + + const handleScrollEnd: EventListener = () => setIsScrolling(false) + + on(eventTarget, "scroll", handleScroll) + if (supportsScrollEnd) { + on(eventTarget, "scrollend", handleScrollEnd) + } + + return () => { + off(eventTarget, "scroll", handleScroll) + if (supportsScrollEnd) { + off(eventTarget, "scrollend", handleScrollEnd) + } + clearTimeout(timeout) + } + }, [target, debounce, fallbackToDocument, isScrolling]) + + return isScrolling +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts new file mode 100644 index 0000000000..54894cf2e5 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts @@ -0,0 +1,48 @@ +import throttle from "lodash.throttle" + +import { useUnmount } from "@/hooks/use-unmount" +import { useMemo } from "react" + +interface ThrottleSettings { + leading?: boolean | undefined + trailing?: boolean | undefined +} + +const defaultOptions: ThrottleSettings = { + leading: false, + trailing: true, +} + +/** + * A hook that returns a throttled callback function. + * + * @param fn The function to throttle + * @param wait The time in ms to wait before calling the function + * @param dependencies The dependencies to watch for changes + * @param options The throttle options + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useThrottledCallback any>( + fn: T, + wait = 250, + dependencies: React.DependencyList = [], + options: ThrottleSettings = defaultOptions +): { + (this: ThisParameterType, ...args: Parameters): ReturnType + cancel: () => void + flush: () => void +} { + const handler = useMemo( + () => throttle(fn, wait, options), + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencies + ) + + useUnmount(() => { + handler.cancel() + }) + + return handler +} + +export default useThrottledCallback diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts new file mode 100644 index 0000000000..44044ccb2e --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts @@ -0,0 +1,47 @@ +import type { Editor } from "@tiptap/react" +import { useCurrentEditor, useEditorState } from "@tiptap/react" +import { useMemo } from "react" + +/** + * Hook that provides access to a Tiptap editor instance. + * + * Accepts an optional editor instance directly, or falls back to retrieving + * the editor from the Tiptap context if available. This allows components + * to work both when given an editor directly and when used within a Tiptap + * editor context. + * + * @param providedEditor - Optional editor instance to use instead of the context editor + * @returns The provided editor or the editor from context, whichever is available + */ +export function useTiptapEditor(providedEditor?: Editor | null): { + editor: Editor | null + editorState?: Editor["state"] + canCommand?: Editor["can"] +} { + const { editor: coreEditor } = useCurrentEditor() + const mainEditor = useMemo( + () => providedEditor || coreEditor, + [providedEditor, coreEditor] + ) + + const editorState = useEditorState({ + editor: mainEditor, + selector(context) { + if (!context.editor) { + return { + editor: null, + editorState: undefined, + canCommand: undefined, + } + } + + return { + editor: context.editor, + editorState: context.editor.state, + canCommand: context.editor.can, + } + }, + }) + + return editorState || { editor: null } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts new file mode 100644 index 0000000000..91a22e7132 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts @@ -0,0 +1,21 @@ +import { useRef, useEffect } from "react" + +/** + * Hook that executes a callback when the component unmounts. + * + * @param callback Function to be called on component unmount + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const useUnmount = (callback: (...args: Array) => any) => { + const ref = useRef(callback) + ref.current = callback + + useEffect( + () => () => { + ref.current() + }, + [] + ) +} + +export default useUnmount diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts new file mode 100644 index 0000000000..dd211d9aac --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts @@ -0,0 +1,93 @@ +"use client" + +import { useEffect, useState } from "react" +import { useThrottledCallback } from "@/hooks/use-throttled-callback" + +export interface WindowSizeState { + /** + * The width of the window's visual viewport in pixels. + */ + width: number + /** + * The height of the window's visual viewport in pixels. + */ + height: number + /** + * The distance from the top of the visual viewport to the top of the layout viewport. + * Particularly useful for handling mobile keyboard appearance. + */ + offsetTop: number + /** + * The distance from the left of the visual viewport to the left of the layout viewport. + */ + offsetLeft: number + /** + * The scale factor of the visual viewport. + * This is useful for scaling elements based on the current zoom level. + */ + scale: number +} + +/** + * Hook that tracks the window's visual viewport dimensions, position, and provides + * a CSS transform for positioning elements. + * + * Uses the Visual Viewport API to get accurate measurements, especially important + * for mobile devices where virtual keyboards can change the visible area. + * Only updates state when values actually change to optimize performance. + * + * @returns An object containing viewport properties and a CSS transform string + */ +export function useWindowSize(): WindowSizeState { + const [windowSize, setWindowSize] = useState({ + width: 0, + height: 0, + offsetTop: 0, + offsetLeft: 0, + scale: 0, + }) + + const handleViewportChange = useThrottledCallback(() => { + if (typeof window === "undefined") return + + const vp = window.visualViewport + if (!vp) return + + const { + width = 0, + height = 0, + offsetTop = 0, + offsetLeft = 0, + scale = 0, + } = vp + + setWindowSize((prevState) => { + if ( + width === prevState.width && + height === prevState.height && + offsetTop === prevState.offsetTop && + offsetLeft === prevState.offsetLeft && + scale === prevState.scale + ) { + return prevState + } + + return { width, height, offsetTop, offsetLeft, scale } + }) + }, 200) + + useEffect(() => { + const visualViewport = window.visualViewport + if (!visualViewport) return + + visualViewport.addEventListener("resize", handleViewportChange) + + handleViewportChange() + + return () => { + visualViewport.removeEventListener("resize", handleViewportChange) + } + }, [handleViewportChange]) + + return windowSize +} diff --git a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts new file mode 100644 index 0000000000..e57c245c8e --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts @@ -0,0 +1,554 @@ +import type { Node as TiptapNode } from "@tiptap/pm/model" +import type { Transaction } from "@tiptap/pm/state" +import { + AllSelection, + NodeSelection, + Selection, + TextSelection, +} from "@tiptap/pm/state" +import type { Editor, NodeWithPos } from "@tiptap/react" + +export const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB + +export const MAC_SYMBOLS: Record = { + mod: "⌘", + command: "⌘", + meta: "⌘", + ctrl: "⌃", + control: "⌃", + alt: "⌥", + option: "⌥", + shift: "⇧", + backspace: "Del", + delete: "⌦", + enter: "⏎", + escape: "⎋", + capslock: "⇪", +} as const + +export const SR_ONLY = { + position: "absolute", + width: "1px", + height: "1px", + padding: 0, + margin: "-1px", + overflow: "hidden", + clip: "rect(0, 0, 0, 0)", + whiteSpace: "nowrap", + borderWidth: 0, +} as const + +export function cn( + ...classes: (string | boolean | undefined | null)[] +): string { + return classes.filter(Boolean).join(" ") +} + +/** + * Determines if the current platform is macOS + * @returns boolean indicating if the current platform is Mac + */ +export function isMac(): boolean { + return ( + typeof navigator !== "undefined" && + navigator.platform.toLowerCase().includes("mac") + ) +} + +/** + * Formats a shortcut key based on the platform (Mac or non-Mac) + * @param key - The key to format (e.g., "ctrl", "alt", "shift") + * @param isMac - Boolean indicating if the platform is Mac + * @param capitalize - Whether to capitalize the key (default: true) + * @returns Formatted shortcut key symbol + */ +export const formatShortcutKey = ( + key: string, + isMac: boolean, + capitalize: boolean = true +) => { + if (isMac) { + const lowerKey = key.toLowerCase() + return MAC_SYMBOLS[lowerKey] || (capitalize ? key.toUpperCase() : key) + } + + return capitalize ? key.charAt(0).toUpperCase() + key.slice(1) : key +} + +/** + * Parses a shortcut key string into an array of formatted key symbols + * @param shortcutKeys - The string of shortcut keys (e.g., "ctrl-alt-shift") + * @param delimiter - The delimiter used to split the keys (default: "-") + * @param capitalize - Whether to capitalize the keys (default: true) + * @returns Array of formatted shortcut key symbols + */ +export const parseShortcutKeys = (props: { + shortcutKeys: string | undefined + delimiter?: string + capitalize?: boolean +}) => { + const { shortcutKeys, delimiter = "+", capitalize = true } = props + + if (!shortcutKeys) return [] + + return shortcutKeys + .split(delimiter) + .map((key) => key.trim()) + .map((key) => formatShortcutKey(key, isMac(), capitalize)) +} + +/** + * Checks if a mark exists in the editor schema + * @param markName - The name of the mark to check + * @param editor - The editor instance + * @returns boolean indicating if the mark exists in the schema + */ +export const isMarkInSchema = ( + markName: string, + editor: Editor | null +): boolean => { + if (!editor?.schema) return false + return editor.schema.spec.marks.get(markName) !== undefined +} + +/** + * Checks if a node exists in the editor schema + * @param nodeName - The name of the node to check + * @param editor - The editor instance + * @returns boolean indicating if the node exists in the schema + */ +export const isNodeInSchema = ( + nodeName: string, + editor: Editor | null +): boolean => { + if (!editor?.schema) return false + return editor.schema.spec.nodes.get(nodeName) !== undefined +} + +/** + * Moves the focus to the next node in the editor + * @param editor - The editor instance + * @returns boolean indicating if the focus was moved + */ +export function focusNextNode(editor: Editor) { + const { state, view } = editor + const { doc, selection } = state + + const nextSel = Selection.findFrom(selection.$to, 1, true) + if (nextSel) { + view.dispatch(state.tr.setSelection(nextSel).scrollIntoView()) + return true + } + + const paragraphType = state.schema.nodes.paragraph + if (!paragraphType) { + console.warn("No paragraph node type found in schema.") + return false + } + + const end = doc.content.size + const para = paragraphType.create() + let tr = state.tr.insert(end, para) + + // Place the selection inside the new paragraph + const $inside = tr.doc.resolve(end + 1) + tr = tr.setSelection(TextSelection.near($inside)).scrollIntoView() + view.dispatch(tr) + return true +} + +/** + * Checks if a value is a valid number (not null, undefined, or NaN) + * @param value - The value to check + * @returns boolean indicating if the value is a valid number + */ +export function isValidPosition(pos: number | null | undefined): pos is number { + return typeof pos === "number" && pos >= 0 +} + +/** + * Checks if one or more extensions are registered in the Tiptap editor. + * @param editor - The Tiptap editor instance + * @param extensionNames - A single extension name or an array of names to check + * @returns True if at least one of the extensions is available, false otherwise + */ +export function isExtensionAvailable( + editor: Editor | null, + extensionNames: string | string[] +): boolean { + if (!editor) return false + + const names = Array.isArray(extensionNames) + ? extensionNames + : [extensionNames] + + const found = names.some((name) => + editor.extensionManager.extensions.some((ext) => ext.name === name) + ) + + if (!found) { + console.warn( + `None of the extensions [${names.join(", ")}] were found in the editor schema. Ensure they are included in the editor configuration.` + ) + } + + return found +} + +/** + * Finds a node at the specified position with error handling + * @param editor The Tiptap editor instance + * @param position The position in the document to find the node + * @returns The node at the specified position, or null if not found + */ +export function findNodeAtPosition(editor: Editor, position: number) { + try { + const node = editor.state.doc.nodeAt(position) + if (!node) { + console.warn(`No node found at position ${position}`) + return null + } + return node + } catch (error) { + console.error(`Error getting node at position ${position}:`, error) + return null + } +} + +/** + * Finds the position and instance of a node in the document + * @param props Object containing editor, node (optional), and nodePos (optional) + * @param props.editor The Tiptap editor instance + * @param props.node The node to find (optional if nodePos is provided) + * @param props.nodePos The position of the node to find (optional if node is provided) + * @returns An object with the position and node, or null if not found + */ +export function findNodePosition(props: { + editor: Editor | null + node?: TiptapNode | null + nodePos?: number | null +}): { pos: number; node: TiptapNode } | null { + const { editor, node, nodePos } = props + + if (!editor || !editor.state?.doc) return null + + // Zero is valid position + const hasValidNode = node !== undefined && node !== null + const hasValidPos = isValidPosition(nodePos) + + if (!hasValidNode && !hasValidPos) { + return null + } + + // First search for the node in the document if we have a node + if (hasValidNode) { + let foundPos = -1 + let foundNode: TiptapNode | null = null + + editor.state.doc.descendants((currentNode, pos) => { + // TODO: Needed? + // if (currentNode.type && currentNode.type.name === node!.type.name) { + if (currentNode === node) { + foundPos = pos + foundNode = currentNode + return false + } + return true + }) + + if (foundPos !== -1 && foundNode !== null) { + return { pos: foundPos, node: foundNode } + } + } + + // If we have a valid position, use findNodeAtPosition + if (hasValidPos) { + const nodeAtPos = findNodeAtPosition(editor, nodePos!) + if (nodeAtPos) { + return { pos: nodePos!, node: nodeAtPos } + } + } + + return null +} + +/** + * Determines whether the current selection contains a node whose type matches + * any of the provided node type names. + * @param editor Tiptap editor instance + * @param nodeTypeNames List of node type names to match against + * @param checkAncestorNodes Whether to check ancestor node types up the depth chain + */ +export function isNodeTypeSelected( + editor: Editor | null, + nodeTypeNames: string[] = [], + checkAncestorNodes: boolean = false +): boolean { + if (!editor || !editor.state.selection) return false + + const { selection } = editor.state + if (selection.empty) return false + + // Direct node selection check + if (selection instanceof NodeSelection) { + const selectedNode = selection.node + return selectedNode ? nodeTypeNames.includes(selectedNode.type.name) : false + } + + // Depth-based ancestor node check + if (checkAncestorNodes) { + const { $from } = selection + for (let depth = $from.depth; depth > 0; depth--) { + const ancestorNode = $from.node(depth) + if (nodeTypeNames.includes(ancestorNode.type.name)) { + return true + } + } + } + + return false +} + +/** + * Check whether the current selection is fully within nodes + * whose type names are in the provided `types` list. + * + * - NodeSelection → checks the selected node. + * - Text/AllSelection → ensures all textblocks within [from, to) are allowed. + */ +export function selectionWithinConvertibleTypes( + editor: Editor, + types: string[] = [] +): boolean { + if (!editor || types.length === 0) return false + + const { state } = editor + const { selection } = state + const allowed = new Set(types) + + if (selection instanceof NodeSelection) { + const nodeType = selection.node?.type?.name + return !!nodeType && allowed.has(nodeType) + } + + if (selection instanceof TextSelection || selection instanceof AllSelection) { + let valid = true + state.doc.nodesBetween(selection.from, selection.to, (node) => { + if (node.isTextblock && !allowed.has(node.type.name)) { + valid = false + return false // stop early + } + return valid + }) + return valid + } + + return false +} + +/** + * Handles image upload with progress tracking and abort capability + * @param file The file to upload + * @param onProgress Optional callback for tracking upload progress + * @param abortSignal Optional AbortSignal for cancelling the upload + * @returns Promise resolving to the URL of the uploaded image + */ +export const handleImageUpload = async ( + file: File, + onProgress?: (event: { progress: number }) => void, + abortSignal?: AbortSignal +): Promise => { + // Validate file + if (!file) { + throw new Error("No file provided") + } + + if (file.size > MAX_FILE_SIZE) { + throw new Error( + `File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)` + ) + } + + // For demo/testing: Simulate upload progress. In production, replace the following code + // with your own upload implementation. + for (let progress = 0; progress <= 100; progress += 10) { + if (abortSignal?.aborted) { + throw new Error("Upload cancelled") + } + await new Promise((resolve) => setTimeout(resolve, 500)) + onProgress?.({ progress }) + } + + return "/images/tiptap-ui-placeholder-image.jpg" +} + +type ProtocolOptions = { + /** + * The protocol scheme to be registered. + * @default ''' + * @example 'ftp' + * @example 'git' + */ + scheme: string + + /** + * If enabled, it allows optional slashes after the protocol. + * @default false + * @example true + */ + optionalSlashes?: boolean +} + +type ProtocolConfig = Array + +const ATTR_WHITESPACE = + // eslint-disable-next-line no-control-regex + /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g + +export function isAllowedUri( + uri: string | undefined, + protocols?: ProtocolConfig +) { + const allowedProtocols: string[] = [ + "http", + "https", + "ftp", + "ftps", + "mailto", + "tel", + "callto", + "sms", + "cid", + "xmpp", + ] + + if (protocols) { + protocols.forEach((protocol) => { + const nextProtocol = + typeof protocol === "string" ? protocol : protocol.scheme + + if (nextProtocol) { + allowedProtocols.push(nextProtocol) + } + }) + } + + return ( + !uri || + uri.replace(ATTR_WHITESPACE, "").match( + new RegExp( + // eslint-disable-next-line no-useless-escape + `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))`, + "i" + ) + ) + ) +} + +export function sanitizeUrl( + inputUrl: string, + baseUrl: string, + protocols?: ProtocolConfig +): string { + try { + const url = new URL(inputUrl, baseUrl) + + if (isAllowedUri(url.href, protocols)) { + return url.href + } + } catch { + // If URL creation fails, it's considered invalid + } + return "#" +} + +/** + * Update a single attribute on multiple nodes. + * + * @param tr - The transaction to mutate + * @param targets - Array of { node, pos } + * @param attrName - Attribute key to update + * @param next - New value OR updater function receiving previous value + * Pass `undefined` to remove the attribute. + * @returns true if at least one node was updated, false otherwise + */ +export function updateNodesAttr( + tr: Transaction, + targets: readonly NodeWithPos[], + attrName: A, + next: V | ((prev: V | undefined) => V | undefined) +): boolean { + if (!targets.length) return false + + let changed = false + + for (const { pos } of targets) { + // Always re-read from the transaction's current doc + const currentNode = tr.doc.nodeAt(pos) + if (!currentNode) continue + + const prevValue = (currentNode.attrs as Record)[ + attrName + ] as V | undefined + const resolvedNext = + typeof next === "function" + ? (next as (p: V | undefined) => V | undefined)(prevValue) + : next + + if (prevValue === resolvedNext) continue + + const nextAttrs: Record = { ...currentNode.attrs } + if (resolvedNext === undefined) { + // Remove the key entirely instead of setting null + delete nextAttrs[attrName] + } else { + nextAttrs[attrName] = resolvedNext + } + + tr.setNodeMarkup(pos, undefined, nextAttrs) + changed = true + } + + return changed +} + +/** + * Selects the entire content of the current block node if the selection is empty. + * If the selection is not empty, it does nothing. + * @param editor The Tiptap editor instance + */ +export function selectCurrentBlockContent(editor: Editor) { + const { selection, doc } = editor.state + + if (!selection.empty) return + + const $pos = selection.$from + let blockNode = null + let blockPos = -1 + + for (let depth = $pos.depth; depth >= 0; depth--) { + const node = $pos.node(depth) + const pos = $pos.start(depth) + + if (node.isBlock && node.textContent.trim()) { + blockNode = node + blockPos = pos + break + } + } + + if (blockNode && blockPos >= 0) { + const from = blockPos + const to = blockPos + blockNode.nodeSize - 2 // -2 to exclude the closing tag + + if (from < to) { + const $from = doc.resolve(from) + const $to = doc.resolve(to) + const newSelection = TextSelection.between($from, $to, 1) + + if (newSelection && !selection.eq(newSelection)) { + editor.view.dispatch(editor.state.tr.setSelection(newSelection)) + } + } + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/styles/_keyframe-animations.scss b/frontends/ol-components/src/components/TiptapEditor/styles/_keyframe-animations.scss new file mode 100644 index 0000000000..dd98b7cbc6 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/styles/_keyframe-animations.scss @@ -0,0 +1,91 @@ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes zoomIn { + from { + transform: scale(0.95); + } + to { + transform: scale(1); + } +} + +@keyframes zoomOut { + from { + transform: scale(1); + } + to { + transform: scale(0.95); + } +} + +@keyframes zoom { + 0% { + opacity: 0; + transform: scale(0.95); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideFromTop { + from { + transform: translateY(-0.5rem); + } + to { + transform: translateY(0); + } +} + +@keyframes slideFromRight { + from { + transform: translateX(0.5rem); + } + to { + transform: translateX(0); + } +} + +@keyframes slideFromLeft { + from { + transform: translateX(-0.5rem); + } + to { + transform: translateX(0); + } +} + +@keyframes slideFromBottom { + from { + transform: translateY(0.5rem); + } + to { + transform: translateY(0); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss b/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss new file mode 100644 index 0000000000..113a16b404 --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss @@ -0,0 +1,296 @@ +:root { + /****************** + Basics + ******************/ + + overflow-wrap: break-word; + text-size-adjust: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /****************** + Colors variables + ******************/ + + /* Gray alpha (light mode) */ + --tt-gray-light-a-50: rgba(56, 56, 56, 0.04); + --tt-gray-light-a-100: rgba(15, 22, 36, 0.05); + --tt-gray-light-a-200: rgba(37, 39, 45, 0.1); + --tt-gray-light-a-300: rgba(47, 50, 55, 0.2); + --tt-gray-light-a-400: rgba(40, 44, 51, 0.42); + --tt-gray-light-a-500: rgba(52, 55, 60, 0.64); + --tt-gray-light-a-600: rgba(36, 39, 46, 0.78); + --tt-gray-light-a-700: rgba(35, 37, 42, 0.87); + --tt-gray-light-a-800: rgba(30, 32, 36, 0.95); + --tt-gray-light-a-900: rgba(29, 30, 32, 0.98); + + /* Gray (light mode) */ + --tt-gray-light-50: rgba(250, 250, 250, 1); + --tt-gray-light-100: rgba(244, 244, 245, 1); + --tt-gray-light-200: rgba(234, 234, 235, 1); + --tt-gray-light-300: rgba(213, 214, 215, 1); + --tt-gray-light-400: rgba(166, 167, 171, 1); + --tt-gray-light-500: rgba(125, 127, 130, 1); + --tt-gray-light-600: rgba(83, 86, 90, 1); + --tt-gray-light-700: rgba(64, 65, 69, 1); + --tt-gray-light-800: rgba(44, 45, 48, 1); + --tt-gray-light-900: rgba(34, 35, 37, 1); + + /* Gray alpha (dark mode) */ + --tt-gray-dark-a-50: rgba(232, 232, 253, 0.05); + --tt-gray-dark-a-100: rgba(231, 231, 243, 0.07); + --tt-gray-dark-a-200: rgba(238, 238, 246, 0.11); + --tt-gray-dark-a-300: rgba(239, 239, 245, 0.22); + --tt-gray-dark-a-400: rgba(244, 244, 255, 0.37); + --tt-gray-dark-a-500: rgba(236, 238, 253, 0.5); + --tt-gray-dark-a-600: rgba(247, 247, 253, 0.64); + --tt-gray-dark-a-700: rgba(251, 251, 254, 0.75); + --tt-gray-dark-a-800: rgba(253, 253, 253, 0.88); + --tt-gray-dark-a-900: rgba(255, 255, 255, 0.96); + + /* Gray (dark mode) */ + --tt-gray-dark-50: rgba(25, 25, 26, 1); + --tt-gray-dark-100: rgba(32, 32, 34, 1); + --tt-gray-dark-200: rgba(45, 45, 47, 1); + --tt-gray-dark-300: rgba(70, 70, 73, 1); + --tt-gray-dark-400: rgba(99, 99, 105, 1); + --tt-gray-dark-500: rgba(124, 124, 131, 1); + --tt-gray-dark-600: rgba(163, 163, 168, 1); + --tt-gray-dark-700: rgba(192, 192, 195, 1); + --tt-gray-dark-800: rgba(224, 224, 225, 1); + --tt-gray-dark-900: rgba(245, 245, 245, 1); + + /* Brand colors */ + --tt-brand-color-50: rgba(239, 238, 255, 1); + --tt-brand-color-100: rgba(222, 219, 255, 1); + --tt-brand-color-200: rgba(195, 189, 255, 1); + --tt-brand-color-300: rgba(157, 138, 255, 1); + --tt-brand-color-400: rgba(122, 82, 255, 1); + --tt-brand-color-500: rgba(98, 41, 255, 1); + --tt-brand-color-600: rgba(84, 0, 229, 1); + --tt-brand-color-700: rgba(75, 0, 204, 1); + --tt-brand-color-800: rgba(56, 0, 153, 1); + --tt-brand-color-900: rgba(43, 25, 102, 1); + --tt-brand-color-950: hsla(257, 100%, 9%, 1); + + /* Green */ + --tt-color-green-inc-5: hsla(129, 100%, 97%, 1); + --tt-color-green-inc-4: hsla(129, 100%, 92%, 1); + --tt-color-green-inc-3: hsla(131, 100%, 86%, 1); + --tt-color-green-inc-2: hsla(133, 98%, 78%, 1); + --tt-color-green-inc-1: hsla(137, 99%, 70%, 1); + --tt-color-green-base: hsla(147, 99%, 50%, 1); + --tt-color-green-dec-1: hsla(147, 97%, 41%, 1); + --tt-color-green-dec-2: hsla(146, 98%, 32%, 1); + --tt-color-green-dec-3: hsla(146, 100%, 24%, 1); + --tt-color-green-dec-4: hsla(144, 100%, 16%, 1); + --tt-color-green-dec-5: hsla(140, 100%, 9%, 1); + + /* Yellow */ + --tt-color-yellow-inc-5: hsla(50, 100%, 97%, 1); + --tt-color-yellow-inc-4: hsla(50, 100%, 91%, 1); + --tt-color-yellow-inc-3: hsla(50, 100%, 84%, 1); + --tt-color-yellow-inc-2: hsla(50, 100%, 77%, 1); + --tt-color-yellow-inc-1: hsla(50, 100%, 68%, 1); + --tt-color-yellow-base: hsla(52, 100%, 50%, 1); + --tt-color-yellow-dec-1: hsla(52, 100%, 41%, 1); + --tt-color-yellow-dec-2: hsla(52, 100%, 32%, 1); + --tt-color-yellow-dec-3: hsla(52, 100%, 24%, 1); + --tt-color-yellow-dec-4: hsla(51, 100%, 16%, 1); + --tt-color-yellow-dec-5: hsla(50, 100%, 9%, 1); + + /* Red */ + --tt-color-red-inc-5: hsla(11, 100%, 96%, 1); + --tt-color-red-inc-4: hsla(11, 100%, 88%, 1); + --tt-color-red-inc-3: hsla(10, 100%, 80%, 1); + --tt-color-red-inc-2: hsla(9, 100%, 73%, 1); + --tt-color-red-inc-1: hsla(7, 100%, 64%, 1); + --tt-color-red-base: hsla(7, 100%, 54%, 1); + --tt-color-red-dec-1: hsla(7, 100%, 41%, 1); + --tt-color-red-dec-2: hsla(5, 100%, 32%, 1); + --tt-color-red-dec-3: hsla(4, 100%, 24%, 1); + --tt-color-red-dec-4: hsla(3, 100%, 16%, 1); + --tt-color-red-dec-5: hsla(1, 100%, 9%, 1); + + /* Basic colors */ + --white: rgba(255, 255, 255, 1); + --black: rgba(14, 14, 17, 1); + --transparent: rgba(255, 255, 255, 0); + + /****************** + Shadow variables + ******************/ + + /* Shadows Light */ + --tt-shadow-elevated-md: + 0px 16px 48px 0px rgba(17, 24, 39, 0.04), + 0px 12px 24px 0px rgba(17, 24, 39, 0.04), + 0px 6px 8px 0px rgba(17, 24, 39, 0.02), + 0px 2px 3px 0px rgba(17, 24, 39, 0.02); + + /************************************************** + Radius variables + **************************************************/ + + --tt-radius-xxs: 0.125rem; /* 2px */ + --tt-radius-xs: 0.25rem; /* 4px */ + --tt-radius-sm: 0.375rem; /* 6px */ + --tt-radius-md: 0.5rem; /* 8px */ + --tt-radius-lg: 0.75rem; /* 12px */ + --tt-radius-xl: 1rem; /* 16px */ + + /************************************************** + Transition variables + **************************************************/ + + --tt-transition-duration-short: 0.1s; + --tt-transition-duration-default: 0.2s; + --tt-transition-duration-long: 0.64s; + --tt-transition-easing-default: cubic-bezier(0.46, 0.03, 0.52, 0.96); + --tt-transition-easing-cubic: cubic-bezier(0.65, 0.05, 0.36, 1); + --tt-transition-easing-quart: cubic-bezier(0.77, 0, 0.18, 1); + --tt-transition-easing-circ: cubic-bezier(0.79, 0.14, 0.15, 0.86); + --tt-transition-easing-back: cubic-bezier(0.68, -0.55, 0.27, 1.55); + + /****************** + Contrast variables + ******************/ + + --tt-accent-contrast: 8%; + --tt-destructive-contrast: 8%; + --tt-foreground-contrast: 8%; + + &, + *, + ::before, + ::after { + box-sizing: border-box; + transition: none var(--tt-transition-duration-default) + var(--tt-transition-easing-default); + } +} + +:root { + /************************************************** + Global colors + **************************************************/ + + /* Global colors - Light mode */ + --tt-bg-color: var(--white); + --tt-border-color: var(--tt-gray-light-a-200); + --tt-border-color-tint: var(--tt-gray-light-a-100); + --tt-sidebar-bg-color: var(--tt-gray-light-100); + --tt-scrollbar-color: var(--tt-gray-light-a-200); + --tt-cursor-color: var(--tt-brand-color-500); + --tt-selection-color: rgba(157, 138, 255, 0.2); + --tt-card-bg-color: var(--white); + --tt-card-border-color: var(--tt-gray-light-a-100); +} + +/* Global colors - Dark mode */ +.dark { + --tt-bg-color: var(--black); + --tt-border-color: var(--tt-gray-dark-a-200); + --tt-border-color-tint: var(--tt-gray-dark-a-100); + --tt-sidebar-bg-color: var(--tt-gray-dark-100); + --tt-scrollbar-color: var(--tt-gray-dark-a-200); + --tt-cursor-color: var(--tt-brand-color-400); + --tt-selection-color: rgba(122, 82, 255, 0.2); + --tt-card-bg-color: var(--tt-gray-dark-50); + --tt-card-border-color: var(--tt-gray-dark-a-50); + + --tt-shadow-elevated-md: + 0px 16px 48px 0px rgba(0, 0, 0, 0.5), 0px 12px 24px 0px rgba(0, 0, 0, 0.24), + 0px 6px 8px 0px rgba(0, 0, 0, 0.22), 0px 2px 3px 0px rgba(0, 0, 0, 0.12); +} + +/* Text colors */ +:root { + --tt-color-text-gray: hsl(45, 2%, 46%); + --tt-color-text-brown: hsl(19, 31%, 47%); + --tt-color-text-orange: hsl(30, 89%, 45%); + --tt-color-text-yellow: hsl(38, 62%, 49%); + --tt-color-text-green: hsl(148, 32%, 39%); + --tt-color-text-blue: hsl(202, 54%, 43%); + --tt-color-text-purple: hsl(274, 32%, 54%); + --tt-color-text-pink: hsl(328, 49%, 53%); + --tt-color-text-red: hsl(2, 62%, 55%); + + --tt-color-text-gray-contrast: hsla(39, 26%, 26%, 0.15); + --tt-color-text-brown-contrast: hsla(18, 43%, 69%, 0.35); + --tt-color-text-orange-contrast: hsla(24, 73%, 55%, 0.27); + --tt-color-text-yellow-contrast: hsla(44, 82%, 59%, 0.39); + --tt-color-text-green-contrast: hsla(126, 29%, 60%, 0.27); + --tt-color-text-blue-contrast: hsla(202, 54%, 59%, 0.27); + --tt-color-text-purple-contrast: hsla(274, 37%, 64%, 0.27); + --tt-color-text-pink-contrast: hsla(331, 60%, 71%, 0.27); + --tt-color-text-red-contrast: hsla(8, 79%, 79%, 0.4); +} + +.dark { + --tt-color-text-gray: hsl(0, 0%, 61%); + --tt-color-text-brown: hsl(18, 35%, 58%); + --tt-color-text-orange: hsl(25, 53%, 53%); + --tt-color-text-yellow: hsl(36, 54%, 55%); + --tt-color-text-green: hsl(145, 32%, 47%); + --tt-color-text-blue: hsl(202, 64%, 52%); + --tt-color-text-purple: hsl(270, 55%, 62%); + --tt-color-text-pink: hsl(329, 57%, 58%); + --tt-color-text-red: hsl(1, 69%, 60%); + + --tt-color-text-gray-contrast: hsla(0, 0%, 100%, 0.09); + --tt-color-text-brown-contrast: hsla(17, 45%, 50%, 0.25); + --tt-color-text-orange-contrast: hsla(27, 82%, 53%, 0.2); + --tt-color-text-yellow-contrast: hsla(35, 49%, 47%, 0.2); + --tt-color-text-green-contrast: hsla(151, 55%, 39%, 0.2); + --tt-color-text-blue-contrast: hsla(202, 54%, 43%, 0.2); + --tt-color-text-purple-contrast: hsla(271, 56%, 60%, 0.18); + --tt-color-text-pink-contrast: hsla(331, 67%, 58%, 0.22); + --tt-color-text-red-contrast: hsla(0, 67%, 60%, 0.25); +} + +/* Highlight colors */ +:root { + --tt-color-highlight-yellow: #fef9c3; + --tt-color-highlight-green: #dcfce7; + --tt-color-highlight-blue: #e0f2fe; + --tt-color-highlight-purple: #f3e8ff; + --tt-color-highlight-red: #ffe4e6; + --tt-color-highlight-gray: rgb(248, 248, 247); + --tt-color-highlight-brown: rgb(244, 238, 238); + --tt-color-highlight-orange: rgb(251, 236, 221); + --tt-color-highlight-pink: rgb(252, 241, 246); + + --tt-color-highlight-yellow-contrast: #fbe604; + --tt-color-highlight-green-contrast: #c7fad8; + --tt-color-highlight-blue-contrast: #ceeafd; + --tt-color-highlight-purple-contrast: #e4ccff; + --tt-color-highlight-red-contrast: #ffccd0; + --tt-color-highlight-gray-contrast: rgba(84, 72, 49, 0.15); + --tt-color-highlight-brown-contrast: rgba(210, 162, 141, 0.35); + --tt-color-highlight-orange-contrast: rgba(224, 124, 57, 0.27); + --tt-color-highlight-pink-contrast: rgba(225, 136, 179, 0.27); +} + +.dark { + --tt-color-highlight-yellow: #6b6524; + --tt-color-highlight-green: #509568; + --tt-color-highlight-blue: #6e92aa; + --tt-color-highlight-purple: #583e74; + --tt-color-highlight-red: #743e42; + --tt-color-highlight-gray: rgb(47, 47, 47); + --tt-color-highlight-brown: rgb(74, 50, 40); + --tt-color-highlight-orange: rgb(92, 59, 35); + --tt-color-highlight-pink: rgb(78, 44, 60); + + --tt-color-highlight-yellow-contrast: #58531e; + --tt-color-highlight-green-contrast: #47855d; + --tt-color-highlight-blue-contrast: #5e86a1; + --tt-color-highlight-purple-contrast: #4c3564; + --tt-color-highlight-red-contrast: #643539; + --tt-color-highlight-gray-contrast: rgba(255, 255, 255, 0.094); + --tt-color-highlight-brown-contrast: rgba(184, 101, 69, 0.25); + --tt-color-highlight-orange-contrast: rgba(233, 126, 37, 0.2); + --tt-color-highlight-pink-contrast: rgba(220, 76, 145, 0.22); +} diff --git a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json new file mode 100644 index 0000000000..3bcfb7636b --- /dev/null +++ b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/components/*": ["./components/*"], + "@/lib/*": ["./lib/*"], + "@/hooks/*": ["./hooks/*"] + } + }, + "include": [ + "./**/*.ts", + "./**/*.tsx" + ] +} + diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index fddd94de71..a04bded224 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -171,6 +171,8 @@ export * from "./components/ThemeProvider/MITLearnGlobalStyles" export { AppRouterCacheProvider as NextJsAppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter" +export { default as TiptapEditor } from "./components/TiptapEditor/TiptapEditor" + // /** // * @deprecated Please use component from @mitodl/smoot-design instead // */ diff --git a/frontends/ol-components/tsconfig.json b/frontends/ol-components/tsconfig.json index 746eeeb926..d319d7d722 100644 --- a/frontends/ol-components/tsconfig.json +++ b/frontends/ol-components/tsconfig.json @@ -4,5 +4,6 @@ "outDir": "./build", "rootDir": "./src" }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"] + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "aliases": {} } From a98a0bc7703484e8319a36e92672a4b368a81824 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:19:25 +0000 Subject: [PATCH 02/16] Update lockfile --- yarn.lock | 2248 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 2082 insertions(+), 166 deletions(-) diff --git a/yarn.lock b/yarn.lock index 889357270f..f03d115493 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1526,6 +1526,13 @@ __metadata: languageName: node linkType: hard +"@bufbuild/protobuf@npm:^2.5.0": + version: 2.10.0 + resolution: "@bufbuild/protobuf@npm:2.10.0" + checksum: 10/58a213899a34d6a742da6ce6f7405583b390a23243287799d869afb21a7a7778475529b50f5bdbf2acbf50cce740dba1292aa7b548c576514f8b55e139ca90c4 + languageName: node + linkType: hard + "@chromatic-com/storybook@npm:^3.2.7": version: 3.2.7 resolution: "@chromatic-com/storybook@npm:3.2.7" @@ -2117,6 +2124,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/core@npm:1.7.3" + dependencies: + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/a8952ff2673ddf28f12feeb86d90c54949e45bcb1af5758b7672850ac0dadb36d4bd61aa45dad1b6a35ba40d4756d3573afac6610b90502639d7266b91e0864e + languageName: node + linkType: hard + "@floating-ui/dom@npm:^1.0.0, @floating-ui/dom@npm:^1.0.1": version: 1.6.11 resolution: "@floating-ui/dom@npm:1.6.11" @@ -2127,6 +2143,28 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:^1.7.4": + version: 1.7.4 + resolution: "@floating-ui/dom@npm:1.7.4" + dependencies: + "@floating-ui/core": "npm:^1.7.3" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/d3d6a23e7b9804ba56338c7c666590258683af14b6026270d32afc1202f72b5b82cca359004bdc7830bf2463a045da6c7bd4e7d5351218cf270ff94206197971 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.6": + version: 2.1.6 + resolution: "@floating-ui/react-dom@npm:2.1.6" + dependencies: + "@floating-ui/dom": "npm:^1.7.4" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/fbfd3319b42edb9c156e4e872f500d2edb112bc9cfd1b45892bff16ccf21c2484ddc9c416f7631c2aaaadec1b2f98b205db8a3f89eb78ca870905fcfe3917c35 + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:^2.0.8, @floating-ui/react-dom@npm:^2.1.1": version: 2.1.2 resolution: "@floating-ui/react-dom@npm:2.1.2" @@ -2139,6 +2177,27 @@ __metadata: languageName: node linkType: hard +"@floating-ui/react@npm:^0.27.16": + version: 0.27.16 + resolution: "@floating-ui/react@npm:0.27.16" + dependencies: + "@floating-ui/react-dom": "npm:^2.1.6" + "@floating-ui/utils": "npm:^0.2.10" + tabbable: "npm:^6.0.0" + peerDependencies: + react: ">=17.0.0" + react-dom: ">=17.0.0" + checksum: 10/b9baedee124035323a8f74794ec782678faf52af1c88731ce7d2641b7e7c97748fda1e711a3c4db007a0153d93158d867f4726ee632d713d3de76ec4bdfd84e1 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae + languageName: node + linkType: hard + "@floating-ui/utils@npm:^0.2.8": version: 0.2.8 resolution: "@floating-ui/utils@npm:0.2.8" @@ -3205,7 +3264,7 @@ __metadata: languageName: node linkType: hard -"@mitodl/mitxonline-api-axios@npm:^2025.11.5": +"@mitodl/mitxonline-api-axios@npm:^2025.10.21, @mitodl/mitxonline-api-axios@npm:^2025.11.5": version: 2025.11.5 resolution: "@mitodl/mitxonline-api-axios@npm:2025.11.5" dependencies: @@ -4134,6 +4193,150 @@ __metadata: languageName: node linkType: hard +"@parcel/watcher-android-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-android-arm64@npm:2.5.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-darwin-arm64@npm:2.5.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-darwin-x64@npm:2.5.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-freebsd-x64@npm:2.5.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.5.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-musl@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm-musl@npm:2.5.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.5.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.5.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.5.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.5.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-win32-arm64@npm:2.5.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-ia32@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-win32-ia32@npm:2.5.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-win32-x64@npm:2.5.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.4.1": + version: 2.5.1 + resolution: "@parcel/watcher@npm:2.5.1" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.5.1" + "@parcel/watcher-darwin-arm64": "npm:2.5.1" + "@parcel/watcher-darwin-x64": "npm:2.5.1" + "@parcel/watcher-freebsd-x64": "npm:2.5.1" + "@parcel/watcher-linux-arm-glibc": "npm:2.5.1" + "@parcel/watcher-linux-arm-musl": "npm:2.5.1" + "@parcel/watcher-linux-arm64-glibc": "npm:2.5.1" + "@parcel/watcher-linux-arm64-musl": "npm:2.5.1" + "@parcel/watcher-linux-x64-glibc": "npm:2.5.1" + "@parcel/watcher-linux-x64-musl": "npm:2.5.1" + "@parcel/watcher-win32-arm64": "npm:2.5.1" + "@parcel/watcher-win32-ia32": "npm:2.5.1" + "@parcel/watcher-win32-x64": "npm:2.5.1" + detect-libc: "npm:^1.0.3" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm-musl": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-ia32": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10/2cc1405166fb3016b34508661902ab08b6dec59513708165c633c84a4696fff64f9b99ea116e747c121215e09619f1decab6f0350d1cb26c9210b98eb28a6a56 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -4210,124 +4413,615 @@ __metadata: languageName: node linkType: hard -"@react-pdf/fns@npm:3.1.2": - version: 3.1.2 - resolution: "@react-pdf/fns@npm:3.1.2" - checksum: 10/b4b48167ae454587e2513b07166f44a21d908817b5e59dd72693f762c647038ecf6b4f18eecb468566613338ad1d5b4e16ade68d39ae852c9ef91ab54820224b +"@radix-ui/primitive@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/primitive@npm:1.1.3" + checksum: 10/ee27abbff0d6d305816e9314655eb35e72478ba47416bc9d5cb0581728be35e3408cfc0748313837561d635f0cb7dfaae26e61831f0e16c0fd7d669a612f2cb0 languageName: node linkType: hard -"@react-pdf/font@npm:^4.0.2": - version: 4.0.2 - resolution: "@react-pdf/font@npm:4.0.2" +"@radix-ui/react-arrow@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-arrow@npm:1.1.7" dependencies: - "@react-pdf/pdfkit": "npm:^4.0.3" - "@react-pdf/types": "npm:^2.9.0" - fontkit: "npm:^2.0.2" - is-url: "npm:^1.2.4" - checksum: 10/7b0504a11b96a08aec60558b0cf14be2374c2e12fbe164be63fb21ebb4356a52ff9cdcbf94c3143643d37f1f0794acb6315c1cf681f65f569b0c05ccf1a6477d + "@radix-ui/react-primitive": "npm:2.1.3" + 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 + checksum: 10/6cdf74f06090f8994cdf6d3935a44ea3ac309163a4f59c476482c4907e8e0775f224045030abf10fa4f9e1cb7743db034429249b9e59354988e247eeb0f4fdcf languageName: node linkType: hard -"@react-pdf/image@npm:^3.0.3": - version: 3.0.3 - resolution: "@react-pdf/image@npm:3.0.3" +"@radix-ui/react-collection@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-collection@npm:1.1.7" dependencies: - "@react-pdf/png-js": "npm:^3.0.0" - jay-peg: "npm:^1.1.1" - checksum: 10/1d72f36dcfe1b5da5a67c9a905e4796ad2f0a9d96df1dea4998363759814712ca46f9877519dfb26653d6ba92705d2821984e09611b43582790fb534de3e7eb3 + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + 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 + checksum: 10/cd53e2a2be82be7bc4014164cac0b42948401a203e5d0294d3947a5193f1d56bd23eb60e878a98dba50d08283254e79c3b873de5f935276b849686a868d51dd5 languageName: node linkType: hard -"@react-pdf/layout@npm:^4.4.0": - version: 4.4.0 - resolution: "@react-pdf/layout@npm:4.4.0" - dependencies: - "@react-pdf/fns": "npm:3.1.2" - "@react-pdf/image": "npm:^3.0.3" - "@react-pdf/primitives": "npm:^4.1.1" - "@react-pdf/stylesheet": "npm:^6.1.0" - "@react-pdf/textkit": "npm:^6.0.0" - "@react-pdf/types": "npm:^2.9.0" - emoji-regex: "npm:^10.3.0" - queue: "npm:^6.0.1" - yoga-layout: "npm:^3.2.1" - checksum: 10/f40c4e9028ed169ee77b49b197acf4bb0b75dacbbdaf55b36f7a9953d7152e377e7bf62756c0efed40fed403b84ee1752120826851fbb670aa8fafb267d0d4f7 +"@radix-ui/react-compose-refs@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-compose-refs@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/9a91f0213014ffa40c5b8aae4debb993be5654217e504e35aa7422887eb2d114486d37e53c482d0fffb00cd44f51b5269fcdf397b280c71666fa11b7f32f165d languageName: node linkType: hard -"@react-pdf/pdfkit@npm:^4.0.3": - version: 4.0.3 - resolution: "@react-pdf/pdfkit@npm:4.0.3" - dependencies: - "@babel/runtime": "npm:^7.20.13" - "@react-pdf/png-js": "npm:^3.0.0" - browserify-zlib: "npm:^0.2.0" - crypto-js: "npm:^4.2.0" - fontkit: "npm:^2.0.2" - jay-peg: "npm:^1.1.1" - linebreak: "npm:^1.1.0" - vite-compatible-readable-stream: "npm:^3.6.1" - checksum: 10/7571931928b1514917e210a0a6e167e7b0ef8a107a5accedf13d461fa4829ebb04844a5d5626f7c78c4410c53b69e30095008a8b3b588666581970b4ea7a5abe +"@radix-ui/react-context@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-context@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/156088367de42afa3c7e3acf5f0ba7cad6b359f3d17485585e80c2418434a6ed7cac2602eb73bca265d0091a1ad380f9405c069f103983e53497097ff35ba8f2 languageName: node linkType: hard -"@react-pdf/png-js@npm:^3.0.0": - version: 3.0.0 - resolution: "@react-pdf/png-js@npm:3.0.0" - dependencies: - browserify-zlib: "npm:^0.2.0" - checksum: 10/5e972302a5d93f67d3dc1438bb4cb8ee708802fb6d513b651aa0857909a185f06e5288b59e82cffa9fe5587289c97de7f5c5401101f50faebc32bef50f398ee5 +"@radix-ui/react-direction@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-direction@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/8cc330285f1d06829568042ca9aabd3295be4690ae93683033fc8632b5c4dfc60f5c1312f6e2cae27c196189c719de3cfbcf792ff74800f9ccae0ab4abc1bc92 languageName: node linkType: hard -"@react-pdf/primitives@npm:^4.1.1": - version: 4.1.1 - resolution: "@react-pdf/primitives@npm:4.1.1" - checksum: 10/adadff1996daeca693aa59844ab613e597fdb674fce9f2c03f52573b593982ef49ff47d861290235861d02462ffbc87b7ed3da0d71af0d61c9226ce61b94ada8 +"@radix-ui/react-dismissable-layer@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-escape-keydown": "npm:1.1.1" + 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 + checksum: 10/c20772588423379dee47fbe1d45c238c45a3bbe612eaf64a86576bf81821975e256d92ac71f9151e91b94a73068656143a11da9a3e77de7564d2a9926468e37a languageName: node linkType: hard -"@react-pdf/reconciler@npm:^1.1.4": - version: 1.1.4 - resolution: "@react-pdf/reconciler@npm:1.1.4" +"@radix-ui/react-dropdown-menu@npm:^2.1.16": + version: 2.1.16 + resolution: "@radix-ui/react-dropdown-menu@npm:2.1.16" dependencies: - object-assign: "npm:^4.1.1" - scheduler: "npm:0.25.0-rc-603e6108-20241029" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-menu": "npm:2.1.16" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/6562961c6ead2b4392a32506d761c84d8c74f43378616cdab84dbdcbfd64e83b18e6e2eeeb962de7e3f3ab672ae335c3560b7f9e0cd275515de2ee55489e5425 + "@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 + checksum: 10/da215196b5dde5619cdb424b1b5236159e4bb949974b7f4ffbf047d467c55116229a8f9cf07eae6457afefb4a2b07888bb30542f303045e05d90a4b072941ae2 languageName: node linkType: hard -"@react-pdf/render@npm:^4.3.0": - version: 4.3.0 - resolution: "@react-pdf/render@npm:4.3.0" - dependencies: - "@babel/runtime": "npm:^7.20.13" - "@react-pdf/fns": "npm:3.1.2" - "@react-pdf/primitives": "npm:^4.1.1" - "@react-pdf/textkit": "npm:^6.0.0" - "@react-pdf/types": "npm:^2.9.0" - abs-svg-path: "npm:^0.1.1" - color-string: "npm:^1.9.1" - normalize-svg-path: "npm:^1.1.0" - parse-svg-path: "npm:^0.1.2" - svg-arc-to-cubic-bezier: "npm:^3.2.0" - checksum: 10/7d1465e9411c1c4f170ef741226b5008b1236ec7eee2ed3b87248aeec7a42ecd5826fe7a351a0069826abbf4b4051f98e76fc1708d4cc5d5542f8be1058da978 +"@radix-ui/react-focus-guards@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-focus-guards@npm:1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/b57878f6cf0ebc3e8d7c5c6bbaad44598daac19c921551ca541c104201048a9a902f3d69196e7a09995fd46e998c309aab64dc30fa184b3609d67d187a6a9c24 languageName: node linkType: hard -"@react-pdf/renderer@npm:^4.3.0": - version: 4.3.0 - resolution: "@react-pdf/renderer@npm:4.3.0" +"@radix-ui/react-focus-scope@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-focus-scope@npm:1.1.7" dependencies: - "@babel/runtime": "npm:^7.20.13" - "@react-pdf/fns": "npm:3.1.2" - "@react-pdf/font": "npm:^4.0.2" - "@react-pdf/layout": "npm:^4.4.0" - "@react-pdf/pdfkit": "npm:^4.0.3" - "@react-pdf/primitives": "npm:^4.1.1" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + 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 + checksum: 10/2a7cd00e39e01756999ebf0bdb3401d6a8efa489a7b19e6b629b40bad3022b7b1f616555ccb4b0505bc0ba53e13a1fb51be905db138b16ec39c4fe319fe701d3 + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-id@npm:1.1.1" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/8d68e200778eb3038906870fc869b3d881f4a46715fb20cddd9c76cba42fdaaa4810a3365b6ec2daf0f185b9201fc99d009167f59c7921bc3a139722c2e976db + languageName: node + linkType: hard + +"@radix-ui/react-menu@npm:2.1.16": + version: 2.1.16 + resolution: "@radix-ui/react-menu@npm:2.1.16" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-roving-focus": "npm:1.1.11" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" + 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 + checksum: 10/2ffdfa08822c8c4ffc265d02d16c83d725114f9c0e9b510e73e431306dedddd507ef2861ccd67ec8c0d21cb24cd6401e42f16f3e65b30be627c7e22159151e40 + languageName: node + linkType: hard + +"@radix-ui/react-popover@npm:^1.1.15": + version: 1.1.15 + resolution: "@radix-ui/react-popover@npm:1.1.15" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" + 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 + checksum: 10/0ea7c8bb827e44d5c02b3f7193d9ac8085c71a01bf601b1afeb2bb0ec0124756e03db3471606e89e4d014e4de7c7066c8e2e9b81bb4b31ea321890ec33421f31 + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-popper@npm:1.2.8" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + "@radix-ui/react-use-rect": "npm:1.1.1" + "@radix-ui/react-use-size": "npm:1.1.1" + "@radix-ui/rect": "npm:1.1.1" + 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 + checksum: 10/01366054e1e63dd9394f77afb9da3367709478a5adf4436c080fc5bbe9456170192ff9d1425d9fae5b246e1ba95173848f84b6f2a06b21b47d966367ec7cb997 + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.1.9": + version: 1.1.9 + resolution: "@radix-ui/react-portal@npm:1.1.9" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + 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 + checksum: 10/bd6be39bf021d5c917e2474ecba411e2625171f7ef96862b9af04bbd68833bb3662a7f1fbdeb5a7a237111b10e811e76d2cd03e957dadd6e668ef16541bfbd68 + languageName: node + linkType: hard + +"@radix-ui/react-presence@npm:1.1.5": + version: 1.1.5 + resolution: "@radix-ui/react-presence@npm:1.1.5" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + 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 + checksum: 10/4cdb05844c18877efb4b9739b46b7e5850b81d7ede994e75b5d62e8153a43c6e16b3ff9e55ff716e20b74b99b9415a94e97fd636bcb8698d5bbf7ab7b8663f9b + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:2.1.3": + version: 2.1.3 + resolution: "@radix-ui/react-primitive@npm:2.1.3" + dependencies: + "@radix-ui/react-slot": "npm:1.2.3" + 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 + checksum: 10/1dbbf932a3527f4e62f210bb72944eff605c3e38c8d3275ed5a5c570c02820ab156169756a65ad9a638d2089a828a04a7903795377384e98c87d0ca456303253 + languageName: node + linkType: hard + +"@radix-ui/react-roving-focus@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-roving-focus@npm:1.1.11" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + 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 + checksum: 10/0eddafa942332c95622ab8b53cce2fa25fd0dcaf4797218e9e6725da0734a81a438852cdcb3f588521018f68d38c6c5e50c64fda78c655f4e69dd45681ecc5e7 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-slot@npm:1.2.3" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/fe484c2741e31d9c20a8fb53c5790a73c0664e2bea35e27f4d484a90c42135fcfffe11a08abfcacb7a8ee2faf013471f0e856818f3ddac8ac51ceb8869e0fd08 + languageName: node + linkType: hard + +"@radix-ui/react-use-callback-ref@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/cde8c40f1d4e79e6e71470218163a746858304bad03758ac84dc1f94247a046478e8e397518350c8d6609c84b7e78565441d7505bb3ed573afce82cfdcd19faf + languageName: node + linkType: hard + +"@radix-ui/react-use-controllable-state@npm:1.2.2": + version: 1.2.2 + resolution: "@radix-ui/react-use-controllable-state@npm:1.2.2" + dependencies: + "@radix-ui/react-use-effect-event": "npm:0.0.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/a100bff3ddecb753dab17444147273c9f70046c5949712c52174b259622eaef12acbf7ebcf289bae4e714eb84d0a7317c1aa44064cd997f327d77b62bc732a7c + languageName: node + linkType: hard + +"@radix-ui/react-use-effect-event@npm:0.0.2": + version: 0.0.2 + resolution: "@radix-ui/react-use-effect-event@npm:0.0.2" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/5a1950a30a399ea7e4b98154da9f536737a610de80189b7aacd4f064a89a3cd0d2a48571d527435227252e72e872bdb544ff6ffcfbdd02de2efd011be4aaa902 + languageName: node + linkType: hard + +"@radix-ui/react-use-escape-keydown@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.1" + dependencies: + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/0eb0756c2c55ddcde9ff01446ab01c085ab2bf799173e97db7ef5f85126f9e8600225570801a1f64740e6d14c39ffe8eed7c14d29737345a5797f4622ac96f6f + languageName: node + linkType: hard + +"@radix-ui/react-use-layout-effect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/bad2ba4f206e6255263582bedfb7868773c400836f9a1b423c0b464ffe4a17e13d3f306d1ce19cf7a19a492e9d0e49747464f2656451bb7c6a99f5a57bd34de2 + languageName: node + linkType: hard + +"@radix-ui/react-use-rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-rect@npm:1.1.1" + dependencies: + "@radix-ui/rect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/116461bebc49472f7497e66a9bd413541181b3d00c5e0aaeef45d790dc1fbd7c8dcea80b169ea273306228b9a3c2b70067e902d1fd5004b3057e3bbe35b9d55d + languageName: node + linkType: hard + +"@radix-ui/react-use-size@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-size@npm:1.1.1" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/64e61f65feb67ffc80e1fc4a8d5e32480fb6d68475e2640377e021178dead101568cba5f936c9c33e6c142c7cf2fb5d76ad7b23ef80e556ba142d56cf306147b + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/rect@npm:1.1.1" + checksum: 10/b6c5eb787640775b53dd52fa47218a089f0a0d8220d3ebff079c0b754e1fb82d89b6bdf08a82fd0d59549bdeb52678c0cca091c302da49dcf74c3c989cb55678 + languageName: node + linkType: hard + +"@react-pdf/fns@npm:3.1.2": + version: 3.1.2 + resolution: "@react-pdf/fns@npm:3.1.2" + checksum: 10/b4b48167ae454587e2513b07166f44a21d908817b5e59dd72693f762c647038ecf6b4f18eecb468566613338ad1d5b4e16ade68d39ae852c9ef91ab54820224b + languageName: node + linkType: hard + +"@react-pdf/font@npm:^4.0.2": + version: 4.0.2 + resolution: "@react-pdf/font@npm:4.0.2" + dependencies: + "@react-pdf/pdfkit": "npm:^4.0.3" + "@react-pdf/types": "npm:^2.9.0" + fontkit: "npm:^2.0.2" + is-url: "npm:^1.2.4" + checksum: 10/7b0504a11b96a08aec60558b0cf14be2374c2e12fbe164be63fb21ebb4356a52ff9cdcbf94c3143643d37f1f0794acb6315c1cf681f65f569b0c05ccf1a6477d + languageName: node + linkType: hard + +"@react-pdf/image@npm:^3.0.3": + version: 3.0.3 + resolution: "@react-pdf/image@npm:3.0.3" + dependencies: + "@react-pdf/png-js": "npm:^3.0.0" + jay-peg: "npm:^1.1.1" + checksum: 10/1d72f36dcfe1b5da5a67c9a905e4796ad2f0a9d96df1dea4998363759814712ca46f9877519dfb26653d6ba92705d2821984e09611b43582790fb534de3e7eb3 + languageName: node + linkType: hard + +"@react-pdf/layout@npm:^4.4.0": + version: 4.4.0 + resolution: "@react-pdf/layout@npm:4.4.0" + dependencies: + "@react-pdf/fns": "npm:3.1.2" + "@react-pdf/image": "npm:^3.0.3" + "@react-pdf/primitives": "npm:^4.1.1" + "@react-pdf/stylesheet": "npm:^6.1.0" + "@react-pdf/textkit": "npm:^6.0.0" + "@react-pdf/types": "npm:^2.9.0" + emoji-regex: "npm:^10.3.0" + queue: "npm:^6.0.1" + yoga-layout: "npm:^3.2.1" + checksum: 10/f40c4e9028ed169ee77b49b197acf4bb0b75dacbbdaf55b36f7a9953d7152e377e7bf62756c0efed40fed403b84ee1752120826851fbb670aa8fafb267d0d4f7 + languageName: node + linkType: hard + +"@react-pdf/pdfkit@npm:^4.0.3": + version: 4.0.3 + resolution: "@react-pdf/pdfkit@npm:4.0.3" + dependencies: + "@babel/runtime": "npm:^7.20.13" + "@react-pdf/png-js": "npm:^3.0.0" + browserify-zlib: "npm:^0.2.0" + crypto-js: "npm:^4.2.0" + fontkit: "npm:^2.0.2" + jay-peg: "npm:^1.1.1" + linebreak: "npm:^1.1.0" + vite-compatible-readable-stream: "npm:^3.6.1" + checksum: 10/7571931928b1514917e210a0a6e167e7b0ef8a107a5accedf13d461fa4829ebb04844a5d5626f7c78c4410c53b69e30095008a8b3b588666581970b4ea7a5abe + languageName: node + linkType: hard + +"@react-pdf/png-js@npm:^3.0.0": + version: 3.0.0 + resolution: "@react-pdf/png-js@npm:3.0.0" + dependencies: + browserify-zlib: "npm:^0.2.0" + checksum: 10/5e972302a5d93f67d3dc1438bb4cb8ee708802fb6d513b651aa0857909a185f06e5288b59e82cffa9fe5587289c97de7f5c5401101f50faebc32bef50f398ee5 + languageName: node + linkType: hard + +"@react-pdf/primitives@npm:^4.1.1": + version: 4.1.1 + resolution: "@react-pdf/primitives@npm:4.1.1" + checksum: 10/adadff1996daeca693aa59844ab613e597fdb674fce9f2c03f52573b593982ef49ff47d861290235861d02462ffbc87b7ed3da0d71af0d61c9226ce61b94ada8 + languageName: node + linkType: hard + +"@react-pdf/reconciler@npm:^1.1.4": + version: 1.1.4 + resolution: "@react-pdf/reconciler@npm:1.1.4" + dependencies: + object-assign: "npm:^4.1.1" + scheduler: "npm:0.25.0-rc-603e6108-20241029" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/6562961c6ead2b4392a32506d761c84d8c74f43378616cdab84dbdcbfd64e83b18e6e2eeeb962de7e3f3ab672ae335c3560b7f9e0cd275515de2ee55489e5425 + languageName: node + linkType: hard + +"@react-pdf/render@npm:^4.3.0": + version: 4.3.0 + resolution: "@react-pdf/render@npm:4.3.0" + dependencies: + "@babel/runtime": "npm:^7.20.13" + "@react-pdf/fns": "npm:3.1.2" + "@react-pdf/primitives": "npm:^4.1.1" + "@react-pdf/textkit": "npm:^6.0.0" + "@react-pdf/types": "npm:^2.9.0" + abs-svg-path: "npm:^0.1.1" + color-string: "npm:^1.9.1" + normalize-svg-path: "npm:^1.1.0" + parse-svg-path: "npm:^0.1.2" + svg-arc-to-cubic-bezier: "npm:^3.2.0" + checksum: 10/7d1465e9411c1c4f170ef741226b5008b1236ec7eee2ed3b87248aeec7a42ecd5826fe7a351a0069826abbf4b4051f98e76fc1708d4cc5d5542f8be1058da978 + languageName: node + linkType: hard + +"@react-pdf/renderer@npm:^4.3.0": + version: 4.3.0 + resolution: "@react-pdf/renderer@npm:4.3.0" + dependencies: + "@babel/runtime": "npm:^7.20.13" + "@react-pdf/fns": "npm:3.1.2" + "@react-pdf/font": "npm:^4.0.2" + "@react-pdf/layout": "npm:^4.4.0" + "@react-pdf/pdfkit": "npm:^4.0.3" + "@react-pdf/primitives": "npm:^4.1.1" "@react-pdf/reconciler": "npm:^1.1.4" "@react-pdf/render": "npm:^4.3.0" "@react-pdf/types": "npm:^2.9.0" @@ -4378,6 +5072,13 @@ __metadata: languageName: node linkType: hard +"@remirror/core-constants@npm:3.0.0": + version: 3.0.0 + resolution: "@remirror/core-constants@npm:3.0.0" + checksum: 10/de15b1df099a7646739e5fb6bb55195618a8ac4fa938db7c719e867eefd72ebc5a05865591788ade449613141619cc1002fb6c0f824de4468dfefa951fbf19a2 + languageName: node + linkType: hard + "@remixicon/react@npm:^4.2.0": version: 4.2.0 resolution: "@remixicon/react@npm:4.2.0" @@ -4387,6 +5088,15 @@ __metadata: languageName: node linkType: hard +"@remixicon/react@npm:^4.7.0": + version: 4.7.0 + resolution: "@remixicon/react@npm:4.7.0" + peerDependencies: + react: ">=18.2.0" + checksum: 10/9ecee093dd4aec3744bcc7562eddb987bf9c7c059787dfecfbcc25a64c2f4833dc815dff715aa2a7f75011d65f877b3751a401d19cf2d725b5bcc685587d37f4 + languageName: node + linkType: hard + "@rollup/plugin-commonjs@npm:28.0.1": version: 28.0.1 resolution: "@rollup/plugin-commonjs@npm:28.0.1" @@ -5712,43 +6422,441 @@ __metadata: version: 6.5.0 resolution: "@testing-library/jest-dom@npm:6.5.0" dependencies: - "@adobe/css-tools": "npm:^4.4.0" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" - redent: "npm:^3.0.0" - checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 + "@adobe/css-tools": "npm:^4.4.0" + aria-query: "npm:^5.0.0" + chalk: "npm:^3.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + lodash: "npm:^4.17.21" + redent: "npm:^3.0.0" + checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 + languageName: node + linkType: hard + +"@testing-library/react@npm:^16.3.0": + version: 16.3.0 + resolution: "@testing-library/react@npm:16.3.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/0ee9e31dd0d2396a924682d0e61a4ecc6bfab8eaff23dbf8a72c3c2ce22c116fa578148baeb4de75b968ef99d22e6e6aa0a00dba40286f71184918bb6bb5b06a + languageName: node + linkType: hard + +"@testing-library/user-event@npm:14.5.2, @testing-library/user-event@npm:^14.5.2": + version: 14.5.2 + resolution: "@testing-library/user-event@npm:14.5.2" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 10/49821459d81c6bc435d97128d6386ca24f1e4b3ba8e46cb5a96fe3643efa6e002d88c1b02b7f2ec58da593e805c59b78d7fdf0db565c1f02ba782f63ee984040 + languageName: node + linkType: hard + +"@tiptap/core@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/core@npm:3.10.5" + peerDependencies: + "@tiptap/pm": ^3.10.5 + checksum: 10/17a00eb0962537406c303a29034a8ac06646c53d0e0e7b77384d7b930a873f09d21680d2a376a5f2a3eaa32e9928a7abf782112bf7373a6ca56474d718757256 + languageName: node + linkType: hard + +"@tiptap/extension-blockquote@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-blockquote@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/e24cceb69c37cbbd3aa13190733d73bdf6a3118ee2830b19e2acf4a8b74d20628cb3baefc464d15109c8ba84bed4f1804d2e32740c7f071c7ac0184cf02a30b1 + languageName: node + linkType: hard + +"@tiptap/extension-bold@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-bold@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/5c2e389a42981e04dcf43112e5329822bd30327dc14f17acb3967565b26ec706cb85e6f3a1ef5eea4149bcd9f0b5dee5e9f0c2f3dfbef706a1f5d0f1e9f930ec + languageName: node + linkType: hard + +"@tiptap/extension-bubble-menu@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-bubble-menu@npm:3.10.5" + dependencies: + "@floating-ui/dom": "npm:^1.0.0" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/f95dcfc771402179a96f6eb05089c1c281d54559b25c0efeccf19ba46bd33ec506a1b1a97923c7215513d4daada13d23c1194e14eaf0e58c35be773ce36009df + languageName: node + linkType: hard + +"@tiptap/extension-bullet-list@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-bullet-list@npm:3.10.5" + peerDependencies: + "@tiptap/extension-list": ^3.10.5 + checksum: 10/9220ee3030d2e98a5caac21a8eae2968979756c038d5010105ec920fdbdeaaf37509d5e7b716dbcc28f2029ab5fe74fe12d7656ab94658a1a00c1a5c53531e4c + languageName: node + linkType: hard + +"@tiptap/extension-code-block@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-code-block@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/76ce92c76a7be22ffacbba9faf71731b34cff765e9117843c8dd451d5c9b7dc0c83a5d12d8a62cca37eb50de97cf38793d4904d90e72ac2aa4c8657f1312198c + languageName: node + linkType: hard + +"@tiptap/extension-code@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-code@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/0554e61969d0a8afd1485ba54380a4e231fc7373f3a29d32aa252d4b55237ebec80f0f06df9dec0df5c16399fd35d1b745c33783aba4914115db32149461b9df + languageName: node + linkType: hard + +"@tiptap/extension-document@npm:^3.10.2, @tiptap/extension-document@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-document@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/89254ad1791cd4cc744b62e514aaf82364bad4009981c30d78092aa6d77017719e385d4d0996ef511ab49041bb22ffb6b408beb504009c2b404dbf4a0b93d1a1 + languageName: node + linkType: hard + +"@tiptap/extension-dropcursor@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-dropcursor@npm:3.10.5" + peerDependencies: + "@tiptap/extensions": ^3.10.5 + checksum: 10/ea87a3065e4f5576b28eb81e306ac5b2e59c20f34b7509a492dcfe6699b0a9d1c7111028e34fc5a20fc25b68b2c17aed203314029103631ed1cfe3599eefc50f + languageName: node + linkType: hard + +"@tiptap/extension-floating-menu@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-floating-menu@npm:3.10.5" + peerDependencies: + "@floating-ui/dom": ^1.0.0 + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/e1a4aec62b381bd7acb2abd633d9bec89446aa19a7cfc1b16fe383db31be0cb9ffcdc872bc96b4744cced64e83fea6c65efe207adfe4860fb80778588ab95a9a + languageName: node + linkType: hard + +"@tiptap/extension-gapcursor@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-gapcursor@npm:3.10.5" + peerDependencies: + "@tiptap/extensions": ^3.10.5 + checksum: 10/11ae46f2f2978f2c1f883fcac7c8d9fc212c6210648074c2f338a874d3b423d48d89caab57c5d7d890e670cdd63968f1f5caa206208afff8c56ade1f7c501bf4 + languageName: node + linkType: hard + +"@tiptap/extension-hard-break@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-hard-break@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/5d36703f3fb0aa5ffcbd1e9b402e5a725dbe2ffc39d5d71f163ce51205e9573742417746f0d680fbfd770ac4f3101b9e1b9673b81c9cf25249532ea7332a145c + languageName: node + linkType: hard + +"@tiptap/extension-heading@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-heading@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/a2aa19acabc0ceda669121af7df4f17bfc00eee9725ecad151c7743955d6d1a8efe6b026695bf021ae1089764771763dcdee4f507e7938fe5c168b2687db23ff + languageName: node + linkType: hard + +"@tiptap/extension-highlight@npm:^3.10.1, @tiptap/extension-highlight@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-highlight@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/c5010058072f84a962cc9efcabf17fea74748e025d9778f13352a1ac19f69147adc632940e6e094defd8eabeeae770b9a84acd88b391b1a27401b8469ab5d24f + languageName: node + linkType: hard + +"@tiptap/extension-horizontal-rule@npm:^3.10.1, @tiptap/extension-horizontal-rule@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-horizontal-rule@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/0836bab3c41217053c60244f162a7a7e751c429d38b4d5134a7d7c7a76d0acaceef6b9ffcb976a9d49bf972232b4563aa1d71cf22a24fd94a0fd61f57f7987fc + languageName: node + linkType: hard + +"@tiptap/extension-image@npm:^3.10.1, @tiptap/extension-image@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-image@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/2a34e75684e9289addee39fae6980b19b6618c0dfbbb79ea40748f43e8b0998d6e87bab0c5fac43328fd94bb0c32dfb12fadd898a50e5531d0a7c6f1f56a7310 + languageName: node + linkType: hard + +"@tiptap/extension-italic@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-italic@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/b442d903de338af99e718484e9b021dc371fbeb838a2e73638f5b295b230dacb16414218e3a55e22446dd4240065da770e1a634354d4848b3aee052e60a771e3 + languageName: node + linkType: hard + +"@tiptap/extension-link@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-link@npm:3.10.5" + dependencies: + linkifyjs: "npm:^4.3.2" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/09f09ba9ce65dfc3e88cbf7a2d690a331e89011d58c57d44686f9c9a98faf9220c03d4b73bf8a87c754a7c822c537b288db94af740b0344f3b14e1f1f3ed4580 + languageName: node + linkType: hard + +"@tiptap/extension-list-item@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-list-item@npm:3.10.5" + peerDependencies: + "@tiptap/extension-list": ^3.10.5 + checksum: 10/15fa06dbaccd9b5e26835889a75ede3911c5581c9886235fac65c8ed6b4ef456da7ecf9f8aa6880cda346f7e9968047526e97db7991fc137b43077a3c8df21a4 + languageName: node + linkType: hard + +"@tiptap/extension-list-keymap@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-list-keymap@npm:3.10.5" + peerDependencies: + "@tiptap/extension-list": ^3.10.5 + checksum: 10/1e00ebd89a6ee622170c93174a44abbd22cc8a92a5daed4dad71e685cc40a3825ae7edf979b169d33bfb1270412c9d16f9c3e4cc7159ecd5113b50928b0424e4 + languageName: node + linkType: hard + +"@tiptap/extension-list@npm:^3.10.1, @tiptap/extension-list@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-list@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/7a52d7abae3f26a03a9be576e7916d8ec4c23c7206be03948c822722ee20e7ab404cb2ac9d29d6999a3387475f8fb3dc1316dfd84c0f083f2c577ca28c2a7e29 + languageName: node + linkType: hard + +"@tiptap/extension-ordered-list@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-ordered-list@npm:3.10.5" + peerDependencies: + "@tiptap/extension-list": ^3.10.5 + checksum: 10/17c8cc6c4a3113e4a3bc2e189b6a19c2c027af92b1c6ec41380671258390aa2fac46671195d29def576e85d17c6fdd4465c4f70cefe262a6a0a637e59009d7b7 + languageName: node + linkType: hard + +"@tiptap/extension-paragraph@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-paragraph@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/ef0b5dad615ffd30fcdfa28f5079feea442ba9776c385ce491567a7552b1b59de8e674b5063e5390f184db897f6eb82b53cdb30ca900a3607afa24d9d5d68840 + languageName: node + linkType: hard + +"@tiptap/extension-strike@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-strike@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/54d8ea0b1f8c103ce2e9e35fedaf18036bb39d941dc6360855e67165d027ec0b0eece5fe2488acdb248534871a43dd4d99b18e046ea020ac8f2a3eb42ee61776 + languageName: node + linkType: hard + +"@tiptap/extension-subscript@npm:^3.10.1, @tiptap/extension-subscript@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-subscript@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/7f862cef396054c0943fd36f7855d8cf755db6d7ca588ffdd55229f08d14ffea3cb5705d62f96b9da80e519912b63aa62c9a12ed31bfc7846208c3bdd5948837 + languageName: node + linkType: hard + +"@tiptap/extension-superscript@npm:^3.10.1, @tiptap/extension-superscript@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-superscript@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/503232e20ef5cac9e5ab71c598256086aa43f30c1bba2e575d1f8941ac716cc6e16d572409fd4e87092dd09bd29f52cbee2d115f69174a9435baf7c0b43336f9 + languageName: node + linkType: hard + +"@tiptap/extension-text-align@npm:^3.10.1, @tiptap/extension-text-align@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-text-align@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/8288cbf18e9831725b99e3749a60eff19a758697d1373c6e89b0f4ac1446768f6289b00294799b18a2aa66300dbd9b67ae7d66c332c47cb8f08f4855d89d04ce + languageName: node + linkType: hard + +"@tiptap/extension-text@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-text@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/1fc3eee565eb15c2ce3fae378c7edb8e8055d306093bc15486c709ea0850ea869596b2f5eaef9fc2a5d6a1138df2037164dbe5d17b02956302939e5600433e76 + languageName: node + linkType: hard + +"@tiptap/extension-typography@npm:^3.10.1, @tiptap/extension-typography@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-typography@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/4c059575647a8848224704488cdfaa3b0181b9e385f7a30568c6b68c349f1fb9ba04dcd0e9b2b3bf4e0eabefab8b14f306f5f11922d2fb8185db1050e06ca891 + languageName: node + linkType: hard + +"@tiptap/extension-underline@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extension-underline@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + checksum: 10/d949a1dd93490c983f54762a6892b48707f13934a9a17490ec832ae748240416e319c24485f84ac3c34256ef924baa436392c61438bbc4b0a2607623764abe58 + languageName: node + linkType: hard + +"@tiptap/extensions@npm:^3.10.1, @tiptap/extensions@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/extensions@npm:3.10.5" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/a3def19cbebbb10d721322173265ab90498376917bf7dd1182c6047f689dbc1d0577418a51f985b4261357de20498235d49196951238eaea5f6581cf478a232d + languageName: node + linkType: hard + +"@tiptap/markdown@npm:^3.10.1": + version: 3.10.5 + resolution: "@tiptap/markdown@npm:3.10.5" + dependencies: + marked: "npm:^16.1.2" + peerDependencies: + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/db61194b89ede8843a884ffc73485ecc1f009c038333e31a92142fb36577b6665c5ee09f5a31f8d87f0b1f29d3f9f80c1ff7f0210b0e33f995d8f68d0fbc29d7 + languageName: node + linkType: hard + +"@tiptap/pm@npm:^3.10.1, @tiptap/pm@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/pm@npm:3.10.5" + dependencies: + prosemirror-changeset: "npm:^2.3.0" + prosemirror-collab: "npm:^1.3.1" + prosemirror-commands: "npm:^1.6.2" + prosemirror-dropcursor: "npm:^1.8.1" + prosemirror-gapcursor: "npm:^1.3.2" + prosemirror-history: "npm:^1.4.1" + prosemirror-inputrules: "npm:^1.4.0" + prosemirror-keymap: "npm:^1.2.2" + prosemirror-markdown: "npm:^1.13.1" + prosemirror-menu: "npm:^1.2.4" + prosemirror-model: "npm:^1.24.1" + prosemirror-schema-basic: "npm:^1.2.3" + prosemirror-schema-list: "npm:^1.5.0" + prosemirror-state: "npm:^1.4.3" + prosemirror-tables: "npm:^1.6.4" + prosemirror-trailing-node: "npm:^3.0.0" + prosemirror-transform: "npm:^1.10.2" + prosemirror-view: "npm:^1.38.1" + checksum: 10/c73fedb51906fc24fdb429d8dce04973e1d14c1eade990f3d8d31df14d70f6d0a28b02c502bf49f807536952cd72a99d9cef531ff7cb146368a7493862f1d37f languageName: node linkType: hard -"@testing-library/react@npm:^16.3.0": - version: 16.3.0 - resolution: "@testing-library/react@npm:16.3.0" +"@tiptap/react@npm:^3.10.1, @tiptap/react@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/react@npm:3.10.5" dependencies: - "@babel/runtime": "npm:^7.12.5" + "@tiptap/extension-bubble-menu": "npm:^3.10.5" + "@tiptap/extension-floating-menu": "npm:^3.10.5" + "@types/use-sync-external-store": "npm:^0.0.6" + fast-deep-equal: "npm:^3.1.3" + use-sync-external-store: "npm:^1.4.0" peerDependencies: - "@testing-library/dom": ^10.0.0 - "@types/react": ^18.0.0 || ^19.0.0 - "@types/react-dom": ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + "@types/react-dom": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + dependenciesMeta: + "@tiptap/extension-bubble-menu": optional: true - "@types/react-dom": + "@tiptap/extension-floating-menu": optional: true - checksum: 10/0ee9e31dd0d2396a924682d0e61a4ecc6bfab8eaff23dbf8a72c3c2ce22c116fa578148baeb4de75b968ef99d22e6e6aa0a00dba40286f71184918bb6bb5b06a + checksum: 10/409b283ad78b923524e37d1cd95570509ed0eaf3da4bd47066f8c434406afb39712f58e5c9f81db1611fad359e7b065652f6056fb11c286592916c12b0d7bee6 languageName: node linkType: hard -"@testing-library/user-event@npm:14.5.2, @testing-library/user-event@npm:^14.5.2": - version: 14.5.2 - resolution: "@testing-library/user-event@npm:14.5.2" +"@tiptap/starter-kit@npm:^3.10.1, @tiptap/starter-kit@npm:^3.10.5": + version: 3.10.5 + resolution: "@tiptap/starter-kit@npm:3.10.5" + dependencies: + "@tiptap/core": "npm:^3.10.5" + "@tiptap/extension-blockquote": "npm:^3.10.5" + "@tiptap/extension-bold": "npm:^3.10.5" + "@tiptap/extension-bullet-list": "npm:^3.10.5" + "@tiptap/extension-code": "npm:^3.10.5" + "@tiptap/extension-code-block": "npm:^3.10.5" + "@tiptap/extension-document": "npm:^3.10.5" + "@tiptap/extension-dropcursor": "npm:^3.10.5" + "@tiptap/extension-gapcursor": "npm:^3.10.5" + "@tiptap/extension-hard-break": "npm:^3.10.5" + "@tiptap/extension-heading": "npm:^3.10.5" + "@tiptap/extension-horizontal-rule": "npm:^3.10.5" + "@tiptap/extension-italic": "npm:^3.10.5" + "@tiptap/extension-link": "npm:^3.10.5" + "@tiptap/extension-list": "npm:^3.10.5" + "@tiptap/extension-list-item": "npm:^3.10.5" + "@tiptap/extension-list-keymap": "npm:^3.10.5" + "@tiptap/extension-ordered-list": "npm:^3.10.5" + "@tiptap/extension-paragraph": "npm:^3.10.5" + "@tiptap/extension-strike": "npm:^3.10.5" + "@tiptap/extension-text": "npm:^3.10.5" + "@tiptap/extension-underline": "npm:^3.10.5" + "@tiptap/extensions": "npm:^3.10.5" + "@tiptap/pm": "npm:^3.10.5" + checksum: 10/3005464bc831cd7fb3cb083528c2000951625b0242467a6ed35c46736e5d359408859ef519cd30f3cae2bbca43c326576205a1b504fb0ebd47d53a31fd17a8d0 + languageName: node + linkType: hard + +"@tiptap/suggestion@npm:^3.10.2": + version: 3.10.5 + resolution: "@tiptap/suggestion@npm:3.10.5" peerDependencies: - "@testing-library/dom": ">=7.21.4" - checksum: 10/49821459d81c6bc435d97128d6386ca24f1e4b3ba8e46cb5a96fe3643efa6e002d88c1b02b7f2ec58da593e805c59b78d7fdf0db565c1f02ba782f63ee984040 + "@tiptap/core": ^3.10.5 + "@tiptap/pm": ^3.10.5 + checksum: 10/a8ca10a5c2fc757d8a97a7ad0fc96f8f0a197a16178f0a69fc9dc4423cb239dc85cf48141f0bccd5e116230e5d039959d536214e75b74c3506a2111330bd2ff5 languageName: node linkType: hard @@ -6101,6 +7209,29 @@ __metadata: languageName: node linkType: hard +"@types/linkify-it@npm:^5": + version: 5.0.0 + resolution: "@types/linkify-it@npm:5.0.0" + checksum: 10/c3919044d4876f9d71d037e861745cd2485c95ac8c36a4fa67b132d4e60eb1d067e123cc7965c9cf5110eea351517d767f0d306af5e9147d6d0af87bc374ddcf + languageName: node + linkType: hard + +"@types/lodash.throttle@npm:^4.1.9": + version: 4.1.9 + resolution: "@types/lodash.throttle@npm:4.1.9" + dependencies: + "@types/lodash": "npm:*" + checksum: 10/6d330072387f062d408747f0dbe62869820ee3f3fbec43965f703ce9c9083e4ff9082faa4fe92aea000d6367b7645955e9c8db6a4e04e6bd769697fdd19c12b1 + languageName: node + linkType: hard + +"@types/lodash@npm:*": + version: 4.17.20 + resolution: "@types/lodash@npm:4.17.20" + checksum: 10/8cd8ad3bd78d2e06a93ae8d6c9907981d5673655fec7cb274a4d9a59549aab5bb5b3017361280773b8990ddfccf363e14d1b37c97af8a9fe363de677f9a61524 + languageName: node + linkType: hard + "@types/lodash@npm:^4.14.167, @types/lodash@npm:^4.17.7": version: 4.17.10 resolution: "@types/lodash@npm:4.17.10" @@ -6108,6 +7239,16 @@ __metadata: languageName: node linkType: hard +"@types/markdown-it@npm:^14.0.0": + version: 14.1.2 + resolution: "@types/markdown-it@npm:14.1.2" + dependencies: + "@types/linkify-it": "npm:^5" + "@types/mdurl": "npm:^2" + checksum: 10/ca2f239c8d59610b9f936fd40261a6ccf2fa1ae27a21816c031e5712542dcf9ee01e2fe29b31118df90716e11ade54e47d92a498e9b6488800e77ca8827255a2 + languageName: node + linkType: hard + "@types/mathjax@npm:^0.0.40": version: 0.0.40 resolution: "@types/mathjax@npm:0.0.40" @@ -6133,6 +7274,13 @@ __metadata: languageName: node linkType: hard +"@types/mdurl@npm:^2": + version: 2.0.0 + resolution: "@types/mdurl@npm:2.0.0" + checksum: 10/78746e96c655ceed63db06382da466fd52c7e9dc54d60b12973dfdd110cae06b9439c4b90e17bb8d4461109184b3ea9f3e9f96b3e4bf4aa9fe18b6ac35f283c8 + languageName: node + linkType: hard + "@types/mdx@npm:^2.0.0": version: 2.0.13 resolution: "@types/mdx@npm:2.0.13" @@ -6390,6 +7538,13 @@ __metadata: languageName: node linkType: hard +"@types/use-sync-external-store@npm:^0.0.6": + version: 0.0.6 + resolution: "@types/use-sync-external-store@npm:0.0.6" + checksum: 10/a95ce330668501ad9b1c5b7f2b14872ad201e552a0e567787b8f1588b22c7040c7c3d80f142cbb9f92d13c4ea41c46af57a20f2af4edf27f224d352abcfe4049 + languageName: node + linkType: hard + "@types/uuid@npm:^9.0.1": version: 9.0.8 resolution: "@types/uuid@npm:9.0.8" @@ -7421,6 +8576,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.2.4": + version: 1.2.6 + resolution: "aria-hidden@npm:1.2.6" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10/1914e5a36225dccdb29f0b88cc891eeca736cdc5b0c905ab1437b90b28b5286263ed3a221c75b7dc788f25b942367be0044b2ac8ccf073a72e07a50b1d964202 + languageName: node + linkType: hard + "aria-query@npm:5.3.0": version: 5.3.0 resolution: "aria-query@npm:5.3.0" @@ -7633,13 +8797,6 @@ __metadata: languageName: node linkType: hard -"async_hooks@npm:^1.0.0": - version: 1.0.0 - resolution: "async_hooks@npm:1.0.0" - checksum: 10/d31a4cb971a980b095cde18879598d1b196b32cb38905d1c2ed0aca7ebc7e040344af52fe2dedba410a1eaf4bc12d63db797b91285048387cac78319a6366901 - languageName: node - linkType: hard - "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -8097,6 +9254,13 @@ __metadata: languageName: node linkType: hard +"buffer-builder@npm:^0.2.0": + version: 0.2.0 + resolution: "buffer-builder@npm:0.2.0" + checksum: 10/16bd9eb8ac6630a05441bcb56522e956ae6a0724371ecc49b9a6bc10d35690489140df73573d0577e1e85c875737e560a4e2e67521fddd14714ddf4e0097d0ec + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -8390,6 +9554,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.0": + version: 4.0.3 + resolution: "chokidar@npm:4.0.3" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10/bf2a575ea5596000e88f5db95461a9d59ad2047e939d5a4aac59dd472d126be8f1c1ff3c7654b477cf532d18f42a97279ef80ee847972fd2a25410bf00b80b59 + languageName: node + linkType: hard + "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -8589,6 +9762,13 @@ __metadata: languageName: node linkType: hard +"colorjs.io@npm:^0.5.0": + version: 0.5.2 + resolution: "colorjs.io@npm:0.5.2" + checksum: 10/a6f6345865b177d19481008cb299c46ec9ff1fd206f472cd9ef69ddbca65832c81237b19fdcd24f3f9540c3e6343a22eb486cd800f5eab9815ce7c98c16a0f0e + languageName: node + linkType: hard + "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -8857,6 +10037,13 @@ __metadata: languageName: node linkType: hard +"crelt@npm:^1.0.0": + version: 1.0.6 + resolution: "crelt@npm:1.0.6" + checksum: 10/5ed326ca6bd243b1dba6b943f665b21c2c04be03271824bc48f20dba324b0f8233e221f8c67312526d24af2b1243c023dc05a41bd8bd05d1a479fd2c72fb39c3 + languageName: node + linkType: hard + "cross-fetch@npm:^4.0.0": version: 4.0.0 resolution: "cross-fetch@npm:4.0.0" @@ -9296,6 +10483,15 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^1.0.3": + version: 1.0.3 + resolution: "detect-libc@npm:1.0.3" + bin: + detect-libc: ./bin/detect-libc.js + checksum: 10/3849fe7720feb153e4ac9407086956e073f1ce1704488290ef0ca8aab9430a8d48c8a9f8351889e7cdc64e5b1128589501e4fef48f3a4a49ba92cd6d112d0757 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.3, detect-libc@npm:^2.0.4": version: 2.0.4 resolution: "detect-libc@npm:2.0.4" @@ -9317,6 +10513,13 @@ __metadata: languageName: node linkType: hard +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: 10/e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449 + languageName: node + linkType: hard + "devlop@npm:^1.0.0, devlop@npm:^1.1.0": version: 1.1.0 resolution: "devlop@npm:1.1.0" @@ -9754,6 +10957,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^4.4.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 10/ede2a35c9bce1aeccd055a1b445d41c75a14a2bb1cd22e242f20cf04d236cdcd7f9c859eb83f76885327bfae0c25bf03303665ee1ce3d47c5927b98b0e3e3d48 + languageName: node + linkType: hard + "entities@npm:^6.0.0": version: 6.0.1 resolution: "entities@npm:6.0.1" @@ -11317,6 +12527,13 @@ __metadata: languageName: node linkType: hard +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: 10/ad5104871d114a694ecc506a2d406e2331beccb961fe1e110dc25556b38bcdbf399a823a8a375976cd8889668156a9561e12ebe3fa6a4c6ba169c8466c2ff868 + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -12131,6 +13348,13 @@ __metadata: languageName: node linkType: hard +"immutable@npm:^5.0.2": + version: 5.1.4 + resolution: "immutable@npm:5.1.4" + checksum: 10/0655b33af249ff99c7a56f9e6d7aee632af2dc25758710ddf224bda645f66dd2dd98119c0d86986895ea52cc889b6c5127a848c6fba21aadabdc4c5ead04be2b + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -13859,6 +15083,22 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: "npm:^2.0.0" + checksum: 10/ef3b7609dda6ec0c0be8a7b879cea195f0d36387b0011660cd6711bba0ad82137f59b458b7e703ec74f11d88e7c1328e2ad9b855a8500c0ded67461a8c4519e6 + languageName: node + linkType: hard + +"linkifyjs@npm:^4.3.2": + version: 4.3.2 + resolution: "linkifyjs@npm:4.3.2" + checksum: 10/b03477486658d1e5531bf65ee1fdc0f79423594e689184c67b8a63c75d9f35d1cd0344edd97d5799502cde4f3163d620e2cbd9e72ad718c6a95084177c004386 + languageName: node + linkType: hard + "load-plugin@npm:^6.0.0": version: 6.0.3 resolution: "load-plugin@npm:6.0.3" @@ -13949,6 +15189,13 @@ __metadata: languageName: node linkType: hard +"lodash.throttle@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.throttle@npm:4.1.1" + checksum: 10/9be9fb2ffd686c20543167883305542f4564062a5f712a40e8c6f2f0d9fd8254a6e9d801c2470b1b24e0cdf2ae83c1277b55aa0fb4799a2db6daf545f53820e1 + languageName: node + linkType: hard + "lodash.truncate@npm:^4.4.2": version: 4.4.2 resolution: "lodash.truncate@npm:4.4.2" @@ -14065,26 +15312,44 @@ __metadata: "@emotion/cache": "npm:^11.13.1" "@emotion/styled": "npm:^11.11.0" "@faker-js/faker": "npm:^10.0.0" + "@floating-ui/react": "npm:^0.27.16" "@mitodl/course-search-utils": "npm:^3.5.0" - "@mitodl/mitxonline-api-axios": "npm:^2025.11.5" + "@mitodl/mitxonline-api-axios": "npm:^2025.10.21" "@mitodl/smoot-design": "npm:^6.17.1" "@next/bundle-analyzer": "npm:^14.2.15" + "@radix-ui/react-dropdown-menu": "npm:^2.1.16" + "@radix-ui/react-popover": "npm:^1.1.15" "@react-pdf/renderer": "npm:^4.3.0" - "@remixicon/react": "npm:^4.2.0" + "@remixicon/react": "npm:^4.7.0" "@sentry/nextjs": "npm:^10.0.0" "@tanstack/react-query": "npm:^5.66" "@testing-library/jest-dom": "npm:^6.4.8" "@testing-library/react": "npm:^16.3.0" "@testing-library/user-event": "npm:^14.5.2" + "@tiptap/extension-document": "npm:^3.10.2" + "@tiptap/extension-highlight": "npm:^3.10.1" + "@tiptap/extension-horizontal-rule": "npm:^3.10.1" + "@tiptap/extension-image": "npm:^3.10.1" + "@tiptap/extension-list": "npm:^3.10.1" + "@tiptap/extension-subscript": "npm:^3.10.1" + "@tiptap/extension-superscript": "npm:^3.10.1" + "@tiptap/extension-text-align": "npm:^3.10.1" + "@tiptap/extension-typography": "npm:^3.10.1" + "@tiptap/extensions": "npm:^3.10.1" + "@tiptap/markdown": "npm:^3.10.1" + "@tiptap/pm": "npm:^3.10.1" + "@tiptap/react": "npm:^3.10.1" + "@tiptap/starter-kit": "npm:^3.10.1" + "@tiptap/suggestion": "npm:^3.10.2" "@types/jest": "npm:^29.5.12" "@types/lodash": "npm:^4.17.7" + "@types/lodash.throttle": "npm:^4.1.9" "@types/node": "npm:^22.0.0" "@types/react": "npm:^19" "@types/react-dom": "npm:^19" "@types/react-slick": "npm:^0.23.13" "@types/slick-carousel": "npm:^1" api: "workspace:*" - async_hooks: "npm:^1.0.0" classnames: "npm:^2.5.1" eslint: "npm:8.57.1" eslint-config-next: "npm:^14.2.7" @@ -14097,6 +15362,7 @@ __metadata: jest-next-dynamic-ts: "npm:^0.1.1" jsdom: "npm:^27" lodash: "npm:^4.17.21" + lodash.throttle: "npm:^4.1.1" moment: "npm:^2.30.1" next: "npm:^15.5.2" next-nprogress-bar: "npm:^2.4.2" @@ -14107,7 +15373,9 @@ __metadata: posthog-js: "npm:^1.157.2" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" + react-hotkeys-hook: "npm:^5.2.1" react-slick: "npm:^0.30.2" + sass: "npm:^1.93.3" sharp: "npm:0.34.4" slick-carousel: "npm:^1.8.1" tiny-invariant: "npm:^1.3.3" @@ -14179,6 +15447,22 @@ __metadata: languageName: node linkType: hard +"markdown-it@npm:^14.0.0": + version: 14.1.0 + resolution: "markdown-it@npm:14.1.0" + dependencies: + argparse: "npm:^2.0.1" + entities: "npm:^4.4.0" + linkify-it: "npm:^5.0.0" + mdurl: "npm:^2.0.0" + punycode.js: "npm:^2.3.1" + uc.micro: "npm:^2.1.0" + bin: + markdown-it: bin/markdown-it.mjs + checksum: 10/f34f921be178ed0607ba9e3e27c733642be445e9bb6b1dba88da7aafe8ba1bc5d2f1c3aa8f3fc33b49a902da4e4c08c2feadfafb290b8c7dda766208bb6483a9 + languageName: node + linkType: hard + "markdown-table@npm:^3.0.0": version: 3.0.3 resolution: "markdown-table@npm:3.0.3" @@ -14195,6 +15479,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^16.1.2": + version: 16.4.2 + resolution: "marked@npm:16.4.2" + bin: + marked: bin/marked.js + checksum: 10/6e40e40661dce97e271198daa2054fc31e6445892a735e416c248fba046bdfa4573cafa08dc254529f105e7178a34485eb7f82573979cfb377a4530f66e79187 + languageName: node + linkType: hard + "material-ui-popup-state@npm:^5.1.0": version: 5.3.1 resolution: "material-ui-popup-state@npm:5.3.1" @@ -14520,6 +15813,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 10/1720349d4a53e401aa993241368e35c0ad13d816ad0b28388928c58ca9faa0cf755fa45f18ccbf64f4ce54a845a50ddce5c84e4016897b513096a68dac4b0158 + languageName: node + linkType: hard + "media-engine@npm:^1.0.3": version: 1.0.3 resolution: "media-engine@npm:1.0.3" @@ -15072,7 +16372,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -15497,6 +16797,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10/ee1e1ed6284a2f8cd1d59ac6175ecbabf8978dcf570345e9a8095a9d0a2b9ced591074ae77f9009287b00c402352b38aa9322a34f2199cdc9f567b842a636b94 + languageName: node + linkType: hard + "node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -15808,11 +17117,14 @@ __metadata: "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@faker-js/faker": "npm:^10.0.0" + "@floating-ui/react": "npm:^0.27.16" "@mui/base": "npm:5.0.0-beta.70" "@mui/lab": "npm:6.0.0-dev.240424162023-9968b4889d" "@mui/material": "npm:^6.4.5" "@mui/material-nextjs": "npm:^6.4.3" "@mui/system": "npm:^6.4.3" + "@radix-ui/react-dropdown-menu": "npm:^2.1.16" + "@radix-ui/react-popover": "npm:^1.1.15" "@remixicon/react": "npm:^4.2.0" "@storybook/addon-actions": "npm:^8.2.9" "@storybook/addon-essentials": "npm:^8.2.9" @@ -15830,6 +17142,19 @@ __metadata: "@testing-library/dom": "npm:^10.4.0" "@testing-library/react": "npm:^16.3.0" "@testing-library/user-event": "npm:^14.5.2" + "@tiptap/extension-highlight": "npm:^3.10.5" + "@tiptap/extension-horizontal-rule": "npm:^3.10.5" + "@tiptap/extension-image": "npm:^3.10.5" + "@tiptap/extension-list": "npm:^3.10.5" + "@tiptap/extension-subscript": "npm:^3.10.5" + "@tiptap/extension-superscript": "npm:^3.10.5" + "@tiptap/extension-text-align": "npm:^3.10.5" + "@tiptap/extension-typography": "npm:^3.10.5" + "@tiptap/extensions": "npm:^3.10.5" + "@tiptap/pm": "npm:^3.10.5" + "@tiptap/react": "npm:^3.10.5" + "@tiptap/starter-kit": "npm:^3.10.5" + "@types/lodash.throttle": "npm:^4.1.9" "@types/react-dom": "npm:^19" "@types/react-slick": "npm:^0" "@types/tinycolor2": "npm:^1.4.6" @@ -15839,6 +17164,7 @@ __metadata: embla-carousel-react: "npm:^8.6.0" embla-carousel-wheel-gestures: "npm:^8.0.2" lodash: "npm:^4.17.21" + lodash.throttle: "npm:^4.1.1" material-ui-popup-state: "npm:^5.1.0" next: "npm:^15.5.2" ol-test-utilities: "npm:0.0.0" @@ -15846,9 +17172,12 @@ __metadata: prop-types: "npm:^15.8.1" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" + react-hotkeys-hook: "npm:^5.2.1" react-select: "npm:^5.7.7" react-share: "npm:^5.0.3" react-slick: "npm:^0.30.2" + sass: "npm:^1.93.3" + sass-embedded: "npm:^1.93.3" storybook: "npm:^8.2.9" tiny-invariant: "npm:^1.3.1" tinycolor2: "npm:^1.6.0" @@ -15996,6 +17325,13 @@ __metadata: languageName: node linkType: hard +"orderedmap@npm:^2.0.0": + version: 2.1.1 + resolution: "orderedmap@npm:2.1.1" + checksum: 10/082cf970b0b66d1c5a904b07880534092ce8a2f2eea7a52cf111f6c956210fa88226c13866aef4d22a3abe56924f21ead12f7ee8c1dfaf2f63d897a4e7c23328 + languageName: node + linkType: hard + "os-browserify@npm:^0.3.0": version: 0.3.0 resolution: "os-browserify@npm:0.3.0" @@ -16704,69 +18040,263 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.3": - version: 2.0.3 - resolution: "progress@npm:2.0.3" - checksum: 10/e6f0bcb71f716eee9dfac0fe8a2606e3704d6a64dd93baaf49fbadbc8499989a610fe14cf1bc6f61b6d6653c49408d94f4a94e124538084efd8e4cf525e0293d +"progress@npm:^2.0.3": + version: 2.0.3 + resolution: "progress@npm:2.0.3" + checksum: 10/e6f0bcb71f716eee9dfac0fe8a2606e3704d6a64dd93baaf49fbadbc8499989a610fe14cf1bc6f61b6d6653c49408d94f4a94e124538084efd8e4cf525e0293d + languageName: node + linkType: hard + +"promise-inflight@npm:^1.0.1": + version: 1.0.1 + resolution: "promise-inflight@npm:1.0.1" + checksum: 10/1560d413ea20c5a74f3631d39ba8cbd1972b9228072a755d01e1f5ca5110382d9af76a1582d889445adc6e75bb5ac4886b56dc4b6eae51b30145d7bb1ac7505b + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10/96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 + languageName: node + linkType: hard + +"prompts@npm:2.4.2, prompts@npm:^2.0.1": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: "npm:^3.0.3" + sisteransi: "npm:^1.0.5" + checksum: 10/c52536521a4d21eff4f2f2aa4572446cad227464066365a7167e52ccf8d9839c099f9afec1aba0eed3d5a2514b3e79e0b3e7a1dc326b9acde6b75d27ed74b1a9 + languageName: node + linkType: hard + +"prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: "npm:^1.4.0" + object-assign: "npm:^4.1.1" + react-is: "npm:^16.13.1" + checksum: 10/7d959caec002bc964c86cdc461ec93108b27337dabe6192fb97d69e16a0c799a03462713868b40749bfc1caf5f57ef80ac3e4ffad3effa636ee667582a75e2c0 + languageName: node + linkType: hard + +"property-expr@npm:^2.0.5": + version: 2.0.6 + resolution: "property-expr@npm:2.0.6" + checksum: 10/89977f4bb230736c1876f460dd7ca9328034502fd92e738deb40516d16564b850c0bbc4e052c3df88b5b8cd58e51c93b46a94bea049a3f23f4a022c038864cab + languageName: node + linkType: hard + +"property-information@npm:^6.0.0": + version: 6.5.0 + resolution: "property-information@npm:6.5.0" + checksum: 10/fced94f3a09bf651ad1824d1bdc8980428e3e480e6d01e98df6babe2cc9d45a1c52eee9a7736d2006958f9b394eb5964dedd37e23038086ddc143fc2fd5e426c + languageName: node + linkType: hard + +"property-information@npm:^7.0.0": + version: 7.0.0 + resolution: "property-information@npm:7.0.0" + checksum: 10/55f443088456cddc2fe499d6f5895e68cbd465e39dc318ecc63a0d2432d1b918f51fb6d13f8b1adf8a78337bc4e608baa6e46afbe0c6d50d2e38588b2c409f86 + languageName: node + linkType: hard + +"prosemirror-changeset@npm:^2.3.0": + version: 2.3.1 + resolution: "prosemirror-changeset@npm:2.3.1" + dependencies: + prosemirror-transform: "npm:^1.0.0" + checksum: 10/a951daee431b9ff6a2aa24ce1eb755ef3c1fba72191760289ed9f91abbccb8e98a5c24697598b93df496662509bfd0345a52ed9a63d9535190ba3a1802a53c10 + languageName: node + linkType: hard + +"prosemirror-collab@npm:^1.3.1": + version: 1.3.1 + resolution: "prosemirror-collab@npm:1.3.1" + dependencies: + prosemirror-state: "npm:^1.0.0" + checksum: 10/6b1ccc52841fbb62a39ef0fb8da2d731381030609ea7a0ba7d533b1937d56fe4b91344e79c023e790bed5392efe9f917c41c8434e0a379dc1dc842ba83594e34 + languageName: node + linkType: hard + +"prosemirror-commands@npm:^1.0.0, prosemirror-commands@npm:^1.6.2": + version: 1.7.1 + resolution: "prosemirror-commands@npm:1.7.1" + dependencies: + prosemirror-model: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.10.2" + checksum: 10/60efe77a9a6b9f06d66442980946f49a4804c24a7aca06ee4d55333a6c55d0f1e3189613bc27f31d16496133969a454f29f071caa8ef2d38126aefbc36f81c4a + languageName: node + linkType: hard + +"prosemirror-dropcursor@npm:^1.8.1": + version: 1.8.2 + resolution: "prosemirror-dropcursor@npm:1.8.2" + dependencies: + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.1.0" + prosemirror-view: "npm:^1.1.0" + checksum: 10/02349c56152d0261c61d462b07684bb8179ab0ea488ab333dadcc9181b9ec8d5aa625feb2c54088090bd8f4e540321219938a0642d8ed043fea8a11f371ef058 + languageName: node + linkType: hard + +"prosemirror-gapcursor@npm:^1.3.2": + version: 1.4.0 + resolution: "prosemirror-gapcursor@npm:1.4.0" + dependencies: + prosemirror-keymap: "npm:^1.0.0" + prosemirror-model: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-view: "npm:^1.0.0" + checksum: 10/ec17d7ca4d9b134d8db04180a9d399a0552373d6a7491fbf1720c15b5ca6d6f617485dcabed774961cc9b1ce01de99796505b30a0fcb8372f9cdff8966b09a7f + languageName: node + linkType: hard + +"prosemirror-history@npm:^1.0.0, prosemirror-history@npm:^1.4.1": + version: 1.4.1 + resolution: "prosemirror-history@npm:1.4.1" + dependencies: + prosemirror-state: "npm:^1.2.2" + prosemirror-transform: "npm:^1.0.0" + prosemirror-view: "npm:^1.31.0" + rope-sequence: "npm:^1.3.0" + checksum: 10/7ac68fc8233dcd159bb15c2aaf542fd9aa0524b50523b24de6c8209b1f5eae9545f7fa82d584c93e68b1e910bcae5e07bee1085094aca4c565c607cf737c39b8 + languageName: node + linkType: hard + +"prosemirror-inputrules@npm:^1.4.0": + version: 1.5.1 + resolution: "prosemirror-inputrules@npm:1.5.1" + dependencies: + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.0.0" + checksum: 10/81f08415a39ad795a2a097c8667eb86e4473eca1389c793139d6bacbb69be740732e8ff8f21b2808130bacd7f53a1b0b7621b48a22ff44d902995b41654e4b80 + languageName: node + linkType: hard + +"prosemirror-keymap@npm:^1.0.0, prosemirror-keymap@npm:^1.2.2": + version: 1.2.3 + resolution: "prosemirror-keymap@npm:1.2.3" + dependencies: + prosemirror-state: "npm:^1.0.0" + w3c-keyname: "npm:^2.2.0" + checksum: 10/acb251b03f57920282342bf404755de6ff68e150aaf252aca9bbeb63bbba8f53e03091095d399def979c95db0649620a22a47a332418c3a2408a0f43737b18d9 + languageName: node + linkType: hard + +"prosemirror-markdown@npm:^1.13.1": + version: 1.13.2 + resolution: "prosemirror-markdown@npm:1.13.2" + dependencies: + "@types/markdown-it": "npm:^14.0.0" + markdown-it: "npm:^14.0.0" + prosemirror-model: "npm:^1.25.0" + checksum: 10/805f5b5b246250ebd14aedb3de5b683c637c58e0ecf13b6a6fedcbec7e761700e1d9c1371d5a24b10577cd574db5a240a883175b80a87fec24165c16c47ca9aa + languageName: node + linkType: hard + +"prosemirror-menu@npm:^1.2.4": + version: 1.2.5 + resolution: "prosemirror-menu@npm:1.2.5" + dependencies: + crelt: "npm:^1.0.0" + prosemirror-commands: "npm:^1.0.0" + prosemirror-history: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + checksum: 10/68ccff3793906a70ef9e64d953027cf231c045119e62ef37a4631f012066c546bee96454ca98103382bf8699e9040e2e6551c518259543ffb149d4d6609114b5 + languageName: node + linkType: hard + +"prosemirror-model@npm:^1.0.0, prosemirror-model@npm:^1.20.0, prosemirror-model@npm:^1.21.0, prosemirror-model@npm:^1.24.1, prosemirror-model@npm:^1.25.0": + version: 1.25.4 + resolution: "prosemirror-model@npm:1.25.4" + dependencies: + orderedmap: "npm:^2.0.0" + checksum: 10/63c5d6dd3b70e42650f07b4a2ed87e7442291b1f95a9930bf4ff2f7c6a1228e95db0e996cdabed667f4cbaf67c3ee290e19b91a31dd199da95477ac4d1b868e3 languageName: node linkType: hard -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 10/1560d413ea20c5a74f3631d39ba8cbd1972b9228072a755d01e1f5ca5110382d9af76a1582d889445adc6e75bb5ac4886b56dc4b6eae51b30145d7bb1ac7505b +"prosemirror-schema-basic@npm:^1.2.3": + version: 1.2.4 + resolution: "prosemirror-schema-basic@npm:1.2.4" + dependencies: + prosemirror-model: "npm:^1.25.0" + checksum: 10/51972732657b7eca6d0fc294d980f7ef0079acee962451280ef79e59790717a1f65e9227364de8780caa8e1e5c7d48a57b3b58488d24e23b8b035ea7ec0ec37a languageName: node linkType: hard -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" +"prosemirror-schema-list@npm:^1.5.0": + version: 1.5.1 + resolution: "prosemirror-schema-list@npm:1.5.1" dependencies: - err-code: "npm:^2.0.2" - retry: "npm:^0.12.0" - checksum: 10/96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 + prosemirror-model: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.7.3" + checksum: 10/eaf308093ecbce7fc7d0e3c3653faa120afb3679bdb20c99f79c75c90b4f57d61c6580ea162dd0af274013e98c5d4529ecbe6b9bd7e66e7d6d6b5be6aae0f052 languageName: node linkType: hard -"prompts@npm:2.4.2, prompts@npm:^2.0.1": - version: 2.4.2 - resolution: "prompts@npm:2.4.2" +"prosemirror-state@npm:^1.0.0, prosemirror-state@npm:^1.2.2, prosemirror-state@npm:^1.4.3": + version: 1.4.4 + resolution: "prosemirror-state@npm:1.4.4" dependencies: - kleur: "npm:^3.0.3" - sisteransi: "npm:^1.0.5" - checksum: 10/c52536521a4d21eff4f2f2aa4572446cad227464066365a7167e52ccf8d9839c099f9afec1aba0eed3d5a2514b3e79e0b3e7a1dc326b9acde6b75d27ed74b1a9 + prosemirror-model: "npm:^1.0.0" + prosemirror-transform: "npm:^1.0.0" + prosemirror-view: "npm:^1.27.0" + checksum: 10/90e66cbc49f2eceeb174f5184ed94df432a8b83866ab74ef602d21555dd151eb1ff9a0794b12d98384981601ebaa885d930f6c667165ff969874944a2f9e2488 languageName: node linkType: hard -"prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": - version: 15.8.1 - resolution: "prop-types@npm:15.8.1" +"prosemirror-tables@npm:^1.6.4": + version: 1.8.1 + resolution: "prosemirror-tables@npm:1.8.1" dependencies: - loose-envify: "npm:^1.4.0" - object-assign: "npm:^4.1.1" - react-is: "npm:^16.13.1" - checksum: 10/7d959caec002bc964c86cdc461ec93108b27337dabe6192fb97d69e16a0c799a03462713868b40749bfc1caf5f57ef80ac3e4ffad3effa636ee667582a75e2c0 + prosemirror-keymap: "npm:^1.2.2" + prosemirror-model: "npm:^1.25.0" + prosemirror-state: "npm:^1.4.3" + prosemirror-transform: "npm:^1.10.3" + prosemirror-view: "npm:^1.39.1" + checksum: 10/d2bc4cd5e17cf51d5a822b1a5db318a5e0e7784a76ae54a87eb43a50359dda8f181b9bee54c59c72b29b7d37ec417a70c16085093810752ee03296b989eb546b languageName: node linkType: hard -"property-expr@npm:^2.0.5": - version: 2.0.6 - resolution: "property-expr@npm:2.0.6" - checksum: 10/89977f4bb230736c1876f460dd7ca9328034502fd92e738deb40516d16564b850c0bbc4e052c3df88b5b8cd58e51c93b46a94bea049a3f23f4a022c038864cab +"prosemirror-trailing-node@npm:^3.0.0": + version: 3.0.0 + resolution: "prosemirror-trailing-node@npm:3.0.0" + dependencies: + "@remirror/core-constants": "npm:3.0.0" + escape-string-regexp: "npm:^4.0.0" + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + checksum: 10/044b199b8001373c1bd4c1573876597840df89e66c1f02497a8bb4f2885ebe830faa9764e1269ed6c24bf2fde06ad5f40322afde648ae331d4663f531000adaa languageName: node linkType: hard -"property-information@npm:^6.0.0": - version: 6.5.0 - resolution: "property-information@npm:6.5.0" - checksum: 10/fced94f3a09bf651ad1824d1bdc8980428e3e480e6d01e98df6babe2cc9d45a1c52eee9a7736d2006958f9b394eb5964dedd37e23038086ddc143fc2fd5e426c +"prosemirror-transform@npm:^1.0.0, prosemirror-transform@npm:^1.1.0, prosemirror-transform@npm:^1.10.2, prosemirror-transform@npm:^1.10.3, prosemirror-transform@npm:^1.7.3": + version: 1.10.4 + resolution: "prosemirror-transform@npm:1.10.4" + dependencies: + prosemirror-model: "npm:^1.21.0" + checksum: 10/a5835bdd7e66e455f52115d63b48a1b9c6a0741fdefa277894c7ed4f5baf3570b226a135b11036abaa7fe7c5e98de466692779d356e65302b8e929a13a1c4391 languageName: node linkType: hard -"property-information@npm:^7.0.0": - version: 7.0.0 - resolution: "property-information@npm:7.0.0" - checksum: 10/55f443088456cddc2fe499d6f5895e68cbd465e39dc318ecc63a0d2432d1b918f51fb6d13f8b1adf8a78337bc4e608baa6e46afbe0c6d50d2e38588b2c409f86 +"prosemirror-view@npm:^1.0.0, prosemirror-view@npm:^1.1.0, prosemirror-view@npm:^1.27.0, prosemirror-view@npm:^1.31.0, prosemirror-view@npm:^1.38.1, prosemirror-view@npm:^1.39.1": + version: 1.41.3 + resolution: "prosemirror-view@npm:1.41.3" + dependencies: + prosemirror-model: "npm:^1.20.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.1.0" + checksum: 10/2cd8c29c28f6061f31cac371667baa47a7f20705897f31b8ba4fb6d2dc226f9ce38a91068e3faf8b40c7f76727c3953df293223dc6aee0079b5dc3a44ccb7dca languageName: node linkType: hard @@ -16808,6 +18338,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 10/f0e946d1edf063f9e3d30a32ca86d8ff90ed13ca40dad9c75d37510a04473340cfc98db23a905cc1e517b1e9deb0f6021dce6f422ace235c60d3c9ac47c5a16a + languageName: node + linkType: hard + "punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" @@ -17018,6 +18555,16 @@ __metadata: languageName: node linkType: hard +"react-hotkeys-hook@npm:^5.2.1": + version: 5.2.1 + resolution: "react-hotkeys-hook@npm:5.2.1" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/4581b6bc2496954b5bd3c41924aba28f0d2e4a996476f38cce80e777b6a63f51e024992a11c2d7d4b4abe4815e0bba04fa692a0ed28aeb5f58df7852a77aab70 + languageName: node + linkType: hard + "react-is@npm:18.1.0": version: 18.1.0 resolution: "react-is@npm:18.1.0" @@ -17082,6 +18629,41 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll-bar@npm:^2.3.7": + version: 2.3.8 + resolution: "react-remove-scroll-bar@npm:2.3.8" + dependencies: + react-style-singleton: "npm:^2.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/6c0f8cff98b9f49a4ee2263f1eedf12926dced5ce220fbe83bd93544460e2a7ec8ec39b35d1b2a75d2fced0b2d64afeb8e66f830431ca896e05a20585f9fc350 + languageName: node + linkType: hard + +"react-remove-scroll@npm:^2.6.3": + version: 2.7.1 + resolution: "react-remove-scroll@npm:2.7.1" + dependencies: + react-remove-scroll-bar: "npm:^2.3.7" + react-style-singleton: "npm:^2.2.3" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.3" + use-sidecar: "npm:^1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/5e571ba35ba527047c54c9c4a271363167770556fb85ee45ead8310673197719425cc8f7a2b7f672abf530294c41c8c34bdae325a571994cc1e694b664b52734 + languageName: node + linkType: hard + "react-select@npm:^5.7.7": version: 5.8.1 resolution: "react-select@npm:5.8.1" @@ -17130,6 +18712,22 @@ __metadata: languageName: node linkType: hard +"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": + version: 2.2.3 + resolution: "react-style-singleton@npm:2.2.3" + dependencies: + get-nonce: "npm:^1.0.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/62498094ff3877a37f351b29e6cad9e38b2eb1ac3c0cb27ebf80aee96554f80b35e17bdb552bcd7ac8b7cb9904fea93ea5668f2057c73d38f90b5d46bb9b27ab + languageName: node + linkType: hard + "react-transition-group@npm:^4.3.0, react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" @@ -17220,6 +18818,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.1.2 + resolution: "readdirp@npm:4.1.2" + checksum: 10/7b817c265940dba90bb9c94d82920d76c3a35ea2d67f9f9d8bd936adcfe02d50c802b14be3dd2e725e002dddbe2cc1c7a0edfb1bc3a365c9dfd5a61e612eea1e + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -17766,6 +19371,13 @@ __metadata: languageName: node linkType: hard +"rope-sequence@npm:^1.3.0": + version: 1.3.4 + resolution: "rope-sequence@npm:1.3.4" + checksum: 10/57b5dd8c28ece05bb5f33eea6ea56facb00d4893269bb83aa8656f69065c1bc0707ec9bb816bce0e5f4d489d88942c7f0f0a1c3655773753ef158c9dd0e9456d + languageName: node + linkType: hard + "rrweb-cssom@npm:^0.8.0": version: 0.8.0 resolution: "rrweb-cssom@npm:0.8.0" @@ -17782,6 +19394,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:^7.4.0": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d + languageName: node + linkType: hard + "sade@npm:^1.7.3": version: 1.8.1 resolution: "sade@npm:1.8.1" @@ -17835,6 +19456,209 @@ __metadata: languageName: node linkType: hard +"sass-embedded-all-unknown@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-all-unknown@npm:1.93.3" + dependencies: + sass: "npm:1.93.3" + conditions: (!cpu=arm | !cpu=arm64 | !cpu=riscv64 | !cpu=x64) + languageName: node + linkType: hard + +"sass-embedded-android-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-arm64@npm:1.93.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-android-arm@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-arm@npm:1.93.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-android-riscv64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-riscv64@npm:1.93.3" + conditions: os=android & cpu=riscv64 + languageName: node + linkType: hard + +"sass-embedded-android-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-x64@npm:1.93.3" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-darwin-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-darwin-arm64@npm:1.93.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-darwin-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-darwin-x64@npm:1.93.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-linux-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-arm64@npm:1.93.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-linux-arm@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-arm@npm:1.93.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-linux-musl-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-arm64@npm:1.93.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-arm@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-arm@npm:1.93.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-linux-musl-riscv64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-riscv64@npm:1.93.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-x64@npm:1.93.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-linux-riscv64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-riscv64@npm:1.93.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"sass-embedded-linux-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-x64@npm:1.93.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-unknown-all@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-unknown-all@npm:1.93.3" + dependencies: + sass: "npm:1.93.3" + conditions: (!os=android | !os=darwin | !os=linux | !os=win32) + languageName: node + linkType: hard + +"sass-embedded-win32-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-win32-arm64@npm:1.93.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-win32-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-win32-x64@npm:1.93.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded@npm:^1.93.3": + version: 1.93.3 + resolution: "sass-embedded@npm:1.93.3" + dependencies: + "@bufbuild/protobuf": "npm:^2.5.0" + buffer-builder: "npm:^0.2.0" + colorjs.io: "npm:^0.5.0" + immutable: "npm:^5.0.2" + rxjs: "npm:^7.4.0" + sass-embedded-all-unknown: "npm:1.93.3" + sass-embedded-android-arm: "npm:1.93.3" + sass-embedded-android-arm64: "npm:1.93.3" + sass-embedded-android-riscv64: "npm:1.93.3" + sass-embedded-android-x64: "npm:1.93.3" + sass-embedded-darwin-arm64: "npm:1.93.3" + sass-embedded-darwin-x64: "npm:1.93.3" + sass-embedded-linux-arm: "npm:1.93.3" + sass-embedded-linux-arm64: "npm:1.93.3" + sass-embedded-linux-musl-arm: "npm:1.93.3" + sass-embedded-linux-musl-arm64: "npm:1.93.3" + sass-embedded-linux-musl-riscv64: "npm:1.93.3" + sass-embedded-linux-musl-x64: "npm:1.93.3" + sass-embedded-linux-riscv64: "npm:1.93.3" + sass-embedded-linux-x64: "npm:1.93.3" + sass-embedded-unknown-all: "npm:1.93.3" + sass-embedded-win32-arm64: "npm:1.93.3" + sass-embedded-win32-x64: "npm:1.93.3" + supports-color: "npm:^8.1.1" + sync-child-process: "npm:^1.0.2" + varint: "npm:^6.0.0" + dependenciesMeta: + sass-embedded-all-unknown: + optional: true + sass-embedded-android-arm: + optional: true + sass-embedded-android-arm64: + optional: true + sass-embedded-android-riscv64: + optional: true + sass-embedded-android-x64: + optional: true + sass-embedded-darwin-arm64: + optional: true + sass-embedded-darwin-x64: + optional: true + sass-embedded-linux-arm: + optional: true + sass-embedded-linux-arm64: + optional: true + sass-embedded-linux-musl-arm: + optional: true + sass-embedded-linux-musl-arm64: + optional: true + sass-embedded-linux-musl-riscv64: + optional: true + sass-embedded-linux-musl-x64: + optional: true + sass-embedded-linux-riscv64: + optional: true + sass-embedded-linux-x64: + optional: true + sass-embedded-unknown-all: + optional: true + sass-embedded-win32-arm64: + optional: true + sass-embedded-win32-x64: + optional: true + bin: + sass: dist/bin/sass.js + checksum: 10/e2a1d6a31da76ce94df75f690a434ecd6467209eca6333951f1008a17d54693643e8a7cf2e82e0514f07f98b6de17dcf2b2fdadd5ad8abdac113c40c350fe154 + languageName: node + linkType: hard + "sass-loader@npm:^13.2.0": version: 13.3.3 resolution: "sass-loader@npm:13.3.3" @@ -17859,6 +19683,23 @@ __metadata: languageName: node linkType: hard +"sass@npm:1.93.3, sass@npm:^1.93.3": + version: 1.93.3 + resolution: "sass@npm:1.93.3" + dependencies: + "@parcel/watcher": "npm:^2.4.1" + chokidar: "npm:^4.0.0" + immutable: "npm:^5.0.2" + source-map-js: "npm:>=0.6.2 <2.0.0" + dependenciesMeta: + "@parcel/watcher": + optional: true + bin: + sass: sass.js + checksum: 10/41f23b10bb203ee46b82b880e566edc3264cd00b0424bb7293c6aedb66fd4d6b9b7a217e91f98fb4653eee6538150bbe1a663abde03ad69cd5172beebf108ae0 + languageName: node + linkType: hard + "saxes@npm:^6.0.0": version: 6.0.0 resolution: "saxes@npm:6.0.0" @@ -18413,7 +20254,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 @@ -19033,7 +20874,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0": +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -19111,6 +20952,22 @@ __metadata: languageName: node linkType: hard +"sync-child-process@npm:^1.0.2": + version: 1.0.2 + resolution: "sync-child-process@npm:1.0.2" + dependencies: + sync-message-port: "npm:^1.0.0" + checksum: 10/6fbdbb7b6f5730a1966d6a77cdbfe7f5cb8d1a582dab955c62c32b56dc6c432ccdbfc68027265486f8f4b1a998cc4d7ee21856e8125748bef70b8874aaedb21c + languageName: node + linkType: hard + +"sync-message-port@npm:^1.0.0": + version: 1.1.3 + resolution: "sync-message-port@npm:1.1.3" + checksum: 10/a84b681afd678f28af4498074c4bc5cd5c763395fbf169f1bc9777c2e01aa8d41a3046dcca43a41e81102a7fd697713dfc03e155d1c662fec88af9481b249b8a + languageName: node + linkType: hard + "synckit@npm:^0.9.0": version: 0.9.2 resolution: "synckit@npm:0.9.2" @@ -19158,6 +21015,13 @@ __metadata: languageName: node linkType: hard +"tabbable@npm:^6.0.0": + version: 6.3.0 + resolution: "tabbable@npm:6.3.0" + checksum: 10/3e54a0b770d26bc20c3de5837652be19f5efa8bfa869f580af24bcf60de934506e9401a577213186b5e86ebcf6b5290a5429d354cc3041471815f5095e44e51a + languageName: node + linkType: hard + "table@npm:^6.9.0": version: 6.9.0 resolution: "table@npm:6.9.0" @@ -19628,7 +21492,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -19805,6 +21669,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 10/37197358242eb9afe367502d4638ac8c5838b78792ab218eafe48287b0ed28aaca268ec0392cc5729f6c90266744de32c06ae938549aee041fc93b0f9672d6b2 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -20221,6 +22092,21 @@ __metadata: languageName: node linkType: hard +"use-callback-ref@npm:^1.3.3": + version: 1.3.3 + resolution: "use-callback-ref@npm:1.3.3" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/adf06a7b6a27d3651c325ac9b66d2b82ccacaed7450b85b211d123e91d9a23cb5a587fcc6db5b4fd07ac7233e5abf024d30cf02ddc2ec46bca712151c0836151 + languageName: node + linkType: hard + "use-isomorphic-layout-effect@npm:^1.1.2": version: 1.1.2 resolution: "use-isomorphic-layout-effect@npm:1.1.2" @@ -20233,6 +22119,22 @@ __metadata: languageName: node linkType: hard +"use-sidecar@npm:^1.1.3": + version: 1.1.3 + resolution: "use-sidecar@npm:1.1.3" + dependencies: + detect-node-es: "npm:^1.1.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/2fec05eb851cdfc4a4657b1dfb434e686f346c3265ffc9db8a974bb58f8128bd4a708a3cc00e8f51655fccf81822ed4419ebed42f41610589e3aab0cf2492edb + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.4.0": version: 1.4.0 resolution: "use-sync-external-store@npm:1.4.0" @@ -20348,6 +22250,13 @@ __metadata: languageName: node linkType: hard +"varint@npm:^6.0.0": + version: 6.0.0 + resolution: "varint@npm:6.0.0" + checksum: 10/7684113c9d497c01e40396e50169c502eb2176203219b96e1c5ac965a3e15b4892bd22b7e48d87148e10fffe638130516b6dbeedd0efde2b2d0395aa1772eea7 + languageName: node + linkType: hard + "vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -20439,6 +22348,13 @@ __metadata: languageName: node linkType: hard +"w3c-keyname@npm:^2.2.0": + version: 2.2.8 + resolution: "w3c-keyname@npm:2.2.8" + checksum: 10/95bafa4c04fa2f685a86ca1000069c1ec43ace1f8776c10f226a73296caeddd83f893db885c2c220ebeb6c52d424e3b54d7c0c1e963bbf204038ff1a944fbb07 + languageName: node + linkType: hard + "w3c-xmlserializer@npm:^4.0.0": version: 4.0.0 resolution: "w3c-xmlserializer@npm:4.0.0" From 65dd4179d4cc5e945db3d729732b13c7c68d6462 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:32:35 +0000 Subject: [PATCH 03/16] Ignore stylelint errors in vendor sheets --- frontends/.stylelintrc.yaml | 2 ++ .../src/app-pages/ArticlePage/NewArticlePage.tsx | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontends/.stylelintrc.yaml b/frontends/.stylelintrc.yaml index 947b90026b..de27ffb702 100644 --- a/frontends/.stylelintrc.yaml +++ b/frontends/.stylelintrc.yaml @@ -6,6 +6,8 @@ rules: - message: "Expected class selector to be kebab-case" ignoreFiles: - "**/*.vendor.css" + - "**/TiptapEditor/components/**/*.scss" + - "**/TiptapEditor/styles/**/*.scss" overrides: - files: - "**/*.scss" diff --git a/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx b/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx index 18b2685553..60be3d0a49 100644 --- a/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx +++ b/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx @@ -2,8 +2,9 @@ import React from "react" import { theme, styled, HEADER_HEIGHT } from "ol-components" - import { TiptapEditor } from "ol-components" +import { Permission } from "api/hooks/user" +import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute" const PageContainer = styled.div({ color: theme.custom.colors.darkGray2, @@ -23,11 +24,13 @@ const StyledTiptapEditor = styled(TiptapEditor)({ const NewArticlePage: React.FC = () => { return ( - - - - - + + + + + + + ) } From 8d1ae8c7f527d2bc4959f8ec5405a07509c6b244 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:33:22 +0000 Subject: [PATCH 04/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- frontends/main/next.config.js | 18 +++--- .../paragraph-node/paragraph-node.scss | 2 +- .../badge/badge-colors.scss | 28 ++++----- .../button/button-colors.scss | 60 +++++++++---------- .../TiptapEditor/styles/_variables.scss | 20 +++---- .../src/components/TiptapEditor/tsconfig.json | 1 - 6 files changed, 64 insertions(+), 65 deletions(-) diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js index 530b353028..4146ea3131 100644 --- a/frontends/main/next.config.js +++ b/frontends/main/next.config.js @@ -128,7 +128,7 @@ const nextConfig = { allowCollectingMemory: true, }) } - + // Custom resolver for TiptapEditor's @/ imports // Only applies special aliases when importing from within TiptapEditor directory // Tiptap's Simple Editor ejects files to the project, which we'd prefer not to edit @@ -138,26 +138,26 @@ const nextConfig = { __dirname, "../ol-components/src/components/TiptapEditor", ) - + config.resolve.plugins = config.resolve.plugins || [] config.resolve.plugins.push({ apply(resolver) { const target = resolver.ensureHook("resolve") - + resolver .getHook("described-resolve") .tapAsync("TiptapEditorAliasPlugin", (request, resolveContext, callback) => { const issuer = request.context?.issuer - + // Only apply custom aliases if the request is coming from TiptapEditor if (!issuer || !issuer.includes("TiptapEditor")) { return callback() } - + // Check if this is a @/components, @/lib, or @/hooks import const originalRequest = request.request let newRequestPath = null - + if (originalRequest?.startsWith("@/components/")) { const importPath = originalRequest.substring("@/components/".length) newRequestPath = path.join(tiptapEditorPath, "components", importPath) @@ -168,7 +168,7 @@ const nextConfig = { const importPath = originalRequest.substring("@/hooks/".length) newRequestPath = path.join(tiptapEditorPath, "hooks", importPath) } - + if (newRequestPath) { const newRequest = { ...request, @@ -182,12 +182,12 @@ const nextConfig = { callback, ) } - + callback() }) }, }) - + return config }, } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss index 7d4145a5ca..e5ff0b965d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/paragraph-node/paragraph-node.scss @@ -19,7 +19,7 @@ } } -/* Ensure each top-level node has relative positioning +/* Ensure each top-level node has relative positioning so absolutely positioned placeholders work correctly */ .tiptap.ProseMirror > * { position: relative; diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss index 8f8a988fbb..2044b94194 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge-colors.scss @@ -1,6 +1,6 @@ .tiptap-badge { - /************************************************** - Default + /************************************************** + Default **************************************************/ /* Light mode */ @@ -49,8 +49,8 @@ ); //more important badge } - /************************************************** - Ghost + /************************************************** + Ghost **************************************************/ &[data-style="ghost"] { @@ -101,8 +101,8 @@ } } - /************************************************** - Gray + /************************************************** + Gray **************************************************/ &[data-style="gray"] { @@ -153,8 +153,8 @@ } } - /************************************************** - Green + /************************************************** + Green **************************************************/ &[data-style="green"] { @@ -213,8 +213,8 @@ } } - /************************************************** - Yellow + /************************************************** + Yellow **************************************************/ &[data-style="yellow"] { @@ -273,8 +273,8 @@ } } - /************************************************** - Red + /************************************************** + Red **************************************************/ &[data-style="red"] { @@ -333,8 +333,8 @@ } } - /************************************************** - Brand + /************************************************** + Brand **************************************************/ &[data-style="brand"] { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss index fc0dd35e44..c9683e4252 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button-colors.scss @@ -1,6 +1,6 @@ .tiptap-button { - /************************************************** - Default button background color + /************************************************** + Default button background color **************************************************/ /* Light mode */ @@ -43,8 +43,8 @@ --tt-button-disabled-bg-color: var(--tt-gray-dark-a-50); } - /************************************************** - Default button text color + /************************************************** + Default button text color **************************************************/ /* Light mode */ @@ -65,8 +65,8 @@ --tt-button-disabled-text-color: var(--tt-gray-dark-a-300); } - /************************************************** - Default button icon color + /************************************************** + Default button icon color **************************************************/ /* Light mode */ @@ -87,8 +87,8 @@ --tt-button-disabled-icon-color: var(--tt-gray-dark-a-400); } - /************************************************** - Default button subicon color + /************************************************** + Default button subicon color **************************************************/ /* Light mode */ @@ -109,8 +109,8 @@ --tt-button-disabled-icon-sub-color: var(--tt-gray-dark-a-100); } - /************************************************** - Default button dropdown / arrows color + /************************************************** + Default button dropdown / arrows color **************************************************/ /* Light mode */ @@ -140,8 +140,8 @@ ---------------------------------------------------------------- */ &[data-style="ghost"] { - /************************************************** - Ghost button background color + /************************************************** + Ghost button background color **************************************************/ /* Light mode */ @@ -184,8 +184,8 @@ --tt-button-disabled-bg-color: var(--transparent); } - /************************************************** - Ghost button text color + /************************************************** + Ghost button text color **************************************************/ /* Light mode */ @@ -206,8 +206,8 @@ --tt-button-disabled-text-color: var(--tt-gray-dark-a-300); } - /************************************************** - Ghost button icon color + /************************************************** + Ghost button icon color **************************************************/ /* Light mode */ @@ -228,8 +228,8 @@ --tt-button-disabled-icon-color: var(--tt-gray-dark-a-400); } - /************************************************** - Ghost button subicon color + /************************************************** + Ghost button subicon color **************************************************/ /* Light mode */ @@ -250,8 +250,8 @@ --tt-button-disabled-icon-sub-color: var(--tt-gray-dark-a-100); } - /************************************************** - Ghost button dropdown / arrows color + /************************************************** + Ghost button dropdown / arrows color **************************************************/ /* Light mode */ @@ -286,8 +286,8 @@ ---------------------------------------------------------------- */ &[data-style="primary"] { - /************************************************** - Primary button background color + /************************************************** + Primary button background color **************************************************/ /* Light mode */ @@ -330,8 +330,8 @@ --tt-button-disabled-bg-color: var(--tt-gray-dark-a-100); } - /************************************************** - Primary button text color + /************************************************** + Primary button text color **************************************************/ /* Light mode */ @@ -352,8 +352,8 @@ --tt-button-disabled-text-color: var(--tt-gray-dark-a-300); } - /************************************************** - Primary button icon color + /************************************************** + Primary button icon color **************************************************/ /* Light mode */ @@ -374,8 +374,8 @@ --tt-button-disabled-icon-color: var(--tt-gray-dark-a-300); } - /************************************************** - Primary button subicon color + /************************************************** + Primary button subicon color **************************************************/ /* Light mode */ @@ -396,8 +396,8 @@ --tt-button-disabled-icon-sub-color: var(--tt-gray-dark-a-100); } - /************************************************** - Primary button dropdown / arrows color + /************************************************** + Primary button dropdown / arrows color **************************************************/ /* Light mode */ diff --git a/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss b/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss index 113a16b404..71aa3ece1e 100644 --- a/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss +++ b/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss @@ -1,5 +1,5 @@ :root { - /****************** + /****************** Basics ******************/ @@ -9,7 +9,7 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - /****************** + /****************** Colors variables ******************/ @@ -118,7 +118,7 @@ --black: rgba(14, 14, 17, 1); --transparent: rgba(255, 255, 255, 0); - /****************** + /****************** Shadow variables ******************/ @@ -129,8 +129,8 @@ 0px 6px 8px 0px rgba(17, 24, 39, 0.02), 0px 2px 3px 0px rgba(17, 24, 39, 0.02); - /************************************************** - Radius variables + /************************************************** + Radius variables **************************************************/ --tt-radius-xxs: 0.125rem; /* 2px */ @@ -140,8 +140,8 @@ --tt-radius-lg: 0.75rem; /* 12px */ --tt-radius-xl: 1rem; /* 16px */ - /************************************************** - Transition variables + /************************************************** + Transition variables **************************************************/ --tt-transition-duration-short: 0.1s; @@ -153,7 +153,7 @@ --tt-transition-easing-circ: cubic-bezier(0.79, 0.14, 0.15, 0.86); --tt-transition-easing-back: cubic-bezier(0.68, -0.55, 0.27, 1.55); - /****************** + /****************** Contrast variables ******************/ @@ -172,8 +172,8 @@ } :root { - /************************************************** - Global colors + /************************************************** + Global colors **************************************************/ /* Global colors - Light mode */ diff --git a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json index 3bcfb7636b..3ab18f3bdd 100644 --- a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json +++ b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json @@ -13,4 +13,3 @@ "./**/*.tsx" ] } - From ff020e4d6d8db6dff2ce453d8a26ef5c6e021968 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:37:18 +0000 Subject: [PATCH 05/16] Run fmt-fix --- frontends/main/next.config.js | 89 +++++++++++-------- .../image-upload-node-extension.ts | 2 +- .../image-upload-node/image-upload-node.tsx | 24 ++--- .../tiptap-ui-primitive/badge/badge.tsx | 4 +- .../tiptap-ui-primitive/button/button.tsx | 6 +- .../tiptap-ui-primitive/card/card.tsx | 10 +-- .../separator/separator.tsx | 2 +- .../tiptap-ui-primitive/toolbar/toolbar.tsx | 12 +-- .../tiptap-ui-primitive/tooltip/tooltip.tsx | 14 +-- .../blockquote-button/blockquote-button.tsx | 6 +- .../blockquote-button/use-blockquote.ts | 2 +- .../code-block-button/code-block-button.tsx | 6 +- .../code-block-button/use-code-block.ts | 2 +- .../color-highlight-button.tsx | 8 +- .../use-color-highlight.ts | 12 +-- .../color-highlight-popover.tsx | 4 +- .../heading-button/heading-button.tsx | 6 +- .../tiptap-ui/heading-button/use-heading.ts | 8 +- .../heading-dropdown-menu.tsx | 6 +- .../use-heading-dropdown-menu.ts | 4 +- .../image-upload-button.tsx | 6 +- .../image-upload-button/use-image-upload.ts | 2 +- .../tiptap-ui/link-popover/link-popover.tsx | 10 +-- .../link-popover/use-link-popover.ts | 4 +- .../tiptap-ui/list-button/list-button.tsx | 11 ++- .../tiptap-ui/list-button/use-list.ts | 2 +- .../list-dropdown-menu/list-dropdown-menu.tsx | 2 +- .../use-list-dropdown-menu.ts | 12 +-- .../tiptap-ui/mark-button/mark-button.tsx | 6 +- .../text-align-button/text-align-button.tsx | 6 +- .../text-align-button/use-text-align.ts | 11 +-- .../undo-redo-button/undo-redo-button.tsx | 6 +- .../undo-redo-button/use-undo-redo.ts | 4 +- .../TiptapEditor/hooks/use-composed-ref.ts | 4 +- .../TiptapEditor/hooks/use-element-rect.ts | 6 +- .../TiptapEditor/hooks/use-menu-navigation.ts | 4 +- .../TiptapEditor/hooks/use-scrolling.ts | 6 +- .../hooks/use-throttled-callback.ts | 4 +- .../TiptapEditor/hooks/use-tiptap-editor.ts | 2 +- .../TiptapEditor/hooks/use-unmount.ts | 2 +- .../TiptapEditor/lib/tiptap-utils.ts | 30 +++---- .../TiptapEditor/styles/_variables.scss | 9 +- .../src/components/TiptapEditor/tsconfig.json | 5 +- 43 files changed, 195 insertions(+), 186 deletions(-) diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js index 4146ea3131..a0898ded6d 100644 --- a/frontends/main/next.config.js +++ b/frontends/main/next.config.js @@ -146,45 +146,58 @@ const nextConfig = { resolver .getHook("described-resolve") - .tapAsync("TiptapEditorAliasPlugin", (request, resolveContext, callback) => { - const issuer = request.context?.issuer - - // Only apply custom aliases if the request is coming from TiptapEditor - if (!issuer || !issuer.includes("TiptapEditor")) { - return callback() - } - - // Check if this is a @/components, @/lib, or @/hooks import - const originalRequest = request.request - let newRequestPath = null - - if (originalRequest?.startsWith("@/components/")) { - const importPath = originalRequest.substring("@/components/".length) - newRequestPath = path.join(tiptapEditorPath, "components", importPath) - } else if (originalRequest?.startsWith("@/lib/")) { - const importPath = originalRequest.substring("@/lib/".length) - newRequestPath = path.join(tiptapEditorPath, "lib", importPath) - } else if (originalRequest?.startsWith("@/hooks/")) { - const importPath = originalRequest.substring("@/hooks/".length) - newRequestPath = path.join(tiptapEditorPath, "hooks", importPath) - } - - if (newRequestPath) { - const newRequest = { - ...request, - request: newRequestPath, + .tapAsync( + "TiptapEditorAliasPlugin", + (request, resolveContext, callback) => { + const issuer = request.context?.issuer + + // Only apply custom aliases if the request is coming from TiptapEditor + if (!issuer || !issuer.includes("TiptapEditor")) { + return callback() } - return resolver.doResolve( - target, - newRequest, - "aliased with TiptapEditor prefix", - resolveContext, - callback, - ) - } - - callback() - }) + + // Check if this is a @/components, @/lib, or @/hooks import + const originalRequest = request.request + let newRequestPath = null + + if (originalRequest?.startsWith("@/components/")) { + const importPath = originalRequest.substring( + "@/components/".length, + ) + newRequestPath = path.join( + tiptapEditorPath, + "components", + importPath, + ) + } else if (originalRequest?.startsWith("@/lib/")) { + const importPath = originalRequest.substring("@/lib/".length) + newRequestPath = path.join(tiptapEditorPath, "lib", importPath) + } else if (originalRequest?.startsWith("@/hooks/")) { + const importPath = originalRequest.substring("@/hooks/".length) + newRequestPath = path.join( + tiptapEditorPath, + "hooks", + importPath, + ) + } + + if (newRequestPath) { + const newRequest = { + ...request, + request: newRequestPath, + } + return resolver.doResolve( + target, + newRequest, + "aliased with TiptapEditor prefix", + resolveContext, + callback, + ) + } + + callback() + }, + ) }, }) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts index c282b1b35c..a870707431 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts @@ -6,7 +6,7 @@ import type { NodeType } from "@tiptap/pm/model" export type UploadFunction = ( file: File, onProgress?: (event: { progress: number }) => void, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, ) => Promise export interface ImageUploadNodeOptions { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx index 4b56f1cb60..81e570bcdb 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx @@ -63,7 +63,7 @@ export interface UploadOptions { upload: ( file: File, onProgress: (event: { progress: number }) => void, - signal: AbortSignal + signal: AbortSignal, ) => Promise /** * Callback triggered when a file is uploaded successfully @@ -88,7 +88,7 @@ function useFileUpload(options: UploadOptions) { const uploadFile = async (file: File): Promise => { if (file.size > options.maxSize) { const error = new Error( - `File size exceeds maximum allowed (${options.maxSize / 1024 / 1024}MB)` + `File size exceeds maximum allowed (${options.maxSize / 1024 / 1024}MB)`, ) options.onError?.(error) return null @@ -117,11 +117,11 @@ function useFileUpload(options: UploadOptions) { (event: { progress: number }) => { setFileItems((prev) => prev.map((item) => - item.id === fileId ? { ...item, progress: event.progress } : item - ) + item.id === fileId ? { ...item, progress: event.progress } : item, + ), ) }, - abortController.signal + abortController.signal, ) if (!url) throw new Error("Upload failed: No URL returned") @@ -131,8 +131,8 @@ function useFileUpload(options: UploadOptions) { prev.map((item) => item.id === fileId ? { ...item, status: "success", url, progress: 100 } - : item - ) + : item, + ), ) options.onSuccess?.(url) return url @@ -145,11 +145,11 @@ function useFileUpload(options: UploadOptions) { prev.map((item) => item.id === fileId ? { ...item, status: "error", progress: 0 } - : item - ) + : item, + ), ) options.onError?.( - error instanceof Error ? error : new Error("Upload failed") + error instanceof Error ? error : new Error("Upload failed"), ) } return null @@ -165,8 +165,8 @@ function useFileUpload(options: UploadOptions) { if (options.limit && files.length > options.limit) { options.onError?.( new Error( - `Maximum ${options.limit} file${options.limit === 1 ? "" : "s"} allowed` - ) + `Maximum ${options.limit} file${options.limit === 1 ? "" : "s"} allowed`, + ), ) return [] } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx index 56ecb5948f..b41415eb51 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx @@ -21,7 +21,7 @@ export const Badge = forwardRef( children, ...props }, - ref + ref, ) => { return (
( {children}
) - } + }, ) Badge.displayName = "Badge" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx index 9dc09a7b76..953bdd88a8 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx @@ -50,11 +50,11 @@ export const Button = forwardRef( "aria-label": ariaLabel, ...props }, - ref + ref, ) => { const shortcuts = useMemo( () => parseShortcutKeys({ shortcutKeys }), - [shortcutKeys] + [shortcutKeys], ) if (!tooltip || !showTooltip) { @@ -86,7 +86,7 @@ export const Button = forwardRef( ) - } + }, ) Button.displayName = "Button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx index 1cd8d32441..8a977f3dad 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx @@ -7,7 +7,7 @@ import "@/components/tiptap-ui-primitive/card/card.scss" const Card = forwardRef>( ({ className, ...props }, ref) => { return
- } + }, ) Card.displayName = "Card" @@ -20,7 +20,7 @@ const CardHeader = forwardRef>( {...props} /> ) - } + }, ) CardHeader.displayName = "CardHeader" @@ -29,7 +29,7 @@ const CardBody = forwardRef>( return (
) - } + }, ) CardBody.displayName = "CardBody" @@ -59,7 +59,7 @@ const CardGroupLabel = forwardRef>( {...props} /> ) - } + }, ) CardGroupLabel.displayName = "CardGroupLabel" @@ -72,7 +72,7 @@ const CardFooter = forwardRef>( {...props} /> ) - } + }, ) CardFooter.displayName = "CardFooter" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx index e2ce9cd242..d581b202fb 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx @@ -25,7 +25,7 @@ export const Separator = forwardRef( ref={ref} /> ) - } + }, ) Separator.displayName = "Separator" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx index b6de1f46f6..c7d80e33a4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx @@ -12,7 +12,7 @@ interface ToolbarProps extends BaseProps { } const useToolbarNavigation = ( - toolbarRef: React.RefObject + toolbarRef: React.RefObject, ) => { const [items, setItems] = useState([]) @@ -20,8 +20,8 @@ const useToolbarNavigation = ( if (!toolbarRef.current) return [] return Array.from( toolbarRef.current.querySelectorAll( - 'button:not([disabled]), [role="button"]:not([disabled]), [tabindex="0"]:not([disabled])' - ) + 'button:not([disabled]), [role="button"]:not([disabled]), [tabindex="0"]:not([disabled])', + ), ) }, [toolbarRef]) @@ -95,7 +95,7 @@ export const Toolbar = forwardRef( {children}
) - } + }, ) Toolbar.displayName = "Toolbar" @@ -109,13 +109,13 @@ export const ToolbarGroup = forwardRef( > {children}
- ) + ), ) ToolbarGroup.displayName = "ToolbarGroup" export const ToolbarSeparator = forwardRef( ({ ...props }, ref) => ( - ) + ), ) ToolbarSeparator.displayName = "ToolbarSeparator" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx index 59ecc44a80..a3a92a5834 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx @@ -59,10 +59,10 @@ interface TooltipContextValue extends UseFloatingReturn { open: boolean setOpen: (open: boolean) => void getReferenceProps: ( - userProps?: React.HTMLProps + userProps?: React.HTMLProps, ) => Record getFloatingProps: ( - userProps?: React.HTMLProps + userProps?: React.HTMLProps, ) => Record } @@ -121,7 +121,7 @@ function useTooltip({ ...interactions, ...data, }), - [open, setOpen, interactions, data] + [open, setOpen, interactions, data], ) } @@ -184,7 +184,7 @@ export const TooltipTrigger = forwardRef( ...props, ...(typeof children.props === "object" ? children.props : {}), ...dataAttributes, - }) + }), ) } @@ -197,13 +197,13 @@ export const TooltipTrigger = forwardRef( {children} ) - } + }, ) export const TooltipContent = forwardRef( function TooltipContent( { style, children, portal = true, portalProps = {}, ...props }, - propRef + propRef, ) { const context = useTooltipContext() const ref = useMergeRefs([context.refs.setFloating, propRef]) @@ -229,7 +229,7 @@ export const TooltipContent = forwardRef( } return content - } + }, ) Tooltip.displayName = "Tooltip" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx index 3052381055..e56d5ef5a0 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx @@ -60,7 +60,7 @@ export const BlockquoteButton = forwardRef< children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -83,7 +83,7 @@ export const BlockquoteButton = forwardRef< if (event.defaultPrevented) return handleToggle() }, - [handleToggle, onClick] + [handleToggle, onClick], ) if (!isVisible) { @@ -117,7 +117,7 @@ export const BlockquoteButton = forwardRef< )} ) - } + }, ) BlockquoteButton.displayName = "BlockquoteButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts index 79d7a9294c..c8887a6e4c 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts @@ -45,7 +45,7 @@ export interface UseBlockquoteConfig { */ export function canToggleBlockquote( editor: Editor | null, - turnInto: boolean = true + turnInto: boolean = true, ): boolean { if (!editor || !editor.isEditable) return false if ( diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx index 3e4cea9684..ddb1040dd3 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx @@ -60,7 +60,7 @@ export const CodeBlockButton = forwardRef< children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -83,7 +83,7 @@ export const CodeBlockButton = forwardRef< if (event.defaultPrevented) return handleToggle() }, - [handleToggle, onClick] + [handleToggle, onClick], ) if (!isVisible) { @@ -117,7 +117,7 @@ export const CodeBlockButton = forwardRef< )} ) - } + }, ) CodeBlockButton.displayName = "CodeBlockButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts index 0475eb72bb..fd7877b320 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts @@ -45,7 +45,7 @@ export interface UseCodeBlockConfig { */ export function canToggle( editor: Editor | null, - turnInto: boolean = true + turnInto: boolean = true, ): boolean { if (!editor || !editor.isEditable) return false if ( diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx index 8fe957cbc2..26b5a488ad 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx @@ -89,7 +89,7 @@ export const ColorHighlightButton = forwardRef< style, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -114,7 +114,7 @@ export const ColorHighlightButton = forwardRef< if (event.defaultPrevented) return handleColorHighlight() }, - [handleColorHighlight, onClick] + [handleColorHighlight, onClick], ) const buttonStyle = useMemo( @@ -123,7 +123,7 @@ export const ColorHighlightButton = forwardRef< ...style, "--highlight-color": highlightColor, }) as React.CSSProperties, - [highlightColor, style] + [highlightColor, style], ) if (!isVisible) { @@ -163,7 +163,7 @@ export const ColorHighlightButton = forwardRef< )} ) - } + }, ) ColorHighlightButton.displayName = "ColorHighlightButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts index b71215e0b8..7cb7350d40 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts @@ -119,7 +119,7 @@ export interface UseColorHighlightConfig { export function pickHighlightColorsByValue(values: string[]) { const colorMap = new Map( - HIGHLIGHT_COLORS.map((color) => [color.value, color]) + HIGHLIGHT_COLORS.map((color) => [color.value, color]), ) return values .map((value) => colorMap.get(value)) @@ -131,7 +131,7 @@ export function pickHighlightColorsByValue(values: string[]) { */ export function canColorHighlight( editor: Editor | null, - mode: HighlightMode = "mark" + mode: HighlightMode = "mark", ): boolean { if (!editor || !editor.isEditable) return false @@ -160,7 +160,7 @@ export function canColorHighlight( export function isColorHighlightActive( editor: Editor | null, highlightColor?: string, - mode: HighlightMode = "mark" + mode: HighlightMode = "mark", ): boolean { if (!editor || !editor.isEditable) return false @@ -194,7 +194,7 @@ export function isColorHighlightActive( */ export function removeHighlight( editor: Editor | null, - mode: HighlightMode = "mark" + mode: HighlightMode = "mark", ): boolean { if (!editor || !editor.isEditable) return false if (!canColorHighlight(editor, mode)) return false @@ -272,7 +272,7 @@ export function useColorHighlight(config: UseColorHighlightConfig) { const highlightMarkType = editor.schema.marks.highlight if (highlightMarkType) { editor.view.dispatch( - editor.state.tr.removeStoredMark(highlightMarkType) + editor.state.tr.removeStoredMark(highlightMarkType), ) } } @@ -322,7 +322,7 @@ export function useColorHighlight(config: UseColorHighlightConfig) { enabled: isVisible && canColorHighlightState, enableOnContentEditable: !isMobile, enableOnFormTags: true, - } + }, ) return { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx index 3ede63f606..43aff245d3 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx @@ -99,7 +99,7 @@ export function ColorHighlightPopoverContent({ const menuItems = useMemo( () => [...colors, { label: "Remove highlight", value: "none" }], - [colors] + [colors], ) const { selectedIndex } = useMenuNavigation({ @@ -109,7 +109,7 @@ export function ColorHighlightPopoverContent({ onSelect: (item) => { if (!containerRef.current) return false const highlightedElement = containerRef.current.querySelector( - '[data-highlighted="true"]' + '[data-highlighted="true"]', ) as HTMLElement if (highlightedElement) highlightedElement.click() if (item.value === "none") handleRemoveHighlight() diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx index 4d32634d69..39cf36ef8c 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx @@ -61,7 +61,7 @@ export const HeadingButton = forwardRef( children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -85,7 +85,7 @@ export const HeadingButton = forwardRef( if (event.defaultPrevented) return handleToggle() }, - [handleToggle, onClick] + [handleToggle, onClick], ) if (!isVisible) { @@ -119,7 +119,7 @@ export const HeadingButton = forwardRef( )} ) - } + }, ) HeadingButton.displayName = "HeadingButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts index 0157f988e7..d308c772b2 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts @@ -73,7 +73,7 @@ export const HEADING_SHORTCUT_KEYS: Record = { export function canToggle( editor: Editor | null, level?: Level, - turnInto: boolean = true + turnInto: boolean = true, ): boolean { if (!editor || !editor.isEditable) return false if ( @@ -114,7 +114,7 @@ export function canToggle( */ export function isHeadingActive( editor: Editor | null, - level?: Level | Level[] + level?: Level | Level[], ): boolean { if (!editor || !editor.isEditable) return false @@ -132,7 +132,7 @@ export function isHeadingActive( */ export function toggleHeading( editor: Editor | null, - level: Level | Level[] + level: Level | Level[], ): boolean { if (!editor || !editor.isEditable) return false @@ -184,7 +184,7 @@ export function toggleHeading( } const isActive = levels.some((l) => - editor.isActive("heading", { level: l }) + editor.isActive("heading", { level: l }), ) const toggle = isActive diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx index 762bd55d02..71930b54d7 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx @@ -54,7 +54,7 @@ export const HeadingDropdownMenu = forwardRef< onOpenChange, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const [isOpen, setIsOpen] = useState(false) @@ -70,7 +70,7 @@ export const HeadingDropdownMenu = forwardRef< setIsOpen(open) onOpenChange?.(open) }, - [canToggle, editor, onOpenChange] + [canToggle, editor, onOpenChange], ) if (!isVisible) { @@ -119,7 +119,7 @@ export const HeadingDropdownMenu = forwardRef< ) - } + }, ) HeadingDropdownMenu.displayName = "HeadingDropdownMenu" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts index b25ce63785..c8539db6ef 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts @@ -43,7 +43,7 @@ export interface UseHeadingDropdownMenuConfig { */ export function getActiveHeadingLevel( editor: Editor | null, - levels: Level[] = [1, 2, 3, 4, 5, 6] + levels: Level[] = [1, 2, 3, 4, 5, 6], ): Level | undefined { if (!editor || !editor.isEditable) return undefined return levels.find((level) => isHeadingActive(editor, level)) @@ -107,7 +107,7 @@ export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) { const handleSelectionUpdate = () => { setIsVisible( - shouldShowButton({ editor, hideWhenUnavailable, level: levels }) + shouldShowButton({ editor, hideWhenUnavailable, level: levels }), ) } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx index 6ec8dd75e3..9d3f10020f 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx @@ -68,7 +68,7 @@ export const ImageUploadButton = forwardRef< children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -91,7 +91,7 @@ export const ImageUploadButton = forwardRef< if (event.defaultPrevented) return handleImage() }, - [handleImage, onClick] + [handleImage, onClick], ) if (!isVisible) { @@ -125,7 +125,7 @@ export const ImageUploadButton = forwardRef< )} ) - } + }, ) ImageUploadButton.displayName = "ImageUploadButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts index 7d91811ff3..13b445c256 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts @@ -177,7 +177,7 @@ export function useImageUpload(config?: UseImageUploadConfig) { enabled: isVisible && canInsert, enableOnContentEditable: !isMobile, enableOnFormTags: true, - } + }, ) return { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx index 4f6151db66..827660d84b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx @@ -94,7 +94,7 @@ export const LinkButton = forwardRef( {children || } ) - } + }, ) LinkButton.displayName = "LinkButton" @@ -216,7 +216,7 @@ export const LinkPopover = forwardRef( children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const [isOpen, setIsOpen] = useState(false) @@ -243,7 +243,7 @@ export const LinkPopover = forwardRef( setIsOpen(nextIsOpen) onOpenChange?.(nextIsOpen) }, - [onOpenChange] + [onOpenChange], ) const handleSetLink = useCallback(() => { @@ -257,7 +257,7 @@ export const LinkPopover = forwardRef( if (event.defaultPrevented) return setIsOpen(!isOpen) }, - [onClick, isOpen] + [onClick, isOpen], ) useEffect(() => { @@ -299,7 +299,7 @@ export const LinkPopover = forwardRef( ) - } + }, ) LinkPopover.displayName = "LinkPopover" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts index ac0b779e06..2b4e9fce95 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts @@ -163,7 +163,7 @@ export function useLinkHandler(props: LinkHandlerProps) { window.open(safeUrl, target, features) } }, - [url] + [url], ) return { @@ -197,7 +197,7 @@ export function useLinkState(props: { shouldShowLinkButton({ editor, hideWhenUnavailable, - }) + }), ) } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx index bbe0f6a436..80c1278f1b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx @@ -12,7 +12,10 @@ import { Button } from "@/components/tiptap-ui-primitive/button" import { Badge } from "@/components/tiptap-ui-primitive/badge" // --- Tiptap UI --- -import type { ListType, UseListConfig } from "@/components/tiptap-ui/list-button" +import type { + ListType, + UseListConfig, +} from "@/components/tiptap-ui/list-button" import { LIST_SHORTCUT_KEYS, useList } from "@/components/tiptap-ui/list-button" export interface ListButtonProps @@ -57,7 +60,7 @@ export const ListButton = forwardRef( children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -81,7 +84,7 @@ export const ListButton = forwardRef( if (event.defaultPrevented) return handleToggle() }, - [handleToggle, onClick] + [handleToggle, onClick], ) if (!isVisible) { @@ -115,7 +118,7 @@ export const ListButton = forwardRef( )} ) - } + }, ) ListButton.displayName = "ListButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts index 55bbbff6bc..2049e97091 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts @@ -70,7 +70,7 @@ export const LIST_SHORTCUT_KEYS: Record = { export function canToggleList( editor: Editor | null, type: ListType, - turnInto: boolean = true + turnInto: boolean = true, ): boolean { if (!editor || !editor.isEditable) return false if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx index 9aa0669762..3a98b14fb1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx @@ -71,7 +71,7 @@ export function ListDropdownMenu({ setIsOpen(open) onOpenChange?.(open) }, - [onOpenChange] + [onOpenChange], ) if (!isVisible || !editor || !editor.isEditable) { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts index ed7b5c1bff..d713b7f125 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts @@ -68,7 +68,7 @@ export const listOptions: ListOption[] = [ export function canToggleAnyList( editor: Editor | null, - listTypes: ListType[] + listTypes: ListType[], ): boolean { if (!editor || !editor.isEditable) return false return listTypes.some((type) => canToggleList(editor, type)) @@ -76,17 +76,17 @@ export function canToggleAnyList( export function isAnyListActive( editor: Editor | null, - listTypes: ListType[] + listTypes: ListType[], ): boolean { if (!editor || !editor.isEditable) return false return listTypes.some((type) => isListActive(editor, type)) } export function getFilteredListOptions( - availableTypes: ListType[] + availableTypes: ListType[], ): typeof listOptions { return listOptions.filter( - (option) => !option.type || availableTypes.includes(option.type) + (option) => !option.type || availableTypes.includes(option.type), ) } @@ -115,7 +115,7 @@ export function shouldShowListDropdown(params: { */ export function getActiveListType( editor: Editor | null, - availableTypes: ListType[] + availableTypes: ListType[], ): ListType | undefined { if (!editor || !editor.isEditable) return undefined return availableTypes.find((type) => isListActive(editor, type)) @@ -190,7 +190,7 @@ export function useListDropdownMenu(config?: UseListDropdownMenuConfig) { hideWhenUnavailable, listInSchema, canToggleAny, - }) + }), ) } diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx index 338bae71c0..0effb69a48 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx @@ -59,7 +59,7 @@ export const MarkButton = forwardRef( children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -83,7 +83,7 @@ export const MarkButton = forwardRef( if (event.defaultPrevented) return handleMark() }, - [handleMark, onClick] + [handleMark, onClick], ) if (!isVisible) { @@ -117,7 +117,7 @@ export const MarkButton = forwardRef( )} ) - } + }, ) MarkButton.displayName = "MarkButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx index b62598831b..a5484155b4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx @@ -76,7 +76,7 @@ export const TextAlignButton = forwardRef< children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { @@ -100,7 +100,7 @@ export const TextAlignButton = forwardRef< if (event.defaultPrevented) return handleTextAlign() }, - [handleTextAlign, onClick] + [handleTextAlign, onClick], ) if (!isVisible) { @@ -139,7 +139,7 @@ export const TextAlignButton = forwardRef< )} ) - } + }, ) TextAlignButton.displayName = "TextAlignButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts index 0db3a10a32..94032f5e4d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts @@ -6,10 +6,7 @@ import { type Editor } from "@tiptap/react" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- -import { - isExtensionAvailable, - isNodeTypeSelected, -} from "@/lib/tiptap-utils" +import { isExtensionAvailable, isNodeTypeSelected } from "@/lib/tiptap-utils" // --- Icons --- import { AlignCenterIcon } from "@/components/tiptap-icons/align-center-icon" @@ -68,7 +65,7 @@ export const textAlignLabels: Record = { */ export function canSetTextAlign( editor: Editor | null, - align: TextAlign + align: TextAlign, ): boolean { if (!editor || !editor.isEditable) return false if ( @@ -81,7 +78,7 @@ export function canSetTextAlign( } export function hasSetTextAlign( - commands: ChainedCommands + commands: ChainedCommands, ): commands is ChainedCommands & { setTextAlign: (align: TextAlign) => ChainedCommands } { @@ -93,7 +90,7 @@ export function hasSetTextAlign( */ export function isTextAlignActive( editor: Editor | null, - align: TextAlign + align: TextAlign, ): boolean { if (!editor || !editor.isEditable) return false return editor.isActive({ textAlign: align }) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx index d990b037ef..cde6d1174a 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx @@ -68,7 +68,7 @@ export const UndoRedoButton = forwardRef< children, ...buttonProps }, - ref + ref, ) => { const { editor } = useTiptapEditor(providedEditor) const { isVisible, handleAction, label, canExecute, Icon, shortcutKeys } = @@ -85,7 +85,7 @@ export const UndoRedoButton = forwardRef< if (event.defaultPrevented) return handleAction() }, - [handleAction, onClick] + [handleAction, onClick], ) if (!isVisible) { @@ -120,7 +120,7 @@ export const UndoRedoButton = forwardRef< )} ) - } + }, ) UndoRedoButton.displayName = "UndoRedoButton" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts index ea4d8455a2..94e73e37a4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts @@ -56,7 +56,7 @@ export const historyIcons = { */ export function canExecuteUndoRedoAction( editor: Editor | null, - action: UndoRedoAction + action: UndoRedoAction, ): boolean { if (!editor || !editor.isEditable) return false if (isNodeTypeSelected(editor, ["image"])) return false @@ -69,7 +69,7 @@ export function canExecuteUndoRedoAction( */ export function executeUndoRedoAction( editor: Editor | null, - action: UndoRedoAction + action: UndoRedoAction, ): boolean { if (!editor || !editor.isEditable) return false if (!canExecuteUndoRedoAction(editor, action)) return false diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts index 30745b8f98..7b6d8aa0dc 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-composed-ref.ts @@ -20,7 +20,7 @@ const updateRef = (ref: NonNullable>, value: T | null) => { export const useComposedRef = ( libRef: React.RefObject, - userRef: UserRef + userRef: UserRef, ) => { const prevUserRef = useRef>(null) @@ -40,7 +40,7 @@ export const useComposedRef = ( updateRef(userRef, instance) } }, - [libRef, userRef] + [libRef, userRef], ) } diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts index 55ba9bd1c9..8922436fbe 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts @@ -100,7 +100,7 @@ export function useElementRect({ }, throttleMs, [enabled, getTargetElement], - { leading: true, trailing: true } + { leading: true, trailing: true }, ) useEffect(() => { @@ -147,7 +147,7 @@ export function useElementRect({ * Convenience hook for tracking document.body rect */ export function useBodyRect( - options: Omit = {} + options: Omit = {}, ): RectState { return useElementRect({ ...options, @@ -160,7 +160,7 @@ export function useBodyRect( */ export function useRefRect( ref: React.RefObject, - options: Omit = {} + options: Omit = {}, ): RectState { return useElementRect({ ...options, element: ref }) } diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts index 58dfd4b3f6..9080453885 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-menu-navigation.ts @@ -60,7 +60,7 @@ export function useMenuNavigation({ autoSelectFirstItem = true, }: MenuNavigationOptions) { const [selectedIndex, setSelectedIndex] = useState( - autoSelectFirstItem ? 0 : -1 + autoSelectFirstItem ? 0 : -1, ) useEffect(() => { @@ -165,7 +165,7 @@ export function useMenuNavigation({ targetElement?.removeEventListener( "keydown", handleKeyboardNavigation, - true + true, ) } } diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts index 8a5fb35db1..0c4bcfcefe 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-scrolling.ts @@ -11,7 +11,7 @@ interface UseScrollingOptions { export function useScrolling( target?: ScrollTarget, - options: UseScrollingOptions = {} + options: UseScrollingOptions = {}, ): boolean { const { debounce = 150, fallbackToDocument = true } = options const [isScrolling, setIsScrolling] = useState(false) @@ -34,13 +34,13 @@ export function useScrolling( const on = ( el: EventTargetWithScroll, event: string, - handler: EventListener + handler: EventListener, ) => el.addEventListener(event, handler, true) const off = ( el: EventTargetWithScroll, event: string, - handler: EventListener + handler: EventListener, ) => el.removeEventListener(event, handler) let timeout: ReturnType diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts index 54894cf2e5..18f4b11c7d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts @@ -26,7 +26,7 @@ export function useThrottledCallback any>( fn: T, wait = 250, dependencies: React.DependencyList = [], - options: ThrottleSettings = defaultOptions + options: ThrottleSettings = defaultOptions, ): { (this: ThisParameterType, ...args: Parameters): ReturnType cancel: () => void @@ -35,7 +35,7 @@ export function useThrottledCallback any>( const handler = useMemo( () => throttle(fn, wait, options), // eslint-disable-next-line react-hooks/exhaustive-deps - dependencies + dependencies, ) useUnmount(() => { diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts index 44044ccb2e..056159dada 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-tiptap-editor.ts @@ -21,7 +21,7 @@ export function useTiptapEditor(providedEditor?: Editor | null): { const { editor: coreEditor } = useCurrentEditor() const mainEditor = useMemo( () => providedEditor || coreEditor, - [providedEditor, coreEditor] + [providedEditor, coreEditor], ) const editorState = useEditorState({ diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts index 91a22e7132..bd229255e9 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-unmount.ts @@ -14,7 +14,7 @@ export const useUnmount = (callback: (...args: Array) => any) => { () => () => { ref.current() }, - [] + [], ) } diff --git a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts index e57c245c8e..846c4f45c5 100644 --- a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts +++ b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts @@ -65,7 +65,7 @@ export function isMac(): boolean { export const formatShortcutKey = ( key: string, isMac: boolean, - capitalize: boolean = true + capitalize: boolean = true, ) => { if (isMac) { const lowerKey = key.toLowerCase() @@ -105,7 +105,7 @@ export const parseShortcutKeys = (props: { */ export const isMarkInSchema = ( markName: string, - editor: Editor | null + editor: Editor | null, ): boolean => { if (!editor?.schema) return false return editor.schema.spec.marks.get(markName) !== undefined @@ -119,7 +119,7 @@ export const isMarkInSchema = ( */ export const isNodeInSchema = ( nodeName: string, - editor: Editor | null + editor: Editor | null, ): boolean => { if (!editor?.schema) return false return editor.schema.spec.nodes.get(nodeName) !== undefined @@ -174,7 +174,7 @@ export function isValidPosition(pos: number | null | undefined): pos is number { */ export function isExtensionAvailable( editor: Editor | null, - extensionNames: string | string[] + extensionNames: string | string[], ): boolean { if (!editor) return false @@ -183,12 +183,12 @@ export function isExtensionAvailable( : [extensionNames] const found = names.some((name) => - editor.extensionManager.extensions.some((ext) => ext.name === name) + editor.extensionManager.extensions.some((ext) => ext.name === name), ) if (!found) { console.warn( - `None of the extensions [${names.join(", ")}] were found in the editor schema. Ensure they are included in the editor configuration.` + `None of the extensions [${names.join(", ")}] were found in the editor schema. Ensure they are included in the editor configuration.`, ) } @@ -282,7 +282,7 @@ export function findNodePosition(props: { export function isNodeTypeSelected( editor: Editor | null, nodeTypeNames: string[] = [], - checkAncestorNodes: boolean = false + checkAncestorNodes: boolean = false, ): boolean { if (!editor || !editor.state.selection) return false @@ -318,7 +318,7 @@ export function isNodeTypeSelected( */ export function selectionWithinConvertibleTypes( editor: Editor, - types: string[] = [] + types: string[] = [], ): boolean { if (!editor || types.length === 0) return false @@ -356,7 +356,7 @@ export function selectionWithinConvertibleTypes( export const handleImageUpload = async ( file: File, onProgress?: (event: { progress: number }) => void, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, ): Promise => { // Validate file if (!file) { @@ -365,7 +365,7 @@ export const handleImageUpload = async ( if (file.size > MAX_FILE_SIZE) { throw new Error( - `File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)` + `File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)`, ) } @@ -407,7 +407,7 @@ const ATTR_WHITESPACE = export function isAllowedUri( uri: string | undefined, - protocols?: ProtocolConfig + protocols?: ProtocolConfig, ) { const allowedProtocols: string[] = [ "http", @@ -439,8 +439,8 @@ export function isAllowedUri( new RegExp( // eslint-disable-next-line no-useless-escape `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))`, - "i" - ) + "i", + ), ) ) } @@ -448,7 +448,7 @@ export function isAllowedUri( export function sanitizeUrl( inputUrl: string, baseUrl: string, - protocols?: ProtocolConfig + protocols?: ProtocolConfig, ): string { try { const url = new URL(inputUrl, baseUrl) @@ -476,7 +476,7 @@ export function updateNodesAttr
( tr: Transaction, targets: readonly NodeWithPos[], attrName: A, - next: V | ((prev: V | undefined) => V | undefined) + next: V | ((prev: V | undefined) => V | undefined), ): boolean { if (!targets.length) return false diff --git a/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss b/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss index 71aa3ece1e..aaf40caa36 100644 --- a/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss +++ b/frontends/ol-components/src/components/TiptapEditor/styles/_variables.scss @@ -123,8 +123,7 @@ ******************/ /* Shadows Light */ - --tt-shadow-elevated-md: - 0px 16px 48px 0px rgba(17, 24, 39, 0.04), + --tt-shadow-elevated-md: 0px 16px 48px 0px rgba(17, 24, 39, 0.04), 0px 12px 24px 0px rgba(17, 24, 39, 0.04), 0px 6px 8px 0px rgba(17, 24, 39, 0.02), 0px 2px 3px 0px rgba(17, 24, 39, 0.02); @@ -200,9 +199,9 @@ --tt-card-bg-color: var(--tt-gray-dark-50); --tt-card-border-color: var(--tt-gray-dark-a-50); - --tt-shadow-elevated-md: - 0px 16px 48px 0px rgba(0, 0, 0, 0.5), 0px 12px 24px 0px rgba(0, 0, 0, 0.24), - 0px 6px 8px 0px rgba(0, 0, 0, 0.22), 0px 2px 3px 0px rgba(0, 0, 0, 0.12); + --tt-shadow-elevated-md: 0px 16px 48px 0px rgba(0, 0, 0, 0.5), + 0px 12px 24px 0px rgba(0, 0, 0, 0.24), 0px 6px 8px 0px rgba(0, 0, 0, 0.22), + 0px 2px 3px 0px rgba(0, 0, 0, 0.12); } /* Text colors */ diff --git a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json index 3ab18f3bdd..dd2c8c9eb1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json +++ b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json @@ -8,8 +8,5 @@ "@/hooks/*": ["./hooks/*"] } }, - "include": [ - "./**/*.ts", - "./**/*.tsx" - ] + "include": ["./**/*.ts", "./**/*.tsx"] } From a28ac140e6e75d7b9cd7d4ec0f26aa6557e782d1 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:50:18 +0000 Subject: [PATCH 06/16] Eslint ignore vendor code --- frontends/.eslintrc.js | 4 ++++ frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontends/.eslintrc.js b/frontends/.eslintrc.js index 074b4b193e..30fb736252 100644 --- a/frontends/.eslintrc.js +++ b/frontends/.eslintrc.js @@ -14,6 +14,10 @@ module.exports = { "mit-learn", "github-pages", "storybook-static", + "**/TiptapEditor/components/**/*.tsx", + "**/TiptapEditor/components/**/*.ts", + "**/TiptapEditor/hooks/**/*.ts", + "**/TiptapEditor/lib/**/*.ts", ], settings: { "import/resolver": { diff --git a/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx b/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx index 60be3d0a49..6832e53b57 100644 --- a/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx +++ b/frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx @@ -1,8 +1,7 @@ "use client" import React from "react" -import { theme, styled, HEADER_HEIGHT } from "ol-components" -import { TiptapEditor } from "ol-components" +import { TiptapEditor, theme, styled, HEADER_HEIGHT } from "ol-components" import { Permission } from "api/hooks/user" import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute" From 1a2c714a5fbc991a2e1a9f7a5eca76c6d10649d9 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:32:34 +0000 Subject: [PATCH 07/16] Revert main package.json --- frontends/main/package.json | 27 ++-------- yarn.lock | 102 +++++++++--------------------------- 2 files changed, 27 insertions(+), 102 deletions(-) diff --git a/frontends/main/package.json b/frontends/main/package.json index 7021ed7412..cfc7859b40 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -13,40 +13,22 @@ "@ebay/nice-modal-react": "^1.2.13", "@emotion/cache": "^11.13.1", "@emotion/styled": "^11.11.0", - "@floating-ui/react": "^0.27.16", "@mitodl/course-search-utils": "^3.5.0", - "@mitodl/mitxonline-api-axios": "^2025.10.21", + "@mitodl/mitxonline-api-axios": "^2025.11.5", "@mitodl/smoot-design": "^6.17.1", "@next/bundle-analyzer": "^14.2.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-popover": "^1.1.15", "@react-pdf/renderer": "^4.3.0", - "@remixicon/react": "^4.7.0", + "@remixicon/react": "^4.2.0", "@sentry/nextjs": "^10.0.0", "@tanstack/react-query": "^5.66", - "@tiptap/extension-document": "^3.10.2", - "@tiptap/extension-highlight": "^3.10.1", - "@tiptap/extension-horizontal-rule": "^3.10.1", - "@tiptap/extension-image": "^3.10.1", - "@tiptap/extension-list": "^3.10.1", - "@tiptap/extension-subscript": "^3.10.1", - "@tiptap/extension-superscript": "^3.10.1", - "@tiptap/extension-text-align": "^3.10.1", - "@tiptap/extension-typography": "^3.10.1", - "@tiptap/extensions": "^3.10.1", - "@tiptap/markdown": "^3.10.1", - "@tiptap/pm": "^3.10.1", - "@tiptap/react": "^3.10.1", - "@tiptap/starter-kit": "^3.10.1", - "@tiptap/suggestion": "^3.10.2", "api": "workspace:*", + "async_hooks": "^1.0.0", "classnames": "^2.5.1", "formik": "^2.4.6", "iso-639-1": "^3.1.4", "isomorphic-dompurify": "^2.27.0", "jsdom": "^27", "lodash": "^4.17.21", - "lodash.throttle": "^4.1.1", "moment": "^2.30.1", "next": "^15.5.2", "next-nprogress-bar": "^2.4.2", @@ -55,7 +37,6 @@ "posthog-js": "^1.157.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-hotkeys-hook": "^5.2.1", "react-slick": "^0.30.2", "sharp": "0.34.4", "slick-carousel": "^1.8.1", @@ -69,7 +50,6 @@ "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.7", - "@types/lodash.throttle": "^4.1.9", "@types/node": "^22.0.0", "@types/react": "^19", "@types/react-dom": "^19", @@ -83,7 +63,6 @@ "jest-next-dynamic-ts": "^0.1.1", "next-router-mock": "^1.0.2", "ol-test-utilities": "0.0.0", - "sass": "^1.93.3", "ts-jest": "^29.2.4", "type-fest": "^5.0.1", "typescript": "^5" diff --git a/yarn.lock b/yarn.lock index f03d115493..56d8d1401f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3264,7 +3264,7 @@ __metadata: languageName: node linkType: hard -"@mitodl/mitxonline-api-axios@npm:^2025.10.21, @mitodl/mitxonline-api-axios@npm:^2025.11.5": +"@mitodl/mitxonline-api-axios@npm:^2025.11.5": version: 2025.11.5 resolution: "@mitodl/mitxonline-api-axios@npm:2025.11.5" dependencies: @@ -5088,15 +5088,6 @@ __metadata: languageName: node linkType: hard -"@remixicon/react@npm:^4.7.0": - version: 4.7.0 - resolution: "@remixicon/react@npm:4.7.0" - peerDependencies: - react: ">=18.2.0" - checksum: 10/9ecee093dd4aec3744bcc7562eddb987bf9c7c059787dfecfbcc25a64c2f4833dc815dff715aa2a7f75011d65f877b3751a401d19cf2d725b5bcc685587d37f4 - languageName: node - linkType: hard - "@rollup/plugin-commonjs@npm:28.0.1": version: 28.0.1 resolution: "@rollup/plugin-commonjs@npm:28.0.1" @@ -6529,7 +6520,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-document@npm:^3.10.2, @tiptap/extension-document@npm:^3.10.5": +"@tiptap/extension-document@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-document@npm:3.10.5" peerDependencies: @@ -6585,7 +6576,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-highlight@npm:^3.10.1, @tiptap/extension-highlight@npm:^3.10.5": +"@tiptap/extension-highlight@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-highlight@npm:3.10.5" peerDependencies: @@ -6594,7 +6585,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-horizontal-rule@npm:^3.10.1, @tiptap/extension-horizontal-rule@npm:^3.10.5": +"@tiptap/extension-horizontal-rule@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-horizontal-rule@npm:3.10.5" peerDependencies: @@ -6604,7 +6595,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-image@npm:^3.10.1, @tiptap/extension-image@npm:^3.10.5": +"@tiptap/extension-image@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-image@npm:3.10.5" peerDependencies: @@ -6652,7 +6643,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-list@npm:^3.10.1, @tiptap/extension-list@npm:^3.10.5": +"@tiptap/extension-list@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-list@npm:3.10.5" peerDependencies: @@ -6689,7 +6680,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-subscript@npm:^3.10.1, @tiptap/extension-subscript@npm:^3.10.5": +"@tiptap/extension-subscript@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-subscript@npm:3.10.5" peerDependencies: @@ -6699,7 +6690,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-superscript@npm:^3.10.1, @tiptap/extension-superscript@npm:^3.10.5": +"@tiptap/extension-superscript@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-superscript@npm:3.10.5" peerDependencies: @@ -6709,7 +6700,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-text-align@npm:^3.10.1, @tiptap/extension-text-align@npm:^3.10.5": +"@tiptap/extension-text-align@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-text-align@npm:3.10.5" peerDependencies: @@ -6727,7 +6718,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extension-typography@npm:^3.10.1, @tiptap/extension-typography@npm:^3.10.5": +"@tiptap/extension-typography@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extension-typography@npm:3.10.5" peerDependencies: @@ -6745,7 +6736,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/extensions@npm:^3.10.1, @tiptap/extensions@npm:^3.10.5": +"@tiptap/extensions@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/extensions@npm:3.10.5" peerDependencies: @@ -6755,19 +6746,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/markdown@npm:^3.10.1": - version: 3.10.5 - resolution: "@tiptap/markdown@npm:3.10.5" - dependencies: - marked: "npm:^16.1.2" - peerDependencies: - "@tiptap/core": ^3.10.5 - "@tiptap/pm": ^3.10.5 - checksum: 10/db61194b89ede8843a884ffc73485ecc1f009c038333e31a92142fb36577b6665c5ee09f5a31f8d87f0b1f29d3f9f80c1ff7f0210b0e33f995d8f68d0fbc29d7 - languageName: node - linkType: hard - -"@tiptap/pm@npm:^3.10.1, @tiptap/pm@npm:^3.10.5": +"@tiptap/pm@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/pm@npm:3.10.5" dependencies: @@ -6793,7 +6772,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/react@npm:^3.10.1, @tiptap/react@npm:^3.10.5": +"@tiptap/react@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/react@npm:3.10.5" dependencies: @@ -6818,7 +6797,7 @@ __metadata: languageName: node linkType: hard -"@tiptap/starter-kit@npm:^3.10.1, @tiptap/starter-kit@npm:^3.10.5": +"@tiptap/starter-kit@npm:^3.10.5": version: 3.10.5 resolution: "@tiptap/starter-kit@npm:3.10.5" dependencies: @@ -6850,16 +6829,6 @@ __metadata: languageName: node linkType: hard -"@tiptap/suggestion@npm:^3.10.2": - version: 3.10.5 - resolution: "@tiptap/suggestion@npm:3.10.5" - peerDependencies: - "@tiptap/core": ^3.10.5 - "@tiptap/pm": ^3.10.5 - checksum: 10/a8ca10a5c2fc757d8a97a7ad0fc96f8f0a197a16178f0a69fc9dc4423cb239dc85cf48141f0bccd5e116230e5d039959d536214e75b74c3506a2111330bd2ff5 - languageName: node - linkType: hard - "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -8797,6 +8766,13 @@ __metadata: languageName: node linkType: hard +"async_hooks@npm:^1.0.0": + version: 1.0.0 + resolution: "async_hooks@npm:1.0.0" + checksum: 10/d31a4cb971a980b095cde18879598d1b196b32cb38905d1c2ed0aca7ebc7e040344af52fe2dedba410a1eaf4bc12d63db797b91285048387cac78319a6366901 + languageName: node + linkType: hard + "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -15312,44 +15288,26 @@ __metadata: "@emotion/cache": "npm:^11.13.1" "@emotion/styled": "npm:^11.11.0" "@faker-js/faker": "npm:^10.0.0" - "@floating-ui/react": "npm:^0.27.16" "@mitodl/course-search-utils": "npm:^3.5.0" - "@mitodl/mitxonline-api-axios": "npm:^2025.10.21" + "@mitodl/mitxonline-api-axios": "npm:^2025.11.5" "@mitodl/smoot-design": "npm:^6.17.1" "@next/bundle-analyzer": "npm:^14.2.15" - "@radix-ui/react-dropdown-menu": "npm:^2.1.16" - "@radix-ui/react-popover": "npm:^1.1.15" "@react-pdf/renderer": "npm:^4.3.0" - "@remixicon/react": "npm:^4.7.0" + "@remixicon/react": "npm:^4.2.0" "@sentry/nextjs": "npm:^10.0.0" "@tanstack/react-query": "npm:^5.66" "@testing-library/jest-dom": "npm:^6.4.8" "@testing-library/react": "npm:^16.3.0" "@testing-library/user-event": "npm:^14.5.2" - "@tiptap/extension-document": "npm:^3.10.2" - "@tiptap/extension-highlight": "npm:^3.10.1" - "@tiptap/extension-horizontal-rule": "npm:^3.10.1" - "@tiptap/extension-image": "npm:^3.10.1" - "@tiptap/extension-list": "npm:^3.10.1" - "@tiptap/extension-subscript": "npm:^3.10.1" - "@tiptap/extension-superscript": "npm:^3.10.1" - "@tiptap/extension-text-align": "npm:^3.10.1" - "@tiptap/extension-typography": "npm:^3.10.1" - "@tiptap/extensions": "npm:^3.10.1" - "@tiptap/markdown": "npm:^3.10.1" - "@tiptap/pm": "npm:^3.10.1" - "@tiptap/react": "npm:^3.10.1" - "@tiptap/starter-kit": "npm:^3.10.1" - "@tiptap/suggestion": "npm:^3.10.2" "@types/jest": "npm:^29.5.12" "@types/lodash": "npm:^4.17.7" - "@types/lodash.throttle": "npm:^4.1.9" "@types/node": "npm:^22.0.0" "@types/react": "npm:^19" "@types/react-dom": "npm:^19" "@types/react-slick": "npm:^0.23.13" "@types/slick-carousel": "npm:^1" api: "workspace:*" + async_hooks: "npm:^1.0.0" classnames: "npm:^2.5.1" eslint: "npm:8.57.1" eslint-config-next: "npm:^14.2.7" @@ -15362,7 +15320,6 @@ __metadata: jest-next-dynamic-ts: "npm:^0.1.1" jsdom: "npm:^27" lodash: "npm:^4.17.21" - lodash.throttle: "npm:^4.1.1" moment: "npm:^2.30.1" next: "npm:^15.5.2" next-nprogress-bar: "npm:^2.4.2" @@ -15373,9 +15330,7 @@ __metadata: posthog-js: "npm:^1.157.2" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" - react-hotkeys-hook: "npm:^5.2.1" react-slick: "npm:^0.30.2" - sass: "npm:^1.93.3" sharp: "npm:0.34.4" slick-carousel: "npm:^1.8.1" tiny-invariant: "npm:^1.3.3" @@ -15479,15 +15434,6 @@ __metadata: languageName: node linkType: hard -"marked@npm:^16.1.2": - version: 16.4.2 - resolution: "marked@npm:16.4.2" - bin: - marked: bin/marked.js - checksum: 10/6e40e40661dce97e271198daa2054fc31e6445892a735e416c248fba046bdfa4573cafa08dc254529f105e7178a34485eb7f82573979cfb377a4530f66e79187 - languageName: node - linkType: hard - "material-ui-popup-state@npm:^5.1.0": version: 5.3.1 resolution: "material-ui-popup-state@npm:5.3.1" From 335c7b9212e0610c5b8afe037895e82feb009d3d Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:03:42 +0000 Subject: [PATCH 08/16] Remove aliases from Tiptap template code imports --- frontends/main/next.config.js | 72 ------------------- .../components/TiptapEditor/TiptapEditor.tsx | 42 +++++------ .../image-upload-node-extension.ts | 2 +- .../image-upload-node/image-upload-node.tsx | 10 +-- .../tiptap-templates/simple/simple-editor.tsx | 62 ++++++++-------- .../tiptap-templates/simple/theme-toggle.tsx | 6 +- .../tiptap-ui-primitive/badge/badge.tsx | 6 +- .../tiptap-ui-primitive/button/button.tsx | 10 +-- .../tiptap-ui-primitive/card/card.tsx | 4 +- .../dropdown-menu/dropdown-menu.tsx | 4 +- .../tiptap-ui-primitive/input/input.tsx | 4 +- .../tiptap-ui-primitive/popover/popover.tsx | 4 +- .../separator/separator.tsx | 4 +- .../tiptap-ui-primitive/toolbar/toolbar.tsx | 10 +-- .../tiptap-ui-primitive/tooltip/tooltip.tsx | 2 +- .../blockquote-button/blockquote-button.tsx | 14 ++-- .../blockquote-button/use-blockquote.ts | 6 +- .../code-block-button/code-block-button.tsx | 14 ++-- .../code-block-button/use-code-block.ts | 6 +- .../color-highlight-button.tsx | 16 ++--- .../use-color-highlight.ts | 32 ++++++--- .../color-highlight-popover.tsx | 24 +++---- .../heading-button/heading-button.tsx | 14 ++-- .../tiptap-ui/heading-button/use-heading.ts | 16 ++--- .../heading-dropdown-menu.tsx | 18 ++--- .../use-heading-dropdown-menu.ts | 6 +- .../image-upload-button.tsx | 14 ++-- .../image-upload-button/use-image-upload.ts | 8 +-- .../tiptap-ui/link-popover/link-popover.tsx | 28 ++++---- .../link-popover/use-link-popover.ts | 6 +- .../tiptap-ui/list-button/list-button.tsx | 14 ++-- .../tiptap-ui/list-button/use-list.ts | 10 +-- .../list-dropdown-menu/list-dropdown-menu.tsx | 16 ++--- .../use-list-dropdown-menu.ts | 12 ++-- .../tiptap-ui/mark-button/mark-button.tsx | 14 ++-- .../tiptap-ui/mark-button/use-mark.ts | 18 ++--- .../text-align-button/text-align-button.tsx | 14 ++-- .../text-align-button/use-text-align.ts | 12 ++-- .../undo-redo-button/undo-redo-button.tsx | 14 ++-- .../undo-redo-button/use-undo-redo.ts | 8 +-- .../hooks/use-cursor-visibility.ts | 4 +- .../TiptapEditor/hooks/use-element-rect.ts | 2 +- .../hooks/use-throttled-callback.ts | 2 +- .../TiptapEditor/hooks/use-window-size.ts | 2 +- .../src/components/TiptapEditor/tsconfig.json | 12 ---- 45 files changed, 275 insertions(+), 343 deletions(-) delete mode 100644 frontends/ol-components/src/components/TiptapEditor/tsconfig.json diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js index a0898ded6d..240d63e645 100644 --- a/frontends/main/next.config.js +++ b/frontends/main/next.config.js @@ -129,78 +129,6 @@ const nextConfig = { }) } - // Custom resolver for TiptapEditor's @/ imports - // Only applies special aliases when importing from within TiptapEditor directory - // Tiptap's Simple Editor ejects files to the project, which we'd prefer not to edit - // to update import paths. - const path = require("path") - const tiptapEditorPath = path.resolve( - __dirname, - "../ol-components/src/components/TiptapEditor", - ) - - config.resolve.plugins = config.resolve.plugins || [] - config.resolve.plugins.push({ - apply(resolver) { - const target = resolver.ensureHook("resolve") - - resolver - .getHook("described-resolve") - .tapAsync( - "TiptapEditorAliasPlugin", - (request, resolveContext, callback) => { - const issuer = request.context?.issuer - - // Only apply custom aliases if the request is coming from TiptapEditor - if (!issuer || !issuer.includes("TiptapEditor")) { - return callback() - } - - // Check if this is a @/components, @/lib, or @/hooks import - const originalRequest = request.request - let newRequestPath = null - - if (originalRequest?.startsWith("@/components/")) { - const importPath = originalRequest.substring( - "@/components/".length, - ) - newRequestPath = path.join( - tiptapEditorPath, - "components", - importPath, - ) - } else if (originalRequest?.startsWith("@/lib/")) { - const importPath = originalRequest.substring("@/lib/".length) - newRequestPath = path.join(tiptapEditorPath, "lib", importPath) - } else if (originalRequest?.startsWith("@/hooks/")) { - const importPath = originalRequest.substring("@/hooks/".length) - newRequestPath = path.join( - tiptapEditorPath, - "hooks", - importPath, - ) - } - - if (newRequestPath) { - const newRequest = { - ...request, - request: newRequestPath, - } - return resolver.doResolve( - target, - newRequest, - "aliased with TiptapEditor prefix", - resolveContext, - callback, - ) - } - - callback() - }, - ) - }, - }) - return config }, } diff --git a/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx b/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx index 762dafb93b..a45ee5d91d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/TiptapEditor.tsx @@ -16,37 +16,37 @@ import { Superscript } from "@tiptap/extension-superscript" import { Selection } from "@tiptap/extensions" // --- UI Primitives --- -import { Spacer } from "@/components/tiptap-ui-primitive/spacer" +import { Spacer } from "./components/tiptap-ui-primitive/spacer" import { Toolbar, ToolbarGroup, ToolbarSeparator, -} from "@/components/tiptap-ui-primitive/toolbar" +} from "./components/tiptap-ui-primitive/toolbar" // --- Tiptap Node --- -import { ImageUploadNode } from "@/components/tiptap-node/image-upload-node/image-upload-node-extension" -import { HorizontalRule } from "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension" -import "@/components/tiptap-node/blockquote-node/blockquote-node.scss" -import "@/components/tiptap-node/code-block-node/code-block-node.scss" -import "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss" -import "@/components/tiptap-node/list-node/list-node.scss" -import "@/components/tiptap-node/image-node/image-node.scss" -import "@/components/tiptap-node/heading-node/heading-node.scss" -import "@/components/tiptap-node/paragraph-node/paragraph-node.scss" +import { ImageUploadNode } from "./components/tiptap-node/image-upload-node/image-upload-node-extension" +import { HorizontalRule } from "./components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension" +import "./components/tiptap-node/blockquote-node/blockquote-node.scss" +import "./components/tiptap-node/code-block-node/code-block-node.scss" +import "./components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss" +import "./components/tiptap-node/list-node/list-node.scss" +import "./components/tiptap-node/image-node/image-node.scss" +import "./components/tiptap-node/heading-node/heading-node.scss" +import "./components/tiptap-node/paragraph-node/paragraph-node.scss" // --- Tiptap UI --- -import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" -import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" -import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button" -import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button" -import { ColorHighlightPopover } from "@/components/tiptap-ui/color-highlight-popover" -import { LinkPopover } from "@/components/tiptap-ui/link-popover" -import { MarkButton } from "@/components/tiptap-ui/mark-button" -import { TextAlignButton } from "@/components/tiptap-ui/text-align-button" -import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button" +import { HeadingDropdownMenu } from "./components/tiptap-ui/heading-dropdown-menu" +import { ListDropdownMenu } from "./components/tiptap-ui/list-dropdown-menu" +import { BlockquoteButton } from "./components/tiptap-ui/blockquote-button" +import { CodeBlockButton } from "./components/tiptap-ui/code-block-button" +import { ColorHighlightPopover } from "./components/tiptap-ui/color-highlight-popover" +import { LinkPopover } from "./components/tiptap-ui/link-popover" +import { MarkButton } from "./components/tiptap-ui/mark-button" +import { TextAlignButton } from "./components/tiptap-ui/text-align-button" +import { UndoRedoButton } from "./components/tiptap-ui/undo-redo-button" // --- Lib --- -import { handleImageUpload, MAX_FILE_SIZE } from "@/lib/tiptap-utils" +import { handleImageUpload, MAX_FILE_SIZE } from "./lib/tiptap-utils" // --- Styles --- import "./styles/_keyframe-animations.scss" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts index a870707431..deceed64ee 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node-extension.ts @@ -1,6 +1,6 @@ import { mergeAttributes, Node } from "@tiptap/react" import { ReactNodeViewRenderer } from "@tiptap/react" -import { ImageUploadNode as ImageUploadNodeComponent } from "@/components/tiptap-node/image-upload-node/image-upload-node" +import { ImageUploadNode as ImageUploadNodeComponent } from "./image-upload-node" import type { NodeType } from "@tiptap/pm/model" export type UploadFunction = ( diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx index 81e570bcdb..c5b5ead26d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-node/image-upload-node/image-upload-node.tsx @@ -1,12 +1,12 @@ "use client" -import { useRef, useState } from "react" +import React, { useRef, useState } from "react" import type { NodeViewProps } from "@tiptap/react" import { NodeViewWrapper } from "@tiptap/react" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { CloseIcon } from "@/components/tiptap-icons/close-icon" -import "@/components/tiptap-node/image-upload-node/image-upload-node.scss" -import { focusNextNode, isValidPosition } from "@/lib/tiptap-utils" +import { Button } from "../../tiptap-ui-primitive/button" +import { CloseIcon } from "../../tiptap-icons/close-icon" +import "./image-upload-node.scss" +import { focusNextNode, isValidPosition } from "../../../lib/tiptap-utils" export interface FileItem { /** diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx index b7c268349a..5be12cc8a7 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/simple-editor.tsx @@ -18,63 +18,63 @@ import { Superscript } from "@tiptap/extension-superscript" import { Selection } from "@tiptap/extensions" // --- UI Primitives --- -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Spacer } from "@/components/tiptap-ui-primitive/spacer" +import { Button } from "../../tiptap-ui-primitive/button" +import { Spacer } from "../../tiptap-ui-primitive/spacer" import { Toolbar, ToolbarGroup, ToolbarSeparator, -} from "@/components/tiptap-ui-primitive/toolbar" +} from "../../tiptap-ui-primitive/toolbar" // --- Tiptap Node --- -import { ImageUploadNode } from "@/components/tiptap-node/image-upload-node/image-upload-node-extension" -import { HorizontalRule } from "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension" -import "@/components/tiptap-node/blockquote-node/blockquote-node.scss" -import "@/components/tiptap-node/code-block-node/code-block-node.scss" -import "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss" -import "@/components/tiptap-node/list-node/list-node.scss" -import "@/components/tiptap-node/image-node/image-node.scss" -import "@/components/tiptap-node/heading-node/heading-node.scss" -import "@/components/tiptap-node/paragraph-node/paragraph-node.scss" +import { ImageUploadNode } from "../../tiptap-node/image-upload-node/image-upload-node-extension" +import { HorizontalRule } from "../../tiptap-node/horizontal-rule-node/horizontal-rule-node-extension" +import "../../tiptap-node/blockquote-node/blockquote-node.scss" +import "../../tiptap-node/code-block-node/code-block-node.scss" +import "../../tiptap-node/horizontal-rule-node/horizontal-rule-node.scss" +import "../../tiptap-node/list-node/list-node.scss" +import "../../tiptap-node/image-node/image-node.scss" +import "../../tiptap-node/heading-node/heading-node.scss" +import "../../tiptap-node/paragraph-node/paragraph-node.scss" // --- Tiptap UI --- -import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" -import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button" -import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" -import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button" -import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button" +import { HeadingDropdownMenu } from "../../tiptap-ui/heading-dropdown-menu" +import { ImageUploadButton } from "../../tiptap-ui/image-upload-button" +import { ListDropdownMenu } from "../../tiptap-ui/list-dropdown-menu" +import { BlockquoteButton } from "../../tiptap-ui/blockquote-button" +import { CodeBlockButton } from "../../tiptap-ui/code-block-button" import { ColorHighlightPopover, ColorHighlightPopoverContent, ColorHighlightPopoverButton, -} from "@/components/tiptap-ui/color-highlight-popover" +} from "../../tiptap-ui/color-highlight-popover" import { LinkPopover, LinkContent, LinkButton, -} from "@/components/tiptap-ui/link-popover" -import { MarkButton } from "@/components/tiptap-ui/mark-button" -import { TextAlignButton } from "@/components/tiptap-ui/text-align-button" -import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button" +} from "../../tiptap-ui/link-popover" +import { MarkButton } from "../../tiptap-ui/mark-button" +import { TextAlignButton } from "../../tiptap-ui/text-align-button" +import { UndoRedoButton } from "../../tiptap-ui/undo-redo-button" // --- Icons --- -import { ArrowLeftIcon } from "@/components/tiptap-icons/arrow-left-icon" -import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" -import { LinkIcon } from "@/components/tiptap-icons/link-icon" +import { ArrowLeftIcon } from "../../tiptap-icons/arrow-left-icon" +import { HighlighterIcon } from "../../tiptap-icons/highlighter-icon" +import { LinkIcon } from "../../tiptap-icons/link-icon" // --- Hooks --- -import { useIsMobile } from "@/hooks/use-mobile" -import { useWindowSize } from "@/hooks/use-window-size" -import { useCursorVisibility } from "@/hooks/use-cursor-visibility" +import { useIsMobile } from "../../../hooks/use-mobile" +import { useWindowSize } from "../../../hooks/use-window-size" +import { useCursorVisibility } from "../../../hooks/use-cursor-visibility" // --- Components --- -import { ThemeToggle } from "@/components/tiptap-templates/simple/theme-toggle" +import { ThemeToggle } from "./theme-toggle" // --- Lib --- -import { handleImageUpload, MAX_FILE_SIZE } from "@/lib/tiptap-utils" +import { handleImageUpload, MAX_FILE_SIZE } from "../../../lib/tiptap-utils" // --- Styles --- -import "@/components/tiptap-templates/simple/simple-editor.scss" +import "./simple-editor.scss" const MainToolbarContent = ({ onHighlighterClick, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx index 18645e8d4f..cd62b8886b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx @@ -1,8 +1,8 @@ -import { Button } from "@/components/tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" // --- Icons --- -import { MoonStarIcon } from "@/components/tiptap-icons/moon-star-icon" -import { SunIcon } from "@/components/tiptap-icons/sun-icon" +import { MoonStarIcon } from "../../tiptap-icons/moon-star-icon" +import { SunIcon } from "../../tiptap-icons/sun-icon" import { useEffect, useState } from "react" export function ThemeToggle() { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx index b41415eb51..85561d29e1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx @@ -1,7 +1,7 @@ import { forwardRef } from "react" -import "@/components/tiptap-ui-primitive/badge/badge-colors.scss" -import "@/components/tiptap-ui-primitive/badge/badge-group.scss" -import "@/components/tiptap-ui-primitive/badge/badge.scss" +import "./badge-colors.scss" +import "./badge-group.scss" +import "./badge.scss" export interface BadgeProps extends React.HTMLAttributes { variant?: "ghost" | "white" | "gray" | "green" | "default" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx index 953bdd88a8..3b60242bb3 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx @@ -5,14 +5,14 @@ import { Tooltip, TooltipContent, TooltipTrigger, -} from "@/components/tiptap-ui-primitive/tooltip" +} from "../tooltip" // --- Lib --- -import { cn, parseShortcutKeys } from "@/lib/tiptap-utils" +import { cn, parseShortcutKeys } from "../../../lib/tiptap-utils" -import "@/components/tiptap-ui-primitive/button/button-colors.scss" -import "@/components/tiptap-ui-primitive/button/button-group.scss" -import "@/components/tiptap-ui-primitive/button/button.scss" +import "./button-colors.scss" +import "./button-group.scss" +import "./button.scss" export interface ButtonProps extends React.ButtonHTMLAttributes { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx index 8a977f3dad..71e11cedbb 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx @@ -1,8 +1,8 @@ "use client" import { forwardRef } from "react" -import { cn } from "@/lib/tiptap-utils" -import "@/components/tiptap-ui-primitive/card/card.scss" +import { cn } from "../../../lib/tiptap-utils" +import "./card.scss" const Card = forwardRef>( ({ className, ...props }, ref) => { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx index 0a980605c5..cb236b2ff0 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx @@ -1,7 +1,7 @@ import { forwardRef } from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { cn } from "@/lib/tiptap-utils" -import "@/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss" +import { cn } from "../../../lib/tiptap-utils" +import "./dropdown-menu.scss" function DropdownMenu({ ...props diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx index 39203ffc5c..87eaa99e1b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx @@ -1,5 +1,5 @@ -import { cn } from "@/lib/tiptap-utils" -import "@/components/tiptap-ui-primitive/input/input.scss" +import { cn } from "../../../lib/tiptap-utils" +import "./input.scss" function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx index 9bd52f141d..c28ab99510 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx @@ -1,6 +1,6 @@ import * as PopoverPrimitive from "@radix-ui/react-popover" -import { cn } from "@/lib/tiptap-utils" -import "@/components/tiptap-ui-primitive/popover/popover.scss" +import { cn } from "../../../lib/tiptap-utils" +import "./popover.scss" function Popover({ ...props diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx index d581b202fb..f91a1043e8 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx @@ -1,6 +1,6 @@ import { forwardRef } from "react" -import "@/components/tiptap-ui-primitive/separator/separator.scss" -import { cn } from "@/lib/tiptap-utils" +import "./separator.scss" +import { cn } from "../../../lib/tiptap-utils" export type Orientation = "horizontal" | "vertical" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx index c7d80e33a4..edef91c57f 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx @@ -1,9 +1,9 @@ import { forwardRef, useCallback, useEffect, useRef, useState } from "react" -import { Separator } from "@/components/tiptap-ui-primitive/separator" -import "@/components/tiptap-ui-primitive/toolbar/toolbar.scss" -import { cn } from "@/lib/tiptap-utils" -import { useMenuNavigation } from "@/hooks/use-menu-navigation" -import { useComposedRef } from "@/hooks/use-composed-ref" +import { Separator } from "../separator" +import "./toolbar.scss" +import { cn } from "../../../lib/tiptap-utils" +import { useMenuNavigation } from "../../../hooks/use-menu-navigation" +import { useComposedRef } from "../../../hooks/use-composed-ref" type BaseProps = React.HTMLAttributes diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx index a3a92a5834..bfc6dddfc1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx @@ -28,7 +28,7 @@ import { type ReferenceType, FloatingDelayGroup, } from "@floating-ui/react" -import "@/components/tiptap-ui-primitive/tooltip/tooltip.scss" +import "./tooltip.scss" interface TooltipProviderProps { children: React.ReactNode diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx index e56d5ef5a0..d82a6577ba 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx @@ -1,22 +1,22 @@ import { forwardRef, useCallback } from "react" // --- Tiptap UI --- -import type { UseBlockquoteConfig } from "@/components/tiptap-ui/blockquote-button" +import type { UseBlockquoteConfig } from "./" import { BLOCKQUOTE_SHORTCUT_KEY, useBlockquote, -} from "@/components/tiptap-ui/blockquote-button" +} from "./" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" export interface BlockquoteButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts index c8887a6e4c..81161fc61b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/use-blockquote.ts @@ -5,10 +5,10 @@ import type { Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { BlockquoteIcon } from "@/components/tiptap-icons/blockquote-icon" +import { BlockquoteIcon } from "../../tiptap-icons/blockquote-icon" // --- UI Utils --- import { @@ -17,7 +17,7 @@ import { isNodeTypeSelected, isValidPosition, selectionWithinConvertibleTypes, -} from "@/lib/tiptap-utils" +} from "../../../lib/tiptap-utils" export const BLOCKQUOTE_SHORTCUT_KEY = "mod+shift+b" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx index ddb1040dd3..85c54fa527 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx @@ -1,22 +1,22 @@ import { forwardRef, useCallback } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Tiptap UI --- -import type { UseCodeBlockConfig } from "@/components/tiptap-ui/code-block-button" +import type { UseCodeBlockConfig } from "./" import { CODE_BLOCK_SHORTCUT_KEY, useCodeBlock, -} from "@/components/tiptap-ui/code-block-button" +} from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" export interface CodeBlockButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts index fd7877b320..167860a625 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/use-code-block.ts @@ -5,7 +5,7 @@ import { type Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- import { @@ -14,10 +14,10 @@ import { isNodeTypeSelected, isValidPosition, selectionWithinConvertibleTypes, -} from "@/lib/tiptap-utils" +} from "../../../lib/tiptap-utils" // --- Icons --- -import { CodeBlockIcon } from "@/components/tiptap-icons/code-block-icon" +import { CodeBlockIcon } from "../../tiptap-icons/code-block-icon" export const CODE_BLOCK_SHORTCUT_KEY = "mod+alt+c" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx index 26b5a488ad..d9bc08e655 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx @@ -1,25 +1,25 @@ import { forwardRef, useCallback, useMemo } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- -import type { UseColorHighlightConfig } from "@/components/tiptap-ui/color-highlight-button" +import type { UseColorHighlightConfig } from "./" import { COLOR_HIGHLIGHT_SHORTCUT_KEY, useColorHighlight, -} from "@/components/tiptap-ui/color-highlight-button" +} from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" // --- Styles --- -import "@/components/tiptap-ui/color-highlight-button/color-highlight-button.scss" +import "./color-highlight-button.scss" export interface ColorHighlightButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts index 7cb7350d40..3b4cfe61db 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/use-color-highlight.ts @@ -5,18 +5,18 @@ import { type Editor } from "@tiptap/react" import { useHotkeys } from "react-hotkeys-hook" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" -import { useIsMobile } from "@/hooks/use-mobile" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" +import { useIsMobile } from "../../../hooks/use-mobile" // --- Lib --- import { isMarkInSchema, isNodeTypeSelected, isExtensionAvailable, -} from "@/lib/tiptap-utils" +} from "../../../lib/tiptap-utils" // --- Icons --- -import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" +import { HighlighterIcon } from "../../tiptap-icons/highlighter-icon" export const COLOR_HIGHLIGHT_SHORTCUT_KEY = "mod+shift+h" export const HIGHLIGHT_COLORS = [ @@ -147,7 +147,16 @@ export function canColorHighlight( if (!isExtensionAvailable(editor, ["nodeBackground"])) return false try { - return editor.can().toggleNodeBackgroundColor("test") + // Some editor instances may not have this command, + // so check for its existence before trying to call it. + const canCommands = editor.can?.() + if ( + canCommands && + typeof (canCommands as any).toggleNodeBackgroundColor === "function" + ) { + return (canCommands as any).toggleNodeBackgroundColor("test") + } + return false } catch { return false } @@ -202,10 +211,17 @@ export function removeHighlight( if (mode === "mark") { return editor.chain().focus().unsetMark("highlight").run() } else { - return editor.chain().focus().unsetNodeBackgroundColor().run() + // The chained command `unsetNodeBackgroundColor` does not exist. + // We'll fallback to a helper if available, or do nothing (return false). + if ( + typeof (editor as any).commands?.unsetNodeBackgroundColor === "function" + ) { + ;(editor as any).commands.unsetNodeBackgroundColor() + return true + } + return false } } - /** * Determines if the highlight button should be shown */ @@ -294,7 +310,7 @@ export function useColorHighlight(config: UseColorHighlightConfig) { const success = editor .chain() .focus() - .toggleNodeBackgroundColor(highlightColor) + .setHighlight({ color: highlightColor }) .run() if (success) { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx index 43aff245d3..3cc1ab2360 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx @@ -2,39 +2,39 @@ import { forwardRef, useMemo, useRef, useState } from "react" import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useMenuNavigation } from "@/hooks/use-menu-navigation" -import { useIsMobile } from "@/hooks/use-mobile" -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useMenuNavigation } from "../../../hooks/use-menu-navigation" +import { useIsMobile } from "../../../hooks/use-mobile" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { BanIcon } from "@/components/tiptap-icons/ban-icon" -import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" +import { BanIcon } from "../../tiptap-icons/ban-icon" +import { HighlighterIcon } from "../../tiptap-icons/highlighter-icon" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "../../tiptap-ui-primitive/button" import { Popover, PopoverTrigger, PopoverContent, -} from "@/components/tiptap-ui-primitive/popover" -import { Separator } from "@/components/tiptap-ui-primitive/separator" +} from "../../tiptap-ui-primitive/popover" +import { Separator } from "../../tiptap-ui-primitive/separator" import { Card, CardBody, CardItemGroup, -} from "@/components/tiptap-ui-primitive/card" +} from "../../tiptap-ui-primitive/card" // --- Tiptap UI --- import type { HighlightColor, UseColorHighlightConfig, -} from "@/components/tiptap-ui/color-highlight-button" +} from "../color-highlight-button" import { ColorHighlightButton, pickHighlightColorsByValue, useColorHighlight, -} from "@/components/tiptap-ui/color-highlight-button" +} from "../color-highlight-button" export interface ColorHighlightPopoverContentProps { /** diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx index 39cf36ef8c..d4ad41171e 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx @@ -1,23 +1,23 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Tiptap UI --- import type { Level, UseHeadingConfig, -} from "@/components/tiptap-ui/heading-button" +} from "./" import { HEADING_SHORTCUT_KEYS, useHeading, -} from "@/components/tiptap-ui/heading-button" +} from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" export interface HeadingButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts index d308c772b2..f6a647b801 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/use-heading.ts @@ -5,7 +5,7 @@ import { type Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- import { @@ -14,15 +14,15 @@ import { isNodeTypeSelected, isValidPosition, selectionWithinConvertibleTypes, -} from "@/lib/tiptap-utils" +} from "../../../lib/tiptap-utils" // --- Icons --- -import { HeadingOneIcon } from "@/components/tiptap-icons/heading-one-icon" -import { HeadingTwoIcon } from "@/components/tiptap-icons/heading-two-icon" -import { HeadingThreeIcon } from "@/components/tiptap-icons/heading-three-icon" -import { HeadingFourIcon } from "@/components/tiptap-icons/heading-four-icon" -import { HeadingFiveIcon } from "@/components/tiptap-icons/heading-five-icon" -import { HeadingSixIcon } from "@/components/tiptap-icons/heading-six-icon" +import { HeadingOneIcon } from "../../tiptap-icons/heading-one-icon" +import { HeadingTwoIcon } from "../../tiptap-icons/heading-two-icon" +import { HeadingThreeIcon } from "../../tiptap-icons/heading-three-icon" +import { HeadingFourIcon } from "../../tiptap-icons/heading-four-icon" +import { HeadingFiveIcon } from "../../tiptap-icons/heading-five-icon" +import { HeadingSixIcon } from "../../tiptap-icons/heading-six-icon" export type Level = 1 | 2 | 3 | 4 | 5 | 6 diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx index 71930b54d7..21a3ff9514 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx @@ -1,26 +1,26 @@ import { forwardRef, useCallback, useState } from "react" // --- Icons --- -import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon" +import { ChevronDownIcon } from "../../tiptap-icons/chevron-down-icon" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- -import { HeadingButton } from "@/components/tiptap-ui/heading-button" -import type { UseHeadingDropdownMenuConfig } from "@/components/tiptap-ui/heading-dropdown-menu" -import { useHeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" +import { HeadingButton } from "../heading-button" +import type { UseHeadingDropdownMenuConfig } from "./" +import { useHeadingDropdownMenu } from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "../../tiptap-ui-primitive/button" import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, -} from "@/components/tiptap-ui-primitive/dropdown-menu" -import { Card, CardBody } from "@/components/tiptap-ui-primitive/card" +} from "../../tiptap-ui-primitive/dropdown-menu" +import { Card, CardBody } from "../../tiptap-ui-primitive/card" export interface HeadingDropdownMenuProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts index c8539db6ef..597109ad00 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts @@ -4,10 +4,10 @@ import { useEffect, useState } from "react" import type { Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { HeadingIcon } from "@/components/tiptap-icons/heading-icon" +import { HeadingIcon } from "../../tiptap-icons/heading-icon" // --- Tiptap UI --- import { @@ -16,7 +16,7 @@ import { isHeadingActive, canToggle, shouldShowButton, -} from "@/components/tiptap-ui/heading-button" +} from "../heading-button" /** * Configuration for the heading dropdown menu functionality diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx index 9d3f10020f..4feab889a3 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx @@ -1,22 +1,22 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- -import type { UseImageUploadConfig } from "@/components/tiptap-ui/image-upload-button" +import type { UseImageUploadConfig } from "./" import { IMAGE_UPLOAD_SHORTCUT_KEY, useImageUpload, -} from "@/components/tiptap-ui/image-upload-button" +} from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" type IconProps = React.SVGProps type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts index 13b445c256..156bf0649b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/use-image-upload.ts @@ -5,14 +5,14 @@ import { useHotkeys } from "react-hotkeys-hook" import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" -import { useIsMobile } from "@/hooks/use-mobile" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" +import { useIsMobile } from "../../../hooks/use-mobile" // --- Lib --- -import { isExtensionAvailable } from "@/lib/tiptap-utils" +import { isExtensionAvailable } from "../../../lib/tiptap-utils" // --- Icons --- -import { ImagePlusIcon } from "@/components/tiptap-icons/image-plus-icon" +import { ImagePlusIcon } from "../../tiptap-icons/image-plus-icon" export const IMAGE_UPLOAD_SHORTCUT_KEY = "mod+shift+i" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx index 827660d84b..1fc3222239 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx @@ -4,34 +4,34 @@ import { forwardRef, useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" // --- Hooks --- -import { useIsMobile } from "@/hooks/use-mobile" -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useIsMobile } from "../../../hooks/use-mobile" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { CornerDownLeftIcon } from "@/components/tiptap-icons/corner-down-left-icon" -import { ExternalLinkIcon } from "@/components/tiptap-icons/external-link-icon" -import { LinkIcon } from "@/components/tiptap-icons/link-icon" -import { TrashIcon } from "@/components/tiptap-icons/trash-icon" +import { CornerDownLeftIcon } from "../../tiptap-icons/corner-down-left-icon" +import { ExternalLinkIcon } from "../../tiptap-icons/external-link-icon" +import { LinkIcon } from "../../tiptap-icons/link-icon" +import { TrashIcon } from "../../tiptap-icons/trash-icon" // --- Tiptap UI --- -import type { UseLinkPopoverConfig } from "@/components/tiptap-ui/link-popover" -import { useLinkPopover } from "@/components/tiptap-ui/link-popover" +import type { UseLinkPopoverConfig } from "./" +import { useLinkPopover } from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "../../tiptap-ui-primitive/button" import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/tiptap-ui-primitive/popover" -import { Separator } from "@/components/tiptap-ui-primitive/separator" +} from "../../tiptap-ui-primitive/popover" +import { Separator } from "../../tiptap-ui-primitive/separator" import { Card, CardBody, CardItemGroup, -} from "@/components/tiptap-ui-primitive/card" -import { Input, InputGroup } from "@/components/tiptap-ui-primitive/input" +} from "../../tiptap-ui-primitive/card" +import { Input, InputGroup } from "../../tiptap-ui-primitive/input" export interface LinkMainProps { /** diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts index 2b4e9fce95..3b1d8abeb5 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/use-link-popover.ts @@ -2,17 +2,17 @@ import { useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { LinkIcon } from "@/components/tiptap-icons/link-icon" +import { LinkIcon } from "../../tiptap-icons/link-icon" // --- Lib --- import { isMarkInSchema, isNodeTypeSelected, sanitizeUrl, -} from "@/lib/tiptap-utils" +} from "../../../lib/tiptap-utils" /** * Configuration for the link popover functionality diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx index 80c1278f1b..3cdcb6077b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx @@ -1,22 +1,22 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" // --- Tiptap UI --- import type { ListType, UseListConfig, -} from "@/components/tiptap-ui/list-button" -import { LIST_SHORTCUT_KEYS, useList } from "@/components/tiptap-ui/list-button" +} from "./" +import { LIST_SHORTCUT_KEYS, useList } from "./" export interface ListButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts index 2049e97091..2309f55439 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/use-list.ts @@ -5,12 +5,12 @@ import { type Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { ListIcon } from "@/components/tiptap-icons/list-icon" -import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon" -import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon" +import { ListIcon } from "../../tiptap-icons/list-icon" +import { ListOrderedIcon } from "../../tiptap-icons/list-ordered-icon" +import { ListTodoIcon } from "../../tiptap-icons/list-todo-icon" // --- Lib --- import { @@ -19,7 +19,7 @@ import { isNodeTypeSelected, isValidPosition, selectionWithinConvertibleTypes, -} from "@/lib/tiptap-utils" +} from "../../../lib/tiptap-utils" export type ListType = "bulletList" | "orderedList" | "taskList" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx index 3a98b14fb1..5a9b947377 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx @@ -2,26 +2,26 @@ import { useCallback, useState } from "react" import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon" +import { ChevronDownIcon } from "../../tiptap-icons/chevron-down-icon" // --- Tiptap UI --- -import { ListButton, type ListType } from "@/components/tiptap-ui/list-button" +import { ListButton, type ListType } from "../list-button" -import { useListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu" +import { useListDropdownMenu } from "./use-list-dropdown-menu" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button, ButtonGroup } from "@/components/tiptap-ui-primitive/button" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button, ButtonGroup } from "../../tiptap-ui-primitive/button" import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, -} from "@/components/tiptap-ui-primitive/dropdown-menu" -import { Card, CardBody } from "@/components/tiptap-ui-primitive/card" +} from "../../tiptap-ui-primitive/dropdown-menu" +import { Card, CardBody } from "../../tiptap-ui-primitive/card" export interface ListDropdownMenuProps extends Omit { /** diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts index d713b7f125..21affaa15b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts @@ -4,15 +4,15 @@ import { useEffect, useMemo, useState } from "react" import type { Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Icons --- -import { ListIcon } from "@/components/tiptap-icons/list-icon" -import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon" -import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon" +import { ListIcon } from "../../tiptap-icons/list-icon" +import { ListOrderedIcon } from "../../tiptap-icons/list-ordered-icon" +import { ListTodoIcon } from "../../tiptap-icons/list-todo-icon" // --- Lib --- -import { isNodeInSchema } from "@/lib/tiptap-utils" +import { isNodeInSchema } from "../../../lib/tiptap-utils" // --- Tiptap UI --- import { @@ -20,7 +20,7 @@ import { isListActive, listIcons, type ListType, -} from "@/components/tiptap-ui/list-button" +} from "../list-button" /** * Configuration for the list dropdown menu functionality diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx index 0effb69a48..0aabe80075 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx @@ -3,19 +3,19 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- -import type { Mark, UseMarkConfig } from "@/components/tiptap-ui/mark-button" -import { MARK_SHORTCUT_KEYS, useMark } from "@/components/tiptap-ui/mark-button" +import type { Mark, UseMarkConfig } from "./" +import { MARK_SHORTCUT_KEYS, useMark } from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" export interface MarkButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts index 59b70759d4..c67352618e 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/use-mark.ts @@ -2,19 +2,19 @@ import { useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- -import { isMarkInSchema, isNodeTypeSelected } from "@/lib/tiptap-utils" +import { isMarkInSchema, isNodeTypeSelected } from "../../../lib/tiptap-utils" // --- Icons --- -import { BoldIcon } from "@/components/tiptap-icons/bold-icon" -import { Code2Icon } from "@/components/tiptap-icons/code2-icon" -import { ItalicIcon } from "@/components/tiptap-icons/italic-icon" -import { StrikeIcon } from "@/components/tiptap-icons/strike-icon" -import { SubscriptIcon } from "@/components/tiptap-icons/subscript-icon" -import { SuperscriptIcon } from "@/components/tiptap-icons/superscript-icon" -import { UnderlineIcon } from "@/components/tiptap-icons/underline-icon" +import { BoldIcon } from "../../tiptap-icons/bold-icon" +import { Code2Icon } from "../../tiptap-icons/code2-icon" +import { ItalicIcon } from "../../tiptap-icons/italic-icon" +import { StrikeIcon } from "../../tiptap-icons/strike-icon" +import { SubscriptIcon } from "../../tiptap-icons/subscript-icon" +import { SuperscriptIcon } from "../../tiptap-icons/superscript-icon" +import { UnderlineIcon } from "../../tiptap-icons/underline-icon" export type Mark = | "bold" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx index a5484155b4..85fee3a115 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx @@ -3,25 +3,25 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- import type { TextAlign, UseTextAlignConfig, -} from "@/components/tiptap-ui/text-align-button" +} from "./" import { TEXT_ALIGN_SHORTCUT_KEYS, useTextAlign, -} from "@/components/tiptap-ui/text-align-button" +} from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" type IconProps = React.SVGProps type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts index 94032f5e4d..46b5381aa8 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts @@ -3,16 +3,16 @@ import type { ChainedCommands } from "@tiptap/react" import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- -import { isExtensionAvailable, isNodeTypeSelected } from "@/lib/tiptap-utils" +import { isExtensionAvailable, isNodeTypeSelected } from "../../../lib/tiptap-utils" // --- Icons --- -import { AlignCenterIcon } from "@/components/tiptap-icons/align-center-icon" -import { AlignJustifyIcon } from "@/components/tiptap-icons/align-justify-icon" -import { AlignLeftIcon } from "@/components/tiptap-icons/align-left-icon" -import { AlignRightIcon } from "@/components/tiptap-icons/align-right-icon" +import { AlignCenterIcon } from "../../tiptap-icons/align-center-icon" +import { AlignJustifyIcon } from "../../tiptap-icons/align-justify-icon" +import { AlignLeftIcon } from "../../tiptap-icons/align-left-icon" +import { AlignRightIcon } from "../../tiptap-icons/align-right-icon" export type TextAlign = "left" | "center" | "right" | "justify" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx index cde6d1174a..b37222c7bd 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx @@ -3,25 +3,25 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" +import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- import type { UndoRedoAction, UseUndoRedoConfig, -} from "@/components/tiptap-ui/undo-redo-button" +} from "./" import { UNDO_REDO_SHORTCUT_KEYS, useUndoRedo, -} from "@/components/tiptap-ui/undo-redo-button" +} from "./" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "../../tiptap-ui-primitive/button" +import { Button } from "../../tiptap-ui-primitive/button" +import { Badge } from "../../tiptap-ui-primitive/badge" export interface UndoRedoButtonProps extends Omit, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts index 94e73e37a4..3add2c48e9 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/use-undo-redo.ts @@ -2,14 +2,14 @@ import { useCallback, useEffect, useState } from "react" import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- -import { isNodeTypeSelected } from "@/lib/tiptap-utils" +import { isNodeTypeSelected } from "../../../lib/tiptap-utils" // --- Icons --- -import { Redo2Icon } from "@/components/tiptap-icons/redo2-icon" -import { Undo2Icon } from "@/components/tiptap-icons/undo2-icon" +import { Redo2Icon } from "../../tiptap-icons/redo2-icon" +import { Undo2Icon } from "../../tiptap-icons/undo2-icon" export type UndoRedoAction = "undo" | "redo" diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts index 19668d8b2f..5146bea8b1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-cursor-visibility.ts @@ -1,6 +1,6 @@ import type { Editor } from "@tiptap/react" -import { useWindowSize } from "@/hooks/use-window-size" -import { useBodyRect } from "@/hooks/use-element-rect" +import { useWindowSize } from "./use-window-size" +import { useBodyRect } from "./use-element-rect" import { useEffect } from "react" export interface CursorVisibilityOptions { diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts index 8922436fbe..eacb84ac29 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-element-rect.ts @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useState } from "react" -import { useThrottledCallback } from "@/hooks/use-throttled-callback" +import { useThrottledCallback } from "./use-throttled-callback" export type RectState = Omit diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts index 18f4b11c7d..dadabd8fdf 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-throttled-callback.ts @@ -1,6 +1,6 @@ import throttle from "lodash.throttle" -import { useUnmount } from "@/hooks/use-unmount" +import { useUnmount } from "./use-unmount" import { useMemo } from "react" interface ThrottleSettings { diff --git a/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts b/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts index dd211d9aac..6df968b353 100644 --- a/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts +++ b/frontends/ol-components/src/components/TiptapEditor/hooks/use-window-size.ts @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { useThrottledCallback } from "@/hooks/use-throttled-callback" +import { useThrottledCallback } from "./use-throttled-callback" export interface WindowSizeState { /** diff --git a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json b/frontends/ol-components/src/components/TiptapEditor/tsconfig.json deleted file mode 100644 index dd2c8c9eb1..0000000000 --- a/frontends/ol-components/src/components/TiptapEditor/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/components/*": ["./components/*"], - "@/lib/*": ["./lib/*"], - "@/hooks/*": ["./hooks/*"] - } - }, - "include": ["./**/*.ts", "./**/*.tsx"] -} From f49a96eb9143ad76c69a8431fa8e1c81af977c7b Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:09:00 +0000 Subject: [PATCH 09/16] Prettier fixes --- .../components/tiptap-ui-primitive/button/button.tsx | 6 +----- .../tiptap-ui/blockquote-button/blockquote-button.tsx | 5 +---- .../tiptap-ui/code-block-button/code-block-button.tsx | 5 +---- .../color-highlight-button/color-highlight-button.tsx | 5 +---- .../color-highlight-popover.tsx | 6 +----- .../tiptap-ui/heading-button/heading-button.tsx | 10 ++-------- .../image-upload-button/image-upload-button.tsx | 5 +---- .../components/tiptap-ui/link-popover/link-popover.tsx | 6 +----- .../components/tiptap-ui/list-button/list-button.tsx | 5 +---- .../tiptap-ui/text-align-button/text-align-button.tsx | 10 ++-------- .../tiptap-ui/text-align-button/use-text-align.ts | 5 ++++- .../tiptap-ui/undo-redo-button/undo-redo-button.tsx | 10 ++-------- 12 files changed, 18 insertions(+), 60 deletions(-) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx index 3b60242bb3..60eb43e384 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx @@ -1,11 +1,7 @@ import { forwardRef, Fragment, useMemo } from "react" // --- Tiptap UI Primitive --- -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "../tooltip" +import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip" // --- Lib --- import { cn, parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx index d82a6577ba..577b448ffe 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx @@ -2,10 +2,7 @@ import { forwardRef, useCallback } from "react" // --- Tiptap UI --- import type { UseBlockquoteConfig } from "./" -import { - BLOCKQUOTE_SHORTCUT_KEY, - useBlockquote, -} from "./" +import { BLOCKQUOTE_SHORTCUT_KEY, useBlockquote } from "./" // --- Hooks --- import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx index 85c54fa527..5ffb4a15f7 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx @@ -8,10 +8,7 @@ import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Tiptap UI --- import type { UseCodeBlockConfig } from "./" -import { - CODE_BLOCK_SHORTCUT_KEY, - useCodeBlock, -} from "./" +import { CODE_BLOCK_SHORTCUT_KEY, useCodeBlock } from "./" // --- UI Primitives --- import type { ButtonProps } from "../../tiptap-ui-primitive/button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx index d9bc08e655..89dbbdfeb3 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx @@ -8,10 +8,7 @@ import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- import type { UseColorHighlightConfig } from "./" -import { - COLOR_HIGHLIGHT_SHORTCUT_KEY, - useColorHighlight, -} from "./" +import { COLOR_HIGHLIGHT_SHORTCUT_KEY, useColorHighlight } from "./" // --- UI Primitives --- import type { ButtonProps } from "../../tiptap-ui-primitive/button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx index 3cc1ab2360..1b5591de9c 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx @@ -19,11 +19,7 @@ import { PopoverContent, } from "../../tiptap-ui-primitive/popover" import { Separator } from "../../tiptap-ui-primitive/separator" -import { - Card, - CardBody, - CardItemGroup, -} from "../../tiptap-ui-primitive/card" +import { Card, CardBody, CardItemGroup } from "../../tiptap-ui-primitive/card" // --- Tiptap UI --- import type { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx index d4ad41171e..0d9fa0646d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx @@ -4,14 +4,8 @@ import { forwardRef, useCallback } from "react" import { parseShortcutKeys } from "../../../lib/tiptap-utils" // --- Tiptap UI --- -import type { - Level, - UseHeadingConfig, -} from "./" -import { - HEADING_SHORTCUT_KEYS, - useHeading, -} from "./" +import type { Level, UseHeadingConfig } from "./" +import { HEADING_SHORTCUT_KEYS, useHeading } from "./" // --- UI Primitives --- import type { ButtonProps } from "../../tiptap-ui-primitive/button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx index 4feab889a3..067b0e3bb2 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx @@ -8,10 +8,7 @@ import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- import type { UseImageUploadConfig } from "./" -import { - IMAGE_UPLOAD_SHORTCUT_KEY, - useImageUpload, -} from "./" +import { IMAGE_UPLOAD_SHORTCUT_KEY, useImageUpload } from "./" // --- UI Primitives --- import type { ButtonProps } from "../../tiptap-ui-primitive/button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx index 1fc3222239..21dfa74844 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx @@ -26,11 +26,7 @@ import { PopoverTrigger, } from "../../tiptap-ui-primitive/popover" import { Separator } from "../../tiptap-ui-primitive/separator" -import { - Card, - CardBody, - CardItemGroup, -} from "../../tiptap-ui-primitive/card" +import { Card, CardBody, CardItemGroup } from "../../tiptap-ui-primitive/card" import { Input, InputGroup } from "../../tiptap-ui-primitive/input" export interface LinkMainProps { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx index 3cdcb6077b..e1693e9b18 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx @@ -12,10 +12,7 @@ import { Button } from "../../tiptap-ui-primitive/button" import { Badge } from "../../tiptap-ui-primitive/badge" // --- Tiptap UI --- -import type { - ListType, - UseListConfig, -} from "./" +import type { ListType, UseListConfig } from "./" import { LIST_SHORTCUT_KEYS, useList } from "./" export interface ListButtonProps diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx index 85fee3a115..7b83ba788e 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx @@ -9,14 +9,8 @@ import { parseShortcutKeys } from "../../../lib/tiptap-utils" import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- -import type { - TextAlign, - UseTextAlignConfig, -} from "./" -import { - TEXT_ALIGN_SHORTCUT_KEYS, - useTextAlign, -} from "./" +import type { TextAlign, UseTextAlignConfig } from "./" +import { TEXT_ALIGN_SHORTCUT_KEYS, useTextAlign } from "./" // --- UI Primitives --- import type { ButtonProps } from "../../tiptap-ui-primitive/button" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts index 46b5381aa8..d52c948404 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/use-text-align.ts @@ -6,7 +6,10 @@ import { type Editor } from "@tiptap/react" import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Lib --- -import { isExtensionAvailable, isNodeTypeSelected } from "../../../lib/tiptap-utils" +import { + isExtensionAvailable, + isNodeTypeSelected, +} from "../../../lib/tiptap-utils" // --- Icons --- import { AlignCenterIcon } from "../../tiptap-icons/align-center-icon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx index b37222c7bd..94aa2d1d08 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx @@ -9,14 +9,8 @@ import { parseShortcutKeys } from "../../../lib/tiptap-utils" import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" // --- Tiptap UI --- -import type { - UndoRedoAction, - UseUndoRedoConfig, -} from "./" -import { - UNDO_REDO_SHORTCUT_KEYS, - useUndoRedo, -} from "./" +import type { UndoRedoAction, UseUndoRedoConfig } from "./" +import { UNDO_REDO_SHORTCUT_KEYS, useUndoRedo } from "./" // --- UI Primitives --- import type { ButtonProps } from "../../tiptap-ui-primitive/button" From 0f941105273f433b6041538db16cf97d88eae94b Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:14:03 +0000 Subject: [PATCH 10/16] CodeQL fix --- .../components/TiptapEditor/lib/tiptap-utils.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts index 846c4f45c5..2c0f1bb813 100644 --- a/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts +++ b/frontends/ol-components/src/components/TiptapEditor/lib/tiptap-utils.ts @@ -435,13 +435,14 @@ export function isAllowedUri( return ( !uri || - uri.replace(ATTR_WHITESPACE, "").match( - new RegExp( - // eslint-disable-next-line no-useless-escape - `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))`, - "i", - ), - ) + uri + .replace(ATTR_WHITESPACE, "") + .match( + new RegExp( + `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.:-]|$))`, + "i", + ), + ) ) } From 638411aa6bc48ca3790b415c0e15e1be86f47bf2 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:31:45 +0000 Subject: [PATCH 11/16] React in scope fixes --- .../TiptapEditor/components/tiptap-icons/align-center-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/align-justify-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/align-left-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/align-right-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/ban-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/blockquote-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/bold-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/close-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/code-block-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/code2-icon.tsx | 2 +- .../components/tiptap-icons/corner-down-left-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/external-link-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-five-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-four-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-one-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-six-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-three-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/heading-two-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/highlighter-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/image-plus-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/italic-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/link-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/list-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/list-todo-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/moon-star-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/redo2-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/strike-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/subscript-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/sun-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/superscript-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/trash-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/underline-icon.tsx | 2 +- .../TiptapEditor/components/tiptap-icons/undo2-icon.tsx | 2 +- .../components/tiptap-templates/simple/theme-toggle.tsx | 2 +- .../TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx | 2 +- .../components/tiptap-ui-primitive/button/button.tsx | 2 +- .../TiptapEditor/components/tiptap-ui-primitive/card/card.tsx | 2 +- .../tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx | 2 +- .../TiptapEditor/components/tiptap-ui-primitive/input/input.tsx | 1 + .../components/tiptap-ui-primitive/popover/popover.tsx | 1 + .../components/tiptap-ui-primitive/separator/separator.tsx | 2 +- .../components/tiptap-ui-primitive/spacer/spacer.tsx | 2 ++ .../components/tiptap-ui-primitive/toolbar/toolbar.tsx | 2 +- .../components/tiptap-ui-primitive/tooltip/tooltip.tsx | 2 +- .../tiptap-ui/blockquote-button/blockquote-button.tsx | 2 +- .../tiptap-ui/code-block-button/code-block-button.tsx | 2 +- .../tiptap-ui/color-highlight-button/color-highlight-button.tsx | 2 +- .../color-highlight-popover/color-highlight-popover.tsx | 2 +- .../components/tiptap-ui/heading-button/heading-button.tsx | 2 +- .../tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx | 2 +- .../tiptap-ui/image-upload-button/image-upload-button.tsx | 2 +- .../components/tiptap-ui/link-popover/link-popover.tsx | 2 +- .../components/tiptap-ui/list-button/list-button.tsx | 2 +- .../tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx | 2 +- .../components/tiptap-ui/mark-button/mark-button.tsx | 2 +- .../tiptap-ui/text-align-button/text-align-button.tsx | 2 +- .../components/tiptap-ui/undo-redo-button/undo-redo-button.tsx | 2 +- 61 files changed, 62 insertions(+), 58 deletions(-) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx index bb720609ff..2b7d2931e8 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-center-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx index 61cc6def89..d41f74582d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-justify-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx index 2972bdba54..07cd9c88f9 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-left-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx index c93fc0599a..354d71aea8 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/align-right-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx index 7cf04d28cd..d1325ee4e0 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/arrow-left-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx index 1995de03f6..fb4e737d73 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/ban-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx index 50e665f8e0..9ae07f20c1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/blockquote-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx index a61cca4e8e..f049c5f00b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/bold-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx index 8f7844d937..883721ce9e 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/chevron-down-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx index 8b506a9356..a5a273e47f 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/close-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx index 9d42238262..7ef6a7d5d7 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code-block-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx index e8d70d842d..f535bf84bb 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/code2-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx index 0dea6393c1..e1bd49dc4c 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/corner-down-left-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx index a4afe67e07..8aee572d59 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/external-link-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx index e1450cfce8..f9a19927f4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-five-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx index 3f35a8b4af..1f34db6967 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-four-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx index ba9aa15138..8ee871acd7 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx index d35093d521..1c3d2830e1 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-one-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx index 9cce22b849..3d72d47f85 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-six-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx index f2448366ff..ac5083bb76 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-three-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx index 24417acbd2..0a98dab228 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/heading-two-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx index b6374feaff..46e77a8c42 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/highlighter-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx index b1fd8c2b09..774e34685d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/image-plus-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx index 7c69b63317..ee95b1fa40 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/italic-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx index 27e1574363..8f2b5d6546 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/link-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx index a3183d212b..9952404f0b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx index 119da7b8a0..6d53e8bef4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-ordered-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx index 9b899c189f..855ba29195 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/list-todo-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx index e2d4422825..722512b84d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/moon-star-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx index 0302a16878..7300743a3b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/redo2-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx index df4b8c8100..2df2e778a3 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/strike-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx index 671030dfef..1b051a9ad4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/subscript-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx index 607fd650fe..b2def196f4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/sun-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx index 6af567b2da..57d0cc5893 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/superscript-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx index ac470bbe24..85e64a5135 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/trash-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx index 9765a3bd57..387abd338a 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/underline-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx index bfb643f3cd..135b57c757 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-icons/undo2-icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import React, { memo } from "react" type SvgProps = React.ComponentPropsWithoutRef<"svg"> diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx index cd62b8886b..473e37ec40 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-templates/simple/theme-toggle.tsx @@ -3,7 +3,7 @@ import { Button } from "../../tiptap-ui-primitive/button" // --- Icons --- import { MoonStarIcon } from "../../tiptap-icons/moon-star-icon" import { SunIcon } from "../../tiptap-icons/sun-icon" -import { useEffect, useState } from "react" +import React, { useEffect, useState } from "react" export function ThemeToggle() { const [isDarkMode, setIsDarkMode] = useState(false) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx index 85561d29e1..13b7ba1f6f 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/badge/badge.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react" +import React, { forwardRef } from "react" import "./badge-colors.scss" import "./badge-group.scss" import "./badge.scss" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx index 60eb43e384..0cb6506c2b 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/button/button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, Fragment, useMemo } from "react" +import React, { forwardRef, Fragment, useMemo } from "react" // --- Tiptap UI Primitive --- import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx index 71e11cedbb..75c53b3391 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/card/card.tsx @@ -1,6 +1,6 @@ "use client" -import { forwardRef } from "react" +import React, { forwardRef } from "react" import { cn } from "../../../lib/tiptap-utils" import "./card.scss" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx index cb236b2ff0..44aee418fd 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react" +import React, { forwardRef } from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { cn } from "../../../lib/tiptap-utils" import "./dropdown-menu.scss" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx index 87eaa99e1b..4ed77cede6 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/input/input.tsx @@ -1,3 +1,4 @@ +import React from "react" import { cn } from "../../../lib/tiptap-utils" import "./input.scss" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx index c28ab99510..8fc8a2f602 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/popover/popover.tsx @@ -1,3 +1,4 @@ +import React from "react" import * as PopoverPrimitive from "@radix-ui/react-popover" import { cn } from "../../../lib/tiptap-utils" import "./popover.scss" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx index f91a1043e8..090a41f32e 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/separator/separator.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react" +import React, { forwardRef } from "react" import "./separator.scss" import { cn } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx index 95e379c21d..bc7eae7105 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/spacer/spacer.tsx @@ -1,5 +1,7 @@ "use client" +import React from "react" + export type SpacerOrientation = "horizontal" | "vertical" export interface SpacerProps extends React.HTMLAttributes { diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx index edef91c57f..5817c6bd53 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback, useEffect, useRef, useState } from "react" +import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react" import { Separator } from "../separator" import "./toolbar.scss" import { cn } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx index bfc6dddfc1..d3fdea69d6 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/tooltip/tooltip.tsx @@ -1,6 +1,6 @@ "use client" -import { +import React, { cloneElement, createContext, forwardRef, diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx index 577b448ffe..1a1812e350 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/blockquote-button/blockquote-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Tiptap UI --- import type { UseBlockquoteConfig } from "./" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx index 5ffb4a15f7..fb90f647be 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/code-block-button/code-block-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Hooks --- import { useTiptapEditor } from "../../../hooks/use-tiptap-editor" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx index 89dbbdfeb3..b064d97bb8 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback, useMemo } from "react" +import React, { forwardRef, useCallback, useMemo } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx index 1b5591de9c..fd2fe7c790 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useMemo, useRef, useState } from "react" +import React, { forwardRef, useMemo, useRef, useState } from "react" import { type Editor } from "@tiptap/react" // --- Hooks --- diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx index 0d9fa0646d..7dedde4d25 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-button/heading-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx index 21a3ff9514..22a9d099a4 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback, useState } from "react" +import React, { forwardRef, useCallback, useState } from "react" // --- Icons --- import { ChevronDownIcon } from "../../tiptap-icons/chevron-down-icon" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx index 067b0e3bb2..f349d1865d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/image-upload-button/image-upload-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx index 21dfa74844..e1d6696177 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/link-popover/link-popover.tsx @@ -1,6 +1,6 @@ "use client" -import { forwardRef, useCallback, useEffect, useState } from "react" +import React, { forwardRef, useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" // --- Hooks --- diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx index e1693e9b18..225e22225d 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-button/list-button.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx index 5a9b947377..d143b9929f 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react" +import React, { useCallback, useState } from "react" import { type Editor } from "@tiptap/react" // --- Hooks --- diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx index 0aabe80075..b6bb47073c 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/mark-button/mark-button.tsx @@ -1,6 +1,6 @@ "use client" -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx index 7b83ba788e..4e6c0db077 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/text-align-button/text-align-button.tsx @@ -1,6 +1,6 @@ "use client" -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx index 94aa2d1d08..5743e02410 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx @@ -1,6 +1,6 @@ "use client" -import { forwardRef, useCallback } from "react" +import React, { forwardRef, useCallback } from "react" // --- Lib --- import { parseShortcutKeys } from "../../../lib/tiptap-utils" From 8ca8b24fe8a9902f4c7d2930c6a041c7d78c7941 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:36:14 +0000 Subject: [PATCH 12/16] Format fix --- .../components/tiptap-ui-primitive/toolbar/toolbar.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx index 5817c6bd53..2dc9f3a7b7 100644 --- a/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx +++ b/frontends/ol-components/src/components/TiptapEditor/components/tiptap-ui-primitive/toolbar/toolbar.tsx @@ -1,4 +1,10 @@ -import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react" +import React, { + forwardRef, + useCallback, + useEffect, + useRef, + useState, +} from "react" import { Separator } from "../separator" import "./toolbar.scss" import { cn } from "../../../lib/tiptap-utils" From ae1b0f5110040296fac32ccf86f83d8987f854f8 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:46:18 +0000 Subject: [PATCH 13/16] Transform react-hotkeys-hook to resolve esm error --- frontends/main/jest.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontends/main/jest.config.ts b/frontends/main/jest.config.ts index b523048ae7..0c1424ee75 100644 --- a/frontends/main/jest.config.ts +++ b/frontends/main/jest.config.ts @@ -7,7 +7,9 @@ const config: Config.InitialOptions = { ...baseConfig.setupFilesAfterEnv, "./test-utils/setupJest.tsx", ], - transformIgnorePatterns: ["node_modules/(?!@faker-js).+"], + transformIgnorePatterns: [ + "node_modules/(?!(@faker-js|react-hotkeys-hook)).+", + ], moduleNameMapper: { ...baseConfig.moduleNameMapper, "^@/(.*)$": path.resolve(__dirname, "src/$1"), From 7783bacadb0e13d0a2f07fe04163ee826643c071 Mon Sep 17 00:00:00 2001 From: Ahtesham Quraish Date: Tue, 11 Nov 2025 17:37:06 +0500 Subject: [PATCH 14/16] feat: adding tiptap integration with article crud operation --- .../app-pages/Articles/ArticleDetailPage.tsx | 18 +++---- .../Articles/ArticleEditPage.test.tsx | 27 ++++++++++ .../app-pages/Articles/ArticleEditPage.tsx | 37 +++++++------- .../src/app-pages/Articles/ArticleNewPage.tsx | 31 +++++------- .../Articles/NewArticlePage.test.tsx | 27 ++++++++++ .../components/TiptapEditor/TiptapEditor.tsx | 49 +++++++++++++------ .../simple/simple-editor.scss | 2 - 7 files changed, 127 insertions(+), 64 deletions(-) diff --git a/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx b/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx index e8f73d53d8..d6273c68e6 100644 --- a/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx +++ b/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx @@ -2,7 +2,13 @@ import React from "react" import { useArticleDetail } from "api/hooks/articles" -import { Container, LoadingSpinner, styled, Typography } from "ol-components" +import { + Container, + LoadingSpinner, + styled, + Typography, + TiptapEditor, +} from "ol-components" import { ButtonLink } from "@mitodl/smoot-design" import { notFound } from "next/navigation" import { Permission } from "api/hooks/user" @@ -24,14 +30,6 @@ const WrapperContainer = styled.div({ paddingBottom: "10px", }) -const PreTag = styled.pre({ - background: "#f6f6f6", - padding: "16px", - borderRadius: "8px", - fontSize: "14px", - overflowX: "auto", -}) - export const ArticleDetailPage = ({ articleId }: { articleId: number }) => { const id = Number(articleId) const { data, isLoading } = useArticleDetail(id) @@ -58,7 +56,7 @@ export const ArticleDetailPage = ({ articleId }: { articleId: number }) => { - {JSON.stringify(data.content, null, 2)} + ) diff --git a/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx b/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx index 2f1bf9b013..a289ecfb16 100644 --- a/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx +++ b/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx @@ -13,6 +13,33 @@ jest.mock("next-nprogress-bar", () => ({ }), })) +jest.mock("ol-components", () => { + // Reuse other exports from ol-components if needed + const actual = jest.requireActual("ol-components") + return { + ...actual, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TiptapEditor: ({ value, onChange, "data-testid": testId }: any) => { + return ( +