SpreadsheetView: formatting improvements for Excel-like display
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Failing after 21s
CI / test-go-edu-search (push) Failing after 19s
CI / test-python-klausur (push) Failing after 11s
CI / test-python-agent-core (push) Failing after 10s
CI / test-nodejs-website (push) Failing after 23s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Failing after 21s
CI / test-go-edu-search (push) Failing after 19s
CI / test-python-klausur (push) Failing after 11s
CI / test-python-agent-core (push) Failing after 10s
CI / test-nodejs-website (push) Failing after 23s
Height: sheet height auto-calculated from row count (26px/row + toolbar), no more cutoff at 21 rows. Row count set to exact (no padding). Box borders: thick colored outside border + thin inner grid lines. Content zone: light gray grid lines on all cells. Headers: bold (bl=1) for is_header rows. Larger font detected via word_box height comparison (>1.3x median → fs=12 + bold). Box cells: light tinted background from box_bg_hex. Header cells in boxes: slightly stronger tint. Multi-line cells: text wrap enabled (tb='2'), \n preserved. Bullet points (•) and indentation preserved in cell text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,9 @@ const Workbook = dynamic(
|
||||
|
||||
import '@fortune-sheet/react/dist/index.css'
|
||||
|
||||
import type { GridZone, GridEditorCell } from '@/components/grid-editor/types'
|
||||
import type { GridZone } from '@/components/grid-editor/types'
|
||||
|
||||
interface SpreadsheetViewProps {
|
||||
/** Multi-zone grid data (grid_editor_result) */
|
||||
gridData: any
|
||||
height?: number
|
||||
}
|
||||
@@ -36,10 +35,9 @@ function zoneToSheet(zone: GridZone, sheetIndex: number, isFirst: boolean): any
|
||||
if (!isBox) {
|
||||
name = 'Vokabeln'
|
||||
} else {
|
||||
// Use first cell text as name (truncated)
|
||||
const firstText = zone.cells?.[0]?.text ?? `Box ${sheetIndex}`
|
||||
const cleaned = firstText.replace(/[^\w\s\u00C0-\u024F]/g, '').trim()
|
||||
name = cleaned.length > 20 ? cleaned.slice(0, 20) + '...' : cleaned || `Box ${sheetIndex}`
|
||||
const cleaned = firstText.replace(/[^\w\s\u00C0-\u024F„"]/g, '').trim()
|
||||
name = cleaned.length > 25 ? cleaned.slice(0, 25) + '…' : cleaned || `Box ${sheetIndex}`
|
||||
}
|
||||
|
||||
const numRows = zone.rows?.length || 0
|
||||
@@ -52,30 +50,58 @@ function zoneToSheet(zone: GridZone, sheetIndex: number, isFirst: boolean): any
|
||||
for (const cell of (zone.cells || [])) {
|
||||
const r = cell.row_index
|
||||
const c = cell.col_index
|
||||
const text = cell.text ?? ''
|
||||
let text = cell.text ?? ''
|
||||
|
||||
// Detect if this is a header row
|
||||
const row = zone.rows?.find((rr) => rr.index === r)
|
||||
const isHeader = row?.is_header ?? false
|
||||
|
||||
// Detect if word_boxes indicate larger font (header detection)
|
||||
const avgWordHeight = cell.word_boxes?.length
|
||||
? cell.word_boxes.reduce((s: number, wb: any) => s + (wb.height || 0), 0) / cell.word_boxes.length
|
||||
: 0
|
||||
const zoneAvgHeight = zone.cells
|
||||
.flatMap((c: any) => (c.word_boxes || []).map((wb: any) => wb.height || 0))
|
||||
.filter((h: number) => h > 0)
|
||||
const medianHeight = zoneAvgHeight.length
|
||||
? [...zoneAvgHeight].sort((a, b) => a - b)[Math.floor(zoneAvgHeight.length / 2)]
|
||||
: 0
|
||||
const isLargerFont = avgWordHeight > 0 && medianHeight > 0 && avgWordHeight > medianHeight * 1.3
|
||||
|
||||
const v: any = {
|
||||
v: text,
|
||||
m: text,
|
||||
}
|
||||
|
||||
// Bold
|
||||
if (cell.is_bold) v.bl = 1
|
||||
|
||||
// Header row styling
|
||||
const row = zone.rows?.find((rr) => rr.index === r)
|
||||
if (row?.is_header) {
|
||||
// Bold: header rows, is_bold cells, or cells with detectably larger font
|
||||
if (cell.is_bold || isHeader || isLargerFont) {
|
||||
v.bl = 1
|
||||
v.bg = '#f0f4ff'
|
||||
}
|
||||
|
||||
// Text color
|
||||
// Larger font for detected large text (e.g., box titles)
|
||||
if (isLargerFont && isBox) {
|
||||
v.fs = 12 // slightly larger
|
||||
}
|
||||
|
||||
// Header row light background
|
||||
if (isHeader) {
|
||||
v.bg = isBox ? `${boxColor || '#2563eb'}18` : '#f0f4ff'
|
||||
}
|
||||
|
||||
// Box cells get light tinted background
|
||||
if (isBox && !isHeader && boxColor) {
|
||||
v.bg = `${boxColor}08`
|
||||
}
|
||||
|
||||
// Text color from OCR word_boxes
|
||||
const color = cell.color_override
|
||||
?? cell.word_boxes?.find((wb: any) => wb.color_name && wb.color_name !== 'black')?.color
|
||||
if (color) v.fc = color
|
||||
|
||||
// Multi-line: text wrap
|
||||
if (text.includes('\n')) v.tb = '2'
|
||||
// Multi-line text: enable text wrap and preserve formatting
|
||||
if (text.includes('\n')) {
|
||||
v.tb = '2' // text wrap enabled
|
||||
}
|
||||
|
||||
celldata.push({ r, c, v })
|
||||
|
||||
@@ -91,27 +117,54 @@ function zoneToSheet(zone: GridZone, sheetIndex: number, isFirst: boolean): any
|
||||
const columnlen: Record<string, number> = {}
|
||||
for (const col of (zone.columns || [])) {
|
||||
const pxW = (col.x_max_px ?? 0) - (col.x_min_px ?? 0)
|
||||
// Scale: wider for small column counts (boxes), narrower for many columns
|
||||
const scaleFactor = numCols <= 2 ? 0.6 : 0.4
|
||||
columnlen[String(col.index)] = Math.max(80, Math.round(pxW * scaleFactor))
|
||||
}
|
||||
|
||||
// Row heights
|
||||
// Row heights — taller for multi-line cells
|
||||
const rowlen: Record<string, number> = {}
|
||||
for (const row of (zone.rows || [])) {
|
||||
const rowCells = (zone.cells || []).filter((c: any) => c.row_index === row.index)
|
||||
const maxLines = Math.max(1, ...rowCells.map((c: any) => (c.text ?? '').split('\n').length))
|
||||
rowlen[String(row.index)] = Math.max(24, 24 * maxLines)
|
||||
const baseH = isBox ? 22 : 24
|
||||
rowlen[String(row.index)] = Math.max(baseH, baseH * maxLines)
|
||||
}
|
||||
|
||||
// Box border around entire content
|
||||
// Border info
|
||||
const borderInfo: any[] = []
|
||||
|
||||
// Box: colored outside border around entire sheet content
|
||||
if (isBox && boxColor && numRows > 0 && numCols > 0) {
|
||||
borderInfo.push({
|
||||
rangeType: 'range',
|
||||
borderType: 'border-outside',
|
||||
color: boxColor,
|
||||
style: 2, // thick
|
||||
style: 5, // thick (style 5 = thick in Fortune Sheet)
|
||||
range: [{
|
||||
row: [0, numRows - 1],
|
||||
column: [0, numCols - 1],
|
||||
}],
|
||||
})
|
||||
// Also add light inner borders for box cells
|
||||
borderInfo.push({
|
||||
rangeType: 'range',
|
||||
borderType: 'border-inside',
|
||||
color: `${boxColor}40`,
|
||||
style: 1, // thin
|
||||
range: [{
|
||||
row: [0, numRows - 1],
|
||||
column: [0, numCols - 1],
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
// Content zone: light grid lines
|
||||
if (!isBox && numRows > 0 && numCols > 0) {
|
||||
borderInfo.push({
|
||||
rangeType: 'range',
|
||||
borderType: 'border-all',
|
||||
color: '#e5e7eb',
|
||||
style: 1,
|
||||
range: [{
|
||||
row: [0, numRows - 1],
|
||||
column: [0, numCols - 1],
|
||||
@@ -123,9 +176,9 @@ function zoneToSheet(zone: GridZone, sheetIndex: number, isFirst: boolean): any
|
||||
name,
|
||||
id: `zone_${zone.zone_index}`,
|
||||
celldata,
|
||||
row: Math.max(numRows, 3), // minimum 3 rows
|
||||
row: numRows, // exact row count — no padding
|
||||
column: Math.max(numCols, 1),
|
||||
status: isFirst ? 1 : 0, // first sheet is active
|
||||
status: isFirst ? 1 : 0,
|
||||
color: isBox ? boxColor : undefined,
|
||||
config: {
|
||||
merge: Object.keys(merges).length > 0 ? merges : undefined,
|
||||
@@ -140,7 +193,6 @@ export function SpreadsheetView({ gridData, height = 600 }: SpreadsheetViewProps
|
||||
const sheets = useMemo(() => {
|
||||
if (!gridData?.zones) return []
|
||||
|
||||
// Sort: content zones first, then boxes by y-position
|
||||
const sorted = [...gridData.zones].sort((a: GridZone, b: GridZone) => {
|
||||
if (a.zone_type === 'content' && b.zone_type !== 'content') return -1
|
||||
if (a.zone_type !== 'content' && b.zone_type === 'content') return 1
|
||||
@@ -152,12 +204,16 @@ export function SpreadsheetView({ gridData, height = 600 }: SpreadsheetViewProps
|
||||
.map((z: GridZone, i: number) => zoneToSheet(z, i, i === 0))
|
||||
}, [gridData])
|
||||
|
||||
// Calculate minimum height to show all rows of the largest sheet
|
||||
const maxRows = Math.max(0, ...sheets.map((s: any) => s.row || 0))
|
||||
const estimatedHeight = Math.max(height, maxRows * 26 + 80) // 26px per row + toolbar
|
||||
|
||||
if (sheets.length === 0) {
|
||||
return <div className="p-4 text-center text-gray-400">Keine Daten für Spreadsheet.</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: `${height}px` }}>
|
||||
<div style={{ width: '100%', height: `${estimatedHeight}px` }}>
|
||||
<Workbook
|
||||
data={sheets}
|
||||
lang="en"
|
||||
|
||||
@@ -100,9 +100,9 @@ export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Fortune Sheet */}
|
||||
{/* RIGHT: Fortune Sheet — height adapts to content */}
|
||||
<div className="flex-1 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-900">
|
||||
<SpreadsheetView gridData={gridData} height={Math.max(650, leftHeight - 10)} />
|
||||
<SpreadsheetView gridData={gridData} height={Math.max(700, leftHeight)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user