feat(quick-260916-dcz): CHANGELOG.md, Seite "Was ist neu" mit Kanalfilter, Versionszeile als Link, Bauzeit-Einbettung

- CHANGELOG.md (Wurzel): Unveroeffentlicht (bwo + dyv + diese Seite, 8 Punkte) und 1.0.0 – 2026-09-15 (11 Punkte aus den Handbuechern), Alltagssprache, echte Umlaute
- next.config.ts liest CHANGELOG.md zur Bauzeit nach env.TESSERA_CHANGELOG_MD; .dockerignore-Ausnahme !CHANGELOG.md und COPY-Zeile im Web-Dockerfile
- lib/changelog.ts: filterChangelogForChannel (live ohne Unveroeffentlicht, beta/dev mit "Noch nicht freigegeben (Beta)", leerer Abschnitt ausgeblendet), 10 Tests
- Server-Seite /changelog (MDEditor.Markdown + rehype-sanitize, Hinweis auf Beta, Leer-Text), 3 Tests; Versionszeile ist Link mit aria-label, 2 Tests
- de/en: sidebar.whatsNew und Namensraum changelog; Web-Suite 49/309, tsc 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-16 11:20:22 +02:00
parent 0db21627f5
commit ba06db98c5
13 changed files with 620 additions and 5 deletions
@@ -0,0 +1,39 @@
'use client';
import { useEffect, useState } from 'react';
import MDEditor from '@uiw/react-md-editor';
import rehypeSanitize from 'rehype-sanitize';
import { useTheme } from 'next-themes';
/**
* Gerenderte Aenderungsliste fuer die Seite "Was ist neu" (quick-260916-dcz).
*
* Nutzt `MDEditor.Markdown` aus dem bereits installierten `@uiw/react-md-editor`
* (wie die Vorschau des Notiz-Widgets) mit `rehype-sanitize` — kein neues
* Paket, kein dangerouslySetInnerHTML. Der Farbmodus folgt `next-themes`
* erst nach dem Mount (vorher 'light'), damit Server- und Client-Markup
* uebereinstimmen (mounted-Guard wie in AppShell). Der Markdown-Text kommt
* als Prop von der Server-Seite; dieses Modul importiert `@/lib/changelog`
* bewusst NICHT, damit der Text nicht in Client-Chunks landet.
*/
export function ChangelogView({ markdown }: { markdown: string }) {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const mode: 'light' | 'dark' = mounted && resolvedTheme === 'dark' ? 'dark' : 'light';
return (
<div data-testid="changelog-markdown" className="rounded-md border border-border bg-card p-4">
<MDEditor.Markdown
source={markdown}
rehypePlugins={[[rehypeSanitize]]}
wrapperElement={{ 'data-color-mode': mode }}
style={{ background: 'transparent' }}
/>
</div>
);
}