From b08615323c0560b89ce0dbb5b79d14960c7c0e33 Mon Sep 17 00:00:00 2001 From: Charlie404 Date: Wed, 2 Sep 2026 14:26:09 +0200 Subject: [PATCH] feat: more text formats in the WYSIWYG toolbar Adds strike, superscript, subscript and inline code. Strike gets its own button, the three others are grouped in a hover dropdown so the bubble still fits the minimum sidebar width. Also in the toolbar: - the link form gets a "New tab" checkbox, and stays open with a message when Tiptap rejects the URL instead of dropping what was typed - the color palette gets a "No color" swatch, so a color can be removed without clearing the rest of the formatting - the bubble keeps a small gap with the sidebar border when the selection is close to it, instead of being clamped flush against it Behaviour notes: - superscript and subscript exclude each other at the schema level, so the keyboard shortcuts cannot stack them either - inline code no longer excludes every other mark, it can be combined with bold, a link or a color like the other formats - links no longer get target="_blank" rel="nofollow" by default, these attributes are now set only when the checkbox is ticked @tiptap/extension-color is dropped: TextStyleKit already registers it, and the duplicate was logging a warning for every mounted editor. --- pnpm-lock.yaml | 65 +++++++++++-- visual-editor/package.json | 9 +- .../Editor/TiptapEditor/TiptapColorPicker.tsx | 41 ++++++-- .../Editor/TiptapEditor/TiptapEditor.tsx | 24 ++++- .../Editor/TiptapEditor/TiptapIcons.tsx | 64 +++++++++++++ .../Editor/TiptapEditor/TiptapToolbar.tsx | 94 ++++++++++++++++--- .../TiptapEditor/TiptapToolbarFormats.tsx | 66 +++++++++++++ 7 files changed, 332 insertions(+), 31 deletions(-) create mode 100644 visual-editor/src/components/Editor/TiptapEditor/TiptapToolbarFormats.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d2a5852..ce910510 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,9 +92,9 @@ importers: '@tiptap/extension-bullet-list': specifier: ^3.0.7 version: 3.0.7(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)) - '@tiptap/extension-color': + '@tiptap/extension-code': specifier: ^3.0.7 - version: 3.0.7(@tiptap/extension-text-style@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))) + version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)) '@tiptap/extension-document': specifier: ^3.0.7 version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)) @@ -125,6 +125,15 @@ importers: '@tiptap/extension-paragraph': specifier: ^3.0.7 version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)) + '@tiptap/extension-strike': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)) + '@tiptap/extension-subscript': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1) + '@tiptap/extension-superscript': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1) '@tiptap/extension-text': specifier: ^3.0.7 version: 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)) @@ -2018,56 +2027,67 @@ packages: resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.45.1': resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.45.1': resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.45.1': resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.45.1': resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.45.1': resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.45.1': resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.45.1': resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.45.1': resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.45.1': resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.45.1': resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} @@ -2227,10 +2247,10 @@ packages: peerDependencies: '@tiptap/extension-list': ^3.0.7 - '@tiptap/extension-color@3.0.7': - resolution: {integrity: sha512-75N0g6y4tga3rHNKplwHkF20n9fKhVrp3c7Gtk8nehckUMB2QuMf5Tr3fa3hVdSbgqxcP+dkOIUMoH1zKyjggA==} + '@tiptap/extension-code@3.0.7': + resolution: {integrity: sha512-6wdUqtXbnIuyKR7xteF2UCnsW2dLNtBKxWvAiOweA7L41HYvburh/tjbkffkNc5KP2XsKzdGbygpunwJMPj6+A==} peerDependencies: - '@tiptap/extension-text-style': ^3.0.7 + '@tiptap/core': ^3.0.7 '@tiptap/extension-document@3.0.7': resolution: {integrity: sha512-HJg1nPPZ9fv5oEMwpONeIfT0FjTrgNGuGAat/hgcBi/R2GUNir2/PM/3d6y8QtkR/EgkgcFakCc9azySXLmyUQ==} @@ -2296,6 +2316,23 @@ packages: peerDependencies: '@tiptap/core': ^3.0.7 + '@tiptap/extension-strike@3.0.7': + resolution: {integrity: sha512-WUCd5CMgS6pg0ZGKXsaxVrnEvO/h6XUehebL0yggAsRKSoGERInR2iLfhU4p1f4zk0cD3ydNLJdqZu0H/MIABw==} + peerDependencies: + '@tiptap/core': ^3.0.7 + + '@tiptap/extension-subscript@3.0.7': + resolution: {integrity: sha512-u+RGXNGs9H20JWKgduMA9zKUiRCpuuwZrTNY1yi5Xj881zzdVbUmLiAv1Op/3q1x1dWaAKPw65Bhg9590GvkcA==} + peerDependencies: + '@tiptap/core': ^3.0.7 + '@tiptap/pm': ^3.0.7 + + '@tiptap/extension-superscript@3.0.7': + resolution: {integrity: sha512-Ey424AdkKWBITNCxj9gmgaShgjlbdCy9jWTVRKRALLFlYKcXP2iUqN/OjYfIz/e468ZR1hl8XZigMWC5wQCyEQ==} + peerDependencies: + '@tiptap/core': ^3.0.7 + '@tiptap/pm': ^3.0.7 + '@tiptap/extension-text-align@3.0.7': resolution: {integrity: sha512-3XywI2+dvb5IIO5NnZqyoXrRnikGyCIN7wWymTI7eqD213Ovs19/sfbW5kXLPsBYiNBb12nbG6pmPhr21mPvoA==} peerDependencies: @@ -9838,9 +9875,9 @@ snapshots: dependencies: '@tiptap/extension-list': 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1) - '@tiptap/extension-color@3.0.7(@tiptap/extension-text-style@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)))': + '@tiptap/extension-code@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))': dependencies: - '@tiptap/extension-text-style': 3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1)) + '@tiptap/core': 3.0.7(@tiptap/pm@2.26.1) '@tiptap/extension-document@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))': dependencies: @@ -9896,6 +9933,20 @@ snapshots: dependencies: '@tiptap/core': 3.0.7(@tiptap/pm@2.26.1) + '@tiptap/extension-strike@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))': + dependencies: + '@tiptap/core': 3.0.7(@tiptap/pm@2.26.1) + + '@tiptap/extension-subscript@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)': + dependencies: + '@tiptap/core': 3.0.7(@tiptap/pm@2.26.1) + '@tiptap/pm': 2.26.1 + + '@tiptap/extension-superscript@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)': + dependencies: + '@tiptap/core': 3.0.7(@tiptap/pm@2.26.1) + '@tiptap/pm': 2.26.1 + '@tiptap/extension-text-align@3.0.7(@tiptap/core@3.0.7(@tiptap/pm@2.26.1))': dependencies: '@tiptap/core': 3.0.7(@tiptap/pm@2.26.1) diff --git a/visual-editor/package.json b/visual-editor/package.json index e71592c8..8d638a12 100644 --- a/visual-editor/package.json +++ b/visual-editor/package.json @@ -42,7 +42,7 @@ "@tiptap/extension-bold": "^3.0.7", "@tiptap/extension-bubble-menu": "^3.0.7", "@tiptap/extension-bullet-list": "^3.0.7", - "@tiptap/extension-color": "^3.0.7", + "@tiptap/extension-code": "^3.0.7", "@tiptap/extension-document": "^3.0.7", "@tiptap/extension-hard-break": "^3.0.7", "@tiptap/extension-heading": "^3.0.7", @@ -53,6 +53,9 @@ "@tiptap/extension-list-item": "^3.0.7", "@tiptap/extension-ordered-list": "^3.0.7", "@tiptap/extension-paragraph": "^3.0.7", + "@tiptap/extension-strike": "^3.0.7", + "@tiptap/extension-subscript": "^3.0.7", + "@tiptap/extension-superscript": "^3.0.7", "@tiptap/extension-text": "^3.0.7", "@tiptap/extension-text-align": "^3.0.7", "@tiptap/extension-text-style": "^3.0.7", @@ -72,6 +75,7 @@ "@types/react": "^18.2.25", "@types/react-datepicker": "^4.15.1", "@types/react-dom": "^18.2.10", + "@vitejs/plugin-react": "^4.7.0", "concurrently": "^7.6.0", "cypress": "^15.6.0", "esbuild": "^0.27.3", @@ -84,8 +88,7 @@ "tsc-alias": "^1.8.8", "typescript": "^4.9.5", "vite": "^7.3.1", - "vitest": "^3.2.4", - "@vitejs/plugin-react": "^4.7.0" + "vitest": "^3.2.4" }, "peerDependencies": { "react": "^18.0.0", diff --git a/visual-editor/src/components/Editor/TiptapEditor/TiptapColorPicker.tsx b/visual-editor/src/components/Editor/TiptapEditor/TiptapColorPicker.tsx index dfb40738..ff0d3a4a 100644 --- a/visual-editor/src/components/Editor/TiptapEditor/TiptapColorPicker.tsx +++ b/visual-editor/src/components/Editor/TiptapEditor/TiptapColorPicker.tsx @@ -5,6 +5,7 @@ import { colorToProperty } from 'src/functions/css' import styled from '@emotion/styled' import { prevent } from 'src/functions/functions' import { useToggle } from 'src/hooks/useToggle' +import { Styles } from 'src/components/ui' type Props = { editor: Editor @@ -19,9 +20,16 @@ export function TiptapColorPicker({ editor, colors }: Props) { ) as string[] const [expanded, toggleExpanded, setExpanded] = useToggle() - const handleChange = (color: string) => { + // A null color removes the color from the selection + const handleChange = (color: string | null) => { toggleExpanded() - editor.chain().focus().setColor(color).run() + const chain = editor.chain().focus() + if (color) { + chain.setColor(color) + } else { + chain.unsetColor() + } + chain.run() } useEffect(() => { @@ -36,7 +44,7 @@ export function TiptapColorPicker({ editor, colors }: Props) { return (
- ) @@ -194,7 +237,15 @@ function ToolbarButtons({ > + + + {formats + .filter((format) => format.name !== current.name) + .map((format) => ( + + ))} + + ) +}