1 Commits

Author SHA1 Message Date
s4luorth
04a04d6e86 /textmenge: volle App-Breite, Vorschau ohne Scroll + Vergroessern + Bild-Download
All checks were successful
Build & Deploy / deploy (push) Successful in 41s
- Wrapper .sk-konf (Breite 1:1 wie Konfigurator/neuer Auftrag).
- Vorschau skaliert (kein Inner-Scroll), Klick oeffnet Lightbox (Vergroessern).
- Buttons 'Vergroessern' und 'Bild herunterladen' (SVG->PNG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:30:40 +02:00

View File

@@ -22,6 +22,41 @@ const inkify = (svg: string): string => {
.replace(/<\/svg>\s*$/i, '</g></svg>'); .replace(/<\/svg>\s*$/i, '</g></svg>');
}; };
/** Musterbild als PNG herunterladen (SVG → Canvas), wie im PreviewPanel. */
function bildSpeichern(svg: string) {
const m = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
const w = Math.round(m ? Number(m[1]) : 794);
const h = Math.round(m ? Number(m[2]) : 1123);
const inkSvg = inkify(svg);
const url = URL.createObjectURL(new Blob([inkSvg], { type: 'image/svg+xml;charset=utf-8' }));
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob((png) => {
if (png) {
const a = document.createElement('a');
a.href = URL.createObjectURL(png);
a.download = 'skrift-textmenge.png';
a.click();
URL.revokeObjectURL(a.href);
}
URL.revokeObjectURL(url);
}, 'image/png');
} else {
URL.revokeObjectURL(url);
}
};
img.onerror = () => URL.revokeObjectURL(url);
img.src = url;
}
/** Stabile anonyme Kennung fürs Vorschau-Kontingent (wie im Konfigurator). */ /** Stabile anonyme Kennung fürs Vorschau-Kontingent (wie im Konfigurator). */
function clientId(): string { function clientId(): string {
const KEY = 'skrift.client'; const KEY = 'skrift.client';
@@ -61,6 +96,7 @@ export default function Textmenge() {
const [doc, setDoc] = useState<PreviewDoc | null>(null); const [doc, setDoc] = useState<PreviewDoc | null>(null);
const [laedt, setLaedt] = useState(false); const [laedt, setLaedt] = useState(false);
const [fehler, setFehler] = useState<string | null>(null); const [fehler, setFehler] = useState<string | null>(null);
const [gross, setGross] = useState(false);
useEffect(() => { useEffect(() => {
fetchFormats() fetchFormats()
@@ -72,6 +108,14 @@ export default function Textmenge() {
fetchSettings().then(setSettings).catch(() => {}); fetchSettings().then(setSettings).catch(() => {});
}, []); }, []);
// Escape schließt die Großansicht.
useEffect(() => {
if (!gross) return;
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setGross(false); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [gross]);
// Gesperrte Zeichen wie im Konfigurator (Zeilenumbruch/Tab bleiben erlaubt). // Gesperrte Zeichen wie im Konfigurator (Zeilenumbruch/Tab bleiben erlaubt).
const verbotenRe = useMemo(() => { const verbotenRe = useMemo(() => {
const wl = settings?.char_whitelist; const wl = settings?.char_whitelist;
@@ -97,6 +141,7 @@ export default function Textmenge() {
useEffect(() => { useEffect(() => {
setDoc(null); setDoc(null);
setFehler(null); setFehler(null);
setGross(false);
}, [text, font, format, realistic]); }, [text, font, format, realistic]);
async function generieren() { async function generieren() {
@@ -127,11 +172,13 @@ export default function Textmenge() {
} }
return ( return (
<div style={{ maxWidth: 820, margin: '0 auto', padding: '40px 20px 72px' }}> <div className="sk-konf">
<div>
<h1 style={{ margin: '0 0 8px', fontSize: 28 }}>Textmenge prüfen</h1> <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 }}> <p style={{ color: 'var(--sk-muted, #666)', margin: 0, 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. Text eingeben, Format und Schrift wählen, dann eine Vorschau erzeugen. So siehst du, ob der Text auf ein Schriftstück passt.
</p> </p>
</div>
<div className="sk-card"> <div className="sk-card">
<div className="sk-card-body" style={{ display: 'flex', flexDirection: 'column', gap: 20 }}> <div className="sk-card-body" style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
@@ -170,7 +217,6 @@ export default function Textmenge() {
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', resize: 'vertical', lineHeight: 1.6 }} style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', resize: 'vertical', lineHeight: 1.6 }}
onChange={(e) => setText(saeubere(e.target.value))} onChange={(e) => setText(saeubere(e.target.value))}
onKeyDown={(e) => { onKeyDown={(e) => {
// Tab fügt einen echten Tabulator ein (z. B. Datum an den rechten Rand).
if (e.key !== 'Tab' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return; if (e.key !== 'Tab' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
e.preventDefault(); e.preventDefault();
const ta = e.currentTarget; const ta = e.currentTarget;
@@ -184,7 +230,6 @@ export default function Textmenge() {
/> />
</label> </label>
{/* Button direkt unter dem Textfeld */}
<button <button
type="button" type="button"
className="sk-btn sk-btn--primary" className="sk-btn sk-btn--primary"
@@ -203,42 +248,38 @@ export default function Textmenge() {
</div> </div>
{/* Vorschau / Ergebnis */} {/* Vorschau / Ergebnis */}
<div className="sk-card" style={{ marginTop: 20 }}> <div className="sk-card">
<div className="sk-card-body"> <div className="sk-card-body">
{fehler ? ( {fehler ? (
<div <div
style={{ style={{
display: 'flex', display: 'flex', gap: 10, alignItems: 'flex-start', color: '#c62828',
gap: 10, background: 'rgba(198,40,40,.08)', border: '1px solid rgba(198,40,40,.25)',
alignItems: 'flex-start', borderRadius: 10, padding: '14px 16px', fontSize: 15,
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 style={{ fontSize: 18, lineHeight: 1 }}>⚠</span>
<span>{fehler}</span> <span>{fehler}</span>
</div> </div>
) : doc ? ( ) : doc ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center' }}>
<div <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap', width: '100%' }}>
<span
style={{ style={{
alignSelf: 'flex-start', fontSize: 13, fontWeight: 600, padding: '4px 12px', borderRadius: 999,
fontSize: 13, background: 'rgba(46,125,50,.12)', color: '#2e7d32',
fontWeight: 600,
padding: '4px 12px',
borderRadius: 999,
background: 'rgba(46,125,50,.12)',
color: '#2e7d32',
}} }}
> >
✓ Passt auf eine Seite ✓ Passt auf eine Seite
</span>
<span style={{ flex: 1 }} />
<button type="button" className="sk-btn" onClick={() => setGross(true)}>🔍 Vergrößern</button>
<button type="button" className="sk-btn sk-btn--primary" onClick={() => bildSpeichern(doc.svg)}>⬇ Bild herunterladen</button>
</div> </div>
<div <div
style={{ background: '#fff', border: '1px solid var(--sk-border, #e2e2e2)', borderRadius: 10, padding: 12, overflow: 'auto', maxHeight: '70vh' }} className="sk-preview-doc-sheet"
onClick={() => setGross(true)}
style={{ width: '100%', maxWidth: 460, cursor: 'zoom-in' }}
dangerouslySetInnerHTML={{ __html: inkify(doc.svg) }} dangerouslySetInnerHTML={{ __html: inkify(doc.svg) }}
/> />
</div> </div>
@@ -249,6 +290,29 @@ export default function Textmenge() {
)} )}
</div> </div>
</div> </div>
{/* Großansicht (Lightbox) */}
{gross && doc && (
<div
onClick={() => setGross(false)}
style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,.72)', zIndex: 1000,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
gap: 16, padding: 24,
}}
>
<div style={{ display: 'flex', gap: 10 }} onClick={(e) => e.stopPropagation()}>
<button type="button" className="sk-btn sk-btn--primary" onClick={() => bildSpeichern(doc.svg)}>⬇ Bild herunterladen</button>
<button type="button" className="sk-btn" onClick={() => setGross(false)}>Schließen</button>
</div>
<div
className="sk-preview-doc-sheet"
onClick={(e) => e.stopPropagation()}
style={{ width: 'min(92vw, 62vh)', maxHeight: '86vh', overflow: 'auto' }}
dangerouslySetInnerHTML={{ __html: inkify(doc.svg) }}
/>
</div>
)}
</div> </div>
); );
} }