1 Commits

Author SHA1 Message Date
Lucas Orth
5f40d0fd5b Vorschau-Kuli in der App: identisch zu Directus (blau, 1.4, dezenter Wobble-Filter mit eindeutiger ID pro Inline-SVG), weisser Grund, PNG in natuerlicher Groesse
All checks were successful
Build & Deploy / deploy (push) Successful in 43s
2026-08-24 13:45:13 +02:00
2 changed files with 28 additions and 12 deletions

View File

@@ -13,6 +13,23 @@ import { useEffect, useMemo, useState } from 'react';
export type PreviewDoc = { art: 'brief' | 'umschlag'; svg: string }; export type PreviewDoc = { art: 'brief' | 'umschlag'; svg: string };
// Vorschau-Optik „Kugelschreiber" – IDENTISCH zu Directus (neuauftrag): blaue
// Tinte, etwas dicker (1.4) + dezenter Wobble (feTurbulence/Displacement) für die
// typische Kuli-Linie. non-scaling-stroke bleibt (Glyphen skalieren per transform),
// weißer Grund. Nur Anzeige/Download – die an die Produktion gehenden SVGs bleiben
// schwarz. Inline-SVGs brauchen eine EINDEUTIGE Filter-ID pro Dokument.
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>');
};
/** Ein Empfänger-Dokument: Schriftstück + optional zugehöriger Umschlag. */ /** Ein Empfänger-Dokument: Schriftstück + optional zugehöriger Umschlag. */
type Gruppe = { brief?: PreviewDoc; umschlag?: PreviewDoc }; type Gruppe = { brief?: PreviewDoc; umschlag?: PreviewDoc };
@@ -81,10 +98,12 @@ export default function PreviewPanel({
const m = zoomSvg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/); const m = zoomSvg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
const w = Math.round(m ? Number(m[1]) : 794); const w = Math.round(m ? Number(m[1]) : 794);
const h = Math.round(m ? Number(m[2]) : 1123); const h = Math.round(m ? Number(m[2]) : 1123);
const scale = 2; // für ein scharfes Musterbild // Natürliche Größe (nicht 2×): non-scaling-stroke wirkt konstant in
// Gespeicherte Vorschau in Kugelschreiber-Blau (wie die Anzeige); die // Gerätepixeln – bei 2× würde die Linie relativ dünn. So sieht das PNG aus
// an die Produktion gehenden SVGs bleiben davon unberührt (schwarz). // wie die Großansicht. Gespeicherte Vorschau in Kuli-Optik (wie Anzeige);
const inkSvg = zoomSvg.replace(/stroke="#0{3,6}"/gi, 'stroke="#1a2a6c"'); // die an die Produktion gehenden SVGs bleiben schwarz.
const scale = 1;
const inkSvg = inkify(zoomSvg);
const url = URL.createObjectURL(new Blob([inkSvg], { type: 'image/svg+xml;charset=utf-8' })); const url = URL.createObjectURL(new Blob([inkSvg], { type: 'image/svg+xml;charset=utf-8' }));
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
@@ -142,7 +161,7 @@ export default function PreviewPanel({
> >
<figcaption className="sk-preview-doc-label">{label}</figcaption> <figcaption className="sk-preview-doc-label">{label}</figcaption>
{/* Das SVG stammt aus unserer eigenen Generierung, nicht aus Nutzereingaben. */} {/* Das SVG stammt aus unserer eigenen Generierung, nicht aus Nutzereingaben. */}
<div className="sk-preview-doc-sheet" dangerouslySetInnerHTML={{ __html: svg }} /> <div className="sk-preview-doc-sheet" dangerouslySetInnerHTML={{ __html: inkify(svg) }} />
</figure> </figure>
); );
@@ -199,7 +218,7 @@ export default function PreviewPanel({
aria-label="Vergrößerte Vorschau"> aria-label="Vergrößerte Vorschau">
<div className="sk-lightbox-sheet" style={{ aspectRatio: seitenverhaeltnis(zoomSvg) }} <div className="sk-lightbox-sheet" style={{ aspectRatio: seitenverhaeltnis(zoomSvg) }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
dangerouslySetInnerHTML={{ __html: zoomSvg }} /> dangerouslySetInnerHTML={{ __html: inkify(zoomSvg) }} />
<div className="sk-lightbox-actions"> <div className="sk-lightbox-actions">
<button className="sk-lightbox-close" onClick={(e) => { e.stopPropagation(); bildSpeichern(); }}> <button className="sk-lightbox-close" onClick={(e) => { e.stopPropagation(); bildSpeichern(); }}>
Bild speichern Bild speichern

View File

@@ -460,21 +460,18 @@ button { font-family: inherit; }
text-transform: uppercase; text-transform: uppercase;
color: var(--sk-muted); color: var(--sk-muted);
} }
/* Kugelschreiber-Optik (blau, dickere Striche, Wobble) kommt aus inkify();
der Untergrund ist weiß (das SVG bringt seinen weißen Grund selbst mit). */
.sk-preview-doc-sheet { .sk-preview-doc-sheet {
border: 1px solid var(--sk-border); border: 1px solid var(--sk-border);
border-radius: var(--sk-radius); border-radius: var(--sk-radius);
background: var(--sk-bg-1); background: var(--sk-white);
padding: var(--sk-space-2); padding: var(--sk-space-2);
overflow: hidden; overflow: hidden;
} }
.sk-preview-doc-sheet svg { display: block; width: 100%; height: auto; } .sk-preview-doc-sheet svg { display: block; width: 100%; height: auto; }
.sk-preview-doc:hover .sk-preview-doc-sheet { border-color: var(--sk-accent); } .sk-preview-doc:hover .sk-preview-doc-sheet { border-color: var(--sk-accent); }
/* Vorschau-Optik „Kugelschreiber": nur die Anzeige färbt die schwarzen Striche
in Kuli-Blau. Die SVG-Daten (und damit die Produktion) bleiben schwarz. */
.sk-preview-doc-sheet svg [stroke],
.sk-lightbox-sheet svg [stroke] { stroke: #1a2a6c; }
/* ── Blättern: Pfeile als klare Buttons + deutliche Nummer ─ */ /* ── Blättern: Pfeile als klare Buttons + deutliche Nummer ─ */
.sk-preview-pager { .sk-preview-pager {
display: flex; display: flex;