Repository Public
1// Workbook preview: the worker read the values of each sheet (never formulas' code, never macros) into JSON.2import { useMemo, useState } from 'react';3import { useT } from '../i18n.ts';4import type { PreviewSource } from '../types.ts';5import { cx } from '../ui.tsx';6import { columnLetter, Grid } from './Grid.tsx';7import { DerivedGate, Note, useDerived } from './shared.tsx';89/** Same shape as SheetPreview in services/api/src/tools-sheet.ts. */10export interface SheetPreview { sheets: { name: string; rows: string[][]; totalRows: number; totalColumns: number; truncated: boolean }[]; truncatedSheets: boolean }11const LIMIT = { rows: 1000, columns: 60, sheets: 30 };1213function Workbook({ data }: { data: SheetPreview }) {14 const t = useT();15 const [index, setIndex] = useState(0);16 const sheet = data.sheets[Math.min(index, data.sheets.length - 1)];17 const columns = useMemo(() => (sheet ? sheet.rows.reduce((m, r) => Math.max(m, r.length), 0) : 0), [sheet]);18 const header = useMemo(() => Array.from({ length: columns }, (_, i) => columnLetter(i)), [columns]);19 if (!sheet) return <Note testId="empty-note">{t('preview.sheetNone')}</Note>;20 return (21 <>22 {sheet.truncated ? <Note tone="warn" testId="sheet-truncated">{t('preview.sheetTruncated', { rows: LIMIT.rows, cols: LIMIT.columns, totalRows: sheet.totalRows, totalCols: sheet.totalColumns })}</Note> : null}23 {data.truncatedSheets ? <Note tone="warn" testId="sheets-truncated">{t('preview.sheetsTruncated', { n: LIMIT.sheets })}</Note> : null}24 {sheet.rows.length === 0 || columns === 025 ? <div className="flex min-h-0 flex-1 items-center justify-center bg-surface p-6 text-sm text-ink-3" data-testid="sheet-empty">{t('preview.sheetEmpty')}</div>26 : <Grid header={header} rows={sheet.rows} columns={columns} label={sheet.name} />}27 <div role="tablist" aria-label={t('preview.sheets')} className="flex shrink-0 items-stretch gap-1 overflow-x-auto border-t border-line bg-surface px-2 py-1" data-noswipe>28 {data.sheets.map((s, i) => (29 <button key={i} type="button" role="tab" aria-selected={i === index} onClick={() => setIndex(i)} data-testid="sheet-tab"30 className={cx('h-10 max-w-48 shrink-0 truncate rounded-xl px-3 text-sm font-medium transition', i === index ? 'bg-accent-soft text-accent' : 'text-ink-2 hover:bg-hover hover:text-ink')}>{s.name}</button>31 ))}32 </div>33 </>34 );35}36export function SheetView({ source }: { source: PreviewSource }) {37 const [state, retry] = useDerived<SheetPreview>(source.previewUrl('sheet'), 'json');38 return <DerivedGate state={state} retry={retry} source={source}>{(data) => <Workbook data={data} />}</DerivedGate>;39}40