T21: Add PrintPreview.tsx component

This commit is contained in:
2026-06-22 23:20:10 +00:00
parent dfbf7b2014
commit 16ee7bbd7b
@@ -0,0 +1,300 @@
import React, { useState, useMemo, useCallback } from 'react';
import type { YjsDocument } from '../../crdt/YjsDocument';
import {
preparePrint,
printPages,
svgToDataUrl,
DEFAULT_PRINT_SETTINGS,
type PrintSettings,
type PrintResult,
type PageSize,
type Orientation,
type ScalePreset,
} from '../../services/printService';
import './PrintPreview.css';
interface PrintPreviewProps {
yjsDoc: React.RefObject<YjsDocument | null>;
onClose: () => void;
}
const PAGE_SIZES: PageSize[] = ['a4', 'a3', 'a2', 'a1'];
const SCALE_PRESETS: ScalePreset[] = ['1:50', '1:100', '1:200', 'custom'];
const PrintPreview: React.FC<PrintPreviewProps> = ({ yjsDoc, onClose }) => {
const [settings, setSettings] = useState<PrintSettings>({ ...DEFAULT_PRINT_SETTINGS });
const [currentPage, setCurrentPage] = useState(0);
const printResult: PrintResult | null = useMemo(() => {
const doc = yjsDoc?.current;
if (!doc) return null;
return preparePrint(doc, settings);
}, [yjsDoc, settings]);
const updateSettings = useCallback((updates: Partial<PrintSettings>) => {
setSettings((prev) => ({ ...prev, ...updates }));
setCurrentPage(0);
}, []);
const updateTitleBlock = useCallback((updates: Partial<NonNullable<PrintSettings['titleBlockData']>>) => {
setSettings((prev) => ({
...prev,
titleBlockData: { ...prev.titleBlockData!, ...updates },
}));
}, []);
const handlePrint = useCallback(() => {
if (printResult) {
printPages(printResult);
}
}, [printResult]);
if (!printResult) {
return (
<div className="print-preview-overlay" role="dialog" aria-modal="true" aria-label="Print Preview">
<div className="print-preview-modal">
<div className="print-preview-header">
<h2>Druckvorschau</h2>
<button className="print-preview-close" onClick={onClose} aria-label="Close"></button>
</div>
<div className="print-preview-empty">Kein Dokument verfügbar.</div>
</div>
</div>
);
}
const { layout, pages, bbox } = printResult;
const page = pages[currentPage];
const previewUrl = page ? svgToDataUrl(page.svg) : '';
return (
<div className="print-preview-overlay" role="dialog" aria-modal="true" aria-label="Print Preview">
<div className="print-preview-modal">
{/* Header */}
<div className="print-preview-header">
<h2>Druckvorschau</h2>
<button className="print-preview-close" onClick={onClose} aria-label="Close"></button>
</div>
<div className="print-preview-body">
{/* Settings sidebar */}
<div className="print-preview-settings">
<fieldset className="print-settings-fieldset">
<legend>Seite</legend>
<label className="print-settings-label">
Format
<select
className="print-settings-select"
value={settings.pageSize}
onChange={(e) => updateSettings({ pageSize: e.target.value as PageSize })}
>
{PAGE_SIZES.map((s) => (
<option key={s} value={s}>{s.toUpperCase()}</option>
))}
</select>
</label>
<label className="print-settings-label">
Ausrichtung
<select
className="print-settings-select"
value={settings.orientation}
onChange={(e) => updateSettings({ orientation: e.target.value as Orientation })}
>
<option value="portrait">Hochformat</option>
<option value="landscape">Querformat</option>
</select>
</label>
<label className="print-settings-label">
Rand (mm)
<input
type="number"
className="print-settings-input"
value={settings.margin}
min="0"
max="50"
onChange={(e) => updateSettings({ margin: parseFloat(e.target.value) || 0 })}
/>
</label>
</fieldset>
<fieldset className="print-settings-fieldset">
<legend>Maßstab</legend>
<label className="print-settings-label">
Vorwahl
<select
className="print-settings-select"
value={settings.scale}
onChange={(e) => updateSettings({ scale: e.target.value as ScalePreset })}
>
{SCALE_PRESETS.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</label>
{settings.scale === 'custom' && (
<label className="print-settings-label">
Custom (1:N)
<input
type="number"
className="print-settings-input"
value={settings.customScale}
min="1"
onChange={(e) => updateSettings({ customScale: parseFloat(e.target.value) || 1 })}
/>
</label>
)}
</fieldset>
<fieldset className="print-settings-fieldset">
<legend>Optionen</legend>
<label className="print-settings-checkbox-label">
<input
type="checkbox"
checked={settings.showBorder}
onChange={(e) => updateSettings({ showBorder: e.target.checked })}
/>
Druckrand
</label>
<label className="print-settings-checkbox-label">
<input
type="checkbox"
checked={settings.showTitleBlock}
onChange={(e) => updateSettings({ showTitleBlock: e.target.checked })}
/>
Titelblock
</label>
</fieldset>
{settings.showTitleBlock && settings.titleBlockData && (
<fieldset className="print-settings-fieldset">
<legend>Titelblock</legend>
<label className="print-settings-label">
Titel
<input
type="text"
className="print-settings-text-input"
value={settings.titleBlockData.title}
onChange={(e) => updateTitleBlock({ title: e.target.value })}
/>
</label>
<label className="print-settings-label">
Autor
<input
type="text"
className="print-settings-text-input"
value={settings.titleBlockData.author}
onChange={(e) => updateTitleBlock({ author: e.target.value })}
/>
</label>
<label className="print-settings-label">
Datum
<input
type="text"
className="print-settings-text-input"
value={settings.titleBlockData.date}
onChange={(e) => updateTitleBlock({ date: e.target.value })}
/>
</label>
</fieldset>
)}
{/* Info */}
<div className="print-preview-info">
<div className="print-info-row">
<span>Zeichnung:</span>
<span>{Math.round(bbox.width)} × {Math.round(bbox.height)} CAD</span>
</div>
<div className="print-info-row">
<span>Seiten:</span>
<span>{layout.totalPages} ({layout.cols} × {layout.rows})</span>
</div>
<div className="print-info-row">
<span>Format:</span>
<span>{layout.pageWidth} × {layout.pageHeight} mm</span>
</div>
<div className="print-info-row">
<span>Maßstab:</span>
<span>1:{layout.scaleValue}</span>
</div>
</div>
</div>
{/* Preview area */}
<div className="print-preview-canvas">
{pages.length > 0 && previewUrl ? (
<>
<div className="print-preview-page-container">
<img
src={previewUrl}
alt={`Page ${currentPage + 1}`}
className="print-preview-page-image"
/>
<div className="print-preview-page-label">
Seite {currentPage + 1} / {pages.length}
</div>
</div>
{/* Page navigation */}
{pages.length > 1 && (
<div className="print-preview-nav">
<button
className="print-preview-nav-btn"
onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
disabled={currentPage === 0}
>
Zurück
</button>
<span className="print-preview-nav-info">
{currentPage + 1} / {pages.length}
</span>
<button
className="print-preview-nav-btn"
onClick={() => setCurrentPage(Math.min(pages.length - 1, currentPage + 1))}
disabled={currentPage === pages.length - 1}
>
Weiter
</button>
</div>
)}
{/* Page grid overview */}
{pages.length > 1 && (
<div className="print-preview-grid">
{pages.map((p, i) => (
<button
key={i}
className={`print-preview-grid-cell ${i === currentPage ? 'active' : ''}`}
onClick={() => setCurrentPage(i)}
title={`Seite ${i + 1} (Spalte ${p.col + 1}, Zeile ${p.row + 1})}`}
>
{i + 1}
</button>
))}
</div>
)}
</>
) : (
<div className="print-preview-empty">Keine Elemente zum Drucken.</div>
)}
</div>
</div>
{/* Footer with actions */}
<div className="print-preview-footer">
<button className="print-preview-btn print-preview-btn-secondary" onClick={onClose}>
Abbrechen
</button>
<button
className="print-preview-btn print-preview-btn-primary"
onClick={handlePrint}
disabled={pages.length === 0}
>
🖨 Drucken
</button>
</div>
</div>
</div>
);
};
export default PrintPreview;