diff --git a/backend/version.py b/backend/version.py
index 576abb2..5a32e9d 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,11 +1,11 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.95"
+APP_VERSION = "0.8.96"
BUILD_DATE = "2026-05-12"
DB_SCHEMA_VERSION = "20260511053"
MODULE_VERSIONS = {
- "legal_documents": "1.3.0", # P-01: Ausgabe §-Nummerierung pro Abschnitt; Markdown im Fließtext + PDF; gem. legalPdfExport
+ "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
"auth": "1.2.3", # P-05b: reset-password min_length=8 via Pydantic PasswordResetConfirm
"profiles": "1.7.0", # exercise_list_prefs JSONB (Standard Übungsfilter); Patch via ProfileUpdate + Json()
"tenant_context": "1.0.5", # Plattform-Admin: effective_club ohne Header aus Profil active_club_id wenn Verein existiert
@@ -34,6 +34,13 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
+ {
+ "version": "0.8.96",
+ "date": "2026-05-12",
+ "changes": [
+ "P-01 Admin Rechtstexte: Live-Vorschau je Abschnitt (Markdown) neben dem Editor; modale „Vollständige Vorschau“ aus dem Formular; Augen-Symbol in der Dokumentenliste für die gerenderte Ansicht (API-Laden).",
+ ],
+ },
{
"version": "0.8.95",
"date": "2026-05-12",
diff --git a/frontend/src/app.css b/frontend/src/app.css
index 1b9c9f1..9379619 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -368,6 +368,82 @@ ul > li.card + li.card,
margin: 0.85em 0;
}
+/* Admin: Rechtstext Editor — Live-Vorschau neben Textarea */
+.legal-section-split {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-top: 4px;
+}
+@media (min-width: 720px) {
+ .legal-section-split {
+ flex-direction: row;
+ align-items: flex-start;
+ }
+ .legal-section-split__editor {
+ flex: 1;
+ min-width: 0;
+ }
+ .legal-section-split__preview {
+ flex: 1;
+ min-width: 0;
+ }
+}
+.legal-section-preview-box {
+ padding: 12px;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ min-height: 100px;
+}
+.legal-section-preview-box h4 {
+ font-size: 0.95rem;
+ font-weight: 700;
+ margin: 0 0 0.45rem;
+ color: var(--text1);
+}
+
+/* Modales Vorschau-Overlay (Rechtstexte Admin) */
+.legal-preview-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ background: rgba(0, 0, 0, 0.45);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding: max(16px, env(safe-area-inset-top, 0px)) 16px 24px;
+ overflow-y: auto;
+}
+.legal-preview-modal {
+ width: 100%;
+ max-width: 760px;
+ margin-top: 4vh;
+ margin-bottom: 4vh;
+ background: var(--surface);
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+ max-height: min(92vh, 960px);
+ display: flex;
+ flex-direction: column;
+}
+.legal-preview-modal__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.legal-preview-modal__body {
+ padding: 16px 18px 20px;
+ overflow-y: auto;
+ min-height: 0;
+ flex: 1;
+}
+
.form-input {
width: 100%;
min-width: 0;
diff --git a/frontend/src/components/LegalDocumentPreview.jsx b/frontend/src/components/LegalDocumentPreview.jsx
new file mode 100644
index 0000000..c7e80ad
--- /dev/null
+++ b/frontend/src/components/LegalDocumentPreview.jsx
@@ -0,0 +1,111 @@
+import { useEffect } from 'react'
+import LegalDocumentBody from './LegalDocumentBody'
+import { legalSectionNumber } from '../utils/legalPdfExport'
+
+/** Inhalt wie auf der öffentlichen Rechtstextseite (inkl. §-Nummerierung). */
+export function LegalDocumentPublicPreviewContent({
+ title,
+ sections,
+ showDraftNotice = true,
+ metaLine,
+}) {
+ const safeTitle = (title || '').trim() || 'Ohne Titel'
+
+ return (
+
+ {showDraftNotice && (
+
+
Vorschau
+
+ So erscheint der Text für Besucher nach Veröffentlichung (Markdown wird gerendert, §-Nummern wie online).
+
+
+ )}
+ {metaLine && (
+
{metaLine}
+ )}
+
{safeTitle}
+ {(sections || []).map((section, i) => (
+
+
+ {section.heading?.trim()
+ ? `${legalSectionNumber(i)} ${section.heading}`
+ : legalSectionNumber(i)}
+
+
+
+ ))}
+ {sections?.length === 0 && (
+
Noch keine Abschnitte.
+ )}
+
+ )
+}
+
+/**
+ * Modal: gerenderte Rechtstext-Vorschau (Editor oder gespeicherte Version).
+ */
+export function LegalPreviewModal({
+ open,
+ onClose,
+ title,
+ sections,
+ metaLine,
+ loading,
+ showDraftNotice = true,
+}) {
+ useEffect(() => {
+ if (!open) return
+ const onKey = (e) => {
+ if (e.key === 'Escape') onClose()
+ }
+ window.addEventListener('keydown', onKey)
+ return () => window.removeEventListener('keydown', onKey)
+ }, [open, onClose])
+
+ if (!open) return null
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+ Öffentliche Darstellung
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/src/pages/AdminLegalDocumentsPage.jsx b/frontend/src/pages/AdminLegalDocumentsPage.jsx
index af6c928..85558f3 100644
--- a/frontend/src/pages/AdminLegalDocumentsPage.jsx
+++ b/frontend/src/pages/AdminLegalDocumentsPage.jsx
@@ -1,7 +1,9 @@
import { useState, useEffect, useCallback } from 'react'
-import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy, Download, ChevronUp, ChevronDown } from 'lucide-react'
+import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy, Download, ChevronUp, ChevronDown, Eye } from 'lucide-react'
import api from '../utils/api'
-import { generateLegalPdf } from '../utils/legalPdfExport'
+import { generateLegalPdf, legalSectionNumber } from '../utils/legalPdfExport'
+import LegalDocumentBody from '../components/LegalDocumentBody'
+import { LegalPreviewModal } from '../components/LegalDocumentPreview'
const PDF_STATUS_META = { published: 'Gültig seit', draft: 'Entwurf — Stand', archived: 'Archiviert — Stand' }
@@ -109,18 +111,35 @@ function SectionEditor({ sections, onChange }) {
placeholder="Abschnittsüberschrift"
/>
-
-
@@ -135,7 +154,7 @@ function SectionEditor({ sections, onChange }) {
)
}
-function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onDownload, onViewAudit }) {
+function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onDownload, onViewAudit, onRenderedPreview }) {
const [downloading, setDownloading] = useState(false)
const handleDownload = async () => {
@@ -183,6 +202,10 @@ function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onDownload, on
)}
+