diff --git a/frontend/src/components/LayoutBar.tsx b/frontend/src/components/LayoutBar.tsx index 8207f86..a335836 100644 --- a/frontend/src/components/LayoutBar.tsx +++ b/frontend/src/components/LayoutBar.tsx @@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import type { LayoutDefinition } from '../types/cad.types'; import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService'; import { insertTitleBlockForLayout } from '../services/titleBlockService'; +import { printLayout } from '../services/printService'; export interface LayoutView { center: { x: number; y: number }; @@ -120,6 +121,10 @@ const LayoutBar: React.FC = ({ doc: docProp, bridge: bridgeProp reload(); }; + const handlePrint = () => { + void printLayout(); + }; + const handleZoomTo = (l: LayoutDefinition) => { bridge?.zoomTo(l.viewport.center, l.viewport.scale); }; @@ -137,6 +142,12 @@ const LayoutBar: React.FC = ({ doc: docProp, bridge: bridgeProp onClick={handleAdd} disabled={!bridge || !doc} >+ + {layouts.length === 0 ? (
Keine Layouts
diff --git a/frontend/src/services/printService.ts b/frontend/src/services/printService.ts new file mode 100644 index 0000000..967b20e --- /dev/null +++ b/frontend/src/services/printService.ts @@ -0,0 +1,37 @@ +/** + * Task G5 – Browser-Print-Service. + * + * Bereitet die App fΓΌr window.print() vor: Der Body erhΓ€lt die Klasse + * `print-layout`, das @media print Stylesheet blendet das UI-Chrome aus + * und zeigt nur den Druckbereich (Canvas/Layout). Nach dem Druck wird + * die Klasse wieder entfernt (kein DOM-Mutation-Restzustand). + */ + +const PRINT_CLASS = 'print-layout'; + +/** Setzt den Print-Modus (Body-Klasse; @media print regelt das Rendering). */ +export function preparePrintLayout(): void { + document.body.classList.add(PRINT_CLASS); +} + +/** Entfernt den Print-Modus (nach Druck oder bei Abbruch). */ +export function cleanupPrintLayout(): void { + document.body.classList.remove(PRINT_CLASS); +} + +/** + * Druck-Flow: Vorbereitung β†’ Rendern der nΓ€chsten Frames (Styles mΓΌssen + * greifen, bevor der Browser den Print-Dialog aufbaut) β†’ window.print + * β†’ AufrΓ€umen. await-fΓ€hig fΓΌr Tests (window.print wird gemockt). + */ +export async function printLayout(): Promise { + preparePrintLayout(); + // Zwei Frames warten: Style-Anwendung + Layout der Print-Ansicht + await new Promise((resolve) => requestAnimationFrame(() => resolve())); + await new Promise((resolve) => requestAnimationFrame(() => resolve())); + try { + window.print(); + } finally { + cleanupPrintLayout(); + } +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 54be126..ab8673b 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2925,3 +2925,52 @@ body.drawer-open { overflow: hidden; } border-radius: 4px; } .layout-bar-tb:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); } + + +/* ═══════════════════════════════════════════════════════════ + Task G5 – Browser-Print: @media print zeigt nur den Druckbereich + ═══════════════════════════════════════════════════════════ */ +@media print { + @page { + size: A4 landscape; + margin: 10mm; + } + + /* UI-Chrome im Druck ausblenden */ + .screen-only { + display: none !important; + } + + /* Druckbereich fΓΌllt die Seite */ + .print-area { + width: 100% !important; + height: auto !important; + overflow: visible !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + } + + /* Farben/Linien so drucken wie angezeigt */ + .print-layout * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + /* Body: kein Scroll, weiße FlΓ€che */ + body { + background: #fff !important; + overflow: hidden !important; + } +} + +.layout-bar-print { + background: transparent; + color: #6b7280; + border: none; + cursor: pointer; + font-size: 13px; + padding: 2px 4px; + border-radius: 4px; +} +.layout-bar-print:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); } diff --git a/frontend/tests/printStylesheet.test.ts b/frontend/tests/printStylesheet.test.ts new file mode 100644 index 0000000..653fe9d --- /dev/null +++ b/frontend/tests/printStylesheet.test.ts @@ -0,0 +1,65 @@ +/** + * Task G5 – Browser-Print: @media print Stylesheet + Print-Service. + * + * printService bereitet den Body auf den Druck vor (print-layout-Klasse, + * Print-Modus ohne UI-Chrome), printLayout() triggert window.print nach + * Vorbereitung und rΓ€umt danach ab. Die CSS-Datei muss einen @media print + * Block mit den Kernregeln (UI ausblenden, Canvas fΓΌllen, @page) + * enthalten β€” verifiziert als Datei-Assertion (jsdom kann kein print-CSS + * rendern). + */ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { preparePrintLayout, cleanupPrintLayout, printLayout } from '../src/services/printService'; + +describe('G5: printService', () => { + afterEach(() => { + cleanupPrintLayout(); + }); + + it('preparePrintLayout setzt print-layout-Klasse am body', () => { + document.body.classList.remove('print-layout'); + preparePrintLayout(); + expect(document.body.classList.contains('print-layout')).toBe(true); + }); + + it('cleanupPrintLayout entfernt die Klasse wieder', () => { + preparePrintLayout(); + cleanupPrintLayout(); + expect(document.body.classList.contains('print-layout')).toBe(false); + }); + + it('printLayout ruft window.print nach Vorbereitung und rΓ€umt ab (async)', async () => { + const printSpy = vi.spyOn(window, 'print').mockImplementation(() => {}); + await printLayout(); + expect(printSpy).toHaveBeenCalledTimes(1); + // nach dem Druck ist wieder aufgerΓ€umt + expect(document.body.classList.contains('print-layout')).toBe(false); + printSpy.mockRestore(); + }); +}); + +describe('G5: Print-Stylesheet-Regeln (Datei-Assertion)', () => { + const css = readFileSync(join(__dirname, '..', 'src', 'styles.css'), 'utf-8'); + const printBlock = css.slice(css.indexOf('@media print')); + + it('enthΓ€lt einen @media print Block mit @page A4 landscape', () => { + expect(css).toContain('@media print'); + expect(printBlock).toMatch(/@page\s*\{[^}]*size:\s*A4 landscape/); + }); + + it('blendet UI-Chrome im Druck aus (screen-only Regel)', () => { + expect(printBlock).toContain('.screen-only'); + expect(printBlock).toMatch(/\.screen-only\s*\{[^}]*display:\s*none/); + }); + + it('Canvas/Druckbereich fΓΌllt die Seite (print-area Regel)', () => { + expect(printBlock).toContain('.print-area'); + expect(printBlock).toMatch(/\.print-area\s*\{[^}]*width:\s*100%/); + }); + + it('print-layout-Modus erzwingt Farbdruck-HintergrΓΌnde (print-color-adjust)', () => { + expect(printBlock).toMatch(/-webkit-print-color-adjust:\s*exact/); + }); +});