> For the complete documentation index, see [llms.txt](https://docs.rowsncolumns.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rowsncolumns.app/configuration/features/print-preview.md).

# Print preview

`PrintPreviewDialog` is a self-contained, full-screen modal that paginates the active sheet into printable page-sized blocks, renders the same blocks into a hidden iframe with the appropriate `@page` rules, and triggers the browser print dialog. Modelled on Google Sheets' Print pane — same sidebar controls, same paginated preview area, same paper sizes.

## Is this a breaking change?

**No.** Print preview is purely additive:

* New named export from `@rowsncolumns/spreadsheet`: `PrintPreviewDialog` and `PrintSettings` / `PaperSizeId` / related types.
* `ButtonPrint`'s API is unchanged — it's still a thin `ToolbarIconButton`. The only thing that updates is **your `onClick` handler** (you used to call `window.print()`, now you open the dialog).
* No props on `useSpreadsheetState`, `CanvasGrid`, or any existing component change. If you don't render `PrintPreviewDialog`, nothing happens.

Upgrade path: drop the new dialog into your app and rewire `ButtonPrint`. That's it.

## Why a dialog instead of `window.print()`?

`window.print()` prints the **entire host page** — your toolbar, formula bar, sidebar, panels, ads, whatever else lives on the route. That's almost never what a user wants when they hit ⌘P on a spreadsheet.

The dialog instead:

1. Paginates the **sheet data** into page-sized blocks at the chosen paper size + orientation + scale.
2. Renders each block as a self-contained HTML table styled from `getEffectiveFormat`.
3. On **Print**, snapshots only those blocks into a hidden iframe alongside the matching `@page { size: ... }` CSS, then calls `iframe.contentWindow.print()`. The browser print dialog shows the actual paginated spreadsheet, nothing else from your app.

## Integration

The dialog reads everything it needs from values you already have on `useSpreadsheetState`. No adapter object; the props are flat.

```tsx
import { useState, useEffect } from "react";
import {
  PrintPreviewDialog,
  ButtonPrint,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function App() {
  const {
    sheets,
    activeSheetId,
    selections,
    getFormattedValue,
    getEffectiveFormat,
    getDataRowCount,
    getDataColumnCount,
    getCellData,
  } = useSpreadsheetState({ /* ... */ });

  const [isPrintOpen, setPrintOpen] = useState(false);

  // ⌘P / Ctrl+P hijack + Esc dismiss. Kept in user space rather than
  // baking into `PrintPreviewDialog` so an embedding app can decide
  // whether Esc should dismiss other overlays first.
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "p") {
        e.preventDefault();
        setPrintOpen(true);
        return;
      }
      if (e.key === "Escape" && isPrintOpen) {
        e.preventDefault();
        setPrintOpen(false);
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [isPrintOpen]);

  return (
    <>
      <ButtonPrint onClick={() => setPrintOpen(true)} />

      <PrintPreviewDialog
        isOpen={isPrintOpen}
        onClose={() => setPrintOpen(false)}
        title="My Workbook"
        sheets={sheets}
        activeSheetId={activeSheetId}
        selections={selections}
        getFormattedValue={getFormattedValue}
        getEffectiveFormat={getEffectiveFormat}
        getDataRowCount={getDataRowCount}
        getDataColumnCount={getDataColumnCount}
        getCellData={getCellData}
      />
    </>
  );
}
```

That's the whole integration. The dialog manages its own settings state internally — the host doesn't need to persist or store anything. Settings carry over across open/close cycles within the same mount, so a user who picks A4 + Landscape and reopens still sees A4 + Landscape. If you want every open to start fresh, pass a changing `key` (e.g. `key={isPrintOpen ? "open" : "closed"}`) — React will remount the component and reset state to defaults.

## `PrintPreviewDialog` props

| Prop                 | Type                                | Required | Purpose                                                                                                                                                |
| -------------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isOpen`             | `boolean`                           | ✅        | Controlled visibility.                                                                                                                                 |
| `onClose`            | `() => void`                        | ✅        | Fires on Cancel, the close icon, or after Print. (Esc binding lives in user space — see Integration above.)                                            |
| `sheets`             | `Sheet[]`                           | ✅        | Full sheet list — same array on `useSpreadsheetState.sheets`. Drives both the active-sheet lookup and the "Workbook" scope.                            |
| `activeSheetId`      | `number`                            | ✅        | Which sheet to preview. The dialog looks it up in `sheets`.                                                                                            |
| `selections`         | `SelectionArea[]?`                  |          | The first selection's range powers the "Selection" scope. Pass `useSpreadsheetState.selections` straight through; omit / pass `[]` to hide the option. |
| `title`              | `string?`                           |          | Workbook display name. Shown in the header strip when the "Workbook title" header field is enabled.                                                    |
| `getFormattedValue`  | `(sheetId, row, col) => string`     | ✅        | From `useSpreadsheetState`.                                                                                                                            |
| `getEffectiveFormat` | `(sheetId, row, col) => CellFormat` | ✅        | From `useSpreadsheetState`.                                                                                                                            |
| `getDataRowCount`    | `(sheetId) => number?`              |          | When present, restricts the print extent to the last non-empty row (matches Google Sheets). Falls back to `sheet.rowCount`.                            |
| `getDataColumnCount` | `(sheetId) => number?`              |          | Same as above for columns.                                                                                                                             |
| `getCellData`        | `(sheetId, row, col) => CellData?`  |          | Surfaces the "Show notes" red corner marker. Without it the toggle still renders but produces no markers.                                              |

The dialog pulls everything else off the `Sheet` it resolves — `title` (display name), `merges`, `frozenRowCount` / `frozenColumnCount`, and per-row / per-column sizes from `rowMetadata` / `columnMetadata`.

## Sidebar settings

The dialog manages its own settings state — defaults match Google Sheets where they line up.

| Setting                   | Options                                                                                                        |
| ------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Print scope**           | Current sheet · Workbook (when `sheets.length > 1`) · Selection (when `selections[0]` is set)                  |
| **Paper size**            | Letter · Tabloid · Legal · Statement · Executive · Folio · A3 · A4 · A5 · B4 · B5                              |
| **Orientation**           | Portrait · Landscape                                                                                           |
| **Scale**                 | Normal (100 %) · Fit to width · Fit to page · Custom (10 – 400 %)                                              |
| **Margins**               | Normal · Narrow · Wide · Custom (per-edge px)                                                                  |
| **Show gridlines**        | toggle                                                                                                         |
| **Show notes**            | toggle — paints a red corner triangle on noted cells (needs `getCellData`)                                     |
| **Page order**            | Over, then down (default) · Down, then over                                                                    |
| **Horizontal alignment**  | Left · Center · Right                                                                                          |
| **Vertical alignment**    | Top · Middle · Bottom                                                                                          |
| **Headers & footers**     | Page numbers · Workbook title · Sheet name · Current date · Current time — auto-placed across the 3-zone strip |
| **Repeat frozen rows**    | toggle, disabled when `Sheet.frozenRowCount` is 0 / unset                                                      |
| **Repeat frozen columns** | toggle, disabled when `Sheet.frozenColumnCount` is 0 / unset                                                   |

## Pagination

The pagination engine (`computePageLayout`) walks the cell range with the active scale, splitting rows and columns into bands whose summed scaled dimensions fit the page content area (paper minus margins minus optional header/footer strips, minus a 1 px reserve for the closing cell border).

* `Over, then down`: outer = row bands, inner = column bands — page order goes left-to-right across the first row band, then advances. (Google Sheets' default.)
* `Down, then over`: outer = column bands, inner = row bands — page order goes top-to-bottom within the first column band, then advances. (Excel's `pageOrder="downThenOver"` default.)

Frozen rows / columns, when their toggle is on, are subtracted from the content area on every page and prepended as a fixed prefix to each band — every page renders with the same frozen header rows / leftmost columns.

## What's NOT in this release

Deferred so we could ship the core flow. None block the integration above.

* **Set custom page breaks** — drag-edit page breaks UI. The underlying `<rowBreaks>` / `<colBreaks>` XLSX metadata still round-trips on import/export.
* **Edit custom fields** — the Google-Sheets 3-zone (`&L` / `&C` / `&R`) header / footer composer. v1 auto-places enabled fields.
* **Show notes — note text** — the toggle ships and marks noted cells with a red corner triangle, but the note text isn't emitted in a trailing endnote section (Google Sheets does this; deferred).

## Files

* `apps/spreadsheet/components/print-preview/settings.ts` — `PrintSettings` model + paper-size / margin constants.
* `apps/spreadsheet/components/print-preview/use-page-layout.ts` — pure pagination math; band slicing, scale resolution.
* `apps/spreadsheet/components/print-preview/cell-styles.ts` — maps `CellFormat` to inline CSS for the printed cells.
* `apps/spreadsheet/components/print-preview/page-block.tsx` — renders one page as a styled HTML table.
* `apps/spreadsheet/components/print-preview/print-preview-dialog.tsx` — the modal, sidebar, and pagination orchestration.
* `apps/spreadsheet/components/print-preview/run-print.ts` — hidden-iframe `window.print()` driver with `@page` rules.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.rowsncolumns.app/configuration/features/print-preview.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
