Просмотр исходного кода

feat(@vben/plugins): add tiptap rich text editor

xingyu4j 2 месяцев назад
Родитель
Сommit
bb78882f72

+ 23 - 0
packages/@core/base/icons/src/lucide.ts

@@ -1,4 +1,7 @@
 export {
+  TextAlignCenter as AlignCenter,
+  TextAlignStart as AlignLeft,
+  TextAlignEnd as AlignRight,
   ArrowDown,
   ArrowLeft,
   ArrowLeftToLine,
@@ -7,6 +10,7 @@ export {
   ArrowUp,
   ArrowUpToLine,
   Bell,
+  Bold,
   BookOpenText,
   Check,
   ChevronDown,
@@ -22,6 +26,7 @@ export {
   Copy,
   CornerDownLeft,
   Ellipsis,
+  Eraser,
   Expand,
   ExternalLink,
   Eye,
@@ -32,12 +37,20 @@ export {
   Grid,
   Grip,
   GripVertical,
+  Heading1,
+  Heading2,
+  Highlighter,
   Menu as IconDefault,
+  ImagePlus,
   Inbox,
   Info,
   InspectionPanel,
+  Italic,
   Languages,
   LayoutGrid,
+  Link2,
+  List,
+  ListOrdered,
   LoaderCircle,
   LockKeyhole,
   LogOut,
@@ -46,15 +59,19 @@ export {
   ArrowRightFromLine as MdiMenuClose,
   ArrowLeftFromLine as MdiMenuOpen,
   Menu,
+  MessageSquareCode,
   Minimize,
   Minimize2,
   MoonStar,
+  Paintbrush,
   Palette,
   PanelLeft,
   PanelRight,
   Pin,
   PinOff,
   Plus,
+  Redo2,
+  RemoveFormatting,
   RotateCw,
   Search,
   SearchX,
@@ -62,10 +79,16 @@ export {
   Shrink,
   Square,
   SquareCheckBig,
+  SquareCode,
   SquareMinus,
+  Strikethrough,
   Sun,
   SunMoon,
   SwatchBook,
+  TextQuote,
+  Underline,
+  Undo2,
+  Unlink2,
   UserRoundPen,
   X,
 } from 'lucide-vue-next';

+ 16 - 0
packages/effects/plugins/package.json

@@ -22,6 +22,10 @@
       "types": "./src/echarts/index.ts",
       "default": "./src/echarts/index.ts"
     },
+    "./tiptap": {
+      "types": "./src/tiptap/index.ts",
+      "default": "./src/tiptap/index.ts"
+    },
     "./vxe-table": {
       "types": "./src/vxe-table/index.ts",
       "default": "./src/vxe-table/index.ts"
@@ -32,8 +36,20 @@
     }
   },
   "dependencies": {
+    "@tiptap/core": "catalog:",
+    "@tiptap/extension-doc": "catalog:",
+    "@tiptap/extension-highlight": "catalog:",
+    "@tiptap/extension-image": "catalog:",
+    "@tiptap/extension-link": "catalog:",
+    "@tiptap/extension-placeholder": "catalog:",
+    "@tiptap/extension-text-align": "catalog:",
+    "@tiptap/extension-text-style": "catalog:",
+    "@tiptap/extension-underline": "catalog:",
+    "@tiptap/starter-kit": "catalog:",
+    "@tiptap/vue-3": "catalog:",
     "@vben-core/design": "workspace:*",
     "@vben-core/form-ui": "workspace:*",
+    "@vben-core/popup-ui": "workspace:*",
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben-core/shared": "workspace:*",
     "@vben/hooks": "workspace:*",

+ 55 - 0
packages/effects/plugins/src/tiptap/extensions.ts

@@ -0,0 +1,55 @@
+import type { Extensions } from '@tiptap/vue-3';
+
+import type { VbenTiptapExtensionOptions } from './types';
+
+import { $t } from '@vben/locales';
+
+import Document from '@tiptap/extension-doc';
+import Highlight from '@tiptap/extension-highlight';
+import Image from '@tiptap/extension-image';
+import Link from '@tiptap/extension-link';
+import Placeholder from '@tiptap/extension-placeholder';
+import TextAlign from '@tiptap/extension-text-align';
+import { Color, TextStyle } from '@tiptap/extension-text-style';
+import Underline from '@tiptap/extension-underline';
+import StarterKit from '@tiptap/starter-kit';
+
+export function createDefaultTiptapExtensions(
+  options: VbenTiptapExtensionOptions = {},
+): Extensions {
+  return [
+    Document,
+    StarterKit.configure({
+      heading: {
+        levels: [1, 2, 3, 4],
+      },
+    }),
+    Underline,
+    TextAlign.configure({
+      types: ['heading', 'paragraph'],
+    }),
+    TextStyle,
+    Color.configure({
+      types: ['textStyle'],
+    }),
+    Highlight.configure({
+      multicolor: true,
+    }),
+    Link.configure({
+      autolink: true,
+      defaultProtocol: 'https',
+      enableClickSelection: true,
+      openOnClick: false,
+      protocols: ['mailto', { optionalSlashes: true, scheme: 'tel' }],
+    }),
+    Image.configure({
+      allowBase64: true,
+      HTMLAttributes: {
+        class: 'vben-tiptap__image',
+      },
+    }),
+    Placeholder.configure({
+      placeholder: options.placeholder ?? $t('ui.tiptap.placeholder'),
+    }),
+  ];
+}

+ 4 - 0
packages/effects/plugins/src/tiptap/index.ts

@@ -0,0 +1,4 @@
+export { default as VbenTiptapPreview } from './preview.vue';
+export { default as VbenTiptap } from './tiptap.vue';
+
+export * from './types';

+ 33 - 0
packages/effects/plugins/src/tiptap/preview.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import type { TipTapPreviewProps } from './types';
+
+import { computed } from 'vue';
+
+import { cn } from '@vben-core/shared/utils';
+
+import './style.css';
+const props = withDefaults(defineProps<TipTapPreviewProps>(), {
+  content: '',
+  minHeight: 160,
+});
+const contentMinHeight = computed(() =>
+  typeof props.minHeight === 'number'
+    ? `${props.minHeight}px`
+    : props.minHeight,
+);
+const previewClass = computed(() =>
+  cn(
+    'vben-tiptap-content bg-transparent p-0 leading-7 text-foreground',
+    props.class,
+  ),
+);
+</script>
+
+<template>
+  <!-- eslint-disable vue/no-v-html -->
+  <div
+    :class="previewClass"
+    :style="{ minHeight: contentMinHeight }"
+    v-html="content"
+  ></div>
+</template>

+ 56 - 0
packages/effects/plugins/src/tiptap/style.css

@@ -0,0 +1,56 @@
+@reference "@vben/tailwind-config/theme";
+
+.vben-tiptap-content > * + * {
+  @apply mt-3;
+}
+
+.vben-tiptap-content h1 {
+  @apply text-2xl font-bold leading-[1.4];
+}
+
+.vben-tiptap-content h2 {
+  @apply text-xl font-bold leading-[1.45];
+}
+
+.vben-tiptap-content h3 {
+  @apply text-lg font-semibold leading-[1.5];
+}
+
+.vben-tiptap-content h4 {
+  @apply text-base font-semibold leading-[1.55];
+}
+
+.vben-tiptap-content ul {
+  @apply list-disc pl-6;
+}
+
+.vben-tiptap-content ol {
+  @apply list-decimal pl-6;
+}
+
+.vben-tiptap-content blockquote {
+  @apply border-l-4 border-primary pl-4 text-muted-foreground;
+}
+
+.vben-tiptap-content a {
+  @apply text-primary underline decoration-1 underline-offset-[3px];
+}
+
+.vben-tiptap-content code {
+  @apply rounded-[0.45rem] border border-border bg-secondary px-[0.35rem] py-[0.15rem] text-[0.9em] text-primary;
+}
+
+.vben-tiptap-content pre {
+  @apply overflow-x-auto rounded-[0.9rem] border border-border bg-popover p-4 text-popover-foreground;
+}
+
+.vben-tiptap-content pre code {
+  @apply border-none bg-transparent p-0 text-inherit;
+}
+
+.vben-tiptap-content img,
+.vben-tiptap-content .vben-tiptap__image {
+  @apply my-4 block h-auto rounded-2xl border border-border;
+
+  max-width: min(100%, 640px);
+}

+ 285 - 0
packages/effects/plugins/src/tiptap/tiptap.vue

@@ -0,0 +1,285 @@
+<script setup lang="ts">
+import type {
+  TipTapProps,
+  ToolbarAction,
+  VbenTiptapChangeEvent,
+} from './types';
+
+import { computed, onBeforeUnmount, watch } from 'vue';
+
+import { Check, ChevronDown, Eye } from '@vben/icons';
+import { $t } from '@vben/locales';
+
+import { useVbenModal } from '@vben-core/popup-ui';
+import { VbenIconButton, VbenPopover } from '@vben-core/shadcn-ui';
+import { cn } from '@vben-core/shared/utils';
+
+import { EditorContent, useEditor } from '@tiptap/vue-3';
+
+import { createDefaultTiptapExtensions } from './extensions';
+import Preview from './preview.vue';
+import { createToolbarGroups } from './toolbar';
+import { useTiptapToolbar } from './use-tiptap-toolbar';
+
+import './style.css';
+const props = withDefaults(defineProps<TipTapProps>(), {
+  editable: true,
+  extensions: undefined,
+  minHeight: 240,
+  placeholder: $t('ui.tiptap.placeholder'),
+  previewable: true,
+  toolbar: true,
+});
+const emit = defineEmits<{
+  change: [payload: VbenTiptapChangeEvent];
+}>();
+const modelValue = defineModel<string>({ default: '' });
+const contentMinHeight = computed(() =>
+  typeof props.minHeight === 'number'
+    ? `${props.minHeight}px`
+    : props.minHeight,
+);
+const tiptapContentClass = cn(
+  'vben-tiptap-content vben-tiptap__content',
+  'min-h-(--vben-tiptap-min-height) leading-7 text-foreground outline-none',
+);
+const editor = useEditor({
+  content: modelValue.value,
+  editable: props.editable,
+  editorProps: {
+    attributes: {
+      class: tiptapContentClass,
+    },
+  },
+  extensions:
+    props.extensions ??
+    createDefaultTiptapExtensions({
+      placeholder: props.placeholder,
+    }),
+  onUpdate: ({ editor }) => {
+    const html = editor.getHTML();
+    if (html !== modelValue.value) {
+      modelValue.value = html;
+    }
+    emit('change', {
+      html,
+      json: editor.getJSON(),
+      text: editor.getText(),
+    });
+  },
+});
+const toolbarGroups = computed<ToolbarAction[][]>(() => {
+  return createToolbarGroups();
+});
+const previewContent = computed(
+  () => editor.value?.getHTML() ?? modelValue.value,
+);
+const [PreviewModal, previewModalApi] = useVbenModal({
+  footer: false,
+  fullscreenButton: false,
+});
+const {
+  applyPaletteColor,
+  canRunAction,
+  canRunMenuItem,
+  clearPaletteColor,
+  getActionIndicatorColor,
+  getMenuItemClass,
+  getPaletteCurrentColor,
+  getPaletteSwatchClass,
+  getToolbarButtonClass,
+  isMenuItemActive,
+  runAction,
+  runMenuItem,
+} = useTiptapToolbar({
+  editable: () => props.editable,
+  editor,
+});
+function openPreviewModal() {
+  previewModalApi.open();
+}
+watch(
+  () => props.editable,
+  (editable) => {
+    editor.value?.setEditable(editable);
+  },
+);
+watch(
+  () => modelValue.value,
+  (nextValue = '') => {
+    if (!editor.value) {
+      return;
+    }
+    const currentValue = editor.value.getHTML();
+    if (nextValue === currentValue) {
+      return;
+    }
+    editor.value.commands.setContent(nextValue, {
+      emitUpdate: false,
+    });
+  },
+);
+onBeforeUnmount(() => {
+  editor.value?.destroy();
+});
+</script>
+
+<template>
+  <div
+    :style="{ '--vben-tiptap-min-height': contentMinHeight }"
+    class="vben-tiptap overflow-hidden rounded-xl border border-border bg-card"
+  >
+    <div
+      v-if="toolbar"
+      class="sticky top-0 z-10 flex flex-wrap items-center gap-2 border-b border-border p-2 backdrop-blur-[14px]"
+    >
+      <div
+        v-for="(group, groupIndex) in toolbarGroups"
+        :key="groupIndex"
+        class="flex items-center gap-1"
+      >
+        <template v-for="action in group" :key="action.label">
+          <VbenPopover
+            v-if="action.menu || action.palette"
+            :content-props="{ align: 'start', side: 'bottom', sideOffset: 8 }"
+            content-class="w-auto p-2"
+          >
+            <template #trigger>
+              <VbenIconButton
+                :aria-label="action.label"
+                :class="getToolbarButtonClass(action)"
+                :disabled="!canRunAction(action)"
+                :tooltip="action.label"
+                tooltip-side="top"
+                variant="ghost"
+              >
+                <template v-if="action.triggerText">
+                  <span class="text-xs font-semibold tracking-wide">
+                    {{
+                      typeof action.triggerText === 'function'
+                        ? action.triggerText(editor)
+                        : action.triggerText
+                    }}
+                  </span>
+                  <ChevronDown class="size-4 opacity-70" />
+                </template>
+                <component
+                  v-else-if="action.icon"
+                  :is="action.icon"
+                  class="size-4"
+                />
+                <span
+                  v-if="getActionIndicatorColor(action)"
+                  :style="{ backgroundColor: getActionIndicatorColor(action) }"
+                  class="absolute bottom-1 left-1/2 h-1 w-4 -translate-x-1/2 rounded-full shadow-[0_0_0_1px_hsl(var(--card)/0.7)]"
+                ></span>
+              </VbenIconButton>
+            </template>
+            <div
+              v-if="action.palette"
+              class="flex max-w-52 flex-wrap items-center gap-2"
+            >
+              <button
+                v-for="color in action.palette.colors"
+                :key="color"
+                :aria-label="`${action.label}-${color}`"
+                :class="getPaletteSwatchClass(action, color)"
+                :style="{ backgroundColor: color }"
+                type="button"
+                @click="applyPaletteColor(action, color)"
+              >
+                <Check
+                  v-if="getPaletteCurrentColor(action) === color"
+                  class="size-4 text-white drop-shadow-sm"
+                />
+              </button>
+              <button
+                v-if="action.palette.clear"
+                class="h-8 w-full rounded-xl border border-border bg-secondary text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
+                type="button"
+                @click="clearPaletteColor(action)"
+              >
+                {{ $t('ui.tiptap.toolbar.clear') }}
+              </button>
+            </div>
+            <div v-else-if="action.menu" class="flex min-w-32 flex-col gap-1">
+              <button
+                v-for="item in action.menu.items"
+                :key="item.shortLabel"
+                :class="getMenuItemClass(item)"
+                :disabled="!canRunMenuItem(item)"
+                type="button"
+                @click="runMenuItem(item)"
+              >
+                <span class="w-7 text-xs font-semibold tracking-wide">
+                  {{ item.shortLabel }}
+                </span>
+                <span class="flex-1">{{ item.label }}</span>
+                <Check
+                  v-if="isMenuItemActive(item)"
+                  class="size-4 text-primary"
+                />
+              </button>
+            </div>
+          </VbenPopover>
+          <VbenIconButton
+            v-else
+            :aria-label="action.label"
+            :class="getToolbarButtonClass(action)"
+            :disabled="!canRunAction(action)"
+            :tooltip="action.label"
+            tooltip-side="top"
+            @click="runAction(action)"
+          >
+            <component :is="action.icon" class="size-4" />
+            <span
+              v-if="getActionIndicatorColor(action)"
+              :style="{ backgroundColor: getActionIndicatorColor(action) }"
+              class="absolute bottom-1 left-1/2 h-1 w-4 -translate-x-1/2 rounded-full shadow-[0_0_0_1px_hsl(var(--card)/0.7)]"
+            ></span>
+          </VbenIconButton>
+        </template>
+        <div
+          v-if="groupIndex < toolbarGroups.length - 1"
+          class="ml-1 h-5 w-px bg-border"
+        ></div>
+      </div>
+      <div v-if="previewable" class="ml-auto flex items-center">
+        <VbenIconButton
+          :aria-label="$t('ui.tiptap.toolbar.preview')"
+          :class="
+            getToolbarButtonClass({
+              action: () => {},
+              label: $t('ui.tiptap.toolbar.preview'),
+            })
+          "
+          :tooltip="$t('ui.tiptap.toolbar.preview')"
+          tooltip-side="top"
+          variant="ghost"
+          @click="openPreviewModal"
+        >
+          <Eye class="size-4" />
+        </VbenIconButton>
+      </div>
+    </div>
+    <EditorContent v-if="editor" :editor="editor" class="p-4" />
+    <PreviewModal
+      v-if="previewable"
+      :title="$t('ui.tiptap.toolbar.preview')"
+      class="w-4/5"
+    >
+      <Preview :content="previewContent" :min-height="320" />
+    </PreviewModal>
+  </div>
+</template>
+
+<style scoped>
+.vben-tiptap
+  :deep(.vben-tiptap__content p.is-editor-empty:first-child::before) {
+  float: left;
+  height: 0;
+  color: hsl(var(--input-placeholder));
+  pointer-events: none;
+  content: attr(data-placeholder);
+}
+</style>

+ 345 - 0
packages/effects/plugins/src/tiptap/toolbar.ts

@@ -0,0 +1,345 @@
+import type { Editor } from '@tiptap/vue-3';
+
+import type { ToolbarAction, ToolbarMenuItem } from './types';
+
+import {
+  AlignCenter,
+  AlignLeft,
+  AlignRight,
+  Bold,
+  Highlighter,
+  ImagePlus,
+  Italic,
+  Link2,
+  List,
+  ListOrdered,
+  MessageSquareCode,
+  Paintbrush,
+  Redo2,
+  RemoveFormatting,
+  SquareCode,
+  Strikethrough,
+  TextQuote,
+  Underline,
+  Undo2,
+  Unlink2,
+} from '@vben/icons';
+import { $t } from '@vben/locales';
+import { COLOR_PRESETS } from '@vben/preferences';
+
+import { prompt } from '@vben-core/popup-ui';
+
+const headingLevels = [1, 2, 3, 4] as const;
+const editorColorPresets = [
+  'hsl(var(--foreground))',
+  'hsl(var(--warning))',
+  'hsl(var(--success))',
+  'hsl(var(--destructive))',
+  ...COLOR_PRESETS.map((item) => item.color),
+];
+const editorHighlightPresets = [
+  withAlpha('hsl(var(--warning))', 0.45),
+  withAlpha('hsl(var(--success))', 0.35),
+  withAlpha('hsl(var(--primary))', 0.3),
+  withAlpha('hsl(var(--destructive))', 0.3),
+  ...COLOR_PRESETS.map((item) => withAlpha(item.color, 0.4)),
+];
+
+function createHeadingMenuItems(): ToolbarMenuItem[] {
+  return [
+    {
+      action: (editor) => editor.chain().focus().setParagraph().run(),
+      can: (editor) => editor.can().chain().focus().setParagraph().run(),
+      isActive: (editor) => editor.isActive('paragraph'),
+      label: $t('ui.tiptap.toolbar.paragraph'),
+      shortLabel: 'P',
+    },
+    ...headingLevels.map((level) => ({
+      action: (editor: Editor) =>
+        editor.chain().focus().toggleHeading({ level }).run(),
+      can: (editor: Editor) =>
+        editor.can().chain().focus().toggleHeading({ level }).run(),
+      isActive: (editor: Editor) => editor.isActive('heading', { level }),
+      label: $t(`ui.tiptap.toolbar.heading${level}`),
+      shortLabel: `H${level}`,
+    })),
+  ];
+}
+
+function getHeadingTriggerText(editor?: Editor) {
+  if (editor?.isActive('paragraph')) {
+    return 'P';
+  }
+
+  const level = headingLevels.find((headingLevel) =>
+    editor?.isActive('heading', { level: headingLevel }),
+  );
+
+  return level ? `H${level}` : 'H';
+}
+
+function normalizeLinkUrl(url: string) {
+  if (/^(https?:|mailto:|tel:)/i.test(url)) {
+    return url;
+  }
+
+  return `https://${url}`;
+}
+
+function withAlpha(color: string, alpha: number) {
+  const normalizedAlpha = Math.min(Math.max(alpha, 0), 1);
+  const hslMatch = color.match(/^hsl\((.+)\)$/);
+
+  if (!hslMatch) {
+    return color;
+  }
+
+  return `hsl(${hslMatch[1]} / ${normalizedAlpha})`;
+}
+
+async function handleLinkAction(editor: Editor) {
+  const currentHref = editor.getAttributes('link').href as string | undefined;
+
+  let url: string | undefined;
+
+  try {
+    url = await prompt<string>({
+      componentProps: {
+        placeholder: 'https://example.com',
+      },
+      content: $t('ui.tiptap.prompts.link'),
+      defaultValue: currentHref ?? '',
+    });
+  } catch {
+    return;
+  }
+
+  const nextUrl = (url ?? '').trim();
+
+  if (!nextUrl) {
+    editor.chain().focus().extendMarkRange('link').unsetLink().run();
+    return;
+  }
+
+  editor
+    .chain()
+    .focus()
+    .extendMarkRange('link')
+    .setLink({
+      href: normalizeLinkUrl(nextUrl),
+    })
+    .run();
+}
+
+async function handleImageAction(editor: Editor) {
+  let url: string | undefined;
+
+  try {
+    url = await prompt<string>({
+      componentProps: {
+        placeholder: 'https://example.com/image.png',
+      },
+      content: $t('ui.tiptap.prompts.image'),
+      defaultValue: '',
+    });
+  } catch {
+    return;
+  }
+
+  const nextUrl = (url ?? '').trim();
+
+  if (!nextUrl) {
+    return;
+  }
+
+  editor.chain().focus().setImage({ src: nextUrl }).run();
+}
+
+export function createToolbarGroups(): ToolbarAction[][] {
+  const headingMenuItems = createHeadingMenuItems();
+
+  return [
+    [
+      {
+        action: (editor) => editor.chain().focus().undo().run(),
+        can: (editor) => editor.can().chain().focus().undo().run(),
+        icon: Undo2,
+        label: $t('ui.tiptap.toolbar.undo'),
+      },
+      {
+        action: (editor) => editor.chain().focus().redo().run(),
+        can: (editor) => editor.can().chain().focus().redo().run(),
+        icon: Redo2,
+        label: $t('ui.tiptap.toolbar.redo'),
+      },
+      {
+        action: (editor) =>
+          editor.chain().focus().clearNodes().unsetAllMarks().run(),
+        icon: RemoveFormatting,
+        label: $t('ui.tiptap.toolbar.clear'),
+      },
+    ],
+    [
+      {
+        action: (editor) => editor.chain().focus().toggleBold().run(),
+        active: { name: 'bold' },
+        can: (editor) => editor.can().chain().focus().toggleBold().run(),
+        icon: Bold,
+        label: $t('ui.tiptap.toolbar.bold'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleItalic().run(),
+        active: { name: 'italic' },
+        can: (editor) => editor.can().chain().focus().toggleItalic().run(),
+        icon: Italic,
+        label: $t('ui.tiptap.toolbar.italic'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleUnderline().run(),
+        active: { name: 'underline' },
+        can: (editor) => editor.can().chain().focus().toggleUnderline().run(),
+        icon: Underline,
+        label: $t('ui.tiptap.toolbar.underline'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleStrike().run(),
+        active: { name: 'strike' },
+        can: (editor) => editor.can().chain().focus().toggleStrike().run(),
+        icon: Strikethrough,
+        label: $t('ui.tiptap.toolbar.strike'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleCode().run(),
+        active: { name: 'code' },
+        can: (editor) => editor.can().chain().focus().toggleCode().run(),
+        icon: SquareCode,
+        label: $t('ui.tiptap.toolbar.code'),
+      },
+    ],
+    [
+      {
+        action: () => {},
+        can: (editor) =>
+          headingMenuItems.some((item) => (item.can ? item.can(editor) : true)),
+        isActive: (editor) =>
+          headingMenuItems.some((item) => item.isActive?.(editor)),
+        label: $t('ui.tiptap.toolbar.heading'),
+        menu: {
+          items: headingMenuItems,
+        },
+        triggerText: (editor) => getHeadingTriggerText(editor),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleBulletList().run(),
+        active: { name: 'bulletList' },
+        can: (editor) => editor.can().chain().focus().toggleBulletList().run(),
+        icon: List,
+        label: $t('ui.tiptap.toolbar.bulletList'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleOrderedList().run(),
+        active: { name: 'orderedList' },
+        can: (editor) => editor.can().chain().focus().toggleOrderedList().run(),
+        icon: ListOrdered,
+        label: $t('ui.tiptap.toolbar.orderedList'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleBlockquote().run(),
+        active: { name: 'blockquote' },
+        can: (editor) => editor.can().chain().focus().toggleBlockquote().run(),
+        icon: TextQuote,
+        label: $t('ui.tiptap.toolbar.blockquote'),
+      },
+      {
+        action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
+        active: { name: 'codeBlock' },
+        can: (editor) => editor.can().chain().focus().toggleCodeBlock().run(),
+        icon: MessageSquareCode,
+        label: $t('ui.tiptap.toolbar.codeBlock'),
+      },
+    ],
+    [
+      {
+        action: (editor) => handleLinkAction(editor),
+        active: { name: 'link' },
+        can: (editor) =>
+          editor.can().chain().focus().extendMarkRange('link').run(),
+        icon: Link2,
+        label: $t('ui.tiptap.toolbar.link'),
+      },
+      {
+        action: (editor) => editor.chain().focus().unsetLink().run(),
+        can: (editor) => editor.can().chain().focus().unsetLink().run(),
+        icon: Unlink2,
+        isActive: (editor) => editor.isActive('link'),
+        label: $t('ui.tiptap.toolbar.unlink'),
+      },
+      {
+        action: (editor) => handleImageAction(editor),
+        icon: ImagePlus,
+        label: $t('ui.tiptap.toolbar.image'),
+      },
+    ],
+    [
+      {
+        action: () => {},
+        icon: Paintbrush,
+        indicatorColor: (editor) =>
+          editor.getAttributes('textStyle').color as string | undefined,
+        isActive: (editor) => Boolean(editor.getAttributes('textStyle').color),
+        label: $t('ui.tiptap.toolbar.textColor'),
+        palette: {
+          apply: (editor, color) =>
+            editor.chain().focus().setColor(color).run(),
+          clear: (editor) => editor.chain().focus().unsetColor().run(),
+          colors: editorColorPresets,
+          currentColor: (editor) =>
+            editor.getAttributes('textStyle').color as string | undefined,
+        },
+      },
+      {
+        action: () => {},
+        icon: Highlighter,
+        indicatorColor: (editor) =>
+          (editor.getAttributes('highlight').color as string | undefined) ??
+          '#fef08a',
+        isActive: (editor) => editor.isActive('highlight'),
+        label: $t('ui.tiptap.toolbar.highlightColor'),
+        palette: {
+          apply: (editor, color) =>
+            editor.chain().focus().setHighlight({ color }).run(),
+          clear: (editor) => editor.chain().focus().unsetHighlight().run(),
+          colors: editorHighlightPresets,
+          currentColor: (editor) =>
+            editor.getAttributes('highlight').color as string | undefined,
+        },
+      },
+    ],
+    [
+      {
+        action: (editor) => editor.chain().focus().setTextAlign('left').run(),
+        can: (editor) =>
+          editor.can().chain().focus().setTextAlign('left').run(),
+        icon: AlignLeft,
+        isActive: (editor) => editor.isActive({ textAlign: 'left' }),
+        label: $t('ui.tiptap.toolbar.alignLeft'),
+      },
+      {
+        action: (editor) => editor.chain().focus().setTextAlign('center').run(),
+        can: (editor) =>
+          editor.can().chain().focus().setTextAlign('center').run(),
+        icon: AlignCenter,
+        isActive: (editor) => editor.isActive({ textAlign: 'center' }),
+        label: $t('ui.tiptap.toolbar.alignCenter'),
+      },
+      {
+        action: (editor) => editor.chain().focus().setTextAlign('right').run(),
+        can: (editor) =>
+          editor.can().chain().focus().setTextAlign('right').run(),
+        icon: AlignRight,
+        isActive: (editor) => editor.isActive({ textAlign: 'right' }),
+        label: $t('ui.tiptap.toolbar.alignRight'),
+      },
+    ],
+  ];
+}

+ 60 - 0
packages/effects/plugins/src/tiptap/types.ts

@@ -0,0 +1,60 @@
+import type { Extensions, JSONContent } from '@tiptap/core';
+import type { Editor } from '@tiptap/vue-3';
+
+import type { Component } from 'vue';
+
+export interface TipTapProps {
+  editable?: boolean;
+  extensions?: Extensions;
+  minHeight?: number | string;
+  placeholder?: string;
+  previewable?: boolean;
+  toolbar?: boolean;
+}
+
+export interface TipTapPreviewProps {
+  class?: any;
+  content?: string;
+  minHeight?: number | string;
+}
+
+export interface VbenTiptapChangeEvent {
+  html: string;
+  json: JSONContent;
+  text: string;
+}
+
+export interface VbenTiptapExtensionOptions {
+  placeholder?: string;
+}
+
+export interface ToolbarAction {
+  action: (editor: Editor) => void;
+  active?: {
+    attrs?: Record<string, unknown>;
+    name: string;
+  };
+  can?: (editor: Editor) => boolean;
+  icon?: Component;
+  indicatorColor?: (editor: Editor) => string | undefined;
+  isActive?: (editor: Editor) => boolean;
+  label: string;
+  menu?: {
+    items: ToolbarMenuItem[];
+  };
+  palette?: {
+    apply: (editor: Editor, color: string) => void;
+    clear?: (editor: Editor) => void;
+    colors: string[];
+    currentColor?: (editor: Editor) => string | undefined;
+  };
+  triggerText?: ((editor?: Editor) => string) | string;
+}
+
+export interface ToolbarMenuItem {
+  action: (editor: Editor) => void;
+  can?: (editor: Editor) => boolean;
+  isActive?: (editor: Editor) => boolean;
+  label: string;
+  shortLabel: string;
+}

+ 176 - 0
packages/effects/plugins/src/tiptap/use-tiptap-toolbar.ts

@@ -0,0 +1,176 @@
+import type { Editor } from '@tiptap/vue-3';
+
+import type { ShallowRef } from 'vue';
+
+import type { ToolbarAction, ToolbarMenuItem } from './types';
+
+import { cn } from '@vben-core/shared/utils';
+
+interface UseTiptapToolbarOptions {
+  editable: () => boolean;
+  editor: Readonly<ShallowRef<Editor | undefined>>;
+}
+
+export function useTiptapToolbar(options: UseTiptapToolbarOptions) {
+  const getEditor = () => options.editor.value;
+
+  function getActionIndicatorColor(action: ToolbarAction) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !action.indicatorColor) {
+      return undefined;
+    }
+
+    return action.indicatorColor(currentEditor);
+  }
+
+  function getPaletteCurrentColor(action: ToolbarAction) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !action.palette?.currentColor) {
+      return undefined;
+    }
+
+    return action.palette.currentColor(currentEditor);
+  }
+
+  function canRunAction(action: ToolbarAction) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !options.editable()) {
+      return false;
+    }
+
+    return action.can ? action.can(currentEditor) : true;
+  }
+
+  function canRunMenuItem(item: ToolbarMenuItem) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !options.editable()) {
+      return false;
+    }
+
+    return item.can ? item.can(currentEditor) : true;
+  }
+
+  function isActionActive(action: ToolbarAction) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor) {
+      return false;
+    }
+
+    if (action.isActive) {
+      return action.isActive(currentEditor);
+    }
+
+    if (!action.active) {
+      return false;
+    }
+
+    return currentEditor.isActive(action.active.name, action.active.attrs);
+  }
+
+  function isMenuItemActive(item: ToolbarMenuItem, currentEditor?: Editor) {
+    const targetEditor = currentEditor ?? getEditor();
+
+    if (!targetEditor || !item.isActive) {
+      return false;
+    }
+
+    return item.isActive(targetEditor);
+  }
+
+  function runAction(action: ToolbarAction) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !options.editable()) {
+      return;
+    }
+
+    if (action.menu || action.palette) {
+      return;
+    }
+
+    action.action(currentEditor);
+  }
+
+  function runMenuItem(item: ToolbarMenuItem) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !options.editable()) {
+      return;
+    }
+
+    item.action(currentEditor);
+  }
+
+  function applyPaletteColor(action: ToolbarAction, color: string) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !action.palette) {
+      return;
+    }
+
+    action.palette.apply(currentEditor, color);
+  }
+
+  function clearPaletteColor(action: ToolbarAction) {
+    const currentEditor = getEditor();
+
+    if (!currentEditor || !action.palette?.clear) {
+      return;
+    }
+
+    action.palette.clear(currentEditor);
+  }
+
+  function getToolbarButtonClass(action: ToolbarAction) {
+    return cn(
+      'relative rounded-[10px] border border-transparent bg-transparent text-muted-foreground shadow-none',
+      'transition-[transform,color,background-color,border-color,box-shadow] duration-200 ease-out',
+      'enabled:hover:-translate-y-px enabled:hover:border-border disabled:opacity-45',
+      'enabled:hover:bg-accent enabled:hover:text-foreground',
+      isActionActive(action) &&
+        'border-primary/30 bg-accent text-primary shadow-primary',
+    );
+  }
+
+  function getPaletteSwatchClass(action: ToolbarAction, color: string) {
+    return cn(
+      'inline-flex size-8 items-center justify-center rounded-full border border-border',
+      'shadow-accent',
+      'transition-[transform,box-shadow,border-color] duration-200 ease-out',
+      'hover:-translate-y-px hover:scale-[1.04]',
+      getPaletteCurrentColor(action) === color &&
+        'border-primary shadow-primary',
+    );
+  }
+
+  function getMenuItemClass(item: ToolbarMenuItem) {
+    return cn(
+      'flex items-center gap-2 rounded-lg p-2 text-left text-sm transition-colors',
+      'disabled:cursor-not-allowed disabled:opacity-45',
+      isMenuItemActive(item)
+        ? 'bg-accent text-foreground'
+        : 'text-muted-foreground hover:bg-accent hover:text-foreground',
+    );
+  }
+
+  return {
+    applyPaletteColor,
+    canRunAction,
+    canRunMenuItem,
+    clearPaletteColor,
+    getActionIndicatorColor,
+    getMenuItemClass,
+    getPaletteCurrentColor,
+    getPaletteSwatchClass,
+    getToolbarButtonClass,
+    isActionActive,
+    isMenuItemActive,
+    runAction,
+    runMenuItem,
+  };
+}

+ 36 - 0
packages/locales/src/langs/en-US/ui.json

@@ -61,6 +61,42 @@
     "cancel": "Cancel cropping",
     "errorTip": "Cropping error"
   },
+  "tiptap": {
+    "placeholder": "Please enter content...",
+    "prompts": {
+      "image": "Enter image URL",
+      "link": "Enter link URL"
+    },
+    "toolbar": {
+      "bold": "Bold",
+      "italic": "Italic",
+      "underline": "Underline",
+      "strike": "Strike",
+      "code": "Code",
+      "codeBlock": "Code Block",
+      "heading": "Heading",
+      "paragraph": "Paragraph",
+      "heading1": "H1",
+      "heading2": "H2",
+      "heading3": "H3",
+      "heading4": "H4",
+      "bulletList": "Bullets",
+      "orderedList": "Numbering",
+      "blockquote": "Quote",
+      "link": "Link",
+      "unlink": "Unlink",
+      "image": "Image",
+      "textColor": "Text Color",
+      "highlightColor": "Highlight Color",
+      "alignLeft": "Left",
+      "alignCenter": "Center",
+      "alignRight": "Right",
+      "preview": "Preview",
+      "undo": "Undo",
+      "redo": "Redo",
+      "clear": "Clear"
+    }
+  },
   "fallback": {
     "pageNotFound": "Oops! Page Not Found",
     "pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.",

+ 36 - 0
packages/locales/src/langs/zh-CN/ui.json

@@ -61,6 +61,42 @@
     "cancel": "取消裁剪",
     "errorTip": "裁剪错误"
   },
+  "tiptap": {
+    "placeholder": "请输入内容...",
+    "prompts": {
+      "image": "请输入图片地址",
+      "link": "请输入链接地址"
+    },
+    "toolbar": {
+      "bold": "加粗",
+      "italic": "斜体",
+      "underline": "下划线",
+      "strike": "删除线",
+      "code": "行内代码",
+      "codeBlock": "代码块",
+      "heading": "标题",
+      "paragraph": "正文",
+      "heading1": "标题1",
+      "heading2": "标题2",
+      "heading3": "标题3",
+      "heading4": "标题4",
+      "bulletList": "无序列表",
+      "orderedList": "有序列表",
+      "blockquote": "引用",
+      "link": "链接",
+      "unlink": "移除链接",
+      "image": "图片",
+      "textColor": "文字颜色",
+      "highlightColor": "高亮颜色",
+      "alignLeft": "左对齐",
+      "alignCenter": "居中",
+      "alignRight": "右对齐",
+      "preview": "预览",
+      "undo": "撤销",
+      "redo": "重做",
+      "clear": "清除"
+    }
+  },
   "fallback": {
     "pageNotFound": "哎呀!未找到页面",
     "pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。",

+ 3 - 0
playground/src/adapter/component/index.ts

@@ -40,6 +40,7 @@ import {
 import { useSortable } from '@vben/hooks';
 import { IconifyIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
+import { VbenTiptap } from '@vben/plugins/tiptap';
 import { isEmpty } from '@vben/utils';
 
 import { message, Modal, notification } from 'ant-design-vue';
@@ -583,6 +584,7 @@ export type ComponentType =
   | 'RadioGroup'
   | 'RangePicker'
   | 'Rate'
+  | 'RichEditor'
   | 'Select'
   | 'Space'
   | 'Switch'
@@ -646,6 +648,7 @@ async function initComponentAdapter() {
     RadioGroup,
     RangePicker,
     Rate,
+    RichEditor: withDefaultPlaceholder(VbenTiptap, 'input'),
     Select: withDefaultPlaceholder(Select, 'select'),
     Space,
     Switch,

+ 3 - 0
playground/src/locales/langs/en-US/examples.json

@@ -79,5 +79,8 @@
   },
   "cropper": {
     "title": "Cropper"
+  },
+  "tiptap": {
+    "title": "Rich Text Editor"
   }
 }

+ 3 - 0
playground/src/locales/langs/zh-CN/examples.json

@@ -79,5 +79,8 @@
   },
   "cropper": {
     "title": "图片裁剪"
+  },
+  "tiptap": {
+    "title": "富文本编辑器"
   }
 }

+ 9 - 0
playground/src/router/routes/modules/examples.ts

@@ -346,6 +346,15 @@ const routes: RouteRecordRaw[] = [
           title: $t('examples.cropper.title'),
         },
       },
+      {
+        name: 'TiptapExample',
+        path: '/examples/tiptap',
+        component: () => import('#/views/examples/tiptap/index.vue'),
+        meta: {
+          icon: 'lucide:square-pen',
+          title: $t('examples.tiptap.title'),
+        },
+      },
     ],
   },
 ];

+ 12 - 0
playground/src/views/examples/form/basic.vue

@@ -409,6 +409,12 @@ const [BaseForm, baseFormApi] = useVbenForm({
       },
       rules: 'selectRequired',
     },
+    {
+      component: 'RichEditor',
+      fieldName: 'richEditor',
+      label: '富文本',
+      formItemClass: 'col-span-3 items-baseline',
+    },
   ],
   // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
   wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
@@ -485,6 +491,12 @@ function handleSetFormValue() {
     timePicker: dayjs('2022-01-01 12:00:00'),
     treeSelect: 'leaf1',
     username: '1',
+    richEditor: `
+      <h1>Vben Tiptap</h1>
+      <p>这个编辑器已经被封装在 <code>packages/effects/plugins/src/tiptap</code> 中。</p>
+      <p>你可以直接在各个 app 里通过 <code>@vben/plugins/tiptap</code> 引入。</p>
+      <blockquote>默认内置 StarterKit、Underline、TextAlign、Placeholder。</blockquote>
+    `,
   });
 
   // 设置单个表单值

+ 39 - 0
playground/src/views/examples/tiptap/index.vue

@@ -0,0 +1,39 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+import { VbenTiptap, VbenTiptapPreview } from '@vben/plugins/tiptap';
+
+import { Card } from 'ant-design-vue';
+const content = ref(`
+  <h1>Vben Tiptap</h1>
+  <p>这个编辑器已经被封装在 <code>packages/effects/plugins/src/tiptap</code> 中。</p>
+  <p>你可以直接在各个 app 里通过 <code>@vben/plugins/tiptap</code> 引入。</p>
+  <blockquote>默认内置 StarterKit、Underline、TextAlign、Placeholder。</blockquote>
+`);
+const previewContent = computed(() => content.value);
+</script>
+
+<template>
+  <Page title="Tiptap 富文本">
+    <template #description>
+      <div class="mt-2 text-foreground/80">
+        统一封装后的富文本编辑器,适合在各个 app 中直接复用。
+      </div>
+    </template>
+
+    <Card class="mb-5" title="编辑器">
+      <VbenTiptap v-model="content" />
+    </Card>
+
+    <Card class="mb-5" title="富文本预览">
+      <VbenTiptapPreview :content="previewContent" />
+    </Card>
+
+    <Card title="HTML 输出">
+      <pre class="overflow-auto rounded-xl border border-border bg-muted p-4">
+        {{ previewContent }}
+      </pre>
+    </Card>
+  </Page>
+</template>

+ 656 - 4
pnpm-lock.yaml

@@ -78,6 +78,36 @@ catalogs:
     '@tanstack/vue-store':
       specifier: ^0.9.3
       version: 0.9.3
+    '@tiptap/core':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-highlight':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-image':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-link':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-placeholder':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-text-align':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-text-style':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/extension-underline':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/starter-kit':
+      specifier: ^3.21.0
+      version: 3.21.0
+    '@tiptap/vue-3':
+      specifier: ^3.21.0
+      version: 3.21.0
     '@tsdown/css':
       specifier: ^0.21.7
       version: 0.21.7
@@ -1758,12 +1788,45 @@ importers:
 
   packages/effects/plugins:
     dependencies:
+      '@tiptap/core':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/extension-highlight':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-image':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-link':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/extension-placeholder':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-text-align':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-text-style':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-underline':
+        specifier: 'catalog:'
+        version: 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/starter-kit':
+        specifier: 'catalog:'
+        version: 3.21.0
+      '@tiptap/vue-3':
+        specifier: 'catalog:'
+        version: 3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(vue@3.5.31(typescript@5.9.3))
       '@vben-core/design':
         specifier: workspace:*
         version: link:../../@core/base/design
       '@vben-core/form-ui':
         specifier: workspace:*
         version: link:../../@core/ui-kit/form-ui
+      '@vben-core/popup-ui':
+        specifier: workspace:*
+        version: link:../../@core/ui-kit/popup-ui
       '@vben-core/shadcn-ui':
         specifier: workspace:*
         version: link:../../@core/ui-kit/shadcn-ui
@@ -4474,6 +4537,9 @@ packages:
   '@quansync/fs@1.0.0':
     resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
 
+  '@remirror/core-constants@3.0.0':
+    resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
+
   '@rolldown/binding-android-arm64@1.0.0-rc.12':
     resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
     engines: {node: ^20.19.0 || >=22.12.0}
@@ -5035,6 +5101,178 @@ packages:
     peerDependencies:
       vue: ^3.5.31
 
+  '@tiptap/core@3.21.0':
+    resolution: {integrity: sha512-IfnQiuEeabDSPr1C/zHFTbnvlTf5z0DE/d/xz4C6bkL4ZBDJ3rr99h2qsaV0l8F+kbNswZMlQdM8rxNlMy95fQ==}
+    peerDependencies:
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-blockquote@3.21.0':
+    resolution: {integrity: sha512-JDM/RR6rM0dMCZ1UnEf7eqmN6pAdIa2llhN+E24HdTGNJCklMFhLAGE/OT8/1r7M0WWA9GVO7/PTe4EdGh6+lQ==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-bold@3.21.0':
+    resolution: {integrity: sha512-iyEJRzG7XTCPlHwEDzUw3HnuYYCfL7lNpcCHmxcpYMrIUA8rv7EUxerIwApT6xY8hQ/07ljuJKgOyPvnJOOzuA==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-bubble-menu@3.21.0':
+    resolution: {integrity: sha512-/fabRRhhf8i4LAx9e8xz9ppqN5KgdJk3TxMuxAD5vAWGsejvhSoPa8O8H/QwwyntXm1Vue8aQiMHsUk48b2hGQ==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-bullet-list@3.21.0':
+    resolution: {integrity: sha512-PWNF+xwxgOeXYGD88sCQLKL0eBoQqjUnZNALxBjN3Y7x4llalh42rHOp2Nt2t6UbQgqTBtBzU/uFcussTpxreQ==}
+    peerDependencies:
+      '@tiptap/extension-list': ^3.21.0
+
+  '@tiptap/extension-code-block@3.21.0':
+    resolution: {integrity: sha512-zrVOcOzDCjHQ8NJcC+qHmZZKiwnP/NMSb3qVJlSMN8TzuHept1MZCDa2Mbo70O6I0txo456SGuXB9sqV1vHmGg==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-code@3.21.0':
+    resolution: {integrity: sha512-D7wA9jp+4X2r1f3FIoga73s6Rn4rmZY57Jes6a4rK3HY+3yHk1r057pPIZSY8Drfs97jxHQVFdfUYUomLSFYBA==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-document@3.21.0':
+    resolution: {integrity: sha512-7oCyzXI9ChvJQUlr23AURdfVar4OIsrYUvqdhEwo3bjcI/Q/j0KJiXfuh6ZzL5eVaINSailH53sZaGg4THQtUg==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-dropcursor@3.21.0':
+    resolution: {integrity: sha512-6fsDSVAM2iz7eElvT6iivMrGBGjIP/oPigVZ/SPm6f31phaYhz6TIOEgV/Lr2jaPIOgyK4U0cU4Yd4KUBCmhzQ==}
+    peerDependencies:
+      '@tiptap/extensions': ^3.21.0
+
+  '@tiptap/extension-floating-menu@3.21.0':
+    resolution: {integrity: sha512-n2HzTB+I/5rAl8R/1sKMv92JiY1oDK1hroXizxEKYa6dskJcAMW0CfYyPcPOZWQQEe7qoeOvQISr2ooLAKW+Mw==}
+    peerDependencies:
+      '@floating-ui/dom': ^1.0.0
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-gapcursor@3.21.0':
+    resolution: {integrity: sha512-wGjgAoYBTvPAe9QYMI5px355XcNeMkaUrMY9IHbMqgqdmHcDxqooxM4H6sYVX2CRcHwXy4I8NQAoOhSYrQJDMg==}
+    peerDependencies:
+      '@tiptap/extensions': ^3.21.0
+
+  '@tiptap/extension-hard-break@3.21.0':
+    resolution: {integrity: sha512-6JFVSAOQ1qhQHi9mVcdn2/XO8YIMgYV8zjarzNUzP6Sf2waeE5BLXjlg6rIH/945sY1J+FndTojLru6gQ07a5A==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-heading@3.21.0':
+    resolution: {integrity: sha512-ji6VJmoRnDzAHYflEYEZohMHRi77UGLW1o3ua7UhI32iJ9nuYssbPNuzEeE4SvENMQwZRszad5+a+dKAa+NC7g==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-highlight@3.21.0':
+    resolution: {integrity: sha512-3f/bVgfm2dJZxalh07TThDxcTaeXJ+dpYyRY9trnFeHbhyYQXSy6yzkNhNcYB4Ua5jxpKQv4b9Q448QVh+KNzA==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-horizontal-rule@3.21.0':
+    resolution: {integrity: sha512-vNBnOfFEY62CoJPGo4nonRM7RiOvhII1vhoO+WFr1GxDqCAfmEFjToflt7JT1UJdo6lMVcD+aaaAgOiuSz5p6g==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-image@3.21.0':
+    resolution: {integrity: sha512-W9786a2K4LSZJMPeRLmoDulJeXOsM0ueRV2MHjTol7ikPRauROB7GUbAz9DyPAJHA2AGUfpswnGAYPO3tz5CLg==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-italic@3.21.0':
+    resolution: {integrity: sha512-2I8oPvwyXhRn1k8lbDFIutzvhtLEjoO5mmQCNX4TnT4PdxxaSrK9+ihYg12VeqhUeO7dg1MKiFqws0HVBrwzWg==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-link@3.21.0':
+    resolution: {integrity: sha512-oMU7Yve1sbgBsaFAUc2R0GPf4d3ZPVJeMUFC6b6X9rJIvx/IhEUEn9toQcSBGfp02uWK9NdQyIFYFdWlVXH++w==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-list-item@3.21.0':
+    resolution: {integrity: sha512-1ZymZmlQVbAoC4q5x3cro0v5+3I6l+BHqbhIMQLjQFlAOJfcE0pvqRzAFW7PduxUj41tXEtsYqp2NREvO9F5Fg==}
+    peerDependencies:
+      '@tiptap/extension-list': ^3.21.0
+
+  '@tiptap/extension-list-keymap@3.21.0':
+    resolution: {integrity: sha512-EzrfW3ASNFPWKhR8sNOq7Kqw4hvaTAOn4dlI7chB8HIANSrlyPOUn+eKAnO6HQgsUgsbjg2GbTUrGrxcoLykUg==}
+    peerDependencies:
+      '@tiptap/extension-list': ^3.21.0
+
+  '@tiptap/extension-list@3.21.0':
+    resolution: {integrity: sha512-KeBlEtLrGce2d3dgL89hmwWEtREuzlW4XY5bYWpKNvCbFqvdSb3n7vkdkw32YclZmMWxAcABgW6ucCStkE0rsQ==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/extension-ordered-list@3.21.0':
+    resolution: {integrity: sha512-+d+0orokMfqaBfvr9tUBgGvo2ZCV+fR3JzsJTmnLBWOkhBSJN7H4pnfXPTue0qwspUwRmkLJxdIlU+J7HkMrng==}
+    peerDependencies:
+      '@tiptap/extension-list': ^3.21.0
+
+  '@tiptap/extension-paragraph@3.21.0':
+    resolution: {integrity: sha512-cMPG/jCoZ9NmLZ5ctFziILaxJGfDtMTb5OLBhifMFZeMVwF1pEJIygDEfnX/HSruv507weZSQG4pERO2tRszMg==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-placeholder@3.21.0':
+    resolution: {integrity: sha512-fs+cQqMh1d1naV6OgOhP/0qbRJwtw8DpQMj3/oqGKbaRRKIeecEaZPXYRd7MYa4e9K0Cfk5Bm0MNs9lwu/BYsw==}
+    peerDependencies:
+      '@tiptap/extensions': ^3.21.0
+
+  '@tiptap/extension-strike@3.21.0':
+    resolution: {integrity: sha512-easnVaN11Wl+5fOtfvzJ10J762S9TRXZaMj5rLBGavgf82DCYHqhGhBqpLQrJ41r4nPABGlYvTRoxfvBLB74Lg==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-text-align@3.21.0':
+    resolution: {integrity: sha512-XaoUaq45nai9LLoUStumMItHhhCnmXirPR8mTjEDKZ2QD0Kg/YHEr6guJYc6qKB7YA+Wa1EgBIrZOv1+d2Pdag==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-text-style@3.21.0':
+    resolution: {integrity: sha512-MI/3X75D45Wa/+0Fp8nYfNJq5makkjtG7B2/lIzNUe0kEKJ56RVQoV1GQSXxAiFNyAYRfKFq8dJslhesW3EkWA==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-text@3.21.0':
+    resolution: {integrity: sha512-Zx8QdB8a5iBuE4uO21c3BjmpBfaJEr2Jd1QFnsdgx11fm6P7dGgZaGko1FaINhfOPRGTN6O/kiF02cDMdOHa/w==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extension-underline@3.21.0':
+    resolution: {integrity: sha512-gGmBEymbWnr8AIS8bI/bPw5rcwo7wAFcBw/TsLd1nAanu1dDqSRNDBrit3m02Ru+D88u2SfNvmbOPI1pz+1f5w==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+
+  '@tiptap/extensions@3.21.0':
+    resolution: {integrity: sha512-MN1uh5PmHT1F2BNsbc21MIS0AMFFA73oODlp/4ckpBR4o5AxRwV+8f43Cd52UL4MgMkKj/A+QfZ7iK9IDb0h5A==}
+    peerDependencies:
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+
+  '@tiptap/pm@3.21.0':
+    resolution: {integrity: sha512-I3sNo7oMMsR6FFz1ecvPb9uCF0VQuS2WV67j8Io2M7DJicRWCE/GM5DaiYjTeWBbnByk6BuG0txoJATAqPVliQ==}
+
+  '@tiptap/starter-kit@3.21.0':
+    resolution: {integrity: sha512-w7fWxglDtqXFBgRYH+LforJyUboSAQllnWQbGVSTyX4rsICqZjkb3f6CTSUWpGoGKmlmbb2ZpEuoik7tur9d8Q==}
+
+  '@tiptap/vue-3@3.21.0':
+    resolution: {integrity: sha512-dfjxBwxg9+GNvsgkCbxLnj/vmG+YZMdcD/qF7bKM710bANWfqzimRUhH5W2KZcxqlYzqpz0u/P0zi7dUMR5IZA==}
+    peerDependencies:
+      '@floating-ui/dom': ^1.0.0
+      '@tiptap/core': ^3.21.0
+      '@tiptap/pm': ^3.21.0
+      vue: ^3.5.31
+
   '@tsdown/css@0.21.7':
     resolution: {integrity: sha512-kydfZ109LIXwoBDrdIeEVi+PtM8375X9d/6UtYtjhj6TS94J25gJVUXw9AyJE6THEqB6OdGKM5MLqJPutO4kkA==}
     engines: {node: '>=20.19.0'}
@@ -6045,8 +6283,8 @@ packages:
       bare-buffer:
         optional: true
 
-  bare-os@3.8.4:
-    resolution: {integrity: sha512-4JboWUl7/2LhgU536tjUszzaVC8/WEWKtyX5crayvlN71ih8+O2SdvBhotQeDsuhhmPZmLCrPBJEcwVPhI/kkQ==}
+  bare-os@3.8.6:
+    resolution: {integrity: sha512-l8xaNWWb/bXuzgsrlF5jaa5QYDJ9S0ddd54cP6CH+081+5iPrbJiCfBWQqrWYzmUhCbsH+WR6qxo9MeHVCr0MQ==}
     engines: {bare: '>=1.14.0'}
 
   bare-path@3.0.0:
@@ -6502,6 +6740,9 @@ packages:
     resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
     engines: {node: '>= 14'}
 
+  crelt@1.0.6:
+    resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
   croner@10.0.1:
     resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
     engines: {node: '>=18.0'}
@@ -8303,6 +8544,12 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
+  linkify-it@5.0.0:
+    resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
+  linkifyjs@4.3.2:
+    resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
+
   listhen@1.9.0:
     resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
     hasBin: true
@@ -8457,6 +8704,10 @@ packages:
   mark.js@8.11.1:
     resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
 
+  markdown-it@14.1.1:
+    resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
+    hasBin: true
+
   math-intrinsics@1.1.0:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
@@ -8476,6 +8727,9 @@ packages:
   mdn-data@2.27.1:
     resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
 
+  mdurl@2.0.0:
+    resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
   medium-zoom@1.1.0:
     resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==}
 
@@ -8802,6 +9056,9 @@ packages:
     resolution: {integrity: sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==}
     engines: {node: '>=20'}
 
+  orderedmap@2.1.1:
+    resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
+
   outdent@0.5.0:
     resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
 
@@ -9159,6 +9416,64 @@ packages:
   property-information@7.1.0:
     resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
 
+  prosemirror-changeset@2.4.0:
+    resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==}
+
+  prosemirror-collab@1.3.1:
+    resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
+
+  prosemirror-commands@1.7.1:
+    resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
+
+  prosemirror-dropcursor@1.8.2:
+    resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
+
+  prosemirror-gapcursor@1.4.1:
+    resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
+
+  prosemirror-history@1.5.0:
+    resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
+
+  prosemirror-inputrules@1.5.1:
+    resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
+
+  prosemirror-keymap@1.2.3:
+    resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
+
+  prosemirror-markdown@1.13.4:
+    resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==}
+
+  prosemirror-menu@1.3.0:
+    resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==}
+
+  prosemirror-model@1.25.4:
+    resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==}
+
+  prosemirror-schema-basic@1.2.4:
+    resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
+
+  prosemirror-schema-list@1.5.1:
+    resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
+
+  prosemirror-state@1.4.4:
+    resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
+
+  prosemirror-tables@1.8.5:
+    resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==}
+
+  prosemirror-trailing-node@3.0.0:
+    resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
+    peerDependencies:
+      prosemirror-model: ^1.22.1
+      prosemirror-state: ^1.4.2
+      prosemirror-view: ^1.33.8
+
+  prosemirror-transform@1.12.0:
+    resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
+
+  prosemirror-view@1.41.7:
+    resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==}
+
   proto-list@1.2.4:
     resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
 
@@ -9174,6 +9489,10 @@ packages:
     engines: {node: '>=18'}
     hasBin: true
 
+  punycode.js@2.3.1:
+    resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+    engines: {node: '>=6'}
+
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -9429,6 +9748,9 @@ packages:
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
+  rope-sequence@1.3.4:
+    resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
+
   run-applescript@7.1.0:
     resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
     engines: {node: '>=18'}
@@ -10258,6 +10580,9 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  uc.micro@2.1.0:
+    resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
   ufo@1.6.3:
     resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
 
@@ -10812,6 +11137,9 @@ packages:
   vxe-table@4.18.10:
     resolution: {integrity: sha512-8V9WL83pB4PrvgBS6DoDU7dSRxOlJw9ZkVcxDnKBFQkTTnLVvC8HYOJ75uwfRNyo9cALNIpJnSkow2uqaMKbNQ==}
 
+  w3c-keyname@2.2.8:
+    resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
   warning@4.0.3:
     resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
 
@@ -13573,6 +13901,8 @@ snapshots:
     dependencies:
       quansync: 1.0.0
 
+  '@remirror/core-constants@3.0.0': {}
+
   '@rolldown/binding-android-arm64@1.0.0-rc.12':
     optional: true
 
@@ -14007,6 +14337,196 @@ snapshots:
       '@tanstack/virtual-core': 3.13.23
       vue: 3.5.31(typescript@5.9.3)
 
+  '@tiptap/core@3.21.0(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@tiptap/pm': 3.21.0
+
+  '@tiptap/extension-blockquote@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-bold@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@floating-ui/dom': 1.7.6
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+    optional: true
+
+  '@tiptap/extension-bullet-list@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+
+  '@tiptap/extension-code@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-document@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-dropcursor@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@floating-ui/dom': 1.7.6
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+    optional: true
+
+  '@tiptap/extension-gapcursor@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-hard-break@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-heading@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-highlight@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-horizontal-rule@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+
+  '@tiptap/extension-image@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-italic@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-link@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+      linkifyjs: 4.3.2
+
+  '@tiptap/extension-list-item@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-list-keymap@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+
+  '@tiptap/extension-ordered-list@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-paragraph@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-placeholder@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-strike@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-text-align@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-text-style@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-text@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extension-underline@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+
+  '@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+
+  '@tiptap/pm@3.21.0':
+    dependencies:
+      prosemirror-changeset: 2.4.0
+      prosemirror-collab: 1.3.1
+      prosemirror-commands: 1.7.1
+      prosemirror-dropcursor: 1.8.2
+      prosemirror-gapcursor: 1.4.1
+      prosemirror-history: 1.5.0
+      prosemirror-inputrules: 1.5.1
+      prosemirror-keymap: 1.2.3
+      prosemirror-markdown: 1.13.4
+      prosemirror-menu: 1.3.0
+      prosemirror-model: 1.25.4
+      prosemirror-schema-basic: 1.2.4
+      prosemirror-schema-list: 1.5.1
+      prosemirror-state: 1.4.4
+      prosemirror-tables: 1.8.5
+      prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7)
+      prosemirror-transform: 1.12.0
+      prosemirror-view: 1.41.7
+
+  '@tiptap/starter-kit@3.21.0':
+    dependencies:
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/extension-blockquote': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-bold': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-bullet-list': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-code': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-code-block': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/extension-document': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-dropcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-gapcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-hard-break': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-heading': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-horizontal-rule': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/extension-italic': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-link': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/extension-list-item': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-list-keymap': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-ordered-list': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))
+      '@tiptap/extension-paragraph': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-strike': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-text': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extension-underline': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))
+      '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+
+  '@tiptap/vue-3@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(vue@3.5.31(typescript@5.9.3))':
+    dependencies:
+      '@floating-ui/dom': 1.7.6
+      '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0)
+      '@tiptap/pm': 3.21.0
+      vue: 3.5.31(typescript@5.9.3)
+    optionalDependencies:
+      '@tiptap/extension-bubble-menu': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+      '@tiptap/extension-floating-menu': 3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)
+
   '@tsdown/css@0.21.7(jiti@2.6.1)(postcss@8.5.8)(sass-embedded@1.98.0)(sass@1.98.0)(tsdown@0.21.7)(yaml@2.8.3)':
     dependencies:
       lightningcss: 1.32.0
@@ -15217,11 +15737,11 @@ snapshots:
       - bare-abort-controller
       - react-native-b4a
 
-  bare-os@3.8.4: {}
+  bare-os@3.8.6: {}
 
   bare-path@3.0.0:
     dependencies:
-      bare-os: 3.8.4
+      bare-os: 3.8.6
 
   bare-stream@2.11.0(bare-events@2.8.2):
     dependencies:
@@ -15686,6 +16206,8 @@ snapshots:
       crc-32: 1.2.2
       readable-stream: 4.7.0
 
+  crelt@1.0.6: {}
+
   croner@10.0.1: {}
 
   cross-env@10.1.0:
@@ -17672,6 +18194,12 @@ snapshots:
 
   lines-and-columns@1.2.4: {}
 
+  linkify-it@5.0.0:
+    dependencies:
+      uc.micro: 2.1.0
+
+  linkifyjs@4.3.2: {}
+
   listhen@1.9.0:
     dependencies:
       '@parcel/watcher': 2.5.6
@@ -17833,6 +18361,15 @@ snapshots:
 
   mark.js@8.11.1: {}
 
+  markdown-it@14.1.1:
+    dependencies:
+      argparse: 2.0.1
+      entities: 4.5.0
+      linkify-it: 5.0.0
+      mdurl: 2.0.0
+      punycode.js: 2.3.1
+      uc.micro: 2.1.0
+
   math-intrinsics@1.1.0: {}
 
   mathml-tag-names@4.0.0: {}
@@ -17855,6 +18392,8 @@ snapshots:
 
   mdn-data@2.27.1: {}
 
+  mdurl@2.0.0: {}
+
   medium-zoom@1.1.0: {}
 
   memoize-one@6.0.0: {}
@@ -18275,6 +18814,8 @@ snapshots:
       stdin-discarder: 0.3.1
       string-width: 8.2.0
 
+  orderedmap@2.1.1: {}
+
   outdent@0.5.0: {}
 
   own-keys@1.0.1:
@@ -18610,6 +19151,109 @@ snapshots:
 
   property-information@7.1.0: {}
 
+  prosemirror-changeset@2.4.0:
+    dependencies:
+      prosemirror-transform: 1.12.0
+
+  prosemirror-collab@1.3.1:
+    dependencies:
+      prosemirror-state: 1.4.4
+
+  prosemirror-commands@1.7.1:
+    dependencies:
+      prosemirror-model: 1.25.4
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+
+  prosemirror-dropcursor@1.8.2:
+    dependencies:
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+      prosemirror-view: 1.41.7
+
+  prosemirror-gapcursor@1.4.1:
+    dependencies:
+      prosemirror-keymap: 1.2.3
+      prosemirror-model: 1.25.4
+      prosemirror-state: 1.4.4
+      prosemirror-view: 1.41.7
+
+  prosemirror-history@1.5.0:
+    dependencies:
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+      prosemirror-view: 1.41.7
+      rope-sequence: 1.3.4
+
+  prosemirror-inputrules@1.5.1:
+    dependencies:
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+
+  prosemirror-keymap@1.2.3:
+    dependencies:
+      prosemirror-state: 1.4.4
+      w3c-keyname: 2.2.8
+
+  prosemirror-markdown@1.13.4:
+    dependencies:
+      '@types/markdown-it': 14.1.2
+      markdown-it: 14.1.1
+      prosemirror-model: 1.25.4
+
+  prosemirror-menu@1.3.0:
+    dependencies:
+      crelt: 1.0.6
+      prosemirror-commands: 1.7.1
+      prosemirror-history: 1.5.0
+      prosemirror-state: 1.4.4
+
+  prosemirror-model@1.25.4:
+    dependencies:
+      orderedmap: 2.1.1
+
+  prosemirror-schema-basic@1.2.4:
+    dependencies:
+      prosemirror-model: 1.25.4
+
+  prosemirror-schema-list@1.5.1:
+    dependencies:
+      prosemirror-model: 1.25.4
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+
+  prosemirror-state@1.4.4:
+    dependencies:
+      prosemirror-model: 1.25.4
+      prosemirror-transform: 1.12.0
+      prosemirror-view: 1.41.7
+
+  prosemirror-tables@1.8.5:
+    dependencies:
+      prosemirror-keymap: 1.2.3
+      prosemirror-model: 1.25.4
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+      prosemirror-view: 1.41.7
+
+  prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7):
+    dependencies:
+      '@remirror/core-constants': 3.0.0
+      escape-string-regexp: 4.0.0
+      prosemirror-model: 1.25.4
+      prosemirror-state: 1.4.4
+      prosemirror-view: 1.41.7
+
+  prosemirror-transform@1.12.0:
+    dependencies:
+      prosemirror-model: 1.25.4
+
+  prosemirror-view@1.41.7:
+    dependencies:
+      prosemirror-model: 1.25.4
+      prosemirror-state: 1.4.4
+      prosemirror-transform: 1.12.0
+
   proto-list@1.2.4: {}
 
   proxy-from-env@2.1.0: {}
@@ -18624,6 +19268,8 @@ snapshots:
       picocolors: 1.1.1
       sade: 1.8.1
 
+  punycode.js@2.3.1: {}
+
   punycode@2.3.1: {}
 
   pupa@3.3.0:
@@ -18939,6 +19585,8 @@ snapshots:
       '@rollup/rollup-win32-x64-msvc': 4.60.1
       fsevents: 2.3.3
 
+  rope-sequence@1.3.4: {}
+
   run-applescript@7.1.0: {}
 
   run-parallel@1.2.0:
@@ -19835,6 +20483,8 @@ snapshots:
 
   typescript@5.9.3: {}
 
+  uc.micro@2.1.0: {}
+
   ufo@1.6.3: {}
 
   ultrahtml@1.6.0: {}
@@ -20439,6 +21089,8 @@ snapshots:
     transitivePeerDependencies:
       - vue
 
+  w3c-keyname@2.2.8: {}
+
   warning@4.0.3:
     dependencies:
       loose-envify: 1.4.0

+ 11 - 0
pnpm-workspace.yaml

@@ -47,6 +47,17 @@ catalog:
   '@tailwindcss/vite': ^4.2.2
   '@tanstack/vue-query': ^5.95.2
   '@tanstack/vue-store': ^0.9.3
+  '@tiptap/core': ^3.21.0
+  '@tiptap/extension-doc': ^3.21.0
+  '@tiptap/extension-highlight': ^3.21.0
+  '@tiptap/extension-image': ^3.21.0
+  '@tiptap/extension-link': ^3.21.0
+  '@tiptap/extension-placeholder': ^3.21.0
+  '@tiptap/extension-text-align': ^3.21.0
+  '@tiptap/extension-text-style': ^3.21.0
+  '@tiptap/extension-underline': ^3.21.0
+  '@tiptap/starter-kit': ^3.21.0
+  '@tiptap/vue-3': ^3.21.0
   '@tsdown/css': ^0.21.7
   '@types/archiver': ^7.0.0
   '@types/html-minifier-terser': ^7.0.2