[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>
This commit is contained in:
@@ -1,44 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { GridZone, LayoutMetrics } from './types'
|
||||
|
||||
interface GridTableProps {
|
||||
zone: GridZone
|
||||
layoutMetrics?: LayoutMetrics
|
||||
selectedCell: string | null
|
||||
selectedCells?: Set<string>
|
||||
onSelectCell: (cellId: string) => void
|
||||
onToggleCellSelection?: (cellId: string) => void
|
||||
onCellTextChange: (cellId: string, text: string) => void
|
||||
onToggleColumnBold: (zoneIndex: number, colIndex: number) => void
|
||||
onToggleRowHeader: (zoneIndex: number, rowIndex: number) => void
|
||||
onNavigate: (cellId: string, direction: 'up' | 'down' | 'left' | 'right') => void
|
||||
onDeleteColumn?: (zoneIndex: number, colIndex: number) => void
|
||||
onAddColumn?: (zoneIndex: number, afterColIndex: number) => void
|
||||
onDeleteRow?: (zoneIndex: number, rowIndex: number) => void
|
||||
onAddRow?: (zoneIndex: number, afterRowIndex: number) => void
|
||||
onSetCellColor?: (cellId: string, color: string | null | undefined) => void
|
||||
}
|
||||
|
||||
/** Color palette for the right-click cell color menu. */
|
||||
const COLOR_OPTIONS: { label: string; value: string | null }[] = [
|
||||
{ label: 'Rot', value: '#dc2626' },
|
||||
{ label: 'Gruen', value: '#16a34a' },
|
||||
{ label: 'Blau', value: '#2563eb' },
|
||||
{ label: 'Orange', value: '#ea580c' },
|
||||
{ label: 'Lila', value: '#9333ea' },
|
||||
{ label: 'Schwarz', value: null },
|
||||
]
|
||||
|
||||
/** Gutter width for row numbers (px). */
|
||||
const ROW_NUM_WIDTH = 36
|
||||
|
||||
/** Minimum column width in px so columns remain usable. */
|
||||
const MIN_COL_WIDTH = 80
|
||||
|
||||
/** Minimum row height in px. */
|
||||
const MIN_ROW_HEIGHT = 26
|
||||
import type { GridEditorCell } from './types'
|
||||
import { MIN_COL_WIDTH, MIN_ROW_HEIGHT, ROW_NUM_WIDTH } from './gridTableConstants'
|
||||
import type { GridTableProps } from './gridTableConstants'
|
||||
import { getCellColor, getRowHeight } from './gridTableUtils'
|
||||
import { GridTableColumnHeader } from './GridTableColumnHeader'
|
||||
import { GridTableRowHeader } from './GridTableRowHeader'
|
||||
import { GridTableCell } from './GridTableCell'
|
||||
import { GridTableColorMenu } from './GridTableColorMenu'
|
||||
|
||||
export function GridTable({
|
||||
zone,
|
||||
@@ -77,10 +47,6 @@ export function GridTable({
|
||||
// ----------------------------------------------------------------
|
||||
// Compute column widths from OCR measurements
|
||||
// ----------------------------------------------------------------
|
||||
// Use the actual total column span as reference width — NOT zone.bbox_px.w.
|
||||
// When union columns are applied across content zones, column boundaries
|
||||
// can extend beyond the zone's bbox, causing overflow if we scale by
|
||||
// the smaller zone width.
|
||||
const [colWidthOverrides, setColWidthOverrides] = useState<number[] | null>(null)
|
||||
|
||||
const columnWidthsPx = zone.columns.map((col) => col.x_max_px - col.x_min_px)
|
||||
@@ -95,31 +61,16 @@ export function GridTable({
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Compute row heights from OCR measurements
|
||||
// Row height
|
||||
// ----------------------------------------------------------------
|
||||
const avgRowHeightPx = layoutMetrics?.avg_row_height_px ?? 30
|
||||
const [rowHeightOverrides, setRowHeightOverrides] = useState<Map<number, number>>(new Map())
|
||||
|
||||
const getRowHeight = (rowIndex: number, isHeader: boolean): number => {
|
||||
const computeRowHeight = (rowIndex: number, isHeader: boolean): number => {
|
||||
if (rowHeightOverrides.has(rowIndex)) {
|
||||
return rowHeightOverrides.get(rowIndex)!
|
||||
}
|
||||
const row = zone.rows.find((r) => r.index === rowIndex)
|
||||
if (!row) return Math.max(MIN_ROW_HEIGHT, avgRowHeightPx * scale)
|
||||
|
||||
// Multi-line cells (containing \n): expand height based on line count
|
||||
const rowCells = zone.cells.filter((c) => c.row_index === rowIndex)
|
||||
const maxLines = Math.max(1, ...rowCells.map((c) => (c.text ?? '').split('\n').length))
|
||||
if (maxLines > 1) {
|
||||
const lineH = Math.max(MIN_ROW_HEIGHT, avgRowHeightPx * scale)
|
||||
return lineH * maxLines
|
||||
}
|
||||
|
||||
if (isHeader) {
|
||||
const measuredH = row.y_max_px - row.y_min_px
|
||||
return Math.max(MIN_ROW_HEIGHT, measuredH * scale)
|
||||
}
|
||||
return Math.max(MIN_ROW_HEIGHT, avgRowHeightPx * scale)
|
||||
return getRowHeight(zone, rowIndex, isHeader, avgRowHeightPx, scale)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
@@ -162,26 +113,11 @@ export function GridTable({
|
||||
// ----------------------------------------------------------------
|
||||
// Cell lookup
|
||||
// ----------------------------------------------------------------
|
||||
const cellMap = new Map<string, (typeof zone.cells)[0]>()
|
||||
const cellMap = new Map<string, GridEditorCell>()
|
||||
for (const cell of zone.cells) {
|
||||
cellMap.set(`${cell.row_index}_${cell.col_index}`, cell)
|
||||
}
|
||||
|
||||
/** Dominant non-black color from a cell's word_boxes, or null.
|
||||
* `color_override` takes priority when set. */
|
||||
const getCellColor = (cell: (typeof zone.cells)[0] | undefined): string | null => {
|
||||
if (!cell) return null
|
||||
// Manual override: explicit color or null (= "clear color bar")
|
||||
if (cell.color_override !== undefined) return cell.color_override ?? null
|
||||
if (!cell.word_boxes?.length) return null
|
||||
for (const wb of cell.word_boxes) {
|
||||
if (wb.color_name && wb.color_name !== 'black' && wb.color) {
|
||||
return wb.color
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Column resize (drag)
|
||||
// ----------------------------------------------------------------
|
||||
@@ -193,7 +129,6 @@ export function GridTable({
|
||||
const deltaPx = (e.clientX - startX) / scale
|
||||
const newWidths = [...baseWidths]
|
||||
newWidths[colIndex] = Math.max(20, baseWidths[colIndex] + deltaPx)
|
||||
// Steal from next column to keep total constant
|
||||
if (colIndex + 1 < newWidths.length) {
|
||||
newWidths[colIndex + 1] = Math.max(20, baseWidths[colIndex + 1] - deltaPx)
|
||||
}
|
||||
@@ -248,7 +183,6 @@ export function GridTable({
|
||||
const isBoxZone = zone.zone_type === 'box'
|
||||
const numCols = zone.columns.length
|
||||
|
||||
// CSS Grid template for columns: row-number gutter + proportional columns
|
||||
const gridTemplateCols = `${ROW_NUM_WIDTH}px ${effectiveColWidths.map((w) => `${w.toFixed(1)}px`).join(' ')}`
|
||||
|
||||
return (
|
||||
@@ -272,9 +206,7 @@ export function GridTable({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ============================================================ */}
|
||||
{/* CSS Grid — column headers */}
|
||||
{/* ============================================================ */}
|
||||
{/* CSS Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
@@ -286,72 +218,24 @@ export function GridTable({
|
||||
{/* Header: row-number corner */}
|
||||
<div className="sticky left-0 z-10 px-1 py-1.5 text-[10px] text-gray-400 dark:text-gray-500 border-b border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50" />
|
||||
|
||||
{/* Header: column labels with resize handles + delete/add */}
|
||||
{/* Header: column labels */}
|
||||
{zone.columns.map((col, ci) => (
|
||||
<div
|
||||
<GridTableColumnHeader
|
||||
key={col.index}
|
||||
className={`group/colhdr relative px-2 py-1.5 text-xs font-medium border-b border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 cursor-pointer select-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||
col.bold ? 'text-teal-700 dark:text-teal-300' : 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
onClick={() => onToggleColumnBold(zone.zone_index, col.index)}
|
||||
title={`Spalte ${col.index + 1} — Klick fuer Fett-Toggle`}
|
||||
>
|
||||
<div className="flex items-center gap-1 justify-center truncate">
|
||||
<span>{col.label}</span>
|
||||
{col.bold && (
|
||||
<span className="text-[9px] px-1 py-0 rounded bg-teal-100 dark:bg-teal-900/40 text-teal-600 dark:text-teal-400">
|
||||
B
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Delete column button (visible on hover) */}
|
||||
{onDeleteColumn && numCols > 1 && (
|
||||
<button
|
||||
className="absolute top-0 left-0 w-4 h-4 flex items-center justify-center bg-red-500 text-white rounded-br text-[9px] leading-none opacity-0 group-hover/colhdr:opacity-100 transition-opacity z-30"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm(`Spalte "${col.label}" loeschen?`)) {
|
||||
onDeleteColumn(zone.zone_index, col.index)
|
||||
}
|
||||
}}
|
||||
title={`Spalte "${col.label}" loeschen`}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
)}
|
||||
{/* Add column button — small icon at bottom-right, below resize handle */}
|
||||
{onAddColumn && (
|
||||
<button
|
||||
className="absolute -right-[7px] -bottom-[7px] w-[14px] h-[14px] flex items-center justify-center text-teal-500 opacity-0 group-hover/colhdr:opacity-100 transition-opacity z-30 hover:bg-teal-100 dark:hover:bg-teal-900/40 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onAddColumn(zone.zone_index, col.index)
|
||||
}}
|
||||
title={`Spalte nach "${col.label}" einfuegen`}
|
||||
>
|
||||
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{/* Right-edge resize handle — wide grab area, highest z-index */}
|
||||
{ci < numCols - 1 && (
|
||||
<div
|
||||
className="absolute top-0 -right-[4px] w-[9px] h-full cursor-col-resize hover:bg-teal-400/40 z-40"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
handleColResizeStart(ci, e.clientX)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
col={col}
|
||||
colIndex={ci}
|
||||
numCols={numCols}
|
||||
zoneIndex={zone.zone_index}
|
||||
onToggleColumnBold={onToggleColumnBold}
|
||||
onDeleteColumn={onDeleteColumn}
|
||||
onAddColumn={onAddColumn}
|
||||
onColResizeStart={handleColResizeStart}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* ============================================================ */}
|
||||
{/* Data rows */}
|
||||
{/* ============================================================ */}
|
||||
{/* Data rows */}
|
||||
{zone.rows.map((row) => {
|
||||
const rowH = getRowHeight(row.index, row.is_header)
|
||||
const rowH = computeRowHeight(row.index, row.is_header)
|
||||
const isSpanning = zone.cells.some(
|
||||
(c) => c.row_index === row.index && c.col_type === 'spanning_header',
|
||||
)
|
||||
@@ -359,60 +243,16 @@ export function GridTable({
|
||||
return (
|
||||
<div key={row.index} style={{ display: 'contents' }}>
|
||||
{/* Row number cell */}
|
||||
<div
|
||||
className={`group/rowhdr relative sticky left-0 z-10 flex items-center justify-center text-[10px] border-b border-r border-gray-200 dark:border-gray-700 cursor-pointer select-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||
row.is_header
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium'
|
||||
: row.is_footer
|
||||
? 'bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 font-medium'
|
||||
: 'bg-gray-50 dark:bg-gray-800/50 text-gray-400 dark:text-gray-500'
|
||||
}`}
|
||||
style={{ height: `${rowH}px` }}
|
||||
onClick={() => onToggleRowHeader(zone.zone_index, row.index)}
|
||||
title={`Zeile ${row.index + 1} — Klick: ${row.is_header ? 'Footer' : row.is_footer ? 'Normal' : 'Header'}`}
|
||||
>
|
||||
{row.index + 1}
|
||||
{row.is_header && <span className="block text-[8px]">H</span>}
|
||||
{row.is_footer && <span className="block text-[8px]">F</span>}
|
||||
{/* Delete row button (visible on hover) */}
|
||||
{onDeleteRow && zone.rows.length > 1 && (
|
||||
<button
|
||||
className="absolute top-0 left-0 w-4 h-4 flex items-center justify-center bg-red-500 text-white rounded-br text-[9px] leading-none opacity-0 group-hover/rowhdr:opacity-100 transition-opacity z-30"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm(`Zeile ${row.index + 1} loeschen?`)) {
|
||||
onDeleteRow(zone.zone_index, row.index)
|
||||
}
|
||||
}}
|
||||
title={`Zeile ${row.index + 1} loeschen`}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
)}
|
||||
{/* Add row button (visible on hover, below this row) */}
|
||||
{onAddRow && (
|
||||
<button
|
||||
className="absolute -bottom-[7px] left-0 w-full h-[14px] flex items-center justify-center text-teal-500 opacity-0 group-hover/rowhdr:opacity-100 transition-opacity z-30 hover:bg-teal-100 dark:hover:bg-teal-900/40 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onAddRow(zone.zone_index, row.index)
|
||||
}}
|
||||
title={`Zeile nach ${row.index + 1} einfuegen`}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{/* Bottom-edge resize handle */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-full h-[4px] cursor-row-resize hover:bg-teal-400/40 z-20"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRowResizeStart(row.index, e.clientY, rowH)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<GridTableRowHeader
|
||||
row={row}
|
||||
zoneIndex={zone.zone_index}
|
||||
rowCount={zone.rows.length}
|
||||
rowH={rowH}
|
||||
onToggleRowHeader={onToggleRowHeader}
|
||||
onDeleteRow={onDeleteRow}
|
||||
onAddRow={onAddRow}
|
||||
onRowResizeStart={handleRowResizeStart}
|
||||
/>
|
||||
|
||||
{/* Cells — spanning header or normal columns */}
|
||||
{isSpanning ? (
|
||||
@@ -456,143 +296,24 @@ export function GridTable({
|
||||
) : (
|
||||
zone.columns.map((col) => {
|
||||
const cell = cellMap.get(`${row.index}_${col.index}`)
|
||||
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 (e.g. 5h
|
||||
// slash-IPA → bracket conversion) 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)
|
||||
|
||||
return (
|
||||
<div
|
||||
<GridTableCell
|
||||
key={col.index}
|
||||
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()
|
||||
setColorMenu({ cellId, x: e.clientX, y: 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 */}
|
||||
{(() => {
|
||||
const cellText = cell?.text ?? ''
|
||||
const isMultiLine = cellText.includes('\n')
|
||||
if (hasColoredWords && !isSelected) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
if (isMultiLine) {
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<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>
|
||||
cell={cell}
|
||||
col={col}
|
||||
row={row}
|
||||
zone={zone}
|
||||
rowH={rowH}
|
||||
selectedCell={selectedCell}
|
||||
selectedCells={selectedCells}
|
||||
onSelectCell={onSelectCell}
|
||||
onToggleCellSelection={onToggleCellSelection}
|
||||
onCellTextChange={onCellTextChange}
|
||||
onNavigate={onNavigate}
|
||||
onSetCellColor={onSetCellColor}
|
||||
onOpenColorMenu={(cellId, x, y) => setColorMenu({ cellId, x, y })}
|
||||
handleKeyDown={handleKeyDown}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
@@ -603,49 +324,13 @@ export function GridTable({
|
||||
|
||||
{/* Color context menu (right-click) */}
|
||||
{colorMenu && onSetCellColor && (
|
||||
<div
|
||||
className="fixed inset-0 z-50"
|
||||
onClick={() => setColorMenu(null)}
|
||||
onContextMenu={(e) => { e.preventDefault(); setColorMenu(null) }}
|
||||
>
|
||||
<div
|
||||
className="absolute bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[140px]"
|
||||
style={{ left: colorMenu.x, top: colorMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="px-3 py-1 text-[10px] text-gray-400 dark:text-gray-500 font-medium uppercase tracking-wider">
|
||||
Textfarbe
|
||||
</div>
|
||||
{COLOR_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.label}
|
||||
className="w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
||||
onClick={() => {
|
||||
onSetCellColor(colorMenu.cellId, opt.value)
|
||||
setColorMenu(null)
|
||||
}}
|
||||
>
|
||||
{opt.value ? (
|
||||
<span className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: opt.value }} />
|
||||
) : (
|
||||
<span className="w-3 h-3 rounded-full flex-shrink-0 border border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
<span>{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 mt-1 pt-1">
|
||||
<button
|
||||
className="w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400"
|
||||
onClick={() => {
|
||||
onSetCellColor(colorMenu.cellId, undefined)
|
||||
setColorMenu(null)
|
||||
}}
|
||||
>
|
||||
Farbe zuruecksetzen (OCR)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GridTableColorMenu
|
||||
cellId={colorMenu.cellId}
|
||||
x={colorMenu.x}
|
||||
y={colorMenu.y}
|
||||
onSetCellColor={onSetCellColor}
|
||||
onClose={() => setColorMenu(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user