> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reducto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SpreadsheetViewer

> A React component for viewing Excel spreadsheets with bounding box overlays for document extraction visualization

<Info>
  **Enterprise Component** — The SpreadsheetViewer component is available to Reducto enterprise customers.
  Contact [sales@reducto.ai](mailto:sales@reducto.ai) to get access.
</Info>

## Overview

The `SpreadsheetViewer` component provides a powerful, read-only Excel spreadsheet viewer with support for bounding box overlays. It's designed to visualize Reducto extraction results directly on your spreadsheet documents.

<CardGroup cols={2}>
  <Card title="Excel Support" icon="file-excel">
    View `.xlsx` and `.xls` files with full sheet navigation
  </Card>

  <Card title="Bounding Boxes" icon="vector-square">
    Overlay extraction results with clickable, colored regions
  </Card>

  <Card title="Zero Tailwind" icon="css3">
    CSS Variables for easy theming without Tailwind dependency
  </Card>

  <Card title="TypeScript" icon="code">
    Full type definitions included for excellent DX
  </Card>
</CardGroup>

## Installation

<Steps>
  <Step title="Configure npm registry">
    Create or update your project's `.npmrc` file to authenticate with GitHub Packages:

    ```bash .npmrc theme={null}
    @reductoai-collab:registry=https://npm.pkg.github.com
    //npm.pkg.github.com/:_authToken=${REDUCTO_NPM_TOKEN}
    ```

    <Warning>
      Never commit your token directly to `.npmrc`. Use an environment variable as shown above.
    </Warning>
  </Step>

  <Step title="Set your authentication token">
    Reducto will provide you with an access token. Set it as an environment variable:

    <Tabs>
      <Tab title="macOS / Linux">
        Add to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):

        ```bash theme={null}
        export REDUCTO_NPM_TOKEN="your-token-from-reducto"
        ```

        Then reload your shell:

        ```bash theme={null}
        source ~/.zshrc  # or ~/.bashrc
        ```
      </Tab>

      <Tab title="Windows">
        Set as a system environment variable:

        ```powershell theme={null}
        [System.Environment]::SetEnvironmentVariable('REDUCTO_NPM_TOKEN', 'your-token-from-reducto', 'User')
        ```

        Restart your terminal after setting.
      </Tab>

      <Tab title="CI/CD">
        Add `REDUCTO_NPM_TOKEN` as a secret in your CI environment:

        ```yaml GitHub Actions theme={null}
        env:
          REDUCTO_NPM_TOKEN: ${{ secrets.REDUCTO_NPM_TOKEN }}
        ```

        ```yaml GitLab CI theme={null}
        variables:
          REDUCTO_NPM_TOKEN: $REDUCTO_NPM_TOKEN
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Install the package">
    <Tabs>
      <Tab title="npm">
        ```bash theme={null}
        npm install @reductoai-collab/components
        ```
      </Tab>

      <Tab title="yarn">
        ```bash theme={null}
        yarn add @reductoai-collab/components
        ```
      </Tab>

      <Tab title="pnpm">
        ```bash theme={null}
        pnpm add @reductoai-collab/components
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Import the styles">
    Import the CSS file in your application's entry point:

    ```tsx App.tsx or main.tsx theme={null}
    import '@reductoai-collab/components/styles/spreadsheet-viewer.css';
    ```
  </Step>
</Steps>

## Quick Start

Here's a minimal example to get you started:

```tsx theme={null}
import { SpreadsheetViewer } from '@reductoai-collab/components';
import '@reductoai-collab/components/styles/spreadsheet-viewer.css';

function App() {
  return (
    <div style={{ height: '600px', width: '100%' }}>
      <SpreadsheetViewer
        url="https://example.com/spreadsheet.xlsx"
        onLoad={(metadata) => {
          console.log(`Loaded ${metadata.sheetCount} sheets`);
        }}
        onError={(error) => {
          console.error(`Error: ${error.message}`);
        }}
      />
    </div>
  );
}
```

<Note>
  The `SpreadsheetViewer` requires a container with defined dimensions. Always wrap it in a parent element with explicit `height` and `width`.
</Note>

## Usage Examples

### Basic Viewer

Display a spreadsheet without any overlays:

```tsx theme={null}
import { SpreadsheetViewer } from '@reductoai-collab/components';

function BasicViewer() {
  return (
    <div style={{ height: '600px', width: '100%' }}>
      <SpreadsheetViewer
        url="/documents/financial-report.xlsx"
        onLoad={(metadata) => {
          console.log('Sheet names:', metadata.sheetNames);
        }}
      />
    </div>
  );
}
```

### With Bounding Boxes

Highlight specific regions of the spreadsheet with colored bounding boxes:

```tsx theme={null}
import { SpreadsheetViewer, type BoundingBox } from '@reductoai-collab/components';

function ViewerWithBboxes() {
  const bboxes: BoundingBox[] = [
    {
      id: 'header-row',
      page: 1,        // Sheet number (1-indexed)
      top: 1,         // Starting row
      left: 1,        // Starting column (A = 1)
      width: 5,       // Number of columns
      height: 1,      // Number of rows
      color: 'blue',
      label: 'Header',
    },
    {
      id: 'data-table',
      page: 1,
      top: 2,
      left: 1,
      width: 5,
      height: 10,
      color: 'green',
      label: 'Sales Data',
      metadata: {
        tableId: 'tbl-001',
        confidence: 0.95,
      },
    },
  ];

  return (
    <div style={{ height: '600px', width: '100%' }}>
      <SpreadsheetViewer
        url="/documents/sales-data.xlsx"
        bboxes={bboxes}
        onBboxClick={(bbox) => {
          console.log('Clicked:', bbox.id);
          console.log('Custom data:', bbox.metadata);
        }}
      />
    </div>
  );
}
```

### Integrating with Reducto API Results

Convert Reducto extraction results to bounding boxes:

```tsx theme={null}
import { useState } from 'react';
import { SpreadsheetViewer, type BoundingBox } from '@reductoai-collab/components';

interface ReductoBlock {
  id: string;
  type: 'table' | 'text' | 'key_value';
  bbox: {
    page: number;
    top: number;
    left: number;
    width: number;
    height: number;
  };
  content: string;
}

interface ExtractionResult {
  blocks: ReductoBlock[];
}

function ReductoViewer({
  documentUrl,
  extractionResult,
}: {
  documentUrl: string;
  extractionResult: ExtractionResult;
}) {
  const [selectedBlock, setSelectedBlock] = useState<ReductoBlock | null>(null);

  // Map block types to colors
  const colorMap: Record<string, BoundingBox['color']> = {
    table: 'blue',
    text: 'green',
    key_value: 'purple',
  };

  // Convert Reducto blocks to bounding boxes
  const bboxes: BoundingBox[] = extractionResult.blocks.map((block) => ({
    id: block.id,
    page: block.bbox.page,
    top: block.bbox.top,
    left: block.bbox.left,
    width: block.bbox.width,
    height: block.bbox.height,
    color: colorMap[block.type] ?? 'gray',
    label: block.type,
    metadata: {
      blockId: block.id,
      type: block.type,
      content: block.content,
    },
  }));

  return (
    <div style={{ display: 'flex', height: '600px' }}>
      {/* Viewer */}
      <div style={{ flex: 1 }}>
        <SpreadsheetViewer
          url={documentUrl}
          bboxes={bboxes}
          onBboxClick={(bbox) => {
            const block = extractionResult.blocks.find(
              (b) => b.id === bbox.metadata?.blockId
            );
            setSelectedBlock(block ?? null);
          }}
        />
      </div>

      {/* Details Panel */}
      <div style={{ width: '300px', padding: '16px', borderLeft: '1px solid #e5e7eb' }}>
        <h3>Selected Block</h3>
        {selectedBlock ? (
          <div>
            <p><strong>Type:</strong> {selectedBlock.type}</p>
            <p><strong>Content:</strong></p>
            <pre style={{ whiteSpace: 'pre-wrap', fontSize: '12px' }}>
              {selectedBlock.content}
            </pre>
          </div>
        ) : (
          <p style={{ color: '#6b7280' }}>Click a bounding box to see details</p>
        )}
      </div>
    </div>
  );
}
```

### Custom Styling with CSS Variables

Override the default styles using CSS variables:

```css custom-theme.css theme={null}
:root {
  /* Container */
  --sv-background: #1a1a2e;
  --sv-border-color: #2d2d44;
  --sv-border-radius: 8px;

  /* Tabs */
  --sv-tab-background: #2d2d44;
  --sv-tab-background-active: #3d3d5c;
  --sv-tab-color: #a0a0b0;
  --sv-tab-color-active: #ffffff;

  /* Grid */
  --sv-header-background: #2d2d44;
  --sv-header-color: #ffffff;
  --sv-cell-background: #1a1a2e;
  --sv-cell-color: #e0e0e0;
  --sv-cell-border-color: #2d2d44;

  /* Bounding boxes */
  --sv-bbox-border-width: 2px;
  --sv-bbox-opacity: 0.15;
  --sv-bbox-opacity-hover: 0.25;
}
```

<Accordion title="All available CSS variables">
  ```css theme={null}
  :root {
    /* Container */
    --sv-font-family: system-ui, -apple-system, sans-serif;
    --sv-background: #ffffff;
    --sv-border-color: #e5e7eb;
    --sv-border-radius: 4px;

    /* Tabs */
    --sv-tab-height: 36px;
    --sv-tab-padding: 0 16px;
    --sv-tab-background: #f9fafb;
    --sv-tab-background-hover: #f3f4f6;
    --sv-tab-background-active: #ffffff;
    --sv-tab-color: #6b7280;
    --sv-tab-color-active: #111827;
    --sv-tab-font-size: 13px;
    --sv-tab-font-weight: 500;

    /* Grid */
    --sv-header-background: #f9fafb;
    --sv-header-color: #374151;
    --sv-header-font-size: 12px;
    --sv-header-font-weight: 600;
    --sv-cell-background: #ffffff;
    --sv-cell-color: #111827;
    --sv-cell-font-size: 13px;
    --sv-cell-padding: 4px 8px;
    --sv-cell-border-color: #e5e7eb;
    --sv-cell-min-width: 80px;
    --sv-cell-height: 24px;
    --sv-row-header-width: 50px;

    /* Selection */
    --sv-selection-background: rgba(59, 130, 246, 0.1);
    --sv-selection-border-color: #3b82f6;

    /* Scrollbar */
    --sv-scrollbar-width: 8px;
    --sv-scrollbar-track: #f1f1f1;
    --sv-scrollbar-thumb: #c1c1c1;
    --sv-scrollbar-thumb-hover: #a1a1a1;

    /* Bounding boxes */
    --sv-bbox-border-width: 2px;
    --sv-bbox-opacity: 0.1;
    --sv-bbox-opacity-hover: 0.2;

    /* Loading & Error states */
    --sv-loading-color: #6b7280;
    --sv-error-color: #dc2626;
    --sv-error-background: #fef2f2;
  }
  ```
</Accordion>

## API Reference

### SpreadsheetViewer Props

<ParamField path="url" type="string" required>
  URL to the Excel file (`.xlsx` or `.xls`). Can be a relative path, absolute URL, or blob URL.
</ParamField>

<ParamField path="bboxes" type="BoundingBox[]">
  Array of bounding boxes to overlay on the spreadsheet.
</ParamField>

<ParamField path="onLoad" type="(metadata: WorkbookMetadata) => void">
  Callback fired when the workbook is successfully loaded.
</ParamField>

<ParamField path="onError" type="(error: SpreadsheetError) => void">
  Callback fired when an error occurs loading or parsing the file.
</ParamField>

<ParamField path="onBboxClick" type="(bbox: BoundingBox) => void">
  Callback fired when a bounding box is clicked.
</ParamField>

<ParamField path="className" type="string">
  Additional CSS class name for the container element.
</ParamField>

<ParamField path="style" type="React.CSSProperties">
  Inline styles for the container element.
</ParamField>

### BoundingBox Type

```typescript theme={null}
interface BoundingBox {
  id: string;                    // Unique identifier
  page: number;                  // Sheet number (1-indexed)
  top: number;                   // Starting row (1-indexed)
  left: number;                  // Starting column (1-indexed, A=1)
  width: number;                 // Number of columns
  height: number;                // Number of rows
  color?: BoundingBoxColor;      // Box color (default: 'blue')
  label?: string;                // Optional label text
  metadata?: Record<string, unknown>;  // Custom data
}

type BoundingBoxColor =
  | 'blue' | 'green' | 'red' | 'yellow'
  | 'purple' | 'orange' | 'pink' | 'gray';
```

### WorkbookMetadata Type

```typescript theme={null}
interface WorkbookMetadata {
  sheetCount: number;      // Total number of sheets
  sheetNames: string[];    // Array of sheet names
  activeSheet: number;     // Currently active sheet index
}
```

### SpreadsheetError Type

```typescript theme={null}
interface SpreadsheetError {
  code: 'FETCH_ERROR' | 'PARSE_ERROR' | 'INVALID_FORMAT';
  message: string;
  originalError?: Error;
}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Package not found during installation">
    Ensure your `.npmrc` is configured correctly and the `REDUCTO_NPM_TOKEN` environment variable is set:

    ```bash theme={null}
    echo $REDUCTO_NPM_TOKEN  # Should print your token
    ```

    If using a monorepo, place the `.npmrc` in the root directory.
  </Accordion>

  <Accordion title="CORS errors when loading spreadsheets">
    If loading files from a different domain, ensure the server sends appropriate CORS headers:

    ```
    Access-Control-Allow-Origin: *
    ```

    Alternatively, proxy the request through your own backend or use blob URLs.
  </Accordion>

  <Accordion title="Bounding boxes not appearing">
    Verify that:

    1. The `page` property matches the sheet number (1-indexed)
    2. The `top` and `left` values are within the sheet bounds
    3. You've imported the CSS file
  </Accordion>

  <Accordion title="Styles not applying">
    Make sure you import the CSS file in your app's entry point:

    ```tsx theme={null}
    import '@reductoai-collab/components/styles/spreadsheet-viewer.css';
    ```

    If using CSS modules or scoped styles, ensure the import is global.
  </Accordion>
</AccordionGroup>

## Support

<Card title="Get Access" icon="key" href="mailto:sales@reducto.ai">
  Contact **[sales@reducto.ai](mailto:sales@reducto.ai)** to get access to `@reductoai-collab/components` and receive your authentication token.
</Card>

For technical support, reach out to your Reducto account representative or email [support@reducto.ai](mailto:support@reducto.ai).
