diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01fdd4d..f580a50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,61 @@ Changelog ist die Upgrade-Anleitung für die Tools.
## [Unreleased]
+### @basicbar/ui (→ wird `ui/v0.7.0`)
+
+**TipTap raus aus den Bundles, die keinen Editor rendern** (Framework-Review):
+`RichTextEditor` liegt jetzt in einem eigenen Entry
+`@basicbar/ui/rich-text-editor`; das Paket ist `sideEffects: false` und wird
+mit Code-Splitting gebaut. Der Haupt-Entry `@basicbar/ui` importiert kein
+`@tiptap/*` mehr. Nagelprobe am gerenderten Template (kein Editor): Bundle
+575 kB → 280 kB (gzip 185 → 91 kB). Die `@tiptap/*`-Pakete bleiben bewusst
+`dependencies` (nicht optionale Peers): sie kosten nur Installationszeit, und
+die eine Pin-Stelle für alle Tools bleibt erhalten.
+
+Außerdem:
+
+- `stripHtml(html)` und `isEmptyHtml(html)` sind exportiert (abstimmbar
+ hatte vier Kopien). `isEmptyHtml` zählt ein Editor-Leer-`
` als
+ leer, einen reinen Bild-Inhalt aber als gefüllt; `TranslatableField` mit
+ `format="html"` nutzt das für seine Ausgefüllt-Punkte (vorher galt ein
+ Bild ohne Text als „nicht übersetzt“).
+- `richTextClass` (der Prosa-Default von `RichText`) ist exportiert, damit
+ Aufrufer ihn ergänzen können statt zu kopieren; `className` ersetzt
+ weiterhin.
+- `RichTextEditor` hat `editable?: boolean` (Default `true`): schreibgeschützt
+ ohne Toolbar und Cursor, Drop/Paste ignoriert; Umschalten zur Laufzeit via
+ `setEditable`.
+- `TranslationFormProvider` hat `controlsClassName?` für die Position der
+ Floating-Controls (Default `fixed bottom-6 right-6 z-40`) — ersetzt den
+ CSS-Override auf die Utility-Klassen in abstimmbar.
+- `renderInput` von `TranslatableField` bekommt `onBlur` durchgereicht.
+- Performance: `TranslatableField` berechnet die Sprachreihenfolge und den
+ Ausgefüllt-Status pro Render nur noch einmal (vorher pro Tab/Lookup, bei
+ HTML jeweils mit DOM-Parse); `targets` im Provider memoisiert.
+- A11y: die Sprach-Tabs tragen den Status („translated“ …) zusätzlich als
+ `sr-only`-Text (der Punkt war farb-only, WCAG 1.4.1); `PreferencesMenu`
+ fokussiert beim Öffnen die erste Zeile und unterstützt Pfeiltasten,
+ Home/End (WAI-ARIA-Menü-Pattern), `role="menu"` hat einen Namen.
+- README: Abschnitt „Übersetzungs-Keys“ mit allen Keys des Pakets.
+
+**Migration:**
+
+1. `package.json`: Tarball auf `ui/v0.7.0`.
+2. Jeden `RichTextEditor`-Import auf den neuen Pfad umstellen:
+ `import { RichTextEditor } from "@basicbar/ui/rich-text-editor";`
+ (`RichText`, `RichTextEditorProps`-Typ: `RichText` bleibt in
+ `@basicbar/ui`, der Props-Typ kommt aus dem Editor-Entry). Betroffen:
+ ausleihbar (`AdminWelcomePage`, `AdminPagesPage`, `AdminPoolsPage`),
+ abstimmbar (`components/TranslatableField.tsx`). Tools ohne Editor
+ (erkennbar, modulierbar) ändern nichts.
+3. Optional, empfohlen: lokale `stripHtml`-Kopien durch den Export ersetzen
+ (abstimmbar: `RoomsPage`, `ResultsPage`, `SetPage`, `QuestionPage` — dort
+ `!stripHtml(x) && !/`-Sync nach WCAG 3.1.1). Die Kataloge bleiben im Tool.
- **`contentLang`** — `localizedText`/`localizedMap`/`setLocalizedLang` & Co.
für `{ lang: text }`-Inhalte (Spiegel des Backend-`resolve_translated_text`).
+- **`TranslatableField` / `TranslationFormProvider`** — Sprach-Tabs pro
+ Feld plus „alle Felder übersetzen“ (siehe `TranslatableField.tsx`).
+- **`RichText` / `stripHtml` / `isEmptyHtml`** — Rendern und Prüfen des
+ gespeicherten Rich-HTML; **`RichTextEditor`** (TipTap) als **eigener Entry**
+ `@basicbar/ui/rich-text-editor`, damit TipTap/ProseMirror nur in Bundles
+ landet, die den Editor wirklich rendern (siehe „Rich text“).
## Einbinden
@@ -68,6 +74,19 @@ 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.
+**Import-Pfade:** der Editor kommt aus dem eigenen Entry
+`@basicbar/ui/rich-text-editor`; `RichText`, `stripHtml` und `isEmptyHtml`
+aus `@basicbar/ui`. Grund: TipTap + ProseMirror sind ~300 kB (≈95 kB gzip)
+und sollen nur in Bundles landen, die den Editor rendern — ein Tool, das nur
+`RichText` anzeigt oder gar kein Rich-Text hat, zahlt sonst mit. Wer den
+Editor nur auf Admin-Seiten braucht, lädt ihn zusätzlich per `React.lazy`
+nach, dann liegt er in einem eigenen Chunk.
+
+```tsx
+import { RichText, stripHtml, isEmptyHtml } from "@basicbar/ui";
+import { RichTextEditor } from "@basicbar/ui/rich-text-editor";
+```
+
**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.
@@ -90,7 +109,7 @@ Drag&Drop/Einfügen aus der Zwischenablage werden ignoriert; bestehende
``-Inhalte bleiben trotzdem sichtbar):
```tsx
-import { RichTextEditor } from "@basicbar/ui";
+import { RichTextEditor } from "@basicbar/ui/rich-text-editor";
```
+**Schreibgeschützt** mit `editable={false}`: kein Toolbar, kein Cursor,
+Dateien per Drop/Paste werden ignoriert — z. B. während ein Formular
+speichert oder für Nutzer ohne Schreibrecht. Für die reine Anzeige
+gespeicherten HTMLs ist `RichText` das richtige Werkzeug (kostet kein TipTap).
+
**Editor mit Bildern** — `onUploadImage` lädt hoch und liefert die relative
URL als String; scheitert der Upload, zeigt der Editor
`t("Image upload failed")` (plus die Fehlermeldung, falls vorhanden) per
@@ -143,20 +167,31 @@ unangetastet.
**Rendern** des serverseitig sanitisierten HTML:
```tsx
-import { RichText } from "@basicbar/ui";
+import { RichText, richTextClass } from "@basicbar/ui";
// eigene Klassen statt des Prosa-Defaults (ersetzt, nicht ergänzt):
+// Prosa-Default ergänzen statt kopieren:
+
```
+**Prüfen** des gespeicherten HTML — `stripHtml(html)` liefert den sichtbaren
+Text (für Listen, Suchtreffer, Platzhalter „kein Fragetext“), `isEmptyHtml(html)`
+ist `true` für leere Werte und das ``, das ein geöffneter Editor
+hinterlässt, aber `false` für reine Bild-Inhalte. Beide parsen per DOM (keine
+Regex), ohne etwas auszuführen. `TranslatableField` mit `format="html"`
+benutzt `isEmptyHtml` für seine Ausgefüllt-Punkte.
+
**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). `renderInput` bekommt neben `id`
auch `labelId` — die Id von `TranslatableField`s eigenem sichtbaren `