Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
249 lines
9.0 KiB
TypeScript
249 lines
9.0 KiB
TypeScript
'use client'
|
|
|
|
import type { EditorMode, ReconstructionStatus } from './StepReconstructionTypes'
|
|
|
|
interface ReconstructionToolbarProps {
|
|
editorMode: EditorMode
|
|
setEditorMode: (mode: EditorMode) => void
|
|
isParentWithBoxes: boolean
|
|
cellCount: number
|
|
changedCount: number
|
|
emptyCellCount: number
|
|
showEmptyHighlight: boolean
|
|
setShowEmptyHighlight: (v: boolean) => void
|
|
showStructure: boolean
|
|
setShowStructure: (v: boolean) => void
|
|
hasStructureElements: boolean
|
|
zoom: number
|
|
setZoom: (fn: (z: number) => number) => void
|
|
undoCount: number
|
|
redoCount: number
|
|
onUndo: () => void
|
|
onRedo: () => void
|
|
status: ReconstructionStatus
|
|
onSave: () => void
|
|
// Overlay-specific
|
|
fontScale: number
|
|
setFontScale: (v: number) => void
|
|
globalBold: boolean
|
|
setGlobalBold: (fn: (b: boolean) => boolean) => void
|
|
imageRotation: 0 | 180
|
|
setImageRotation: (fn: (r: 0 | 180) => 0 | 180) => void
|
|
}
|
|
|
|
export function ReconstructionToolbar({
|
|
editorMode,
|
|
setEditorMode,
|
|
isParentWithBoxes,
|
|
cellCount,
|
|
changedCount,
|
|
emptyCellCount,
|
|
showEmptyHighlight,
|
|
setShowEmptyHighlight,
|
|
showStructure,
|
|
setShowStructure,
|
|
hasStructureElements,
|
|
zoom,
|
|
setZoom,
|
|
undoCount,
|
|
redoCount,
|
|
onUndo,
|
|
onRedo,
|
|
status,
|
|
onSave,
|
|
fontScale,
|
|
setFontScale,
|
|
globalBold,
|
|
setGlobalBold,
|
|
imageRotation,
|
|
setImageRotation,
|
|
}: ReconstructionToolbarProps) {
|
|
return (
|
|
<div className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Schritt 7: Rekonstruktion
|
|
</h3>
|
|
{/* Mode toggle */}
|
|
<div className="flex items-center ml-2 border border-gray-300 dark:border-gray-600 rounded overflow-hidden text-xs">
|
|
<button
|
|
onClick={() => setEditorMode('simple')}
|
|
className={`px-2 py-0.5 transition-colors ${
|
|
editorMode === 'simple'
|
|
? 'bg-teal-600 text-white'
|
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Einfach
|
|
</button>
|
|
{isParentWithBoxes && (
|
|
<button
|
|
onClick={() => setEditorMode('overlay')}
|
|
className={`px-2 py-0.5 transition-colors ${
|
|
editorMode === 'overlay'
|
|
? 'bg-teal-600 text-white'
|
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Overlay
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => setEditorMode('editor')}
|
|
className={`px-2 py-0.5 transition-colors ${
|
|
editorMode === 'editor'
|
|
? 'bg-teal-600 text-white'
|
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Editor
|
|
</button>
|
|
</div>
|
|
<span className="text-xs text-gray-400">
|
|
{cellCount} Zellen · {changedCount} geaendert
|
|
{emptyCellCount > 0 && showEmptyHighlight && (
|
|
<span className="text-red-400 ml-1">· {emptyCellCount} leer</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{/* Undo/Redo */}
|
|
<button
|
|
onClick={onUndo}
|
|
disabled={undoCount === 0}
|
|
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-30"
|
|
title="Rueckgaengig (Ctrl+Z)"
|
|
>
|
|
↩
|
|
</button>
|
|
<button
|
|
onClick={onRedo}
|
|
disabled={redoCount === 0}
|
|
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-30"
|
|
title="Wiederholen (Ctrl+Shift+Z)"
|
|
>
|
|
↪
|
|
</button>
|
|
|
|
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
|
|
|
|
{/* Overlay-specific toolbar */}
|
|
{editorMode === 'overlay' && (
|
|
<>
|
|
<label className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
|
|
Schrift
|
|
<input
|
|
type="range" min={30} max={120} value={Math.round(fontScale * 100)}
|
|
onChange={e => setFontScale(Number(e.target.value) / 100)}
|
|
className="w-20 h-1 accent-teal-600"
|
|
/>
|
|
<span className="w-8 text-right font-mono">{Math.round(fontScale * 100)}%</span>
|
|
</label>
|
|
<button
|
|
onClick={() => setGlobalBold(b => !b)}
|
|
className={`px-2 py-1 text-xs rounded border transition-colors font-bold ${
|
|
globalBold
|
|
? 'bg-teal-600 text-white border-teal-600'
|
|
: 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600'
|
|
}`}
|
|
>
|
|
B
|
|
</button>
|
|
<button
|
|
onClick={() => setImageRotation(r => r === 0 ? 180 : 0)}
|
|
className={`px-2 py-1 text-xs rounded border transition-colors ${
|
|
imageRotation === 180
|
|
? 'bg-teal-600 text-white border-teal-600'
|
|
: 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600'
|
|
}`}
|
|
title="Bild 180 Grad drehen"
|
|
>
|
|
180°
|
|
</button>
|
|
{hasStructureElements && (
|
|
<button
|
|
onClick={() => setShowStructure(!showStructure)}
|
|
className={`px-2 py-1 text-xs border rounded transition-colors ${
|
|
showStructure
|
|
? 'border-violet-300 bg-violet-50 text-violet-600 dark:border-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
|
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
title="Strukturelemente anzeigen"
|
|
>
|
|
Struktur
|
|
</button>
|
|
)}
|
|
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
|
|
</>
|
|
)}
|
|
|
|
{/* Non-overlay controls */}
|
|
{editorMode !== 'overlay' && (
|
|
<>
|
|
{/* Empty field toggle */}
|
|
<button
|
|
onClick={() => setShowEmptyHighlight(!showEmptyHighlight)}
|
|
className={`px-2 py-1 text-xs border rounded transition-colors ${
|
|
showEmptyHighlight
|
|
? 'border-red-300 bg-red-50 text-red-600 dark:border-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
title="Leere Pflichtfelder markieren"
|
|
>
|
|
Leer
|
|
</button>
|
|
|
|
{/* Structure toggle */}
|
|
{hasStructureElements && (
|
|
<button
|
|
onClick={() => setShowStructure(!showStructure)}
|
|
className={`px-2 py-1 text-xs border rounded transition-colors ${
|
|
showStructure
|
|
? 'border-violet-300 bg-violet-50 text-violet-600 dark:border-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
|
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
title="Strukturelemente anzeigen"
|
|
>
|
|
Struktur
|
|
</button>
|
|
)}
|
|
|
|
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
|
|
|
|
{/* Zoom controls */}
|
|
<button
|
|
onClick={() => setZoom(z => Math.max(50, z - 25))}
|
|
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
−
|
|
</button>
|
|
<span className="text-xs text-gray-500 w-10 text-center">{zoom}%</span>
|
|
<button
|
|
onClick={() => setZoom(z => Math.min(200, z + 25))}
|
|
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
+
|
|
</button>
|
|
<button
|
|
onClick={() => setZoom(() => 100)}
|
|
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
Fit
|
|
</button>
|
|
|
|
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
|
|
</>
|
|
)}
|
|
|
|
<button
|
|
onClick={onSave}
|
|
disabled={status === 'saving'}
|
|
className="px-4 py-1.5 text-xs bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50 transition-colors font-medium"
|
|
>
|
|
{status === 'saving' ? 'Speichert...' : 'Speichern'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|