Files
breakpilot-lehrer/admin-lehrer/components/grid-editor/GridTableCell.tsx
Benjamin Admin b681ddb131 [split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)
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>
2026-04-24 17:28:57 +02:00

170 lines
5.8 KiB
TypeScript

'use client'
import type { GridColumn, GridEditorCell, GridRow } from './types'
import { getCellColor } from './gridTableUtils'
interface GridTableCellProps {
cell: GridEditorCell | undefined
col: GridColumn
row: GridRow
zone: { zone_index: number }
rowH: number
selectedCell: string | null
selectedCells?: Set<string>
onSelectCell: (cellId: string) => void
onToggleCellSelection?: (cellId: string) => void
onCellTextChange: (cellId: string, text: string) => void
onNavigate: (cellId: string, direction: 'up' | 'down' | 'left' | 'right') => void
onSetCellColor?: (cellId: string, color: string | null | undefined) => void
onOpenColorMenu: (cellId: string, x: number, y: number) => void
handleKeyDown: (e: React.KeyboardEvent, cellId: string) => void
}
export function GridTableCell({
cell,
col,
row,
zone,
rowH,
selectedCell,
selectedCells,
onSelectCell,
onToggleCellSelection,
onCellTextChange,
onNavigate,
onSetCellColor,
onOpenColorMenu,
handleKeyDown,
}: GridTableCellProps) {
const cellId =
cell?.cell_id ??
`Z${zone.zone_index}_R${String(row.index).padStart(2, '0')}_C${col.index}`
const isSelected = selectedCell === cellId
const isBold = col.bold || cell?.is_bold
const isLowConf = cell && cell.confidence > 0 && cell.confidence < 60
const isMultiSelected = selectedCells?.has(cellId)
// Show per-word colored display only when word_boxes match the cell text.
// Post-processing steps modify cell.text but not individual word_boxes,
// so we fall back to the plain input when they diverge.
const wbText = cell?.word_boxes?.map((wb) => wb.text).join(' ') ?? ''
const textMatches = !cell?.text || wbText === cell.text
// Color: prefer manual override, else word_boxes when text matches
const hasOverride = cell?.color_override !== undefined
const cellColor = hasOverride ? getCellColor(cell) : (textMatches ? getCellColor(cell) : null)
const hasColoredWords =
!hasOverride &&
textMatches &&
(cell?.word_boxes?.some(
(wb) => wb.color_name && wb.color_name !== 'black',
) ?? false)
const cellText = cell?.text ?? ''
const isMultiLine = cellText.includes('\n')
return (
<div
className={`relative border-b border-r border-gray-200 dark:border-gray-700 flex items-center ${
isSelected ? 'ring-2 ring-teal-500 ring-inset z-10' : ''
} ${isMultiSelected ? 'bg-teal-50/60 dark:bg-teal-900/20' : ''} ${
isLowConf && !isMultiSelected ? 'bg-amber-50/50 dark:bg-amber-900/10' : ''
} ${row.is_header && !isMultiSelected ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`}
style={{
height: `${rowH}px`,
...(cell?.box_region?.bg_hex ? {
backgroundColor: `${cell.box_region.bg_hex}12`,
borderLeft: cell.box_region.border ? `3px solid ${cell.box_region.bg_hex}60` : undefined,
} : {}),
}}
onContextMenu={(e) => {
if (onSetCellColor) {
e.preventDefault()
onOpenColorMenu(cellId, e.clientX, e.clientY)
}
}}
>
{cellColor && (
<span
className="flex-shrink-0 w-1.5 self-stretch rounded-l-sm"
style={{ backgroundColor: cellColor }}
title={`Farbe: ${cell?.word_boxes?.find((wb) => wb.color_name !== 'black')?.color_name}`}
/>
)}
{/* Per-word colored display when not editing */}
{hasColoredWords && !isSelected ? (
<div
className={`w-full px-2 cursor-text truncate ${isBold ? 'font-bold' : 'font-normal'}`}
onClick={(e) => {
if ((e.metaKey || e.ctrlKey) && onToggleCellSelection) {
onToggleCellSelection(cellId)
} else {
onSelectCell(cellId)
setTimeout(() => document.getElementById(`cell-${cellId}`)?.focus(), 0)
}
}}
>
{cell!.word_boxes!.map((wb, i) => (
<span
key={i}
style={
wb.color_name && wb.color_name !== 'black'
? { color: wb.color }
: undefined
}
>
{wb.text}
{i < cell!.word_boxes!.length - 1 ? ' ' : ''}
</span>
))}
</div>
) : isMultiLine ? (
<textarea
id={`cell-${cellId}`}
value={cellText}
onChange={(e) => onCellTextChange(cellId, e.target.value)}
onFocus={() => onSelectCell(cellId)}
onClick={(e) => {
if ((e.metaKey || e.ctrlKey) && onToggleCellSelection) {
e.preventDefault()
onToggleCellSelection(cellId)
}
}}
onKeyDown={(e) => {
if (e.key === 'Tab') {
e.preventDefault()
onNavigate(cellId, e.shiftKey ? 'left' : 'right')
}
}}
rows={cellText.split('\n').length}
className={`w-full px-2 bg-transparent border-0 outline-none resize-none ${
isBold ? 'font-bold' : 'font-normal'
}`}
style={{ color: cellColor || undefined }}
spellCheck={false}
/>
) : (
<input
id={`cell-${cellId}`}
type="text"
value={cellText}
onChange={(e) => onCellTextChange(cellId, e.target.value)}
onFocus={() => onSelectCell(cellId)}
onClick={(e) => {
if ((e.metaKey || e.ctrlKey) && onToggleCellSelection) {
e.preventDefault()
onToggleCellSelection(cellId)
}
}}
onKeyDown={(e) => handleKeyDown(e, cellId)}
className={`w-full px-2 bg-transparent border-0 outline-none ${
isBold ? 'font-bold' : 'font-normal'
}`}
style={{ color: cellColor || undefined }}
spellCheck={false}
/>
)}
</div>
)
}