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>
  );
}