Own it · example handover · app/
src/ui/grid.tsx
← All files · the demo this builds
/**
* The "before": a purpose-built grid that reproduces Excel's grammar from
* the workbook description — column letters, row numbers, gridlines,
* merged spans, cell formats, the works. It is deliberately NOT a
* spreadsheet engine; it renders the description faithfully, damage
* included, and the same description feeds the .xlsx download.
*/
import { addr, parseAddr, type WbSheet, type WbStyle } from "@portfolio/fabricator";
function serialToUS(serial: number): string {
const ms = Date.UTC(1899, 11, 30) + serial * 86_400_000;
const d = new Date(ms);
return `${d.getUTCMonth() + 1}/${d.getUTCDate()}/${d.getUTCFullYear()}`;
}
export function displayValue(v: string | number | undefined, t: string | undefined, style: WbStyle | undefined): string {
if (v === undefined) return "";
if (t === "d" && typeof v === "number") return serialToUS(v);
if (typeof v === "number" && style?.fmt === "money") {
return v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
return String(v);
}
export function Grid({ sheet, styles, selected, onSelect }: {
sheet: WbSheet;
styles: WbStyle[];
selected: string | null;
onSelect: (a: string) => void;
}) {
// merged ranges: the anchor spans; covered cells don't render
const span = new Map<string, { c: number; r: number }>();
const covered = new Set<string>();
for (const m of sheet.merges ?? []) {
const [from, to] = m.split(":");
const a = parseAddr(from!);
const b = parseAddr(to!);
span.set(from!, { c: b.c - a.c + 1, r: b.r - a.r + 1 });
for (let r = a.r; r <= b.r; r++) {
for (let c = a.c; c <= b.c; c++) {
if (!(r === a.r && c === a.c)) covered.add(addr(c, r));
}
}
}
const cols = Array.from({ length: sheet.nCols }, (_, c) => c);
const rows = Array.from({ length: sheet.nRows }, (_, r) => r);
const colW = (c: number) => Math.round((sheet.cols?.[c] ?? 9) * 7.2);
return (
<table class="ssr-grid" role="grid" aria-label={sheet.name}>
<thead>
<tr>
<th class="ssr-corner" />
{cols.map((c) => (
<th scope="col" class="ssr-colhead" style={{ minWidth: `${colW(c)}px` }}>
{addr(c, 0).replace(/\d+$/, "")}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr>
<th scope="row" class="ssr-rowhead">{r + 1}</th>
{cols.map((c) => {
const a = addr(c, r);
if (covered.has(a)) return null;
const cell = sheet.cells[a];
const sp = span.get(a);
const st = cell?.s ? styles[cell.s] : undefined;
const cls = [
"ssr-cell",
cell?.s ? `st-${cell.s}` : "",
cell?.t === "e" ? "is-err" : "",
typeof cell?.v === "number" && !st?.align ? "is-num" : "",
selected === a ? "is-selected" : "",
].join(" ");
return (
<td
class={cls}
colSpan={sp?.c}
rowSpan={sp?.r}
data-addr={a}
onClick={() => onSelect(a)}
>
{displayValue(cell?.v, cell?.t, st)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
}