From 0be534a70b969829b912d178b0e4994c486b9d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Rolf?= Date: Sun, 27 Sep 2026 11:55:00 +0200 Subject: [PATCH 1/4] feat(ui): RichTextEditor + RichText (TipTap), moved from AbstimmBAR Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 28 ++ packages/ui/README.md | 85 ++++ packages/ui/package-lock.json | 709 ++++++++++++++++++++++++++++- packages/ui/package.json | 14 +- packages/ui/src/RichText.tsx | 29 ++ packages/ui/src/RichTextEditor.tsx | 271 +++++++++++ packages/ui/src/index.ts | 5 + 7 files changed, 1131 insertions(+), 10 deletions(-) create mode 100644 packages/ui/src/RichText.tsx create mode 100644 packages/ui/src/RichTextEditor.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index d76abe7..2e72801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ Changelog ist die Upgrade-Anleitung für die Tools. ## [Unreleased] +### @basicbar/ui (→ wird `ui/v0.4.0`) + +**`RichTextEditor` + `RichText`** (modulierbar#5), aus AbstimmBAR verschoben: +der eine WYSIWYG-Editor (TipTap) für formatierte Langtext-Felder — Fett, +Kursiv, Aufzählungen, nummerierte Listen, Link, Überschriften (H2/H3) und +optional Bilder — plus `RichText` zum Rendern des gespeicherten HTML. +Bilder (Toolbar-Button, Drag&Drop, Einfügen aus der Zwischenablage) gibt es +nur, wenn die App `onUploadImage(file) => Promise` übergibt (relative +URL, z. B. `/media/rich/x.png`); ohne die Prop bleiben bestehende ``- +Inhalte sichtbar, es lassen sich nur keine neuen einfügen. Auf +Upload-Fehler zeigt der Editor `t("Image upload failed")` per +`window.alert` und lässt den Inhalt unverändert. Passt zum +Sanitizing-Vertrag von `basicbar_integrations.html_sanitize.clean_html` +(oder einem gleichwertigen Allowlist-Sanitizer im Tool) — die gespeicherte +HTML wird dort auf genau dieses Subset reduziert. + +Neue Dependencies: `@tiptap/react`, `@tiptap/pm`, `@tiptap/starter-kit`, +`@tiptap/extension-link`, `@tiptap/extension-image`, +`@tiptap/extension-bold` (alle `^3.27.2`). + +Migration: keine für Tools, die die Komponente noch nicht nutzen — additiv. +Ein Tool, das den Editor einsetzt, übernimmt `RichTextEditor`/`RichText` +aus `@basicbar/ui` statt einer lokalen Kopie (z. B. via +`TranslatableField`s `renderInput`) und ergänzt die Übersetzungs-Keys +`"Bold"`, `"Italic"`, `"Heading (large)"`, `"Heading (small)"`, +`"Bulleted list"`, `"Numbered list"`, `"Link"`, `"Enter URL"`, +`"Insert image (or drag and drop)"`, `"Image upload failed"`. + ### @basicbar/ui (→ wird `ui/v0.3.1`) **Globaler Sprach-Umschalter** (modulierbar#99): Der schwebende diff --git a/packages/ui/README.md b/packages/ui/README.md index 45d5eb9..a0cb540 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -49,3 +49,88 @@ initI18n({ resources: { en, de } }); Build: `npm install && npm run build` (tsup → `dist/`). Distribution als npm-Tarball über ein GitHub-Release-Asset (siehe ADR-0002, ADR-0004 und Repo-CI). + +## Rich text + +`RichTextEditor` (TipTap) + `RichText` — der eine WYSIWYG-Editor für +formatierte Langtext-Felder (Fett, Kursiv, Aufzählungen, nummerierte Listen, +Link, Überschriften H2/H3, optional Bilder) und die passende Renderkomponente +für das gespeicherte HTML. Aus AbstimmBAR in die Basis verschoben +(modulierbar#5), damit alle -bar-Tools eine Implementierung teilen. + +**Sanitizing-Vertrag:** die Komponenten selbst sanitizen nichts — die +Sicherheitsgrenze ist das Backend. Jedes Rich-Text-Feld muss beim Speichern +(und beim Import) durch einen Allowlist-Sanitizer laufen, z. B. +`basicbar_integrations.html_sanitize.clean_html`, der auf genau das Subset +reduziert, das der Editor erzeugt (`p`, `strong`, `em`, `h2`, `h3`, `ul`, +`ol`, `li`, `a[href,rel]`, `img[src]`, …). Bild-URLs müssen relativ sein +(`/media/…`) — der Editor fügt nur ein, was `onUploadImage` zurückgibt, ohne +es zu validieren. + +**Editor ohne Bilder** (kein Upload-Endpunkt verdrahtet — kein Bild-Button, +Drag&Drop/Einfügen aus der Zwischenablage werden ignoriert; bestehende +``-Inhalte bleiben trotzdem sichtbar): + +```tsx +import { RichTextEditor } from "@basicbar/ui"; + + +``` + +**Editor mit Bildern** — `onUploadImage` lädt hoch und liefert die relative +URL; scheitert der Upload, zeigt der Editor `t("Image upload failed")` per +`window.alert` und lässt den Inhalt unverändert: + +```tsx + { + const { url } = await api.uploadRichImage(file); + return url; // z. B. "/media/rich/x.png" + }} + id="description-editor" + ariaLabel={t("Description")} +/> +``` + +**Rendern** des serverseitig sanitisierten HTML: + +```tsx +import { RichText } from "@basicbar/ui"; + + +// eigene Klassen statt des Prosa-Defaults (ersetzt, nicht ergänzt): + +``` + +**Integration in `TranslatableField`** über `renderInput` (pro Sprache ein +Editor, mit `format="html"` bleiben die Ausgefüllt-Punkte markup-blind und +die Maschinenübersetzung erhält die Tags): + +```tsx + setField(`description_${lang}`, html)} + format="html" + renderInput={({ value, onChange, id }) => ( + + )} +/> +``` + +**Übersetzungs-Keys**, die das Tool bereitstellen muss (Englisch als Key, +siehe `initI18n`): `"Bold"`, `"Italic"`, `"Heading (large)"`, +`"Heading (small)"`, `"Bulleted list"`, `"Numbered list"`, `"Link"`, +`"Enter URL"`, `"Insert image (or drag and drop)"`, +`"Image upload failed"`. diff --git a/packages/ui/package-lock.json b/packages/ui/package-lock.json index 502cdea..778b739 100644 --- a/packages/ui/package-lock.json +++ b/packages/ui/package-lock.json @@ -1,19 +1,29 @@ { "name": "@basicbar/ui", - "version": "0.2.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@basicbar/ui", - "version": "0.2.1", + "version": "0.4.0", "license": "Apache-2.0", + "dependencies": { + "@tiptap/extension-bold": "^3.27.2", + "@tiptap/extension-image": "^3.27.2", + "@tiptap/extension-link": "^3.27.2", + "@tiptap/pm": "^3.27.2", + "@tiptap/react": "^3.27.2", + "@tiptap/starter-kit": "^3.27.2" + }, "devDependencies": { "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", "i18next": "^23.16.0", "i18next-browser-languagedetector": "^8.0.0", "lucide-react": "^0.469.0", "react": "^18.3.0", + "react-dom": "^18.3.0", "react-i18next": "^15.1.0", "tsup": "^8.3.0", "typescript": "^5.6.0" @@ -478,6 +488,34 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT", + "optional": true + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -906,6 +944,449 @@ "win32" ] }, + "node_modules/@tiptap/core": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.31.3.tgz", + "integrity": "sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.31.3.tgz", + "integrity": "sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.31.3.tgz", + "integrity": "sha512-dIuYhKk8TitKU/FeDpoTeWZhU42YgDN5npgWNjAmMmRktPdoxnH3/wGSiwXlqZgJWjehN7kPWDePWpJeAImGpQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.31.3.tgz", + "integrity": "sha512-EV6ZnwKc++2OM/OcD54n8s1B7C9LP7GKAtdEPwu0t3BYJf3sG8Ueoinf7SgGPO9oMPg96GneOkDNm1urMV167g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.31.3.tgz", + "integrity": "sha512-qEyyoPapPef4LO8XKaN83bxtaNzkJ4kFn/IxLnEKd4BJ3Mvi4MH2yJYlyDqwFnMoev4pMA1zBHRAt/C0THRmZw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.31.3.tgz", + "integrity": "sha512-SzxOqchrD2AcN3uT67PjKmRFEMOU3vNNiwNaamZJUbZrI6Hmy+bvdHJrc3jrIddyyCrAtsnAfRI0cmorW1jGfg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.31.3.tgz", + "integrity": "sha512-nvknt4FhyJQjYcvxptmeUlFsIAc8ibua3E5BN4Pim374/9RWepH4cdE9X0/qUTtguHElLx0iKtSIY3rW2qGTFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.31.3.tgz", + "integrity": "sha512-EexgmqnyDNyGlISxo7SMrp5MygpJYmqD+0cY5jB6L1U6L4CpKKRWUt8OO1sWzEHDE1+TTvwt+WIFoIWAziOtEA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.31.3.tgz", + "integrity": "sha512-NWomSfu5CSC7VacnMSDzKT8qm66SzMfZwVPEtwY5bPpRTJgTiT1rNK0neDrrzfMN27MfylGyKWWf7Q5Qf8w/fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.31.3" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.31.3.tgz", + "integrity": "sha512-rd4VJ9PGSP9Eop8ZTEwaLZcMzMXLuJKe3hUNf58rq+zANpwM+9fI+vB7g9MAc3eXwUNDxNDVACFIL2oeBqpQ3A==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.31.3.tgz", + "integrity": "sha512-EBXKb1FrVStsNYCcRGtd9jmzveCvR+eqgg1rVqoONrqFK6U7bga6LN+1dMKroP1kliDVgveYkP3vRYxqw+rFqg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.31.3" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.31.3.tgz", + "integrity": "sha512-QAdCvNO4+yW9ATwsrej11NTkDYFqPLIEQr3ARNrKOK1qaiS7A0fia2SEukb/hrkP3A6mbozhoQt2r2RGUf/DpQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.31.3.tgz", + "integrity": "sha512-rk5VHMAeQcg06SLauN6EGdD2jc0O2qY8QkZYPd0LxNvLblw2BxBx+lxUQSYwLAT9Ie5914gKIK2YbRyO2Ts3ig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.31.3.tgz", + "integrity": "sha512-YnHGy2KShRwvCseAmmxl9VP7R0qaj8QMp3DA6DJWZqp7r5gLGvDkAqhedxqqefqsE4Y43hjkBdjtB9Ce78LkIw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.31.3.tgz", + "integrity": "sha512-wWNG9BOtx2Cg4vwxPVGDIJ5DGX+ETkldeoaFJLB1NXUL4OPUiVTf8cHZ+hdYgJWP704BEB7jQgjlpvdi6VigTg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.31.3.tgz", + "integrity": "sha512-ibGvdvAPyfxBMUVNRI43eb9h2/Jka1MRG5GtnGqbcCX/2+Y/y0EOfFrPQPkuYphiWQYyu9+FzPKMB/pjuaLuKQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.31.3.tgz", + "integrity": "sha512-986wOQzTL9Zr5lf84LCLpm+YOms8A0K39/8DVoqRfebqcOe0/eq4bnztmAlfabOd+kJY92g3AgZERFUx/w+dcw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.31.3.tgz", + "integrity": "sha512-LoveGnC0FVdCV4jUNBaG1ZA+KWE07+adzV3kGy6uUYFcJEjbVUHTnPDrBOob3IwOSO3sCwIkvQb6EVYeXn/4yg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.31.3.tgz", + "integrity": "sha512-4QlKOriJJMvg95QJTEsy9BUYPBQ6UvJyb8WURRwdUtQUkkqb8h32lg/eyQUv2FzW9IT1AdUyNZfVaxH64kRfQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.31.3.tgz", + "integrity": "sha512-If8UOEdDZbPJU6iYTvLtH6DOp2KBy6BKxg9UELL1AevVetGHEF/7lW8hP50Gn1tHMVpPRqmhSzVrJRpEJJgb/Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.31.3.tgz", + "integrity": "sha512-mp3g11NgA/PYu8rj7J7Ez3l4qBy6WfTSmHIG4PZvEGG5w2oUAIkgb9DU7nPPzjmeme27oazFYZw+AtZA0+u4tw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.31.3" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.31.3.tgz", + "integrity": "sha512-+iPku7wJfy5hbNDNLX8dveFtYVsZMmh7vztjuq8hT3mipSC4IByDehDbU8fzUCjfXiEzmI7mQn7c8LGmHULuzA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.31.3.tgz", + "integrity": "sha512-G29bhKttYwcKHT+BI6emWVFol3RO/gUXxQVcmr/iT8LXXy7j8J6HFUnsKM+Kg5YlP1rxMRgsa65dbrQVahZi0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.31.3.tgz", + "integrity": "sha512-gdsWtF+taeaCu6V+5Ct10fGo0ACUy1GnYtbb+mcathBt8OqbT+Ws60p/yEmKesBDz2Hn+B5IcWy6+2BBZl5ZTg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.31.3.tgz", + "integrity": "sha512-HghdJaOwRqYzsAxqSyNyb+IWyOMcdCl8IoiBETA9BZCJAqdXzFLcuWp7CqCqJPam9dgkWosSoHqTCAO4nTBfpw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.31.3.tgz", + "integrity": "sha512-8sJNPGGUe8f3aDojcOW5cfVL7I5NrBbE0UWxG08qoi9Tea6qWbvQJsCR9tsrOapr/DaLr3kpbGZ9s1gEEfcNcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.31.3.tgz", + "integrity": "sha512-sZime0SWsz/k62W2WvHx5Ig7G2h7kVhrrmnqy+wEgIHfDwEfOlelRjaWCiBCFlF7dxGUntJusCh9FxlLhni0Ag==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.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-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.42.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.31.3.tgz", + "integrity": "sha512-QiwQqvaLFLm5EMFu5tg7nAgXJxCUiUTLD8EsK+TqVV5P4bqOoMOCM39khbhXTJyahCuYpiFWx5YOSDtC/JiPtg==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.31.3", + "@tiptap/extension-floating-menu": "^3.31.3" + }, + "peerDependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/pm": "3.31.3", + "@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" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.31.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.31.3.tgz", + "integrity": "sha512-WKof9RewdmGHvWJ1wn0/HVNG2mV+HOgVRyJkKekuM9fgr6BZAAH/xZsWE1eon+94JnQ+KtK2ThydXQM/qc6b2A==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "3.31.3", + "@tiptap/extension-blockquote": "3.31.3", + "@tiptap/extension-bold": "3.31.3", + "@tiptap/extension-bullet-list": "3.31.3", + "@tiptap/extension-code": "3.31.3", + "@tiptap/extension-code-block": "3.31.3", + "@tiptap/extension-document": "3.31.3", + "@tiptap/extension-dropcursor": "3.31.3", + "@tiptap/extension-gapcursor": "3.31.3", + "@tiptap/extension-hard-break": "3.31.3", + "@tiptap/extension-heading": "3.31.3", + "@tiptap/extension-horizontal-rule": "3.31.3", + "@tiptap/extension-italic": "3.31.3", + "@tiptap/extension-link": "3.31.3", + "@tiptap/extension-list": "3.31.3", + "@tiptap/extension-list-item": "3.31.3", + "@tiptap/extension-list-keymap": "3.31.3", + "@tiptap/extension-ordered-list": "3.31.3", + "@tiptap/extension-paragraph": "3.31.3", + "@tiptap/extension-strike": "3.31.3", + "@tiptap/extension-text": "3.31.3", + "@tiptap/extension-underline": "3.31.3", + "@tiptap/extensions": "3.31.3", + "@tiptap/pm": "3.31.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -917,20 +1398,33 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -1024,7 +1518,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -1087,6 +1580,15 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/fast-equals": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.3.tgz", + "integrity": "sha512-8unmvA0qrfpFWBYUate9BTGtQdKHWBYt+KdAIkag7Vvxe7wz8Q2nJg7fLMC0bJqe0bjP31kca7Ckaawrs4Istg==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1190,7 +1692,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/lilconfig": { @@ -1213,6 +1714,12 @@ "dev": true, "license": "MIT" }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -1227,7 +1734,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -1298,6 +1804,12 @@ "node": ">=0.10.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1390,11 +1902,149 @@ } } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.4.tgz", + "integrity": "sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.4.tgz", + "integrity": "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.12", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.12.tgz", + "integrity": "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.2.tgz", + "integrity": "sha512-PE/aY0HEY4zczvmqrilgkUK/WautF0chvMqkmM/iN5/aPHvwrCWaQZjS6CIZp+K84YrvPFqrL+YArrfJX8xX7g==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.6", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.6.tgz", + "integrity": "sha512-3g25f6hKV2bBxo3puH3EcYWmmVNSH0iWmNsMVT7sugDSpA025jgpOLarcDWr39SdzPjYMaiYtRdNtXialPSjEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.12", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -1403,6 +2053,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-i18next": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", @@ -1499,6 +2162,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -1670,6 +2348,15 @@ "dev": true, "license": "MIT" }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -1679,6 +2366,12 @@ "engines": { "node": ">=0.10.0" } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" } } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 48fb2d0..30a3176 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@basicbar/ui", - "version": "0.3.1", - "description": "Design-System-Basis der virtUOS -bar-Tools: Tailwind-Preset, Basis-Styles, Theme (Dark Mode), i18n-Bootstrap, contentLang", + "version": "0.4.0", + "description": "Design-System-Basis der virtUOS -bar-Tools: Tailwind-Preset, Basis-Styles, Theme (Dark Mode), i18n-Bootstrap, contentLang, RichTextEditor/RichText (TipTap)", "license": "Apache-2.0", "author": "Universität Osnabrück (virtUOS)", "type": "module", @@ -41,12 +41,22 @@ "react": ">=18", "react-i18next": ">=14" }, + "dependencies": { + "@tiptap/extension-bold": "^3.27.2", + "@tiptap/extension-image": "^3.27.2", + "@tiptap/extension-link": "^3.27.2", + "@tiptap/pm": "^3.27.2", + "@tiptap/react": "^3.27.2", + "@tiptap/starter-kit": "^3.27.2" + }, "devDependencies": { "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", "i18next": "^23.16.0", "i18next-browser-languagedetector": "^8.0.0", "lucide-react": "^0.469.0", "react": "^18.3.0", + "react-dom": "^18.3.0", "react-i18next": "^15.1.0", "tsup": "^8.3.0", "typescript": "^5.6.0" diff --git a/packages/ui/src/RichText.tsx b/packages/ui/src/RichText.tsx new file mode 100644 index 0000000..58eabe5 --- /dev/null +++ b/packages/ui/src/RichText.tsx @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Universität Osnabrück (virtUOS) + +/** Renders server-sanitized rich HTML (an allowlist such as + * `basicbar_integrations.html_sanitize.clean_html`). The backend is the + * security boundary: every rich field is cleaned on save/import, so the + * stored HTML is safe to inject here. Pairs with `RichTextEditor`. */ + +export interface RichTextProps { + html: string; + className?: string; +} + +export function RichText({ html, className }: RichTextProps) { + // A caller-supplied className fully REPLACES the default styling (it is not + // appended), so a migrated call site can render byte-identically to its + // previous markup — some sites use large-display classes (text-3xl, + // [&_img]:max-h-64, [&_ul]:pl-8) that would clash with the prose default. + // Sites that pass nothing get a sane prose default. + const cls = + className ?? + "text-slate-700 dark:text-slate-300 [&_img]:max-w-full [&_img]:h-auto [&_p]:my-2 " + + "[&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:my-0.5 " + + "[&_h2]:mt-4 [&_h2]:mb-1 [&_h2]:text-xl [&_h2]:font-bold [&_h2]:text-slate-900 dark:[&_h2]:text-slate-100 " + + "[&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-base [&_h3]:font-semibold [&_h3]:text-slate-900 dark:[&_h3]:text-slate-100 " + + "[&_a]:font-medium [&_a]:text-brand-700 [&_a]:underline dark:[&_a]:text-brand-300 " + + "[&_strong]:font-semibold [&_em]:italic"; + return
; +} diff --git a/packages/ui/src/RichTextEditor.tsx b/packages/ui/src/RichTextEditor.tsx new file mode 100644 index 0000000..981f318 --- /dev/null +++ b/packages/ui/src/RichTextEditor.tsx @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Universität Osnabrück (virtUOS) + +/** The shared WYSIWYG editor for formatted long-form fields across the -bar + * tools: bold, italic, lists, link, headings (H2/H3), and — when the host + * app wires up `onUploadImage` — images. Stores HTML; the backend sanitizes + * to exactly this subset (e.g. `basicbar_integrations.html_sanitize.clean_html`) + * — anything else is stripped. Short fields (titles, options) stay plain and + * should not use this. Moved from AbstimmBAR (#49) so all -bar tools share + * one implementation. */ +// Bold ships inside @tiptap/starter-kit (same pinned version); we import it +// directly only to override its parse rules — no extra dependency added. +import Bold from "@tiptap/extension-bold"; +import Image from "@tiptap/extension-image"; +import Link from "@tiptap/extension-link"; +import { EditorContent, useEditor, type Editor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { + Bold as BoldIcon, + Heading2, + Heading3, + ImagePlus, + Italic as ItalicIcon, + Link as LinkIcon, + List, + ListOrdered, +} from "lucide-react"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +/** Bold that only recognises explicit / tags. TipTap's default + * also parses any `font-weight: 500–900` as bold — so text typed after a + * DOM reparse or pasted from Word/PDF turned bold on its own. We bold only + * via the toolbar (which emits , the one tag the backend keeps). */ +const PlainBold = Bold.extend({ + parseHTML() { + return [{ tag: "strong" }, { tag: "b" }]; + }, +}); + +export interface RichTextEditorProps { + value: string; + onChange: (html: string) => void; + /** Upload an image and resolve to its relative URL (e.g. "/media/rich/x.png"). + * Omit to disable images (no button; pasted/dropped files ignored). */ + onUploadImage?: (file: File) => Promise; + ariaLabel?: string; + id?: string; +} + +function ToolbarButton({ + active, + label, + onClick, + children, +}: { + active?: boolean; + label: string; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +export function RichTextEditor({ + value, + onChange, + onUploadImage, + ariaLabel, + id, +}: RichTextEditorProps) { + const { t } = useTranslation(); + + async function insertImageFile(editor: Editor, file: File, pos?: number) { + if (!onUploadImage) return; + let url: string; + try { + url = await onUploadImage(file); + } catch { + window.alert(t("Image upload failed")); + return; + } + const chain = editor.chain().focus(); + if (pos !== undefined) chain.insertContentAt(pos, { type: "image", attrs: { src: url } }); + else chain.setImage({ src: url }); + chain.run(); + } + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + heading: { levels: [2, 3] }, + blockquote: false, + codeBlock: false, + code: false, + horizontalRule: false, + link: false, + bold: false, + }), + PlainBold, + // Links may point anywhere; the backend forces rel="noopener". We do not + // open a new tab, and we do not auto-link typed URLs (toolbar only). + Link.configure({ openOnClick: false, autolink: false, HTMLAttributes: { rel: "noopener" } }), + // Kept unconditionally so existing content survives even when + // this instance has no onUploadImage (view-only fields, other tools' + // legacy content, …) — only insertion is gated on onUploadImage. + Image, + ], + content: value, + onUpdate: ({ editor }) => onChange(editor.getHTML()), + editorProps: { + attributes: { + ...(id ? { id } : {}), + ...(ariaLabel ? { "aria-label": ariaLabel } : {}), + role: "textbox", + "aria-multiline": "true", + class: + // Match the single-line inputs' ink so typed text is crisp in both + // themes (the editor otherwise inherits a dim slate-400/500). + "text-slate-900 dark:text-slate-100 " + + // The editable fills the frame edge-to-edge, so the global + // :focus-visible ring (base.css) would bulge out past the border and + // look wider than the toolbar. Suppress it — the container's + // focus-within:border-brand-600 already signals focus. + "min-h-28 max-h-[420px] overflow-y-auto rounded-b-lg px-3 py-2 " + + "focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 " + + "[&_img]:max-w-full [&_img]:h-auto [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 " + + "[&_p]:my-1 [&_li]:my-0.5 [&_h2]:mt-2 [&_h2]:mb-1 [&_h2]:text-xl [&_h2]:font-bold " + + "[&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-base [&_h3]:font-semibold " + + "[&_a]:text-brand-700 [&_a]:underline dark:[&_a]:text-brand-300", + }, + handleDrop: (view, event) => { + if (!onUploadImage) return false; + const file = event.dataTransfer?.files?.[0]; + if (file && file.type.startsWith("image/") && editor) { + event.preventDefault(); + const at = view.posAtCoords({ left: event.clientX, top: event.clientY }); + void insertImageFile(editor, file, at?.pos); + return true; + } + return false; + }, + handlePaste: (_view, event) => { + if (!onUploadImage) return false; + const file = Array.from(event.clipboardData?.files ?? []).find((f) => + f.type.startsWith("image/"), + ); + if (file && editor) { + event.preventDefault(); + void insertImageFile(editor, file); + return true; + } + return false; + }, + }, + }); + + // Sync when the value changes from OUTSIDE (e.g. applying an AI rephrasing, + // or switching the active language tab). The guard avoids clobbering the + // cursor during normal typing (there the stored value already equals the + // editor's HTML). emitUpdate:false so this settles without a second round + // that would reset the caret to the top while the user is typing (#50). + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value, { emitUpdate: false }); + } + }, [editor, value]); + + if (!editor) return null; + + function setLink() { + if (!editor) return; + const previous = (editor.getAttributes("link").href as string) || ""; + const url = window.prompt(t("Enter URL"), previous); + if (url === null) return; // cancelled + if (url === "") { + editor.chain().focus().extendMarkRange("link").unsetLink().run(); + return; + } + editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run(); + } + + return ( +
+
+ editor.chain().focus().toggleBold().run()} + > + + + editor.chain().focus().toggleItalic().run()} + > + + + editor.chain().focus().toggleHeading({ level: 2 }).run()} + > + + + editor.chain().focus().toggleHeading({ level: 3 }).run()} + > + + + editor.chain().focus().toggleBulletList().run()} + > + + + editor.chain().focus().toggleOrderedList().run()} + > + + + + + + {onUploadImage && ( + + )} +
+ +
+ ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7f61105..f50a719 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -21,6 +21,11 @@ export type { export { TranslatableField } from "./TranslatableField"; export type { RenderInputArgs, TranslatableFieldProps } from "./TranslatableField"; +export { RichTextEditor } from "./RichTextEditor"; +export type { RichTextEditorProps } from "./RichTextEditor"; +export { RichText } from "./RichText"; +export type { RichTextProps } from "./RichText"; + export { defaultContentLangLabel, getDefaultContentLang, From 10259885fbb0be989c883e876ecc247ea1544c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Rolf?= Date: Sun, 27 Sep 2026 12:01:37 +0200 Subject: [PATCH 2/4] fix(ui): drop external images from pasted HTML; react-dom peer dependency Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 ++++++- packages/ui/README.md | 12 ++++++++++++ packages/ui/package.json | 1 + packages/ui/src/RichTextEditor.tsx | 26 +++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e72801..7e4e3ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,12 @@ Upload-Fehler zeigt der Editor `t("Image upload failed")` per `window.alert` und lässt den Inhalt unverändert. Passt zum Sanitizing-Vertrag von `basicbar_integrations.html_sanitize.clean_html` (oder einem gleichwertigen Allowlist-Sanitizer im Tool) — die gespeicherte -HTML wird dort auf genau dieses Subset reduziert. +HTML wird dort auf genau dieses Subset reduziert. Eingefügtes Rich-HTML +(z. B. aus einer Webseite kopiert) wird per `transformPastedHTML` gefiltert: +jedes ``, dessen `src` nicht mit `/media/` beginnt, wird entfernt — +ohne `onUploadImage` wird jedes eingefügte `` entfernt. So kann kein +externes Bild am Upload-Flow vorbei ins Dokument gelangen; vorhandene Bilder +im initialen `value` bleiben unangetastet. Neue Dependencies: `@tiptap/react`, `@tiptap/pm`, `@tiptap/starter-kit`, `@tiptap/extension-link`, `@tiptap/extension-image`, diff --git a/packages/ui/README.md b/packages/ui/README.md index a0cb540..01fc967 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -98,6 +98,18 @@ URL; scheitert der Upload, zeigt der Editor `t("Image upload failed")` per /> ``` +**Bilder aus eingefügtem HTML werden gefiltert, nicht nur Datei-Paste/-Drop:** +Fügt man Rich-HTML aus einer Webseite ein (Browser-Copy&Paste, nicht als +Datei), landet es über ProseMirrors HTML-Parser im Dokument — ein +`` würde sonst am `onUploadImage`-Flow vorbei direkt +eingefügt und wäre nach dem Speichern ein kaputtes `` (der Backend- +Sanitizer erlaubt nur `/media/…`-Quellen). Der Editor filtert deshalb per +`transformPastedHTML` jedes eingefügte ``, dessen `src` nicht mit +`/media/` beginnt; ohne `onUploadImage` wird jedes eingefügte `` +entfernt (Bilder sind dann vollständig deaktiviert). Das betrifft nur +eingefügtes HTML — vorhandene Bilder im initialen `value` bleiben +unangetastet. + **Rendern** des serverseitig sanitisierten HTML: ```tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 30a3176..a8d163f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -39,6 +39,7 @@ "i18next-browser-languagedetector": ">=7", "lucide-react": ">=0.400", "react": ">=18", + "react-dom": ">=18", "react-i18next": ">=14" }, "dependencies": { diff --git a/packages/ui/src/RichTextEditor.tsx b/packages/ui/src/RichTextEditor.tsx index 981f318..c6ef134 100644 --- a/packages/ui/src/RichTextEditor.tsx +++ b/packages/ui/src/RichTextEditor.tsx @@ -42,7 +42,14 @@ export interface RichTextEditorProps { value: string; onChange: (html: string) => void; /** Upload an image and resolve to its relative URL (e.g. "/media/rich/x.png"). - * Omit to disable images (no button; pasted/dropped files ignored). */ + * Omit to disable images (no button; dropped/pasted image files ignored). + * Pasting rich HTML (e.g. copied from a web page) is gated too: any + * `` whose `src` does not start with "/media/" is stripped from the + * pasted content — so an external image can never sneak in through paste, + * bypassing the upload flow and the "/media/…"-only contract the backend + * enforces. With this prop omitted, every pasted `` is stripped. + * Existing images already in `value` are unaffected — this only filters + * newly pasted HTML, not the initial/external content load. */ onUploadImage?: (file: File) => Promise; ariaLabel?: string; id?: string; @@ -166,6 +173,23 @@ export function RichTextEditor({ } return false; }, + // Rich HTML paste (e.g. copied from a web page) goes through ProseMirror's + // HTML parser, not handlePaste above — so an would + // otherwise slip straight into the document without ever calling + // onUploadImage, and without the backend's "/media/…"-only contract + // catching it until save (leaving a broken bare behind). Strip + // every pasted that isn't already a "/media/…" reference; strip + // all of them when onUploadImage is omitted (images disabled entirely). + // Only pasted HTML passes through here — the initial `content: value` + // load below never calls this, so existing images are unaffected. + transformPastedHTML: (html) => { + const doc = new DOMParser().parseFromString(html, "text/html"); + doc.body.querySelectorAll("img").forEach((img) => { + const src = img.getAttribute("src") ?? ""; + if (!onUploadImage || !src.startsWith("/media/")) img.remove(); + }); + return doc.body.innerHTML; + }, }, }); From 90cf535c8bf911b4daa60a4cb2cad258ac639095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Rolf?= Date: Sun, 27 Sep 2026 13:24:41 +0200 Subject: [PATCH 3/4] fix(ui): editor offers only sanitizer-kept formatting; label wiring; history-safe sync Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 25 +++++++++++++---- packages/ui/README.md | 26 +++++++++++++----- packages/ui/src/RichTextEditor.tsx | 39 +++++++++++++++++++++------ packages/ui/src/TranslatableField.tsx | 10 +++++-- 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4e3ed..e0b9c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,15 +30,30 @@ im initialen `value` bleiben unangetastet. Neue Dependencies: `@tiptap/react`, `@tiptap/pm`, `@tiptap/starter-kit`, `@tiptap/extension-link`, `@tiptap/extension-image`, -`@tiptap/extension-bold` (alle `^3.27.2`). +`@tiptap/extension-bold` (alle `^3.27.2`); `react-dom` (`>=18`) ist jetzt +zusätzlich zu `react` als Peer-Dependency deklariert (`@tiptap/react` +importiert es zur Laufzeit). + +Der Editor bietet bewusst nur das Subset an, das der Sanitizer behält: +Unterstreichen/Durchgestrichen sind nicht registriert und Links tragen kein +`target`/`rel` (das Backend erzwingt `rel="noopener"` ohnehin). Der externe +`value`-Sync (Sprachwechsel u. ä.) läuft jetzt außerhalb der Undo-History +(`setMeta("addToHistory", false)`), damit ein Undo direkt danach nicht in +den vorherigen Inhalt hineinspringt. `RichTextEditor` hat eine neue optionale +Prop `labelledBy` (Id eines externen `