Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@ 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<string>` übergibt (relative
URL, z. B. `/media/rich/x.png`); ohne die Prop bleiben bestehende `<img>`-
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. Eingefügtes Rich-HTML
(z. B. aus einer Webseite kopiert) wird per `transformPastedHTML` gefiltert:
jedes `<img>`, dessen `src` nicht mit `/media/` beginnt, wird entfernt —
ohne `onUploadImage` wird jedes eingefügte `<img>` 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`,
`@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 `<label>`, Alternative zu `ariaLabel`);
`RenderInputArgs` (für `renderInput`) hat entsprechend ein neues optionales
Feld `labelId` — `TranslatableField`s eigene Label-Id, additiv und
abwärtskompatibel. Der Bild-Upload-Button ist jetzt per `aria-label` benannt
und zeigt einen sichtbaren Fokusring; ein fehlgeschlagener Bild-Upload zeigt
zusätzlich zu `t("Image upload failed")` die Fehlermeldung, falls vorhanden.

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`, `labelledBy={labelId}` durchreichen) 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
Expand Down
112 changes: 112 additions & 0 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,115 @@ 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. Der Editor selbst bietet bewusst **nur** dieses Subset an:
Unterstreichen/Durchgestrichen sind nicht registriert (TipTap-`underline`/
`strike` explizit aus) und Links tragen kein `target`/`rel` (das Backend
erzwingt `rel="noopener"` ohnehin) — eine Formatierung, die der Sanitizer
später wieder entfernt, soll der Nutzer erst gar nicht setzen können.

**Zugänglicher Name:** der Editor braucht immer entweder `ariaLabel` (kein
sichtbares Label) oder `labelledBy` (Id eines bereits vorhandenen sichtbaren
`<label>`-Elements, z. B. `TranslatableField`s `labelId` — siehe unten).

**Editor ohne Bilder** (kein Upload-Endpunkt verdrahtet — kein Bild-Button,
Drag&Drop/Einfügen aus der Zwischenablage werden ignoriert; bestehende
`<img>`-Inhalte bleiben trotzdem sichtbar):

```tsx
import { RichTextEditor } from "@basicbar/ui";

<RichTextEditor
value={description}
onChange={setDescription}
ariaLabel={t("Description")}
/>
```

**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
`window.alert` und lässt den Inhalt unverändert:

```tsx
<RichTextEditor
value={description}
onChange={setDescription}
onUploadImage={async (file) => {
// Eigener Upload-Endpunkt der App; liefert z. B. {"url": "/media/rich/x.png"}.
const { url } = await postImage(file);
return url; // relative URL als String
}}
id="description-editor"
ariaLabel={t("Description")}
/>
```

**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
`<img src="https://…">` würde sonst am `onUploadImage`-Flow vorbei direkt
eingefügt und wäre nach dem Speichern ein kaputtes `<img>` (der Backend-
Sanitizer erlaubt nur `/media/…`-Quellen). Der Editor filtert deshalb per
`transformPastedHTML` jedes eingefügte `<img>`, dessen `src` nicht mit
`/media/` beginnt; ohne `onUploadImage` wird jedes eingefügte `<img>`
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
import { RichText } from "@basicbar/ui";

<RichText html={product.description} />
// eigene Klassen statt des Prosa-Defaults (ersetzt, nicht ergänzt):
<RichText html={product.description} className="text-3xl [&_img]:max-h-64" />
```

**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 `<label>`
(nur gesetzt, wenn die `label`-Prop übergeben wurde); durchgereicht als
`labelledBy` bindet der Editor sich per `aria-labelledby` an dieses Label,
statt ein zweites, redundantes `ariaLabel` zu brauchen:

```tsx
<TranslatableField
label={t("Description")}
values={{ de: form.description_de, en: form.description_en }}
onChange={(lang, html) => setField(`description_${lang}`, html)}
format="html"
renderInput={({ value, onChange, id, labelId }) => (
<RichTextEditor
value={value}
onChange={onChange}
onUploadImage={uploadRichImage}
id={id}
labelledBy={labelId}
/>
)}
/>
```

**Ü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"`.
Loading
Loading