From 6f2fe8612e495c5350c48f22bc0cb83e48afe4c7 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:21:25 +0000 Subject: [PATCH] feat(books): add rich text description formatting bar Replace the plain textarea for book descriptions with a Tiptap-based rich text editor in the metadata edit tab. Include HTML sanitization utilities to ensure safe rendering of user-provided descriptions. Closes #460 --- client/package.json | 4 + .../detail/tabs/EditMetadataTab.vue | 8 +- .../detail/tabs/RichDescriptionEditor.vue | 452 +++++++++++++ .../book/composables/useSafeHtml.test.ts | 5 +- .../features/book/composables/useSafeHtml.ts | 6 +- .../book/lib/description-html.test.ts | 29 + .../src/features/book/lib/description-html.ts | 26 + pnpm-lock.yaml | 592 ++++++++++++++++++ 8 files changed, 1110 insertions(+), 12 deletions(-) create mode 100644 client/src/features/book/components/detail/tabs/RichDescriptionEditor.vue create mode 100644 client/src/features/book/lib/description-html.test.ts create mode 100644 client/src/features/book/lib/description-html.ts diff --git a/client/package.json b/client/package.json index 5988815d8..bbd8e7157 100644 --- a/client/package.json +++ b/client/package.json @@ -43,6 +43,10 @@ "@tailwindcss/vite": "^4.3.1", "@tanstack/vue-table": "^8.21.3", "@tanstack/vue-virtual": "^3.13.29", + "@tiptap/extension-link": "^3.27.1", + "@tiptap/pm": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/vue-3": "^3.27.1", "@types/howler": "^2.2.13", "@vueuse/core": "^14.3.0", "canvas-confetti": "^1.9.4", diff --git a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue index 553ccc1c1..62dd2bf6c 100644 --- a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue +++ b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue @@ -18,6 +18,7 @@ import ChipInput from '@/components/ui/ChipInput.vue' import CoverEditorPanel from './CoverEditorPanel.vue' import MetadataSearchDrawer from './MetadataSearchDrawer.vue' import MetadataFieldLabel from './MetadataFieldLabel.vue' +import RichDescriptionEditor from './RichDescriptionEditor.vue' import SeriesMembershipEditor from './SeriesMembershipEditor.vue' import WriteAndRenameResultPanel from '../WriteAndRenameResultPanel.vue' import type { MetadataPatch } from '../../../composables/useMetadataDiff' @@ -1422,12 +1423,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) { multiline @toggle="handleLockToggle" > -