'use client' /** * StepAnsicht — Read-only page layout preview. * * Shows the reconstructed page with all zones (content grid + embedded boxes) * positioned at their original coordinates. Pure CSS positioning, no canvas. */ import { useCallback, useEffect, useRef, useState } from 'react' import { useGridEditor } from '@/components/grid-editor/useGridEditor' import type { GridZone, GridEditorCell } from '@/components/grid-editor/types' const KLAUSUR_API = '/klausur-api' interface StepAnsichtProps { sessionId: string | null onNext: () => void } /** Get dominant color from a cell's word_boxes or color_override. */ function getCellColor(cell: GridEditorCell | undefined): string | null { if (!cell) return null if ((cell as any).color_override) return (cell as any).color_override const colored = cell.word_boxes?.find((wb) => wb.color_name && wb.color_name !== 'black') return colored?.color ?? null } export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) { const { grid, loading, error, loadGrid, } = useGridEditor(sessionId) const containerRef = useRef(null) const [containerWidth, setContainerWidth] = useState(0) const [showOriginal, setShowOriginal] = useState(false) // Load grid on mount useEffect(() => { if (sessionId) loadGrid() }, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps // Track container width useEffect(() => { if (!containerRef.current) return const ro = new ResizeObserver(([entry]) => { setContainerWidth(entry.contentRect.width) }) ro.observe(containerRef.current) return () => ro.disconnect() }, []) if (loading) { return (
Lade Vorschau...
) } if (error || !grid) { return (

{error || 'Keine Grid-Daten vorhanden.'}

) } const imgW = grid.image_width || 1 const imgH = grid.image_height || 1 const scale = containerWidth > 0 ? containerWidth / imgW : 1 const containerHeight = imgH * scale // Font size: scale from original, with minimum const baseFontPx = (grid as any).layout_metrics?.font_size_suggestion_px || 14 const scaledFont = Math.max(7, baseFontPx * scale * 0.85) return (
{/* Header */}

Ansicht — Seitenrekonstruktion

Vorschau der rekonstruierten Seite mit allen Zonen und Boxen an Originalpositionen.

{/* Page container */}
0 ? `${containerHeight}px` : 'auto' }} > {/* Original image background (toggleable) */} {showOriginal && sessionId && ( Original )} {/* Render zones */} {grid.zones.map((zone) => ( ))}
) } // --------------------------------------------------------------------------- // Zone renderer // --------------------------------------------------------------------------- function ZoneRenderer({ zone, scale, fontSize }: { zone: GridZone scale: number fontSize: number }) { const isBox = zone.zone_type === 'box' const boxColor = (zone as any).box_bg_hex || '#6b7280' if (!zone.cells || zone.cells.length === 0) return null const left = zone.bbox_px.x * scale const top = zone.bbox_px.y * scale const width = zone.bbox_px.w * scale const height = zone.bbox_px.h * scale // Build cell map const cellMap = new Map() for (const cell of zone.cells) { cellMap.set(`${cell.row_index}_${cell.col_index}`, cell) } // Column widths (relative to zone) const colWidths = zone.columns.map((col) => { const w = (col.x_max_px ?? 0) - (col.x_min_px ?? 0) return Math.max(10, w * scale) }) const totalColW = colWidths.reduce((s, w) => s + w, 0) // Scale columns to fit zone width const colScale = totalColW > 0 ? width / totalColW : 1 const scaledColWidths = colWidths.map((w) => w * colScale) const gridTemplateCols = scaledColWidths.map((w) => `${w.toFixed(1)}px`).join(' ') const numCols = zone.columns.length return (
{zone.rows.map((row) => { const isSpanning = zone.cells.some( (c) => c.row_index === row.index && c.col_type === 'spanning_header', ) // Row height from measured px const measuredH = (row.y_max_px ?? 0) - (row.y_min_px ?? 0) const rowH = Math.max(fontSize * 1.4, measuredH * scale) return (
{isSpanning ? ( // Render spanning cells zone.cells .filter((c) => c.row_index === row.index && c.col_type === 'spanning_header') .sort((a, b) => a.col_index - b.col_index) .map((cell) => { const colspan = (cell as any).colspan || numCols const gridColStart = cell.col_index + 1 const gridColEnd = gridColStart + colspan const color = getCellColor(cell) return (
{cell.text}
) }) ) : ( // Render normal columns zone.columns.map((col) => { const cell = cellMap.get(`${row.index}_${col.index}`) if (!cell) { return
} const color = getCellColor(cell) const isBold = col.bold || cell.is_bold || row.is_header const text = cell.text ?? '' const isMultiLine = text.includes('\n') return (
{text}
) }) )}
) })}
) }