/textmenge: Design ueberarbeitet, eigener Vorschau-Button, einseitig

- Eigener 'Vorschau generieren'-Button direkt unter dem Textfeld (nicht mehr am
  Vorschau-Kontingent haengend -> war der Grund, dass er nicht klickbar war).
- Checkbox 'Realistische Handschrift'.
- Einseitig: kein Seitenumbruch-Hinweis mehr.
- Mehr Layout-Abstand (nicht mehr alles oben), keine Gedankenstriche.
- Vorschau als Karte darunter mit Kugelschreiber-Optik bzw. Ueberlauf-Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-01 15:19:47 +02:00
parent cf60a1aaa2
commit 658f56c929

View File

@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { fetchFormats, fetchSettings, generatePreview } from '../lib/api';
import type { Format, PublicSettings, PreviewDoc, PreviewOverflow } from '../lib/api';
import PreviewPanel from '../components/PreviewPanel';
/** Schriftarten wie im Konfigurator/manuellen Druck. */
const SCHRIFTEN = [
@@ -10,6 +9,19 @@ const SCHRIFTEN = [
{ key: 'ellie', name: 'Ellie' },
];
/** Kugelschreiber-Optik (identisch zum PreviewPanel): blaue Tinte, dezenter Wobble. */
let inkSeq = 0;
const inkify = (svg: string): string => {
if (typeof svg !== 'string') return svg;
const id = `skink${inkSeq++}`;
return svg
.replace(/stroke="#0{3,6}"/gi, 'stroke="#1a2a6c" stroke-opacity="0.95"')
.replace(/stroke-width="0\.6"/gi, 'stroke-width="1.4"')
.replace(/(<svg[^>]*>)/i, `$1<defs><filter id="${id}" x="-4%" y="-4%" width="108%" height="108%" color-interpolation-filters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="1" seed="4" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="1.3" xChannelSelector="R" yChannelSelector="G"/></filter></defs>`)
.replace(/(<rect[^>]*fill="#FFFFFF"[^>]*\/>)/i, `$1<g filter="url(#${id})">`)
.replace(/<\/svg>\s*$/i, '</g></svg>');
};
/** Stabile anonyme Kennung fürs Vorschau-Kontingent (wie im Konfigurator). */
function clientId(): string {
const KEY = 'skrift.client';
@@ -32,24 +44,23 @@ function overflowMeldung(overflow?: PreviewOverflow[]) {
}
/**
* Textmengen-Tool (/textmenge): Format- und Schriftwahl, Texteingabe mit denselben
* Möglichkeiten wie sonst (Tabulator, gesperrte Zeichen, [[seitenumbruch]]). Über
* „Vorschau generieren" wird das Schriftstück gerendert (Kugelschreiber-Optik) –
* so sieht man, ob der Text passt, sonst kommt eine Fehlermeldung.
* Textmengen-Tool (/textmenge): Format- und Schriftwahl, Texteingabe (Tabulator,
* gesperrte Zeichen) und „Vorschau generieren" → rendert EIN Schriftstück
* (Kugelschreiber-Optik). Passt der Text nicht auf eine Seite, kommt die Fehlermeldung.
* Bewusst einseitig: kein Seitenumbruch.
*/
export default function Textmenge() {
const [formate, setFormate] = useState<Format[]>([]);
const [settings, setSettings] = useState<PublicSettings | null>(null);
const [format, setFormat] = useState('a4');
const [font, setFont] = useState('tilda');
const [realistic, setRealistic] = useState(true);
const [text, setText] = useState('');
const [hinweis, setHinweis] = useState('');
const [docs, setDocs] = useState<PreviewDoc[]>([]);
const [doc, setDoc] = useState<PreviewDoc | null>(null);
const [laedt, setLaedt] = useState(false);
const [fehler, setFehler] = useState<string | null>(null);
const [verbleibend, setVerbleibend] = useState(0);
const [maximal, setMaximal] = useState(0);
useEffect(() => {
fetchFormats()
@@ -82,14 +93,14 @@ export default function Textmenge() {
return v.replace(verbotenRe, '');
};
// Neuer Text/Format/Schrift → alte Vorschau verwerfen (nicht live neu laden).
// Änderung an Eingaben → alte Vorschau verwerfen.
useEffect(() => {
setDocs([]);
setDoc(null);
setFehler(null);
}, [text, font, format]);
}, [text, font, format, realistic]);
async function generieren() {
if (!text.trim()) return;
if (!text.trim() || laedt) return;
setLaedt(true);
setFehler(null);
try {
@@ -98,18 +109,17 @@ export default function Textmenge() {
text,
font,
format,
realistic,
entries: [{}],
});
setVerbleibend(ergebnis.remaining ?? 0);
setMaximal(ergebnis.limit ?? 0);
if (ergebnis.has_overflow) {
setDocs([]);
setDoc(null);
setFehler(overflowMeldung(ergebnis.overflow));
} else {
setDocs(ergebnis.docs ?? []);
setDoc((ergebnis.docs ?? []).find((d) => d.art === 'brief') ?? ergebnis.docs?.[0] ?? null);
}
} catch (e) {
setDocs([]);
setDoc(null);
setFehler((e as Error)?.message || 'Vorschau fehlgeschlagen.');
} finally {
setLaedt(false);
@@ -117,15 +127,15 @@ export default function Textmenge() {
}
return (
<div className="sk-page" style={{ maxWidth: 980, margin: '0 auto' }}>
<h1 style={{ margin: '0 0 6px' }}>Textmenge prüfen</h1>
<p style={{ color: 'var(--sk-muted, #666)', margin: '0 0 20px' }}>
Text eingeben, Format und Schrift wählen und eine Vorschau erzeugen – so siehst du, ob der Text auf ein Schriftstück passt.
<div style={{ maxWidth: 820, margin: '0 auto', padding: '40px 20px 72px' }}>
<h1 style={{ margin: '0 0 8px', fontSize: 28 }}>Textmenge prüfen</h1>
<p style={{ color: 'var(--sk-muted, #666)', margin: '0 0 28px', lineHeight: 1.6 }}>
Text eingeben, Format und Schrift wählen, dann eine Vorschau erzeugen. So siehst du, ob der Text auf ein Schriftstück passt.
</p>
<div className="sk-card">
<div className="sk-card-body" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<div className="sk-card-body" style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<label className="sk-field" style={{ flex: '1 1 200px' }}>
<span className="sk-label">Format</span>
<select className="sk-input" value={format} onChange={(e) => setFormat(e.target.value)}>
@@ -145,6 +155,11 @@ export default function Textmenge() {
</label>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', fontSize: 15 }}>
<input type="checkbox" checked={realistic} onChange={(e) => setRealistic(e.target.checked)} style={{ width: 18, height: 18 }} />
Realistische Handschrift (natürliche Variation)
</label>
<label className="sk-field">
<span className="sk-label">Text · {text.length} Zeichen</span>
<textarea
@@ -152,7 +167,7 @@ export default function Textmenge() {
rows={12}
value={text}
placeholder="Text eingeben oder einfügen …"
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', resize: 'vertical' }}
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', resize: 'vertical', lineHeight: 1.6 }}
onChange={(e) => setText(saeubere(e.target.value))}
onKeyDown={(e) => {
// Tab fügt einen echten Tabulator ein (z. B. Datum an den rechten Rand).
@@ -168,26 +183,71 @@ export default function Textmenge() {
}}
/>
</label>
{/* Button direkt unter dem Textfeld */}
<button
type="button"
className="sk-btn sk-btn--primary"
disabled={!text.trim() || laedt}
onClick={generieren}
style={{ alignSelf: 'flex-start', opacity: !text.trim() || laedt ? 0.6 : 1 }}
>
{laedt ? 'Vorschau wird erzeugt …' : 'Vorschau generieren'}
</button>
<p style={{ color: 'var(--sk-muted, #666)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
Tabulator (↹) rückt Text ein · <code>[[seitenumbruch]]</code> erzwingt eine neue Seite · gesperrte Zeichen werden entfernt.
Tabulator (↹) rückt Text ein. Gesperrte Zeichen werden automatisch entfernt.
</p>
{hinweis && <p style={{ color: 'var(--sk-danger, #c62828)', fontSize: 13, margin: 0 }}>{hinweis}</p>}
</div>
</div>
<div style={{ marginTop: 16 }}>
<PreviewPanel
titel="Vorschau"
format={format}
docs={docs}
gesamtDokumente={docs.length}
verbleibend={verbleibend}
maximal={maximal}
laedt={laedt}
fehler={fehler}
onGenerate={generieren}
bereit={!!text.trim()}
/>
{/* Vorschau / Ergebnis */}
<div className="sk-card" style={{ marginTop: 20 }}>
<div className="sk-card-body">
{fehler ? (
<div
style={{
display: 'flex',
gap: 10,
alignItems: 'flex-start',
color: '#c62828',
background: 'rgba(198,40,40,.08)',
border: '1px solid rgba(198,40,40,.25)',
borderRadius: 10,
padding: '14px 16px',
fontSize: 15,
}}
>
<span style={{ fontSize: 18, lineHeight: 1 }}>⚠</span>
<span>{fehler}</span>
</div>
) : doc ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div
style={{
alignSelf: 'flex-start',
fontSize: 13,
fontWeight: 600,
padding: '4px 12px',
borderRadius: 999,
background: 'rgba(46,125,50,.12)',
color: '#2e7d32',
}}
>
✓ Passt auf eine Seite
</div>
<div
style={{ background: '#fff', border: '1px solid var(--sk-border, #e2e2e2)', borderRadius: 10, padding: 12, overflow: 'auto', maxHeight: '70vh' }}
dangerouslySetInnerHTML={{ __html: inkify(doc.svg) }}
/>
</div>
) : (
<p style={{ color: 'var(--sk-muted, #666)', margin: 0, textAlign: 'center', padding: '40px 0' }}>
Noch keine Vorschau. Text eingeben und „Vorschau generieren" drücken.
</p>
)}
</div>
</div>
</div>
);